diff --git a/Wabbajack.Lib/AInstaller.cs b/Wabbajack.Lib/AInstaller.cs index c2833ea1..6c8d7512 100644 --- a/Wabbajack.Lib/AInstaller.cs +++ b/Wabbajack.Lib/AInstaller.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; +using System.Windows.Navigation; using Wabbajack.Common; using Wabbajack.Lib.Downloaders; using Wabbajack.VirtualFileSystem; @@ -160,6 +161,9 @@ namespace Wabbajack.Lib File.Move(from, to); else File.Copy(from, to); + // If we don't do this, the file will use the last-modified date of the file when it was compressed + // into an archive, which isn't really what we want in the case of files installed archives + File.SetLastWriteTime(to, DateTime.Now); } vFiles.GroupBy(f => f.FromFile) @@ -285,5 +289,40 @@ namespace Wabbajack.Lib File.WriteAllText(cache, e.FileHash()); return HashArchive(e); } + + /// + /// The user may already have some files in the OutputFolder. If so we can go through these and + /// figure out which need to be updated, deleted, or left alone + /// + public void OptimizeModlist() + { + Utils.Log("Optimizing Modlist directives"); + var indexed = ModList.Directives.ToDictionary(d => d.To); + + indexed.Values.PMap(Queue, d => + { + // Bit backwards, but we want to return null for + // all files we *want* installed. We return the files + // to remove from the install list. + var path = Path.Combine(OutputFolder, d.To); + if (!File.Exists(path)) return null; + + var fi = new FileInfo(path); + if (fi.Length != d.Size) return null; + + return path.FileHash() == d.Hash ? d : null; + }).Where(d => d != null) + .Do(d => indexed.Remove(d.To)); + + Utils.Log($"Optimized {ModList.Directives.Count} directives to {indexed.Count} required"); + var requiredArchives = indexed.Values.OfType() + .GroupBy(d => d.ArchiveHashPath[0]) + .Select(d => d.Key) + .ToHashSet(); + + ModList.Archives = ModList.Archives.Where(a => requiredArchives.Contains(a.Hash)).ToList(); + ModList.Directives = indexed.Values.ToList(); + + } } } diff --git a/Wabbajack.Lib/MO2Compiler.cs b/Wabbajack.Lib/MO2Compiler.cs index 9aa041d1..b13e67da 100644 --- a/Wabbajack.Lib/MO2Compiler.cs +++ b/Wabbajack.Lib/MO2Compiler.cs @@ -305,7 +305,7 @@ namespace Wabbajack.Lib private void BuildArchivePatches(string archive_sha, IEnumerable group, Dictionary absolute_paths) { - using (var files = VFS.StageWith(group.Select(g => VFS.Index.FileForArchiveHashPath(g.ArchiveHashPath))).Result) + using (var files = VFS.StageWith(group.Select(g => VFS.Index.FileForArchiveHashPath(g.ArchiveHashPath)))) { var by_path = files.GroupBy(f => string.Join("|", f.FilesInFullPath.Skip(1).Select(i => i.Name))) .ToDictionary(f => f.Key, f => f.First()); diff --git a/Wabbajack.Lib/MO2Installer.cs b/Wabbajack.Lib/MO2Installer.cs index 0620cfb4..3658ba1a 100644 --- a/Wabbajack.Lib/MO2Installer.cs +++ b/Wabbajack.Lib/MO2Installer.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Text; using System.Windows; +using System.Windows.Forms.VisualStyles; using Wabbajack.Common; using Wabbajack.Lib.Downloaders; using Wabbajack.Lib.NexusApi; @@ -17,6 +18,8 @@ namespace Wabbajack.Lib { public class MO2Installer : AInstaller { + public bool WarnOnOverwrite { get; set; } = true; + public MO2Installer(string archive, ModList mod_list, string output_folder) { ModManager = ModManager.MO2; @@ -52,7 +55,7 @@ namespace Wabbajack.Lib Directory.CreateDirectory(OutputFolder); Directory.CreateDirectory(DownloadFolder); - if (Directory.Exists(Path.Combine(OutputFolder, "mods"))) + if (Directory.Exists(Path.Combine(OutputFolder, "mods")) && WarnOnOverwrite) { if (MessageBox.Show( "There already appears to be a Mod Organizer 2 install in this folder, are you sure you wish to continue" + @@ -66,6 +69,7 @@ namespace Wabbajack.Lib } } + OptimizeModlist(); HashArchives(); DownloadArchives(); @@ -99,6 +103,7 @@ namespace Wabbajack.Lib return true; } + private void InstallIncludedDownloadMetas() { ModList.Directives diff --git a/Wabbajack.Test/ACompilerTest.cs b/Wabbajack.Test/ACompilerTest.cs index 4197ad33..fd86133e 100644 --- a/Wabbajack.Test/ACompilerTest.cs +++ b/Wabbajack.Test/ACompilerTest.cs @@ -57,6 +57,7 @@ namespace Wabbajack.Test { var modlist = MO2Installer.LoadFromFile(compiler.ModListOutputFile); var installer = new MO2Installer(compiler.ModListOutputFile, modlist, utils.InstallFolder); + installer.WarnOnOverwrite = false; installer.DownloadFolder = utils.DownloadsFolder; installer.GameFolder = utils.GameFolder; installer.Begin().Wait(); diff --git a/Wabbajack.Test/SanityTests.cs b/Wabbajack.Test/SanityTests.cs index fc49e42a..22f2485a 100644 --- a/Wabbajack.Test/SanityTests.cs +++ b/Wabbajack.Test/SanityTests.cs @@ -55,6 +55,55 @@ namespace Wabbajack.Test utils.VerifyInstalledFile(mod, @"Data\scripts\test.pex.copy"); } + [TestMethod] + public void TestUpdating() + { + + var profile = utils.AddProfile(); + var mod = utils.AddMod(); + var unchanged = utils.AddModFile(mod, @"Data\scripts\unchanged.pex", 10); + var deleted = utils.AddModFile(mod, @"Data\scripts\deleted.pex", 10); + var modified = utils.AddModFile(mod, @"Data\scripts\modified.pex", 10); + + utils.Configure(); + + utils.AddManualDownload( + new Dictionary + { + { "/baz/unchanged.pex", File.ReadAllBytes(unchanged) }, + { "/baz/deleted.pex", File.ReadAllBytes(deleted) }, + { "/baz/modified.pex", File.ReadAllBytes(modified) }, + }); + + CompileAndInstall(profile); + + utils.VerifyInstalledFile(mod, @"Data\scripts\unchanged.pex"); + utils.VerifyInstalledFile(mod, @"Data\scripts\deleted.pex"); + utils.VerifyInstalledFile(mod, @"Data\scripts\modified.pex"); + + var unchanged_path = utils.PathOfInstalledFile(mod, @"Data\scripts\unchanged.pex"); + var deleted_path = utils.PathOfInstalledFile(mod, @"Data\scripts\deleted.pex"); + var modified_path = utils.PathOfInstalledFile(mod, @"Data\scripts\modified.pex"); + + + var unchanged_modified = File.GetLastWriteTime(unchanged_path); + var modified_modified = File.GetLastWriteTime(modified_path); + + File.WriteAllText(modified_path, "random data"); + File.Delete(deleted_path); + + CompileAndInstall(profile); + + utils.VerifyInstalledFile(mod, @"Data\scripts\unchanged.pex"); + utils.VerifyInstalledFile(mod, @"Data\scripts\deleted.pex"); + utils.VerifyInstalledFile(mod, @"Data\scripts\modified.pex"); + + Assert.AreEqual(unchanged_modified, File.GetLastWriteTime(unchanged_path)); + Assert.AreNotEqual(modified_modified, File.GetLastWriteTime(modified_path)); + + + } + [TestMethod] public void CleanedESMTest() diff --git a/Wabbajack.Test/TestUtils.cs b/Wabbajack.Test/TestUtils.cs index abb10136..f39e95d3 100644 --- a/Wabbajack.Test/TestUtils.cs +++ b/Wabbajack.Test/TestUtils.cs @@ -182,6 +182,11 @@ namespace Wabbajack.Test } } + public string PathOfInstalledFile(string mod, string file) + { + return Path.Combine(InstallFolder, "mods", mod, file); + } + public void VerifyAllFiles() { foreach (var dest_file in Directory.EnumerateFiles(InstallFolder, "*", DirectoryEnumerationOptions.Recursive)) diff --git a/Wabbajack.VirtualFileSystem/Context.cs b/Wabbajack.VirtualFileSystem/Context.cs index 0eba5c69..2137ce7d 100644 --- a/Wabbajack.VirtualFileSystem/Context.cs +++ b/Wabbajack.VirtualFileSystem/Context.cs @@ -139,7 +139,7 @@ namespace Wabbajack.VirtualFileSystem { var magic = Encoding.ASCII.GetString(br.ReadBytes(Encoding.ASCII.GetBytes(Magic).Length)); var fileVersion = br.ReadUInt64(); - if (fileVersion != FileVersion || magic != magic) + if (fileVersion != FileVersion || magic != Magic) throw new InvalidDataException("Bad Data Format"); var numFiles = br.ReadUInt64(); @@ -223,7 +223,7 @@ namespace Wabbajack.VirtualFileSystem } } - public async Task> StageWith(IEnumerable files) + public DisposableList StageWith(IEnumerable files) { return new DisposableList(Stage(files), files); }