2024-06-21 19:37:17 -05:00

274 lines
10 KiB
Java

package prc.autodoc;
import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.stream.IntStream;
import java.util.HashMap;
import static prc.Main.spinner;
import static prc.Main.verbose;
/**
* This class forms an interface for accessing TLK files in the
* PRC automated manual generator.
*/
public class Data_TLK {
private final HashMap<Integer, String> mainData = new HashMap<Integer, String>();
private int highestEntry = 0;
/**
* Creates a new Data_TLK on the TLK file specified.
*
* @param filePath The path of the TLK file to be loaded
* @throws IllegalArgumentException <code>filePath</code> does not filePath a TLK file
* @throws TLKReadException reading the TLK file specified does not succeed
*/
public Data_TLK(String filePath) {
// Some paranoia checking for bad parameters
if (!filePath.toLowerCase().endsWith("tlk"))
throw new IllegalArgumentException("Non-tlk filename passed to Data_TLK: " + filePath);
File baseFile = new File(filePath);
if (!baseFile.exists())
throw new IllegalArgumentException("Nonexistent file passed to Data_TLK: " + filePath);
if (!baseFile.isFile())
throw new IllegalArgumentException("Nonfile passed to Data_TLK: " + filePath);
// Create a RandomAccessFile for reading the TLK. Read-only
MappedByteBuffer reader = null;
RandomAccessFile fileReader;
try {
fileReader = new RandomAccessFile(baseFile, "r");
reader = fileReader.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, fileReader.length());
} catch (IOException e) {
throw new TLKReadException("Cannot access TLK file: " + filePath, e);
}
byte[] bytes4 = new byte[4],
bytes8 = new byte[8];
// Drop the path from the filename
String fileName = baseFile.getName();
// Tell the user what we are doing
if (verbose) System.out.print("Reading TLK file: " + fileName + " ");
try {
// Check the header
reader.get(bytes4);
if (!new String(bytes4).equals("TLK "))
throw new TLKReadException("Wrong file type field in: " + fileName);
// Check the version
reader.get(bytes4);
if (!new String(bytes4).equals("V3.0"))
throw new TLKReadException("Wrong TLK version number in: " + fileName);
// Skip the language ID
reader.position(reader.position() + 4);
// Read the entrycount
int stringCount = readLittleEndianInt(reader, bytes4);
int stringOffset = readLittleEndianInt(reader, bytes4);
// Read the entry lengths
int[] stringLengths = readStringLengths(reader, stringCount);
// Read the strings themselves
readStrings(reader, stringLengths, stringOffset);
// Store the highest string for writing back later
highestEntry = stringLengths.length;
} catch (IOException e) {
throw new TLKReadException("IOException while reading TLK file: " + fileName, e);
} finally {
try {
fileReader.close();
} catch (IOException e) {
// No idea under what conditions closing a file could fail and not cause an Error to be thrown...
e.printStackTrace();
}
}
if (verbose) System.out.println("- Done");
}
/**
* Get the given TLK entry.
*
* @param strRef the number of the entry to get
* @return the entry string or "Bad StrRef" if the entry wasn't in the TLK
*/
public String getEntry(int strRef) {
if (strRef > 0x01000000) strRef -= 0x01000000;
String toReturn = mainData.get(strRef);
if (toReturn == null) toReturn = Main.badStrRef;
return toReturn;
}
/**
* Get the given TLK entry.
*
* @param strRef the number of the entry to get as a string
* @return the entry string or "Bad StrRef" if the entry wasn't in the TLK
* @throws NumberFormatException if <code>strRef</code> cannot be converted to an integer
*/
public String getEntry(String strRef) {
try {
return getEntry(Integer.parseInt(strRef));
} catch (NumberFormatException e) {
return Main.badStrRef;
}
}
/**
* Set the given TLK entry.
*
* @param strRef the number of the entry to set
* @param value the value of the entry to set
*/
public void setEntry(int strRef, String value) {
if (strRef > 0x01000000) strRef -= 0x01000000;
mainData.put(strRef, value);
if (strRef > highestEntry)
highestEntry = strRef;
}
/**
* Saves the tlk file to the given XML.
*
* @param name the name of the resulting file, without extensions
* @param path the path to the directory to save the file to
* @param allowOverWrite Whether to allow overwriting existing files
* @throws IOException if cannot overwrite, or the underlying IO throws one
*/
public void saveAsXML(String name, String path, boolean allowOverWrite) throws IOException {
if (path == null || path.equals(""))
path = "." + File.separator;
if (!path.endsWith(File.separator))
path += File.separator;
File file = new File(path + name + ".tlk.xml");
if (file.exists() && !allowOverWrite)
throw new IOException("File exists already: " + file.getAbsolutePath());
// Inform user
if (verbose) System.out.print("Saving tlk file: " + name + " ");
PrintWriter writer = new PrintWriter(file);
//write the header
writer.println("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>");
writer.println("<!DOCTYPE tlk SYSTEM \"tlk2xml.dtd\">");
writer.println("<tlk>");
//loop over each row and write it
for (int row = 0; row < highestEntry; row++) {
String data = mainData.get(row);
if (data != null) {
//replace with paired characters
data = data.replace("&", "&amp;"); //this must be before the others
data = data.replace("<", "&lt;");
data = data.replace(">", "&gt;");
writer.println(" <entry id=\"" + row + "\" lang=\"en\" sex=\"m\">" + data + "</entry>");
}
if (verbose) spinner.spin();
}
//write the footer
writer.println("</tlk>");
writer.flush();
writer.close();
if (verbose) System.out.println("- Done");
}
/**
* Reads the string lengths from this TLK's string data elements.
*
* @param reader RandomAccessFile read from
* @param stringCount number of strings in the TLK
* @return an array of integers containing the lengths of the strings in this TLK
* @throws IOException if there is an error while reading from <code>reader</code>
*/
private int[] readStringLengths(MappedByteBuffer reader, int stringCount) throws IOException {
int[] toReturn = new int[stringCount];
byte[] bytes4 = new byte[4];
int curOffset = 20; // The number of bytes in the TLK header section
for (int i = 0; i < stringCount; i++) {
// Skip everything up to the length
curOffset += 32;
reader.position(curOffset);
// Read the value
toReturn[i] = readLittleEndianInt(reader, bytes4);
// Skip to the end of the record
curOffset += 8;
if (verbose) spinner.spin();
}
return toReturn;
}
/**
* Reads the strings from the TLK into the hashmap.
*
* @param reader RandomAccessFile read from
* @param stringLengths an array of integers containing the lengths of the strings in this TLK
* @param curOffset the offset to start reading from in the file
* @throws IOException if there is an error while reading from <code>reader</code>
*/
private void readStrings(MappedByteBuffer reader, int[] stringLengths, int curOffset) throws IOException {
StringBuffer buffer = new StringBuffer(200);
reader.position(curOffset);
for (int i = 0; i < stringLengths.length; i++) {
if (stringLengths[i] > 0) {
// Read the specified number of bytes, convert them into chars
// and put them in the buffer
for (int j = 0; j < stringLengths[i]; j++)
buffer.append((char) (reader.get() & 0xff));
// Store the buffer contents
mainData.put(i, buffer.toString());
// Wipe the buffer for next round
buffer.delete(0, buffer.length());
}
if (verbose) spinner.spin();
}
}
/**
* Reads the next 4 bytes into the given array from the TLK and then
* writes them into an integer in inverse order.
*
* @param reader RandomAccessFile read from
* @param readArray array of bytes read to. For efficiency of not having to create a new array every time
* @return integer read
* @throws IOException if there is an error while reading from <code>reader</code>
*/
private int readLittleEndianInt(MappedByteBuffer reader, byte[] readArray) throws IOException {
int toReturn = 0;
reader.get(readArray);
for (int i = readArray.length - 1; i >= 0; i--) {
// What's missing here is the implicit promotion of readArray[i] to
// int. A byte is a signed element, and as such, has max value of 0x7f.
toReturn = (toReturn << 8) | readArray[i] & 0xff;
}
return toReturn;
}
/**
* The main method, as usual
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Data_TLK test = new Data_TLK(args[0]);
}
}