diff --git a/Program.cs b/Program.cs index c9b65e0..6bec5e3 100644 --- a/Program.cs +++ b/Program.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Diagnostics; using ICSharpCode.SharpZipLib.BZip2; using NDesk.Options; +using System.Text.RegularExpressions; namespace RSCacheTool { @@ -20,7 +21,7 @@ namespace RSCacheTool { bool error = false; - bool help = false, extract = false, combine = false, overwrite = false, combineMergeIncomplete = false; + bool help = false, extract = false, combine = false, overwrite = false, incomplete = false, nameMusic = false; int extractArchive = -1, combineArchive = 40; OptionSet argsParser = new OptionSet() { @@ -28,12 +29,17 @@ namespace RSCacheTool { "o", "overwrite existing files, for all actions", val => { overwrite = true; } }, - { "e", "extract files from cache", val => { extract = true; } }, - { "a=", "single archive to extract, if not given all archives will be extracted", val => { extractArchive = Convert.ToInt32(val); } }, + { "e:", "extract files from cache, supply a number to extract only a specific archive", val => { + extract = true; + if (!String.IsNullOrEmpty(val)) + int.TryParse(val, out extractArchive); + }}, { "c", "combine sound", val => { combine = true; } }, { "s=", "archive to combine sounds of, defaults to 40", val => { combineArchive = Convert.ToInt32(val); } }, - { "i", "merge incomplete files (into special directory)", val => { combineMergeIncomplete = true; } }, + { "i", "merge incomplete files (into special directory)", val => { incomplete = true; } }, + + { "n", "try to name music (archive 40, needs archive 17 file 5 too), renames incompletes too if i is set", val => { nameMusic = true; } }, }; List otherArgs = argsParser.Parse(args); @@ -77,11 +83,18 @@ namespace RSCacheTool } else if (!error) { + //create outdir + if (!Directory.Exists(outDir)) + Directory.CreateDirectory(outDir); + if (extract) ExtractFiles(extractArchive, overwrite); if (combine) - CombineSounds(combineArchive, overwrite, combineMergeIncomplete); + CombineSounds(combineArchive, overwrite, incomplete); + + if (nameMusic) + NameMusic(incomplete, overwrite); } return 0; @@ -116,7 +129,7 @@ namespace RSCacheTool { bool fileError = false; - indexFile.Seek(fileIndex * 6, SeekOrigin.Begin); + indexFile.Position = fileIndex * 6L; uint fileSize = indexFile.ReadBytes(3); long startChunkOffset = indexFile.ReadBytes(3) * 520L; @@ -131,7 +144,7 @@ namespace RSCacheTool for (int chunkIndex = 0; writeOffset < fileSize && currentChunkOffset > 0; chunkIndex++) { - cacheFile.Seek(currentChunkOffset, SeekOrigin.Begin); + cacheFile.Position = currentChunkOffset; int chunkSize; int checksumFileIndex = 0; @@ -344,7 +357,7 @@ namespace RSCacheTool using (FileStream indexFileStream = File.Open(indexFileString, FileMode.Open, FileAccess.Read, FileShare.Read)) { - indexFileStream.Seek(32, SeekOrigin.Begin); + indexFileStream.Position = 32L; while (indexFileStream.ReadBytes(4) != 0x4f676753) { @@ -358,7 +371,7 @@ namespace RSCacheTool } //copy the first chunk to a temp file so SoX can handle the combining - indexFileStream.Seek(-4, SeekOrigin.Current); + indexFileStream.Position -= 4L; //wait till file is available while (true) @@ -431,59 +444,140 @@ namespace RSCacheTool } /// - /// Returns when a certain string is found in the files. - /// Used mainly for debugging (this is e.g. how I found where the sound index was located (by searching for "wildwood")) - /// Pauses with info whenever a match has been found. + /// Tries to parse Archive 17 file 5 to obtain a list of music and their corresponding index file id in archive 40. + /// Returns a dictionary that can resolve index file id to the name of the track as it appears in-game. /// - public static void FindInFiles(string needle) + public static void NameMusic(bool incomplete, bool overwrite) { - int bufferSize = 10000; - byte[] buffer = new byte[bufferSize]; - for (int archive = 0; archive < 256; archive++) + //the following is based on even more assumptions than normal made while comparing 2 extracted caches, it's therefore probably the first thing to break + //4B magic number (0x00016902) - 2B a file id? - 2B amount of files (higher than actual entries sometimes) - 2B amount of files + + string resolveFileName = outDir + "17\\5"; + + if (File.Exists(resolveFileName)) { - if (Directory.Exists(outDir + archive)) + using (FileStream resolveFile = File.Open(resolveFileName, FileMode.Open, FileAccess.Read, FileShare.Read)) { - string[] fileNames = Directory.GetFiles(outDir + archive); + Dictionary trackIdNames = new Dictionary(); + Dictionary fileIdTracks = new Dictionary(); + + byte[] magicNumber = new byte[] { + 0x00, + 0x01, + 0x69, + 0x02 + }; - int i = 0; - foreach (string fileName in fileNames) + //locate start of the music names (8th magic number) and file ids (12th) + long namesStartPos = resolveFile.IndexOf(magicNumber, 8); + long filesStartPos = resolveFile.IndexOf(magicNumber, 12); + + if (namesStartPos != -1 && filesStartPos != -1) { - using (FileStream file = File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) + resolveFile.Position = namesStartPos + 8; + uint musicCount = resolveFile.ReadBytes(2); + + //construct trackIdNames + Regex regex = new Regex("[" + Regex.Escape(new string(Path.GetInvalidFileNameChars())) + "]"); + for(int i = 0; i < musicCount; i++) { - int offset = 0; - int readBytes; - do - { - readBytes = file.Read(buffer, 0, bufferSize); + int trackId = (int)resolveFile.ReadBytes(2); + string trackName = resolveFile.ReadNullTerminatedString(); - string readString = Encoding.Default.GetString(buffer, 0, readBytes); - int index = readString.IndexOf(needle, StringComparison.CurrentCultureIgnoreCase); - if (index != -1) + //remove characters that can't be used in files from trackName + trackName = regex.Replace(trackName, ""); + + //add only if the string is of any use + if (!String.IsNullOrWhiteSpace(trackName)) + trackIdNames.Add(trackId, trackName); + } + + //construct fileIdTracks + resolveFile.Position = filesStartPos + 8; + uint fileCount = resolveFile.ReadBytes(2); + for (int i = 0; i < fileCount; i++) + { + int trackId = (int)resolveFile.ReadBytes(2); + uint fileId = resolveFile.ReadBytes(4); + + //only add if it doesn't exist already + if (!fileIdTracks.ContainsKey(fileId)) + fileIdTracks.Add(fileId, trackId); + } + + //let's do this! + if (!Directory.Exists(outDir + "sound\\named\\")) + Directory.CreateDirectory(outDir + "sound\\named\\"); + + foreach (string file in Directory.GetFiles(outDir + "sound\\")) + { + string fileIdString = Path.GetFileNameWithoutExtension(file); + uint fileId; + if (uint.TryParse(fileIdString, out fileId)) + { + if (fileIdTracks.ContainsKey(fileId)) { - Console.WriteLine(fileName + " @ " + offset + index + "B"); - Console.ReadLine(); - } + int trackId = fileIdTracks[fileId]; + if (trackIdNames.ContainsKey(trackId)) + { + string trackName = trackIdNames[trackId]; + string destFile = outDir + "sound\\named\\" + trackName + ".ogg"; - //dont fully add readBytes, so the next string can find the full match if it started on the end of this buffer but couldn't complete - offset += readBytes - needle.Length + 1; + if (!File.Exists(destFile) || overwrite) + File.Copy(file, destFile, true); + + Console.WriteLine(destFile); + } + } } - while (readBytes == bufferSize); } - Console.WriteLine("a" + archive + "f" + i + "/" + (fileNames.Length - 1)); + //redundancy, whatever + if (incomplete) + { + if (!Directory.Exists(outDir + "sound\\named\\incomplete\\")) + Directory.CreateDirectory(outDir + "sound\\named\\incomplete"); + + foreach (string file in Directory.GetFiles(outDir + "sound\\incomplete\\")) + { + string fileIdString = Path.GetFileNameWithoutExtension(file); + uint fileId; + if (uint.TryParse(fileIdString, out fileId)) + { + if (fileIdTracks.ContainsKey(fileId)) + { + int trackId = fileIdTracks[fileId]; + if (trackIdNames.ContainsKey(trackId)) + { + string trackName = trackIdNames[trackId]; + string destFile = outDir + "sound\\named\\incomplete\\" + trackName + ".ogg"; + + if (!File.Exists(destFile) || overwrite) + File.Copy(file, destFile, true); - i++; + Console.WriteLine(destFile); + } + } + } + } + } } + else + Console.WriteLine("Entry points within resolving file could not be found."); } } + else + Console.WriteLine("File for resolving music names (" + resolveFileName + ") does not exist."); + + Console.WriteLine("Done naming music."); } /// - /// Reads a given amount of bytes from the stream. + /// Reads a given amount of unsigned bytes from the stream and combines them into one unsigned integer. /// - public static uint ReadBytes(this Stream stream, byte bytes) + public static uint ReadBytes(this Stream stream, int bytes) { - if (bytes == 0 || bytes > 4) + if (bytes < 1 || bytes > 4) throw new ArgumentOutOfRangeException(); uint result = 0; @@ -493,5 +587,72 @@ namespace RSCacheTool return result; } + + /// + /// Reads ANSI characters into a string until \0 or EOF occurs. + /// + public static string ReadNullTerminatedString(this Stream stream) + { + string result = ""; + int readByte = stream.ReadByte(); + + while (readByte > 0) + { + result += Encoding.Default.GetString(new byte[] { (byte)readByte }); + readByte = stream.ReadByte(); + } + + return result; + } + + /// + /// Returns the stream location of the matchNumber-th occurence of needle, or -1 when there are no(t enough) matches. + /// + public static long IndexOf(this Stream stream, byte[] needle, int matchNumber = 1, int bufferSize = 10000) + { + //for resetting after method + long startPosition = stream.Position; + + byte[] buffer = new byte[bufferSize]; + int offset = 0, readBytes, matches = 0; + + do + { + stream.Position = offset; + readBytes = stream.Read(buffer, 0, bufferSize); + + for (int pos = 0; pos < readBytes - needle.Length + 1; pos++) + { + //try to find the rest of the match if the first byte matches + int matchIndex = 0; + while (buffer[pos + matchIndex] == needle[matchIndex]) + { + //full match found + if (matchIndex == needle.Length - 1) + { + //this is the chosen one, return the position + if (++matches == matchNumber) + { + stream.Position = 0; + return offset + pos; + } + + break; + } + + matchIndex++; + } + } + + //don't fully add readBytes, so the next string can find the full match if it started on the end of this buffer but couldn't complete + offset += readBytes - needle.Length + 1; + } + while (readBytes == bufferSize); + + //no result + stream.Position = startPosition; + return -1; + } + } }