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 mainData = new HashMap(); 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 filePath 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 strRef 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(""); writer.println(""); writer.println(""); //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("&", "&"); //this must be before the others data = data.replace("<", "<"); data = data.replace(">", ">"); writer.println(" " + data + ""); } if (verbose) spinner.spin(); } //write the footer writer.println(""); 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 reader */ 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 reader */ 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 reader */ 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]); } }