|
|
|
|
@ -0,0 +1,344 @@
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
|
|
|
|
class Program
|
|
|
|
|
{
|
|
|
|
|
static void Main()
|
|
|
|
|
{
|
|
|
|
|
//TEST REMOVE LATER
|
|
|
|
|
string[] args = new string[] { "--all", "C:\\Users\\Lauren\\Desktop\\Ken Ga Kimi\\PATCH\\EN Files", "C:\\Users\\Lauren\\Desktop\\Ken Ga Kimi\\PATCH\\EN Files\\OUTPUT" };
|
|
|
|
|
|
|
|
|
|
if (args.Length < 1)
|
|
|
|
|
{
|
|
|
|
|
PrintUsage();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string outputDirectory;
|
|
|
|
|
|
|
|
|
|
switch (args[0])
|
|
|
|
|
{
|
|
|
|
|
case "--all":
|
|
|
|
|
if (args.Length != 3)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("ERROR: incorrect number of arguments");
|
|
|
|
|
PrintUsage();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
string inputDirectory = args[1];
|
|
|
|
|
outputDirectory = args[2];
|
|
|
|
|
// Validate and process folders
|
|
|
|
|
RegenerateAll(inputDirectory, outputDirectory);
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "--single":
|
|
|
|
|
if (args.Length != 4)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("ERROR: incorrect number of arguments");
|
|
|
|
|
PrintUsage();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
string scriptName = args[1];
|
|
|
|
|
string alrName = args[2];
|
|
|
|
|
outputDirectory = args[3];
|
|
|
|
|
// Validate and process files
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
PrintUsage();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void PrintUsage()
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("Console tool for regenerating Ken ga Kimi _alr files");
|
|
|
|
|
Console.WriteLine("Use to regenerate all _alr files or a single file");
|
|
|
|
|
Console.WriteLine("Adding or removing \n<ANY_TAG>\nor\n@ANY_NAME\nANY_DIALOGUE\n<pb>\n breaks single file mode");
|
|
|
|
|
Console.WriteLine("Usage:");
|
|
|
|
|
Console.WriteLine(" --all <scriptFolder> <outputDirectory>");
|
|
|
|
|
Console.WriteLine(" --single <scriptFile> <alrFile> <outputDirectory>");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void RegenerateAll(string inputDirectory, string outputDirectory)
|
|
|
|
|
{
|
|
|
|
|
if (!Directory.Exists(inputDirectory))
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"ERROR: Input folder '{inputDirectory}' does not exist.");
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
//Read both _alr and script files from the input directory
|
|
|
|
|
List<string> alrFileNames = Directory.GetFiles(inputDirectory)
|
|
|
|
|
.Where(f => Path.GetFileName(f).Contains("_alr", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
.ToList();
|
|
|
|
|
List<string> scriptFileNames = Directory.GetFiles(inputDirectory)
|
|
|
|
|
.Where(f => !Path.GetFileName(f).Contains("_alr", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
//Create lists to hold the files
|
|
|
|
|
List<AlrFile> alrFiles = new List<AlrFile>();
|
|
|
|
|
List<ScriptFile> scriptFiles = new List<ScriptFile>();
|
|
|
|
|
|
|
|
|
|
if (alrFileNames.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("No _alr files found");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (!Directory.Exists(outputDirectory))
|
|
|
|
|
{
|
|
|
|
|
Directory.CreateDirectory(outputDirectory);
|
|
|
|
|
}
|
|
|
|
|
foreach (var file in alrFileNames)
|
|
|
|
|
{
|
|
|
|
|
alrFiles.Add(ReadAlrFile(file));
|
|
|
|
|
}
|
|
|
|
|
foreach (var file in scriptFileNames)
|
|
|
|
|
{
|
|
|
|
|
scriptFiles.Add(ReadScriptFile(file));
|
|
|
|
|
}
|
|
|
|
|
// Sort alrFiles by StartingFlag, with nulls first
|
|
|
|
|
alrFiles = alrFiles
|
|
|
|
|
.OrderBy(f => f.StartingFlag.HasValue ? 1 : 0) // nulls first
|
|
|
|
|
.ThenBy(f => f.StartingFlag)
|
|
|
|
|
.ToList();
|
|
|
|
|
int position = 0;
|
|
|
|
|
foreach (var alrFile in alrFiles)
|
|
|
|
|
{
|
|
|
|
|
ScriptFile? matchingScript = scriptFiles.FirstOrDefault(s => string.Equals(s.ScriptTitle, alrFile.ScriptTitle.Substring(0, alrFile.ScriptTitle.Length - 4), StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
if (matchingScript != null)
|
|
|
|
|
{
|
|
|
|
|
byte[] alr = RegenerateAlrFile(alrFile, matchingScript, ref position);
|
|
|
|
|
File.WriteAllBytes(Path.Combine(outputDirectory, alrFile.FileName), alr);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"No matching script file found for alr: {alrFile.ScriptTitle}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Console.WriteLine($"Found {alrFiles.Count} _alr files in {inputDirectory}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static AlrFile ReadAlrFile(string filePath)
|
|
|
|
|
{
|
|
|
|
|
byte[] data = File.ReadAllBytes(filePath);
|
|
|
|
|
|
|
|
|
|
int offset = 0;
|
|
|
|
|
int nameLen = BitConverter.ToInt32(data, offset);
|
|
|
|
|
offset += 4;
|
|
|
|
|
if (nameLen < 0 || data.Length < offset + nameLen) {
|
|
|
|
|
throw new InvalidDataException("Invalid file name length in _alr file.");
|
|
|
|
|
}
|
|
|
|
|
string scriptFileName = Encoding.UTF8.GetString(data, offset, nameLen);
|
|
|
|
|
offset += nameLen;
|
|
|
|
|
int size = BitConverter.ToInt32(data, offset);
|
|
|
|
|
offset += 4;
|
|
|
|
|
offset = RoundUpToNextMultipleOf4(offset);
|
|
|
|
|
int uabeaOffset = offset;
|
|
|
|
|
|
|
|
|
|
var entries = new List<KeyValuePair>();
|
|
|
|
|
if (size > 0)
|
|
|
|
|
{
|
|
|
|
|
while (offset + 8 <= data.Length)
|
|
|
|
|
{
|
|
|
|
|
int key = BitConverter.ToInt32(data, offset);
|
|
|
|
|
int value = BitConverter.ToInt32(data, offset + 4);
|
|
|
|
|
entries.Add(new KeyValuePair { Key = key, Value = value });
|
|
|
|
|
offset += 8;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new AlrFile
|
|
|
|
|
{
|
|
|
|
|
FileName = Path.GetFileName(filePath),
|
|
|
|
|
ScriptTitleLength = nameLen,
|
|
|
|
|
ScriptTitle = scriptFileName,
|
|
|
|
|
DataSize = size,
|
|
|
|
|
UabeaHeaderSize = uabeaOffset,
|
|
|
|
|
StartingFlag = entries.Count > 0 ? entries[0].Value : null,
|
|
|
|
|
Entries = entries
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static ScriptFile ReadScriptFile(string filePath)
|
|
|
|
|
{
|
|
|
|
|
string allScriptText = File.ReadAllText(filePath, Encoding.Unicode);
|
|
|
|
|
byte[] data = Encoding.Unicode.GetBytes(allScriptText);
|
|
|
|
|
|
|
|
|
|
int offset = 0;
|
|
|
|
|
int nameLen = BitConverter.ToInt32(data, offset);
|
|
|
|
|
offset += 4;
|
|
|
|
|
if (nameLen < 0 || data.Length < offset + nameLen)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidDataException("Invalid file name length in _alr file.");
|
|
|
|
|
}
|
|
|
|
|
string scriptFileName = Encoding.UTF8.GetString(data, offset, nameLen);
|
|
|
|
|
offset += nameLen;
|
|
|
|
|
offset = RoundUpToNextMultipleOf4(offset);
|
|
|
|
|
int size = BitConverter.ToInt32(data, offset);
|
|
|
|
|
offset += 4;
|
|
|
|
|
if (data[offset] == 255 && data[offset + 1] == 254) {
|
|
|
|
|
offset += 2;
|
|
|
|
|
}
|
|
|
|
|
//offset = RoundUpToNextMultipleOf4(offset);
|
|
|
|
|
int uabeaOffset = offset;
|
|
|
|
|
|
|
|
|
|
byte[] scriptText = data.Skip(uabeaOffset).ToArray();
|
|
|
|
|
|
|
|
|
|
return new ScriptFile
|
|
|
|
|
{
|
|
|
|
|
FileName = Path.GetFileName(filePath),
|
|
|
|
|
ScriptTitleLength = nameLen,
|
|
|
|
|
ScriptTitle = scriptFileName,
|
|
|
|
|
DataSize = size,
|
|
|
|
|
UabeaHeaderSize = uabeaOffset,
|
|
|
|
|
ScriptText = scriptText,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static byte[] RegenerateAlrFile(AlrFile alr, ScriptFile script, ref int position)
|
|
|
|
|
{
|
|
|
|
|
List<byte> newAlr = new List<byte>();
|
|
|
|
|
|
|
|
|
|
newAlr.AddRange(BitConverter.GetBytes(alr.ScriptTitleLength));
|
|
|
|
|
newAlr.AddRange(Encoding.UTF8.GetBytes(alr.ScriptTitle));
|
|
|
|
|
|
|
|
|
|
if (alr.StartingFlag == null)
|
|
|
|
|
{
|
|
|
|
|
newAlr.AddRange(BitConverter.GetBytes(0));
|
|
|
|
|
int padding = (4 - (newAlr.Count % 4)) % 4;
|
|
|
|
|
for (int i = 0; i < padding; i++)
|
|
|
|
|
{
|
|
|
|
|
newAlr.Add(0x00);
|
|
|
|
|
}
|
|
|
|
|
return newAlr.ToArray();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
//Padding for title
|
|
|
|
|
int padding = (4 - (newAlr.Count % 4)) % 4;
|
|
|
|
|
for (int i = 0; i < padding; i++)
|
|
|
|
|
{
|
|
|
|
|
newAlr.Add(0x00);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
List<string> searchStrings = new List<string>() {
|
|
|
|
|
"@",
|
|
|
|
|
"<voice name=",
|
|
|
|
|
"<textboxtype type=\"monologue\">",
|
|
|
|
|
"<textboxtype type=\"monologue2\">",
|
|
|
|
|
"<textboxtype type=\"monologue3\">",
|
|
|
|
|
"<monologue background=\"on\">",
|
|
|
|
|
};
|
|
|
|
|
List<(string, int)> stringPositions = FindStringPositions(script.ScriptText);
|
|
|
|
|
List<(string, int)> filteredStringPositions = stringPositions
|
|
|
|
|
.Where(pos => searchStrings.Any(s => pos.Item1.Contains(s)))
|
|
|
|
|
.ToList();
|
|
|
|
|
//Add length of data
|
|
|
|
|
newAlr.AddRange(BitConverter.GetBytes(filteredStringPositions.Count * 8));
|
|
|
|
|
|
|
|
|
|
if (alr.ScriptTitle == "KEN_00_00_00_alr")
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("Found KEN_00_00_00");
|
|
|
|
|
}
|
|
|
|
|
//position += 1;
|
|
|
|
|
for (int i = 0; i < stringPositions.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
if (searchStrings.Any(s => stringPositions[i].Item1.Contains(s)))
|
|
|
|
|
{
|
|
|
|
|
newAlr.AddRange(BitConverter.GetBytes(stringPositions[i].Item2));
|
|
|
|
|
newAlr.AddRange(BitConverter.GetBytes(position));
|
|
|
|
|
var isNormalTag = searchStrings.Any(t => stringPositions[i].Item1.Contains(t));
|
|
|
|
|
if (stringPositions[i].Item1.Contains("<voice name="))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
else if (isNormalTag)
|
|
|
|
|
{
|
|
|
|
|
position += 1;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
//Don't add to position if the previous string was a tag
|
|
|
|
|
//Console.WriteLine("OOPS");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return newAlr.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static List<(string SearchString, int Position)> FindStringPositions(byte[] scriptBytes)
|
|
|
|
|
{
|
|
|
|
|
var results = new List<(string, int)>();
|
|
|
|
|
string scriptText = Encoding.Unicode.GetString(scriptBytes);
|
|
|
|
|
|
|
|
|
|
// 1. Find all <...> tags not commented out
|
|
|
|
|
var tagRegex = new System.Text.RegularExpressions.Regex(@"<[^>\r\n]+>");
|
|
|
|
|
foreach (System.Text.RegularExpressions.Match match in tagRegex.Matches(scriptText))
|
|
|
|
|
{
|
|
|
|
|
if (match.Value == "<pb>") continue; // Skip <pb> tags
|
|
|
|
|
|
|
|
|
|
int lineStart = scriptText.LastIndexOf('\n', match.Index);
|
|
|
|
|
if (lineStart == -1) lineStart = 0; else lineStart++;
|
|
|
|
|
int lineEnd = scriptText.IndexOf('\n', match.Index);
|
|
|
|
|
if (lineEnd == -1) lineEnd = scriptText.Length;
|
|
|
|
|
string line = scriptText.Substring(lineStart, lineEnd - lineStart).TrimStart();
|
|
|
|
|
|
|
|
|
|
if (!line.StartsWith("//"))
|
|
|
|
|
{
|
|
|
|
|
int byteIndex = Encoding.Unicode.GetByteCount(scriptText.Substring(0, match.Index));
|
|
|
|
|
results.Add((match.Value, byteIndex));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Find all lines that start with @ and are not commented out
|
|
|
|
|
int currentIndex = 0;
|
|
|
|
|
foreach (var line in scriptText.Split('\n'))
|
|
|
|
|
{
|
|
|
|
|
string trimmedLine = line.TrimStart();
|
|
|
|
|
if (!trimmedLine.StartsWith("//") && trimmedLine.StartsWith("@"))
|
|
|
|
|
{
|
|
|
|
|
int byteIndex = Encoding.Unicode.GetByteCount(scriptText.Substring(0, currentIndex));
|
|
|
|
|
results.Add((line, byteIndex));
|
|
|
|
|
}
|
|
|
|
|
currentIndex += line.Length + 1; // +1 for '\n'
|
|
|
|
|
}
|
|
|
|
|
return results.OrderBy(r => r.Item2).ToList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int RoundUpToNextMultipleOf4(int number)
|
|
|
|
|
{
|
|
|
|
|
return (number % 4 == 0) ? number : number + (4 - (number % 4));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class AlrFile
|
|
|
|
|
{
|
|
|
|
|
public string? FileName;
|
|
|
|
|
public int ScriptTitleLength;
|
|
|
|
|
public string? ScriptTitle;
|
|
|
|
|
public int DataSize;
|
|
|
|
|
public int UabeaHeaderSize;
|
|
|
|
|
public int? StartingFlag;
|
|
|
|
|
public List<KeyValuePair> Entries = new List<KeyValuePair>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class ScriptFile
|
|
|
|
|
{
|
|
|
|
|
public string? FileName;
|
|
|
|
|
public int ScriptTitleLength;
|
|
|
|
|
public string? ScriptTitle;
|
|
|
|
|
public int DataSize;
|
|
|
|
|
public int? UabeaHeaderSize;
|
|
|
|
|
public byte[]? ScriptText;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public struct KeyValuePair
|
|
|
|
|
{
|
|
|
|
|
public int Key;
|
|
|
|
|
public int Value;
|
|
|
|
|
}
|
|
|
|
|
}
|