1db7dced76
For cache-deconstructing, debugging and public use.
497 lines
16 KiB
C#
497 lines
16 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Linq;
|
|
using System.Collections.Generic;
|
|
using System.IO.Compression;
|
|
using System.Threading;
|
|
using System.Diagnostics;
|
|
using ICSharpCode.SharpZipLib.BZip2;
|
|
using NDesk.Options;
|
|
|
|
namespace RSCacheTool
|
|
{
|
|
static class Program
|
|
{
|
|
static string cacheDir = Environment.ExpandEnvironmentVariables(@"%USERPROFILE%\jagexcache\runescape\LIVE\");
|
|
static string outDir = "cache\\";
|
|
|
|
static int Main(string[] args)
|
|
{
|
|
bool error = false;
|
|
|
|
bool help = false, extract = false, combine = false, overwrite = false, combineMergeIncomplete = false;
|
|
int extractArchive = -1, combineArchive = 40;
|
|
|
|
OptionSet argsParser = new OptionSet() {
|
|
{ "h", "show this message", val => { help = true; } },
|
|
|
|
{ "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); } },
|
|
|
|
{ "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; } },
|
|
};
|
|
|
|
List<string> otherArgs = argsParser.Parse(args);
|
|
|
|
for (int i = 0; i < otherArgs.Count; i++)
|
|
{
|
|
string parsedPath = otherArgs[i];
|
|
if (!parsedPath.EndsWith("\\"))
|
|
parsedPath += "\\";
|
|
|
|
parsedPath = Environment.ExpandEnvironmentVariables(parsedPath);
|
|
|
|
if (Directory.Exists(parsedPath))
|
|
{
|
|
if (i == 0)
|
|
outDir = parsedPath;
|
|
else if (i == 1)
|
|
cacheDir = parsedPath;
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("The directory: " + parsedPath + " is not valid.");
|
|
error = true;
|
|
}
|
|
}
|
|
|
|
if (args.Length == 0 || help)
|
|
{
|
|
Console.WriteLine(
|
|
"Usage: rscachetools [options] outDir cacheDir\n" +
|
|
"Provides various tools for extracting and manipulating RuneScape's cache files.\n" +
|
|
"\n" +
|
|
"Arguments:\n" +
|
|
"outDir - The directory in which all files generated by this tool will be placed. Default: cache\\\n" +
|
|
"cacheDir - The directory that contains all cache files. Default: %USERPROFILE%\\jagexcache\\runescape\\LIVE\\.\n" +
|
|
"\n" +
|
|
"Options:"
|
|
);
|
|
|
|
argsParser.WriteOptionDescriptions(Console.Out);
|
|
}
|
|
else if (!error)
|
|
{
|
|
if (extract)
|
|
ExtractFiles(extractArchive, overwrite);
|
|
|
|
if (combine)
|
|
CombineSounds(combineArchive, overwrite, combineMergeIncomplete);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rips all files from the cachefile and puts them (structured and given a fitting extension where possible) in the fileDir.
|
|
/// </summary>
|
|
static void ExtractFiles(int archive, bool overwriteExisting)
|
|
{
|
|
int startArchive = 0, endArchive = 255;
|
|
|
|
if (archive != -1)
|
|
{
|
|
startArchive = archive;
|
|
endArchive = archive;
|
|
}
|
|
|
|
using (FileStream cacheFile = File.Open(cacheDir + "main_file_cache.dat2", FileMode.Open, FileAccess.Read))
|
|
{
|
|
for (int archiveIndex = startArchive; archiveIndex <= endArchive; archiveIndex++)
|
|
{
|
|
string indexFileString = cacheDir + "main_file_cache.idx" + archiveIndex.ToString();
|
|
|
|
if (File.Exists(indexFileString))
|
|
{
|
|
FileStream indexFile = File.Open(indexFileString, FileMode.Open, FileAccess.Read);
|
|
|
|
int fileCount = (int)indexFile.Length / 6;
|
|
|
|
for (int fileIndex = 0; fileIndex < fileCount; fileIndex++)
|
|
{
|
|
bool fileError = false;
|
|
|
|
indexFile.Seek(fileIndex * 6, SeekOrigin.Begin);
|
|
|
|
uint fileSize = indexFile.ReadBytes(3);
|
|
long startChunkOffset = indexFile.ReadBytes(3) * 520L;
|
|
|
|
//Console.WriteLine("New file: archive: {0} file: {1} offset: {3} size: {2}", archiveIndex, fileIndex, fileSize, startChunkOffset);
|
|
|
|
if (fileSize > 0 && startChunkOffset > 0 && startChunkOffset + fileSize <= cacheFile.Length)
|
|
{
|
|
byte[] buffer = new byte[fileSize];
|
|
int writeOffset = 0;
|
|
long currentChunkOffset = startChunkOffset;
|
|
|
|
for (int chunkIndex = 0; writeOffset < fileSize && currentChunkOffset > 0; chunkIndex++)
|
|
{
|
|
cacheFile.Seek(currentChunkOffset, SeekOrigin.Begin);
|
|
|
|
int chunkSize;
|
|
int checksumFileIndex = 0;
|
|
|
|
if (fileIndex < 65536)
|
|
{
|
|
chunkSize = (int)Math.Min(512, fileSize - writeOffset);
|
|
}
|
|
else
|
|
{
|
|
//if file index exceeds 2 bytes, add 65536 and read 2(?) extra bytes
|
|
chunkSize = (int)Math.Min(510, fileSize - writeOffset);
|
|
|
|
cacheFile.ReadByte();
|
|
checksumFileIndex = (cacheFile.ReadByte() << 16);
|
|
}
|
|
|
|
checksumFileIndex += (int)cacheFile.ReadBytes(2);
|
|
int checksumChunkIndex = (int)cacheFile.ReadBytes(2);
|
|
long nextChunkOffset = cacheFile.ReadBytes(3) * 520L;
|
|
int checksumArchiveIndex = cacheFile.ReadByte();
|
|
|
|
//Console.WriteLine("Chunk {2}: archive: {3} file: {1} size: {0} nextoffset: {4}", chunkSize, checksumFileIndex, checksumChunkIndex, checksumArchiveIndex, nextChunkOffset);
|
|
|
|
if (checksumFileIndex == fileIndex && checksumChunkIndex == chunkIndex && checksumArchiveIndex == archiveIndex &&
|
|
nextChunkOffset >= 0 && nextChunkOffset < cacheFile.Length)
|
|
{
|
|
cacheFile.Read(buffer, writeOffset, chunkSize);
|
|
writeOffset += chunkSize;
|
|
currentChunkOffset = nextChunkOffset;
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Ignoring file because a chunk's checksum doesn't match, ideally should not happen.");
|
|
|
|
fileError = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!fileError)
|
|
{
|
|
//process file
|
|
string outFileDir = outDir + archiveIndex + "\\";
|
|
string outFileName = fileIndex.ToString();
|
|
byte[] tempBuffer;
|
|
|
|
//decompress gzip
|
|
if (buffer.Length > 10 && (buffer[9] << 8) + buffer[10] == 0x1f8b) //gzip
|
|
{
|
|
//remove the first 9 bytes cause they seem to be descriptors of sorts (no idea what they do but they are not part of the file)
|
|
tempBuffer = new byte[fileSize - 9];
|
|
Array.Copy(buffer, 9, tempBuffer, 0, fileSize - 9);
|
|
buffer = tempBuffer;
|
|
|
|
GZipStream decompressionStream = new GZipStream(new MemoryStream(buffer), CompressionMode.Decompress);
|
|
|
|
int readBytes;
|
|
tempBuffer = new byte[0];
|
|
|
|
do
|
|
{
|
|
byte[] readBuffer = new byte[100000];
|
|
readBytes = decompressionStream.Read(readBuffer, 0, 100000);
|
|
|
|
int storedBytes = tempBuffer.Length;
|
|
Array.Resize(ref tempBuffer, tempBuffer.Length + readBytes);
|
|
Array.Copy(readBuffer, 0, tempBuffer, storedBytes, readBytes);
|
|
}
|
|
while (readBytes == 100000);
|
|
|
|
buffer = tempBuffer;
|
|
|
|
Console.WriteLine("File decompressed as gzip.");
|
|
}
|
|
|
|
//decompress bzip2
|
|
if (buffer.Length > 14 && buffer[9] == 0x31 && buffer[10] == 0x41 && buffer[11] == 0x59 && buffer[12] == 0x26 && buffer[13] == 0x53 && buffer[14] == 0x59) //bzip2
|
|
{
|
|
//remove the first 9 bytes cause they seem to be descriptors of sorts (no idea what they do but they are not part of the file)
|
|
tempBuffer = new byte[fileSize - 9];
|
|
Array.Copy(buffer, 9, tempBuffer, 0, fileSize - 9);
|
|
buffer = tempBuffer;
|
|
|
|
//prepend file header
|
|
byte[] magic = new byte[] {
|
|
0x42, 0x5a, //BZ (signature)
|
|
0x68, //h (version)
|
|
0x31 //*100kB block-size
|
|
};
|
|
|
|
tempBuffer = new byte[magic.Length + buffer.Length];
|
|
magic.CopyTo(tempBuffer, 0);
|
|
buffer.CopyTo(tempBuffer, magic.Length);
|
|
buffer = tempBuffer;
|
|
|
|
BZip2InputStream decompressionStream = new BZip2InputStream(new MemoryStream(buffer));
|
|
|
|
int readBytes;
|
|
tempBuffer = new byte[0];
|
|
|
|
do
|
|
{
|
|
byte[] readBuffer = new byte[100000];
|
|
readBytes = decompressionStream.Read(readBuffer, 0, 100000);
|
|
|
|
int storedBytes = tempBuffer.Length;
|
|
Array.Resize(ref tempBuffer, tempBuffer.Length + readBytes);
|
|
Array.Copy(readBuffer, 0, tempBuffer, storedBytes, readBytes);
|
|
}
|
|
while (readBytes == 100000);
|
|
|
|
buffer = tempBuffer;
|
|
|
|
Console.WriteLine("File decompressed as bzip2.");
|
|
}
|
|
|
|
//detect ogg: OggS
|
|
if (buffer.Length > 3 && (buffer[0] << 24) + (buffer[1] << 16) + (buffer[2] << 8) + buffer[3] == 0x4f676753)
|
|
outFileName += ".ogg";
|
|
|
|
//detect ogg: 5 bytes - OggS
|
|
if (buffer.Length > 8 && (buffer[5] << 24) + (buffer[6] << 16) + (buffer[7] << 8) + buffer[8] == 0x4f676753)
|
|
{
|
|
tempBuffer = new byte[fileSize - 5];
|
|
Array.Copy(buffer, 5, tempBuffer, 0, fileSize - 5);
|
|
buffer = tempBuffer;
|
|
|
|
outFileName += ".ogg";
|
|
}
|
|
|
|
//detect jag: JAGA
|
|
if (buffer.Length > 3 && (buffer[0] << 24) + (buffer[1] << 16) + (buffer[2] << 8) + buffer[3] == 0x4a414741)
|
|
outFileName += ".jaga";
|
|
|
|
//detect jag: 5 bytes - JAGA
|
|
if (buffer.Length > 8 && (buffer[5] << 24) + (buffer[6] << 16) + (buffer[7] << 8) + buffer[8] == 0x4a414741)
|
|
{
|
|
tempBuffer = new byte[fileSize - 5];
|
|
Array.Copy(buffer, 5, tempBuffer, 0, fileSize - 5);
|
|
buffer = tempBuffer;
|
|
|
|
outFileName += ".jaga";
|
|
}
|
|
|
|
//detect png: .PNG
|
|
if (buffer.Length > 3 && (uint)(buffer[0] << 24) + (buffer[1] << 16) + (buffer[2] << 8) + buffer[3] == 0x89504e47)
|
|
outFileName += ".png";
|
|
|
|
//detect png: 5 bytes - .PNG
|
|
if (buffer.Length > 8 && (uint)(buffer[5] << 24) + (buffer[6] << 16) + (buffer[7] << 8) + buffer[8] == 0x89504e47)
|
|
{
|
|
tempBuffer = new byte[fileSize - 5];
|
|
Array.Copy(buffer, 5, tempBuffer, 0, fileSize - 5);
|
|
buffer = tempBuffer;
|
|
|
|
outFileName += ".png";
|
|
}
|
|
|
|
//create and write file
|
|
if (!Directory.Exists(outFileDir))
|
|
Directory.CreateDirectory(outFileDir);
|
|
|
|
//(over)write file
|
|
if (!File.Exists(outFileDir + outFileName) || overwriteExisting)
|
|
{
|
|
using (FileStream outFile = File.Open(outFileDir + outFileName, FileMode.Create, FileAccess.Write))
|
|
{
|
|
outFile.Write(buffer, 0, buffer.Length);
|
|
Console.WriteLine(outFileDir + outFileName);
|
|
}
|
|
}
|
|
else
|
|
Console.WriteLine("Skipping file because it already exists.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Ignoring file because of size or offset.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Console.WriteLine("Done extracting files.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Combines the sound files (.jag & .ogg) in the specified archive (40 for the build it was made on), and puts them into the soundtracks directory.
|
|
/// </summary>
|
|
static void CombineSounds(int archive, bool overwriteExisting, bool mergeIncomplete)
|
|
{
|
|
string archiveDir = outDir + archive + "\\";
|
|
string soundDir = outDir + "sound\\";
|
|
|
|
//gather all index files
|
|
string[] indexFiles = Directory.GetFiles(archiveDir, "*.jaga", SearchOption.TopDirectoryOnly);
|
|
|
|
//create directories
|
|
if (!Directory.Exists(soundDir + "incomplete\\"))
|
|
Directory.CreateDirectory(soundDir + "incomplete\\");
|
|
|
|
foreach (string indexFileString in indexFiles)
|
|
{
|
|
string indexFileIdString = Path.GetFileNameWithoutExtension(indexFileString);
|
|
|
|
bool incomplete = false;
|
|
List<string> chunkFiles = new List<string>();
|
|
|
|
using (FileStream indexFileStream = File.Open(indexFileString, FileMode.Open, FileAccess.Read, FileShare.Read))
|
|
{
|
|
indexFileStream.Seek(32, SeekOrigin.Begin);
|
|
|
|
while (indexFileStream.ReadBytes(4) != 0x4f676753)
|
|
{
|
|
uint fileId = indexFileStream.ReadBytes(4);
|
|
|
|
//check if the file exists and add it to the buffer if it does
|
|
if (File.Exists(archiveDir + fileId + ".ogg"))
|
|
chunkFiles.Add(archiveDir + fileId + ".ogg");
|
|
else
|
|
incomplete = true;
|
|
}
|
|
|
|
//copy the first chunk to a temp file so SoX can handle the combining
|
|
indexFileStream.Seek(-4, SeekOrigin.Current);
|
|
|
|
//wait till file is available
|
|
while (true)
|
|
{
|
|
try
|
|
{
|
|
using (FileStream tempIndexFile = File.Open("~index.ogg", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
|
|
{
|
|
indexFileStream.CopyTo(tempIndexFile);
|
|
break;
|
|
}
|
|
}
|
|
catch (IOException)
|
|
{
|
|
Thread.Sleep(200);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!incomplete || incomplete && mergeIncomplete)
|
|
{
|
|
string outFile = soundDir + (incomplete ? "incomplete\\" : "") + indexFileIdString + ".ogg";
|
|
|
|
if (!overwriteExisting && File.Exists(outFile))
|
|
Console.WriteLine("Skipping track because it already exists.");
|
|
else
|
|
{
|
|
//combine the files with sox
|
|
Console.WriteLine("Running SoX to concatenate ogg audio chunks.");
|
|
|
|
Process soxProcess = new Process();
|
|
soxProcess.StartInfo.FileName = "sox.exe";
|
|
|
|
soxProcess.StartInfo.Arguments = "--combine concatenate ~index.ogg";
|
|
chunkFiles.ForEach((str) =>
|
|
{
|
|
soxProcess.StartInfo.Arguments += " " + str;
|
|
});
|
|
soxProcess.StartInfo.Arguments += " " + soundDir + "incomplete\\" + indexFileIdString + ".ogg ";
|
|
soxProcess.StartInfo.UseShellExecute = false;
|
|
|
|
soxProcess.Start();
|
|
soxProcess.WaitForExit();
|
|
|
|
if (soxProcess.ExitCode == 0)
|
|
{
|
|
if (!incomplete)
|
|
{
|
|
//clear space
|
|
if (File.Exists(outFile))
|
|
File.Delete(outFile);
|
|
|
|
File.Move(soundDir + "incomplete\\" + indexFileIdString + ".ogg", outFile);
|
|
}
|
|
|
|
Console.WriteLine(outFile);
|
|
}
|
|
else
|
|
Console.WriteLine("SoX encountered error code " + soxProcess.ExitCode + " and probably didn't finish processing the files.");
|
|
}
|
|
}
|
|
else
|
|
Console.WriteLine("Skipping track because it's incomplete.");
|
|
}
|
|
|
|
//cleanup on isle 4
|
|
File.Delete("~index.ogg");
|
|
|
|
Console.WriteLine("Done combining sound.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static void FindInFiles(string needle)
|
|
{
|
|
int bufferSize = 10000;
|
|
byte[] buffer = new byte[bufferSize];
|
|
for (int archive = 0; archive < 256; archive++)
|
|
{
|
|
if (Directory.Exists(outDir + archive))
|
|
{
|
|
string[] fileNames = Directory.GetFiles(outDir + archive);
|
|
|
|
int i = 0;
|
|
foreach (string fileName in fileNames)
|
|
{
|
|
using (FileStream file = File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
|
|
{
|
|
int offset = 0;
|
|
int readBytes;
|
|
do
|
|
{
|
|
readBytes = file.Read(buffer, 0, bufferSize);
|
|
|
|
string readString = Encoding.Default.GetString(buffer, 0, readBytes);
|
|
int index = readString.IndexOf(needle, StringComparison.CurrentCultureIgnoreCase);
|
|
if (index != -1)
|
|
{
|
|
Console.WriteLine(fileName + " @ " + offset + index + "B");
|
|
Console.ReadLine();
|
|
}
|
|
|
|
//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;
|
|
}
|
|
while (readBytes == bufferSize);
|
|
}
|
|
|
|
Console.WriteLine("a" + archive + "f" + i + "/" + (fileNames.Length - 1));
|
|
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a given amount of bytes from the stream.
|
|
/// </summary>
|
|
public static uint ReadBytes(this Stream stream, byte bytes)
|
|
{
|
|
if (bytes == 0 || bytes > 4)
|
|
throw new ArgumentOutOfRangeException();
|
|
|
|
uint result = 0;
|
|
|
|
for (int i = 0; i < bytes; i++)
|
|
result += (uint)stream.ReadByte() << (bytes - i - 1) * 8;
|
|
|
|
return result;
|
|
}
|
|
}
|
|
}
|