diff --git a/Branding/PNGs/Wabba_Mouth_Small.png b/Branding/PNGs/Wabba_Mouth_Small.png new file mode 100644 index 00000000..88c5c6d0 Binary files /dev/null and b/Branding/PNGs/Wabba_Mouth_Small.png differ diff --git a/Compression.BSA.Test/BSATests.cs b/Compression.BSA.Test/BSATests.cs index 8e2d2916..8f383e5d 100644 --- a/Compression.BSA.Test/BSATests.cs +++ b/Compression.BSA.Test/BSATests.cs @@ -73,7 +73,7 @@ namespace Compression.BSA.Test var state = new NexusDownloader.State { ModID = info.Item2.ToString(), - GameName = GameRegistry.Games[info.Item1].NexusName, + GameName = info.Item1.MetaData().NexusName, FileID = file.file_id.ToString() }; state.Download(src); diff --git a/Wabbajack.Common/BSDiff.cs b/Wabbajack.Common/BSDiff.cs index d0352127..e17f9184 100644 --- a/Wabbajack.Common/BSDiff.cs +++ b/Wabbajack.Common/BSDiff.cs @@ -677,4 +677,4 @@ namespace Wabbajack.Common return a < b ? b < c ? b : a < c ? c : a : b > c ? b : a > c ? c : a; } } -} \ No newline at end of file +} diff --git a/Wabbajack.Common/GameMetaData.cs b/Wabbajack.Common/GameMetaData.cs index 11f24dcb..1a315d99 100644 --- a/Wabbajack.Common/GameMetaData.cs +++ b/Wabbajack.Common/GameMetaData.cs @@ -67,12 +67,13 @@ namespace Wabbajack.Common public List RequiredFiles { get; internal set; } public bool Disabled { get; internal set; } - public string GameLocation(bool steam) + public string GameLocation() { if (Consts.TestMode) return Directory.GetCurrentDirectory(); - return steam ? SteamHandler.Instance.Games.FirstOrDefault(g => g.Game == Game)?.InstallDir : GOGHandler.Instance.Games.FirstOrDefault(g => g.Game == Game)?.Path; + return SteamHandler.Instance.Games.FirstOrDefault(g => g.Game == Game)?.InstallDir ?? + GOGHandler.Instance.Games.FirstOrDefault(g => g.Game == Game)?.Path; } } diff --git a/Wabbajack.Common/SteamHandler.cs b/Wabbajack.Common/SteamHandler.cs index ae187bfb..35934868 100644 --- a/Wabbajack.Common/SteamHandler.cs +++ b/Wabbajack.Common/SteamHandler.cs @@ -95,6 +95,9 @@ namespace Wabbajack.Common paths.Add(s); }); + // Default path in the Steam folder isn't in the configs + paths.Add(Path.Combine(SteamPath, "steamapps")); + InstallFolders = paths; } diff --git a/Wabbajack.Common/Utils.cs b/Wabbajack.Common/Utils.cs index b9a5a2d1..4a227f8c 100644 --- a/Wabbajack.Common/Utils.cs +++ b/Wabbajack.Common/Utils.cs @@ -665,6 +665,7 @@ namespace Wabbajack.Common using (var f = File.OpenWrite(tmpName)) { + Status("Creating Patch"); BSDiff.Create(a, b, f); } diff --git a/Wabbajack.Lib/CerasConfig.cs b/Wabbajack.Lib/CerasConfig.cs index 1b08961f..290bc843 100644 --- a/Wabbajack.Lib/CerasConfig.cs +++ b/Wabbajack.Lib/CerasConfig.cs @@ -24,7 +24,7 @@ namespace Wabbajack.Lib typeof(BSAStateObject), typeof(BSAFileStateObject), typeof(BA2StateObject), typeof(BA2DX10EntryState), typeof(BA2FileEntryState), typeof(MediaFireDownloader.State), typeof(ArchiveMeta), typeof(PropertyFile), typeof(SteamMeta), typeof(SteamWorkshopDownloader), typeof(SteamWorkshopDownloader.State), - typeof(LoversLabDownloader.State) + typeof(LoversLabDownloader.State), typeof(GameFileSourceDownloader.State) } }; diff --git a/Wabbajack.Lib/Downloaders/DownloadDispatcher.cs b/Wabbajack.Lib/Downloaders/DownloadDispatcher.cs index c1e7c229..e7a016ea 100644 --- a/Wabbajack.Lib/Downloaders/DownloadDispatcher.cs +++ b/Wabbajack.Lib/Downloaders/DownloadDispatcher.cs @@ -9,6 +9,7 @@ namespace Wabbajack.Lib.Downloaders { public static readonly List Downloaders = new List() { + new GameFileSourceDownloader(), new MegaDownloader(), new DropboxDownloader(), new GoogleDriveDownloader(), diff --git a/Wabbajack.Lib/Downloaders/GameFileSourceDownloader.cs b/Wabbajack.Lib/Downloaders/GameFileSourceDownloader.cs new file mode 100644 index 00000000..430371dc --- /dev/null +++ b/Wabbajack.Lib/Downloaders/GameFileSourceDownloader.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Alphaleonis.Win32.Filesystem; +using Wabbajack.Common; +using Wabbajack.Lib.Validation; +using File = Alphaleonis.Win32.Filesystem.File; +using Game = Wabbajack.Common.Game; + +namespace Wabbajack.Lib.Downloaders +{ + public class GameFileSourceDownloader : IDownloader + { + public AbstractDownloadState GetDownloaderState(dynamic archiveINI) + { + var gameName = (string)archiveINI?.General?.gameName; + var gameFile = (string)archiveINI?.General?.gameFile; + + if (gameFile == null || gameFile == null) + return null; + + var game = GameRegistry.GetByMO2ArchiveName(gameName); + if (game == null) return null; + + var path = game.GameLocation(); + var filePath = Path.Combine(path, gameFile); + + if (!File.Exists(filePath)) + return null; + + var hash = filePath.FileHashCached(); + + return new State + { + Game = GameRegistry.GetByMO2ArchiveName(gameName).Game, + GameFile = gameFile, + Hash = hash, + }; + } + + public void Prepare() + { + } + + public class State : AbstractDownloadState + { + public Game Game { get; set; } + public string GameFile { get; set; } + public string Hash { get; set; } + + internal string SourcePath => Path.Combine(Game.MetaData().GameLocation(), GameFile); + + public override bool IsWhitelisted(ServerWhitelist whitelist) + { + return true; + } + + public override void Download(Archive a, string destination) + { + using(var src = File.OpenRead(SourcePath)) + using (var dest = File.OpenWrite(destination)) + { + var size = new FileInfo(SourcePath).Length; + src.CopyToWithStatus(size, dest, "Copying from Game folder"); + } + } + + public override bool Verify() + { + return File.Exists(SourcePath) && SourcePath.FileHashCached() == Hash; + } + + public override IDownloader GetDownloader() + { + return DownloadDispatcher.GetInstance(); + } + + public override string GetReportEntry(Archive a) + { + return $"* Game File {Game} - {GameFile}"; + } + } + } +} diff --git a/Wabbajack.Lib/MO2Compiler.cs b/Wabbajack.Lib/MO2Compiler.cs index 952c2e25..a200c24d 100644 --- a/Wabbajack.Lib/MO2Compiler.cs +++ b/Wabbajack.Lib/MO2Compiler.cs @@ -5,12 +5,15 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Alphaleonis.Win32.Filesystem; using Wabbajack.Common; using Wabbajack.Lib.CompilationSteps; using Wabbajack.Lib.NexusApi; using Wabbajack.Lib.Validation; using Directory = Alphaleonis.Win32.Filesystem.Directory; using File = Alphaleonis.Win32.Filesystem.File; +using FileInfo = Alphaleonis.Win32.Filesystem.FileInfo; +using Game = Wabbajack.Common.Game; using Path = Alphaleonis.Win32.Filesystem.Path; namespace Wabbajack.Lib @@ -29,6 +32,8 @@ namespace Wabbajack.Lib public override string GamePath { get; } + public GameMetaData CompilingGame { get; set; } + public override string ModListOutputFolder => "output_folder"; public override string ModListOutputFile { get; } @@ -38,6 +43,8 @@ namespace Wabbajack.Lib MO2Folder = mo2Folder; MO2Profile = mo2Profile; MO2Ini = Path.Combine(MO2Folder, "ModOrganizer.ini").LoadIniFile(); + var mo2game = (string)MO2Ini.General.gameName; + CompilingGame = GameRegistry.Games.First(g => g.Value.MO2Name == mo2game).Value; GamePath = ((string)MO2Ini.General.gamePath).Replace("\\\\", "\\"); ModListOutputFile = outputFile; } @@ -72,7 +79,7 @@ namespace Wabbajack.Lib protected override bool _Begin() { - ConfigureProcessor(16); + ConfigureProcessor(18); UpdateTracker.Reset(); UpdateTracker.NextStep("Gathering information"); Info("Looking for other profiles"); @@ -116,6 +123,13 @@ namespace Wabbajack.Lib if (Directory.Exists(ModListOutputFolder)) Utils.DeleteDirectory(ModListOutputFolder); + UpdateTracker.NextStep("Inferring metas for game file downloads"); + InferMetas(); + + UpdateTracker.NextStep("Reindexing downloads after meta inferring"); + VFS.AddRoot(MO2DownloadsFolder); + VFS.WriteToFile(_vfsCacheName); + UpdateTracker.NextStep("Finding Install Files"); Directory.CreateDirectory(ModListOutputFolder); @@ -244,7 +258,7 @@ namespace Wabbajack.Lib ModList = new ModList { - GameType = GameRegistry.Games.Values.First(f => f.MO2Name == MO2Ini.General.gameName).Game, + GameType = CompilingGame.Game, WabbajackVersion = WabbajackVersion, Archives = SelectedArchives.ToList(), ModManager = ModManager.MO2, @@ -275,6 +289,41 @@ namespace Wabbajack.Lib return true; } + private void InferMetas() + { + var to_find = Directory.EnumerateFiles(MO2DownloadsFolder) + .Where(f => !f.EndsWith(".meta") && !f.EndsWith(Consts.HashFileExtension)) + .Where(f => !File.Exists(f + ".meta")) + .ToList(); + + if (to_find.Count == 0) return; + + var games = new[]{CompilingGame}.Concat(GameRegistry.Games.Values.Where(g => g != CompilingGame)); + var game_files = games + .Where(g => g.GameLocation() != null) + .SelectMany(game => Directory.EnumerateFiles(game.GameLocation(), "*", DirectoryEnumerationOptions.Recursive).Select(name => (game, name))) + .GroupBy(f => (Path.GetFileName(f.name), new FileInfo(f.name).Length)) + .ToDictionary(f => f.Key); + + to_find.PMap(Queue, f => + { + var vf = VFS.Index.ByFullPath[f]; + if (!game_files.TryGetValue((Path.GetFileName(f), vf.Size), out var found)) + return; + + var (game, name) = found.FirstOrDefault(ff => ff.name.FileHash() == vf.Hash); + if (name == null) + return; + + File.WriteAllLines(f+".meta", new[] + { + "[General]", + $"gameName={game.MO2ArchiveName}", + $"gameFile={name.RelativeTo(game.GameLocation()).Replace("\\", "/")}" + }); + }); + + } private void IncludeArchiveMetadata() diff --git a/Wabbajack.Lib/MO2Installer.cs b/Wabbajack.Lib/MO2Installer.cs index 0002e2f9..c192e3e6 100644 --- a/Wabbajack.Lib/MO2Installer.cs +++ b/Wabbajack.Lib/MO2Installer.cs @@ -37,10 +37,10 @@ namespace Wabbajack.Lib protected override bool _Begin() { ConfigureProcessor(18, RecommendQueueSize()); - var game = GameRegistry.Games[ModList.GameType]; + var game = ModList.GameType.MetaData(); if (GameFolder == null) - GameFolder = game.GameLocation(SteamHandler.Instance.Games.Any(g => g.Game == game.Game)); + GameFolder = game.GameLocation(); if (GameFolder == null) { diff --git a/Wabbajack.Lib/NexusApi/NexusApi.cs b/Wabbajack.Lib/NexusApi/NexusApi.cs index 757d13d7..5cc3dafd 100644 --- a/Wabbajack.Lib/NexusApi/NexusApi.cs +++ b/Wabbajack.Lib/NexusApi/NexusApi.cs @@ -322,13 +322,13 @@ namespace Wabbajack.Lib.NexusApi public GetModFilesResponse GetModFiles(Game game, int modid) { - var url = $"https://api.nexusmods.com/v1/games/{GameRegistry.Games[game].NexusName}/mods/{modid}/files.json"; + var url = $"https://api.nexusmods.com/v1/games/{game.MetaData().NexusName}/mods/{modid}/files.json"; return GetCached(url); } public List GetModInfoFromMD5(Game game, string md5Hash) { - var url = $"https://api.nexusmods.com/v1/games/{GameRegistry.Games[game].NexusName}/mods/md5_search/{md5Hash}.json"; + var url = $"https://api.nexusmods.com/v1/games/{game.MetaData().NexusName}/mods/md5_search/{md5Hash}.json"; return Get>(url); } diff --git a/Wabbajack.Lib/NexusApi/NexusApiUtils.cs b/Wabbajack.Lib/NexusApi/NexusApiUtils.cs index 29915564..f29959e4 100644 --- a/Wabbajack.Lib/NexusApi/NexusApiUtils.cs +++ b/Wabbajack.Lib/NexusApi/NexusApiUtils.cs @@ -14,7 +14,7 @@ namespace Wabbajack.Lib.NexusApi public static string GetModURL(Game game, string argModId) { - return $"https://nexusmods.com/{GameRegistry.Games[game].NexusName}/mods/{argModId}"; + return $"https://nexusmods.com/{game.MetaData().NexusName}/mods/{argModId}"; } public static string FixupSummary(string argSummary) diff --git a/Wabbajack.Lib/Validation/ValidateModlist.cs b/Wabbajack.Lib/Validation/ValidateModlist.cs index 51ff03de..5233707f 100644 --- a/Wabbajack.Lib/Validation/ValidateModlist.cs +++ b/Wabbajack.Lib/Validation/ValidateModlist.cs @@ -140,7 +140,7 @@ namespace Wabbajack.Lib.Validation } }); - var nexus = NexusApi.NexusApiUtils.ConvertGameName(GameRegistry.Games[modlist.GameType].NexusName); + var nexus = NexusApi.NexusApiUtils.ConvertGameName(modlist.GameType.MetaData().NexusName); modlist.Archives .Where(a => a.State is NexusDownloader.State) diff --git a/Wabbajack.Lib/VortexCompiler.cs b/Wabbajack.Lib/VortexCompiler.cs index 9031a699..8ce00592 100644 --- a/Wabbajack.Lib/VortexCompiler.cs +++ b/Wabbajack.Lib/VortexCompiler.cs @@ -50,7 +50,7 @@ namespace Wabbajack.Lib Game = game; GamePath = gamePath; - GameName = GameRegistry.Games[game].NexusName; + GameName = game.MetaData().NexusName; VortexFolder = vortexFolder; DownloadsFolder = downloadsFolder; StagingFolder = stagingFolder; @@ -261,7 +261,7 @@ namespace Wabbajack.Lib Directory.EnumerateFiles(GamePath, "vortex.deployment.json", SearchOption.AllDirectories) .Where(File.Exists) .Do(f => deploymentFile = f); - var currentGame = GameRegistry.Games[Game]; + var currentGame = Game.MetaData(); if (currentGame.AdditionalFolders != null && currentGame.AdditionalFolders.Count != 0) currentGame.AdditionalFolders.Do(f => Directory.EnumerateFiles(f, "vortex.deployment.json", SearchOption.AllDirectories) .Where(File.Exists) @@ -300,7 +300,7 @@ namespace Wabbajack.Lib /// private void AddExternalFolder() { - var currentGame = GameRegistry.Games[Game]; + var currentGame = Game.MetaData(); if (currentGame.AdditionalFolders == null || currentGame.AdditionalFolders.Count == 0) return; currentGame.AdditionalFolders.Do(f => { @@ -472,13 +472,13 @@ namespace Wabbajack.Lib public static string RetrieveDownloadLocation(Game game, string vortexFolderPath = null) { vortexFolderPath = vortexFolderPath ?? TypicalVortexFolder(); - return Path.Combine(vortexFolderPath, "downloads", GameRegistry.Games[game].NexusName); + return Path.Combine(vortexFolderPath, "downloads", game.MetaData().NexusName); } public static string RetrieveStagingLocation(Game game, string vortexFolderPath = null) { vortexFolderPath = vortexFolderPath ?? TypicalVortexFolder(); - var gameName = GameRegistry.Games[game].NexusName; + var gameName = game.MetaData().NexusName; return Path.Combine(vortexFolderPath, gameName, "mods"); } @@ -508,7 +508,7 @@ namespace Wabbajack.Lib public static bool IsActiveVortexGame(Game g) { - return GameRegistry.Games[g].SupportedModManager == ModManager.Vortex && !GameRegistry.Games[g].Disabled; + return g.MetaData().SupportedModManager == ModManager.Vortex && !GameRegistry.Games[g].Disabled; } } diff --git a/Wabbajack.Lib/VortexInstaller.cs b/Wabbajack.Lib/VortexInstaller.cs index 98e933f9..93a35335 100644 --- a/Wabbajack.Lib/VortexInstaller.cs +++ b/Wabbajack.Lib/VortexInstaller.cs @@ -29,7 +29,7 @@ namespace Wabbajack.Lib IgnoreMissingFiles = true; #endif - GameInfo = GameRegistry.Games[ModList.GameType]; + GameInfo = ModList.GameType.MetaData(); } protected override bool _Begin() @@ -85,7 +85,7 @@ namespace Wabbajack.Lib var manualFilesDir = Path.Combine(OutputFolder, Consts.ManualGameFilesDir); - var gameFolder = GameInfo.GameLocation(SteamHandler.Instance.Games.Any(g => g.Game == GameInfo.Game)); + var gameFolder = GameInfo.GameLocation(); Info($"Copying files from {manualFilesDir} " + $"to the game folder at {gameFolder}"); diff --git a/Wabbajack.Lib/Wabbajack.Lib.csproj b/Wabbajack.Lib/Wabbajack.Lib.csproj index ce951b6c..de25b312 100644 --- a/Wabbajack.Lib/Wabbajack.Lib.csproj +++ b/Wabbajack.Lib/Wabbajack.Lib.csproj @@ -116,6 +116,7 @@ + diff --git a/Wabbajack.Test/DownloaderTests.cs b/Wabbajack.Test/DownloaderTests.cs index 0480f5a6..0a52f563 100644 --- a/Wabbajack.Test/DownloaderTests.cs +++ b/Wabbajack.Test/DownloaderTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reactive.Disposables; using System.Reactive.Linq; +using Alphaleonis.Win32.Filesystem; using Microsoft.VisualStudio.TestTools.UnitTesting; using Wabbajack.Common; using Wabbajack.Common.StatusFeed; @@ -11,6 +12,7 @@ using Wabbajack.Lib.LibCefHelpers; using Wabbajack.Lib.NexusApi; using Wabbajack.Lib.Validation; using File = Alphaleonis.Win32.Filesystem.File; +using Game = Wabbajack.Common.Game; namespace Wabbajack.Test { @@ -298,8 +300,32 @@ namespace Wabbajack.Test Assert.AreEqual("eSIyd+KOG3s=", Utils.FileHash(filename)); Assert.AreEqual(File.ReadAllText(filename), "Cheese for Everyone!"); - } + + [TestMethod] + public void GameFileSourceDownload() + { + DownloadDispatcher.GetInstance().Prepare(); + var ini = $@"[General] + gameName={Game.SkyrimSpecialEdition.MetaData().MO2ArchiveName} + gameFile=Data/Update.esm"; + + var state = (AbstractDownloadState)DownloadDispatcher.ResolveArchive(ini.LoadIniString()); + + Assert.IsNotNull(state); + + var converted = state.ViaJSON(); + Assert.IsTrue(converted.Verify()); + var filename = Guid.NewGuid().ToString(); + + Assert.IsTrue(converted.IsWhitelisted(new ServerWhitelist { AllowedPrefixes = new List() })); + + converted.Download(new Archive { Name = "Update.esm" }, filename); + + Assert.AreEqual("/DLG/LjdGXI=", Utils.FileHash(filename)); + CollectionAssert.AreEqual(File.ReadAllBytes(Path.Combine(Game.SkyrimSpecialEdition.MetaData().GameLocation(), "Data/Update.esm")), File.ReadAllBytes(filename)); + } + } diff --git a/Wabbajack.Test/EndToEndTests.cs b/Wabbajack.Test/EndToEndTests.cs index 7395403d..91c5df2c 100644 --- a/Wabbajack.Test/EndToEndTests.cs +++ b/Wabbajack.Test/EndToEndTests.cs @@ -112,7 +112,7 @@ namespace Wabbajack.Test new List { "[General]", - $"gameName={GameRegistry.Games[game].MO2ArchiveName}", + $"gameName={game.MetaData().MO2ArchiveName}", $"modID={modid}", $"fileID={file.file_id}" }); diff --git a/Wabbajack.Test/TestUtils.cs b/Wabbajack.Test/TestUtils.cs index 2020a40f..0f68c4f0 100644 --- a/Wabbajack.Test/TestUtils.cs +++ b/Wabbajack.Test/TestUtils.cs @@ -46,7 +46,7 @@ namespace Wabbajack.Test File.WriteAllLines(Path.Combine(MO2Folder, "ModOrganizer.ini"), new [] { "[General]", - $"gameName={GameRegistry.Games[Game].MO2Name}", + $"gameName={Game.MetaData().MO2Name}", $"gamePath={GameFolder.Replace("\\", "\\\\")}", $"download_directory={DownloadsFolder}" }); diff --git a/Wabbajack/Resources/Wabba_Mouth_Small.png b/Wabbajack/Resources/Wabba_Mouth_Small.png new file mode 100644 index 00000000..88c5c6d0 Binary files /dev/null and b/Wabbajack/Resources/Wabba_Mouth_Small.png differ diff --git a/Wabbajack/Wabbajack.csproj b/Wabbajack/Wabbajack.csproj index 651f4c74..f400adf8 100644 --- a/Wabbajack/Wabbajack.csproj +++ b/Wabbajack/Wabbajack.csproj @@ -544,6 +544,8 @@ - + + + \ No newline at end of file