PRC8/nwn/nwnprc/DocGen/trunk/prc/utils/ScrollMerchantGen.java
2024-06-21 19:37:17 -05:00

386 lines
18 KiB
Java

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<Integer> getAllSpellCastingClasses(TwoDAStore twoDA)
{
Set<Integer> vanillaSpellcasterClasses = Set.of(1,2,3,6,7,9,10);
HashSet<Integer> result = new HashSet<Integer>();
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<ScrollInfo> 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<Integer, Integer> 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<ScrollInfo> 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<Integer, TreeMap<String, ScrollInfo>> arcaneScrollResRefs = new TreeMap<Integer, TreeMap<String, ScrollInfo>>();
TreeMap<Integer, TreeMap<String, ScrollInfo>> divineScrollResRefs = new TreeMap<Integer, TreeMap<String, ScrollInfo>>();
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<ScrollInfo> arcaneLevelScrollRefRefs = arcaneScrollResRefs.values()
.stream().flatMap(scrollMap -> scrollMap.values().stream());
Stream<ScrollInfo> divineLevelScrollRefRefs = divineScrollResRefs.values()
.stream().flatMap(scrollMap -> scrollMap.values().stream());
Stream<ScrollInfo> 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 <shopName>.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<ScrollInfo> allScrolls, String shopName, Predicate<ScrollInfo> scrollFilter) throws IOException {
// Load the 2da file
Data_2da scrolls2da = twoDA.get("des_crft_scroll");
Data_2da spells2da = twoDA.get("spells");
String xmlPrefix =
"<gff name=\"" + shopName + ".utm\" type=\"UTM \" version=\"V3.2\" >" + "\n" +
" <struct id=\"-1\" >" + "\n" +
" <element name=\"ResRef\" type=\"11\" value=\"" + shopName + "\" />" + "\n" +
" <element name=\"LocName\" type=\"12\" value=\"64113\" >" + "\n" +
" <localString languageId=\"0\" value=\"" + shopName + "\" />" + "\n" +
" </element>" + "\n" +
" <element name=\"Tag\" type=\"10\" value=\"" + shopName + "\" />" + "\n" +
" <element name=\"MarkUp\" type=\"5\" value=\"100\" />" + "\n" +
" <element name=\"MarkDown\" type=\"5\" value=\"100\" />" + "\n" +
" <element name=\"BlackMarket\" type=\"0\" value=\"0\" />" + "\n" +
" <element name=\"BM_MarkDown\" type=\"5\" value=\"15\" />" + "\n" +
" <element name=\"IdentifyPrice\" type=\"5\" value=\"-1\" />" + "\n" +
" <element name=\"MaxBuyPrice\" type=\"5\" value=\"-1\" />" + "\n" +
" <element name=\"StoreGold\" type=\"5\" value=\"-1\" />" + "\n" +
" <element name=\"OnOpenStore\" type=\"11\" value=\"\" />" + "\n" +
" <element name=\"OnStoreClosed\" type=\"11\" value=\"\" />" + "\n" +
" <element name=\"WillNotBuy\" type=\"15\" />" + "\n" +
" <element name=\"WillOnlyBuy\" type=\"15\" >" + "\n" +
" <struct id=\"97869\" >" + "\n" +
" <element name=\"BaseItem\" type=\"5\" value=\"29\" />" + "\n" +
" </struct>" + "\n" +
" </element>" + "\n" +
" <element name=\"StoreList\" type=\"15\" >" + "\n" +
" <struct id=\"0\" />" + "\n" +
" <struct id=\"4\" />" + "\n" +
" <struct id=\"2\" >" + "\n" +
" <element name=\"ItemList\" type=\"15\" >" + "\n";
String xmlSuffix =
" </element>" + "\n" +
" </struct>" + "\n" +
" <struct id=\"3\" />" + "\n" +
" <struct id=\"1\" />" + "\n" +
" </element>" + "\n" +
" <element name=\"ID\" type=\"0\" value=\"5\" />" + "\n" +
" <element name=\"Comment\" type=\"10\" value=\"\" />" + "\n" +
" </struct>" + "\n" +
"</gff>" + "\n";
StringBuffer xmlString = new StringBuffer();
Stream<ScrollInfo> 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(
" <struct id=\"" + posCounter + "\" >" + "\n" +
" <element name=\"InventoryRes\" type=\"11\" value=\"" + resref + "\" />" + "<!-- " + name + " -->" + "\n" +
" <element name=\"Repos_PosX\" type=\"2\" value=\"" + (posCounter % 10) + "\" />" + "\n" +
" <element name=\"Repos_Posy\" type=\"2\" value=\"" + (posCounter / 10) + "\" />" + "\n" +
" <element name=\"Infinite\" type=\"0\" value=\"1\" />" + "\n" +
" </struct>" + "\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<String, String> levelScrollResRefs : divineScrollResRefs.values())
// for (String name : levelScrollResRefs.keySet()) {
// String resref = levelScrollResRefs.get(name);
// xmlString.append(
// " <struct id=\"" + posCounter + "\" >" + "\n" +
// " <element name=\"InventoryRes\" type=\"11\" value=\"" + resref + "\" />" + "<!-- " + name + " -->" + "\n" +
// " <element name=\"Repos_PosX\" type=\"2\" value=\"" + (posCounter % 10) + "\" />" + "\n" +
// " <element name=\"Repos_Posy\" type=\"2\" value=\"" + (posCounter / 10) + "\" />" + "\n" +
// " <element name=\"Infinite\" type=\"0\" value=\"1\" />" + "\n" +
// " </struct>" + "\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<Integer, Integer> 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<Integer, TreeMap<String, ScrollInfo>> 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<String, ScrollInfo>());
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);
}
}