Updated Release Archive. Fixed Mage-killer prereqs. Removed old LETO & ConvoCC related files. Added organized spell scroll store. Fixed Gloura spellbook. Various TLK fixes. Reorganized Repo. Removed invalid user folders. Added DocGen back in.
1073 lines
42 KiB
Java
1073 lines
42 KiB
Java
package prc.autodoc;
|
|
|
|
import java.io.File;
|
|
import java.io.FileWriter;
|
|
import java.io.IOException;
|
|
import java.io.RandomAccessFile;
|
|
import java.util.ArrayList;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Scanner;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
import static prc.Main.*;
|
|
|
|
/**
|
|
* This class forms an interface for accessing 2da files in the
|
|
* PRC automated manual generator.
|
|
*/
|
|
public class Data_2da implements Cloneable {
|
|
// String matching pattern. Gets a block of non-whitespace (tab is not counted as whitespace here) that does not contain any " OR " followed by any characters until the next "
|
|
private static final Pattern pattern = Pattern.compile("[[\\S&&[^\"]][\t]]+|\"[^\"]+\"");
|
|
// Same as the above, but counts tab as whitespace. Bug-compatibility with BioWare's own violations of 2da spec
|
|
private static final Pattern bugCompatPattern = Pattern.compile("[\\S&&[^\"]]+|\"[^\"]+\"");
|
|
//private static Matcher matcher = pattern.matcher("");
|
|
|
|
private LinkedHashMap<String, ArrayList<String>> mainData = new LinkedHashMap<String, ArrayList<String>>();
|
|
private final String name;
|
|
private String defaultValue;
|
|
|
|
/**
|
|
* Bugcompatibility feature for a special case where the first line of a 2da has * for line number instead of 0
|
|
*/
|
|
private boolean starOnLine0 = false;
|
|
|
|
/**
|
|
* Used for storing the original case of the labels. The ones used in the hashmap are lowercase
|
|
*/
|
|
private ArrayList<String> realLabels = new ArrayList<String>();
|
|
|
|
private final boolean TLKEditCompatible = true;
|
|
|
|
/**
|
|
* Creates a new, empty Data_2da with the specified name.
|
|
*
|
|
* @param name The new 2da file's name.
|
|
*/
|
|
public Data_2da(String name) {
|
|
this(name, "");
|
|
}
|
|
|
|
/**
|
|
* Creates a new, empty Data_2da with the specified name and default value.
|
|
*
|
|
* @param name the new 2da file's name
|
|
* @param defaultValue the new 2da file's default value
|
|
*/
|
|
public Data_2da(String name, String defaultValue) {
|
|
this.name = name;
|
|
this.defaultValue = defaultValue;
|
|
}
|
|
|
|
/**
|
|
* Private constructor for use in cloning.
|
|
*
|
|
* @param name the file name
|
|
* @param defaultValue the default value
|
|
* @param realLabels the labels with original case
|
|
* @param mainData the contents
|
|
*/
|
|
public Data_2da(String name, String defaultValue, ArrayList<String> realLabels, LinkedHashMap<String, ArrayList<String>> mainData) {
|
|
this.name = name;
|
|
this.defaultValue = defaultValue;
|
|
this.realLabels = realLabels;
|
|
this.mainData = mainData;
|
|
}
|
|
|
|
/**
|
|
* Saves the 2da represented by this object to file. Equivalent to calling
|
|
* <code>save2da(path, false, false)</code>.
|
|
*
|
|
* @param path the directory to save the file in. If this is "" or null,
|
|
* the current directory is used.
|
|
* @throws IOException if <code>true</code> and a file with the same name as this 2da
|
|
* exists at <code>path</code>, it is overwritten.
|
|
* If <code>false</code> and in the same situation, an IOException is
|
|
* thrown.
|
|
*/
|
|
public void save2da(String path) throws IOException {
|
|
save2da(path, false, false);
|
|
}
|
|
|
|
/**
|
|
* Saves the 2da represented by this object to file. CRLFs are explicitly used
|
|
* instead of system specific line terminator.
|
|
*
|
|
* @param path the directory to save the file in. If this is "" or null,
|
|
* the current directory is used.
|
|
* @param allowOverWrite if <code>true</code> and a file with the same name as this 2da
|
|
* exists at <code>path</code>, it is overwritten.
|
|
* If <code>false</code> and in the same situation, an IOException is
|
|
* thrown.
|
|
* @param evenColumns if <code>true</code>, every entry in a column will be padded until they
|
|
* start from the same position
|
|
* @throws IOException if cannot overwrite, or the underlying IO throws one
|
|
*/
|
|
public void save2da(String path, boolean allowOverWrite, boolean evenColumns) throws IOException {
|
|
String CRLF = "\r\n";
|
|
if (path == null || path.equals(""))
|
|
path = "." + File.separator;
|
|
if (!path.endsWith(File.separator))
|
|
path += File.separator;
|
|
|
|
File file = new File(path + name + ".2da");
|
|
if (file.exists() && !allowOverWrite)
|
|
throw new IOException("File exists already: " + file.getAbsolutePath());
|
|
|
|
// Inform user
|
|
if (verbose) System.out.print("Saving 2da file: " + name + " ");
|
|
|
|
FileWriter fw = new FileWriter(file, false);
|
|
String[] labels = this.getLabels();
|
|
String toWrite;
|
|
|
|
// Get the amount of padding used, if any
|
|
int[] widths = new int[labels.length + 1];// All initialised to 0
|
|
if (evenColumns) {
|
|
ArrayList<String> column;
|
|
int pad;
|
|
// Loop over columns
|
|
for (int i = 0; i < labels.length; i++) {
|
|
pad = labels[i].length();
|
|
column = mainData.get(labels[i]);
|
|
// Loop over rows
|
|
for (int j = 0; j < this.getEntryCount(); j++) {
|
|
toWrite = column.get(j);
|
|
// If the string contains spaces, it needs to be wrapped in "
|
|
if (toWrite.indexOf(" ") != -1)
|
|
toWrite = "\"" + toWrite + "\"";
|
|
if (toWrite.length() > pad) pad = toWrite.length();
|
|
}
|
|
widths[i] = pad;
|
|
}
|
|
|
|
// The last entry in the array is used for the numbers column
|
|
widths[widths.length - 1] = new Integer(this.getEntryCount()).toString().length();
|
|
}
|
|
|
|
// Write the header and default lines
|
|
fw.write("2DA V2.0" + CRLF);
|
|
if (!defaultValue.equals(""))
|
|
fw.write("DEFAULT: " + defaultValue + CRLF);
|
|
else
|
|
fw.write(CRLF);
|
|
|
|
// Write the labels row using the original case
|
|
for (int i = 0; i < widths[widths.length - 1]; i++) fw.write(" ");
|
|
for (int i = 0; i < realLabels.size(); i++) {
|
|
fw.write(" " + realLabels.get(i));
|
|
for (int j = 0; j < widths[i] - realLabels.get(i).length(); j++) fw.write(" ");
|
|
}
|
|
fw.write((TLKEditCompatible ? " " : "") + CRLF);
|
|
|
|
// Write the data
|
|
for (int i = 0; i < this.getEntryCount(); i++) {
|
|
// Write the number row and it's padding
|
|
if (i == 0 && starOnLine0)
|
|
fw.write("*");
|
|
else
|
|
fw.write("" + i);
|
|
for (int j = 0; j < widths[widths.length - 1] - new Integer(i).toString().length(); j++) fw.write(" ");
|
|
// Loop over columns
|
|
for (int j = 0; j < labels.length; j++) {
|
|
toWrite = mainData.get(labels[j]).get(i);
|
|
// If the string contains spaces, it needs to be wrapped in "
|
|
if (toWrite.indexOf(" ") != -1)
|
|
toWrite = "\"" + toWrite + "\"";
|
|
fw.write(" " + toWrite);
|
|
// Write padding
|
|
for (int k = 0; k < widths[j] - toWrite.length(); k++) fw.write(" ");
|
|
}
|
|
fw.write((TLKEditCompatible ? " " : "") + CRLF);
|
|
|
|
if (verbose) spinner.spin();
|
|
}
|
|
|
|
fw.flush();
|
|
fw.close();
|
|
|
|
if (verbose) System.out.println("- Done");
|
|
}
|
|
|
|
/**
|
|
* Creates a new Data_2da on the 2da file specified.
|
|
*
|
|
* @param filePath path to the 2da file to load
|
|
* @return a Data_2da instance containing the read 2da
|
|
* @throws IllegalArgumentException <code>filePath</code> does not specify a 2da file
|
|
* @throws TwoDAReadException reading the 2da file specified does not succeed,
|
|
* or the file does not contain any data
|
|
*/
|
|
public static Data_2da load2da(String filePath) throws IllegalArgumentException, TwoDAReadException {
|
|
return load2da(filePath, false);
|
|
}
|
|
|
|
/**
|
|
* Creates a new Data_2da on the 2da file specified.
|
|
*
|
|
* @param filePath path to the 2da file to load
|
|
* @param bugCompat if this is <code>true</code>, ignores
|
|
* departures from the 2da spec present in Bioware 2das
|
|
* @return a Data_2da instance containing the read 2da
|
|
* @throws IllegalArgumentException <code>filePath</code> does not specify a 2da file
|
|
* @throws TwoDAReadException reading the 2da file specified does not succeed,
|
|
* or the file does not contain any data
|
|
*/
|
|
public static Data_2da load2da(String filePath, boolean bugCompat) throws IllegalArgumentException, TwoDAReadException {
|
|
Data_2da toReturn;
|
|
Matcher matcher = bugCompat ? bugCompatPattern.matcher("") : pattern.matcher("");
|
|
String name;
|
|
|
|
// Some paranoia checking for bad parameters
|
|
if (!filePath.toLowerCase().endsWith("2da"))
|
|
throw new IllegalArgumentException("Non-2da filename passed to Data_2da: " + filePath);
|
|
|
|
// Create the file handle
|
|
File baseFile = new File(filePath);
|
|
// More paraoia
|
|
if (!baseFile.exists())
|
|
throw new IllegalArgumentException("Nonexistent file passed to Data_2da: " + filePath);
|
|
if (!baseFile.isFile())
|
|
throw new IllegalArgumentException("Nonfile passed to Data_2da: " + filePath);
|
|
|
|
|
|
// Drop the path from the filename
|
|
name = baseFile.getName().substring(0, baseFile.getName().length() - 4);
|
|
//toReturn = new Data_2da(baseFile.getName().substring(0, baseFile.getName().length() - 4));
|
|
|
|
// Tell the user what we are doing
|
|
if (verbose) System.out.print("Reading 2da file: " + name + " ");
|
|
|
|
// Create a Scanner for reading the 2da
|
|
Scanner reader = null;
|
|
try {
|
|
// Fully read the file into a byte array
|
|
RandomAccessFile raf = new RandomAccessFile(baseFile, "r");
|
|
byte[] bytebuf = new byte[(int) raf.length()];
|
|
raf.readFully(bytebuf);
|
|
raf.close();
|
|
//reader = new Scanner(baseFile);
|
|
reader = new Scanner(new String(bytebuf));
|
|
} catch (Exception e) {
|
|
err_pr.println("Error: File operation failed. Aborting.\nException data:\n" + e);
|
|
System.exit(1);
|
|
}
|
|
|
|
try {
|
|
// Check the 2da header
|
|
if (!reader.hasNextLine())
|
|
throw new TwoDAReadException("Empty file: " + name + "!");
|
|
String data = reader.nextLine();
|
|
|
|
if (!(bugCompat ? Pattern.matches("2DA[ \t]V2\\.0\\s*", data) : data.contains("2DA V2.0")))
|
|
throw new TwoDAReadException("2da header missing or invalid: " + name);
|
|
|
|
// Initialise the return object
|
|
toReturn = new Data_2da(name);
|
|
|
|
// Start the actual reading
|
|
try {
|
|
toReturn.createData(reader, matcher, bugCompat);
|
|
} catch (TwoDAReadException e) {
|
|
throw new TwoDAReadException("Exception occurred when reading 2da file: " + toReturn.getName() + "\n" + e.getMessage(), e);
|
|
}
|
|
} finally {
|
|
// Cleanup
|
|
reader.close();
|
|
}
|
|
|
|
if (verbose) System.out.println("- Done");
|
|
return toReturn;
|
|
}
|
|
|
|
/**
|
|
* Reads the data rows from the 2da into the hashmap and
|
|
* does validity checking on the 2da while doing so.
|
|
*
|
|
* @param reader Scanner that the method reads from
|
|
* @param matcher Matcher being used to parse the data read
|
|
* @param bugCompat if this is <code>true</code>, ignores
|
|
* departures from the 2da spec present in Bioware 2das
|
|
*/
|
|
private void createData(Scanner reader, Matcher matcher, boolean bugCompat) {
|
|
Scanner rowParser;
|
|
String data, bugCompat_data;
|
|
boolean bugCompat_MissingDefaultLine = false;
|
|
int line = 0;
|
|
|
|
// Get the default - though it's not used by this implementation, it should not be lost by opening and resaving a file
|
|
if (!reader.hasNextLine())
|
|
throw new TwoDAReadException("No contents after header in 2da file " + name + "!");
|
|
bugCompat_data = data = reader.nextLine();
|
|
matcher.reset(data);
|
|
if (matcher.find()) { // Non-blank default line
|
|
data = matcher.group();
|
|
if (data.trim().equalsIgnoreCase("DEFAULT:")) {
|
|
if (matcher.find())
|
|
this.defaultValue = matcher.group();
|
|
else
|
|
throw new TwoDAReadException("Malformed default line in 2da file " + name + "!");
|
|
} else if (!bugCompat)
|
|
throw new TwoDAReadException("Malformed default line in 2da file " + name + "!");
|
|
else
|
|
bugCompat_MissingDefaultLine = true;
|
|
}
|
|
|
|
// Find the labels row
|
|
if (bugCompat_MissingDefaultLine) // Handle cases where the labels are on the second line in the file instead of 3rd
|
|
data = bugCompat_data;
|
|
else {
|
|
if (!reader.hasNextLine())
|
|
throw new TwoDAReadException("No labels found in 2da file!");
|
|
data = reader.nextLine();
|
|
}
|
|
|
|
// Check for blank lines between the DEFAULT line and labels
|
|
if (data.trim().equals(""))
|
|
if (!bugCompat)
|
|
throw new TwoDAReadException("Labels not present on third line of the file!");
|
|
else
|
|
while (true) {
|
|
if (reader.hasNextLine()) {
|
|
data = reader.nextLine();
|
|
if (!data.trim().equals(""))
|
|
break;
|
|
} else
|
|
throw new TwoDAReadException("No data in 2da file!");
|
|
}
|
|
|
|
// Parse the labels
|
|
String[] localrealLabels = data.trim().split("\\p{javaWhitespace}+");
|
|
String[] labels = new String[localrealLabels.length];
|
|
//System.arraycopy(realLabels, 0, labels, 0, localrealLabels.length);
|
|
|
|
// Create the row containers and the main store
|
|
for (int i = 0; i < labels.length; i++) {
|
|
realLabels.add(localrealLabels[i]);
|
|
labels[i] = localrealLabels[i].toLowerCase();
|
|
mainData.put(labels[i], new ArrayList<String>());
|
|
}
|
|
|
|
// Error if there are empty lines between the header and the data or no lines at all
|
|
if (!reader.hasNextLine())
|
|
if (!bugCompat)
|
|
throw new TwoDAReadException("No data in 2da file!");
|
|
else
|
|
return;
|
|
if ((data = reader.nextLine()).trim().equals(""))
|
|
if (!bugCompat)
|
|
throw new TwoDAReadException("Blank lines following labels row!");
|
|
else
|
|
while (true) {
|
|
if (reader.hasNextLine()) {
|
|
data = reader.nextLine();
|
|
if (!data.trim().equals(""))
|
|
break;
|
|
} else
|
|
return;
|
|
}
|
|
|
|
while (true) {
|
|
//rowParser = new Scanner(data);
|
|
matcher.reset(data);
|
|
matcher.find();
|
|
|
|
// Check for the presence of the row number
|
|
try {
|
|
// Special case - In bugcompatibility mode, 0 can be replaced with *
|
|
if (bugCompat && line == 0 && matcher.group().trim().equals("*")) {
|
|
starOnLine0 = true;
|
|
} else
|
|
line = Integer.parseInt(matcher.group());
|
|
} catch (NumberFormatException e) {
|
|
throw new TwoDAReadException("Numberless 2da line: " + (line + 1));
|
|
}
|
|
|
|
// Start parsing the row
|
|
for (int i = 0; i < labels.length; i++) {
|
|
// Find the next match and check for too short rows
|
|
if (!matcher.find())
|
|
throw new TwoDAReadException("Too short 2da line: " + line);
|
|
|
|
// Get the next element and add it to the data structure
|
|
data = matcher.group();
|
|
// Remove the surrounding quotes if they are present
|
|
if (data.startsWith("\"")) data = data.substring(1, data.length() - 1);
|
|
mainData.get(labels[i]).add(data);
|
|
}
|
|
|
|
// Check for too long rows
|
|
if (matcher.find())
|
|
throw new TwoDAReadException("Too long 2da line: " + line);
|
|
|
|
// Increment the entry counter
|
|
//entries++;
|
|
|
|
/* Get the next line if there is one, or break the loop
|
|
* A bit ugly, but I couldn't figure an easy way of making the loop go right
|
|
* even for 2das with only one row without biggish changes
|
|
*/
|
|
if (reader.hasNextLine()) {
|
|
data = reader.nextLine();
|
|
if (data.trim().equals(""))
|
|
break;
|
|
} else
|
|
break;
|
|
|
|
if (verbose) spinner.spin();
|
|
}
|
|
|
|
// Some validity checking on the 2da. Empty rows allowed only in the end
|
|
if (getNextNonEmptyRow(reader) != null)
|
|
throw new TwoDAReadException("Empty row in the middle of 2da. After row: " + line);
|
|
}
|
|
|
|
/**
|
|
* Reads rows from a Scanner pointed at a 2da file until it finds a
|
|
* row containing non-whitespace characters.
|
|
*
|
|
* @param reader Scanner that the method reads from
|
|
* @return The row found, or null if none were found.
|
|
*/
|
|
private static String getNextNonEmptyRow(Scanner reader) {
|
|
String toReturn = null;
|
|
while (reader.hasNextLine()) {
|
|
toReturn = reader.nextLine();
|
|
if (!toReturn.trim().equals(""))
|
|
break;
|
|
}
|
|
|
|
if (toReturn == null || toReturn.trim().equals(""))
|
|
return null;
|
|
|
|
return toReturn;
|
|
}
|
|
|
|
/**
|
|
* Get the list of column labels in this 2da.
|
|
*
|
|
* @return an array of Strings containing the column labels
|
|
*/
|
|
public String[] getLabels() {
|
|
// For some reason, it won't let me cast the keyset directly into a String[]
|
|
// return (String[])(mainData.keySet().toArray());
|
|
Object[] temp = mainData.keySet().toArray();
|
|
String[] toReturn = new String[temp.length];
|
|
for (int i = 0; i < temp.length; i++) toReturn[i] = (String) temp[i];
|
|
/*String[] toReturn = (String[])mainData.keySet().toArray();*/
|
|
return toReturn;
|
|
}
|
|
|
|
/**
|
|
* Get the 2da entry on the given row and column
|
|
*
|
|
* @param label the label of the column to get
|
|
* @param row the number of the row to get, as string
|
|
* @return int represeting the 2da entry or <code>null</code> if the column does not exist
|
|
* if the column is **** then 0 will be returned
|
|
* @throws NumberFormatException if <code>row</code> cannot be converted to an integer
|
|
*/
|
|
public int getBiowareEntryAsInt(String label, String row) {
|
|
String returnString = this.getEntry(label, Integer.parseInt(row));
|
|
if (returnString.equals("****"))
|
|
return 0;
|
|
else if (returnString.matches("\\D")) //check its a number
|
|
return 0;
|
|
return Integer.decode(returnString);
|
|
}
|
|
|
|
/**
|
|
* Get the 2da entry on the given row and column
|
|
*
|
|
* @param label the label of the column to get
|
|
* @param row the number of the row to get
|
|
* @return String represeting the 2da entry or <code>null</code> if the column does not exist
|
|
* if the column is **** then a zero length string will be returned
|
|
*/
|
|
public int getBiowareEntryAsInt(String label, int row) {
|
|
ArrayList<String> column = mainData.get(label.toLowerCase());
|
|
String returnString = column != null ? column.get(row) : null;
|
|
if (returnString.equals("****"))
|
|
return 0;
|
|
else if (returnString.matches("\\D")) //check its a number
|
|
return 0;
|
|
return Integer.decode(returnString);
|
|
}
|
|
|
|
/**
|
|
* Get the 2da entry on the given row and column
|
|
*
|
|
* @param label the label of the column to get
|
|
* @param row the number of the row to get, as string
|
|
* @return String represeting the 2da entry or <code>null</code> if the column does not exist
|
|
* if the column is **** then a zero length string will be returned
|
|
* @throws NumberFormatException if <code>row</code> cannot be converted to an integer
|
|
*/
|
|
public String getBiowareEntry(String label, String row) {
|
|
String returnString = this.getEntry(label, Integer.parseInt(row));
|
|
if (returnString.equals("****"))
|
|
return "";
|
|
return returnString;
|
|
}
|
|
|
|
/**
|
|
* Get the 2da entry on the given row and column
|
|
*
|
|
* @param label the label of the column to get
|
|
* @param row the number of the row to get
|
|
* @return String represeting the 2da entry or <code>null</code> if the column does not exist
|
|
* if the column is **** then a zero length string will be returned
|
|
*/
|
|
public String getBiowareEntry(String label, int row) {
|
|
ArrayList<String> column = mainData.get(label.toLowerCase());
|
|
String returnString = column != null ? column.get(row) : null;
|
|
if (returnString.equals("****"))
|
|
return "";
|
|
return returnString;
|
|
}
|
|
|
|
/**
|
|
* Get the 2da entry on the given row and column
|
|
*
|
|
* @param label the label of the column to get
|
|
* @param row the number of the row to get, as string
|
|
* @return String represeting the 2da entry or <code>null</code> if the column does not exist
|
|
* @throws NumberFormatException if <code>row</code> cannot be converted to an integer
|
|
*/
|
|
public String getEntry(String label, String row) {
|
|
return this.getEntry(label, Integer.parseInt(row));
|
|
}
|
|
|
|
/**
|
|
* Get the 2da entry on the given row and column
|
|
*
|
|
* @param label the label of the column to get
|
|
* @param row the number of the row to get
|
|
* @return String represeting the 2da entry or <code>null</code> if the column does not exist
|
|
*/
|
|
public String getEntry(String label, int row) {
|
|
ArrayList<String> column = mainData.get(label.toLowerCase());
|
|
return column != null ? column.get(row) : null;
|
|
}
|
|
|
|
/**
|
|
* Get number of entries in this 2da. Works by returning the size of one of the columns in the 2da.
|
|
*
|
|
* @return integer equal to the number of entries in this 2da
|
|
*/
|
|
public int getEntryCount() {
|
|
if (mainData.entrySet().size() == 0) {
|
|
return 0;
|
|
}
|
|
return mainData.entrySet().iterator().next().getValue().size();
|
|
}
|
|
|
|
/**
|
|
* Get the name of this 2da
|
|
*
|
|
* @return String representing this 2da's name
|
|
*/
|
|
public String getName() {
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
* Sets the 2da entry on the given row and column
|
|
*
|
|
* @param label the label of the column to get
|
|
* @param row the number of the row to get, as string
|
|
* @param entry the new contents of the entry. If this is null or empty, it is replaced with ****
|
|
* @throws NumberFormatException if <code>row</code> cannot be converted to an integer
|
|
*/
|
|
public void setEntry(String label, String row, String entry) {
|
|
this.setEntry(label, Integer.parseInt(row), entry);
|
|
}
|
|
|
|
/**
|
|
* Sets the 2da entry on the given row and column
|
|
*
|
|
* @param label the label of the column to get
|
|
* @param row the number of the row to get, as string
|
|
* @param entry the new contents of the entry. If this is null or empty, it is replaced with ****
|
|
* or with the default value if that is set
|
|
*/
|
|
public void setEntry(String label, int row, String entry) {
|
|
if (entry == null || entry.equals(""))
|
|
if (defaultValue.equals(""))
|
|
entry = "****";
|
|
else
|
|
entry = defaultValue;
|
|
mainData.get(label.toLowerCase()).set(row, entry);
|
|
}
|
|
|
|
/**
|
|
* Returns the contents of the requested row as a string array. The order the columns are
|
|
* taken is the same as the order of labels from getLabels().
|
|
*
|
|
* @param index the index of the row to get
|
|
* @return an array of strings containing the elements in the r
|
|
* @throws NumberFormatException if <code>index</code> cannot be converted to an integer
|
|
*/
|
|
public String[] getRow(String index) {
|
|
return getRow(Integer.parseInt(index));
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the contents of the requested row as a string array. The order the columns are
|
|
* taken is the same as the order of labels from getLabels().
|
|
*
|
|
* @param index the index of the row to get
|
|
* @return an array of strings containing the elements in the row
|
|
*/
|
|
public String[] getRow(int index) {
|
|
String[] labels = this.getLabels();
|
|
String[] toReturn = new String[labels.length];
|
|
|
|
for (int i = 0; i < labels.length; i++) {
|
|
toReturn[i] = mainData.get(labels[i]).get(index);
|
|
}
|
|
|
|
return toReturn;
|
|
}
|
|
|
|
/**
|
|
* Appends a new, empty row to the end of the 2da file, with entries defaulting to the
|
|
* default value or if that is not set, ****
|
|
*/
|
|
public void appendRow() {
|
|
String[] labels = this.getLabels();
|
|
|
|
for (String label : labels) {
|
|
if (defaultValue.equals(""))
|
|
mainData.get(label).add("****");
|
|
else
|
|
mainData.get(label).add(defaultValue);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Appends a new, empty row to the end of the 2da file. The new row will be filled with the values
|
|
* given as parameter.
|
|
*
|
|
* @param data the strings that will be used to fill the new row
|
|
* @throws IllegalArgumentException if the number of elements in <code>data</code> array is not
|
|
* same as number of columns in the 2da
|
|
*/
|
|
public void appendRow(String[] data) {
|
|
String[] labels = this.getLabels();
|
|
|
|
// Sanity check
|
|
if (labels.length != data.length)
|
|
throw new IllegalArgumentException("Differing column width when attempting to insert row");
|
|
|
|
for (int i = 0; i < labels.length; i++) {
|
|
mainData.get(labels[i]).add(data[i]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inserts a new row into the given index in the 2da file. The row currently at the index and all
|
|
* subsequent rows have their index increased by one. The new row will be filled with the values
|
|
* given as parameter.
|
|
*
|
|
* @param index the index where the new row will be located
|
|
* @param data the strings that will be used to fill the new row
|
|
* @throws IllegalArgumentException if the number of elements in <code>data</code> array is not
|
|
* same as number of columns in the 2da
|
|
* @throws NumberFormatException if <code>index</code> cannot be converted to an integer
|
|
*/
|
|
public void insertRow(String index, String[] data) {
|
|
insertRow(Integer.parseInt(index), data);
|
|
}
|
|
|
|
/**
|
|
* Inserts a new row into the given index in the 2da file. The row currently at the index and all
|
|
* subsequent rows have their index increased by one. The new row will be filled with the values
|
|
* given as parameter.
|
|
*
|
|
* @param index the index where the new row will be located
|
|
* @param data the strings that will be used to fill the new row
|
|
* @throws IllegalArgumentException if the number of elements in <code>data</code> array is not
|
|
* same as number of columns in the 2da
|
|
*/
|
|
public void insertRow(int index, String[] data) {
|
|
String[] labels = this.getLabels();
|
|
|
|
// Sanity check
|
|
if (labels.length != data.length)
|
|
throw new IllegalArgumentException("Differing column width when attempting to insert row");
|
|
|
|
for (int i = 0; i < labels.length; i++) {
|
|
mainData.get(labels[i]).add(index, data[i]);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Adds a new, empty row to the given index in the 2da file. The row currently at the index and all
|
|
* subsequent rows have their index increased by one.
|
|
* The entries default to ****.
|
|
*
|
|
* @param index the index where the new row will be located
|
|
* @throws NumberFormatException if <code>index</code> cannot be converted to an integer
|
|
*/
|
|
public void insertRow(String index) {
|
|
insertRow(Integer.parseInt(index));
|
|
}
|
|
|
|
/**
|
|
* Adds a new, empty row to the given index in the 2da file. The row currently at the index and all
|
|
* subsequent rows have their index increased by one.
|
|
* The entries default to default value or if that is not set, ****.
|
|
*
|
|
* @param index the index where the new row will be located
|
|
*/
|
|
public void insertRow(int index) {
|
|
String[] labels = this.getLabels();
|
|
|
|
for (String label : labels) {
|
|
if (defaultValue.equals(""))
|
|
mainData.get(label).add(index, "****");
|
|
else
|
|
mainData.get(label).add(index, defaultValue);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the row at the given index. All subsequent rows have their indexed shifted down by one.
|
|
*
|
|
* @param index the index of the row to remove
|
|
* @throws NumberFormatException if <code>index</code> cannot be converted to an integer
|
|
*/
|
|
public void removeRow(String index) {
|
|
removeRow(Integer.parseInt(index));
|
|
}
|
|
|
|
/**
|
|
* Removes the row at the given index. All subsequent rows have their indexed shifted down by one.
|
|
*
|
|
* @param index the index of the row to remove
|
|
*/
|
|
public void removeRow(int index) {
|
|
String[] labels = this.getLabels();
|
|
|
|
for (String label : labels) {
|
|
mainData.get(label).remove(index);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a new column to the 2da file. The new column will be the last in the file.
|
|
*
|
|
* @param label the name of the column to add
|
|
*/
|
|
public void addColumn(String label) {
|
|
ArrayList<String> column = new ArrayList<String>();
|
|
mainData.put(label.toLowerCase(), column);
|
|
realLabels.add(label);
|
|
|
|
if (defaultValue.equals("")) {
|
|
for (int i = 0; i < this.getEntryCount(); i++) {
|
|
column.add("****");
|
|
}
|
|
} else {
|
|
for (int i = 0; i < this.getEntryCount(); i++) {
|
|
column.add(defaultValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the column with the given label from the 2da.
|
|
*
|
|
* @param label the name of the column to remove
|
|
*/
|
|
public void removeColumn(String label) {
|
|
mainData.remove(label);
|
|
realLabels.remove(label);
|
|
}
|
|
|
|
|
|
/**
|
|
* The main method, as usual
|
|
*
|
|
* @param args
|
|
*/
|
|
public static void main(String[] args) {
|
|
if (args.length == 0) readMe();
|
|
List<String> fileNames = new ArrayList<String>();
|
|
boolean compare = false;
|
|
boolean resave = false;
|
|
boolean minimal = false;
|
|
boolean ignoreErrors = false;
|
|
boolean readStdin = false;
|
|
boolean bugCompat = false;
|
|
|
|
for (String param : args) {//[-bcrmnqs] file... | -
|
|
// Parameter parseage
|
|
if (param.startsWith("-")) {
|
|
if (param.equals("-"))
|
|
readStdin = true;
|
|
else if (param.equals("--help")) readMe();
|
|
else {
|
|
for (char c : param.substring(1).toCharArray()) {
|
|
switch (c) {
|
|
case 'b':
|
|
bugCompat = true;
|
|
break;
|
|
case 'c':
|
|
compare = true;
|
|
if (resave) resave = false;
|
|
break;
|
|
case 'r':
|
|
resave = true;
|
|
if (compare) compare = false;
|
|
break;
|
|
case 'm':
|
|
minimal = true;
|
|
break;
|
|
case 'n':
|
|
ignoreErrors = true;
|
|
break;
|
|
case 'q':
|
|
verbose = false;
|
|
break;
|
|
case 's':
|
|
spinner.disable();
|
|
break;
|
|
default:
|
|
err_pr.println("Error: Unknown parameter: " + c);
|
|
readMe();
|
|
}
|
|
}
|
|
}
|
|
} else
|
|
// It's a filename
|
|
fileNames.add(param);
|
|
}
|
|
|
|
// Read files from stdin if specified
|
|
if (readStdin) {
|
|
Scanner scan = new Scanner(System.in);
|
|
String s;
|
|
while (scan.hasNextLine()) {
|
|
s = scan.nextLine();
|
|
if (s.charAt(0) == '"' && s.charAt(s.length() - 1) == '"')
|
|
s = s.substring(1, s.length() - 1);
|
|
fileNames.add(s);
|
|
}
|
|
}
|
|
|
|
// Run the specified operation
|
|
if (compare) {
|
|
Data_2da file1, file2;
|
|
file1 = load2da(args[1], bugCompat);
|
|
file2 = load2da(args[2], bugCompat);
|
|
|
|
doComparison(file1, file2);
|
|
} else if (resave) {
|
|
Data_2da temp;
|
|
for (String fileName : fileNames) {
|
|
try {
|
|
temp = load2da(fileName, bugCompat);
|
|
temp.save2da(new File(fileName).getCanonicalFile().getParent() + File.separator, true, !minimal);
|
|
} catch (Exception e) {
|
|
// Print the error
|
|
err_pr.printException(e);
|
|
// If ignoring errors, and this error is of expected type, continue
|
|
if (e instanceof IllegalArgumentException ||
|
|
e instanceof TwoDAReadException ||
|
|
e instanceof IOException)
|
|
if (ignoreErrors)
|
|
continue;
|
|
System.exit(1);
|
|
}
|
|
}
|
|
} else {
|
|
// Validify by loading
|
|
for (String fileName : fileNames) {
|
|
try {
|
|
load2da(fileName, bugCompat);
|
|
} catch (Exception e) {
|
|
// Print the error
|
|
err_pr.printException(e);
|
|
// If ignoring errors, and this error is of expected type, continue
|
|
if (e instanceof IllegalArgumentException || e instanceof TwoDAReadException)
|
|
if (ignoreErrors)
|
|
continue;
|
|
System.exit(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void readMe() {
|
|
System.out.println("Usage:\n" +
|
|
" [-bcrmnqs] file... | -\n" +
|
|
"\n" +
|
|
" -b bug-compatibility mode. Counts tabs as whitespace instead of data\n" +
|
|
" -c prints the differing lines between the 2das given as first two\n" +
|
|
" parameters. They must have the same label set and entrycount.\n" +
|
|
" Mutually exclusive with -r\n" +
|
|
" -r resaves the 2das given as parameters. Mutually exclusive with -c\n" +
|
|
" -m saves the files with minimal spaces. Only relevant when resaving\n" +
|
|
" -n ignores errors that occur during validity testing and resaving,\n" +
|
|
" just skips to the next file\n" +
|
|
" -q quiet mode\n" +
|
|
" -s no spinner\n" +
|
|
" - a line given as a lone parameter means that the list of files is\n" +
|
|
" read from stdin in addition to the ones passed from command line.\n" +
|
|
" The list passed in such manner should contain one filename per line\n" +
|
|
"\n" +
|
|
" --help prints this text\n" +
|
|
"\n" +
|
|
"\n" +
|
|
"if neither -c or -r is specified, performs validity testing on the given files"
|
|
);
|
|
System.exit(0);
|
|
}
|
|
|
|
/**
|
|
* Compares the given two 2da files and prints differences it finds
|
|
* Differing number of rows, or row names will cause comparison to abort.
|
|
*
|
|
* @param file1 Data_2da containing one of the files to be compared
|
|
* @param file2 Data_2da containing the other file to be compared
|
|
*/
|
|
public static void doComparison(Data_2da file1, Data_2da file2) {
|
|
// Check labels
|
|
String[] labels1 = file1.getLabels(),
|
|
labels2 = file2.getLabels();
|
|
if (labels1.length != labels2.length) {
|
|
System.out.println("Differing amount of row labels\n" +
|
|
file1.getName() + ": " + labels1.length + "\n" +
|
|
file2.getName() + ": " + labels2.length);
|
|
return;
|
|
}
|
|
for (int i = 0; i < labels1.length; i++) {
|
|
if (!labels1[i].equals(labels2[i])) {
|
|
System.out.println("Differing labels");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check lengths
|
|
int shortCount = file1.getEntryCount();
|
|
if (file1.getEntryCount() != file2.getEntryCount()) {
|
|
System.out.println("Differing line counts.\n" +
|
|
file1.getName() + ": " + file1.getEntryCount() + "\n" +
|
|
file2.getName() + ": " + file2.getEntryCount());
|
|
|
|
shortCount = shortCount > file2.getEntryCount() ? file2.getEntryCount() : shortCount;
|
|
}
|
|
|
|
// Check elements
|
|
for (int i = 0; i < shortCount; i++) {
|
|
for (String label : labels1) {
|
|
if (!file1.getEntry(label, i).equals(file2.getEntry(label, i))) {
|
|
System.out.println("Differing entries on row " + i + ", column " + label + "\n" +
|
|
file1.getName() + ": " + file1.getEntry(label, i) + "\n" +
|
|
file2.getName() + ": " + file2.getEntry(label, i));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public String toString() {
|
|
return this.name + "(" + this.getEntryCount() + " entries)";
|
|
}
|
|
|
|
/**
|
|
* @see java.lang.Object#toString()
|
|
*/
|
|
public String toOutputString() {
|
|
String CRLF = "\r\n";
|
|
StringBuffer toReturn = new StringBuffer();
|
|
boolean evenColumns = true;
|
|
String[] labels = this.getLabels();
|
|
String toWrite;
|
|
|
|
// Get the amount of padding used, if any
|
|
int[] widths = new int[labels.length + 1];// All initialised to 0
|
|
ArrayList<String> column;
|
|
int pad;
|
|
// Loop over columns
|
|
for (int i = 0; i < labels.length; i++) {
|
|
pad = labels[i].length();
|
|
column = mainData.get(labels[i]);
|
|
// Loop over rows
|
|
for (int j = 0; j < this.getEntryCount(); j++) {
|
|
toWrite = column.get(j);
|
|
// If the string contains spaces, it needs to be wrapped in "
|
|
if (toWrite.indexOf(" ") != -1)
|
|
toWrite = "\"" + toWrite + "\"";
|
|
if (toWrite.length() > pad) pad = toWrite.length();
|
|
}
|
|
widths[i] = pad;
|
|
}
|
|
|
|
// The last entry in the array is used for the numbers column
|
|
widths[widths.length - 1] = new Integer(this.getEntryCount()).toString().length();
|
|
|
|
// Write the header and default lines
|
|
toReturn.append("2DA V2.0" + CRLF);
|
|
if (!defaultValue.equals(""))
|
|
toReturn.append("DEFAULT: " + defaultValue + CRLF);
|
|
else
|
|
toReturn.append(CRLF);
|
|
|
|
// Write the labels row using the original case
|
|
for (int i = 0; i < widths[widths.length - 1]; i++) toReturn.append(" ");
|
|
for (int i = 0; i < realLabels.size(); i++) {
|
|
toReturn.append(" " + realLabels.get(i));
|
|
for (int j = 0; j < widths[i] - realLabels.get(i).length(); j++) toReturn.append(" ");
|
|
}
|
|
toReturn.append((TLKEditCompatible ? " " : "") + CRLF);
|
|
|
|
// Write the data
|
|
for (int i = 0; i < this.getEntryCount(); i++) {
|
|
// Write the number row and it's padding
|
|
toReturn.append("" + i);
|
|
for (int j = 0; j < widths[widths.length - 1] - new Integer(i).toString().length(); j++)
|
|
toReturn.append(" ");
|
|
// Loop over columns
|
|
for (int j = 0; j < labels.length; j++) {
|
|
toWrite = mainData.get(labels[j]).get(i);
|
|
// If the string contains spaces, it needs to be wrapped in "
|
|
if (toWrite.indexOf(" ") != -1)
|
|
toWrite = "\"" + toWrite + "\"";
|
|
toReturn.append(" " + toWrite);
|
|
// Write padding
|
|
for (int k = 0; k < widths[j] - toWrite.length(); k++) toReturn.append(" ");
|
|
}
|
|
toReturn.append((TLKEditCompatible ? " " : "") + CRLF);
|
|
}
|
|
|
|
return toReturn.toString();
|
|
}
|
|
|
|
/**
|
|
* Makes an independent copy of this 2da.
|
|
*
|
|
* @see java.lang.Object#clone()
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
public Object clone() {
|
|
// Make a sufficiently deep copy of the main data arrays
|
|
LinkedHashMap<String, ArrayList<String>> cloneData = new LinkedHashMap<String, ArrayList<String>>();
|
|
for (String key : this.getLabels()) // Use real labels to preserve order
|
|
cloneData.put(key, (ArrayList<String>) this.mainData.get(key).clone());
|
|
|
|
|
|
// Create a new Data_2da. The Strings are immutable, so they can be used as-is and clone()
|
|
// on an array produces a sufficiently deep copy right away
|
|
return new Data_2da(
|
|
this.name,
|
|
this.defaultValue,
|
|
(ArrayList<String>) this.realLabels.clone(),
|
|
cloneData
|
|
);
|
|
}
|
|
} |