package prc.utils; import prc.autodoc.*; import prc.autodoc.Main.TLKStore; import prc.autodoc.Main.TwoDAStore; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import static prc.Main.verbose; /** * A little tool that parses des_crft_scroll, extracts unique item resrefs from it and * makes a merchant selling those resrefs. * * @author Heikki 'Ornedan' Aitakangas */ public class ScrollMerchantGen { /** * Ye olde maine methode. * * @param args The arguments * @throws IOException If the writing fails, just die on the exception */ public static void main(String[] args) throws IOException { if (args.length == 0) readMe(); String twoDAPath = null; String tlkPath = null; // parse args for (String param : args) {//2dadir tlkdir | [--help] // Parameter parseage if (param.startsWith("-")) { if (param.equals("--help")) readMe(); else { for (char c : param.substring(1).toCharArray()) { switch (c) { default: System.out.println("Unknown parameter: " + c); readMe(); } } } } else { // It's a pathname if (twoDAPath == null) twoDAPath = param; else if (tlkPath == null) tlkPath = param; else { System.out.println("Unknown parameter: " + param); readMe(); } } } // Load data TwoDAStore twoDA = new TwoDAStore(twoDAPath); TLKStore tlks = new TLKStore("dialog.tlk", "prc8_consortium.tlk", tlkPath); doScrollMerchantGen(twoDA, tlks, "scrolltemp"); } public static Set getAllSpellCastingClasses(TwoDAStore twoDA) { Set vanillaSpellcasterClasses = Set.of(1,2,3,6,7,9,10); HashSet result = new HashSet(); Data_2da classes = twoDA.get("classes"); for(int i = 0; i < classes.getEntryCount(); i++) { // handle the vanilla spellcasters if (vanillaSpellcasterClasses.contains(i)) { result.add(i); continue; } boolean isSpellCaster = classes.getEntry("SpellCaster", i).equals("1"); boolean isPlayerClass = classes.getEntry("PlayerClass", i).equals("1"); if (!isSpellCaster || !isPlayerClass) continue; // Attempt to load the spellbook 2da for the class. If it fails, not a spellcaster, // or a spellcaster that reuses another book (so we don't need a shop for them). try { String classAbrev = classes.getEntry("FeatsTable", i).toLowerCase().substring(9); twoDA.get("cls_spcr_" + classAbrev); } catch (TwoDAReadException e) { continue; } String label = classes.getEntry("Label", i); if (!label.equals("****")) result.add(i); } return result; } /** * Performs the scroll merchant generation for all scroll shops. * @param twoDA A TwoDAStore for loading 2da data from * @param tlks A TLKStore for reading tlk data from * @throws IOException Just tossed back up */ public static void doScrollMerchantGen(TwoDAStore twoDA, TLKStore tlks, String outPath) throws IOException { String shopPrefix = "prcSS_"; List allScrolls = getAllScrolls(twoDA, tlks); doScrollMerchantGenShop(twoDA, tlks, outPath, false, allScrolls, "prc_scrolls", scroll -> true); for(int classId : getAllSpellCastingClasses(twoDA)) { String allClassSpellsShop = shopPrefix + "C" + classId; doScrollMerchantGenShop(twoDA, tlks, outPath, false, allScrolls, allClassSpellsShop, scroll -> scroll.classesForSpellWithLevel.containsKey(classId)); for(int level = 0; level <= 9; level++) { final int spellLevel = level; String shopName = shopPrefix + "C" + classId + "L" + level; doScrollMerchantGenShop(twoDA, tlks, outPath, false, allScrolls, shopName, scroll -> { Map classSpellInfo = scroll.classesForSpellWithLevel; return classSpellInfo.containsKey(classId) && classSpellInfo.get(classId).equals(spellLevel); }); } } char[] letters = "abcdefghijklmnopqrstuvwxyz".toCharArray(); for(char letter : letters) { String shopName = "prcSS_alpha_" + letter; doScrollMerchantGenShop(twoDA, tlks, outPath, true, allScrolls, shopName, scroll -> scroll.Name.toLowerCase().startsWith("" + letter)); } } private static List getAllScrolls(TwoDAStore twoDA, TLKStore tlks) { Data_2da spells2da = twoDA.get("spells"); Data_2da scrolls2da = twoDA.get("des_crft_scroll"); // Loop over the scroll entries and get a list of unique resrefs TreeMap> arcaneScrollResRefs = new TreeMap>(); TreeMap> divineScrollResRefs = new TreeMap>(); String entry; for (int i = 0; i < scrolls2da.getEntryCount(); i++) { // Skip subradials if (spells2da.getEntry("Master", i).equals("****")) { if (!(entry = scrolls2da.getEntry("Wiz_Sorc", i)).equals("****")) addScroll(arcaneScrollResRefs, twoDA, tlks, i, entry.toLowerCase()); if (!(entry = scrolls2da.getEntry("Cleric", i)).equals("****")) addScroll(divineScrollResRefs, twoDA, tlks, i, entry.toLowerCase()); if (!(entry = scrolls2da.getEntry("Paladin", i)).equals("****")) addScroll(divineScrollResRefs, twoDA, tlks, i, entry.toLowerCase()); if (!(entry = scrolls2da.getEntry("Druid", i)).equals("****")) addScroll(divineScrollResRefs, twoDA, tlks, i, entry.toLowerCase()); if (!(entry = scrolls2da.getEntry("Ranger", i)).equals("****")) addScroll(divineScrollResRefs, twoDA, tlks, i, entry.toLowerCase()); if (!(entry = scrolls2da.getEntry("Bard", i)).equals("****")) addScroll(arcaneScrollResRefs, twoDA, tlks, i, entry.toLowerCase()); } } //int posCounter = 0; Stream arcaneLevelScrollRefRefs = arcaneScrollResRefs.values() .stream().flatMap(scrollMap -> scrollMap.values().stream()); Stream divineLevelScrollRefRefs = divineScrollResRefs.values() .stream().flatMap(scrollMap -> scrollMap.values().stream()); Stream allScrollInfos = Stream.concat(arcaneLevelScrollRefRefs, divineLevelScrollRefRefs) .distinct() .sorted((scroll1, scroll2) -> -scroll1.Name.compareTo(scroll2.Name)); return allScrollInfos.collect(Collectors.toList()); } /** * Performs the scroll merchant generation. Made public for the purposes of BuildScrollHack. * * @param twoDA A TwoDAStore for loading 2da data from * @param tlks A TLKStore for reading tlk data from * @param allScrolls A list containing ScrollInfo for all scrolls * @param shopName The name of the shop (will generate a .utm.xml file) * @param scrollFilter A predicate used to filter the scrolls for the given shop * @throws IOException Just tossed back up */ private static void doScrollMerchantGenShop(TwoDAStore twoDA, TLKStore tlks, String outPath, boolean generateEmptyShops, List allScrolls, String shopName, Predicate scrollFilter) throws IOException { // Load the 2da file Data_2da scrolls2da = twoDA.get("des_crft_scroll"); Data_2da spells2da = twoDA.get("spells"); String xmlPrefix = "" + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n"; String xmlSuffix = " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + "" + "\n"; StringBuffer xmlString = new StringBuffer(); Stream allScrollRefRefs = allScrolls.stream().filter(scrollFilter); int[] posCounterRef = new int[] { 0 }; allScrollRefRefs.forEach(scroll -> { int posCounter = posCounterRef[0]; String resref = scroll.ScrollResRef; String name = scroll.Name; xmlString.append( " " + "\n" + " " + "" + "\n" + " " + "\n" + " " + "\n" + " " + "\n" + " " + "\n" ); posCounterRef[0]+= 1; }); if (posCounterRef[0] == 0) { System.out.println("No scrolls found for shop " + shopName); if (!generateEmptyShops) return; } // // Then divine scrolls // for (Map levelScrollResRefs : divineScrollResRefs.values()) // for (String name : levelScrollResRefs.keySet()) { // String resref = levelScrollResRefs.get(name); // xmlString.append( // " " + "\n" + // " " + "" + "\n" + // " " + "\n" + // " " + "\n" + // " " + "\n" + // " " + "\n" // ); // posCounter++; // } String shopFilePath = outPath + File.separator + shopName + ".utm.xml"; File target = new File(shopFilePath); // Clean up old version if necessary if (target.exists()) { if (verbose) System.out.println("Deleting previous version of " + target.getName()); target.delete(); } if (verbose) System.out.println("Writing brand new version of " + target.getName()); target.createNewFile(); // Creater the writer and print FileWriter writer = new FileWriter(target, true); writer.write(xmlPrefix + xmlString.toString() + xmlSuffix); // Clean up writer.flush(); writer.close(); } private static class ScrollInfo { private TwoDAStore twoDAStore; public ScrollInfo(TwoDAStore twoDA, int rowNum, String scrollResRef, String name) { this.twoDAStore = twoDA; this.ScrollResRef = scrollResRef; this.Name = name; Data_2da spells2da = twoDA.get("spells"); try { this.SpellLevel = Integer.parseInt(spells2da.getEntry("Innate", rowNum)); } catch (NumberFormatException e) { System.err.println("Non-number value in spells.2da Innate column on line " + rowNum + ": " + spells2da.getEntry("Innate", rowNum)); return; } Data_2da classes = twoDA.get("classes"); this.classesForSpellWithLevel = ScrollGen.getClassesForSpell(twoDA, rowNum); } public int SpellLevel = -1; public String ScrollResRef; public String Name; public Map classesForSpellWithLevel; public boolean equals(Object other) { if (this == other) return true; if (other == null) return false; if (getClass() != other.getClass()) return false; ScrollInfo otherScroll = (ScrollInfo)other; return ScrollResRef.equals(otherScroll.ScrollResRef); } public int hashCode() { return Objects.hash(ScrollResRef); } } private static void addScroll(TreeMap> scrollResRefs, TwoDAStore twoDAStore, TLKStore tlks, int rowNum, String scrollResRef) { int innateLevel = -1, tlkRef = -1; Data_2da spells2da = twoDAStore.get("spells"); // HACK - Skip non-PRC scrolls if (!scrollResRef.startsWith("prc_scr_")) return; try { tlkRef = Integer.parseInt(spells2da.getEntry("Name", rowNum)); } catch (NumberFormatException e) { System.err.println("Non-number value in spells.2da Name column on line " + rowNum + ": " + spells2da.getEntry("Name", rowNum)); return; } ScrollInfo scroll = new ScrollInfo(twoDAStore, rowNum, scrollResRef, tlks.get(tlkRef)); scroll.SpellLevel = innateLevel; innateLevel = scroll.SpellLevel; if (scrollResRefs.get(innateLevel) == null) scrollResRefs.put(innateLevel, new TreeMap()); scrollResRefs.get(innateLevel).put(tlks.get(tlkRef), scroll); } /** * Prints the use instructions for this program and kills execution. */ private static void readMe() { // 0 1 2 3 4 5 6 7 8 // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 System.out.println("Usage:\n" + " java -jar prc.jar scrmrchgen 2dadir tlkdir | [--help]\n" + "\n" + "2dadir Path to a directory containing des_crft_scroll.2da and spells.2da.\n" + "tlkdir Path to a directory containing dialog.tlk and prc8_consortium.tlk\n" + "\n" + "--help prints this info you are reading\n" + "\n" + "\n" + "Generates a merchant file - prc_scrolls.utm - based on the given scroll list\n" + "2da file. The merchant file will be written to current directory in Pspeed's\n" + "XML <-> Gff -xml format\n" ); System.exit(0); } }