diff options
| author | Benjamin J. Culkin <bjculkin@mix.wvu.edu> | 2019-01-05 11:29:00 -0400 |
|---|---|---|
| committer | Benjamin J. Culkin <bjculkin@mix.wvu.edu> | 2019-01-05 11:29:00 -0400 |
| commit | 11bcfd3632668d57d4df07760121fc6ebda044f3 (patch) | |
| tree | 824f848cad1cf973007d0d874d6c1ffe8d55681c | |
| parent | 98c5fd3f7c3cfc8dea8908d04b8d694a53fa1aeb (diff) | |
Split AffixLister
This splits AffixLister into a separate file for each class that it once
contained.
| -rw-r--r-- | src/main/java/AffixLister.java | 1018 | ||||
| -rw-r--r-- | src/main/java/tlIItools/Affix.java | 224 | ||||
| -rw-r--r-- | src/main/java/tlIItools/AffixLister.java | 494 | ||||
| -rw-r--r-- | src/main/java/tlIItools/Effect.java | 340 | ||||
| -rw-r--r-- | src/main/java/tlIItools/ReplPair.java (renamed from src/main/java/ReplPair.java) | 2 |
5 files changed, 1060 insertions, 1018 deletions
diff --git a/src/main/java/AffixLister.java b/src/main/java/AffixLister.java deleted file mode 100644 index efd5bd84..00000000 --- a/src/main/java/AffixLister.java +++ /dev/null @@ -1,1018 +0,0 @@ -import java.io.IOException; -import java.io.FileReader; -import java.io.PrintStream; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Scanner; -import java.util.Set; - -/** - * Lists randomly generated affixes for Torchlight II gear. - * - * @author Ben Culkin - */ -public class AffixLister { - private static int effectCount = 0; - - private static boolean doTiming = false; - private static boolean addFileName = false; - - public static PrintStream normOut = System.out; - public static PrintStream errOut = System.err; - - private static Map<String, String> readEffectDetails(Scanner scn) { - Map<String, String> detals = new HashMap<>(); - - while (scn.hasNextLine()) { - String name = scn.nextLine().trim(); - if (name.equals("")) continue; - if (name.startsWith("#")) continue; - if (name.startsWith("alias ")) { - String[] pts = name.split("\\t"); - - detals.put(pts[1], detals.get(pts[2])); - } - - String body; - do { - body = scn.nextLine().trim(); - } while (body.startsWith("#")); - - detals.put(name, body); - } - - return detals; - } - - /** - * Represents an effect attached to an affix. - */ - public static class Effect { - private static Map<String, String> detals; - private static Map<String, String> timeDetals; - - private static List<ReplPair> replList; - - static { - try (FileReader detalReader = new FileReader("data/affix-detals.txt")) { - detals = readEffectDetails(new Scanner(detalReader)); - } catch (IOException ioex) { - errOut.println("Error loading affix detail text"); - } - - try (FileReader timedDetalReader = new FileReader("data/timed-affix-detals.txt")) { - timeDetals = readEffectDetails(new Scanner(timedDetalReader)); - } catch (IOException ioex) { - errOut.println("Error loading timed affix detail text"); - } - - try (FileReader replListReader = new FileReader("data/replace-list.txt")) { - replList = ReplPair.readList(new Scanner(replListReader)); - } catch (IOException ioex) { - errOut.println("Error loading replacement lists"); - } - } - - private static void sanityCheckFormats() { - for (Entry<String, String> detal : detals.entrySet()) { - String fmt = detal.getValue(); - - errOut.printf("\tTRACE: Applying replacements for %s\n", detal.getKey()); - for (ReplPair repl : replList) { - String tmp = fmt; - fmt = fmt.replaceAll(repl.find, repl.replace); - if (!fmt.equals(tmp)) { - errOut.printf("\t\tTRACE: Replaced %s with %s: \n\t\t%s\n\t\t%s\n", repl.find, repl.replace, tmp, fmt); - } - } - - if (fmt.contains("<") || fmt.contains(">")) { - errOut.printf("WARN: Details for effect %s are malformated (contains < or >):\n\t%s\n", detal.getKey(), fmt); - } - } - - for (Entry<String, String> detal : timeDetals.entrySet()) { - String fmt = detal.getValue(); - - for (ReplPair repl : replList) { - fmt = fmt.replaceAll(repl.find, repl.replace); - } - - if (fmt.contains("<") || fmt.contains(">")) { - errOut.printf("WARN: Details for effect %s are malformated (contains < or >):\n\t%s\n", detal.getKey(), fmt); - } - } - } - /** - * The file name this effect came from. - */ - public String fName; - - /** - * The name of the effect. - */ - public String name; - - /** - * The specific effect that happens. - */ - public String type; - - /** - * Damage type for the effect, if applicable. - */ - public String damageType = "physical"; - - /** - * Duration of the effect. - */ - public double duration; - - /** - * Whether or not we have a duration or not. - */ - public boolean hasDuration; - - /** - * Minimum value for the effect. - */ - public double minValue; - /** - * Maximum value for the effect. - */ - public double maxValue; - - /** - * The name of the stat that applies to this affect. - */ - public String statName; - /** - * The percent of the stat value to apply. - */ - public double statPercent; - /** - * Whether or not this stat is a bonus value. - */ - public boolean isStatBonus; - - /** - * Whether or not this uses the owners level to modify - * any applicable graph. - */ - public boolean ownerLevel; - - /** - * The graph to use instead of the default graph. - */ - public String graphOverride; - - /** - * Whether or not a graph is used for this effect. - */ - public boolean useGraph = true; - - /** - * Whether this effect can stack with itself. - */ - public boolean exclusive; - - /** - * The amount the targets armor is reduced by for this - * effect. - */ - public double soakScale = 1.0; - - /** - * Level of the effect. - */ - public int level = -1; - - /** - * Whether or not this effect is a 'transfer' - * effect (Applied to the enemy on a hit). - */ - public boolean isTransfer; - - /** - * The amount to resist/do knockback by. - */ - public double resist; - - /** - * Minimum value per monster. - */ - public double minPer; - - /** - * Maximum value per monster. - */ - public double maxPer; - - /** - * Range for effect. - */ - public double range; - - /** - * Maximum count of monsters. - */ - public double maxCount; - - /** - * The rate at which the effect fires. - */ - public double pulse; - /** - * Create a new blank effect. - */ - public Effect() { - - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - if (isTransfer) { - sb.append("Inflict on Hit: "); - } - - Map<String, String> detMap = hasDuration ? timeDetals : detals; - - if (detMap.containsKey(type) || (hasDuration && !timeDetals.containsKey(type) && detals.containsKey(type))) { - String fmt; - if (hasDuration && !timeDetals.containsKey(type) && detals.containsKey(type)) { - errOut.printf("Improvised details for timed %s\n", type); - fmt = detals.get(type) + "for <DUR> seconds"; - } else { - fmt = detMap.get(type); - } - - // Expand aliases first. - - for (ReplPair repl : replList) { - fmt = fmt.replaceAll(repl.find, repl.replace); - } - - if (minValue <= 0 && maxValue <= 0) { - fmt = fmt.replaceAll("<C\\|([^|>]+)\\|([^|>]+)>", "$1"); - } - - if (minValue >= 0 && maxValue >= 0) { - fmt = fmt.replaceAll("<C\\|([^|>]+)\\|([^|>]+)>", "$2"); - } - - if (minPer <= 0 && maxPer <= 0) { - fmt = fmt.replaceAll("<MC\\|(\\w+)\\|(\\w+)>", "$1"); - } - - if (minPer >= 0 && maxPer >= 0) { - fmt = fmt.replaceAll("<MC\\|([^|>]+)\\|([^|>]+)>", "$2"); - } - - if (fmt.contains("<") || fmt.contains(">")) { - errOut.printf("WARN: Details for effect %s are malformated (contains < or >):\n\t%s\n", type, fmt); - } - - sb.append(String.format(fmt, Math.abs(minValue), Math.abs(maxValue), duration, damageType.toLowerCase(), level, resist, name, Math.abs(minPer), Math.abs(maxPer), range, maxCount, pulse)); - } else { - sb.append("No effect details for effect "); - sb.append(type); - sb.append(String.format(" with parameters (min %.2f, max %.2f, dur %.2f, type %s, level %d)", minValue, maxValue, duration, damageType.toLowerCase(), level, name)); - - if (addFileName) { - sb.append(" from file "); - sb.append(fName); - } - - if (hasDuration) errOut.print("TIMED: "); - errOut.println(sb.toString()); - } - - if (name != null) { - sb.append(" (named "); - sb.append(name); - sb.append(")"); - } - - if (exclusive) sb.append(" (Exclusive)"); - - if (graphOverride != null) { - sb.append(" (Uses "); - sb.append(graphOverride); - sb.append(" graph)"); - } - - if (ownerLevel) { - sb.append(" (Uses owner level for graph)"); - } - - if (soakScale != 1.0) { - String fmt = " (%.2f%% reduced effectiveness of armor)"; - - sb.append(String.format(fmt, (1 - soakScale) * 100)); - } - - if (level != -1) { - sb.append(" (Level "); - sb.append(level); - sb.append(")"); - } - - if (statName != null) { - sb.append(String.format(" (%.2f of stat %s", statPercent, statName)); - if (isStatBonus) sb.append(" as bonus)"); - else sb.append(")"); - } - - if (!useGraph) sb.append(" (Ignoring graph)"); - - return sb.toString(); - } - } - - /** - * Represents a Torchlight II affix. - */ - public static class Affix { - /** - * Internal name of the affix. - */ - public String intName; - - /** - * The prefix/suffix attached to the affix. - * - * In general, only one of these is set for a given affix. - * - * NOTE: Some affixes have a mis-set suffix/prefix ordering, and - * may need to be changed. - */ - public String affixSuffix; - public String affixPrefix; - - /** - * The min/max levels the affix can spawn at. - */ - public int minLevel; - public int maxLevel; - - /** - * The spawn weight for the affix. - */ - public int weight; - - /** - * The number of affix slots taken up. - */ - public int slots; - - /** - * Whether this is a socketable affix. - */ - public boolean isSocketable; - /** - * Whether this is an enchantment, instead of a standard affix. - */ - public boolean isEnchantment; - /** - * Whether this is a mob/player affix. - */ - public boolean isPerson; - - /** - * The types of equipment this can spawn on. - */ - public List<String> equipTypes; - /** - * The types of equipment this can't spawn on. - */ - public List<String> nonequipTypes; - - /** - * Places to get this enchantment from; - */ - public List<String> enchantSources; - - /** - * The socketable types this spawns on. - */ - public List<String> socketableTypes; - - /** - * The effects attached to this affix. - */ - public List<Effect> effects; - - /* - * Are invalid equip types being added? - */ - private boolean inNonEquip; - - /** - * Create a new blank affix. - */ - public Affix() { - equipTypes = new ArrayList<>(); - nonequipTypes = new ArrayList<>(); - enchantSources = new ArrayList<>(); - socketableTypes = new ArrayList<>(); - effects = new ArrayList<>(); - } - - /** - * Sets the set of equip types that is added to. - * - * @param nonequip - * True if the equip types being added are prohibited, or - * false if they are allowed. - */ - public void setEquipType(boolean nonequip) { - inNonEquip = nonequip; - } - - /** - * Add an equip type to the right set of equip types. - * - * @param type - * The equip type to add. - */ - public void addEquipType(String type) { - if (inNonEquip) { - nonequipTypes.add(type); - } else { - if (type.equals("SOCKETABLE") || type.contains(" EMBER")) { - isSocketable = true; - socketableTypes.add(type); - } else if (type.startsWith("ENCHANTER")) { - isEnchantment = true; - enchantSources.add(type.substring(10)); - } else if (type.equals("MONSTER") || type.equals("PLAYER")) { - isPerson = true; - } else { - equipTypes.add(type); - } - } - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - if (isSocketable) sb.append("Socketable "); - else if (isPerson || (intName != null && intName.startsWith("HERO_"))) sb.append("Personal "); - else if (intName != null & intName.startsWith("MAP_")) sb.append("Area "); - - if (isEnchantment) sb.append("Enchantment: "); - else sb.append("Affix: "); - - sb.append(intName); - sb.append("\n"); - - if (affixSuffix != null) { - sb.append("\tSuffix: "); - sb.append(affixSuffix); - sb.append("\n"); - } - - if (affixPrefix != null) { - sb.append("\tPrefix: "); - sb.append(affixPrefix); - sb.append("\n"); - - } - - sb.append("\t"); - if (minLevel <= 1 && maxLevel == 999) { - sb.append("No Level Range"); - } else if (minLevel != 1 && maxLevel != 999) { - sb.append("Level Range: "); - sb.append(minLevel); - sb.append("-"); - sb.append(maxLevel); - } else if (minLevel <= 1) { - sb.append("Max Level: "); - sb.append(maxLevel); - } else if (maxLevel == 999) { - sb.append("Minimum Level: "); - sb.append(minLevel); - } - - sb.append("\n"); - - sb.append("\tSpawn Weight: "); - sb.append(weight); - sb.append("\n"); - - if (slots == 0) { - sb.append("\tOccupies no slots\n"); - } else { - sb.append("\tSlots: "); - sb.append(slots); - sb.append("\n"); - } - - if (equipTypes.size() != 0) { - if (isSocketable) sb.append("\tSocketable Into: "); - else if (isEnchantment) sb.append("\tEnchants Onto: "); - else sb.append("\tSpawns On: "); - sb.append(equipTypes); - sb.append("\n"); - } - - // Socketables & enchantments use this as a duplicate. - if (!isSocketable && !isEnchantment && nonequipTypes.size() != 0) { - sb.append("\tCan't Spawn On: "); - sb.append(nonequipTypes); - sb.append("\n"); - } - - if (enchantSources.size() != 0) { - sb.append("\tEnchantment Sources: "); - sb.append(enchantSources); - sb.append("\n"); - } - - if (socketableTypes.size() != 0) { - sb.append("\tSocketable Types: "); - sb.append(socketableTypes); - sb.append("\n"); - } - - if (effects.size() != 0) { - sb.append("\tEffects: "); - for (Effect eft : effects) { - sb.append("\n\t\t"); - sb.append(eft.toString()); - } - sb.append("\n"); - } - - return sb.toString(); - } - } - - /** - * Indicates how to treat affixes with regards to their names. - */ - public static enum NameMode { - /** - * Show all affixes. - */ - ALL, - /** - * Show only unnamed affixes. - */ - UNNAMED, - /** - * Show only named affixes. - */ - NAMED; - } - - private static int readNamesFromFile(Map<String, List<String>> fNames, String from, String defGroup, boolean guessGroups) { - int numFiles = 0; - try (FileReader fr = new FileReader(from)) { - Scanner scn = new Scanner(fr); - - String curGroup = defGroup; - List<String> curList = fNames.get(curGroup); - - while (scn.hasNextLine()) { - String ln = scn.nextLine(); - - if (ln.startsWith("#")) { - curGroup = ln.substring(1); - - if (!fNames.containsKey(curGroup)) { - curList = new ArrayList<>(); - fNames.put(curGroup, curList); - } else { - curList = fNames.get(curGroup); - } - } else if (guessGroups && ln.contains("/mods/")) { - curGroup = ln.replaceAll(".*/mods/([^/]+)/*", "$1"); - - if (!fNames.containsKey(curGroup)) { - curList = new ArrayList<>(); - fNames.put(curGroup, curList); - } else { - curList = fNames.get(curGroup); - } - } else { - numFiles += 1; - curList.add(ln); - } - } - } catch (IOException ioex) { - errOut.printf("Error reading names from file %s\n", from); - ioex.printStackTrace(errOut); - errOut.println(); - } - - return numFiles; - } - /** - * Main method. - * - * @param args The names of the files to read affix data from. - */ - public static void main(String[] args) { - boolean doingArgs = true; - - boolean omitZeros = false; - boolean listZeros = false; - boolean guessGroups = false; - - NameMode nameMode = NameMode.ALL; - - long startTime = System.nanoTime(); - - int fCount = 0; - - int namedCount = 0; - int unnamedCount = 0; - int zeroCount = 0; - int groupCount = 0; - - Map<String, List<String>> groupContents = new HashMap<>(); - - List<String> nonGroupContents = new ArrayList<>(); - - int argCount = 0; - - Map<String, List<String>> fGroups = new HashMap<>(); - List<String> fNames = new ArrayList<>(); - - String curGroup = "default"; - fGroups.put(curGroup, fNames); - for(int i = 0; i < args.length; i++) { - String fName = args[i]; - - if (doingArgs && fName.startsWith("-")) { - boolean isArg = true; - switch (fName) { - case "--": - doingArgs = false; - break; - case "--omit-zero": - case "-z": - omitZeros = true; - break; - case "--no-omit-zero": - case "-Z": - omitZeros = false; - break; - case "--list-zero": - case "-l": - listZeros = true; - break; - case "--no-list-zero": - case "-L": - listZeros = false; - break; - case "--timing": - case "-t": - doTiming = true; - break; - case "--no-timing": - case "-T": - doTiming = false; - break; - case "--file-names": - case "-f": - addFileName = true; - break; - case "--no-file-names": - case "-F": - addFileName = true; - break; - case "--name-mode": - case "-n": - if (i + 1 >= args.length) { - errOut.printf("ERROR: name mode argument requires the mode to use be specified (all, unnamed or named)\n"); - break; - } - - nameMode = NameMode.valueOf(args[++i].toUpperCase()); - break; - case "--file-group": - case "-g": - if (i + 1 >= args.length) { - errOut.printf("ERROR: file group argument requires the group name to use be specified\n"); - break; - } - - curGroup = args[++i]; - if (fGroups.containsKey(curGroup)) { - fNames = fGroups.get(curGroup); - } else { - fNames = new ArrayList<>(); - fGroups.put(curGroup, fNames); - } - break; - case "--guess-groups": - guessGroups = true; - break; - case "--no-guess-groups": - guessGroups = false; - break; - case "--read-names-from-file": - case "-r": - if (i + 1 >= args.length) { - errOut.printf("ERROR: read name file argument requires the file to use be specified\n"); - break; - } - - fCount += readNamesFromFile(fGroups, args[++i], curGroup, guessGroups); - break; - case "--output": - case "-o": - if (i + 1 >= args.length) { - errOut.printf("ERROR: output file argument requires the file to use be specified\n"); - break; - } - try { - normOut = new PrintStream(args[++i]); - } catch (IOException ioex) { - errOut.printf("Could not open output file %s\n", args[i]); - - ioex.printStackTrace(errOut); - } - break; - case "--output-errors": - case "-e": - if (i + 1 >= args.length) { - errOut.printf("ERROR: error output file argument requires the file to use be specified\n"); - break; - } - try { - errOut = new PrintStream(args[++i]); - } catch (IOException ioex) { - errOut.printf("Could not open error output file %s\n", args[i]); - - ioex.printStackTrace(errOut); - } - break; - default: - isArg = false; - } - - if (isArg) { - argCount += 1; - continue; - } - - fCount += 1; - fNames.add(fName); - } else { - fCount += 1; - fNames.add(fName); - } - } - - for (Entry<String, List<String>> fGroup : fGroups.entrySet()) { - if (fGroup.getValue().size() == 0) continue; - normOut.printf("\nFile Group '%s' starting\n", fGroup.getKey()); - for (String fName : fGroup.getValue()) { - try (FileReader fr = new FileReader(fName)) { - Scanner sc = new Scanner(fr); - - Affix afx = processFile(sc, fName); - - if (afx.intName != null && afx.weight != 0) { - String groupRx = "(.*_?)\\d+\\Z"; - boolean hasGroup = afx.intName.matches(groupRx); - String groupName = afx.intName.replaceAll(groupRx, "$1"); - - if (!groupContents.containsKey(groupName)) { - groupCount += 1; - // errOut.printf("\tTRACE: Counted distinct group %s from %s\n", groupName, afx.intName); - - if (hasGroup) { - // errOut.printf("\tTRACE: Counted actual group %s from %s\n", groupName, afx.intName); - - groupContents.put(groupName, new ArrayList<>()); - } else { - nonGroupContents.add(afx.intName); - } - } else if (hasGroup) { - groupContents.get(groupName).add(afx.intName); - } else { - nonGroupContents.add(afx.intName); - } - } - - if (afx.weight == 0) zeroCount += 1; - - if (afx.weight == 0 && !listZeros) { - if (!omitZeros) - normOut.printf("\nAffix %s has zero spawn weight\n", afx.intName); - } else { - boolean isNamed = (afx.affixSuffix != null) || (afx.affixPrefix != null); - - if (isNamed) namedCount += 1; - else unnamedCount += 1; - - if (nameMode == NameMode.UNNAMED && isNamed) continue; - if (nameMode == NameMode.NAMED && !isNamed) continue; - - normOut.printf("\n%s\n", afx.toString()); - } - } catch (Exception ex) { - errOut.printf("Something bad happened for file %s:%s\n", fName, ex.getMessage()); - - ex.printStackTrace(errOut); - - errOut.println(); - } - } - normOut.printf("\nFile Group '%s' ending\n", fGroup.getKey()); - } - - errOut.println("\nGroup Contents: "); - for (Entry<String, List<String>> ent: groupContents.entrySet()) { - errOut.printf("\t%s: %s\n", ent.getKey(), ent.getValue()); - } - errOut.println(); - errOut.println(); - - long endTime = System.nanoTime(); - errOut.printf("\nProcessed %,d affixes (%,d named, %,d unnamed, %,d zero-weight) (%,d effects) (%,d distinct groups, %,d actual groups, %,d nongrouped affixes) out of %,d files (%,d groups) in %,d nanoseconds (%.2f seconds)\n", fCount, namedCount, unnamedCount, zeroCount, effectCount, groupCount, groupContents.size(), nonGroupContents.size(), fCount, fGroups.size(), endTime - startTime, ((double)(endTime - startTime) / 1000000000)); - errOut.printf("\tOptions: Name Mode: %s, Special-case zero weight: %s, Noting zero-weight in special case: %s\n", nameMode, !listZeros, !omitZeros); - } - - // Process an affix file - private static Affix processFile(Scanner scn, String fName) { - Affix afx = new Affix(); - - long startTime = System.nanoTime(); - - while (scn.hasNextLine()) { - String ln = scn.nextLine(); - ln = ln.replaceAll("\\p{Cntrl}", ""); - - if (ln.contains("[NOT_UNITTYPES]")) { - afx.setEquipType(true); - continue; - } else if (ln.contains("[UNITTYPES]")) { - afx.setEquipType(false); - continue; - } - - String[] splits = ln.split(":"); - if (ln.contains("<TRANSLATE>")) { - splits[0] = splits[0].substring(11); - - switch (splits[0]) { - case "SUFFIX": - afx.affixSuffix = splits[1]; - break; - case "PREFIX": - afx.affixPrefix = splits[1]; - break; - default: - errOut.printf("Misformed affix translation: (%s) (%s) (%s)\n", splits[0], splits[1], fName); - } - } else if (ln.contains("MIN_SPAWN_RANGE")) { - afx.minLevel = Integer.parseInt(splits[1]); - } else if (ln.contains("MAX_SPAWN_RANGE")) { - afx.maxLevel = Integer.parseInt(splits[1]); - } else if (ln.contains("WEIGHT:")) { - afx.weight = Integer.parseInt(splits[1]); - } else if (ln.contains("SLOTS_OCCUPY")) { - afx.weight = Integer.parseInt(splits[1]); - } else if (ln.contains("UNITTYPE") && !ln.contains("/")) { - if (splits.length == 1) - errOut.printf("Malformed equip type: (%s) (%s)\n", splits[0], fName); - afx.addEquipType(splits[1]); - } else if (splits[0].equals("<STRING>NAME")) { - if (splits.length == 1) - errOut.printf("Malformed name: (%s) (%s)\n", splits[0], fName); - afx.intName = splits[1]; - } else if (ln.contains("[EFFECT]")) { - afx.effects.add(parseEffect(afx, scn, fName)); - } - } - - long endTime = System.nanoTime(); - if (doTiming) errOut.printf("\tProcessed affix %s from %s in %d nanoseconds (%.2f seconds)\n\n", afx.intName, fName, endTime - startTime, ((double)(endTime - startTime) / 1000000000)); - - return afx; - } - - private static Effect parseEffect(Affix afx, Scanner scn, String fName) { - Effect efct = new Effect(); - - long startTime = System.nanoTime(); - - efct.fName = fName; - - while (scn.hasNextLine()) { - String ln = scn.nextLine(); - ln = ln.replaceAll("\\p{Cntrl}", ""); - - if (ln.contains("[/EFFECT]")) break; - - String[] splits = ln.split(":"); - - // Empty field - if (splits.length == 1) continue; - - if (ln.contains("NAME")) { - efct.name = splits[1]; - } else if (ln.contains("DAMAGE_TYPE")) { - efct.damageType = splits[1]; - } else if (ln.contains("TYPE")) { - efct.type = splits[1]; - } else if (ln.contains("ACTIVATION")) { - switch (splits[1]) { - case "DYNAMIC": - case "PASSIVE": - // Passive is the default, and - // dynamic doesn't have much - // actual difference. - break; - case "TRANSFER": - efct.isTransfer = true; - break; - default: - errOut.printf("Malformed activation type: (%s) (%s) (%s)\n", splits[1], efct.name, afx.intName); - } - } else if (ln.contains("DURATION")) { - if (splits[1].equals("ALWAYS")) { - efct.hasDuration = false; - - efct.duration = Double.POSITIVE_INFINITY; - } else if (splits[1].equals("INSTANT")) { - efct.hasDuration = false; - - efct.duration = Double.NaN; - } else if (splits[1].equals("PERCENT")) { - efct.hasDuration = false; - - efct.duration = Double.NaN; - - errOut.printf("WARN: Punting on DURATION:PERCENT for %s\n", fName); - } else if (splits[1].equals("0")) { - efct.hasDuration = false; - efct.duration = 0.0; - } else { - efct.hasDuration = true; - - efct.duration = Double.parseDouble(splits[1]); - } - } else if (ln.contains("<FLOAT>MIN:")) { - efct.minValue = Double.parseDouble(splits[1]); - } else if (ln.contains("<FLOAT>MAX:")) { - efct.maxValue = Double.parseDouble(splits[1]); - } else if (ln.contains("USEOWNERLEVEL")) { - // We don't care about this, for now - } else if (ln.contains("LEVEL:")) { - efct.level = Integer.parseInt(splits[1]); - } else if (ln.contains("EXCLUSIVE")) { - efct.exclusive = Boolean.parseBoolean(splits[1]); - } else if (ln.contains("GRAPHOVERRIDE")) { - efct.graphOverride = splits[1]; - } else if (ln.contains("USEOWNERLEVEL")) { - efct.ownerLevel = Boolean.parseBoolean(splits[1]); - } else if (ln.contains("NOGRAPH")) { - efct.useGraph = Boolean.parseBoolean(splits[1]); - } else if (ln.contains("STATNAME")) { - efct.statName = splits[1]; - } else if (ln.contains("STATPERCENT")) { - efct.statPercent = Double.parseDouble(splits[1]); - } else if (ln.contains("STATMODIFIERISBONUS")) { - efct.isStatBonus = Boolean.parseBoolean(splits[1]); - } else if (ln.contains("RESISTANCE:")) { - efct.resist = Double.parseDouble(splits[1]); - } else if (ln.contains("FORCE:")) { - efct.resist = Double.parseDouble(splits[1]); - } else if (ln.contains("MIN_PER")) { - efct.minPer = Double.parseDouble(splits[1]); - } else if (ln.contains("MAX_PER")) { - efct.maxPer = Double.parseDouble(splits[1]); - } else if (ln.contains("RANGE:") || ln.contains("RADIUS")) { - efct.range = Double.parseDouble(splits[1]); - } else if (ln.contains("MAX_COUNT:") || ln.contains("MAX_TARGETS")) { - efct.maxCount = Double.parseDouble(splits[1]); - } else if (ln.contains("PULSE_RATE")) { - efct.pulse = Double.parseDouble(splits[1]); - } else if (ln.contains("CHANCE:")) { - // NOTE: Should really use its own field - efct.resist = Double.parseDouble(splits[1]); - } - } - - long endTime = System.nanoTime(); - if (doTiming) errOut.printf("\t\tProcessed effect %s from %s in %d nanoseconds (%.2f seconds)\n", efct.name, fName, endTime - startTime, ((double)((endTime - startTime) / 1000000000))); - - effectCount += 1; - - return efct; - } -} diff --git a/src/main/java/tlIItools/Affix.java b/src/main/java/tlIItools/Affix.java new file mode 100644 index 00000000..424b2dad --- /dev/null +++ b/src/main/java/tlIItools/Affix.java @@ -0,0 +1,224 @@ +package tlIItools; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a Torchlight II affix. + */ +public class Affix { + /** + * Internal name of the affix. + */ + public String intName; + + /** + * The prefix/suffix attached to the affix. + * + * In general, only one of these is set for a given affix. + * + * NOTE: Some affixes have a mis-set suffix/prefix ordering, and + * may need to be changed. + */ + public String affixSuffix; + public String affixPrefix; + + /** + * The min/max levels the affix can spawn at. + */ + public int minLevel; + public int maxLevel; + + /** + * The spawn weight for the affix. + */ + public int weight; + + /** + * The number of affix slots taken up. + */ + public int slots; + + /** + * Whether this is a socketable affix. + */ + public boolean isSocketable; + /** + * Whether this is an enchantment, instead of a standard affix. + */ + public boolean isEnchantment; + /** + * Whether this is a mob/player affix. + */ + public boolean isPerson; + + /** + * The types of equipment this can spawn on. + */ + public List<String> equipTypes; + /** + * The types of equipment this can't spawn on. + */ + public List<String> nonequipTypes; + + /** + * Places to get this enchantment from; + */ + public List<String> enchantSources; + + /** + * The socketable types this spawns on. + */ + public List<String> socketableTypes; + + /** + * The effects attached to this affix. + */ + public List<Effect> effects; + + /* + * Are invalid equip types being added? + */ + private boolean inNonEquip; + + /** + * Create a new blank affix. + */ + public Affix() { + equipTypes = new ArrayList<>(); + nonequipTypes = new ArrayList<>(); + enchantSources = new ArrayList<>(); + socketableTypes = new ArrayList<>(); + effects = new ArrayList<>(); + } + + /** + * Sets the set of equip types that is added to. + * + * @param nonequip + * True if the equip types being added are prohibited, or + * false if they are allowed. + */ + public void setEquipType(boolean nonequip) { + inNonEquip = nonequip; + } + + /** + * Add an equip type to the right set of equip types. + * + * @param type + * The equip type to add. + */ + public void addEquipType(String type) { + if (inNonEquip) { + nonequipTypes.add(type); + } else { + if (type.equals("SOCKETABLE") || type.contains(" EMBER")) { + isSocketable = true; + socketableTypes.add(type); + } else if (type.startsWith("ENCHANTER")) { + isEnchantment = true; + enchantSources.add(type.substring(10)); + } else if (type.equals("MONSTER") || type.equals("PLAYER")) { + isPerson = true; + } else { + equipTypes.add(type); + } + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (isSocketable) sb.append("Socketable "); + else if (isPerson || (intName != null && intName.startsWith("HERO_"))) sb.append("Personal "); + else if (intName != null & intName.startsWith("MAP_")) sb.append("Area "); + + if (isEnchantment) sb.append("Enchantment: "); + else sb.append("Affix: "); + + sb.append(intName); + sb.append("\n"); + + if (affixSuffix != null) { + sb.append("\tSuffix: "); + sb.append(affixSuffix); + sb.append("\n"); + } + + if (affixPrefix != null) { + sb.append("\tPrefix: "); + sb.append(affixPrefix); + sb.append("\n"); + + } + + sb.append("\t"); + if (minLevel <= 1 && maxLevel == 999) { + sb.append("No Level Range"); + } else if (minLevel != 1 && maxLevel != 999) { + sb.append("Level Range: "); + sb.append(minLevel); + sb.append("-"); + sb.append(maxLevel); + } else if (minLevel <= 1) { + sb.append("Max Level: "); + sb.append(maxLevel); + } else if (maxLevel == 999) { + sb.append("Minimum Level: "); + sb.append(minLevel); + } + + sb.append("\n"); + + sb.append("\tSpawn Weight: "); + sb.append(weight); + sb.append("\n"); + + if (slots == 0) { + sb.append("\tOccupies no slots\n"); + } else { + sb.append("\tSlots: "); + sb.append(slots); + sb.append("\n"); + } + + if (equipTypes.size() != 0) { + if (isSocketable) sb.append("\tSocketable Into: "); + else if (isEnchantment) sb.append("\tEnchants Onto: "); + else sb.append("\tSpawns On: "); + sb.append(equipTypes); + sb.append("\n"); + } + + // Socketables & enchantments use this as a duplicate. + if (!isSocketable && !isEnchantment && nonequipTypes.size() != 0) { + sb.append("\tCan't Spawn On: "); + sb.append(nonequipTypes); + sb.append("\n"); + } + + if (enchantSources.size() != 0) { + sb.append("\tEnchantment Sources: "); + sb.append(enchantSources); + sb.append("\n"); + } + + if (socketableTypes.size() != 0) { + sb.append("\tSocketable Types: "); + sb.append(socketableTypes); + sb.append("\n"); + } + + if (effects.size() != 0) { + sb.append("\tEffects: "); + for (Effect eft : effects) { + sb.append("\n\t\t"); + sb.append(eft.toString()); + } + sb.append("\n"); + } + + return sb.toString(); + } +} diff --git a/src/main/java/tlIItools/AffixLister.java b/src/main/java/tlIItools/AffixLister.java new file mode 100644 index 00000000..27ea0c70 --- /dev/null +++ b/src/main/java/tlIItools/AffixLister.java @@ -0,0 +1,494 @@ +package tlIItools; + +import java.io.IOException; +import java.io.FileReader; +import java.io.PrintStream; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Scanner; +import java.util.Set; + +/** + * Lists randomly generated affixes for Torchlight II gear. + * + * @author Ben Culkin + */ +public class AffixLister { + private static int effectCount = 0; + + private static boolean doTiming = false; + + /** + * Should the file name be attached to things? + */ + public static boolean addFileName = false; + + public static PrintStream normOut = System.out; + public static PrintStream errOut = System.err; + + /** + * Indicates how to treat affixes with regards to their names. + */ + public static enum NameMode { + /** + * Show all affixes. + */ + ALL, + /** + * Show only unnamed affixes. + */ + UNNAMED, + /** + * Show only named affixes. + */ + NAMED; + } + + private static int readNamesFromFile(Map<String, List<String>> fNames, String from, String defGroup, boolean guessGroups) { + int numFiles = 0; + try (FileReader fr = new FileReader(from)) { + Scanner scn = new Scanner(fr); + + String curGroup = defGroup; + List<String> curList = fNames.get(curGroup); + + while (scn.hasNextLine()) { + String ln = scn.nextLine(); + + if (ln.startsWith("#")) { + curGroup = ln.substring(1); + + if (!fNames.containsKey(curGroup)) { + curList = new ArrayList<>(); + fNames.put(curGroup, curList); + } else { + curList = fNames.get(curGroup); + } + } else if (guessGroups && ln.contains("/mods/")) { + curGroup = ln.replaceAll(".*/mods/([^/]+)/*", "$1"); + + if (!fNames.containsKey(curGroup)) { + curList = new ArrayList<>(); + fNames.put(curGroup, curList); + } else { + curList = fNames.get(curGroup); + } + } else { + numFiles += 1; + curList.add(ln); + } + } + } catch (IOException ioex) { + errOut.printf("Error reading names from file %s\n", from); + ioex.printStackTrace(errOut); + errOut.println(); + } + + return numFiles; + } + + /** + * Main method. + * + * @param args The names of the files to read affix data from. + */ + public static void main(String[] args) { + boolean doingArgs = true; + + boolean omitZeros = false; + boolean listZeros = false; + boolean guessGroups = false; + + NameMode nameMode = NameMode.ALL; + + long startTime = System.nanoTime(); + + int fCount = 0; + + int namedCount = 0; + int unnamedCount = 0; + int zeroCount = 0; + int groupCount = 0; + + Map<String, List<String>> groupContents = new HashMap<>(); + + List<String> nonGroupContents = new ArrayList<>(); + + int argCount = 0; + + Map<String, List<String>> fGroups = new HashMap<>(); + List<String> fNames = new ArrayList<>(); + + String curGroup = "default"; + fGroups.put(curGroup, fNames); + for(int i = 0; i < args.length; i++) { + String fName = args[i]; + + if (doingArgs && fName.startsWith("-")) { + boolean isArg = true; + switch (fName) { + case "--": + doingArgs = false; + break; + case "--omit-zero": + case "-z": + omitZeros = true; + break; + case "--no-omit-zero": + case "-Z": + omitZeros = false; + break; + case "--list-zero": + case "-l": + listZeros = true; + break; + case "--no-list-zero": + case "-L": + listZeros = false; + break; + case "--timing": + case "-t": + doTiming = true; + break; + case "--no-timing": + case "-T": + doTiming = false; + break; + case "--file-names": + case "-f": + addFileName = true; + break; + case "--no-file-names": + case "-F": + addFileName = true; + break; + case "--name-mode": + case "-n": + if (i + 1 >= args.length) { + errOut.printf("ERROR: name mode argument requires the mode to use be specified (all, unnamed or named)\n"); + break; + } + + nameMode = NameMode.valueOf(args[++i].toUpperCase()); + break; + case "--file-group": + case "-g": + if (i + 1 >= args.length) { + errOut.printf("ERROR: file group argument requires the group name to use be specified\n"); + break; + } + + curGroup = args[++i]; + if (fGroups.containsKey(curGroup)) { + fNames = fGroups.get(curGroup); + } else { + fNames = new ArrayList<>(); + fGroups.put(curGroup, fNames); + } + break; + case "--guess-groups": + guessGroups = true; + break; + case "--no-guess-groups": + guessGroups = false; + break; + case "--read-names-from-file": + case "-r": + if (i + 1 >= args.length) { + errOut.printf("ERROR: read name file argument requires the file to use be specified\n"); + break; + } + + fCount += readNamesFromFile(fGroups, args[++i], curGroup, guessGroups); + break; + case "--output": + case "-o": + if (i + 1 >= args.length) { + errOut.printf("ERROR: output file argument requires the file to use be specified\n"); + break; + } + try { + normOut = new PrintStream(args[++i]); + } catch (IOException ioex) { + errOut.printf("Could not open output file %s\n", args[i]); + + ioex.printStackTrace(errOut); + } + break; + case "--output-errors": + case "-e": + if (i + 1 >= args.length) { + errOut.printf("ERROR: error output file argument requires the file to use be specified\n"); + break; + } + try { + errOut = new PrintStream(args[++i]); + } catch (IOException ioex) { + errOut.printf("Could not open error output file %s\n", args[i]); + + ioex.printStackTrace(errOut); + } + break; + default: + isArg = false; + } + + if (isArg) { + argCount += 1; + continue; + } + + fCount += 1; + fNames.add(fName); + } else { + fCount += 1; + fNames.add(fName); + } + } + + for (Entry<String, List<String>> fGroup : fGroups.entrySet()) { + if (fGroup.getValue().size() == 0) continue; + normOut.printf("\nFile Group '%s' starting\n", fGroup.getKey()); + for (String fName : fGroup.getValue()) { + try (FileReader fr = new FileReader(fName)) { + Scanner sc = new Scanner(fr); + + Affix afx = processFile(sc, fName); + + if (afx.intName != null && afx.weight != 0) { + String groupRx = "(.*_?)\\d+\\Z"; + boolean hasGroup = afx.intName.matches(groupRx); + String groupName = afx.intName.replaceAll(groupRx, "$1"); + + if (!groupContents.containsKey(groupName)) { + groupCount += 1; + // errOut.printf("\tTRACE: Counted distinct group %s from %s\n", groupName, afx.intName); + + if (hasGroup) { + // errOut.printf("\tTRACE: Counted actual group %s from %s\n", groupName, afx.intName); + + groupContents.put(groupName, new ArrayList<>()); + } else { + nonGroupContents.add(afx.intName); + } + } else if (hasGroup) { + groupContents.get(groupName).add(afx.intName); + } else { + nonGroupContents.add(afx.intName); + } + } + + if (afx.weight == 0) zeroCount += 1; + + if (afx.weight == 0 && !listZeros) { + if (!omitZeros) + normOut.printf("\nAffix %s has zero spawn weight\n", afx.intName); + } else { + boolean isNamed = (afx.affixSuffix != null) || (afx.affixPrefix != null); + + if (isNamed) namedCount += 1; + else unnamedCount += 1; + + if (nameMode == NameMode.UNNAMED && isNamed) continue; + if (nameMode == NameMode.NAMED && !isNamed) continue; + + normOut.printf("\n%s\n", afx.toString()); + } + } catch (Exception ex) { + errOut.printf("Something bad happened for file %s:%s\n", fName, ex.getMessage()); + + ex.printStackTrace(errOut); + + errOut.println(); + } + } + normOut.printf("\nFile Group '%s' ending\n", fGroup.getKey()); + } + + errOut.println("\nGroup Contents: "); + for (Entry<String, List<String>> ent: groupContents.entrySet()) { + errOut.printf("\t%s: %s\n", ent.getKey(), ent.getValue()); + } + errOut.println(); + errOut.println(); + + long endTime = System.nanoTime(); + errOut.printf("\nProcessed %,d affixes (%,d named, %,d unnamed, %,d zero-weight) (%,d effects) (%,d distinct groups, %,d actual groups, %,d nongrouped affixes) out of %,d files (%,d groups) in %,d nanoseconds (%.2f seconds)\n", fCount, namedCount, unnamedCount, zeroCount, effectCount, groupCount, groupContents.size(), nonGroupContents.size(), fCount, fGroups.size(), endTime - startTime, ((double)(endTime - startTime) / 1000000000)); + errOut.printf("\tOptions: Name Mode: %s, Special-case zero weight: %s, Noting zero-weight in special case: %s\n", nameMode, !listZeros, !omitZeros); + } + + // Process an affix file + private static Affix processFile(Scanner scn, String fName) { + Affix afx = new Affix(); + + long startTime = System.nanoTime(); + + while (scn.hasNextLine()) { + String ln = scn.nextLine(); + ln = ln.replaceAll("\\p{Cntrl}", ""); + + if (ln.contains("[NOT_UNITTYPES]")) { + afx.setEquipType(true); + continue; + } else if (ln.contains("[UNITTYPES]")) { + afx.setEquipType(false); + continue; + } + + String[] splits = ln.split(":"); + if (ln.contains("<TRANSLATE>")) { + splits[0] = splits[0].substring(11); + + switch (splits[0]) { + case "SUFFIX": + afx.affixSuffix = splits[1]; + break; + case "PREFIX": + afx.affixPrefix = splits[1]; + break; + default: + errOut.printf("Misformed affix translation: (%s) (%s) (%s)\n", splits[0], splits[1], fName); + } + } else if (ln.contains("MIN_SPAWN_RANGE")) { + afx.minLevel = Integer.parseInt(splits[1]); + } else if (ln.contains("MAX_SPAWN_RANGE")) { + afx.maxLevel = Integer.parseInt(splits[1]); + } else if (ln.contains("WEIGHT:")) { + afx.weight = Integer.parseInt(splits[1]); + } else if (ln.contains("SLOTS_OCCUPY")) { + afx.weight = Integer.parseInt(splits[1]); + } else if (ln.contains("UNITTYPE") && !ln.contains("/")) { + if (splits.length == 1) + errOut.printf("Malformed equip type: (%s) (%s)\n", splits[0], fName); + afx.addEquipType(splits[1]); + } else if (splits[0].equals("<STRING>NAME")) { + if (splits.length == 1) + errOut.printf("Malformed name: (%s) (%s)\n", splits[0], fName); + afx.intName = splits[1]; + } else if (ln.contains("[EFFECT]")) { + afx.effects.add(parseEffect(afx, scn, fName)); + } + } + + long endTime = System.nanoTime(); + if (doTiming) errOut.printf("\tProcessed affix %s from %s in %d nanoseconds (%.2f seconds)\n\n", afx.intName, fName, endTime - startTime, ((double)(endTime - startTime) / 1000000000)); + + return afx; + } + + private static Effect parseEffect(Affix afx, Scanner scn, String fName) { + Effect efct = new Effect(); + + long startTime = System.nanoTime(); + + efct.fName = fName; + + while (scn.hasNextLine()) { + String ln = scn.nextLine(); + ln = ln.replaceAll("\\p{Cntrl}", ""); + + if (ln.contains("[/EFFECT]")) break; + + String[] splits = ln.split(":"); + + // Empty field + if (splits.length == 1) continue; + + if (ln.contains("NAME")) { + efct.name = splits[1]; + } else if (ln.contains("DAMAGE_TYPE")) { + efct.damageType = splits[1]; + } else if (ln.contains("TYPE")) { + efct.type = splits[1]; + } else if (ln.contains("ACTIVATION")) { + switch (splits[1]) { + case "DYNAMIC": + case "PASSIVE": + // Passive is the default, and + // dynamic doesn't have much + // actual difference. + break; + case "TRANSFER": + efct.isTransfer = true; + break; + default: + errOut.printf("Malformed activation type: (%s) (%s) (%s)\n", splits[1], efct.name, afx.intName); + } + } else if (ln.contains("DURATION")) { + if (splits[1].equals("ALWAYS")) { + efct.hasDuration = false; + + efct.duration = Double.POSITIVE_INFINITY; + } else if (splits[1].equals("INSTANT")) { + efct.hasDuration = false; + + efct.duration = Double.NaN; + } else if (splits[1].equals("PERCENT")) { + efct.hasDuration = false; + + efct.duration = Double.NaN; + + errOut.printf("WARN: Punting on DURATION:PERCENT for %s\n", fName); + } else if (splits[1].equals("0")) { + efct.hasDuration = false; + efct.duration = 0.0; + } else { + efct.hasDuration = true; + + efct.duration = Double.parseDouble(splits[1]); + } + } else if (ln.contains("<FLOAT>MIN:")) { + efct.minValue = Double.parseDouble(splits[1]); + } else if (ln.contains("<FLOAT>MAX:")) { + efct.maxValue = Double.parseDouble(splits[1]); + } else if (ln.contains("USEOWNERLEVEL")) { + // We don't care about this, for now + } else if (ln.contains("LEVEL:")) { + efct.level = Integer.parseInt(splits[1]); + } else if (ln.contains("EXCLUSIVE")) { + efct.exclusive = Boolean.parseBoolean(splits[1]); + } else if (ln.contains("GRAPHOVERRIDE")) { + efct.graphOverride = splits[1]; + } else if (ln.contains("USEOWNERLEVEL")) { + efct.ownerLevel = Boolean.parseBoolean(splits[1]); + } else if (ln.contains("NOGRAPH")) { + efct.useGraph = Boolean.parseBoolean(splits[1]); + } else if (ln.contains("STATNAME")) { + efct.statName = splits[1]; + } else if (ln.contains("STATPERCENT")) { + efct.statPercent = Double.parseDouble(splits[1]); + } else if (ln.contains("STATMODIFIERISBONUS")) { + efct.isStatBonus = Boolean.parseBoolean(splits[1]); + } else if (ln.contains("RESISTANCE:")) { + efct.resist = Double.parseDouble(splits[1]); + } else if (ln.contains("FORCE:")) { + efct.resist = Double.parseDouble(splits[1]); + } else if (ln.contains("MIN_PER")) { + efct.minPer = Double.parseDouble(splits[1]); + } else if (ln.contains("MAX_PER")) { + efct.maxPer = Double.parseDouble(splits[1]); + } else if (ln.contains("RANGE:") || ln.contains("RADIUS")) { + efct.range = Double.parseDouble(splits[1]); + } else if (ln.contains("MAX_COUNT:") || ln.contains("MAX_TARGETS")) { + efct.maxCount = Double.parseDouble(splits[1]); + } else if (ln.contains("PULSE_RATE")) { + efct.pulse = Double.parseDouble(splits[1]); + } else if (ln.contains("CHANCE:")) { + // NOTE: Should really use its own field + efct.resist = Double.parseDouble(splits[1]); + } + } + + long endTime = System.nanoTime(); + if (doTiming) errOut.printf("\t\tProcessed effect %s from %s in %d nanoseconds (%.2f seconds)\n", efct.name, fName, endTime - startTime, ((double)((endTime - startTime) / 1000000000))); + + effectCount += 1; + + return efct; + } +} diff --git a/src/main/java/tlIItools/Effect.java b/src/main/java/tlIItools/Effect.java new file mode 100644 index 00000000..c4f3cfe7 --- /dev/null +++ b/src/main/java/tlIItools/Effect.java @@ -0,0 +1,340 @@ +package tlIItools; + +import java.io.FileReader; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Scanner; + +/** + * Represents an effect attached to an affix. + */ +public class Effect { + private static Map<String, String> detals; + private static Map<String, String> timeDetals; + + private static List<ReplPair> replList; + + static { + try (FileReader detalReader = new FileReader("data/affix-detals.txt")) { + detals = readDetails(new Scanner(detalReader)); + } catch (IOException ioex) { + AffixLister.errOut.println("Error loading affix detail text"); + } + + try (FileReader timedDetalReader = new FileReader("data/timed-affix-detals.txt")) { + timeDetals = readDetails(new Scanner(timedDetalReader)); + } catch (IOException ioex) { + AffixLister.errOut.println("Error loading timed affix detail text"); + } + + try (FileReader replListReader = new FileReader("data/replace-list.txt")) { + replList = ReplPair.readList(new Scanner(replListReader)); + } catch (IOException ioex) { + AffixLister.errOut.println("Error loading replacement lists"); + } + } + + /** + * Read effect detail strings from an input source. + * + * @param scn + * The source to read from. + * @return + * The map of effect details to use. + */ + public static Map<String, String> readDetails(Scanner scn) { + Map<String, String> detals = new HashMap<>(); + + return readDetails(detals, scn); + } + + /** + * Read effect detail strings from an input source, adding to an + * existing set. + * + * @param detals + * The details to add to. + * @param scn + * The source to read from. + * @return + * The map of effect details to use. + */ + public static Map<String, String> readDetails(Map<String, String> detals, Scanner scn) { + while (scn.hasNextLine()) { + String name = scn.nextLine().trim(); + if (name.equals("")) continue; + if (name.startsWith("#")) continue; + + String body; + do { + body = scn.nextLine().trim(); + } while (body.startsWith("#")); + + detals.put(name, body); + } + + return detals; + } + + private static void sanityCheckFormats() { + for (Entry<String, String> detal : detals.entrySet()) { + String fmt = detal.getValue(); + + AffixLister.errOut.printf("\tTRACE: Applying replacements for %s\n", detal.getKey()); + for (ReplPair repl : replList) { + String tmp = fmt; + fmt = fmt.replaceAll(repl.find, repl.replace); + if (!fmt.equals(tmp)) { + AffixLister.errOut.printf("\t\tTRACE: Replaced %s with %s: \n\t\t%s\n\t\t%s\n", repl.find, repl.replace, tmp, fmt); + } + } + + if (fmt.contains("<") || fmt.contains(">")) { + AffixLister.errOut.printf("WARN: Details for effect %s are malformated (contains < or >):\n\t%s\n", detal.getKey(), fmt); + } + } + + for (Entry<String, String> detal : timeDetals.entrySet()) { + String fmt = detal.getValue(); + + for (ReplPair repl : replList) { + fmt = fmt.replaceAll(repl.find, repl.replace); + } + + if (fmt.contains("<") || fmt.contains(">")) { + AffixLister.errOut.printf("WARN: Details for effect %s are malformated (contains < or >):\n\t%s\n", detal.getKey(), fmt); + } + } + } + /** + * The file name this effect came from. + */ + public String fName; + + /** + * The name of the effect. + */ + public String name; + + /** + * The specific effect that happens. + */ + public String type; + + /** + * Damage type for the effect, if applicable. + */ + public String damageType = "physical"; + + /** + * Duration of the effect. + */ + public double duration; + + /** + * Whether or not we have a duration or not. + */ + public boolean hasDuration; + + /** + * Minimum value for the effect. + */ + public double minValue; + /** + * Maximum value for the effect. + */ + public double maxValue; + + /** + * The name of the stat that applies to this affect. + */ + public String statName; + /** + * The percent of the stat value to apply. + */ + public double statPercent; + /** + * Whether or not this stat is a bonus value. + */ + public boolean isStatBonus; + + /** + * Whether or not this uses the owners level to modify + * any applicable graph. + */ + public boolean ownerLevel; + + /** + * The graph to use instead of the default graph. + */ + public String graphOverride; + + /** + * Whether or not a graph is used for this effect. + */ + public boolean useGraph = true; + + /** + * Whether this effect can stack with itself. + */ + public boolean exclusive; + + /** + * The amount the targets armor is reduced by for this + * effect. + */ + public double soakScale = 1.0; + + /** + * Level of the effect. + */ + public int level = -1; + + /** + * Whether or not this effect is a 'transfer' + * effect (Applied to the enemy on a hit). + */ + public boolean isTransfer; + + /** + * The amount to resist/do knockback by. + */ + public double resist; + + /** + * Minimum value per monster. + */ + public double minPer; + + /** + * Maximum value per monster. + */ + public double maxPer; + + /** + * Range for effect. + */ + public double range; + + /** + * Maximum count of monsters. + */ + public double maxCount; + + /** + * The rate at which the effect fires. + */ + public double pulse; + /** + * Create a new blank effect. + */ + public Effect() { + + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + if (isTransfer) { + sb.append("Inflict on Hit: "); + } + + Map<String, String> detMap = hasDuration ? timeDetals : detals; + + if (detMap.containsKey(type) || (hasDuration && !timeDetals.containsKey(type) && detals.containsKey(type))) { + String fmt; + if (hasDuration && !timeDetals.containsKey(type) && detals.containsKey(type)) { + AffixLister.errOut.printf("Improvised details for timed %s\n", type); + fmt = detals.get(type) + "for <DUR> seconds"; + } else { + fmt = detMap.get(type); + } + + // Expand aliases first. + + for (ReplPair repl : replList) { + fmt = fmt.replaceAll(repl.find, repl.replace); + } + + if (minValue <= 0 && maxValue <= 0) { + fmt = fmt.replaceAll("<C\\|([^|>]+)\\|([^|>]+)>", "$1"); + } + + if (minValue >= 0 && maxValue >= 0) { + fmt = fmt.replaceAll("<C\\|([^|>]+)\\|([^|>]+)>", "$2"); + } + + if (minPer <= 0 && maxPer <= 0) { + fmt = fmt.replaceAll("<MC\\|(\\w+)\\|(\\w+)>", "$1"); + } + + if (minPer >= 0 && maxPer >= 0) { + fmt = fmt.replaceAll("<MC\\|([^|>]+)\\|([^|>]+)>", "$2"); + } + + if (fmt.contains("<") || fmt.contains(">")) { + AffixLister.errOut.printf("WARN: Details for effect %s are malformated (contains < or >):\n\t%s\n", type, fmt); + } + + sb.append(String.format(fmt, Math.abs(minValue), Math.abs(maxValue), duration, damageType.toLowerCase(), level, resist, name, Math.abs(minPer), Math.abs(maxPer), range, maxCount, pulse)); + } else { + sb.append("No effect details for effect "); + sb.append(type); + sb.append(String.format(" with parameters (min %.2f, max %.2f, dur %.2f, type %s, level %d)", minValue, maxValue, duration, damageType.toLowerCase(), level, name)); + + if (AffixLister.addFileName) { + sb.append(" from file "); + sb.append(fName); + } + + if (hasDuration) AffixLister.errOut.print("TIMED: "); + AffixLister.errOut.println(sb.toString()); + } + + if (name != null) { + sb.append(" (named "); + sb.append(name); + sb.append(")"); + } + + if (exclusive) sb.append(" (Exclusive)"); + + if (graphOverride != null) { + sb.append(" (Uses "); + sb.append(graphOverride); + sb.append(" graph)"); + } + + if (ownerLevel) { + sb.append(" (Uses owner level for graph)"); + } + + if (soakScale != 1.0) { + String fmt = " (%.2f%% reduced effectiveness of armor)"; + + sb.append(String.format(fmt, (1 - soakScale) * 100)); + } + + if (level != -1) { + sb.append(" (Level "); + sb.append(level); + sb.append(")"); + } + + if (statName != null) { + sb.append(String.format(" (%.2f of stat %s", statPercent, statName)); + if (isStatBonus) sb.append(" as bonus)"); + else sb.append(")"); + } + + if (!useGraph) sb.append(" (Ignoring graph)"); + + return sb.toString(); + } +} diff --git a/src/main/java/ReplPair.java b/src/main/java/tlIItools/ReplPair.java index 474f7c93..41d483b3 100644 --- a/src/main/java/ReplPair.java +++ b/src/main/java/tlIItools/ReplPair.java @@ -1,3 +1,5 @@ +package tlIItools; + import java.util.ArrayList; import java.util.List; import java.util.Scanner; |
