diff --git a/pk3DS/Main.cs b/pk3DS/Main.cs index 9c5e454..63f9a5e 100644 --- a/pk3DS/Main.cs +++ b/pk3DS/Main.cs @@ -985,11 +985,11 @@ public void restoreGARCs(bool oras_define, params string[] g) // Text Requests internal static string[] getText(int file) { - return xytext.getStringsFromFile("gametext" + Path.DirectorySeparatorChar + file.ToString("000") + ".bin"); + return TextFile.getStrings("gametext" + Path.DirectorySeparatorChar + file.ToString("000") + ".bin"); } internal static bool setText(int file, string[] strings) { - byte[] data = xytext.getBytesForFile(strings); + byte[] data = TextFile.getBytes(strings); string path = "gametext" + Path.DirectorySeparatorChar + file.ToString("000") + ".bin"; File.WriteAllBytes(path, data); return true; diff --git a/pk3DS/Subforms/xytext.cs b/pk3DS/Subforms/xytext.cs index 728b728..9b27bdc 100644 --- a/pk3DS/Subforms/xytext.cs +++ b/pk3DS/Subforms/xytext.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using System.Windows.Forms; @@ -54,7 +53,7 @@ private void exportTextFile(string fileName, bool newline) for (int i = 0; i < files.Length; i++) { // Get Strings for the File - string[] data = getStringsFromFile(files[i]); + string[] data = TextFile.getStrings(files[i]); // Append the File Header tw.WriteLine("~~~~~~~~~~~~~~~"); tw.WriteLine("Text File : " + i); @@ -87,15 +86,15 @@ private bool importTextFile(string fileName) for (int i = 0; i < fileText.Length; i++) { string line = fileText[i]; - if (line != "~~~~~~~~~~~~~~~") - continue; - string[] brokenLine = fileText[i++ + 1].Split(new[] {" : "}, StringSplitOptions.None); - if (brokenLine.Length != 2) - { Util.Error($"Invalid Line @ {i}, expected Text File : {ctr}"); return false; } + if (line != "~~~~~~~~~~~~~~~") + continue; + string[] brokenLine = fileText[i++ + 1].Split(new[] { " : " }, StringSplitOptions.None); + if (brokenLine.Length != 2) + { Util.Error($"Invalid Line @ {i}, expected Text File : {ctr}"); return false; } int file = Util.ToInt32(brokenLine[1]); - if (file != ctr) - { Util.Error($"Invalid Line @ {i}, expected Text File : {ctr}"); return false; } - i+=2; // Skip over the other header line + if (file != ctr) + { Util.Error($"Invalid Line @ {i}, expected Text File : {ctr}"); return false; } + i += 2; // Skip over the other header line List Lines = new List(); while (i < fileText.Length && fileText[i] != "~~~~~~~~~~~~~~~") { @@ -116,11 +115,9 @@ private bool importTextFile(string fileName) "When exporting text, do not remove newline formatting."); return false; } // All Text Lines received. Store all back. - try { - for (int i = 0; i < files.Length; i++) - File.WriteAllBytes(files[i], getBytesForFile(textLines[i])); - } - catch { return false; } + for (int i = 0; i < files.Length; i++) + try { File.WriteAllBytes(files[i], TextFile.getBytes(textLines[i])); } + catch (Exception e) { Util.Error($"The input Text File (# {i}) failed to convert:", e.ToString()); return false; } return true; } private void changeEntry(object sender, EventArgs e) @@ -128,15 +125,18 @@ private void changeEntry(object sender, EventArgs e) // Save All the old text if (entry > -1 && sender != null) { - byte[] bin = getBytesForFile(getCurrentDGLines()); - if (bin != null) + try + { + byte[] bin = TextFile.getBytes(getCurrentDGLines()); File.WriteAllBytes(files[entry], bin); + } + catch (Exception ex) { Util.Error(ex.ToString()); } } // Reset entry = CB_Entry.SelectedIndex; string file = files[entry]; - string[] data = getStringsFromFile(file); + string[] data = TextFile.getStrings(file); setStringsDataGridView(data); } // Main Handling @@ -144,13 +144,10 @@ private void setStringsDataGridView(string[] textArray) { // Clear the datagrid row content to remove all text lines. dgv.Rows.Clear(); - - if (textArray == null) // Error handling for bad inputs. - return; - // Clear the header columns, these are repopulated every time. dgv.Columns.Clear(); - + if (textArray == null || textArray.Length == 0) + return; // Reset settings and columns. dgv.AllowUserToResizeColumns = false; DataGridViewColumn dgvLine = new DataGridViewTextBoxColumn @@ -170,12 +167,9 @@ private void setStringsDataGridView(string[] textArray) SortMode = DataGridViewColumnSortMode.NotSortable, AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill }; - - // Re-add the columns. + dgv.Columns.Add(dgvLine); dgv.Columns.Add(dgvText); - - // Add empty rows equal to how many entries. dgv.Rows.Add(textArray.Length); // Add the text lines into their cells. @@ -231,431 +225,10 @@ private void B_RemoveLine_Click(object sender, EventArgs e) for (int i = 0; i < dgv.Rows.Count; i++) dgv.Rows[i].Cells[0].Value = i.ToString(); } - - // Top Level Functions - internal static string[] getStringsFromFile(string path) - { - byte[] data = File.ReadAllBytes(path); - ushort textSections = BitConverter.ToUInt16(data, 0); - ushort lineCount = BitConverter.ToUInt16(data, 2); - if (lineCount == 0) return null; - uint totalLength = BitConverter.ToUInt32(data, 4); - uint initialKey = BitConverter.ToUInt32(data, 8); - int sectionData = BitConverter.ToInt32(data, 12); - - try // Some sanity checking to prevent errors. - { - if (initialKey != 0) throw new Exception("Invalid initial key! Not 0?"); - if (sectionData + totalLength != data.Length || textSections != 1) throw new Exception("Invalid Text File"); - - uint sectionLength = BitConverter.ToUInt32(data, sectionData); - if (sectionLength != totalLength) throw new Exception("Section size and overall size do not match."); - } - catch { return null; } - - // Prep result storage. - ushort key = 0x7C89; - string[] result = new string[lineCount]; - - for (int i = 0; i < lineCount; i++) - { - // Init - ushort k = key; - string s = ""; - int offset = BitConverter.ToInt32(data, i * 8 + sectionData + 4) + sectionData; - int length = BitConverter.ToInt16(data, i * 8 + sectionData + 8); - int start = offset; - ushort c = 0; // u16 char - - while (offset < start + length * 2) // loop through the entire text line - { - decryptU16(data, ref offset, ref c, ref k); - if (c == 0) // Terminated Line - break; - switch (c) - { - case '\n': s += "\\n"; - break; - case '\\': s += "\\\\"; - break; - case '[': s += "\\["; - break; - case 0x10: decryptVar(data, ref offset, ref s, ref c, ref k); - break; - case 0xE07F: s += (char)0x202F; // nbsp - break; - case 0xE08D: s += (char)0x2026; // … - break; - case 0xE08E: s += (char)0x2642; // ♂ - break; - case 0xE08F: s += (char)0x2640; // ♀ - break; - default: s += (char)c; - break; - } - } - // store string and set key for next line (if needed) - result[i] = s; - key += 0x2983; - } - return result; - } - internal static byte[] getBytesForFile(string[] lines) - { - - using (MemoryStream textFile = new MemoryStream()) - using (BinaryWriter bw = new BinaryWriter(textFile)) - using (MemoryStream data = new MemoryStream()) - using (BinaryWriter bz = new BinaryWriter(data)) - { - ushort baseKey = 0x7C89; - - // Write up header template - bw.Write((ushort)1); // Always 1 ? - bw.Write((ushort)lines.Length); // Line Count - bw.Write((uint)0); // (Temporary) Data Length - fixed at the end. - bw.Write((uint)0); // Key, constant 0. - bw.Write((uint)0x10); // Pointer to line data. - - // Begin data - bw.Write((uint)0); // (Temporary) Data Length - fixed at the end. - - for (int i = 0; i < lines.Length; i++) - { - try - { - ushort key = baseKey; - uint pos = (uint) data.Position; - if (lines[i] == null) lines[i] = $"[~ {i}]"; - // Get crypted line data. - - // Write each character to the data stream, with handling for special characters. - for (int j = 0; j < lines[i].Length; j++) - { - ushort val = lines[i][j]; - - // Handle special text characters - // Private Use Area characters - if (val == 0x202F) val = 0xE07F; // nbsp - else if (val == 0x2026) val = 0xE08D; // … - else if (val == 0x2642) val = 0xE08E; // ♂ - else if (val == 0x2640) val = 0xE08F; // ♀ - - // Variables - if (val == '[' || val == '\\') // Variable - encryptVar(bz, lines[i], ref j, ref key); - - // Text - else bz.Write(encryptU16(val, ref key)); - } - bz.Write(encryptU16(0, ref key)); // Add the null terminator, after encrypting it. - - // Write the lineOffset and charCount to the header. - bw.Write((uint) (pos + 0x4 + lines.Length*8)); - bw.Write((uint) (data.Position - pos)/2); - if (data.Position%4 > 0) bz.Write((ushort) 0); - - // Increment the line initial key for the next line. - baseKey += 0x2983; - } - catch (Exception e) { Util.Error(e.ToString()); return null; } - } - - // Copy the data stream to the textFile stream. - data.Position = 0; data.CopyTo(textFile); - - // Fix the temp values - textFile.Position = 0x4; bw.Write((uint)textFile.Length - 0x10); - textFile.Position = 0x10; bw.Write((uint)textFile.Length - 0x10); - - return textFile.ToArray(); - } - } - // Text Encrypting - internal static ushort encryptU16(ushort val, ref ushort key) - { - val = (ushort)(key ^ val); - key = (ushort)(key << 3 | key >> 13); - return val; - } - internal static ushort decryptU16(byte[] data, ref int offset, ref ushort val, ref ushort key) - { - val = (ushort)(BitConverter.ToUInt16(data, offset) ^ key); - offset += 2; - key = (ushort)(key << 3 | key >> 13); - return val; - } - // Variable Handling - internal static ushort getVariableBytes(string varType, ref List args, bool noArgs = false) - { - // Fetch the variable name... - int bracket = varType.IndexOf('('); - string variable = noArgs ? varType : varType.Substring(0, bracket); - string[] arguments = noArgs ? null : varType.Substring(bracket + 1, varType.Length - bracket - 2).Split(','); - - ushort varVal; - - switch (variable) - { - case "COLOR": varVal = 0xFF00; break; - case "TRNAME": varVal = 0x0100; break; - case "PKNAME": varVal = 0x0101; break; - case "PKNICK": varVal = 0x0102; break; - case "TYPE": varVal = 0x0103; break; - case "LOCATION": varVal = 0x0105; break; - case "ABILITY": varVal = 0x0106; break; - case "MOVE": varVal = 0x0107; break; - case "ITEM1": varVal = 0x0108; break; - case "ITEM2": varVal = 0x0109; break; - case "sTRBAG": varVal = 0x010A; break; - case "BOX": varVal = 0x010B; break; - case "EVSTAT": varVal = 0x010D; break; - case "OPOWER": varVal = 0x0110; break; - case "RIBBON": varVal = 0x0127; break; - case "MIINAME": varVal = 0x0134; break; - case "WEATHER": varVal = 0x013E; break; - case "TRNICK": varVal = 0x0189; break; - case "1stchrTR": varVal = 0x018A; break; - case "SHOUTOUT": varVal = 0x018B; break; - case "BERRY": varVal = 0x018E; break; - case "REMFEEL": varVal = 0x018F; break; - case "REMQUAL": varVal = 0x0190; break; - case "WEBSITE": varVal = 0x0191; break; - case "CHOICECOS": varVal = 0x019C; break; - case "GSYNCID": varVal = 0x01A1; break; - case "PRVIDSAY": varVal = 0x0192; break; - case "BTLTEST": varVal = 0x0193; break; - case "GENLOC": varVal = 0x0195; break; - case "CHOICEFOOD": varVal = 0x0199; break; - case "HOTELITEM": varVal = 0x019A; break; - case "TAXISTOP": varVal = 0x019B; break; - case "MAISTITLE": varVal = 0x019F; break; - case "ITEMPLUR0": varVal = 0x1000; break; - case "ITEMPLUR1": varVal = 0x1001; break; - case "GENDBR": varVal = 0x1100; break; - case "NUMBRNCH": varVal = 0x1101; break; - case "iCOLOR2": varVal = 0x1302; break; - case "iCOLOR3": varVal = 0x1303; break; - case "NUM1": varVal = 0x0200; break; - case "NUM2": varVal = 0x0201; break; - case "NUM3": varVal = 0x0202; break; - case "NUM4": varVal = 0x0203; break; - case "NUM5": varVal = 0x0204; break; - case "NUM6": varVal = 0x0205; break; - case "NUM7": varVal = 0x0206; break; - case "NUM8": varVal = 0x0207; break; - case "NUM9": varVal = 0x0208; break; - default: varVal = Convert.ToUInt16(variable, 16); break; - } - if (!noArgs) - // Set arguments in. - args.AddRange(arguments.Select(t => Convert.ToUInt16(t, 16))); - - // All done. - return varVal; - } - internal static void encryptVar(BinaryWriter bw, string line, ref int i, ref ushort key) - { - ushort val = line[i]; - if (val == '\\') // Line Break - if (line[i + 1] == 'n') - { - i++; - bw.Write(encryptU16('\n', ref key)); - } - else if (line[i + 1] == 'r') - { - bw.Write(encryptU16(0x10, ref key)); i++; - bw.Write(encryptU16(1, ref key)); - bw.Write(encryptU16(0xBE00, ref key)); - } - else if (line[i + 1] == 'c') - { - bw.Write(encryptU16(0x10, ref key)); i++; - bw.Write(encryptU16(1, ref key)); - bw.Write(encryptU16(0xBE01, ref key)); - } - else if (line[i + 1] == '\\') // Not Formatting - { - i++; - bw.Write(encryptU16('\\', ref key)); - } - else if (line[i + 1] == '[') // Not Variable - { - i++; - bw.Write(encryptU16('[', ref key)); - } - else - { - if (DialogResult.Yes != Util.Prompt( - MessageBoxButtons.YesNo, - "Encoding Error - Please Resolve:", - string.Format("File: {3}{0}Char: {1}{0}Line:{0}{2}", Environment.NewLine, line[i+1], line, i), - "Treat as literal '\\'?")) - throw new Exception("Invalid terminated line: " + line); - - i++; - bw.Write(encryptU16('\\', ref key)); - } - else if (val == '[') // Special Variable - { - int bracket = line.Substring(i + 1).IndexOf(']'); - if (bracket < 3) throw new Exception("Variable encoding error!"); - - // [VAR X(a, b)] - // Remove the [ ] -> VAR X(a, b) - string varCMD = line.Substring(i + 1, bracket); - i += bracket + 1; // Advance the index to the end of the bracket. - - string[] split = varCMD.Split(' '); - string varMethod = split[0]; // Returns VAR or WAIT or ~ - string varType = split.Length > 1 ? varCMD.Substring(varMethod.Length + 1) : ""; // Returns the remainder of the var command data. - ushort varValue; - - // Set up argument storage (even if it not used) - List args = new List(); - - try - { - switch (varMethod) - { - case "~": // Blank Text Line Variable (No text set - debug/quality testing variable?) - { - varValue = 0xBDFF; - args.Add(Convert.ToUInt16(varType)); - break; - } - case "WAIT": // Event pause Variable. - { - varValue = 0xBE02; - args.Add(Convert.ToUInt16(varType)); - break; - } - case "VAR": // Text Variable - { - varValue = getVariableBytes(varType, ref args, varCMD.IndexOf('(') < 0); - break; - } - default: throw new Exception("Unknown variable method type!"); - } - } - catch (Exception e) - { - Util.Alert("Variable error. Current line text is:", line, e.ToString()); - throw new Exception("Cannot save the file."); - } - - // Write the Variable prefix. - bw.Write(encryptU16(0x0010, ref key)); - // Write Length of the following Variable Data - bw.Write(encryptU16((ushort)(1 + args.Count), ref key)); - // Write the Variable type. - bw.Write(encryptU16(varValue, ref key)); - - foreach (ushort t in args) - bw.Write(encryptU16(t, ref key)); - - // Done. - } - } - internal static void decryptVar(byte[] d, ref int o, ref string s, ref ushort v, ref ushort k) - { - ushort length = decryptU16(d, ref o, ref v, ref k); - ushort varType = decryptU16(d, ref o, ref v, ref k); - switch (varType) - { - // Check the nonvariable types... - case 0xBE00: // "Waitbutton then scroll text;; \r" - { s += "\\r"; return; } - case 0xBE01: // "Waitbutton then clear text;; \c" - { s += "\\c"; return; } - case 0xBE02: // Dramatic pause for a text line. New! - { s += "[WAIT " + decryptU16(d, ref o, ref v, ref k) + "]"; return; } - case 0xBDFF: // Empty Text line? Includes linenum so maybe for betatest finding used-unused lines? - { s += "[~ " + decryptU16(d, ref o, ref v, ref k) + "]"; return; } - - // Else a text variable, so let's loop through all the variable types. If we cannot find it, we just write the u16 val. - default: - { - ushort varCode = varType; // decryptU16(d, ref o, ref v, ref k); - string tvname; - switch (varCode) // get variable's info name - { - case 0xFF00: tvname = "COLOR"; break; // Change text line color (0 = white, 1 = red, 2 = blue...) - case 0x0100: tvname = "TRNAME"; break; // - case 0x0101: tvname = "PKNAME"; break; - case 0x0102: tvname = "PKNICK"; break; - case 0x0103: tvname = "TYPE"; break; - case 0x0105: tvname = "LOCATION"; break; - case 0x0106: tvname = "ABILITY"; break; - case 0x0107: tvname = "MOVE"; break; - case 0x0108: tvname = "ITEM1"; break; - case 0x0109: tvname = "ITEM2"; break; - case 0x010A: tvname = "sTRBAG"; break; - case 0x010B: tvname = "BOX"; break; - case 0x010D: tvname = "EVSTAT"; break; - case 0x0110: tvname = "OPOWER"; break; - case 0x0127: tvname = "RIBBON"; break; - case 0x0134: tvname = "MIINAME"; break; - case 0x013E: tvname = "WEATHER"; break; - case 0x0189: tvname = "TRNICK"; break; - case 0x018A: tvname = "1stchrTR"; break; - case 0x018B: tvname = "SHOUTOUT"; break; - case 0x018E: tvname = "BERRY"; break; - case 0x018F: tvname = "REMFEEL"; break; - case 0x0190: tvname = "REMQUAL"; break; - case 0x0191: tvname = "WEBSITE"; break; - case 0x019C: tvname = "CHOICECOS"; break; - case 0x01A1: tvname = "GSYNCID"; break; - case 0x0192: tvname = "PRVIDSAY"; break; - case 0x0193: tvname = "BTLTEST"; break; - case 0x0195: tvname = "GENLOC"; break; - case 0x0199: tvname = "CHOICEFOOD"; break; - case 0x019A: tvname = "HOTELITEM"; break; - case 0x019B: tvname = "TAXISTOP"; break; - case 0x019F: tvname = "MAISTITLE"; break; - case 0x1000: tvname = "ITEMPLUR0"; break; - case 0x1001: tvname = "ITEMPLUR1"; break; - case 0x1100: tvname = "GENDBR"; break; - case 0x1101: tvname = "NUMBRNCH"; break; - case 0x1302: tvname = "iCOLOR2"; break; - case 0x1303: tvname = "iCOLOR3"; break; - case 0x0200: tvname = "NUM1"; break; - case 0x0201: tvname = "NUM2"; break; - case 0x0202: tvname = "NUM3"; break; - case 0x0203: tvname = "NUM4"; break; - case 0x0204: tvname = "NUM5"; break; - case 0x0205: tvname = "NUM6"; break; - case 0x0206: tvname = "NUM7"; break; - case 0x0207: tvname = "NUM8"; break; - case 0x0208: tvname = "NUM9"; break; - default: tvname = varCode.ToString("X4"); break; - } - s += "[VAR" + " " + tvname; - if (length > 1) - { - s += '('; - while (length > 1) - { - // Write arguments - ushort arg = decryptU16(d, ref o, ref v, ref k); - length--; - s += arg.ToString("X4"); - if (length == 1) break; - s += ","; - } - s += ')'; - } - s += "]"; - break; - } - } - } - private void xytext_FormClosing(object sender, FormClosingEventArgs e) { // Save All the old text - if (entry > -1) File.WriteAllBytes(files[entry], getBytesForFile(getCurrentDGLines())); + if (entry > -1) File.WriteAllBytes(files[entry], TextFile.getBytes(getCurrentDGLines())); } } } \ No newline at end of file diff --git a/pk3DS/Tools/TextFile.cs b/pk3DS/Tools/TextFile.cs new file mode 100644 index 0000000..b057f92 --- /dev/null +++ b/pk3DS/Tools/TextFile.cs @@ -0,0 +1,426 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace pk3DS +{ + public class TextFile + { + // Text Formatting Config + private const ushort KEY_BASE = 0x7C89; + private const ushort KEY_ADVANCE = 0x2983; + private const ushort KEY_VARIABLE = 0x0010; + private const ushort KEY_TERMINATOR = 0x0000; + private const ushort KEY_TEXTRETURN = 0xBE00; + private const ushort KEY_TEXTCLEAR = 0xBE01; + private const ushort KEY_TEXTWAIT = 0xBE02; + private const ushort KEY_TEXTNULL = 0xBDFF; + private const bool SETEMPTYTEXT = true; + private static readonly byte[] emptyTextFile = { 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00 }; + + public TextFile(byte[] data = null) + { + Data = (byte[])(data ?? emptyTextFile).Clone(); + + if (InitialKey != 0) + throw new Exception("Invalid initial key! Not 0?"); + if (SectionDataOffset + TotalLength != Data.Length || TextSections != 1) + throw new Exception("Invalid Text File"); + if (SectionLength != TotalLength) + throw new Exception("Section size and overall size do not match."); + } + private ushort TextSections { get { return BitConverter.ToUInt16(Data, 0x0); } set { BitConverter.GetBytes(value).CopyTo(Data, 0x0); } } // Always 0x0001 + private ushort LineCount { get { return BitConverter.ToUInt16(Data, 0x2); } set { BitConverter.GetBytes(value).CopyTo(Data, 0x2); } } + private uint TotalLength { get { return BitConverter.ToUInt32(Data, 0x4); } set { BitConverter.GetBytes(value).CopyTo(Data, 0x4); } } + private uint InitialKey { get { return BitConverter.ToUInt32(Data, 0x8); } set { BitConverter.GetBytes(value).CopyTo(Data, 0x8); } } // Always 0x00000000 + private uint SectionDataOffset { get { return BitConverter.ToUInt32(Data, 0xC); } set { BitConverter.GetBytes(value).CopyTo(Data, 0xC); } } // Always 0x0010 + private uint SectionLength { get { return BitConverter.ToUInt32(Data, (int)SectionDataOffset); } set { BitConverter.GetBytes(value).CopyTo(Data, SectionDataOffset); } } + private LineInfo[] LineOffsets + { + get + { + LineInfo[] result = new LineInfo[LineCount]; + int sdo = (int)SectionDataOffset; + for (int i = 0; i < result.Length; i++) + result[i] = new LineInfo + { + Offset = BitConverter.ToInt32(Data, i * 8 + sdo + 4) + sdo, + Length = BitConverter.ToInt32(Data, i * 8 + sdo + 8) + }; + return result; + } + set + { + if (value == null) + return; + int sdo = (int)SectionDataOffset; + for (int i = 0; i < value.Length; i++) + { + BitConverter.GetBytes(value[i].Offset).CopyTo(Data, i * 8 + sdo + 4); + BitConverter.GetBytes(value[i].Length).CopyTo(Data, i * 8 + sdo + 8); + } + } + } + private class LineInfo + { + public int Offset, Length; + } + + public string[] Lines + { + get + { + ushort key = KEY_BASE; + string[] result = new string[LineCount]; + LineInfo[] lines = LineOffsets; + for (int i = 0; i < lines.Length; i++) + { + byte[] EncryptedLineData = Data.Skip(lines[i].Offset).Take(lines[i].Length * 2).ToArray(); + byte[] DecryptedLineData = cryptLineData(EncryptedLineData, key); + result[i] = getLineString(DecryptedLineData); + key += KEY_ADVANCE; + } + return result; + } + set + { + if (value == null) + value = new string[0]; + + ushort key = KEY_BASE; + LineInfo[] lines = new LineInfo[value.Length]; + + // Get Line Data + byte[][] lineData = new byte[lines.Length][]; + int sdo = (int)SectionDataOffset; + int bytesUsed = 0; + for (int i = 0; i < lines.Length; i++) + { + string text = value[i].Trim(); // ReSharper disable once RedundantLogicalConditionalExpressionOperand + if (text.Length == 0 && SETEMPTYTEXT) + text = $"[~ {i}]"; + byte[] DecryptedLineData = getLineData(text); + lineData[i] = cryptLineData(DecryptedLineData, key); + if (lineData[i].Length % 4 == 2) + Array.Resize(ref lineData[i], lineData[i].Length + 2); + key += KEY_ADVANCE; + lines[i] = new LineInfo { Offset = 4 + 8 * value.Length + bytesUsed, Length = DecryptedLineData.Length / 2 }; + bytesUsed += lineData[i].Length; + } + + // Apply Line Data + Array.Resize(ref Data, sdo + 4 + 8 * value.Length + bytesUsed); + LineOffsets = lines; // Handled by LineInfo[] set {} + lineData.SelectMany(i => i).ToArray().CopyTo(Data, Data.Length - bytesUsed); + TotalLength = SectionLength = (uint)(Data.Length - sdo); + LineCount = (ushort)value.Length; + } + } + public byte[] Data; + + private static byte[] cryptLineData(byte[] data, ushort key) + { + byte[] result = new byte[data.Length]; + for (int i = 0; i < result.Length; i += 2) + { + BitConverter.GetBytes((ushort)(BitConverter.ToUInt16(data, i) ^ key)).CopyTo(result, i); + key = (ushort)(key << 3 | key >> 13); + } + return result; + } + private static byte[] getLineData(string line) + { + if (line == null) + return new byte[2]; + + MemoryStream ms = new MemoryStream(); + using (BinaryWriter bw = new BinaryWriter(ms)) + { + int i = 0; + while (i < line.Length) + { + ushort val = line[i++]; + + if (val == 0x202F) val = 0xE07F; // nbsp + else if (val == 0x2026) val = 0xE08D; // … + else if (val == 0x2642) val = 0xE08E; // ♂ + else if (val == 0x2640) val = 0xE08F; // ♀ + + if (val == '[') // Variable + { + // grab the string + int bracket = line.IndexOf("]", i, StringComparison.Ordinal); + if (bracket < 0) + throw new ArgumentException("Variable text is not capped properly."); + string varText = line.Substring(i, bracket - i); + var varValues = getVariableValues(varText); + foreach (ushort v in varValues) bw.Write(v); + i += 1 + varText.Length; + } + else if (val == '\\') // Escaped Formatting + { + var escapeValues = getEscapeValues(line[i++]); + foreach (ushort v in escapeValues) bw.Write(v); + } + else + bw.Write(val); + } + bw.Write(KEY_TERMINATOR); // cap the line off + return ms.ToArray(); + } + } + private static string getLineString(byte[] data) + { + if (data == null) + return null; + + string s = ""; + int i = 0; + while (i < data.Length) + { + ushort val = BitConverter.ToUInt16(data, i); + if (val == KEY_TERMINATOR) break; + i += 2; + + switch (val) + { + case KEY_TERMINATOR: return s; + case KEY_VARIABLE: s += getVariableString(data, ref i); break; + case '\n': s += @"\n"; break; + case '\\': s += @"\\"; break; + case '[': s += @"\["; break; + case 0xE07F: s += (char)0x202F; break; // nbsp + case 0xE08D: s += (char)0x2026; break; // … + case 0xE08E: s += (char)0x2642; break; // ♂ + case 0xE08F: s += (char)0x2640; break; // ♀ + default: s += (char)val; break; + } + } + return s; // Shouldn't get hit if the string is properly terminated. + } + private static string getVariableString(byte[] data, ref int i) + { + string s = ""; + ushort count = BitConverter.ToUInt16(data, i); i += 2; + ushort variable = BitConverter.ToUInt16(data, i); i += 2; + + switch (variable) + { + case KEY_TEXTRETURN: // "Waitbutton then scroll text; \r" + return "\\r"; + case KEY_TEXTCLEAR: // "Waitbutton then clear text;; \c" + return "\\c"; + case KEY_TEXTWAIT: // Dramatic pause for a text line. New! + ushort time = BitConverter.ToUInt16(data, i); i += 2; + return $"[WAIT {time}]"; + case KEY_TEXTNULL: // Empty Text line? Includes linenum so maybe for betatest finding used-unused lines? + ushort line = BitConverter.ToUInt16(data, i); i += 2; + return $"[~ {line}]"; + } + + string varName = getVariableString(variable); + + s += "[VAR" + " " + varName; + if (count > 1) + { + s += '('; + while (count > 1) + { + ushort arg = BitConverter.ToUInt16(data, i); i += 2; + s += arg.ToString("X4"); + if (--count == 1) break; + s += ","; + } + s += ')'; + } + s += "]"; + return s; + } + private static IEnumerable getEscapeValues(char esc) + { + var vals = new List(); + switch (esc) + { + case 'n': vals.Add('\n'); return vals; + case '\\': vals.Add('\\'); return vals; + case 'r': vals.AddRange(new ushort[] { KEY_VARIABLE, 1, KEY_TEXTRETURN }); return vals; + case 'c': vals.AddRange(new ushort[] { KEY_VARIABLE, 1, KEY_TEXTCLEAR }); return vals; + default: throw new Exception("Invalid terminated line: \"\\" + esc + "\""); + } + } + private static IEnumerable getVariableValues(string variable) + { + string[] split = variable.Split(' '); + if (split.Length < 2) + throw new ArgumentException("Incorrectly formatted variable text!"); + + var vals = new List { KEY_VARIABLE }; + switch (split[0]) + { + case "~": // Blank Text Line Variable (No text set - debug/quality testing variable?) + vals.Add(1); + vals.Add(KEY_TEXTNULL); + vals.Add(Convert.ToUInt16(split[1])); + break; + case "WAIT": // Event pause Variable. + vals.Add(1); + vals.Add(KEY_TEXTWAIT); + vals.Add(Convert.ToUInt16(split[1])); + break; + case "VAR": // Text Variable + vals.AddRange(getVariableParameters(split[1])); + break; + default: throw new Exception("Unknown variable method type!"); + } + return vals; + } + private static IEnumerable getVariableParameters(string text) + { + var vals = new List(); + int bracket = text.IndexOf('('); + bool noArgs = bracket < 0; + string variable = noArgs ? text : text.Substring(0, bracket); + ushort varVal = getVariableNumber(variable); + + if (!noArgs) + { + string[] args = text.Substring(bracket + 1, text.Length - bracket - 2).Split(','); + vals.Add((ushort)(1 + args.Length)); + vals.Add(varVal); + vals.AddRange(args.Select(t => Convert.ToUInt16(t, 16))); + } + else + { + vals.Add(1); + vals.Add(varVal); + } + return vals; + } + private static ushort getVariableNumber(string variable) + { + switch (variable) + { + case "COLOR": return 0xFF00; + case "TRNAME": return 0x0100; + case "PKNAME": return 0x0101; + case "PKNICK": return 0x0102; + case "TYPE": return 0x0103; + case "LOCATION": return 0x0105; + case "ABILITY": return 0x0106; + case "MOVE": return 0x0107; + case "ITEM1": return 0x0108; + case "ITEM2": return 0x0109; + case "sTRBAG": return 0x010A; + case "BOX": return 0x010B; + case "EVSTAT": return 0x010D; + case "OPOWER": return 0x0110; + case "RIBBON": return 0x0127; + case "MIINAME": return 0x0134; + case "WEATHER": return 0x013E; + case "TRNICK": return 0x0189; + case "1stchrTR": return 0x018A; + case "SHOUTOUT": return 0x018B; + case "BERRY": return 0x018E; + case "REMFEEL": return 0x018F; + case "REMQUAL": return 0x0190; + case "WEBSITE": return 0x0191; + case "CHOICECOS": return 0x019C; + case "GSYNCID": return 0x01A1; + case "PRVIDSAY": return 0x0192; + case "BTLTEST": return 0x0193; + case "GENLOC": return 0x0195; + case "CHOICEFOOD": return 0x0199; + case "HOTELITEM": return 0x019A; + case "TAXISTOP": return 0x019B; + case "MAISTITLE": return 0x019F; + case "ITEMPLUR0": return 0x1000; + case "ITEMPLUR1": return 0x1001; + case "GENDBR": return 0x1100; + case "NUMBRNCH": return 0x1101; + case "iCOLOR2": return 0x1302; + case "iCOLOR3": return 0x1303; + case "NUM1": return 0x0200; + case "NUM2": return 0x0201; + case "NUM3": return 0x0202; + case "NUM4": return 0x0203; + case "NUM5": return 0x0204; + case "NUM6": return 0x0205; + case "NUM7": return 0x0206; + case "NUM8": return 0x0207; + case "NUM9": return 0x0208; + default: + try { return Convert.ToUInt16(variable, 16); } + catch { throw new ArgumentException($"Variable \"{variable}\" parse error."); } + } + } + private static string getVariableString(ushort variable) + { + switch (variable) + { + case 0xFF00: return "COLOR"; // Change text line color (0 = white, 1 = red, 2 = blue...) + case 0x0100: return "TRNAME"; + case 0x0101: return "PKNAME"; + case 0x0102: return "PKNICK"; + case 0x0103: return "TYPE"; + case 0x0105: return "LOCATION"; + case 0x0106: return "ABILITY"; + case 0x0107: return "MOVE"; + case 0x0108: return "ITEM1"; + case 0x0109: return "ITEM2"; + case 0x010A: return "sTRBAG"; + case 0x010B: return "BOX"; + case 0x010D: return "EVSTAT"; + case 0x0110: return "OPOWER"; + case 0x0127: return "RIBBON"; + case 0x0134: return "MIINAME"; + case 0x013E: return "WEATHER"; + case 0x0189: return "TRNICK"; + case 0x018A: return "1stchrTR"; + case 0x018B: return "SHOUTOUT"; + case 0x018E: return "BERRY"; + case 0x018F: return "REMFEEL"; + case 0x0190: return "REMQUAL"; + case 0x0191: return "WEBSITE"; + case 0x019C: return "CHOICECOS"; + case 0x01A1: return "GSYNCID"; + case 0x0192: return "PRVIDSAY"; + case 0x0193: return "BTLTEST"; + case 0x0195: return "GENLOC"; + case 0x0199: return "CHOICEFOOD"; + case 0x019A: return "HOTELITEM"; + case 0x019B: return "TAXISTOP"; + case 0x019F: return "MAISTITLE"; + case 0x1000: return "ITEMPLUR0"; + case 0x1001: return "ITEMPLUR1"; + case 0x1100: return "GENDBR"; + case 0x1101: return "NUMBRNCH"; + case 0x1302: return "iCOLOR2"; + case 0x1303: return "iCOLOR3"; + case 0x0200: return "NUM1"; + case 0x0201: return "NUM2"; + case 0x0202: return "NUM3"; + case 0x0203: return "NUM4"; + case 0x0204: return "NUM5"; + case 0x0205: return "NUM6"; + case 0x0206: return "NUM7"; + case 0x0207: return "NUM8"; + case 0x0208: return "NUM9"; + default: return variable.ToString("X4"); + } + } + + // Exposed Methods + internal static string[] getStrings(string path) + { + byte[] data = File.ReadAllBytes(path); + TextFile t; + try { t = new TextFile(data); } catch { return null; } + return t.Lines; + } + internal static byte[] getBytes(string[] lines) + { + return new TextFile { Lines = lines }.Data; + } + } +} diff --git a/pk3DS/pk3DS.csproj b/pk3DS/pk3DS.csproj index 7643958..881fdbe 100644 --- a/pk3DS/pk3DS.csproj +++ b/pk3DS/pk3DS.csproj @@ -242,6 +242,7 @@ Icon.cs + Form