2019-10-16 21:36:14 +00:00
|
|
|
|
using System;
|
2019-07-23 04:17:52 +00:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.IO;
|
2019-10-07 17:33:34 +00:00
|
|
|
|
using System.IO.Compression;
|
2019-07-23 04:17:52 +00:00
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Text;
|
2019-11-15 13:37:04 +00:00
|
|
|
|
using System.Threading.Tasks;
|
2019-08-26 23:02:49 +00:00
|
|
|
|
using System.Windows;
|
2019-07-23 04:17:52 +00:00
|
|
|
|
using Wabbajack.Common;
|
2019-10-16 03:10:34 +00:00
|
|
|
|
using Wabbajack.Lib.Downloaders;
|
|
|
|
|
using Wabbajack.Lib.NexusApi;
|
|
|
|
|
using Wabbajack.Lib.Validation;
|
2019-11-15 13:06:34 +00:00
|
|
|
|
using Wabbajack.VirtualFileSystem;
|
2019-09-18 21:33:23 +00:00
|
|
|
|
using Directory = Alphaleonis.Win32.Filesystem.Directory;
|
|
|
|
|
using File = Alphaleonis.Win32.Filesystem.File;
|
|
|
|
|
using FileInfo = Alphaleonis.Win32.Filesystem.FileInfo;
|
|
|
|
|
using Path = Alphaleonis.Win32.Filesystem.Path;
|
2019-07-23 04:17:52 +00:00
|
|
|
|
|
2019-10-16 03:10:34 +00:00
|
|
|
|
namespace Wabbajack.Lib
|
2019-07-23 04:17:52 +00:00
|
|
|
|
{
|
|
|
|
|
public class Installer
|
|
|
|
|
{
|
2019-09-03 22:12:39 +00:00
|
|
|
|
private string _downloadsFolder;
|
2019-09-26 20:18:41 +00:00
|
|
|
|
|
2019-10-01 22:39:25 +00:00
|
|
|
|
public Installer(string archive, ModList mod_list, string output_folder)
|
2019-07-23 04:17:52 +00:00
|
|
|
|
{
|
2019-10-01 22:39:25 +00:00
|
|
|
|
ModListArchive = archive;
|
2019-07-23 04:17:52 +00:00
|
|
|
|
Outputfolder = output_folder;
|
|
|
|
|
ModList = mod_list;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-15 13:06:34 +00:00
|
|
|
|
public Context VFS { get; } = new Context();
|
2019-09-14 04:35:42 +00:00
|
|
|
|
|
2019-07-23 04:17:52 +00:00
|
|
|
|
public string Outputfolder { get; }
|
2019-09-14 04:35:42 +00:00
|
|
|
|
|
2019-07-23 04:17:52 +00:00
|
|
|
|
public string DownloadFolder
|
|
|
|
|
{
|
2019-09-03 22:12:39 +00:00
|
|
|
|
get => _downloadsFolder ?? Path.Combine(Outputfolder, "downloads");
|
|
|
|
|
set => _downloadsFolder = value;
|
2019-07-23 04:17:52 +00:00
|
|
|
|
}
|
2019-09-14 04:35:42 +00:00
|
|
|
|
|
2019-10-01 22:39:25 +00:00
|
|
|
|
public string ModListArchive { get; }
|
2019-07-23 04:17:52 +00:00
|
|
|
|
public ModList ModList { get; }
|
|
|
|
|
public Dictionary<string, string> HashedArchives { get; private set; }
|
2019-07-26 20:59:14 +00:00
|
|
|
|
|
2019-08-07 23:06:38 +00:00
|
|
|
|
public bool IgnoreMissingFiles { get; internal set; }
|
2019-09-24 04:20:24 +00:00
|
|
|
|
public string GameFolder { get; set; }
|
2019-07-23 04:17:52 +00:00
|
|
|
|
|
2019-09-26 22:32:15 +00:00
|
|
|
|
public void Info(string msg)
|
2019-07-23 04:17:52 +00:00
|
|
|
|
{
|
2019-09-26 22:32:15 +00:00
|
|
|
|
Utils.Log(msg);
|
2019-07-23 04:17:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-26 22:32:15 +00:00
|
|
|
|
public void Status(string msg)
|
2019-07-23 04:17:52 +00:00
|
|
|
|
{
|
|
|
|
|
WorkQueue.Report(msg, 0);
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-26 22:32:15 +00:00
|
|
|
|
public void Status(string msg, int progress)
|
2019-07-23 04:17:52 +00:00
|
|
|
|
{
|
|
|
|
|
WorkQueue.Report(msg, progress);
|
|
|
|
|
}
|
2019-09-14 04:35:42 +00:00
|
|
|
|
|
2019-09-26 22:32:15 +00:00
|
|
|
|
private void Error(string msg)
|
2019-07-23 04:17:52 +00:00
|
|
|
|
{
|
2019-09-26 22:32:15 +00:00
|
|
|
|
Utils.Log(msg);
|
2019-07-23 04:17:52 +00:00
|
|
|
|
throw new Exception(msg);
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-02 15:38:03 +00:00
|
|
|
|
public byte[] LoadBytesFromPath(string path)
|
2019-10-01 22:39:25 +00:00
|
|
|
|
{
|
|
|
|
|
using (var fs = new FileStream(ModListArchive, FileMode.Open, FileAccess.Read, FileShare.Read))
|
2019-10-07 17:33:34 +00:00
|
|
|
|
using (var ar = new ZipArchive(fs, ZipArchiveMode.Read))
|
2019-10-01 22:39:25 +00:00
|
|
|
|
using (var ms = new MemoryStream())
|
|
|
|
|
{
|
|
|
|
|
var entry = ar.GetEntry(path);
|
|
|
|
|
using (var e = entry.Open())
|
|
|
|
|
e.CopyTo(ms);
|
|
|
|
|
return ms.ToArray();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static ModList LoadFromFile(string path)
|
|
|
|
|
{
|
|
|
|
|
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
|
|
|
|
using (var ar = new ZipArchive(fs, ZipArchiveMode.Read))
|
|
|
|
|
{
|
2019-10-26 19:46:52 +00:00
|
|
|
|
var entry = ar.GetEntry("modlist");
|
2019-10-27 13:59:23 +00:00
|
|
|
|
if (entry == null)
|
|
|
|
|
{
|
|
|
|
|
entry = ar.GetEntry("modlist.json");
|
|
|
|
|
using (var e = entry.Open())
|
|
|
|
|
return e.FromJSON<ModList>();
|
|
|
|
|
}
|
2019-10-01 22:39:25 +00:00
|
|
|
|
using (var e = entry.Open())
|
2019-10-27 12:12:35 +00:00
|
|
|
|
return e.FromCERAS<ModList>(ref CerasConfig.Config);
|
2019-10-01 22:39:25 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-23 04:17:52 +00:00
|
|
|
|
public void Install()
|
|
|
|
|
{
|
2019-11-02 23:20:41 +00:00
|
|
|
|
var game = GameRegistry.Games[ModList.GameType];
|
|
|
|
|
|
|
|
|
|
if (GameFolder == null)
|
|
|
|
|
GameFolder = game.GameLocation;
|
|
|
|
|
|
|
|
|
|
if (GameFolder == null)
|
|
|
|
|
{
|
|
|
|
|
MessageBox.Show(
|
|
|
|
|
$"In order to do a proper install Wabbajack needs to know where your {game.MO2Name} folder resides. We tried looking the" +
|
|
|
|
|
"game location up in the windows registry but were unable to find it, please make sure you launch the game once before running this installer. ",
|
|
|
|
|
"Could not find game location", MessageBoxButton.OK);
|
|
|
|
|
Utils.Log("Exiting because we couldn't find the game folder.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-02 21:08:37 +00:00
|
|
|
|
ValidateGameESMs();
|
2019-09-29 22:21:18 +00:00
|
|
|
|
ValidateModlist.RunValidation(ModList);
|
|
|
|
|
|
2019-07-23 04:17:52 +00:00
|
|
|
|
Directory.CreateDirectory(Outputfolder);
|
|
|
|
|
Directory.CreateDirectory(DownloadFolder);
|
|
|
|
|
|
2019-09-04 21:19:37 +00:00
|
|
|
|
if (Directory.Exists(Path.Combine(Outputfolder, "mods")))
|
|
|
|
|
{
|
2019-09-14 04:35:42 +00:00
|
|
|
|
if (MessageBox.Show(
|
2019-09-24 15:26:44 +00:00
|
|
|
|
"There already appears to be a Mod Organizer 2 install in this folder, are you sure you wish to continue" +
|
2019-09-14 04:35:42 +00:00
|
|
|
|
" with installation? If you do, you may render both your existing install and the new modlist inoperable.",
|
|
|
|
|
"Existing MO2 installation in install folder",
|
|
|
|
|
MessageBoxButton.YesNo,
|
|
|
|
|
MessageBoxImage.Exclamation) == MessageBoxResult.No)
|
2019-09-18 21:33:23 +00:00
|
|
|
|
{
|
2019-09-04 21:19:37 +00:00
|
|
|
|
Utils.Log("Existing installation at the request of the user, existing mods folder found.");
|
2019-09-18 21:33:23 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
2019-09-04 21:19:37 +00:00
|
|
|
|
}
|
2019-09-14 04:35:42 +00:00
|
|
|
|
|
2019-08-24 23:20:54 +00:00
|
|
|
|
|
2019-07-23 04:17:52 +00:00
|
|
|
|
HashArchives();
|
|
|
|
|
DownloadArchives();
|
|
|
|
|
HashArchives();
|
|
|
|
|
|
|
|
|
|
var missing = ModList.Archives.Where(a => !HashedArchives.ContainsKey(a.Hash)).ToList();
|
|
|
|
|
if (missing.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
foreach (var a in missing)
|
2019-09-26 22:32:15 +00:00
|
|
|
|
Info($"Unable to download {a.Name}");
|
2019-08-07 23:06:38 +00:00
|
|
|
|
if (IgnoreMissingFiles)
|
|
|
|
|
Info("Missing some archives, but continuing anyways at the request of the user");
|
|
|
|
|
else
|
|
|
|
|
Error("Cannot continue, was unable to download one or more archives");
|
2019-07-23 04:17:52 +00:00
|
|
|
|
}
|
2019-08-20 04:57:08 +00:00
|
|
|
|
|
2019-11-15 13:37:04 +00:00
|
|
|
|
PrimeVFS().Wait();
|
2019-08-20 04:57:08 +00:00
|
|
|
|
|
2019-07-23 04:17:52 +00:00
|
|
|
|
BuildFolderStructure();
|
|
|
|
|
InstallArchives();
|
2019-07-23 04:27:26 +00:00
|
|
|
|
InstallIncludedFiles();
|
2019-11-04 04:36:25 +00:00
|
|
|
|
InctallIncludedDownloadMetas();
|
2019-07-28 23:04:23 +00:00
|
|
|
|
BuildBSAs();
|
2019-07-23 04:17:52 +00:00
|
|
|
|
|
2019-11-02 15:38:03 +00:00
|
|
|
|
zEditIntegration.GenerateMerges(this);
|
|
|
|
|
|
2019-07-23 04:17:52 +00:00
|
|
|
|
Info("Installation complete! You may exit the program.");
|
2019-09-05 22:18:54 +00:00
|
|
|
|
// Removed until we decide if we want this functionality
|
|
|
|
|
// Nexus devs weren't sure this was a good idea, I (halgari) agree.
|
|
|
|
|
//AskToEndorse();
|
2019-08-26 23:02:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-04 04:36:25 +00:00
|
|
|
|
private void InctallIncludedDownloadMetas()
|
|
|
|
|
{
|
|
|
|
|
ModList.Directives
|
|
|
|
|
.OfType<ArchiveMeta>()
|
|
|
|
|
.PMap(directive =>
|
|
|
|
|
{
|
|
|
|
|
Status($"Writing included .meta file {directive.To}");
|
|
|
|
|
var out_path = Path.Combine(DownloadFolder, directive.To);
|
|
|
|
|
if (File.Exists(out_path)) File.Delete(out_path);
|
|
|
|
|
File.WriteAllBytes(out_path, LoadBytesFromPath(directive.SourceDataID));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-02 21:08:37 +00:00
|
|
|
|
private void ValidateGameESMs()
|
|
|
|
|
{
|
|
|
|
|
foreach (var esm in ModList.Directives.OfType<CleanedESM>().ToList())
|
|
|
|
|
{
|
|
|
|
|
var filename = Path.GetFileName(esm.To);
|
|
|
|
|
var game_file = Path.Combine(GameFolder, "Data", filename);
|
|
|
|
|
Utils.Log($"Validating {filename}");
|
|
|
|
|
var hash = game_file.FileHash();
|
|
|
|
|
if (hash != esm.SourceESMHash)
|
|
|
|
|
{
|
|
|
|
|
Utils.Error("Game ESM hash doesn't match, is the ESM already cleaned? Please verify your local game files.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-26 23:02:49 +00:00
|
|
|
|
private void AskToEndorse()
|
|
|
|
|
{
|
2019-08-27 00:19:23 +00:00
|
|
|
|
var mods = ModList.Archives
|
2019-10-12 22:15:20 +00:00
|
|
|
|
.Select(m => m.State)
|
|
|
|
|
.OfType<NexusDownloader.State>()
|
2019-08-26 23:02:49 +00:00
|
|
|
|
.GroupBy(f => (f.GameName, f.ModID))
|
|
|
|
|
.Select(mod => mod.First())
|
|
|
|
|
.ToArray();
|
|
|
|
|
|
|
|
|
|
var result = MessageBox.Show(
|
2019-08-27 00:19:23 +00:00
|
|
|
|
$"Installation has completed, but you have installed {mods.Length} from the Nexus, would you like to" +
|
2019-08-26 23:02:49 +00:00
|
|
|
|
" endorse these mods to show support to the authors? It will only take a few moments.", "Endorse Mods?",
|
|
|
|
|
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
|
|
|
|
|
|
|
|
|
if (result != MessageBoxResult.Yes) return;
|
|
|
|
|
|
|
|
|
|
// Shuffle mods so that if we hit a API limit we don't always miss the same mods
|
|
|
|
|
var r = new Random();
|
|
|
|
|
for (var i = 0; i < mods.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var a = r.Next(mods.Length);
|
|
|
|
|
var b = r.Next(mods.Length);
|
|
|
|
|
var tmp = mods[a];
|
|
|
|
|
mods[a] = mods[b];
|
|
|
|
|
mods[b] = tmp;
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-27 00:19:23 +00:00
|
|
|
|
mods.PMap(mod =>
|
|
|
|
|
{
|
2019-09-29 00:18:42 +00:00
|
|
|
|
var er = new NexusApiClient().EndorseMod(mod);
|
2019-08-27 00:19:23 +00:00
|
|
|
|
Utils.Log($"Endorsed {mod.GameName} - {mod.ModID} - Result: {er.message}");
|
|
|
|
|
});
|
2019-08-26 23:02:49 +00:00
|
|
|
|
Info("Done! You may now exit the application!");
|
2019-07-23 04:17:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-20 04:57:08 +00:00
|
|
|
|
/// <summary>
|
2019-09-14 04:35:42 +00:00
|
|
|
|
/// We don't want to make the installer index all the archives, that's just a waste of time, so instead
|
|
|
|
|
/// we'll pass just enough information to VFS to let it know about the files we have.
|
2019-08-20 04:57:08 +00:00
|
|
|
|
/// </summary>
|
2019-11-15 13:37:04 +00:00
|
|
|
|
private async Task PrimeVFS()
|
2019-08-20 04:57:08 +00:00
|
|
|
|
{
|
2019-11-15 13:06:34 +00:00
|
|
|
|
VFS.AddKnown(HashedArchives.Select(a => new KnownFile
|
2019-08-20 04:57:08 +00:00
|
|
|
|
{
|
2019-10-07 17:33:34 +00:00
|
|
|
|
Paths = new[] { a.Value },
|
2019-08-20 22:37:55 +00:00
|
|
|
|
Hash = a.Key
|
2019-08-20 04:57:08 +00:00
|
|
|
|
}));
|
|
|
|
|
|
2019-11-15 13:37:04 +00:00
|
|
|
|
|
|
|
|
|
VFS.AddKnown(
|
2019-08-20 04:57:08 +00:00
|
|
|
|
ModList.Directives
|
2019-09-14 04:35:42 +00:00
|
|
|
|
.OfType<FromArchive>()
|
2019-11-15 13:37:04 +00:00
|
|
|
|
.Select(f => new KnownFile { Paths = f.ArchiveHashPath}));
|
2019-08-20 04:57:08 +00:00
|
|
|
|
|
2019-11-15 13:37:04 +00:00
|
|
|
|
await VFS.BackfillMissing();
|
2019-08-20 04:57:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-07-28 23:04:23 +00:00
|
|
|
|
private void BuildBSAs()
|
|
|
|
|
{
|
|
|
|
|
var bsas = ModList.Directives.OfType<CreateBSA>().ToList();
|
2019-07-30 03:32:52 +00:00
|
|
|
|
Info($"Building {bsas.Count} bsa files");
|
2019-07-28 23:04:23 +00:00
|
|
|
|
|
|
|
|
|
bsas.Do(bsa =>
|
|
|
|
|
{
|
|
|
|
|
Status($"Building {bsa.To}");
|
|
|
|
|
var source_dir = Path.Combine(Outputfolder, Consts.BSACreationDir, bsa.TempID);
|
|
|
|
|
|
2019-10-11 23:31:36 +00:00
|
|
|
|
using (var a = bsa.State.MakeBuilder())
|
|
|
|
|
{
|
|
|
|
|
bsa.FileStates.PMap(state =>
|
2019-07-28 23:04:23 +00:00
|
|
|
|
{
|
2019-10-11 23:31:36 +00:00
|
|
|
|
Status($"Adding {state.Path} to BSA");
|
|
|
|
|
using (var fs = File.OpenRead(Path.Combine(source_dir, state.Path)))
|
2019-09-14 04:35:42 +00:00
|
|
|
|
{
|
2019-10-11 23:31:36 +00:00
|
|
|
|
a.AddFile(state, fs);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Info($"Writing {bsa.To}");
|
|
|
|
|
a.Build(Path.Combine(Outputfolder, bsa.To));
|
|
|
|
|
}
|
2019-07-28 23:04:23 +00:00
|
|
|
|
});
|
|
|
|
|
|
2019-09-18 21:33:23 +00:00
|
|
|
|
|
|
|
|
|
var bsa_dir = Path.Combine(Outputfolder, Consts.BSACreationDir);
|
|
|
|
|
if (Directory.Exists(bsa_dir))
|
2019-08-20 22:37:55 +00:00
|
|
|
|
{
|
|
|
|
|
Info($"Removing temp folder {Consts.BSACreationDir}");
|
2019-11-15 13:06:34 +00:00
|
|
|
|
Directory.Delete(bsa_dir, true, true);
|
2019-08-20 22:37:55 +00:00
|
|
|
|
}
|
2019-07-28 23:04:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-07-23 04:27:26 +00:00
|
|
|
|
private void InstallIncludedFiles()
|
|
|
|
|
{
|
|
|
|
|
Info("Writing inline files");
|
|
|
|
|
ModList.Directives
|
2019-09-14 04:35:42 +00:00
|
|
|
|
.OfType<InlineFile>()
|
|
|
|
|
.PMap(directive =>
|
|
|
|
|
{
|
2019-09-26 22:32:15 +00:00
|
|
|
|
Status($"Writing included file {directive.To}");
|
2019-09-14 04:35:42 +00:00
|
|
|
|
var out_path = Path.Combine(Outputfolder, directive.To);
|
|
|
|
|
if (File.Exists(out_path)) File.Delete(out_path);
|
|
|
|
|
if (directive is RemappedInlineFile)
|
2019-10-07 17:33:34 +00:00
|
|
|
|
WriteRemappedFile((RemappedInlineFile)directive);
|
2019-09-14 04:35:42 +00:00
|
|
|
|
else if (directive is CleanedESM)
|
2019-10-07 17:33:34 +00:00
|
|
|
|
GenerateCleanedESM((CleanedESM)directive);
|
2019-09-14 04:35:42 +00:00
|
|
|
|
else
|
2019-10-01 22:39:25 +00:00
|
|
|
|
File.WriteAllBytes(out_path, LoadBytesFromPath(directive.SourceDataID));
|
2019-09-14 04:35:42 +00:00
|
|
|
|
});
|
2019-07-23 04:27:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-25 03:46:32 +00:00
|
|
|
|
private void GenerateCleanedESM(CleanedESM directive)
|
|
|
|
|
{
|
|
|
|
|
var filename = Path.GetFileName(directive.To);
|
|
|
|
|
var game_file = Path.Combine(GameFolder, "Data", filename);
|
|
|
|
|
Info($"Generating cleaned ESM for {filename}");
|
2019-09-14 04:35:42 +00:00
|
|
|
|
if (!File.Exists(game_file)) throw new InvalidDataException($"Missing {filename} at {game_file}");
|
2019-08-25 03:46:32 +00:00
|
|
|
|
Status($"Hashing game version of {filename}");
|
2019-10-31 03:40:33 +00:00
|
|
|
|
var sha = game_file.FileHash();
|
2019-08-25 03:46:32 +00:00
|
|
|
|
if (sha != directive.SourceESMHash)
|
2019-09-14 04:35:42 +00:00
|
|
|
|
throw new InvalidDataException(
|
|
|
|
|
$"Cannot patch {filename} from the game folder hashes don't match have you already cleaned the file?");
|
2019-08-25 03:46:32 +00:00
|
|
|
|
|
2019-10-01 22:39:25 +00:00
|
|
|
|
var patch_data = LoadBytesFromPath(directive.SourceDataID);
|
2019-08-25 03:46:32 +00:00
|
|
|
|
var to_file = Path.Combine(Outputfolder, directive.To);
|
|
|
|
|
Status($"Patching {filename}");
|
2019-09-14 04:35:42 +00:00
|
|
|
|
using (var output = File.OpenWrite(to_file))
|
2019-11-02 21:08:37 +00:00
|
|
|
|
using (var input = File.OpenRead(game_file))
|
2019-09-14 04:35:42 +00:00
|
|
|
|
{
|
2019-11-02 21:08:37 +00:00
|
|
|
|
BSDiff.Apply(input, () => new MemoryStream(patch_data), output);
|
2019-08-25 03:46:32 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-24 23:20:54 +00:00
|
|
|
|
private void WriteRemappedFile(RemappedInlineFile directive)
|
|
|
|
|
{
|
2019-10-01 22:39:25 +00:00
|
|
|
|
var data = Encoding.UTF8.GetString(LoadBytesFromPath(directive.SourceDataID));
|
2019-08-24 23:20:54 +00:00
|
|
|
|
|
|
|
|
|
data = data.Replace(Consts.GAME_PATH_MAGIC_BACK, GameFolder);
|
|
|
|
|
data = data.Replace(Consts.GAME_PATH_MAGIC_DOUBLE_BACK, GameFolder.Replace("\\", "\\\\"));
|
|
|
|
|
data = data.Replace(Consts.GAME_PATH_MAGIC_FORWARD, GameFolder.Replace("\\", "/"));
|
|
|
|
|
|
|
|
|
|
data = data.Replace(Consts.MO2_PATH_MAGIC_BACK, Outputfolder);
|
|
|
|
|
data = data.Replace(Consts.MO2_PATH_MAGIC_DOUBLE_BACK, Outputfolder.Replace("\\", "\\\\"));
|
|
|
|
|
data = data.Replace(Consts.MO2_PATH_MAGIC_FORWARD, Outputfolder.Replace("\\", "/"));
|
|
|
|
|
|
2019-09-03 22:12:39 +00:00
|
|
|
|
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_BACK, DownloadFolder);
|
|
|
|
|
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_DOUBLE_BACK, DownloadFolder.Replace("\\", "\\\\"));
|
|
|
|
|
data = data.Replace(Consts.DOWNLOAD_PATH_MAGIC_FORWARD, DownloadFolder.Replace("\\", "/"));
|
|
|
|
|
|
2019-08-24 23:20:54 +00:00
|
|
|
|
File.WriteAllText(Path.Combine(Outputfolder, directive.To), data);
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-23 04:17:52 +00:00
|
|
|
|
private void BuildFolderStructure()
|
|
|
|
|
{
|
|
|
|
|
Info("Building Folder Structure");
|
|
|
|
|
ModList.Directives
|
2019-09-14 04:35:42 +00:00
|
|
|
|
.Select(d => Path.Combine(Outputfolder, Path.GetDirectoryName(d.To)))
|
|
|
|
|
.ToHashSet()
|
|
|
|
|
.Do(f =>
|
|
|
|
|
{
|
|
|
|
|
if (Directory.Exists(f)) return;
|
|
|
|
|
Directory.CreateDirectory(f);
|
|
|
|
|
});
|
2019-07-23 04:17:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void InstallArchives()
|
|
|
|
|
{
|
|
|
|
|
Info("Installing Archives");
|
2019-08-07 23:06:38 +00:00
|
|
|
|
Info("Grouping Install Files");
|
2019-07-23 04:17:52 +00:00
|
|
|
|
var grouped = ModList.Directives
|
2019-09-14 04:35:42 +00:00
|
|
|
|
.OfType<FromArchive>()
|
|
|
|
|
.GroupBy(e => e.ArchiveHashPath[0])
|
|
|
|
|
.ToDictionary(k => k.Key);
|
2019-07-23 04:17:52 +00:00
|
|
|
|
var archives = ModList.Archives
|
2019-10-07 17:33:34 +00:00
|
|
|
|
.Select(a => new { Archive = a, AbsolutePath = HashedArchives.GetOrDefault(a.Hash) })
|
2019-09-14 04:35:42 +00:00
|
|
|
|
.Where(a => a.AbsolutePath != null)
|
|
|
|
|
.ToList();
|
2019-07-23 04:17:52 +00:00
|
|
|
|
|
2019-08-07 23:06:38 +00:00
|
|
|
|
Info("Installing Archives");
|
2019-07-23 04:17:52 +00:00
|
|
|
|
archives.PMap(a => InstallArchive(a.Archive, a.AbsolutePath, grouped[a.Archive.Hash]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void InstallArchive(Archive archive, string absolutePath, IGrouping<string, FromArchive> grouping)
|
|
|
|
|
{
|
2019-08-22 22:05:16 +00:00
|
|
|
|
Status($"Extracting {archive.Name}");
|
2019-07-23 04:17:52 +00:00
|
|
|
|
|
2019-08-26 04:32:05 +00:00
|
|
|
|
var vfiles = grouping.Select(g =>
|
2019-07-23 04:17:52 +00:00
|
|
|
|
{
|
2019-11-15 13:06:34 +00:00
|
|
|
|
var file = VFS.Index.FileForArchiveHashPath(g.ArchiveHashPath);
|
2019-08-26 04:32:05 +00:00
|
|
|
|
g.FromFile = file;
|
|
|
|
|
return g;
|
2019-08-20 04:57:08 +00:00
|
|
|
|
}).ToList();
|
|
|
|
|
|
2019-11-15 13:06:34 +00:00
|
|
|
|
var on_finish = VFS.Stage(vfiles.Select(f => f.FromFile).Distinct()).Result;
|
2019-08-20 04:57:08 +00:00
|
|
|
|
|
2019-07-23 04:17:52 +00:00
|
|
|
|
|
2019-09-26 22:32:15 +00:00
|
|
|
|
Status($"Copying files for {archive.Name}");
|
2019-07-23 04:17:52 +00:00
|
|
|
|
|
2019-11-07 04:43:30 +00:00
|
|
|
|
void CopyFile(string from, string to, bool use_move)
|
|
|
|
|
{
|
|
|
|
|
if (File.Exists(to))
|
2019-11-10 23:03:10 +00:00
|
|
|
|
{
|
|
|
|
|
var fi = new FileInfo(to);
|
|
|
|
|
if (fi.IsReadOnly)
|
|
|
|
|
fi.IsReadOnly = false;
|
2019-11-07 04:43:30 +00:00
|
|
|
|
File.Delete(to);
|
2019-11-10 23:03:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (File.Exists(from))
|
|
|
|
|
{
|
|
|
|
|
var fi = new FileInfo(from);
|
|
|
|
|
if (fi.IsReadOnly)
|
|
|
|
|
fi.IsReadOnly = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2019-11-07 04:43:30 +00:00
|
|
|
|
if (use_move)
|
|
|
|
|
File.Move(from, to);
|
|
|
|
|
else
|
|
|
|
|
File.Copy(from, to);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vfiles.GroupBy(f => f.FromFile)
|
|
|
|
|
.DoIndexed((idx, group) =>
|
2019-07-23 04:17:52 +00:00
|
|
|
|
{
|
2019-09-14 04:35:42 +00:00
|
|
|
|
Utils.Status("Installing files", idx * 100 / vfiles.Count);
|
2019-11-07 04:43:30 +00:00
|
|
|
|
var first_dest = Path.Combine(Outputfolder, group.First().To);
|
|
|
|
|
CopyFile(group.Key.StagedPath, first_dest, true);
|
|
|
|
|
|
|
|
|
|
foreach (var copy in group.Skip(1))
|
|
|
|
|
{
|
|
|
|
|
var next_dest = Path.Combine(Outputfolder, copy.To);
|
|
|
|
|
CopyFile(first_dest, next_dest, false);
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-26 04:32:05 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Status("Unstaging files");
|
|
|
|
|
on_finish();
|
2019-07-23 04:17:52 +00:00
|
|
|
|
|
|
|
|
|
// Now patch all the files from this archive
|
|
|
|
|
foreach (var to_patch in grouping.OfType<PatchedFromArchive>())
|
|
|
|
|
using (var patch_stream = new MemoryStream())
|
|
|
|
|
{
|
2019-09-26 22:32:15 +00:00
|
|
|
|
Status($"Patching {Path.GetFileName(to_patch.To)}");
|
2019-07-23 04:17:52 +00:00
|
|
|
|
// Read in the patch data
|
|
|
|
|
|
2019-10-01 22:39:25 +00:00
|
|
|
|
var patch_data = LoadBytesFromPath(to_patch.PatchID);
|
2019-07-23 04:17:52 +00:00
|
|
|
|
|
|
|
|
|
var to_file = Path.Combine(Outputfolder, to_patch.To);
|
2019-09-14 04:35:42 +00:00
|
|
|
|
var old_data = new MemoryStream(File.ReadAllBytes(to_file));
|
2019-07-23 04:17:52 +00:00
|
|
|
|
|
2019-08-03 13:02:12 +00:00
|
|
|
|
// Remove the file we're about to patch
|
|
|
|
|
File.Delete(to_file);
|
|
|
|
|
|
2019-07-23 04:17:52 +00:00
|
|
|
|
// Patch it
|
|
|
|
|
using (var out_stream = File.OpenWrite(to_file))
|
|
|
|
|
{
|
|
|
|
|
BSDiff.Apply(old_data, () => new MemoryStream(patch_data), out_stream);
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-14 03:44:07 +00:00
|
|
|
|
Status($"Verifying Patch {Path.GetFileName(to_patch.To)}");
|
2019-10-31 03:40:33 +00:00
|
|
|
|
var result_sha = to_file.FileHash();
|
2019-09-14 03:44:07 +00:00
|
|
|
|
if (result_sha != to_patch.Hash)
|
|
|
|
|
throw new InvalidDataException($"Invalid Hash for {to_patch.To} after patching");
|
2019-07-23 04:17:52 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DownloadArchives()
|
|
|
|
|
{
|
|
|
|
|
var missing = ModList.Archives.Where(a => !HashedArchives.ContainsKey(a.Hash)).ToList();
|
2019-09-26 22:32:15 +00:00
|
|
|
|
Info($"Missing {missing.Count} archives");
|
2019-07-23 04:17:52 +00:00
|
|
|
|
|
|
|
|
|
Info("Getting Nexus API Key, if a browser appears, please accept");
|
2019-08-04 22:08:03 +00:00
|
|
|
|
|
2019-11-06 23:59:18 +00:00
|
|
|
|
var dispatchers = missing.Select(m => m.State.GetDownloader()).Distinct();
|
2019-10-12 22:15:20 +00:00
|
|
|
|
|
|
|
|
|
foreach (var dispatcher in dispatchers)
|
|
|
|
|
dispatcher.Prepare();
|
2019-11-06 23:59:18 +00:00
|
|
|
|
|
2019-07-23 04:17:52 +00:00
|
|
|
|
DownloadMissingArchives(missing);
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-14 04:35:42 +00:00
|
|
|
|
private void DownloadMissingArchives(List<Archive> missing, bool download = true)
|
2019-07-23 04:17:52 +00:00
|
|
|
|
{
|
2019-11-06 23:59:18 +00:00
|
|
|
|
if (download)
|
|
|
|
|
{
|
|
|
|
|
foreach (var a in missing.Where(a => a.State.GetType() == typeof(ManualDownloader.State)))
|
|
|
|
|
{
|
|
|
|
|
var output_path = Path.Combine(DownloadFolder, a.Name);
|
|
|
|
|
a.State.Download(a, output_path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
missing.Where(a => a.State.GetType() != typeof(ManualDownloader.State))
|
|
|
|
|
.PMap(archive =>
|
2019-07-23 04:17:52 +00:00
|
|
|
|
{
|
2019-08-04 22:08:03 +00:00
|
|
|
|
Info($"Downloading {archive.Name}");
|
|
|
|
|
var output_path = Path.Combine(DownloadFolder, archive.Name);
|
|
|
|
|
|
2019-08-30 04:24:31 +00:00
|
|
|
|
if (download)
|
|
|
|
|
if (output_path.FileExists())
|
|
|
|
|
File.Delete(output_path);
|
2019-08-04 22:08:03 +00:00
|
|
|
|
|
2019-08-30 04:24:31 +00:00
|
|
|
|
return DownloadArchive(archive, download);
|
|
|
|
|
});
|
|
|
|
|
}
|
2019-08-11 22:57:32 +00:00
|
|
|
|
|
2019-08-30 04:24:31 +00:00
|
|
|
|
public bool DownloadArchive(Archive archive, bool download)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2019-10-12 22:15:20 +00:00
|
|
|
|
archive.State.Download(archive, Path.Combine(DownloadFolder, archive.Name));
|
2019-07-26 20:59:14 +00:00
|
|
|
|
}
|
2019-08-30 04:24:31 +00:00
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Utils.Log($"Download error for file {archive.Name}");
|
|
|
|
|
Utils.Log(ex.ToString());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2019-09-14 04:35:42 +00:00
|
|
|
|
|
2019-08-30 04:24:31 +00:00
|
|
|
|
return false;
|
2019-07-23 04:17:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void HashArchives()
|
|
|
|
|
{
|
|
|
|
|
HashedArchives = Directory.EnumerateFiles(DownloadFolder)
|
2019-09-14 04:35:42 +00:00
|
|
|
|
.Where(e => !e.EndsWith(".sha"))
|
|
|
|
|
.PMap(e => (HashArchive(e), e))
|
|
|
|
|
.OrderByDescending(e => File.GetLastWriteTime(e.Item2))
|
|
|
|
|
.GroupBy(e => e.Item1)
|
|
|
|
|
.Select(e => e.First())
|
|
|
|
|
.ToDictionary(e => e.Item1, e => e.Item2);
|
2019-07-23 04:17:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string HashArchive(string e)
|
|
|
|
|
{
|
|
|
|
|
var cache = e + ".sha";
|
|
|
|
|
if (cache.FileExists() && new FileInfo(cache).LastWriteTime >= new FileInfo(e).LastWriteTime)
|
|
|
|
|
return File.ReadAllText(cache);
|
|
|
|
|
|
2019-09-26 22:32:15 +00:00
|
|
|
|
Status($"Hashing {Path.GetFileName(e)}");
|
2019-10-31 03:40:33 +00:00
|
|
|
|
File.WriteAllText(cache, e.FileHash());
|
2019-07-23 04:17:52 +00:00
|
|
|
|
return HashArchive(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-09-24 15:26:44 +00:00
|
|
|
|
}
|