mirror of
https://github.com/kwsch/pk3DS.git
synced 2026-03-26 03:34:41 -05:00
Preserves ingame character usage (namely m/f symbols) Compile with set to true if remapping is desired Closes #137
357 lines
14 KiB
C#
357 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
|
|
namespace pk3DS.Core
|
|
{
|
|
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 = false;
|
|
private static bool REMAPCHARS = false;
|
|
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(GameConfig config, 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.");
|
|
|
|
Config = config;
|
|
}
|
|
private GameConfig Config { get; set; }
|
|
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.ToInt16(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 = new byte[lines[i].Length * 2];
|
|
Array.Copy(Data, lines[i].Offset, EncryptedLineData, 0, EncryptedLineData.Length);
|
|
byte[] DecryptedLineData = cryptLineData(EncryptedLineData, key);
|
|
result[i] = getLineString(Config, 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();
|
|
if (text.Length == 0 && SETEMPTYTEXT)
|
|
text = $"[~ {i}]";
|
|
byte[] DecryptedLineData = getLineData(Config, 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 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 byte[] getLineData(GameConfig config, 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++];
|
|
val = TryRemapChar(val);
|
|
|
|
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: " + line);
|
|
string varText = line.Substring(i, bracket - i);
|
|
var varValues = getVariableValues(config, 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 ushort TryRemapChar(ushort val)
|
|
{
|
|
if (!REMAPCHARS)
|
|
return val;
|
|
switch (val)
|
|
{
|
|
case 0x202F: return 0xE07F; // nbsp
|
|
case 0x2026: return 0xE08D; // …
|
|
case 0x2642: return 0xE08E; // ♂
|
|
case 0x2640: return 0xE08F; // ♀
|
|
default: return val;
|
|
}
|
|
}
|
|
private static ushort TryUnmapChar(ushort val)
|
|
{
|
|
if (!REMAPCHARS)
|
|
return val;
|
|
switch (val)
|
|
{
|
|
case 0xE07F: return 0x202F; // nbsp
|
|
case 0xE08D: return 0x2026; // …
|
|
case 0xE08E: return 0x2642; // ♂
|
|
case 0xE08F: return 0x2640; // ♀
|
|
default: return val;
|
|
}
|
|
}
|
|
|
|
private string getLineString(GameConfig config, 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(config, data, ref i); break;
|
|
case '\n': s += @"\n"; break;
|
|
case '\\': s += @"\\"; break;
|
|
case '[': s += @"\["; break;
|
|
default: s += (char)TryUnmapChar(val); break;
|
|
}
|
|
}
|
|
return s; // Shouldn't get hit if the string is properly terminated.
|
|
}
|
|
private string getVariableString(GameConfig config, 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(config, 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 IEnumerable<ushort> getEscapeValues(char esc)
|
|
{
|
|
var vals = new List<ushort>();
|
|
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 IEnumerable<ushort> getVariableValues(GameConfig config, string variable)
|
|
{
|
|
string[] split = variable.Split(' ');
|
|
if (split.Length < 2)
|
|
throw new ArgumentException("Incorrectly formatted variable text: " + variable);
|
|
|
|
var vals = new List<ushort> { 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(config, split[1]));
|
|
break;
|
|
default: throw new Exception("Unknown variable method type: " + variable);
|
|
}
|
|
return vals;
|
|
}
|
|
private IEnumerable<ushort> getVariableParameters(GameConfig config, string text)
|
|
{
|
|
var vals = new List<ushort>();
|
|
int bracket = text.IndexOf('(');
|
|
bool noArgs = bracket < 0;
|
|
string variable = noArgs ? text : text.Substring(0, bracket);
|
|
ushort varVal = getVariableNumber(config, 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 ushort getVariableNumber(GameConfig config, string variable)
|
|
{
|
|
var v = config.getVariableCode(variable);
|
|
if (v != null)
|
|
return (ushort) v.Code;
|
|
|
|
try
|
|
{
|
|
return Convert.ToUInt16(variable, 16);
|
|
}
|
|
catch { throw new ArgumentException("Variable parse error: " + variable); }
|
|
}
|
|
private string getVariableString(GameConfig config, ushort variable)
|
|
{
|
|
var v = config.getVariableName(variable);
|
|
return v == null ? variable.ToString("X4") : v.Name;
|
|
}
|
|
|
|
// Exposed Methods
|
|
public static string[] getStrings(GameConfig config, byte[] data)
|
|
{
|
|
TextFile t;
|
|
try { t = new TextFile(config, data); } catch { return null; }
|
|
return t.Lines;
|
|
}
|
|
public static byte[] getBytes(GameConfig config, string[] lines)
|
|
{
|
|
return new TextFile (config) { Lines = lines }.Data;
|
|
}
|
|
}
|
|
}
|