mirror of
https://github.com/Marlamin/BuildBackup
synced 2026-08-14 06:29:04 -04:00
3326 lines
143 KiB
C#
3326 lines
143 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace BuildBackup
|
|
{
|
|
class Program
|
|
{
|
|
private static readonly Uri baseUrl = new("https://us.version.battle.net/");
|
|
|
|
private static string[] checkPrograms;
|
|
private static string[] backupPrograms;
|
|
|
|
private static VersionsFile versions;
|
|
private static CdnsFile cdns;
|
|
private static GameBlobFile productConfig;
|
|
private static BuildConfigFile buildConfig;
|
|
private static BuildConfigFile[] cdnBuildConfigs;
|
|
private static CDNConfigFile cdnConfig;
|
|
private static EncodingFile encoding;
|
|
private static InstallFile install;
|
|
private static DownloadFile download;
|
|
private static RootFile root;
|
|
private static PatchFile patch;
|
|
|
|
private static bool fullDownload = true;
|
|
|
|
private static bool overrideVersions;
|
|
private static string overrideBuildconfig;
|
|
private static string overrideCDNconfig;
|
|
|
|
private static Dictionary<string, IndexEntry> indexDictionary = [];
|
|
private static Dictionary<string, IndexEntry> patchIndexDictionary = [];
|
|
private static Dictionary<string, IndexEntry> fileIndexList = [];
|
|
private static Dictionary<string, IndexEntry> patchFileIndexList = [];
|
|
private static ReaderWriterLockSlim cacheLock = new();
|
|
|
|
public static Dictionary<string, byte[]> cachedArmadilloKeys = [];
|
|
|
|
private static readonly CDN cdn = new();
|
|
|
|
static async Task Main(string[] args)
|
|
{
|
|
cdn.cacheDir = SettingsManager.cacheDir;
|
|
cdn.client = new HttpClient
|
|
{
|
|
Timeout = new TimeSpan(0, 5, 0)
|
|
};
|
|
|
|
cdn.cdnList = [
|
|
//"blzddist1-a.akamaihd.net", // Akamai first
|
|
"level3.blizzard.com", // Level3
|
|
"eu.cdn.blizzard.com", // Official EU CDN
|
|
"us.cdn.blizzard.com", // Official US CDN
|
|
//"kr.cdn.blizzard.com", // Official KR CDN
|
|
"cdn.blizzard.com", // Official regionless CDN
|
|
//"blizzard.nefficient.co.kr", // Korea
|
|
//"cdn.arctium.tools", // Arctium archive
|
|
//"casc.wago.tools", // Wago archive
|
|
//"tact.mirror.reliquaryhq.com", // ReliquaryHQ archive
|
|
];
|
|
|
|
// Check if cache/backup directory exists
|
|
try
|
|
{
|
|
if (!Directory.Exists(cdn.cacheDir)) { Directory.CreateDirectory(cdn.cacheDir); }
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Error creating cache directory: " + e.Message);
|
|
Console.ReadKey();
|
|
return;
|
|
}
|
|
|
|
#region Commands
|
|
if (args.Length > 0)
|
|
{
|
|
if (args[0] == "dumpinfo")
|
|
{
|
|
if (args.Length != 4) throw new Exception("Not enough arguments. Need mode, product, buildconfig, cdnconfig");
|
|
|
|
cdns = GetCDNs(args[1]);
|
|
|
|
buildConfig = GetBuildConfig(cdns.entries[0].path, args[2]);
|
|
if (string.IsNullOrWhiteSpace(buildConfig.buildName)) { Console.WriteLine("Invalid buildConfig!"); }
|
|
|
|
cdnConfig = GetCDNconfig(cdns.entries[0].path, args[3]);
|
|
if (cdnConfig.archives == null) { Console.WriteLine("Invalid cdnConfig"); }
|
|
|
|
encoding = GetEncoding(cdns.entries[0].path, buildConfig.encoding[1]).Result;
|
|
|
|
string rootKey = "";
|
|
string downloadKey = "";
|
|
string installKey = "";
|
|
|
|
if (buildConfig.download.Length == 2)
|
|
{
|
|
downloadKey = buildConfig.download[1];
|
|
Console.WriteLine("download = " + downloadKey.ToLower());
|
|
}
|
|
|
|
if (buildConfig.install.Length == 2)
|
|
{
|
|
installKey = buildConfig.install[1];
|
|
Console.WriteLine("install = " + installKey.ToLower());
|
|
}
|
|
|
|
Dictionary<string, string> hashes = [];
|
|
|
|
foreach (var entry in encoding.aEntries)
|
|
{
|
|
if (entry.cKey.Equals(buildConfig.root, StringComparison.OrdinalIgnoreCase)) { rootKey = entry.eKeys[0]; Console.WriteLine("root = " + entry.eKeys[0].ToLower()); }
|
|
if (string.IsNullOrEmpty(downloadKey) && entry.cKey.Equals(buildConfig.download[0], StringComparison.OrdinalIgnoreCase)) { downloadKey = entry.eKeys[0]; Console.WriteLine("download = " + entry.eKeys[0].ToLower()); }
|
|
if (string.IsNullOrEmpty(installKey) && entry.cKey.Equals(buildConfig.install[0], StringComparison.OrdinalIgnoreCase)) { installKey = entry.eKeys[0]; Console.WriteLine("install = " + entry.eKeys[0].ToLower()); }
|
|
hashes.TryAdd(entry.eKeys[0], entry.cKey);
|
|
}
|
|
|
|
GetIndexes(cdns.entries[0].path, cdnConfig.archives);
|
|
|
|
foreach (var entry in indexDictionary)
|
|
{
|
|
hashes.Remove(entry.Key.ToUpper());
|
|
}
|
|
|
|
int h = 1;
|
|
var tot = hashes.Count;
|
|
|
|
foreach (var entry in hashes)
|
|
{
|
|
Console.WriteLine("unarchived = " + entry.Key.ToLower());
|
|
h++;
|
|
}
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumproot")
|
|
{
|
|
if (args.Length != 2) throw new Exception("Not enough arguments. Need mode, root");
|
|
cdns = GetCDNs("wow");
|
|
|
|
var fileNames = new Dictionary<ulong, string>();
|
|
UpdateListfile();
|
|
var hasher = new Jenkins96();
|
|
foreach (var line in File.ReadLines("listfile.txt"))
|
|
{
|
|
fileNames.Add(hasher.ComputeHash(line), line);
|
|
}
|
|
|
|
var root = GetRoot(cdns.entries[0].path + "/", args[1], true);
|
|
|
|
foreach (var entry in root.entriesFDID)
|
|
{
|
|
foreach (var subentry in entry.Value)
|
|
{
|
|
if (subentry.contentFlags.HasFlag(ContentFlags.LowViolence)) continue;
|
|
|
|
if (!subentry.localeFlags.HasFlag(LocaleFlags.All_WoW) && !subentry.localeFlags.HasFlag(LocaleFlags.enUS))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (entry.Key > 0 && fileNames.ContainsKey(entry.Key))
|
|
{
|
|
Console.WriteLine(fileNames[entry.Key] + ";" + entry.Key.ToString("x").PadLeft(16, '0') + ";" + subentry.fileDataID + ";" + Convert.ToHexString(subentry.md5).ToLower());
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("unknown;" + entry.Key.ToString("x").PadLeft(16, '0') + ";" + subentry.fileDataID + ";" + Convert.ToHexString(subentry.md5).ToLower());
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumproot2")
|
|
{
|
|
if (args.Length < 2) throw new Exception("Not enough arguments. Need mode, root");
|
|
|
|
var product = "wow";
|
|
if (args.Length == 3)
|
|
{
|
|
product = args[2];
|
|
}
|
|
|
|
cdns = GetCDNs(product);
|
|
|
|
var hasher = new Jenkins96();
|
|
UpdateListfile();
|
|
var hashes = File
|
|
.ReadLines("listfile.txt")
|
|
.Select<string, Tuple<ulong, string>>(fileName => new Tuple<ulong, string>(hasher.ComputeHash(fileName), fileName))
|
|
.ToDictionary(key => key.Item1, value => value.Item2);
|
|
|
|
var root = GetRoot(cdns.entries[0].path + "/", args[1], true);
|
|
|
|
Action<RootEntry> print = delegate (RootEntry entry)
|
|
{
|
|
var lookup = "";
|
|
var fileName = "";
|
|
|
|
if (entry.lookup > 0)
|
|
{
|
|
lookup = entry.lookup.ToString("x").PadLeft(16, '0');
|
|
fileName = hashes.TryGetValue(entry.lookup, out string value) ? value : "";
|
|
}
|
|
|
|
var md5 = Convert.ToHexString(entry.md5).ToLower();
|
|
var dataId = entry.fileDataID;
|
|
Console.WriteLine("{0};{1};{2};{3}", fileName, lookup, dataId, md5);
|
|
};
|
|
|
|
foreach (var entry in root.entriesFDID)
|
|
{
|
|
RootEntry? prioritizedEntry = entry.Value.FirstOrDefault(subentry =>
|
|
subentry.contentFlags.HasFlag(ContentFlags.LowViolence) == false && (subentry.localeFlags.HasFlag(LocaleFlags.All_WoW) || subentry.localeFlags.HasFlag(LocaleFlags.enUS))
|
|
);
|
|
|
|
var selectedEntry = (prioritizedEntry.Value.md5 != null) ? prioritizedEntry.Value : entry.Value.First();
|
|
print(selectedEntry);
|
|
}
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumproot3")
|
|
{
|
|
if (args.Length != 2) throw new Exception("Not enough arguments. Need mode, root");
|
|
cdns = GetCDNs("wow");
|
|
|
|
var root = GetRoot(cdns.entries[0].path + "/", args[1], true);
|
|
|
|
foreach (var entry in root.entriesFDID)
|
|
{
|
|
foreach (var subentry in entry.Value)
|
|
{
|
|
Console.WriteLine(subentry.fileDataID + ";" + Convert.ToHexString(subentry.md5).ToLower() + ";" + subentry.localeFlags.ToString() + ";" + subentry.contentFlags.ToString());
|
|
}
|
|
}
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumproot4")
|
|
{
|
|
if (args.Length != 3) throw new Exception("Not enough arguments. Need mode, product, root");
|
|
|
|
var root = GetRoot("tpr/" + args[1] + "/", args[2], true);
|
|
|
|
foreach (var entry in root.entriesFDID)
|
|
{
|
|
foreach (var subentry in entry.Value)
|
|
{
|
|
Console.WriteLine(subentry.fileDataID + ";" + Convert.ToHexString(subentry.md5).ToLower() + ";" + subentry.localeFlags.ToString() + ";" + subentry.contentFlags.ToString());
|
|
}
|
|
}
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "calchash")
|
|
{
|
|
var hasher = new Jenkins96();
|
|
var hash = hasher.ComputeHash(args[1]);
|
|
Console.WriteLine(hash + " " + hash.ToString("x").PadLeft(16, '0'));
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "calchashlistfile")
|
|
{
|
|
string target = "";
|
|
|
|
if (args.Length == 2 && File.Exists(args[1]))
|
|
{
|
|
target = args[1];
|
|
}
|
|
else
|
|
{
|
|
UpdateListfile();
|
|
target = "listfile.txt";
|
|
}
|
|
|
|
var hasher = new Jenkins96();
|
|
|
|
foreach (var line in File.ReadLines(target))
|
|
{
|
|
if (string.IsNullOrEmpty(line)) continue;
|
|
var hash = hasher.ComputeHash(line);
|
|
Console.WriteLine(line + " = " + hash.ToString("x").PadLeft(16, '0'));
|
|
}
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumpinstall")
|
|
{
|
|
if (args.Length != 3) throw new Exception("Not enough arguments. Need mode, product, install");
|
|
|
|
cdns = GetCDNs(args[1]);
|
|
install = GetInstall(cdns.entries[0].path + "/", args[2], true);
|
|
foreach (var entry in install.entries)
|
|
{
|
|
Console.WriteLine(entry.name + " (size: " + entry.size + ", md5: " + Convert.ToHexString(entry.contentHash).ToLower() + ", tags: " + string.Join(",", entry.tags) + ")");
|
|
}
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumpdownload")
|
|
{
|
|
if (args.Length != 3) throw new Exception("Not enough arguments. Need mode, product, download");
|
|
|
|
cdns = GetCDNs(args[1]);
|
|
var download = GetDownload(cdns.entries[0].path + "/", args[2], true);
|
|
foreach (var entry in download.entries)
|
|
{
|
|
Console.WriteLine(entry.eKey + " (size: " + entry.size + ", priority: " + entry.priority + ", flags: " + entry.flags + ")");
|
|
}
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumpdecodedencoding")
|
|
{
|
|
if (args.Length != 3) throw new Exception("Not enough arguments. Need mode, product, encoding");
|
|
|
|
encoding = GetEncoding(args[1], args[2], 0, true, false, false).Result;
|
|
foreach (var entry in encoding.aEntries)
|
|
{
|
|
for (var i = 0; i < entry.keyCount; i++)
|
|
{
|
|
var table2Entry = encoding.bEntries[entry.eKeys[i]];
|
|
Console.WriteLine(entry.cKey.ToLower() + " " + entry.eKeys[i].ToLower() + " " + entry.keyCount + " " + entry.size + " " + encoding.stringBlockEntries[table2Entry.stringIndex]);
|
|
}
|
|
}
|
|
Console.WriteLine("ENCODINGESPEC " + encoding.encodingESpec);
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumpencoding")
|
|
{
|
|
if (args.Length != 3) throw new Exception("Not enough arguments. Need mode, product, encoding");
|
|
|
|
encoding = GetEncoding("tpr/" + args[1] + "/", args[2], 0, true).Result;
|
|
foreach (var entry in encoding.aEntries)
|
|
{
|
|
for (var i = 0; i < entry.keyCount; i++)
|
|
{
|
|
var table2Entry = encoding.bEntries[entry.eKeys[i]];
|
|
Console.WriteLine(entry.cKey.ToLower() + " " + entry.eKeys[i].ToLower() + " " + entry.keyCount + " " + entry.size + " " + encoding.stringBlockEntries[table2Entry.stringIndex]);
|
|
}
|
|
}
|
|
Console.WriteLine("ENCODINGESPEC " + encoding.encodingESpec);
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumpconfig")
|
|
{
|
|
if (args.Length != 3) throw new Exception("Not enough arguments. Need mode, product, hash");
|
|
var product = args[1];
|
|
var hash = Path.GetFileNameWithoutExtension(args[2]);
|
|
var content = Encoding.UTF8.GetString(cdn.Get("tpr/" + product + "/config/" + hash[0] + hash[1] + "/" + hash[2] + hash[3] + "/" + hash).Result);
|
|
Console.WriteLine(content);
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "extractfilebycontenthash" || args[0] == "extractrawfilebycontenthash")
|
|
{
|
|
if (args.Length != 6) throw new Exception("Not enough arguments. Need mode, product, buildconfig, cdnconfig, contenthash, outname");
|
|
|
|
cdns = GetCDNs(args[1]);
|
|
|
|
args[4] = args[4].ToLower();
|
|
|
|
buildConfig = GetBuildConfig(cdns.entries[0].path, args[2]);
|
|
if (string.IsNullOrWhiteSpace(buildConfig.buildName)) { Console.WriteLine("Invalid buildConfig!"); }
|
|
|
|
encoding = GetEncoding(cdns.entries[0].path, buildConfig.encoding[1]).Result;
|
|
|
|
string target = "";
|
|
|
|
foreach (var entry in encoding.aEntries)
|
|
{
|
|
if (entry.cKey.Equals(args[4], StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
target = entry.eKeys[0].ToLower();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(target))
|
|
{
|
|
throw new Exception("File not found in encoding!");
|
|
}
|
|
|
|
cdnConfig = GetCDNconfig(cdns.entries[0].path, args[3]);
|
|
|
|
GetIndexes(cdns.entries[0].path, cdnConfig.archives);
|
|
|
|
if (args[0] == "extractrawfilebycontenthash")
|
|
{
|
|
var unarchivedName = Path.Combine(cdn.cacheDir, cdns.entries[0].path, "data", target[0] + "" + target[1], target[2] + "" + target[3], target);
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(unarchivedName));
|
|
|
|
File.WriteAllBytes(unarchivedName, RetrieveFileBytes(target, true, cdns.entries[0].path));
|
|
}
|
|
else
|
|
{
|
|
File.WriteAllBytes(args[5], RetrieveFileBytes(target, false, cdns.entries[0].path));
|
|
}
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "extractfilebyencodingkey")
|
|
{
|
|
if (args.Length != 5) throw new Exception("Not enough arguments. Need mode, product, cdnconfig, contenthash, outname");
|
|
|
|
cdns = GetCDNs(args[1]);
|
|
cdnConfig = GetCDNconfig(cdns.entries[0].path, args[2]);
|
|
|
|
var target = args[3];
|
|
|
|
GetIndexes(cdns.entries[0].path, cdnConfig.archives);
|
|
|
|
File.WriteAllBytes(args[4], RetrieveFileBytes(target, false, cdns.entries[0].path, true));
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "extractfilesbylist")
|
|
{
|
|
if (args.Length != 5) throw new Exception("Not enough arguments. Need mode, buildconfig, cdnconfig, basedir, list");
|
|
|
|
buildConfig = GetBuildConfig(Path.Combine("tpr", "wow"), args[1]);
|
|
if (string.IsNullOrWhiteSpace(buildConfig.buildName)) { Console.WriteLine("Invalid buildConfig!"); }
|
|
|
|
encoding = GetEncoding(Path.Combine("tpr", "wow"), buildConfig.encoding[1]).Result;
|
|
|
|
var basedir = args[3];
|
|
|
|
var lines = File.ReadLines(args[4]);
|
|
|
|
cdnConfig = GetCDNconfig(Path.Combine("tpr", "wow"), args[2]);
|
|
|
|
GetIndexes(Path.Combine("tpr", "wow"), cdnConfig.archives);
|
|
|
|
foreach (var line in lines)
|
|
{
|
|
var splitLine = line.Split(',');
|
|
var contenthash = splitLine[0];
|
|
var filename = splitLine[1];
|
|
|
|
if (!Directory.Exists(Path.Combine(basedir, Path.GetDirectoryName(filename))))
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(basedir, Path.GetDirectoryName(filename)));
|
|
}
|
|
|
|
Console.WriteLine(filename);
|
|
|
|
string target = "";
|
|
|
|
foreach (var entry in encoding.aEntries)
|
|
{
|
|
if (entry.cKey.Equals(contenthash, StringComparison.OrdinalIgnoreCase)) { target = entry.eKeys[0].ToLower(); Console.WriteLine("Found target: " + target); break; }
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(target))
|
|
{
|
|
Console.WriteLine("File " + filename + " (" + contenthash + ") not found in encoding!");
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
File.WriteAllBytes(Path.Combine(basedir, filename), RetrieveFileBytes(target));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e.Message);
|
|
}
|
|
}
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "cachebuild")
|
|
{
|
|
if (args.Length != 4) throw new Exception("Not enough arguments. Need mode, buildconfig, cdnconfig, basedir");
|
|
|
|
buildConfig = GetBuildConfig(Path.Combine("tpr", "wow"), args[1]);
|
|
if (string.IsNullOrWhiteSpace(buildConfig.buildName)) { Console.WriteLine("Invalid buildConfig!"); }
|
|
|
|
encoding = GetEncoding(Path.Combine("tpr", "wow"), buildConfig.encoding[1]).Result;
|
|
|
|
cdnConfig = GetCDNconfig(Path.Combine("tpr", "wow"), args[2]);
|
|
|
|
GetIndexes(Path.Combine("tpr", "wow"), cdnConfig.archives);
|
|
|
|
var basedir = args[3];
|
|
var rootHash = "";
|
|
|
|
foreach (var entry in encoding.aEntries)
|
|
{
|
|
if (entry.cKey.Equals(buildConfig.root, StringComparison.OrdinalIgnoreCase)) { rootHash = entry.eKeys[0].ToLower(); break; }
|
|
}
|
|
|
|
var fdidList = new List<uint>();
|
|
|
|
root = GetRoot(Path.Combine("tpr", "wow"), rootHash, true);
|
|
|
|
if (File.Exists(Path.Combine(basedir, "lastextractedroot.txt")))
|
|
{
|
|
var oldRootHash = File.ReadAllLines(Path.Combine(basedir, "lastextractedroot.txt"))[0];
|
|
var oldRoot = GetRoot(Path.Combine("tpr", "wow"), oldRootHash, true);
|
|
|
|
var rootFromEntries = oldRoot.entriesFDID;
|
|
var fromEntries = rootFromEntries.Keys.ToHashSet();
|
|
|
|
var rootToEntries = root.entriesFDID;
|
|
var toEntries = rootToEntries.Keys.ToHashSet();
|
|
|
|
var commonEntries = fromEntries.Intersect(toEntries);
|
|
var removedEntries = fromEntries.Except(commonEntries);
|
|
var addedEntries = toEntries.Except(commonEntries);
|
|
|
|
static RootEntry prioritize(List<RootEntry> entries)
|
|
{
|
|
var prioritized = entries.FirstOrDefault(subentry =>
|
|
subentry.contentFlags.HasFlag(ContentFlags.LowViolence) == false && (subentry.localeFlags.HasFlag(LocaleFlags.All_WoW) || subentry.localeFlags.HasFlag(LocaleFlags.enUS))
|
|
);
|
|
|
|
if (prioritized.fileDataID != 0)
|
|
{
|
|
return prioritized;
|
|
}
|
|
else
|
|
{
|
|
return entries.First();
|
|
}
|
|
}
|
|
|
|
var addedFiles = addedEntries.Select(entry => rootToEntries[entry]).Select(prioritize);
|
|
var removedFiles = removedEntries.Select(entry => rootFromEntries[entry]).Select(prioritize);
|
|
|
|
var modifiedFiles = new List<RootEntry>();
|
|
|
|
foreach (var entry in commonEntries)
|
|
{
|
|
var originalFile = prioritize(rootFromEntries[entry]);
|
|
var patchedFile = prioritize(rootToEntries[entry]);
|
|
|
|
if (!originalFile.md5.SequenceEqual(patchedFile.md5))
|
|
{
|
|
modifiedFiles.Add(patchedFile);
|
|
}
|
|
else if (originalFile.contentFlags.HasFlag(ContentFlags.Encrypted))
|
|
{
|
|
modifiedFiles.Add(patchedFile);
|
|
}
|
|
}
|
|
|
|
Console.WriteLine($"Added: {addedEntries.Count()}, removed: {removedFiles.Count()}, modified: {modifiedFiles.Count}, common: {commonEntries.Count()}");
|
|
|
|
fdidList.AddRange(addedFiles.Select(x => x.fileDataID));
|
|
fdidList.AddRange(modifiedFiles.Select(x => x.fileDataID));
|
|
}
|
|
else
|
|
{
|
|
fdidList.AddRange(root.entriesFDID.Keys);
|
|
}
|
|
|
|
Console.WriteLine("Looking up in root..");
|
|
|
|
var encodingList = new Dictionary<string, List<string>>();
|
|
|
|
foreach (var entry in root.entriesFDID)
|
|
{
|
|
foreach (var subentry in entry.Value)
|
|
{
|
|
if (subentry.contentFlags.HasFlag(ContentFlags.LowViolence))
|
|
continue;
|
|
|
|
if (!subentry.localeFlags.HasFlag(LocaleFlags.All_WoW) && !subentry.localeFlags.HasFlag(LocaleFlags.enUS))
|
|
continue;
|
|
|
|
if (fdidList.Contains(subentry.fileDataID))
|
|
{
|
|
var cleanContentHash = Convert.ToHexString(subentry.md5).ToLower();
|
|
|
|
if (encodingList.TryGetValue(cleanContentHash, out List<string> value))
|
|
{
|
|
value.Add(subentry.fileDataID.ToString());
|
|
}
|
|
else
|
|
{
|
|
encodingList.Add(cleanContentHash, [subentry.fileDataID.ToString()]);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var fileList = new Dictionary<string, List<string>>();
|
|
|
|
Console.WriteLine("Looking up in encoding..");
|
|
foreach (var encodingEntry in encoding.aEntries)
|
|
{
|
|
string target = "";
|
|
|
|
if (encodingList.ContainsKey(encodingEntry.cKey.ToLower()))
|
|
{
|
|
target = encodingEntry.eKeys[0].ToLower();
|
|
//Console.WriteLine(target);
|
|
foreach (var subName in encodingList[encodingEntry.cKey.ToLower()])
|
|
{
|
|
if (fileList.TryGetValue(target, out List<string> value))
|
|
{
|
|
value.Add(subName);
|
|
}
|
|
else
|
|
{
|
|
fileList.Add(target, [subName]);
|
|
}
|
|
}
|
|
encodingList.Remove(encodingEntry.cKey.ToLower());
|
|
}
|
|
}
|
|
|
|
var archivedFileList = new Dictionary<string, Dictionary<string, List<string>>>();
|
|
var unarchivedFileList = new Dictionary<string, List<string>>();
|
|
|
|
Console.WriteLine("Looking up in indexes..");
|
|
foreach (var fileEntry in fileList)
|
|
{
|
|
if (!indexDictionary.TryGetValue(fileEntry.Key.ToUpper(), out IndexEntry entry))
|
|
{
|
|
unarchivedFileList.Add(fileEntry.Key, fileEntry.Value);
|
|
}
|
|
|
|
var index = cdnConfig.archives[entry.index];
|
|
if (!archivedFileList.TryGetValue(index, out Dictionary<string, List<string>> value))
|
|
{
|
|
value = [];
|
|
archivedFileList.Add(index, value);
|
|
}
|
|
|
|
value.Add(fileEntry.Key, fileEntry.Value);
|
|
}
|
|
|
|
var extractedFiles = 0;
|
|
var totalFiles = fileList.Count;
|
|
|
|
Console.WriteLine("Extracting " + unarchivedFileList.Count + " unarchived files..");
|
|
foreach (var fileEntry in unarchivedFileList)
|
|
{
|
|
var target = fileEntry.Key;
|
|
|
|
foreach (var filename in fileEntry.Value)
|
|
{
|
|
if (!Directory.Exists(Path.Combine(basedir, rootHash, Path.GetDirectoryName(filename))))
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(basedir, rootHash, Path.GetDirectoryName(filename)));
|
|
}
|
|
}
|
|
|
|
var unarchivedName = Path.Combine(cdn.cacheDir, "tpr", "wow", "data", target[0] + "" + target[1], target[2] + "" + target[3], target);
|
|
if (File.Exists(unarchivedName))
|
|
{
|
|
foreach (var filename in fileEntry.Value)
|
|
{
|
|
try
|
|
{
|
|
File.WriteAllBytes(Path.Combine(basedir, rootHash, filename), BLTE.Parse(File.ReadAllBytes(unarchivedName)));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e.Message);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Unarchived file does not exist " + unarchivedName + ", cannot extract " + string.Join(',', fileEntry.Value));
|
|
}
|
|
|
|
extractedFiles++;
|
|
|
|
if (extractedFiles % 100 == 0)
|
|
{
|
|
Console.WriteLine("[" + DateTime.Now.ToString() + "] Extracted " + extractedFiles + " out of " + totalFiles + " files");
|
|
}
|
|
}
|
|
|
|
foreach (var archiveEntry in archivedFileList)
|
|
{
|
|
var archiveName = Path.Combine(cdn.cacheDir, "tpr", "wow", "data", archiveEntry.Key[0] + "" + archiveEntry.Key[1], archiveEntry.Key[2] + "" + archiveEntry.Key[3], archiveEntry.Key);
|
|
Console.WriteLine("[" + DateTime.Now.ToString() + "] Extracting " + archiveEntry.Value.Count + " files from archive " + archiveEntry.Key + "..");
|
|
|
|
using (var stream = new MemoryStream(File.ReadAllBytes(archiveName)))
|
|
{
|
|
foreach (var fileEntry in archiveEntry.Value)
|
|
{
|
|
var target = fileEntry.Key;
|
|
|
|
foreach (var filename in fileEntry.Value)
|
|
{
|
|
if (!Directory.Exists(Path.Combine(basedir, rootHash, Path.GetDirectoryName(filename))))
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(basedir, rootHash, Path.GetDirectoryName(filename)));
|
|
}
|
|
}
|
|
|
|
if (indexDictionary.TryGetValue(target.ToUpper(), out IndexEntry entry))
|
|
{
|
|
foreach (var filename in fileEntry.Value)
|
|
{
|
|
try
|
|
{
|
|
stream.Seek(entry.offset, SeekOrigin.Begin);
|
|
|
|
if (entry.offset > stream.Length || entry.offset + entry.size > stream.Length)
|
|
{
|
|
throw new Exception("File is beyond archive length, incomplete archive!");
|
|
}
|
|
|
|
var archiveBytes = new byte[entry.size];
|
|
stream.Read(archiveBytes, 0, (int)entry.size);
|
|
File.WriteAllBytes(Path.Combine(basedir, rootHash, filename), BLTE.Parse(archiveBytes));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e.Message);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("!!!!! Unable to find " + fileEntry.Key + " (" + fileEntry.Value[0] + ") in archives!");
|
|
}
|
|
|
|
extractedFiles++;
|
|
|
|
if (extractedFiles % 1000 == 0)
|
|
{
|
|
Console.WriteLine("[" + DateTime.Now.ToString() + "] Extracted " + extractedFiles + " out of " + totalFiles + " files");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
File.WriteAllText(Path.Combine(basedir, "lastextractedroot.txt"), rootHash);
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "extractfilesbyfnamelist" || args[0] == "extractfilesbyfdidlist")
|
|
{
|
|
if (args.Length < 5) throw new Exception("Not enough arguments. Need mode, buildconfig, cdnconfig, basedir, list, (product)");
|
|
|
|
var product = "wow";
|
|
if (args.Length == 6)
|
|
product = args[5];
|
|
|
|
buildConfig = GetBuildConfig("tpr/" + product, args[1]);
|
|
if (string.IsNullOrWhiteSpace(buildConfig.buildName)) { Console.WriteLine("Invalid buildConfig!"); }
|
|
|
|
encoding = GetEncoding("tpr/" + product, buildConfig.encoding[1]).Result;
|
|
|
|
cdnConfig = GetCDNconfig("tpr/" + product, args[2]);
|
|
|
|
GetIndexes("tpr/" + product, cdnConfig.archives);
|
|
|
|
var basedir = args[3];
|
|
|
|
var lines = File.ReadLines(args[4]);
|
|
|
|
var rootHash = "";
|
|
|
|
foreach (var entry in encoding.aEntries)
|
|
{
|
|
if (entry.cKey.Equals(buildConfig.root, StringComparison.OrdinalIgnoreCase)) { rootHash = entry.eKeys[0].ToLower(); break; }
|
|
}
|
|
|
|
var hasher = new Jenkins96();
|
|
var nameList = new Dictionary<ulong, string>();
|
|
var fdidList = new Dictionary<uint, string>();
|
|
|
|
if (args[0] == "extractfilesbyfnamelist")
|
|
{
|
|
foreach (var line in lines)
|
|
{
|
|
var hash = hasher.ComputeHash(line);
|
|
nameList.Add(hash, line);
|
|
}
|
|
}
|
|
else if (args[0] == "extractfilesbyfdidlist")
|
|
{
|
|
foreach (var line in lines)
|
|
{
|
|
if (string.IsNullOrEmpty(line))
|
|
continue;
|
|
|
|
var expl = line.Split(';');
|
|
if (expl.Length == 1)
|
|
{
|
|
fdidList.Add(uint.Parse(expl[0]), expl[0]);
|
|
}
|
|
else
|
|
{
|
|
fdidList.Add(uint.Parse(expl[0]), expl[1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
Console.WriteLine("Looking up " + nameList.Count + " named files and " + fdidList.Count + " unnamed files in root..");
|
|
|
|
root = GetRoot("tpr/" + product, rootHash, true);
|
|
|
|
var encodingList = new Dictionary<string, List<string>>();
|
|
var chashList = new Dictionary<uint, string>();
|
|
|
|
foreach (var entry in root.entriesFDID)
|
|
{
|
|
var subentry = entry.Value.Where(x => x.contentFlags.HasFlag(ContentFlags.LowViolence) == false && x.localeFlags.HasFlag(LocaleFlags.All_WoW) || x.localeFlags.HasFlag(LocaleFlags.enUS)).FirstOrDefault();
|
|
|
|
if (subentry.md5 == null)
|
|
subentry = entry.Value.First();
|
|
|
|
if (args[0] == "extractfilesbyfnamelist")
|
|
{
|
|
if (nameList.ContainsKey(subentry.lookup))
|
|
{
|
|
var cleanContentHash = Convert.ToHexString(subentry.md5).ToLower();
|
|
|
|
if (encodingList.TryGetValue(cleanContentHash, out List<string> value))
|
|
{
|
|
value.Add(nameList[subentry.lookup]);
|
|
}
|
|
else
|
|
{
|
|
encodingList.Add(cleanContentHash, [nameList[subentry.lookup]]);
|
|
}
|
|
}
|
|
}
|
|
else if (args[0] == "extractfilesbyfdidlist")
|
|
{
|
|
if (fdidList.ContainsKey(subentry.fileDataID))
|
|
{
|
|
var cleanContentHash = Convert.ToHexString(subentry.md5).ToLower();
|
|
|
|
if (encodingList.TryGetValue(cleanContentHash, out List<string> value))
|
|
{
|
|
value.Add(fdidList[subentry.fileDataID]);
|
|
}
|
|
else
|
|
{
|
|
encodingList.Add(cleanContentHash, [fdidList[subentry.fileDataID]]);
|
|
}
|
|
|
|
chashList.TryAdd(subentry.fileDataID, cleanContentHash);
|
|
}
|
|
}
|
|
}
|
|
|
|
var fileList = new Dictionary<string, List<string>>();
|
|
|
|
Console.WriteLine("Looking up " + encodingList.Count + " files in encoding..");
|
|
foreach (var encodingEntry in encoding.aEntries)
|
|
{
|
|
string target = "";
|
|
|
|
if (encodingList.ContainsKey(encodingEntry.cKey.ToLower()))
|
|
{
|
|
target = encodingEntry.eKeys[0].ToLower();
|
|
//Console.WriteLine(target);
|
|
foreach (var subName in encodingList[encodingEntry.cKey.ToLower()])
|
|
{
|
|
if (fileList.TryGetValue(target, out List<string> value))
|
|
{
|
|
value.Add(subName);
|
|
}
|
|
else
|
|
{
|
|
fileList.Add(target, [subName]);
|
|
}
|
|
}
|
|
encodingList.Remove(encodingEntry.cKey.ToLower());
|
|
}
|
|
}
|
|
|
|
var archivedFileList = new Dictionary<string, Dictionary<string, List<string>>>();
|
|
var unarchivedFileList = new Dictionary<string, List<string>>();
|
|
|
|
Console.WriteLine("Looking up in indexes..");
|
|
foreach (var fileEntry in fileList)
|
|
{
|
|
if (!indexDictionary.TryGetValue(fileEntry.Key.ToUpper(), out IndexEntry entry))
|
|
{
|
|
unarchivedFileList.Add(fileEntry.Key, fileEntry.Value);
|
|
continue;
|
|
}
|
|
|
|
var index = cdnConfig.archives[entry.index];
|
|
if (!archivedFileList.TryGetValue(index, out Dictionary<string, List<string>> value))
|
|
{
|
|
value = [];
|
|
archivedFileList.Add(index, value);
|
|
}
|
|
|
|
value.Add(fileEntry.Key, fileEntry.Value);
|
|
}
|
|
|
|
var extractedFiles = 0;
|
|
var totalFiles = fileList.Count;
|
|
|
|
if (unarchivedFileList.Count > 0)
|
|
Console.WriteLine("Extracting " + unarchivedFileList.Count + " unarchived files..");
|
|
|
|
foreach (var fileEntry in unarchivedFileList)
|
|
{
|
|
var target = fileEntry.Key;
|
|
|
|
foreach (var filename in fileEntry.Value)
|
|
{
|
|
if (!Directory.Exists(Path.Combine(basedir, Path.GetDirectoryName(filename))))
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(basedir, Path.GetDirectoryName(filename)));
|
|
}
|
|
}
|
|
|
|
var unarchivedName = Path.Combine(cdn.cacheDir, "tpr", product, "data", target[0] + "" + target[1], target[2] + "" + target[3], target);
|
|
if (File.Exists(unarchivedName))
|
|
{
|
|
foreach (var filename in fileEntry.Value)
|
|
{
|
|
try
|
|
{
|
|
if (product == "wowdev")
|
|
{
|
|
File.WriteAllBytes(Path.Combine(basedir, filename), BLTE.Parse(BLTE.DecryptFile(target, File.ReadAllBytes(unarchivedName), "wowdevalpha")));
|
|
}
|
|
else
|
|
{
|
|
File.WriteAllBytes(Path.Combine(basedir, filename), BLTE.Parse(File.ReadAllBytes(unarchivedName)));
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e.Message);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Unarchived file does not exist " + unarchivedName + ", cannot extract " + string.Join(',', fileEntry.Value));
|
|
}
|
|
|
|
extractedFiles++;
|
|
|
|
if (extractedFiles % 100 == 0)
|
|
{
|
|
Console.WriteLine("[" + DateTime.Now.ToString() + "] Extracted " + extractedFiles + " out of " + totalFiles + " files");
|
|
}
|
|
}
|
|
|
|
foreach (var archiveEntry in archivedFileList)
|
|
{
|
|
var archiveName = Path.Combine(cdn.cacheDir, "tpr", product, "data", archiveEntry.Key[0] + "" + archiveEntry.Key[1], archiveEntry.Key[2] + "" + archiveEntry.Key[3], archiveEntry.Key);
|
|
Console.WriteLine("[" + DateTime.Now.ToString() + "] Extracting " + archiveEntry.Value.Count + " files from archive " + archiveEntry.Key + "..");
|
|
|
|
using (var stream = new MemoryStream(await cdn.Get("tpr/" + product + "/data/" + archiveEntry.Key[0] + "" + archiveEntry.Key[1] + "/" + archiveEntry.Key[2] + "" + archiveEntry.Key[3] + "/" + archiveEntry.Key), true))
|
|
{
|
|
foreach (var fileEntry in archiveEntry.Value)
|
|
{
|
|
var target = fileEntry.Key;
|
|
|
|
foreach (var filename in fileEntry.Value)
|
|
{
|
|
if (!Directory.Exists(Path.Combine(basedir, Path.GetDirectoryName(filename))))
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(basedir, Path.GetDirectoryName(filename)));
|
|
}
|
|
}
|
|
|
|
if (indexDictionary.TryGetValue(target.ToUpper(), out IndexEntry entry))
|
|
{
|
|
foreach (var filename in fileEntry.Value)
|
|
{
|
|
try
|
|
{
|
|
stream.Seek(entry.offset, SeekOrigin.Begin);
|
|
|
|
if (entry.offset > stream.Length || entry.offset + entry.size > stream.Length)
|
|
{
|
|
throw new Exception("File is beyond archive length, incomplete archive!");
|
|
}
|
|
|
|
var archiveBytes = new byte[entry.size];
|
|
stream.Read(archiveBytes, 0, (int)entry.size);
|
|
File.WriteAllBytes(Path.Combine(basedir, filename), BLTE.Parse(archiveBytes));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e.Message);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("!!!!! Unable to find " + fileEntry.Key + " (" + fileEntry.Value[0] + ") in archives!");
|
|
}
|
|
|
|
extractedFiles++;
|
|
|
|
if (extractedFiles % 1000 == 0)
|
|
{
|
|
Console.WriteLine("[" + DateTime.Now.ToString() + "] Extracted " + extractedFiles + " out of " + totalFiles + " files");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "forcebuild")
|
|
{
|
|
if (args.Length == 4)
|
|
{
|
|
checkPrograms = [args[1]];
|
|
backupPrograms = [args[1]];
|
|
overrideBuildconfig = args[2];
|
|
overrideCDNconfig = args[3];
|
|
overrideVersions = true;
|
|
}
|
|
}
|
|
if (args[0] == "forceprogram")
|
|
{
|
|
if (args.Length == 2)
|
|
{
|
|
checkPrograms = [args[1]];
|
|
backupPrograms = [args[1]];
|
|
}
|
|
}
|
|
if (args[0] == "dumpencrypted")
|
|
{
|
|
if (args.Length != 3) throw new Exception("Not enough arguments. Need mode, product, buildconfig");
|
|
|
|
if (args[1] != "wow")
|
|
{
|
|
Console.WriteLine("Only WoW is currently supported due to root/fileDataID usage");
|
|
return;
|
|
}
|
|
|
|
cdns = GetCDNs(args[1]);
|
|
|
|
buildConfig = GetBuildConfig(cdns.entries[0].path, args[2]);
|
|
|
|
encoding = GetEncoding(cdns.entries[0].path, buildConfig.encoding[1], 0, true).Result;
|
|
|
|
var encryptedKeys = new Dictionary<string, string>();
|
|
var encryptedSizes = new Dictionary<string, ulong>();
|
|
foreach (var entry in encoding.bEntries)
|
|
{
|
|
var stringBlockEntry = encoding.stringBlockEntries[entry.Value.stringIndex];
|
|
if (stringBlockEntry.Contains("e:"))
|
|
{
|
|
encryptedKeys.Add(entry.Key, stringBlockEntry);
|
|
encryptedSizes.Add(entry.Key, entry.Value.compressedSize);
|
|
}
|
|
}
|
|
|
|
string rootKey = "";
|
|
var encryptedContentHashes = new Dictionary<string, List<string>>();
|
|
var encryptedContentSizes = new Dictionary<string, ulong>();
|
|
foreach (var entry in encoding.aEntries)
|
|
{
|
|
for (var i = 0; i < entry.eKeys.Count; i++)
|
|
{
|
|
if (encryptedKeys.ContainsKey(entry.eKeys[i]))
|
|
{
|
|
if (encryptedContentHashes.TryGetValue(entry.cKey, out List<string> value))
|
|
{
|
|
value.Add(encryptedKeys[entry.eKeys[i]]);
|
|
}
|
|
else
|
|
{
|
|
encryptedContentHashes.Add(entry.cKey, [encryptedKeys[entry.eKeys[i]]]);
|
|
encryptedContentSizes.Add(entry.cKey, entry.size);
|
|
}
|
|
}
|
|
|
|
if (entry.cKey.Equals(buildConfig.root, StringComparison.OrdinalIgnoreCase)) { rootKey = entry.eKeys[i].ToLower(); }
|
|
}
|
|
}
|
|
|
|
root = GetRoot(cdns.entries[0].path, rootKey, true);
|
|
|
|
foreach (var entry in root.entriesFDID)
|
|
{
|
|
foreach (var subentry in entry.Value)
|
|
{
|
|
var md5string = Convert.ToHexString(subentry.md5);
|
|
if (encryptedContentHashes.TryGetValue(md5string, out List<string> stringBlocks))
|
|
{
|
|
foreach (var rawStringBlock in stringBlocks)
|
|
{
|
|
var stringBlock = rawStringBlock;
|
|
var keyList = new List<string>();
|
|
while (stringBlock.Contains("e:"))
|
|
{
|
|
var keyName = stringBlock.Substring(stringBlock.IndexOf("e:{") + 3, 16);
|
|
if (!keyList.Contains(keyName))
|
|
{
|
|
keyList.Add(keyName);
|
|
}
|
|
stringBlock = stringBlock.Remove(stringBlock.IndexOf("e:{"), 19);
|
|
}
|
|
|
|
Console.WriteLine(subentry.fileDataID + " " + string.Join(',', keyList) + " " + rawStringBlock + " " + encryptedContentSizes[md5string]);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumpbadlyencrypted")
|
|
{
|
|
if (args.Length != 3) throw new Exception("Not enough arguments. Need mode, product, buildconfig");
|
|
|
|
if (args[1] != "wow")
|
|
{
|
|
Console.WriteLine("Only WoW is currently supported due to root/fileDataID usage");
|
|
return;
|
|
}
|
|
|
|
cdns = GetCDNs(args[1]);
|
|
|
|
buildConfig = GetBuildConfig(cdns.entries[0].path, args[2]);
|
|
encoding = GetEncoding(cdns.entries[0].path, buildConfig.encoding[1], 0, true).Result;
|
|
|
|
var encryptedKeys = new HashSet<string>();
|
|
var notEncryptedKeys = new HashSet<string>();
|
|
foreach (var entry in encoding.bEntries)
|
|
{
|
|
var stringBlockEntry = encoding.stringBlockEntries[entry.Value.stringIndex];
|
|
if (stringBlockEntry.Contains("e:"))
|
|
{
|
|
encryptedKeys.Add(entry.Key);
|
|
}
|
|
else
|
|
{
|
|
notEncryptedKeys.Add(entry.Key);
|
|
}
|
|
}
|
|
|
|
string rootKey = "";
|
|
var encryptedContentHashes = new HashSet<string>();
|
|
|
|
foreach (var entry in encoding.aEntries)
|
|
{
|
|
for (var i = 0; i < entry.eKeys.Count; i++)
|
|
{
|
|
if (encryptedKeys.Contains(entry.eKeys[i]) && !encryptedContentHashes.Contains(entry.cKey))
|
|
{
|
|
encryptedContentHashes.Add(entry.cKey);
|
|
}
|
|
|
|
if (entry.cKey.Equals(buildConfig.root, StringComparison.OrdinalIgnoreCase)) { rootKey = entry.eKeys[i].ToLower(); }
|
|
}
|
|
|
|
for (var i = 0; i < entry.eKeys.Count; i++)
|
|
{
|
|
if (!encryptedKeys.Contains(entry.eKeys[i]))
|
|
{
|
|
encryptedContentHashes.Remove(entry.cKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
root = GetRoot(cdns.entries[0].path, rootKey, true);
|
|
|
|
foreach (var entry in root.entriesFDID)
|
|
{
|
|
foreach (var subentry in entry.Value)
|
|
{
|
|
var contenthash = Convert.ToHexString(subentry.md5);
|
|
|
|
if (encryptedContentHashes.Contains(contenthash))
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (subentry.contentFlags.HasFlag(ContentFlags.Encrypted))
|
|
{
|
|
Console.WriteLine(subentry.fileDataID);
|
|
}
|
|
}
|
|
}
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumpsizes")
|
|
{
|
|
if (args.Length != 3) throw new Exception("Not enough arguments. Need mode, product, buildconfig");
|
|
|
|
if (args[1] != "wow")
|
|
{
|
|
Console.WriteLine("Only WoW is currently supported due to root/fileDataID usage");
|
|
return;
|
|
}
|
|
|
|
cdns = GetCDNs(args[1]);
|
|
|
|
buildConfig = GetBuildConfig(cdns.entries[0].path, args[2]);
|
|
|
|
encoding = GetEncoding(cdns.entries[0].path, buildConfig.encoding[1], 0, true).Result;
|
|
|
|
foreach (var entry in encoding.aEntries)
|
|
{
|
|
Console.WriteLine(entry.cKey.ToLower() + " " + entry.size);
|
|
}
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumprawfile")
|
|
{
|
|
if (args.Length < 2) throw new Exception("Not enough arguments. Need mode, path, (numbytes)");
|
|
|
|
var file = BLTE.Parse(File.ReadAllBytes(args[1]));
|
|
|
|
if (args.Length == 3)
|
|
{
|
|
file = [.. file.Take(int.Parse(args[2]))];
|
|
}
|
|
|
|
Console.Write(Encoding.UTF8.GetString(file));
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumprawfiletofile" || File.Exists(args[0]))
|
|
{
|
|
if (args.Length == 1)
|
|
{
|
|
File.WriteAllBytes(args[0] + ".dump", BLTE.Parse(File.ReadAllBytes(args[0])));
|
|
}
|
|
else if (args.Length == 3)
|
|
{
|
|
File.WriteAllBytes(args[2], BLTE.Parse(File.ReadAllBytes(args[1])));
|
|
}
|
|
else
|
|
{
|
|
throw new Exception("Not enough arguments. Need mode, path, outfile");
|
|
}
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumpencrypteddirtodir")
|
|
{
|
|
|
|
if (args.Length < 3) throw new Exception("Not enough arguments. Need mode, src, dest");
|
|
|
|
var whiteList = new List<string>() { "" };
|
|
var allFiles = Directory.GetFiles(args[1], "*", SearchOption.AllDirectories).ToList();
|
|
allFiles.Sort();
|
|
|
|
var fileCount = 0;
|
|
var totalCount = allFiles.Count;
|
|
if (!args[1].Contains("wowdev"))
|
|
{
|
|
throw new Exception("unk encryptedproduct");
|
|
}
|
|
|
|
Parallel.ForEach(allFiles, new ParallelOptions { MaxDegreeOfParallelism = 4 }, file =>
|
|
{
|
|
//if (!file.EndsWith(".index"))
|
|
// continue;
|
|
|
|
var newName = file.Replace(args[1], args[2]);
|
|
|
|
if (File.Exists(newName))
|
|
{
|
|
var newFileInfo = new FileInfo(newName);
|
|
var oldFileInfo = new FileInfo(file);
|
|
|
|
if (oldFileInfo.Length != newFileInfo.Length)
|
|
{
|
|
Console.WriteLine("Length mismatch for " + newName + ", re-extracting");
|
|
}
|
|
else
|
|
{
|
|
fileCount++;
|
|
return;
|
|
}
|
|
}
|
|
|
|
Console.WriteLine("[" + fileCount + "/" + totalCount + "] " + newName);
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(newName));
|
|
|
|
try
|
|
{
|
|
File.WriteAllBytes(newName, BLTE.DecryptFile(Path.GetFileNameWithoutExtension(file), File.ReadAllBytes(file), "wowdev3"));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Failed to decrypt/write " + Path.GetFileNameWithoutExtension(newName) + ": " + e.Message);
|
|
}
|
|
|
|
fileCount++;
|
|
});
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumpencryptedfiletofile")
|
|
{
|
|
if (args.Length < 3) throw new Exception("Not enough arguments. Need mode, path, dest");
|
|
|
|
byte[] fileBytes = File.ReadAllBytes(args[1]);
|
|
|
|
if (args[1].Contains("wowdev"))
|
|
{
|
|
fileBytes = BLTE.DecryptFile(Path.GetFileNameWithoutExtension(args[1]), fileBytes, "wowdevalpha");
|
|
}
|
|
else
|
|
{
|
|
throw new Exception("unk encryptedproduct");
|
|
}
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(args[2]));
|
|
File.WriteAllBytes(args[2], fileBytes);
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumprawinstall")
|
|
{
|
|
var install = new InstallFile();
|
|
|
|
byte[] content = File.ReadAllBytes(args[1]);
|
|
|
|
|
|
using (BinaryReader bin = new(new MemoryStream(BLTE.Parse(content))))
|
|
{
|
|
if (Encoding.UTF8.GetString(bin.ReadBytes(2)) != "IN") { throw new Exception("Error while parsing install file. Did BLTE header size change?"); }
|
|
|
|
bin.ReadByte();
|
|
|
|
install.hashSize = bin.ReadByte();
|
|
if (install.hashSize != 16) throw new Exception("Unsupported install hash size!");
|
|
|
|
install.numTags = bin.ReadUInt16(true);
|
|
install.numEntries = bin.ReadUInt32(true);
|
|
|
|
int bytesPerTag = ((int)install.numEntries + 7) / 8;
|
|
|
|
install.tags = new InstallTagEntry[install.numTags];
|
|
|
|
for (var i = 0; i < install.numTags; i++)
|
|
{
|
|
install.tags[i].name = bin.ReadCString();
|
|
install.tags[i].type = bin.ReadUInt16(true);
|
|
|
|
var filebits = bin.ReadBytes(bytesPerTag);
|
|
|
|
for (int j = 0; j < bytesPerTag; j++)
|
|
filebits[j] = (byte)((filebits[j] * 0x0202020202 & 0x010884422010) % 1023);
|
|
|
|
install.tags[i].files = new BitArray(filebits);
|
|
}
|
|
|
|
install.entries = new InstallFileEntry[install.numEntries];
|
|
|
|
for (var i = 0; i < install.numEntries; i++)
|
|
{
|
|
install.entries[i].name = bin.ReadCString();
|
|
install.entries[i].contentHash = bin.ReadBytes(install.hashSize);
|
|
install.entries[i].size = bin.ReadUInt32(true);
|
|
install.entries[i].tags = [];
|
|
for (var j = 0; j < install.numTags; j++)
|
|
{
|
|
if (install.tags[j].files[i] == true)
|
|
{
|
|
install.entries[i].tags.Add(install.tags[j].type + "=" + install.tags[j].name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var entry in install.entries)
|
|
{
|
|
Console.WriteLine(entry.name + " (size: " + entry.size + ", md5: " + Convert.ToHexString(entry.contentHash).ToLower() + ", tags: " + string.Join(",", entry.tags) + ")");
|
|
}
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "dumpindex")
|
|
{
|
|
if (args.Length < 3) throw new Exception("Not enough arguments. Need mode, product, hash, (folder)");
|
|
|
|
cdns = GetCDNs(args[1]);
|
|
|
|
var folder = "data";
|
|
|
|
if (args.Length == 4)
|
|
{
|
|
folder = args[3];
|
|
}
|
|
|
|
var index = ParseIndex(cdns.entries[0].path + "/", args[2], folder);
|
|
|
|
foreach (var entry in index)
|
|
{
|
|
Console.WriteLine(entry.Key + " " + entry.Value.size);
|
|
}
|
|
Environment.Exit(0);
|
|
}
|
|
if (args[0] == "partialdl")
|
|
{
|
|
fullDownload = false;
|
|
}
|
|
if (args[0] == "dumparchive")
|
|
{
|
|
var indexContent = File.ReadAllBytes(args[1]);
|
|
using (var ms = new MemoryStream(indexContent))
|
|
using (BinaryReader bin = new(ms))
|
|
{
|
|
int indexEntries = indexContent.Length / 4096;
|
|
|
|
for (var b = 0; b < indexEntries; b++)
|
|
{
|
|
for (var bi = 0; bi < 170; bi++)
|
|
{
|
|
var headerHash = Convert.ToHexString(bin.ReadBytes(16));
|
|
|
|
var entry = new IndexEntry()
|
|
{
|
|
index = 0,
|
|
size = bin.ReadUInt32(true),
|
|
offset = bin.ReadUInt32(true)
|
|
};
|
|
if (!indexDictionary.ContainsKey(headerHash) && headerHash != "00000000000000000000000000000000")
|
|
{
|
|
if (!indexDictionary.TryAdd(headerHash, entry))
|
|
{
|
|
Console.WriteLine("Duplicate index entry for " + headerHash + " " + "(size: " + entry.size + ", offset: " + entry.offset);
|
|
}
|
|
}
|
|
}
|
|
bin.ReadBytes(16);
|
|
}
|
|
}
|
|
|
|
var outPath = Path.Combine(Path.GetDirectoryName(args[1]), Path.GetFileNameWithoutExtension(args[1]) + "_extract");
|
|
if (!Directory.Exists(outPath))
|
|
Directory.CreateDirectory(outPath);
|
|
|
|
var archivePath = Path.Combine(Path.GetDirectoryName(args[1]), Path.GetFileNameWithoutExtension(args[1]));
|
|
using (var ms = new MemoryStream(File.ReadAllBytes(archivePath)))
|
|
using (var bin = new BinaryReader(ms))
|
|
{
|
|
foreach (var entry in indexDictionary)
|
|
{
|
|
bin.BaseStream.Seek(entry.Value.offset, SeekOrigin.Begin);
|
|
var data = BLTE.Parse(bin.ReadBytes((int)entry.Value.size));
|
|
var outFilePath = Path.Combine(outPath, entry.Key);
|
|
if (data[0] == 'B' && data[1] == 'L' && data[2] == 'P' && data[3] == '2')
|
|
outFilePath += ".blp";
|
|
else if (data[0] == 'M' && data[1] == 'Z')
|
|
outFilePath += ".exe";
|
|
else if (data[0] == 'W' && data[1] == 'D' && data[2] == 'C')
|
|
outFilePath += ".db2";
|
|
else if (data[0] == 'M' && data[1] == '3')
|
|
outFilePath += ".m3lib";
|
|
|
|
File.WriteAllBytes(outFilePath, data);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
// Load programs
|
|
checkPrograms ??= SettingsManager.checkProducts;
|
|
backupPrograms ??= SettingsManager.backupProducts;
|
|
|
|
var finishedCDNConfigs = new List<string>();
|
|
var finishedEncodings = new List<string>();
|
|
|
|
var downloadThrottler = new SemaphoreSlim(initialCount: 10);
|
|
|
|
foreach (string program in checkPrograms)
|
|
{
|
|
var archiveSizes = new Dictionary<string, uint>();
|
|
|
|
if (File.Exists("archiveSizes.txt"))
|
|
{
|
|
foreach (var line in File.ReadAllLines("archiveSizes.txt"))
|
|
{
|
|
var split = line.Split(' ');
|
|
if (uint.TryParse(split[1], out uint archiveSize))
|
|
{
|
|
archiveSizes.Add(split[0], archiveSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
Console.WriteLine("Using program " + program);
|
|
|
|
try
|
|
{
|
|
cdns = GetCDNs(program);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Error parsing CDNs: " + e.Message);
|
|
}
|
|
|
|
if (!overrideVersions)
|
|
{
|
|
try
|
|
{
|
|
versions = GetVersions(program);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Error parsing versions: " + e.Message);
|
|
}
|
|
|
|
if (versions.entries == null || versions.entries.Length == 0) { Console.WriteLine("Invalid versions file for " + program + ", skipping!"); continue; }
|
|
|
|
Console.WriteLine("Loaded " + versions.entries.Length + " versions");
|
|
|
|
// Retrieve keyring
|
|
if (!string.IsNullOrEmpty(versions.entries[0].keyRing))
|
|
await cdn.Get(cdns.entries[0].path + "/config/" + versions.entries[0].keyRing[0] + versions.entries[0].keyRing[1] + "/" + versions.entries[0].keyRing[2] + versions.entries[0].keyRing[3] + "/" + versions.entries[0].keyRing);
|
|
}
|
|
|
|
if (cdns.entries == null || cdns.entries.Length == 0) { Console.WriteLine("Invalid CDNs file for " + program + ", skipping!"); continue; }
|
|
|
|
Console.WriteLine("Loaded " + cdns.entries.Length + " cdns");
|
|
|
|
var decryptionKeyName = "";
|
|
|
|
if (versions.entries != null)
|
|
{
|
|
if (!string.IsNullOrEmpty(versions.entries[0].productConfig))
|
|
{
|
|
productConfig = GetProductConfig(cdns.entries[0].configPath + "/", versions.entries[0].productConfig);
|
|
}
|
|
|
|
|
|
if (productConfig.decryptionKeyName != null && productConfig.decryptionKeyName != string.Empty)
|
|
{
|
|
decryptionKeyName = productConfig.decryptionKeyName;
|
|
}
|
|
|
|
cdn.decryptionKeyName = decryptionKeyName;
|
|
|
|
// Retrieve all buildconfigs
|
|
for (var i = 0; i < versions.entries.Length; i++)
|
|
{
|
|
GetBuildConfig(cdns.entries[0].path + "/", versions.entries[i].buildConfig);
|
|
}
|
|
}
|
|
|
|
if (overrideVersions && !string.IsNullOrEmpty(overrideBuildconfig))
|
|
{
|
|
buildConfig = GetBuildConfig(cdns.entries[0].path + "/", overrideBuildconfig);
|
|
}
|
|
else
|
|
{
|
|
buildConfig = GetBuildConfig(cdns.entries[0].path + "/", versions.entries[0].buildConfig);
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(buildConfig.buildName))
|
|
{
|
|
Console.WriteLine("Missing buildname in buildConfig for " + program + ", setting build name!");
|
|
buildConfig.buildName = "UNKNOWN";
|
|
}
|
|
|
|
var currentCDNConfig = "";
|
|
|
|
if (overrideVersions && !string.IsNullOrEmpty(overrideCDNconfig))
|
|
{
|
|
cdnConfig = GetCDNconfig(cdns.entries[0].path + "/", overrideCDNconfig);
|
|
currentCDNConfig = overrideCDNconfig;
|
|
}
|
|
else
|
|
{
|
|
cdnConfig = GetCDNconfig(cdns.entries[0].path + "/", versions.entries[0].cdnConfig);
|
|
currentCDNConfig = versions.entries[0].cdnConfig;
|
|
}
|
|
|
|
if (cdnConfig.builds != null)
|
|
{
|
|
cdnBuildConfigs = new BuildConfigFile[cdnConfig.builds.Length];
|
|
}
|
|
else if (cdnConfig.archives != null)
|
|
{
|
|
//Console.WriteLine("CDNConfig loaded, " + cdnConfig.archives.Count() + " archives");
|
|
}
|
|
else if (cdnConfig.fileIndex != null)
|
|
{
|
|
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Invalid cdnConfig for " + program + "!");
|
|
continue;
|
|
}
|
|
|
|
if (!backupPrograms.Contains(program))
|
|
{
|
|
Console.WriteLine("No need to backup, moving on..");
|
|
continue;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(decryptionKeyName) && cdnConfig.archives == null) // Let us ignore this whole encryption thing if archives are set, surely this will never break anything and it'll back it up perfectly fine.
|
|
{
|
|
if (!File.Exists(decryptionKeyName + ".ak"))
|
|
{
|
|
Console.WriteLine("Decryption key is set and not available on disk, skipping.");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
Console.Write("Downloading patch files..");
|
|
try
|
|
{
|
|
if (!string.IsNullOrEmpty(buildConfig.patch))
|
|
patch = GetPatch(cdns.entries[0].path + "/", buildConfig.patch, true);
|
|
|
|
if (!string.IsNullOrEmpty(buildConfig.patchConfig))
|
|
await cdn.Get(cdns.entries[0].path + "/config/" + buildConfig.patchConfig[0] + buildConfig.patchConfig[1] + "/" + buildConfig.patchConfig[2] + buildConfig.patchConfig[3] + "/" + buildConfig.patchConfig);
|
|
|
|
if (buildConfig.patchIndex != null && buildConfig.patchIndex.Length == 2 && !string.IsNullOrEmpty(buildConfig.patchIndex[1]))
|
|
await cdn.Get(cdns.entries[0].path + "/data/" + buildConfig.patchIndex[1][0] + buildConfig.patchIndex[1][1] + "/" + buildConfig.patchIndex[1][2] + buildConfig.patchIndex[1][3] + "/" + buildConfig.patchIndex[1]);
|
|
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Failed to download patch files: " + e.Message);
|
|
}
|
|
|
|
Console.Write("..done\n");
|
|
|
|
if (!finishedCDNConfigs.Contains(currentCDNConfig))
|
|
{
|
|
Console.WriteLine("CDN config " + currentCDNConfig + " has not been loaded yet, loading..");
|
|
|
|
if (cdnConfig.archives != null)
|
|
{
|
|
Console.Write("Loading " + cdnConfig.archives.Length + " indexes..");
|
|
try
|
|
{
|
|
GetIndexes(cdns.entries[0].path + "/", cdnConfig.archives);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Failed to get indexes: " + e.Message);
|
|
}
|
|
Console.Write("..done\n");
|
|
|
|
if (fullDownload)
|
|
{
|
|
Console.Write("Fetching and saving archive sizes..");
|
|
|
|
for (short i = 0; i < cdnConfig.archives.Length; i++)
|
|
{
|
|
var archive = cdnConfig.archives[i];
|
|
if (!archiveSizes.ContainsKey(archive))
|
|
{
|
|
try
|
|
{
|
|
var remoteFileSize = await cdn.GetRemoteFileSize(cdns.entries[0].path + "/data/" + archive[0] + archive[1] + "/" + archive[2] + archive[3] + "/" + archive);
|
|
archiveSizes.Add(archive, remoteFileSize);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Failed to get remote file size for " + archive + ": " + e.Message);
|
|
archiveSizes.Add(archive, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
var archiveSizesLines = new List<string>();
|
|
foreach (var archiveSize in archiveSizes)
|
|
{
|
|
archiveSizesLines.Add(archiveSize.Key + " " + archiveSize.Value);
|
|
}
|
|
|
|
await File.WriteAllLinesAsync("archiveSizes.txt", archiveSizesLines);
|
|
|
|
Console.WriteLine("..done");
|
|
|
|
Console.Write("Downloading " + cdnConfig.archives.Length + " archives..");
|
|
|
|
var archiveTasks = new List<Task>();
|
|
for (short i = 0; i < cdnConfig.archives.Length; i++)
|
|
{
|
|
var archive = cdnConfig.archives[i];
|
|
await downloadThrottler.WaitAsync();
|
|
archiveTasks.Add(
|
|
Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
uint archiveSize = 0;
|
|
if (archiveSizes.TryGetValue(archive, out uint value))
|
|
{
|
|
archiveSize = value;
|
|
}
|
|
|
|
await cdn.Get(cdns.entries[0].path + "/data/" + archive[0] + archive[1] + "/" + archive[2] + archive[3] + "/" + archive, false, false, archiveSize, true);
|
|
}
|
|
finally
|
|
{
|
|
downloadThrottler.Release();
|
|
}
|
|
}));
|
|
}
|
|
try
|
|
{
|
|
await Task.WhenAll(archiveTasks);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Failed to download archives: " + e.Message);
|
|
}
|
|
Console.Write("..done\n");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Not a full run, skipping archive downloads..");
|
|
if (!finishedCDNConfigs.Contains(currentCDNConfig)) { finishedCDNConfigs.Add(currentCDNConfig); }
|
|
}
|
|
}
|
|
}
|
|
|
|
string rootKey = "";
|
|
string downloadKey = "";
|
|
string installKey = "";
|
|
Dictionary<string, string> hashes = [];
|
|
|
|
if (buildConfig.encoding != null && buildConfig.encoding.Length == 2)
|
|
{
|
|
//if (finishedEncodings.Contains(buildConfig.encoding[1]))
|
|
//{
|
|
// Console.WriteLine("Encoding file " + buildConfig.encoding[1] + " already loaded, skipping rest of product loading..");
|
|
// continue;
|
|
//}
|
|
|
|
Console.Write("Loading encoding..");
|
|
|
|
try
|
|
{
|
|
if (buildConfig.encodingSize == null || buildConfig.encodingSize.Length < 2)
|
|
{
|
|
encoding = await GetEncoding(cdns.entries[0].path + "/", buildConfig.encoding[1], 0);
|
|
}
|
|
else
|
|
{
|
|
encoding = await GetEncoding(cdns.entries[0].path + "/", buildConfig.encoding[1], int.Parse(buildConfig.encodingSize[1]));
|
|
}
|
|
|
|
finishedEncodings.Add(buildConfig.encoding[1]);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Fatal error occurred during encoding parsing: " + e.Message);
|
|
}
|
|
|
|
if (buildConfig.install.Length == 2)
|
|
{
|
|
installKey = buildConfig.install[1];
|
|
}
|
|
|
|
if (buildConfig.download.Length == 2)
|
|
{
|
|
downloadKey = buildConfig.download[1];
|
|
}
|
|
|
|
if (encoding.aEntries != null)
|
|
{
|
|
foreach (var entry in encoding.aEntries)
|
|
{
|
|
if (entry.cKey.Equals(buildConfig.root, StringComparison.OrdinalIgnoreCase)) { rootKey = entry.eKeys[0].ToLower(); }
|
|
if (downloadKey == "" && entry.cKey.Equals(buildConfig.download[0], StringComparison.OrdinalIgnoreCase)) { downloadKey = entry.eKeys[0].ToLower(); }
|
|
if (installKey == "" && entry.cKey.Equals(buildConfig.install[0], StringComparison.OrdinalIgnoreCase)) { installKey = entry.eKeys[0].ToLower(); }
|
|
hashes.TryAdd(entry.eKeys[0], entry.cKey);
|
|
}
|
|
}
|
|
|
|
Console.Write("..done\n");
|
|
}
|
|
|
|
|
|
if (program.StartsWith("wow")) // Only these are supported right now
|
|
{
|
|
Console.Write("Loading root..");
|
|
if (rootKey == "")
|
|
{
|
|
Console.WriteLine("Unable to find root key in encoding!");
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
root = GetRoot(cdns.entries[0].path + "/", rootKey);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Error loading root: " + e.Message);
|
|
}
|
|
}
|
|
Console.Write("..done\n");
|
|
|
|
Console.Write("Loading download..");
|
|
if (downloadKey == "")
|
|
{
|
|
Console.WriteLine("Unable to find download key in encoding!");
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
download = GetDownload(cdns.entries[0].path + "/", downloadKey);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Error loading download: " + e.Message);
|
|
}
|
|
}
|
|
Console.Write("..done\n");
|
|
|
|
Console.Write("Loading install..");
|
|
try
|
|
{
|
|
if (installKey == "")
|
|
{
|
|
Console.WriteLine("Unable to find install key in encoding!");
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
install = GetInstall(cdns.entries[0].path + "/", installKey);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Error loading install: " + e.Message);
|
|
}
|
|
}
|
|
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Error loading install: " + e.Message);
|
|
}
|
|
}
|
|
Console.Write("..done\n");
|
|
|
|
|
|
if (!fullDownload)
|
|
{
|
|
Console.WriteLine("Not a full run, skipping rest of download..");
|
|
continue;
|
|
}
|
|
|
|
foreach (var entry in indexDictionary)
|
|
{
|
|
hashes.Remove(entry.Key.ToUpper());
|
|
}
|
|
|
|
if (!finishedCDNConfigs.Contains(currentCDNConfig))
|
|
{
|
|
if (!string.IsNullOrEmpty(cdnConfig.fileIndex))
|
|
{
|
|
Console.Write("Parsing file index..");
|
|
try
|
|
{
|
|
fileIndexList = ParseIndex(cdns.entries[0].path + "/", cdnConfig.fileIndex);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Unable to get file index: " + e.Message);
|
|
}
|
|
Console.Write("..done\n");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(cdnConfig.fileIndex) && fileIndexList.Count > 0)
|
|
{
|
|
Console.Write("Downloading " + fileIndexList.Count + " unarchived files from file index..");
|
|
|
|
var fileIndexTasks = new List<Task>();
|
|
foreach (var entry in fileIndexList.Keys)
|
|
{
|
|
await downloadThrottler.WaitAsync();
|
|
fileIndexTasks.Add(
|
|
Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await cdn.Get(
|
|
cdns.entries[0].path + "/data/" + entry[0] + entry[1] + "/" + entry[2] +
|
|
entry[3] + "/" + entry, false, false, fileIndexList[entry].size, true);
|
|
}
|
|
finally
|
|
{
|
|
downloadThrottler.Release();
|
|
}
|
|
}));
|
|
}
|
|
|
|
await Task.WhenAll(fileIndexTasks);
|
|
|
|
Console.Write("..done\n");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(cdnConfig.patchFileIndex))
|
|
{
|
|
Console.Write("Parsing patch file index..");
|
|
try
|
|
{
|
|
patchFileIndexList = ParseIndex(cdns.entries[0].path + "/", cdnConfig.patchFileIndex, "patch");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Error retrieving patch file index: " + e.Message);
|
|
}
|
|
Console.Write("..done\n");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(cdnConfig.patchFileIndex) && SettingsManager.downloadPatchFiles && patchFileIndexList.Count > 0)
|
|
{
|
|
Console.Write("Downloading " + patchFileIndexList.Count + " unarchived patch files from patch file index..");
|
|
|
|
var patchFileTasks = new List<Task>();
|
|
foreach (var entry in patchFileIndexList.Keys)
|
|
{
|
|
await downloadThrottler.WaitAsync();
|
|
patchFileTasks.Add(
|
|
Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await cdn.Get(
|
|
cdns.entries[0].path + "/patch/" + entry[0] + entry[1] + "/" + entry[2] +
|
|
entry[3] + "/" + entry, false);
|
|
}
|
|
finally
|
|
{
|
|
downloadThrottler.Release();
|
|
}
|
|
}));
|
|
}
|
|
|
|
await Task.WhenAll(patchFileTasks);
|
|
|
|
Console.Write("..done\n");
|
|
}
|
|
|
|
if (SettingsManager.downloadPatchFiles && cdnConfig.patchArchives != null)
|
|
{
|
|
Console.Write("Downloading " + cdnConfig.patchArchives.Length + " patch archives..");
|
|
|
|
var patchArchiveTasks = new List<Task>();
|
|
foreach (var archive in cdnConfig.patchArchives)
|
|
{
|
|
await downloadThrottler.WaitAsync();
|
|
patchArchiveTasks.Add(
|
|
Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await cdn.Get(
|
|
cdns.entries[0].path + "/patch/" + archive[0] + archive[1] + "/" +
|
|
archive[2] + archive[3] + "/" + archive, false, false, 0, true);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Failed to download patch archive: " + e.Message);
|
|
}
|
|
finally
|
|
{
|
|
downloadThrottler.Release();
|
|
}
|
|
}));
|
|
}
|
|
|
|
await Task.WhenAll(patchArchiveTasks);
|
|
Console.Write("..done\n");
|
|
|
|
Console.Write("Downloading " + cdnConfig.patchArchives.Length + " patch archive indexes..");
|
|
GetPatchIndexes(cdns.entries[0].path + "/", cdnConfig.patchArchives);
|
|
Console.Write("..done\n");
|
|
}
|
|
|
|
finishedCDNConfigs.Add(currentCDNConfig);
|
|
}
|
|
|
|
// Unarchived files -- files in encoding but not in indexes. Can vary per build!
|
|
Console.Write("Downloading " + hashes.Count + " unarchived files..");
|
|
|
|
var unarchivedFileTasks = new List<Task>();
|
|
foreach (var entry in hashes)
|
|
{
|
|
await downloadThrottler.WaitAsync();
|
|
unarchivedFileTasks.Add(
|
|
Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await cdn.Get(cdns.entries[0].path + "/data/" + entry.Key[0] + entry.Key[1] + "/" + entry.Key[2] + entry.Key[3] + "/" + entry.Key, false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Unable to download unarchived file: " + e.Message);
|
|
}
|
|
finally
|
|
{
|
|
downloadThrottler.Release();
|
|
}
|
|
}));
|
|
}
|
|
await Task.WhenAll(unarchivedFileTasks);
|
|
Console.Write("..done\n");
|
|
|
|
if (SettingsManager.downloadPatchFiles)
|
|
{
|
|
if (cdnConfig.patchArchives != null)
|
|
{
|
|
if (patch.blocks != null)
|
|
{
|
|
var unarchivedPatchKeyList = new List<string>();
|
|
foreach (var block in patch.blocks)
|
|
{
|
|
foreach (var fileBlock in block.files)
|
|
{
|
|
foreach (var patch in fileBlock.patches)
|
|
{
|
|
var pKey = Convert.ToHexString(patch.patchEncodingKey);
|
|
if (!patchIndexDictionary.ContainsKey(pKey))
|
|
{
|
|
unarchivedPatchKeyList.Add(pKey);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (unarchivedPatchKeyList.Count > 0)
|
|
{
|
|
Console.Write("Downloading " + unarchivedPatchKeyList.Count + " unarchived patch files..");
|
|
|
|
var unarchivedPatchFileTasks = new List<Task>();
|
|
foreach (var entry in unarchivedPatchKeyList)
|
|
{
|
|
await downloadThrottler.WaitAsync();
|
|
unarchivedPatchFileTasks.Add(
|
|
Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await cdn.Get(cdns.entries[0].path + "/patch/" + entry[0] + entry[1] + "/" + entry[2] + entry[3] + "/" + entry, false);
|
|
}
|
|
finally
|
|
{
|
|
downloadThrottler.Release();
|
|
}
|
|
}));
|
|
}
|
|
await Task.WhenAll(unarchivedPatchFileTasks);
|
|
|
|
Console.Write("..done\n");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
GC.Collect();
|
|
}
|
|
}
|
|
|
|
private static byte[] RetrieveFileBytes(string target, bool raw = false, string cdndir = "tpr/wow", bool tryUnarchived = false)
|
|
{
|
|
var unarchivedName = Path.Combine(cdn.cacheDir, cdndir, "data", target[0] + "" + target[1], target[2] + "" + target[3], target);
|
|
|
|
if (File.Exists(unarchivedName))
|
|
{
|
|
if (!raw)
|
|
{
|
|
if (cdndir == "tpr/wowdev")
|
|
{
|
|
return BLTE.Parse(BLTE.DecryptFile(target, File.ReadAllBytes(unarchivedName), "wowdevalpha"));
|
|
}
|
|
else if (cdndir == "tpr/hse")
|
|
{
|
|
return BLTE.Parse(BLTE.DecryptFile(target, File.ReadAllBytes(unarchivedName), "hse1"));
|
|
}
|
|
else
|
|
{
|
|
return BLTE.Parse(File.ReadAllBytes(unarchivedName));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (cdndir == "tpr/wowdev")
|
|
{
|
|
return BLTE.DecryptFile(target, File.ReadAllBytes(unarchivedName), "wowdevalpha");
|
|
}
|
|
else if (cdndir == "tpr/hse")
|
|
{
|
|
return BLTE.DecryptFile(target, File.ReadAllBytes(unarchivedName), "hse1");
|
|
}
|
|
else
|
|
{
|
|
return File.ReadAllBytes(unarchivedName);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (tryUnarchived)
|
|
{
|
|
try
|
|
{
|
|
var file = cdn.Get(cdndir + "/" + "data" + "/" + target[0] + target[1] + "/" + target[2] + target[3] + "/" + target, true, false, 0, true).Result;
|
|
return BLTE.Parse(file);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Warn: Unable to retrieve file as unarchived from CDN: " + e.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!indexDictionary.TryGetValue(target.ToUpper(), out IndexEntry entry))
|
|
{
|
|
throw new Exception("Unable to find file in archives. File is not available!?");
|
|
}
|
|
else
|
|
{
|
|
var index = cdnConfig.archives[entry.index];
|
|
|
|
using (BinaryReader bin = new(new MemoryStream(cdn.Get(cdndir + "/data/" + index[0] + "" + index[1] + "/" + index[2] + "" + index[3] + "/" + index).Result, true)))
|
|
{
|
|
bin.BaseStream.Position = entry.offset;
|
|
try
|
|
{
|
|
if (!raw)
|
|
{
|
|
return BLTE.Parse(bin.ReadBytes((int)entry.size));
|
|
}
|
|
else
|
|
{
|
|
return bin.ReadBytes((int)entry.size);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
private static CDNConfigFile GetCDNconfig(string url, string hash)
|
|
{
|
|
string content;
|
|
var cdnConfig = new CDNConfigFile();
|
|
|
|
try
|
|
{
|
|
content = Encoding.UTF8.GetString(cdn.Get(url + "/config/" + hash[0] + hash[1] + "/" + hash[2] + hash[3] + "/" + hash).Result);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Error retrieving CDN config: " + e.Message);
|
|
return cdnConfig;
|
|
}
|
|
|
|
var cdnConfigLines = content.Split(["\n"], StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
for (var i = 0; i < cdnConfigLines.Length; i++)
|
|
{
|
|
if (cdnConfigLines[i].StartsWith('#') || cdnConfigLines[i].Length == 0) { continue; }
|
|
var cols = cdnConfigLines[i].Split([" = "], StringSplitOptions.RemoveEmptyEntries);
|
|
switch (cols[0])
|
|
{
|
|
case "archives":
|
|
var archives = cols[1].Split(' ');
|
|
cdnConfig.archives = archives;
|
|
break;
|
|
case "archive-group":
|
|
cdnConfig.archiveGroup = cols[1];
|
|
break;
|
|
case "patch-archives":
|
|
if (cols.Length > 1)
|
|
{
|
|
var patchArchives = cols[1].Split(' ');
|
|
cdnConfig.patchArchives = patchArchives;
|
|
}
|
|
break;
|
|
case "patch-archive-group":
|
|
cdnConfig.patchArchiveGroup = cols[1];
|
|
break;
|
|
case "builds":
|
|
var builds = cols[1].Split(' ');
|
|
cdnConfig.builds = builds;
|
|
break;
|
|
case "file-index":
|
|
cdnConfig.fileIndex = cols[1];
|
|
break;
|
|
case "file-index-size":
|
|
cdnConfig.fileIndexSize = cols[1];
|
|
break;
|
|
case "patch-file-index":
|
|
cdnConfig.patchFileIndex = cols[1];
|
|
break;
|
|
case "patch-file-index-size":
|
|
cdnConfig.patchFileIndexSize = cols[1];
|
|
break;
|
|
default:
|
|
//Console.WriteLine("!!!!!!!! Unknown cdnconfig variable '" + cols[0] + "'");
|
|
break;
|
|
}
|
|
}
|
|
|
|
return cdnConfig;
|
|
}
|
|
|
|
private static VersionsFile GetVersions(string program)
|
|
{
|
|
string content;
|
|
var versions = new VersionsFile();
|
|
|
|
using (HttpResponseMessage response = cdn.client.GetAsync(new Uri(baseUrl + "v2/products/" + program + "/" + "versions")).Result)
|
|
{
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
using (HttpContent res = response.Content)
|
|
{
|
|
content = res.ReadAsStringAsync().Result;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Error during retrieving HTTP versions: Received bad HTTP code " + response.StatusCode);
|
|
return versions;
|
|
}
|
|
}
|
|
|
|
content = content.Replace("\0", "");
|
|
var lines = content.Split(["\n"], StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
var lineList = new List<string>();
|
|
|
|
for (var i = 0; i < lines.Length; i++)
|
|
{
|
|
if (lines[i][0] != '#')
|
|
{
|
|
lineList.Add(lines[i]);
|
|
}
|
|
}
|
|
|
|
lines = [.. lineList];
|
|
|
|
if (lines.Length > 0)
|
|
{
|
|
versions.entries = new VersionsEntry[lines.Length - 1];
|
|
|
|
var cols = lines[0].Split('|');
|
|
|
|
for (var c = 0; c < cols.Length; c++)
|
|
{
|
|
var friendlyName = cols[c].Split('!').ElementAt(0);
|
|
|
|
for (var i = 1; i < lines.Length; i++)
|
|
{
|
|
var row = lines[i].Split('|');
|
|
|
|
switch (friendlyName)
|
|
{
|
|
case "Region":
|
|
versions.entries[i - 1].region = row[c];
|
|
break;
|
|
case "BuildConfig":
|
|
versions.entries[i - 1].buildConfig = row[c];
|
|
break;
|
|
case "CDNConfig":
|
|
versions.entries[i - 1].cdnConfig = row[c];
|
|
break;
|
|
case "Keyring":
|
|
case "KeyRing":
|
|
versions.entries[i - 1].keyRing = row[c];
|
|
break;
|
|
case "BuildId":
|
|
versions.entries[i - 1].buildId = row[c];
|
|
break;
|
|
case "VersionName":
|
|
case "VersionsName":
|
|
versions.entries[i - 1].versionsName = row[c].Trim('\r');
|
|
break;
|
|
case "ProductConfig":
|
|
versions.entries[i - 1].productConfig = row[c];
|
|
break;
|
|
default:
|
|
Console.WriteLine("!!!!!!!! Unknown versions variable '" + friendlyName + "'");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return versions;
|
|
}
|
|
|
|
private static CdnsFile GetCDNs(string program)
|
|
{
|
|
string content;
|
|
|
|
var cdns = new CdnsFile();
|
|
|
|
if (program == "gryphon")
|
|
{
|
|
cdns.entries = new CdnsEntry[1];
|
|
cdns.entries[0].hosts = ["http://cdn.blizzard.com", "http://blzddist1-a.akamaihd.net"];
|
|
cdns.entries[0].path = "tpr/gryphon";
|
|
cdns.entries[0].configPath = "configs/data/";
|
|
return cdns;
|
|
}
|
|
|
|
using (HttpResponseMessage response = cdn.client.GetAsync(new Uri(baseUrl + "v2/products/" + program + "/" + "cdns")).Result)
|
|
{
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
using (HttpContent res = response.Content)
|
|
{
|
|
content = res.ReadAsStringAsync().Result;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Error during retrieving HTTP cdns: Received bad HTTP code " + response.StatusCode);
|
|
return cdns;
|
|
}
|
|
}
|
|
|
|
var lines = content.Split(["\n", "\r"], StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
var lineList = new List<string>();
|
|
|
|
for (var i = 0; i < lines.Length; i++)
|
|
{
|
|
if (lines[i][0] != '#')
|
|
{
|
|
lineList.Add(lines[i]);
|
|
}
|
|
}
|
|
|
|
lines = [.. lineList];
|
|
|
|
if (lines.Length > 0)
|
|
{
|
|
cdns.entries = new CdnsEntry[lines.Length - 1];
|
|
|
|
var cols = lines[0].Split('|');
|
|
|
|
for (var c = 0; c < cols.Length; c++)
|
|
{
|
|
var friendlyName = cols[c].Split('!').ElementAt(0);
|
|
|
|
for (var i = 1; i < lines.Length; i++)
|
|
{
|
|
var row = lines[i].Split('|');
|
|
|
|
switch (friendlyName)
|
|
{
|
|
case "Name":
|
|
cdns.entries[i - 1].name = row[c];
|
|
break;
|
|
case "Path":
|
|
cdns.entries[i - 1].path = row[c];
|
|
break;
|
|
case "Hosts":
|
|
var hosts = row[c].Split(' ');
|
|
cdns.entries[i - 1].hosts = new string[hosts.Length];
|
|
for (var h = 0; h < hosts.Length; h++)
|
|
{
|
|
cdns.entries[i - 1].hosts[h] = hosts[h];
|
|
}
|
|
break;
|
|
case "ConfigPath":
|
|
cdns.entries[i - 1].configPath = row[c];
|
|
break;
|
|
default:
|
|
//Console.WriteLine("!!!!!!!! Unknown cdns variable '" + friendlyName + "'");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var subcdn in cdns.entries)
|
|
{
|
|
foreach (var cdnHost in subcdn.hosts)
|
|
{
|
|
if (!cdn.cdnList.Contains(cdnHost))
|
|
{
|
|
//cdn.cdnList.Add(cdnHost);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return cdns;
|
|
}
|
|
|
|
private static GameBlobFile GetProductConfig(string url, string hash)
|
|
{
|
|
string content;
|
|
|
|
var gblob = new GameBlobFile();
|
|
|
|
try
|
|
{
|
|
content = Encoding.UTF8.GetString(cdn.Get(url + hash[0] + hash[1] + "/" + hash[2] + hash[3] + "/" + hash).Result);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Error retrieving product config: " + e.Message);
|
|
return gblob;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(content))
|
|
{
|
|
Console.WriteLine("Error reading product config!");
|
|
return gblob;
|
|
}
|
|
|
|
dynamic json = Newtonsoft.Json.JsonConvert.DeserializeObject(content);
|
|
if (json.all.config.decryption_key_name != null)
|
|
gblob.decryptionKeyName = json.all.config.decryption_key_name.Value;
|
|
|
|
return gblob;
|
|
}
|
|
|
|
private static BuildConfigFile GetBuildConfig(string url, string hash)
|
|
{
|
|
string content;
|
|
|
|
var buildConfig = new BuildConfigFile();
|
|
|
|
try
|
|
{
|
|
if (!File.Exists("fakebuildconfig"))
|
|
{
|
|
content = Encoding.UTF8.GetString(cdn.Get(url + "/config/" + hash[0] + hash[1] + "/" + hash[2] + hash[3] + "/" + hash).Result);
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("!!!!!!!!! LOADING FAKE BUILDCONFIG");
|
|
content = File.ReadAllText("fakebuildconfig").Replace("\r", "");
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Error retrieving build config: " + e.Message);
|
|
return buildConfig;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(content) || !content.StartsWith("# Build"))
|
|
{
|
|
Console.WriteLine("Error reading build config");
|
|
return buildConfig;
|
|
}
|
|
|
|
var lines = content.Split(["\n"], StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
for (var i = 0; i < lines.Length; i++)
|
|
{
|
|
if (lines[i].StartsWith('#') || lines[i].Length == 0) { continue; }
|
|
var cols = lines[i].Split([" = "], StringSplitOptions.RemoveEmptyEntries);
|
|
switch (cols[0])
|
|
{
|
|
case "root":
|
|
buildConfig.root = cols[1];
|
|
break;
|
|
case "download":
|
|
buildConfig.download = cols[1].Split(' ');
|
|
break;
|
|
case "install":
|
|
buildConfig.install = cols[1].Split(' ');
|
|
break;
|
|
case "encoding":
|
|
buildConfig.encoding = cols[1].Split(' ');
|
|
break;
|
|
case "encoding-size":
|
|
var encodingSize = cols[1].Split(' ');
|
|
buildConfig.encodingSize = encodingSize;
|
|
break;
|
|
case "size":
|
|
buildConfig.size = cols[1].Split(' ');
|
|
break;
|
|
case "size-size":
|
|
buildConfig.sizeSize = cols[1].Split(' ');
|
|
break;
|
|
case "build-name":
|
|
buildConfig.buildName = cols[1];
|
|
break;
|
|
case "build-playbuild-installer":
|
|
buildConfig.buildPlaybuildInstaller = cols[1];
|
|
break;
|
|
case "build-product":
|
|
buildConfig.buildProduct = cols[1];
|
|
break;
|
|
case "build-uid":
|
|
buildConfig.buildUid = cols[1];
|
|
break;
|
|
case "patch":
|
|
buildConfig.patch = cols[1];
|
|
break;
|
|
case "patch-size":
|
|
buildConfig.patchSize = cols[1];
|
|
break;
|
|
case "patch-config":
|
|
buildConfig.patchConfig = cols[1];
|
|
break;
|
|
case "build-branch": // Overwatch
|
|
buildConfig.buildBranch = cols[1];
|
|
break;
|
|
case "build-num": // Agent
|
|
case "build-number": // Overwatch
|
|
case "build-version": // Catalog
|
|
buildConfig.buildNumber = cols[1];
|
|
break;
|
|
case "build-attributes": // Agent
|
|
buildConfig.buildAttributes = cols[1];
|
|
break;
|
|
case "build-comments": // D3
|
|
buildConfig.buildComments = cols[1];
|
|
break;
|
|
case "build-creator": // D3
|
|
buildConfig.buildCreator = cols[1];
|
|
break;
|
|
case "build-fixed-hash": // S2
|
|
buildConfig.buildFixedHash = cols[1];
|
|
break;
|
|
case "build-replay-hash": // S2
|
|
buildConfig.buildReplayHash = cols[1];
|
|
break;
|
|
case "build-t1-manifest-version":
|
|
buildConfig.buildManifestVersion = cols[1];
|
|
break;
|
|
case "install-size":
|
|
buildConfig.installSize = cols[1].Split(' ');
|
|
break;
|
|
case "download-size":
|
|
buildConfig.downloadSize = cols[1].Split(' ');
|
|
break;
|
|
case "build-partial-priority":
|
|
case "partial-priority":
|
|
buildConfig.partialPriority = cols[1];
|
|
break;
|
|
case "partial-priority-size":
|
|
buildConfig.partialPrioritySize = cols[1];
|
|
break;
|
|
case "build-signature-file":
|
|
buildConfig.buildSignatureFile = cols[1];
|
|
break;
|
|
case "patch-index":
|
|
buildConfig.patchIndex = cols[1].Split(' ');
|
|
break;
|
|
case "patch-index-size":
|
|
buildConfig.patchIndexSize = cols[1].Split(' ');
|
|
break;
|
|
default:
|
|
//Console.WriteLine("!!!!!!!! Unknown buildconfig variable '" + cols[0] + "'");
|
|
break;
|
|
}
|
|
}
|
|
|
|
return buildConfig;
|
|
}
|
|
|
|
private static Dictionary<string, IndexEntry> ParseIndex(string url, string hash, string folder = "data")
|
|
{
|
|
byte[] indexContent = cdn.Get(url + folder + "/" + hash[0] + hash[1] + "/" + hash[2] + hash[3] + "/" + hash + ".index").Result;
|
|
|
|
var returnDict = new Dictionary<string, IndexEntry>();
|
|
|
|
using (MemoryStream ms = new(indexContent))
|
|
using (BinaryReader bin = new(ms))
|
|
{
|
|
bin.BaseStream.Position = bin.BaseStream.Length - 28;
|
|
|
|
var footer = new IndexFooter
|
|
{
|
|
tocHash = bin.ReadBytes(8),
|
|
version = bin.ReadByte(),
|
|
unk0 = bin.ReadByte(),
|
|
unk1 = bin.ReadByte(),
|
|
blockSizeKB = bin.ReadByte(),
|
|
offsetBytes = bin.ReadByte(),
|
|
sizeBytes = bin.ReadByte(),
|
|
keySizeInBytes = bin.ReadByte(),
|
|
checksumSize = bin.ReadByte(),
|
|
numElements = bin.ReadUInt32()
|
|
};
|
|
|
|
footer.footerChecksum = bin.ReadBytes(footer.checksumSize);
|
|
|
|
// TODO: Read numElements as BE if it is wrong as LE
|
|
if ((footer.numElements & 0xff000000) != 0)
|
|
{
|
|
bin.BaseStream.Position -= footer.checksumSize + 4;
|
|
footer.numElements = bin.ReadUInt32(true);
|
|
}
|
|
|
|
bin.BaseStream.Position = 0;
|
|
|
|
var indexBlockSize = 1024 * footer.blockSizeKB;
|
|
var recordSize = footer.keySizeInBytes + footer.sizeBytes + footer.offsetBytes;
|
|
var recordsPerBlock = indexBlockSize / recordSize;
|
|
var recordsRead = 0;
|
|
|
|
while (recordsRead != footer.numElements)
|
|
{
|
|
var blockRecordsRead = 0;
|
|
|
|
for (var blockIndex = 0; blockIndex < recordsPerBlock && recordsRead < footer.numElements; blockIndex++, recordsRead++)
|
|
{
|
|
var headerHash = Convert.ToHexString(bin.ReadBytes(footer.keySizeInBytes));
|
|
var entry = new IndexEntry();
|
|
|
|
if (footer.sizeBytes == 4)
|
|
{
|
|
entry.size = bin.ReadUInt32(true);
|
|
}
|
|
else
|
|
{
|
|
throw new NotImplementedException("Index size reading other than 4 is not implemented!");
|
|
}
|
|
|
|
if (footer.offsetBytes == 4)
|
|
{
|
|
// Archive index
|
|
entry.offset = bin.ReadUInt32(true);
|
|
}
|
|
else if (footer.offsetBytes == 6)
|
|
{
|
|
// Group index
|
|
throw new NotImplementedException("Group index reading is not implemented!");
|
|
}
|
|
else if (footer.offsetBytes == 0)
|
|
{
|
|
// File index
|
|
}
|
|
else
|
|
{
|
|
throw new NotImplementedException("Offset size reading other than 4/6/0 is not implemented!");
|
|
}
|
|
|
|
returnDict.Add(headerHash, entry);
|
|
|
|
blockRecordsRead++;
|
|
}
|
|
|
|
bin.ReadBytes(indexBlockSize - (blockRecordsRead * recordSize));
|
|
}
|
|
}
|
|
|
|
return returnDict;
|
|
}
|
|
|
|
private static List<string> ParsePatchFileIndex(string url, string hash)
|
|
{
|
|
byte[] indexContent = cdn.Get(url + "/patch/" + hash[0] + hash[1] + "/" + hash[2] + hash[3] + "/" + hash + ".index").Result;
|
|
|
|
var list = new List<string>();
|
|
|
|
using (BinaryReader bin = new(new MemoryStream(indexContent)))
|
|
{
|
|
int indexEntries = indexContent.Length / 4096;
|
|
|
|
for (var b = 0; b < indexEntries; b++)
|
|
{
|
|
for (var bi = 0; bi < 170; bi++)
|
|
{
|
|
var headerHash = Convert.ToHexString(bin.ReadBytes(16));
|
|
|
|
var size = bin.ReadUInt32(true);
|
|
|
|
list.Add(headerHash);
|
|
}
|
|
bin.ReadBytes(16);
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
private static void GetIndexes(string url, string[] archives)
|
|
{
|
|
Parallel.ForEach(archives, (archive, state, i) =>
|
|
{
|
|
try
|
|
{
|
|
byte[] indexContent = cdn.Get(url + "/data/" + archives[i][0] + archives[i][1] + "/" + archives[i][2] + archives[i][3] + "/" + archives[i] + ".index").Result;
|
|
|
|
using (BinaryReader bin = new(new MemoryStream(indexContent)))
|
|
{
|
|
int indexEntries = indexContent.Length / 4096;
|
|
|
|
for (var b = 0; b < indexEntries; b++)
|
|
{
|
|
for (var bi = 0; bi < 170; bi++)
|
|
{
|
|
var headerHash = Convert.ToHexString(bin.ReadBytes(16));
|
|
|
|
var entry = new IndexEntry()
|
|
{
|
|
index = (short)i,
|
|
size = bin.ReadUInt32(true),
|
|
offset = bin.ReadUInt32(true)
|
|
};
|
|
|
|
cacheLock.EnterUpgradeableReadLock();
|
|
try
|
|
{
|
|
if (!indexDictionary.ContainsKey(headerHash))
|
|
{
|
|
cacheLock.EnterWriteLock();
|
|
try
|
|
{
|
|
if (!indexDictionary.TryAdd(headerHash, entry))
|
|
{
|
|
Console.WriteLine("Duplicate index entry for " + headerHash + " " + "(index: " + archives[i] + ", size: " + entry.size + ", offset: " + entry.offset);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
cacheLock.ExitWriteLock();
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
cacheLock.ExitUpgradeableReadLock();
|
|
}
|
|
}
|
|
bin.ReadBytes(16);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Error retrieving index: " + e.Message);
|
|
}
|
|
});
|
|
}
|
|
private static void GetPatchIndexes(string url, string[] archives)
|
|
{
|
|
Parallel.ForEach(archives, (archive, state, i) =>
|
|
{
|
|
try
|
|
{
|
|
byte[] indexContent = cdn.Get(url + "/patch/" + archives[i][0] + archives[i][1] + "/" + archives[i][2] + archives[i][3] + "/" + archives[i] + ".index").Result;
|
|
|
|
using (BinaryReader bin = new(new MemoryStream(indexContent)))
|
|
{
|
|
int indexEntries = indexContent.Length / 4096;
|
|
|
|
for (var b = 0; b < indexEntries; b++)
|
|
{
|
|
for (var bi = 0; bi < 170; bi++)
|
|
{
|
|
var headerHash = Convert.ToHexString(bin.ReadBytes(16));
|
|
|
|
var entry = new IndexEntry()
|
|
{
|
|
index = (short)i,
|
|
size = bin.ReadUInt32(true),
|
|
offset = bin.ReadUInt32(true)
|
|
};
|
|
|
|
cacheLock.EnterUpgradeableReadLock();
|
|
try
|
|
{
|
|
if (!patchIndexDictionary.ContainsKey(headerHash))
|
|
{
|
|
cacheLock.EnterWriteLock();
|
|
try
|
|
{
|
|
if (!patchIndexDictionary.TryAdd(headerHash, entry))
|
|
{
|
|
Console.WriteLine("Duplicate patch index entry for " + headerHash + " " + "(index: " + archives[i] + ", size: " + entry.size + ", offset: " + entry.offset);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
cacheLock.ExitWriteLock();
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
cacheLock.ExitUpgradeableReadLock();
|
|
}
|
|
}
|
|
bin.ReadBytes(16);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Unable to retrieve patch index: " + e.Message);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
private static RootFile GetRoot(string url, string hash, bool parseIt = false)
|
|
{
|
|
var root = new RootFile
|
|
{
|
|
entriesLookup = [],
|
|
entriesFDID = []
|
|
};
|
|
|
|
byte[] content = cdn.Get(url + "/data/" + hash[0] + hash[1] + "/" + hash[2] + hash[3] + "/" + hash).Result;
|
|
if (!parseIt) return root;
|
|
|
|
var namedCount = 0;
|
|
var unnamedCount = 0;
|
|
uint totalFiles = 0;
|
|
uint namedFiles = 0;
|
|
var newRoot = false;
|
|
|
|
uint dfHeaderSize = 0;
|
|
uint dfVersion = 0;
|
|
|
|
using (BinaryReader bin = new(new MemoryStream(BLTE.Parse(content))))
|
|
{
|
|
var header = bin.ReadUInt32();
|
|
|
|
if (header == 1296454484)
|
|
{
|
|
totalFiles = bin.ReadUInt32();
|
|
namedFiles = bin.ReadUInt32();
|
|
|
|
if (namedFiles == 1 || namedFiles == 2)
|
|
{
|
|
// Post 10.1.7
|
|
dfHeaderSize = totalFiles;
|
|
dfVersion = namedFiles;
|
|
|
|
if (dfVersion == 1 || dfVersion == 2)
|
|
{
|
|
totalFiles = bin.ReadUInt32();
|
|
namedFiles = bin.ReadUInt32();
|
|
}
|
|
else
|
|
{
|
|
throw new Exception("Unsupported root version: " + dfVersion);
|
|
}
|
|
|
|
bin.BaseStream.Position = dfHeaderSize;
|
|
}
|
|
|
|
newRoot = true;
|
|
}
|
|
else
|
|
{
|
|
bin.BaseStream.Position = 0;
|
|
}
|
|
|
|
var blockCount = 0;
|
|
|
|
while (bin.BaseStream.Position < bin.BaseStream.Length)
|
|
{
|
|
uint count = 0;
|
|
ContentFlags contentFlags = 0;
|
|
LocaleFlags localeFlags = 0;
|
|
|
|
if (dfVersion == 2)
|
|
{
|
|
count = bin.ReadUInt32();
|
|
localeFlags = (LocaleFlags)bin.ReadUInt32();
|
|
var unkFlags = bin.ReadUInt32();
|
|
contentFlags = (ContentFlags)bin.ReadUInt32();
|
|
var unkByte = bin.ReadByte();
|
|
}
|
|
else
|
|
{
|
|
count = bin.ReadUInt32();
|
|
contentFlags = (ContentFlags)bin.ReadUInt32();
|
|
localeFlags = (LocaleFlags)bin.ReadUInt32();
|
|
}
|
|
|
|
//Console.WriteLine("[Block " + blockCount + "] " + count + " entries. Content flags: " + contentFlags.ToString() + ", Locale flags: " + localeFlags.ToString());
|
|
var entries = new RootEntry[count];
|
|
var filedataIds = new int[count];
|
|
|
|
var fileDataIndex = 0;
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
entries[i].localeFlags = localeFlags;
|
|
entries[i].contentFlags = contentFlags;
|
|
|
|
filedataIds[i] = fileDataIndex + bin.ReadInt32();
|
|
entries[i].fileDataID = (uint)filedataIds[i];
|
|
fileDataIndex = filedataIds[i] + 1;
|
|
}
|
|
|
|
var blockFdids = new List<string>();
|
|
if (!newRoot)
|
|
{
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
entries[i].md5 = bin.ReadBytes(16);
|
|
entries[i].lookup = bin.ReadUInt64();
|
|
root.entriesLookup.Add(entries[i].lookup, entries[i]);
|
|
root.entriesFDID.Add(entries[i].fileDataID, entries[i]);
|
|
blockFdids.Add(entries[i].fileDataID.ToString());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
entries[i].md5 = bin.ReadBytes(16);
|
|
}
|
|
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
if (contentFlags.HasFlag(ContentFlags.NoNames))
|
|
{
|
|
entries[i].lookup = 0;
|
|
unnamedCount++;
|
|
}
|
|
else
|
|
{
|
|
entries[i].lookup = bin.ReadUInt64();
|
|
root.entriesLookup.Add(entries[i].lookup, entries[i]);
|
|
namedCount++;
|
|
}
|
|
|
|
root.entriesFDID.Add(entries[i].fileDataID, entries[i]);
|
|
blockFdids.Add(entries[i].fileDataID.ToString());
|
|
}
|
|
}
|
|
|
|
//File.WriteAllLinesAsync("blocks/Block" + blockCount + ".txt", blockFdids);
|
|
blockCount++;
|
|
}
|
|
}
|
|
|
|
if ((namedFiles > 0) && namedFiles != namedCount)
|
|
throw new Exception("Didn't read correct amount of named files! Read " + namedCount + " but expected " + namedFiles);
|
|
|
|
if ((totalFiles > 0) && totalFiles != (namedCount + unnamedCount))
|
|
throw new Exception("Didn't read correct amount of total files! Read " + (namedCount + unnamedCount) + " but expected " + totalFiles);
|
|
|
|
return root;
|
|
}
|
|
|
|
private static DownloadFile GetDownload(string url, string hash, bool parseIt = false)
|
|
{
|
|
var download = new DownloadFile();
|
|
|
|
byte[] content = cdn.Get(url + "/data/" + hash[0] + hash[1] + "/" + hash[2] + hash[3] + "/" + hash).Result;
|
|
|
|
if (!parseIt) return download;
|
|
|
|
using (BinaryReader bin = new(new MemoryStream(BLTE.Parse(content))))
|
|
{
|
|
if (Encoding.UTF8.GetString(bin.ReadBytes(2)) != "DL") { throw new Exception("Error while parsing download file. Did BLTE header size change?"); }
|
|
download.version = bin.ReadByte();
|
|
download.hashSizeEKey = bin.ReadByte();
|
|
download.hasChecksumInEntry = bin.ReadBoolean();
|
|
download.numEntries = bin.ReadUInt32(true);
|
|
download.numTags = bin.ReadUInt16(true);
|
|
download.flagSize = bin.ReadByte();
|
|
|
|
download.entries = new DownloadEntry[download.numEntries];
|
|
for (int i = 0; i < download.numEntries; i++)
|
|
{
|
|
download.entries[i].eKey = Convert.ToHexString(bin.ReadBytes(download.hashSizeEKey));
|
|
download.entries[i].size = bin.ReadUInt40(true);
|
|
download.entries[i].priority = bin.ReadByte();
|
|
|
|
if (download.hasChecksumInEntry)
|
|
{
|
|
download.entries[i].checksum = bin.ReadUInt32(true);
|
|
}
|
|
|
|
if (download.flagSize == 1)
|
|
{
|
|
download.entries[i].flags = bin.ReadByte();
|
|
}
|
|
else
|
|
{
|
|
throw new Exception("Unexpected download flag size");
|
|
}
|
|
}
|
|
}
|
|
|
|
return download;
|
|
}
|
|
|
|
private static InstallFile GetInstall(string url, string hash, bool parseIt = false)
|
|
{
|
|
var install = new InstallFile();
|
|
|
|
byte[] content = cdn.Get(url + "/data/" + hash[0] + hash[1] + "/" + hash[2] + hash[3] + "/" + hash).Result;
|
|
|
|
if (!parseIt) return install;
|
|
|
|
using (BinaryReader bin = new(new MemoryStream(BLTE.Parse(content))))
|
|
{
|
|
if (Encoding.UTF8.GetString(bin.ReadBytes(2)) != "IN") { throw new Exception("Error while parsing install file. Did BLTE header size change?"); }
|
|
|
|
bin.ReadByte();
|
|
|
|
install.hashSize = bin.ReadByte();
|
|
if (install.hashSize != 16) throw new Exception("Unsupported install hash size!");
|
|
|
|
install.numTags = bin.ReadUInt16(true);
|
|
install.numEntries = bin.ReadUInt32(true);
|
|
|
|
int bytesPerTag = ((int)install.numEntries + 7) / 8;
|
|
|
|
install.tags = new InstallTagEntry[install.numTags];
|
|
|
|
for (var i = 0; i < install.numTags; i++)
|
|
{
|
|
install.tags[i].name = bin.ReadCString();
|
|
install.tags[i].type = bin.ReadUInt16(true);
|
|
|
|
var filebits = bin.ReadBytes(bytesPerTag);
|
|
|
|
for (int j = 0; j < bytesPerTag; j++)
|
|
filebits[j] = (byte)((filebits[j] * 0x0202020202 & 0x010884422010) % 1023);
|
|
|
|
install.tags[i].files = new BitArray(filebits);
|
|
}
|
|
|
|
install.entries = new InstallFileEntry[install.numEntries];
|
|
|
|
for (var i = 0; i < install.numEntries; i++)
|
|
{
|
|
install.entries[i].name = bin.ReadCString();
|
|
install.entries[i].contentHash = bin.ReadBytes(install.hashSize);
|
|
install.entries[i].size = bin.ReadUInt32(true);
|
|
install.entries[i].tags = [];
|
|
for (var j = 0; j < install.numTags; j++)
|
|
{
|
|
if (install.tags[j].files[i] == true)
|
|
{
|
|
install.entries[i].tags.Add(install.tags[j].type + "=" + install.tags[j].name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return install;
|
|
}
|
|
|
|
private static async Task<EncodingFile> GetEncoding(string url, string hash, int encodingSize = 0, bool parseTableB = false, bool checkStuff = false, bool encoded = true)
|
|
{
|
|
var encoding = new EncodingFile();
|
|
|
|
byte[] content;
|
|
BinaryReader bin;
|
|
if (encoded)
|
|
{
|
|
content = await cdn.Get(url + "/data/" + hash[0] + hash[1] + "/" + hash[2] + hash[3] + "/" + hash);
|
|
|
|
if (encodingSize != 0 && encodingSize != content.Length)
|
|
{
|
|
content = await cdn.Get(url + "/data/" + hash[0] + hash[1] + "/" + hash[2] + hash[3] + "/" + hash, true);
|
|
|
|
if (encodingSize != content.Length && encodingSize != 0)
|
|
{
|
|
throw new Exception("File corrupt/not fully downloaded! Remove " + "data / " + hash[0] + hash[1] + " / " + hash[2] + hash[3] + " / " + hash + " from cache.");
|
|
}
|
|
}
|
|
|
|
bin = new BinaryReader(new MemoryStream(BLTE.Parse(content)));
|
|
}
|
|
else
|
|
{
|
|
bin = new BinaryReader(new MemoryStream(File.ReadAllBytes(url)));
|
|
}
|
|
|
|
|
|
if (Encoding.UTF8.GetString(bin.ReadBytes(2)) != "EN") { throw new Exception("Error while parsing encoding file. Did BLTE header size change?"); }
|
|
encoding.unk1 = bin.ReadByte();
|
|
encoding.checksumSizeA = bin.ReadByte();
|
|
encoding.checksumSizeB = bin.ReadByte();
|
|
encoding.sizeA = bin.ReadUInt16(true);
|
|
encoding.sizeB = bin.ReadUInt16(true);
|
|
encoding.numEntriesA = bin.ReadUInt32(true);
|
|
encoding.numEntriesB = bin.ReadUInt32(true);
|
|
bin.ReadByte(); // unk
|
|
encoding.stringBlockSize = bin.ReadUInt32(true);
|
|
|
|
var headerLength = bin.BaseStream.Position;
|
|
var stringBlockEntries = new List<string>();
|
|
|
|
if (parseTableB)
|
|
{
|
|
while ((bin.BaseStream.Position - headerLength) != (long)encoding.stringBlockSize)
|
|
{
|
|
stringBlockEntries.Add(bin.ReadCString());
|
|
}
|
|
|
|
encoding.stringBlockEntries = [.. stringBlockEntries];
|
|
}
|
|
else
|
|
{
|
|
bin.BaseStream.Position += (long)encoding.stringBlockSize;
|
|
}
|
|
|
|
/* Table A */
|
|
if (checkStuff)
|
|
{
|
|
encoding.aHeaders = new EncodingHeaderEntry[encoding.numEntriesA];
|
|
|
|
for (int i = 0; i < encoding.numEntriesA; i++)
|
|
{
|
|
encoding.aHeaders[i].firstHash = Convert.ToHexString(bin.ReadBytes(16));
|
|
encoding.aHeaders[i].checksum = Convert.ToHexString(bin.ReadBytes(16));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bin.BaseStream.Position += encoding.numEntriesA * 32;
|
|
}
|
|
|
|
var tableAstart = bin.BaseStream.Position;
|
|
|
|
List<EncodingFileEntry> entries = [];
|
|
|
|
for (int i = 0; i < encoding.numEntriesA; i++)
|
|
{
|
|
ushort keysCount;
|
|
while ((keysCount = bin.ReadUInt16()) != 0)
|
|
{
|
|
EncodingFileEntry entry = new()
|
|
{
|
|
keyCount = keysCount,
|
|
size = bin.ReadUInt32(true),
|
|
cKey = Convert.ToHexString(bin.ReadBytes(16)),
|
|
eKeys = []
|
|
};
|
|
|
|
for (int key = 0; key < entry.keyCount; key++)
|
|
{
|
|
entry.eKeys.Add(Convert.ToHexString(bin.ReadBytes(16)));
|
|
}
|
|
|
|
entries.Add(entry);
|
|
}
|
|
|
|
var remaining = 4096 - ((bin.BaseStream.Position - tableAstart) % 4096);
|
|
if (remaining > 0) { bin.BaseStream.Position += remaining; }
|
|
}
|
|
|
|
encoding.aEntries = [.. entries];
|
|
|
|
if (!parseTableB)
|
|
{
|
|
return encoding;
|
|
}
|
|
|
|
/* Table B */
|
|
if (checkStuff)
|
|
{
|
|
encoding.bHeaders = new EncodingHeaderEntry[encoding.numEntriesB];
|
|
|
|
for (int i = 0; i < encoding.numEntriesB; i++)
|
|
{
|
|
encoding.bHeaders[i].firstHash = Convert.ToHexString(bin.ReadBytes(16));
|
|
encoding.bHeaders[i].checksum = Convert.ToHexString(bin.ReadBytes(16));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bin.BaseStream.Position += encoding.numEntriesB * 32;
|
|
}
|
|
|
|
var tableBstart = bin.BaseStream.Position;
|
|
|
|
encoding.bEntries = [];
|
|
|
|
while (bin.BaseStream.Position < tableBstart + 4096 * encoding.numEntriesB)
|
|
{
|
|
var remaining = 4096 - (bin.BaseStream.Position - tableBstart) % 4096;
|
|
|
|
if (remaining < 25)
|
|
{
|
|
bin.BaseStream.Position += remaining;
|
|
continue;
|
|
}
|
|
|
|
var key = Convert.ToHexString(bin.ReadBytes(16));
|
|
|
|
EncodingFileDescEntry entry = new()
|
|
{
|
|
stringIndex = bin.ReadUInt32(true),
|
|
compressedSize = bin.ReadUInt40(true)
|
|
};
|
|
|
|
if (entry.stringIndex == uint.MaxValue) break;
|
|
|
|
encoding.bEntries.Add(key, entry);
|
|
}
|
|
|
|
// Go to the end until we hit a non-NUL byte
|
|
while (bin.BaseStream.Position < bin.BaseStream.Length)
|
|
{
|
|
if (bin.ReadByte() != 0)
|
|
break;
|
|
}
|
|
|
|
bin.BaseStream.Position -= 1;
|
|
var eespecSize = bin.BaseStream.Length - bin.BaseStream.Position;
|
|
encoding.encodingESpec = new string(bin.ReadChars(int.Parse(eespecSize.ToString())));
|
|
|
|
return encoding;
|
|
}
|
|
|
|
private static PatchFile GetPatch(string url, string hash, bool parseIt = false)
|
|
{
|
|
var patchFile = new PatchFile();
|
|
|
|
byte[] content = cdn.Get(url + "/patch/" + hash[0] + hash[1] + "/" + hash[2] + hash[3] + "/" + hash).Result;
|
|
|
|
if (!parseIt) return patchFile;
|
|
|
|
using (BinaryReader bin = new(new MemoryStream(content)))
|
|
{
|
|
if (Encoding.UTF8.GetString(bin.ReadBytes(2)) != "PA") { throw new Exception("Error while parsing patch file!"); }
|
|
|
|
patchFile.version = bin.ReadByte();
|
|
patchFile.fileKeySize = bin.ReadByte();
|
|
patchFile.sizeB = bin.ReadByte();
|
|
patchFile.patchKeySize = bin.ReadByte();
|
|
patchFile.blockSizeBits = bin.ReadByte();
|
|
patchFile.blockCount = bin.ReadUInt16(true);
|
|
patchFile.flags = bin.ReadByte();
|
|
patchFile.encodingContentKey = bin.ReadBytes(16);
|
|
patchFile.encodingEncodingKey = bin.ReadBytes(16);
|
|
patchFile.decodedSize = bin.ReadUInt32(true);
|
|
patchFile.encodedSize = bin.ReadUInt32(true);
|
|
patchFile.especLength = bin.ReadByte();
|
|
patchFile.encodingSpec = new string(bin.ReadChars(patchFile.especLength));
|
|
|
|
patchFile.blocks = new PatchBlock[patchFile.blockCount];
|
|
for (var i = 0; i < patchFile.blockCount; i++)
|
|
{
|
|
patchFile.blocks[i].lastFileContentKey = bin.ReadBytes(patchFile.fileKeySize);
|
|
patchFile.blocks[i].blockMD5 = bin.ReadBytes(16);
|
|
patchFile.blocks[i].blockOffset = bin.ReadUInt32(true);
|
|
|
|
var prevPos = bin.BaseStream.Position;
|
|
|
|
var files = new List<BlockFile>();
|
|
|
|
bin.BaseStream.Position = patchFile.blocks[i].blockOffset;
|
|
while (bin.BaseStream.Position <= patchFile.blocks[i].blockOffset + 0x10000)
|
|
{
|
|
var file = new BlockFile
|
|
{
|
|
numPatches = bin.ReadByte()
|
|
};
|
|
|
|
if (file.numPatches == 0) break;
|
|
file.targetFileContentKey = bin.ReadBytes(patchFile.fileKeySize);
|
|
file.decodedSize = bin.ReadUInt40(true);
|
|
|
|
var filePatches = new List<FilePatch>();
|
|
|
|
for (var j = 0; j < file.numPatches; j++)
|
|
{
|
|
var filePatch = new FilePatch
|
|
{
|
|
sourceFileEncodingKey = bin.ReadBytes(patchFile.fileKeySize),
|
|
decodedSize = bin.ReadUInt40(true),
|
|
patchEncodingKey = bin.ReadBytes(patchFile.patchKeySize),
|
|
patchSize = bin.ReadUInt32(true),
|
|
patchIndex = bin.ReadByte()
|
|
};
|
|
filePatches.Add(filePatch);
|
|
}
|
|
|
|
file.patches = [.. filePatches];
|
|
|
|
files.Add(file);
|
|
}
|
|
|
|
patchFile.blocks[i].files = [.. files];
|
|
bin.BaseStream.Position = prevPos;
|
|
}
|
|
}
|
|
|
|
return patchFile;
|
|
}
|
|
private static void UpdateListfile()
|
|
{
|
|
if (!File.Exists("listfile.txt") || DateTime.Now.AddHours(-1) > File.GetLastWriteTime("listfile.txt"))
|
|
{
|
|
using (var client = new HttpClient())
|
|
{
|
|
client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip");
|
|
client.DefaultRequestHeaders.Add("User-Agent", "BuildBackup");
|
|
|
|
var response = client.GetAsync("https://github.com/wowdev/wow-listfile/releases/latest/download/verified-listfile.csv").Result;
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
using (var responseStream = response.Content.ReadAsStreamAsync().Result)
|
|
using (var decompressedStream = new System.IO.Compression.GZipStream(responseStream, System.IO.Compression.CompressionMode.Decompress))
|
|
using (var stream = new MemoryStream())
|
|
{
|
|
decompressedStream.CopyTo(stream);
|
|
File.WriteAllBytes("listfile.txt", stream.ToArray());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|