using System; using System.Drawing; using System.IO; using System.Text; using static Program; using static System.Net.Mime.MediaTypeNames; using static System.Runtime.InteropServices.JavaScript.JSType; class Program { static void Main() { //TEST REMOVE LATER string[] args = new string[] { "--all", "C:\\Users\\Lauren\\Desktop\\Ken Ga Kimi\\TEST", "C:\\Users\\Lauren\\Desktop\\Ken Ga Kimi\\TEST\\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\nor\n@ANY_NAME\nANY_DIALOGUE\n\n breaks single file mode"); Console.WriteLine("Usage:"); Console.WriteLine(" --all "); Console.WriteLine(" --single "); } 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 alrFileNames = Directory.GetFiles(inputDirectory) .Where(f => Path.GetFileName(f).Contains("_alr", StringComparison.OrdinalIgnoreCase)) .ToList(); List scriptFileNames = Directory.GetFiles(inputDirectory) .Where(f => !Path.GetFileName(f).Contains("_alr", StringComparison.OrdinalIgnoreCase)) .ToList(); //Create lists to hold the files List alrFiles = new List(); List scriptFiles = new List(); 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(); 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+1] == 255 && data[offset + 2] == 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 newAlr = new List(); 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 searchStrings = new List() { "\n@", "\n", "\n", }; List<(string, int)> stringPositions = FindStringPositions(script.ScriptText, searchStrings); //Add length of data newAlr.AddRange(BitConverter.GetBytes(stringPositions.Count * 8)); //position += 1; for (int i = 0; i < stringPositions.Count; i++) { newAlr.AddRange(BitConverter.GetBytes(stringPositions[i].Item2)); newAlr.AddRange(BitConverter.GetBytes(position)); if (stringPositions[i].Item1 != "\n@") { position += 1; } else if (stringPositions[i].Item1 == "\n@" && i > 0 && stringPositions[i - 1].Item1 != "\n@") { //Don't add to position if the previous string was a tag } else if (stringPositions.Count == 1) { position += 1; } } return newAlr.ToArray(); } public static List<(string SearchString, int Position)> FindStringPositions(byte[] scriptBytes, IEnumerable searchStrings) { var results = new List<(string, int)>(); // Decode the script bytes to string for line analysis string scriptText = Encoding.Unicode.GetString(scriptBytes); foreach (var search in searchStrings) { byte[] searchBytes = Encoding.Unicode.GetBytes(search); int index = 0; while (index <= scriptBytes.Length - searchBytes.Length) { bool match = true; for (int j = 0; j < searchBytes.Length; j++) { if (scriptBytes[index + j] != searchBytes[j]) { match = false; break; } } if (match) { // Find the character index in the string corresponding to this byte index int charIndex = Encoding.Unicode.GetString(scriptBytes, 0, index).Length; // Find the start of the line int lineStart = scriptText.LastIndexOf('\n', charIndex - 1); if (lineStart == -1) lineStart = 0; else lineStart++; int lineEnd = scriptText.IndexOf('\n', charIndex); if (lineEnd == -1) lineEnd = scriptText.Length; string line = scriptText.Substring(lineStart, lineEnd - lineStart).TrimStart(); // Only add if the line does NOT start with // if (!line.StartsWith("//")) { results.Add((search, index)); } index += searchBytes.Length; // Move past this match } else { index++; } } } // Sort by position in order of appearance 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 Entries = new List(); } 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; } }