diff --git a/CHANGELOG.md b/CHANGELOG.md index 39dae32a..dbcde670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ### Changelog +#### Version - 2.1.0.0 - ??? +* Game files are available as downloads automatically during compilation/installation +* Game files are patched/copied/used in BSA creation automatically +* CleanedESM support removed from the compiler stack (still usable during installation for backwards compatability) +* VR games automatically pull from base games if they are required and are installed during compilation + #### Version - 2.0.9.4 - 6/16/2020 * Improve interactions between server and client code diff --git a/Wabbajack.CLI/OptionsDefinition.cs b/Wabbajack.CLI/OptionsDefinition.cs index 65af0719..30aab2dc 100644 --- a/Wabbajack.CLI/OptionsDefinition.cs +++ b/Wabbajack.CLI/OptionsDefinition.cs @@ -20,7 +20,8 @@ namespace Wabbajack.CLI typeof(Changelog), typeof(FindSimilar), typeof(BSADump), - typeof(MigrateGameFolderFiles) + typeof(MigrateGameFolderFiles), + typeof(HashFile) }; } } diff --git a/Wabbajack.CLI/Program.cs b/Wabbajack.CLI/Program.cs index cd0d445a..65b18926 100644 --- a/Wabbajack.CLI/Program.cs +++ b/Wabbajack.CLI/Program.cs @@ -28,6 +28,7 @@ namespace Wabbajack.CLI (FindSimilar opts) => opts.Execute(), (BSADump opts) => opts.Execute(), (MigrateGameFolderFiles opts) => opts.Execute(), + (HashFile opts) => opts.Execute(), errs => 1); } } diff --git a/Wabbajack.CLI/Verbs/HashFile.cs b/Wabbajack.CLI/Verbs/HashFile.cs new file mode 100644 index 00000000..2e63778a --- /dev/null +++ b/Wabbajack.CLI/Verbs/HashFile.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; +using CommandLine; +using Wabbajack.Common; + +namespace Wabbajack.CLI.Verbs +{ + [Verb("hash-file", HelpText = "Hash a file and print the result")] + public class HashFile : AVerb + { + + [Option('i', "input", Required = true, HelpText = "Input file name")] + public string Input { get; set; } = ""; + + protected override async Task Run() + { + var abs = (AbsolutePath)Input; + Console.WriteLine($"{abs} hash: {await abs.FileHashAsync()}"); + return ExitCode.Ok; + } + } +} diff --git a/Wabbajack.Common/GameMetaData.cs b/Wabbajack.Common/GameMetaData.cs index d399bcc9..87f6a1fa 100644 --- a/Wabbajack.Common/GameMetaData.cs +++ b/Wabbajack.Common/GameMetaData.cs @@ -73,8 +73,15 @@ namespace Wabbajack.Common // Games that this game are commonly confused with, for example Skyrim SE vs Skyrim LE public Game[] CommonlyConfusedWith { get; set; } = Array.Empty(); + /// + /// Other games this game can pull source files from (if the game is installed on the user's machine) + /// + public Game[] CanSourceFrom { get; set; } = Array.Empty(); + public string HumanFriendlyGameName => Game.GetDescription(); + private AbsolutePath _cachedPath = default; + public string InstalledVersion { get @@ -97,9 +104,16 @@ namespace Wabbajack.Common public bool TryGetGameLocation(out AbsolutePath path) { + if (_cachedPath != default) + { + path = _cachedPath; + return true; + } + var ret = TryGetGameLocation(); if (ret != null) { + _cachedPath = ret.Value; path = ret.Value; return true; } @@ -329,7 +343,7 @@ namespace Wabbajack.Common "SkyrimSE.exe" }, MainExecutable = "SkyrimSE.exe", - CommonlyConfusedWith = new []{Game.Skyrim, Game.SkyrimVR} + CommonlyConfusedWith = new []{Game.Skyrim, Game.SkyrimVR}, } }, { @@ -347,7 +361,7 @@ namespace Wabbajack.Common "Fallout4.exe" }, MainExecutable = "Fallout4.exe", - CommonlyConfusedWith = new [] {Game.Fallout4VR} + CommonlyConfusedWith = new [] {Game.Fallout4VR}, } }, { @@ -365,7 +379,8 @@ namespace Wabbajack.Common "SkyrimVR.exe" }, MainExecutable = "SkyrimVR.exe", - CommonlyConfusedWith = new []{Game.Skyrim, Game.SkyrimSpecialEdition} + CommonlyConfusedWith = new []{Game.Skyrim, Game.SkyrimSpecialEdition}, + CanSourceFrom = new [] {Game.SkyrimSpecialEdition} } }, { @@ -398,7 +413,8 @@ namespace Wabbajack.Common "Fallout4VR.exe" }, MainExecutable = "Fallout4VR.exe", - CommonlyConfusedWith = new [] {Game.Fallout4} + CommonlyConfusedWith = new [] {Game.Fallout4}, + CanSourceFrom = new [] {Game.Fallout4} } }, { diff --git a/Wabbajack.Common/Paths.cs b/Wabbajack.Common/Paths.cs index 4a3de07a..f9b8ee1c 100644 --- a/Wabbajack.Common/Paths.cs +++ b/Wabbajack.Common/Paths.cs @@ -198,6 +198,7 @@ namespace Wabbajack.Common return new RelativePath(relPath); } + public async Task ReadAllTextAsync() { await using var fs = File.OpenRead(_path); @@ -460,6 +461,12 @@ namespace Wabbajack.Common { return (RelativePath)Guid.NewGuid().ToString(); } + + + public RelativePath Munge() + { + return (RelativePath)_path.Replace('\\', '_').Replace('/', '_').Replace(':', '_'); + } private void Validate() { diff --git a/Wabbajack.Common/Utils.cs b/Wabbajack.Common/Utils.cs index a7dd2819..591356d9 100644 --- a/Wabbajack.Common/Utils.cs +++ b/Wabbajack.Common/Utils.cs @@ -942,8 +942,16 @@ namespace Wabbajack.Common /// public static async ValueTask ToEcryptedJson(this T data, string key) { - var bytes = Encoding.UTF8.GetBytes(data.ToJson()); - await bytes.ToEcryptedData(key); + try + { + var bytes = Encoding.UTF8.GetBytes(data.ToJson()); + await bytes.ToEcryptedData(key); + } + catch (Exception ex) + { + Log($"Error encrypting data {key} {ex}"); + throw; + } } public static async Task FromEncryptedJson(string key) @@ -1125,7 +1133,7 @@ namespace Wabbajack.Common if (lst.Count > 0 && lst.Count != size) yield return lst; } - + private static Random _random = new Random(); public static int NextRandom(int min, int max) { diff --git a/Wabbajack.Lib/ACompiler.cs b/Wabbajack.Lib/ACompiler.cs index ef214a04..dca73208 100644 --- a/Wabbajack.Lib/ACompiler.cs +++ b/Wabbajack.Lib/ACompiler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; using System.Linq; @@ -221,9 +222,10 @@ namespace Wabbajack.Lib throw new ArgumentException($"No match found for Archive sha: {hash.ToBase64()} this shouldn't happen"); } - public async Task ResolveArchive(IndexedArchive archive) + public async Task ResolveArchive([NotNull] IndexedArchive archive) { - Utils.Status($"Checking link for {archive.Name}", alsoLog: true); + if (!string.IsNullOrWhiteSpace(archive.Name)) + Utils.Status($"Checking link for {archive.Name}", alsoLog: true); if (archive.IniData == null) Error( @@ -234,7 +236,7 @@ namespace Wabbajack.Lib if (result.State == null) Error($"{archive.Name} could not be handled by any of the downloaders"); - result.Name = archive.Name; + result.Name = archive.Name ?? ""; result.Hash = archive.File.Hash; result.Size = archive.File.Size; diff --git a/Wabbajack.Lib/ClientAPI.cs b/Wabbajack.Lib/ClientAPI.cs index f8fae1c2..6911f8ed 100644 --- a/Wabbajack.Lib/ClientAPI.cs +++ b/Wabbajack.Lib/ClientAPI.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; -using System.Net; + using System.Linq; + using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -8,8 +9,9 @@ using Wabbajack.Common; using Wabbajack.Common.Exceptions; using Wabbajack.Common.Serialization.Json; using Wabbajack.Lib.Downloaders; + using Wabbajack.VirtualFileSystem; -namespace Wabbajack.Lib + namespace Wabbajack.Lib { [JsonName("ModUpgradeRequest")] public class ModUpgradeRequest @@ -115,20 +117,27 @@ namespace Wabbajack.Lib .GetJsonAsync($"{Consts.WabbajackBuildServerUri}nexus_cache/stats"); } - public static async Task> GetGameFiles(Game game, Version version) - { - // TODO: Disabled for now - return new Dictionary(); - /* - return await GetClient() - .GetJsonAsync>($"{Consts.WabbajackBuildServerUri}game_files/{game}/{version}"); - */ - } - public static async Task SendModListDefinition(ModList modList) { var client = await GetClient(); await client.PostAsync($"{Consts.WabbajackBuildServerUri}list_definitions/ingest", new StringContent(modList.ToJson(), Encoding.UTF8, "application/json")); } + + public static async Task GetExistingGameFiles(WorkQueue queue, Game game) + { + var client = await GetClient(); + var metaData = game.MetaData(); + var results = + await client.GetJsonAsync( + $"{Consts.WabbajackBuildServerUri}game_files/{game}/{metaData.InstalledVersion}"); + + return (await results.PMap(queue, async file => (await file.State.Verify(file), file))).Where(f => f.Item1) + .Select(f => + { + f.file.Name = ((GameFileSourceDownloader.State)f.file.State).GameFile.Munge().ToString(); + return f.file; + }) + .ToArray(); + } } } diff --git a/Wabbajack.Lib/CompilationSteps/IncludePatches.cs b/Wabbajack.Lib/CompilationSteps/IncludePatches.cs index 15b39778..7d8cc4ec 100644 --- a/Wabbajack.Lib/CompilationSteps/IncludePatches.cs +++ b/Wabbajack.Lib/CompilationSteps/IncludePatches.cs @@ -13,7 +13,7 @@ namespace Wabbajack.Lib.CompilationSteps { private readonly Dictionary> _indexed; private VirtualFile? _bsa; - private Dictionary _indexedByName; + private Dictionary> _indexedByName; private MO2Compiler _mo2Compiler; public IncludePatches(ACompiler compiler, VirtualFile? constructingFromBSA = null) : base(compiler) @@ -27,7 +27,8 @@ namespace Wabbajack.Lib.CompilationSteps _indexedByName = _indexed.Values .SelectMany(s => s) .Where(f => f.IsNative) - .ToDictionary(f => f.FullPath.FileName); + .GroupBy(f => f.FullPath.FileName) + .ToDictionary(f => f.Key, f => (IEnumerable)f); } public override async ValueTask Run(RawSourceFile source) @@ -78,7 +79,7 @@ namespace Wabbajack.Lib.CompilationSteps if (_indexedByName.TryGetValue(relName, out var arch)) { // Just match some file in the archive based on the smallest delta difference - found = arch.ThisAndAllChildren + found = arch.SelectMany(a => a.ThisAndAllChildren) .OrderBy(o => Math.Abs(o.Size - source.File.Size)) .First(); } diff --git a/Wabbajack.Lib/FileUploader/AuthorAPI.cs b/Wabbajack.Lib/FileUploader/AuthorAPI.cs index 221555f0..4c75364c 100644 --- a/Wabbajack.Lib/FileUploader/AuthorAPI.cs +++ b/Wabbajack.Lib/FileUploader/AuthorAPI.cs @@ -53,41 +53,6 @@ namespace Wabbajack.Lib.FileUploader return await RunJob("UpdateModLists"); } - public static async Task UploadPackagedInis(IEnumerable archives) - { - archives = archives.ToArray(); // defensive copy - Utils.Log($"Packaging {archives.Count()} archive states"); - try - { - await using var ms = new MemoryStream(); - using (var z = new ZipArchive(ms, ZipArchiveMode.Create, true)) - { - foreach (var e in archives) - { - if (e.State == null) continue; - var entry = z.CreateEntry(Path.GetFileName(e.Name)); - await using var os = entry.Open(); - e.ToJson(os); - } - } - - var client = new Common.Http.Client(); - var response = await client.PostAsync($"{Consts.WabbajackBuildServerUri}indexed_files/notify", new ByteArrayContent(ms.ToArray())); - - if (response.IsSuccessStatusCode) return true; - - Utils.Log("Error sending Inis"); - Utils.Log(await response.Content.ReadAsStringAsync()); - return false; - - } - catch (Exception ex) - { - Utils.Log(ex.ToString()); - return false; - } - } - public static async Task GetServerLog() { return await (await GetAuthorizedClient()).GetStringAsync($"{Consts.WabbajackBuildServerUri}heartbeat/logs"); diff --git a/Wabbajack.Lib/MO2Compiler.cs b/Wabbajack.Lib/MO2Compiler.cs index f6f9e216..8ee64682 100644 --- a/Wabbajack.Lib/MO2Compiler.cs +++ b/Wabbajack.Lib/MO2Compiler.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Threading; using System.Threading.Tasks; using Wabbajack.Common; @@ -31,6 +32,11 @@ namespace Wabbajack.Lib public override AbsolutePath GamePath { get; } public GameMetaData CompilingGame { get; } + + /// + /// All games available for sourcing during compilation (including the Compiling Game) + /// + public List AvailableGames { get; } public override AbsolutePath ModListOutputFolder => ((RelativePath)"output_folder").RelativeToEntryPoint(); @@ -61,6 +67,8 @@ namespace Wabbajack.Lib CompilingGame = GameRegistry.Games.First(g => g.Value.MO2Name == mo2game).Value; GamePath = new AbsolutePath((string)MO2Ini.General.gamePath.Replace("\\\\", "\\")); ModListOutputFile = outputFile; + + AvailableGames = CompilingGame.CanSourceFrom.Cons(CompilingGame.Game).Where(g => g.MetaData().IsInstalled).ToList(); } public AbsolutePath MO2DownloadsFolder @@ -104,8 +112,9 @@ namespace Wabbajack.Lib { roots = new List { - MO2Folder, GamePath, MO2DownloadsFolder, CompilingGame.GameLocation() + MO2Folder, GamePath, MO2DownloadsFolder }; + roots.AddRange(AvailableGames.Select(g => g.MetaData().GameLocation())); } else { @@ -191,36 +200,24 @@ namespace Wabbajack.Lib })).ToList(); - var stockGameFolder = CompilingGame.GameLocation(); - - var installedVersion = CompilingGame.InstalledVersion; - if (installedVersion != null) + if (UseGamePaths) { - foreach (var (relativePath, hash) in await ClientAPI.GetGameFiles(CompilingGame.Game, Version.Parse(installedVersion))) + foreach (var ag in AvailableGames) { - if (!VFS.Index.ByRootPath.TryGetValue(relativePath.RelativeTo(stockGameFolder), out var virtualFile)) - continue; - if (virtualFile.Hash != hash) - { - Utils.Log( - $"File {relativePath} int the game folder appears to be modified, it will not be used during compilation"); - continue; - } + var files = await ClientAPI.GetExistingGameFiles(Queue, ag); + Utils.Log($"Including {files.Length} stock game files from {ag} as download sources"); - var state = new GameFileSourceDownloader.State + IndexedArchives.AddRange(files.Select(f => { - Game = CompilingGame.Game, - GameVersion = CompilingGame.InstalledVersion, - GameFile = relativePath - }; - - Utils.Log($"Adding Game file: {relativePath}"); - IndexedArchives.Add(new IndexedArchive(virtualFile) - { - Name = (string)relativePath.FileName, - IniData = state.GetMetaIniString().LoadIniString(), - Meta = state.GetMetaIniString() - }); + var meta = f.State.GetMetaIniString(); + var ini = meta.LoadIniString(); + var state = (GameFileSourceDownloader.State)f.State; + return new IndexedArchive( + VFS.Index.ByRootPath[ag.MetaData().GameLocation().Combine(state.GameFile)]) + { + IniData = ini, Meta = meta, + }; + })); } } @@ -320,10 +317,6 @@ namespace Wabbajack.Lib UpdateTracker.NextStep("Gathering Archives"); await GatherArchives(); - // Don't await this because we don't care if it fails. - Utils.Log("Finding States to package"); - await AuthorAPI.UploadPackagedInis(SelectedArchives.ToArray()); - UpdateTracker.NextStep("Including Archive Metadata"); await IncludeArchiveMetadata(); UpdateTracker.NextStep("Building Patches"); @@ -438,13 +431,15 @@ namespace Wabbajack.Lib Utils.Log($"Including {SelectedArchives.Count} .meta files for downloads"); await SelectedArchives.PMap(Queue, async a => { + if (a.State is GameFileSourceDownloader.State) return; + var source = MO2DownloadsFolder.Combine(a.Name + Consts.MetaFileExtension); var ini = a.State.GetMetaIniString(); var (id, fullPath) = await IncludeString(ini); InstallDirectives.Add(new ArchiveMeta { SourceDataID = id, - Size = source.Size, + Size = fullPath.Size, Hash = await fullPath.FileHashAsync(), To = source.FileName }); @@ -602,7 +597,7 @@ namespace Wabbajack.Lib new IgnoreWabbajackInstallCruft(this), - new PatchStockESMs(this), + //new PatchStockESMs(this), new IncludeAllConfigs(this), new zEditIntegration.IncludeZEditPatches(this), diff --git a/Wabbajack.Server/Controllers/GameFiles.cs b/Wabbajack.Server/Controllers/GameFiles.cs new file mode 100644 index 00000000..a4c695be --- /dev/null +++ b/Wabbajack.Server/Controllers/GameFiles.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Wabbajack.Common; +using Wabbajack.Lib; +using Wabbajack.Lib.Downloaders; +using Wabbajack.Server.DataLayer; +using Wabbajack.Server.Services; + +namespace Wabbajack.BuildServer.Controllers +{ + [Route("/game_files")] + public class EnqueueGameFiles : ControllerBase + { + private readonly ILogger _logger; + private readonly SqlService _sql; + private readonly QuickSync _quickSync; + + public EnqueueGameFiles(ILogger logger, SqlService sql, QuickSync quickSync) + { + _logger = logger; + _sql = sql; + _quickSync = quickSync; + } + + + [Authorize(Roles = "Author")] + [HttpGet("enqueue")] + public async Task Enqueue() + { + var games = GameRegistry.Games.Where(g => g.Value.IsInstalled).Select(g => g.Value).ToList(); + _logger.Log(LogLevel.Information, $"Found {games.Count} installed games"); + + var files = games.SelectMany(game => + game.GameLocation().EnumerateFiles(true).Select(file => new {File = file, Game = game})).ToList(); + + _logger.Log(LogLevel.Information, $"Found {files.Count} game files"); + + using var queue = new WorkQueue(); + var hashed = await files.PMap(queue, async pair => + { + var hash = await pair.File.FileHashCachedAsync(); + return await _sql.GetOrEnqueueArchive(new Archive(new GameFileSourceDownloader.State + { + Game = pair.Game.Game, + GameFile = pair.File.RelativeTo(pair.Game.GameLocation()), + GameVersion = pair.Game.InstalledVersion, + Hash = hash + }) {Name = pair.File.FileName.ToString(), Size = pair.File.Size, Hash = hash}); + }); + + await _quickSync.Notify(); + return Ok(hashed); + } + + [Authorize(Roles = "User")] + [HttpGet("{game}/{version}")] + public async Task GetFiles(string game, string version) + { + if (!GameRegistry.TryGetByFuzzyName(game, out var meta)) + return NotFound($"Game {game} not found"); + + var files = await _sql.GetGameFiles(meta.Game, version); + return Ok(files.ToJson()); + } + + + + } +} diff --git a/Wabbajack.Server/DTOs/DiscordMessage.cs b/Wabbajack.Server/DTOs/DiscordMessage.cs index f9903fa6..9060f386 100644 --- a/Wabbajack.Server/DTOs/DiscordMessage.cs +++ b/Wabbajack.Server/DTOs/DiscordMessage.cs @@ -23,6 +23,9 @@ namespace Wabbajack.Server.DTOs [JsonName("DiscordEmbed")] public class DiscordEmbed { + [JsonProperty("title")] + public string Title { get; set; } + [JsonProperty("color")] public int Color { get; set; } diff --git a/Wabbajack.Server/DataLayer/ArchiveDownloads.cs b/Wabbajack.Server/DataLayer/ArchiveDownloads.cs index 8a2b84d2..1bd102e7 100644 --- a/Wabbajack.Server/DataLayer/ArchiveDownloads.cs +++ b/Wabbajack.Server/DataLayer/ArchiveDownloads.cs @@ -105,7 +105,7 @@ namespace Wabbajack.Server.DataLayer public async Task GetOrEnqueueArchive(Archive a) { await using var conn = await Open(); - using var trans = await conn.BeginTransactionAsync(); + await using var trans = await conn.BeginTransactionAsync(); var result = await conn.QueryFirstOrDefaultAsync<(Guid, long?, Hash?, bool?, AbstractDownloadState, DateTime?)>( "SELECT Id, Size, Hash, IsFailed, DownloadState, DownloadFinished FROM dbo.ArchiveDownloads WHERE PrimaryKeyString = @PrimaryKeyString AND Hash = @Hash AND Size = @Size", new @@ -114,7 +114,7 @@ namespace Wabbajack.Server.DataLayer Hash = a.Hash, Size = a.Size }, trans); - if (result != default) + if (result.Item1 != default) { return new ArchiveDownload { @@ -152,12 +152,12 @@ namespace Wabbajack.Server.DataLayer if (ignoreNexus) { result = await conn.QueryFirstOrDefaultAsync<(Guid, long?, Hash?, AbstractDownloadState)>( - "SELECT Id, Size, Hash, DownloadState FROM dbo.ArchiveDownloads WHERE DownloadFinished is NULL AND Downloader != 'NexusDownloader+State'"); + "SELECT TOP(1) Id, Size, Hash, DownloadState FROM dbo.ArchiveDownloads WHERE DownloadFinished is NULL AND Downloader != 'NexusDownloader+State'"); } else { result = await conn.QueryFirstOrDefaultAsync<(Guid, long?, Hash?, AbstractDownloadState)>( - "SELECT Id, Size, Hash, DownloadState FROM dbo.ArchiveDownloads WHERE DownloadFinished is NULL"); + "SELECT TOP(1) Id, Size, Hash, DownloadState FROM dbo.ArchiveDownloads WHERE DownloadFinished is NULL"); } if (result == default) @@ -197,5 +197,18 @@ namespace Wabbajack.Server.DataLayer WHERE ad.PrimaryKeyString is null"); } + public async Task> GetGameFiles(Game game, string version) + { + await using var conn = await Open(); + var files = (await conn.QueryAsync<(Hash, long, AbstractDownloadState)>( + $"SELECT Hash, Size, DownloadState FROM dbo.ArchiveDownloads WHERE PrimaryKeyString like 'GameFileSourceDownloader+State|{game}|{version}|%'")) + .Select(f => new Archive(f.Item3) + { + Hash = f.Item1, + Size = f.Item2 + }).ToList(); + return files; + } + } } diff --git a/Wabbajack.Server/Services/ArchiveDownloader.cs b/Wabbajack.Server/Services/ArchiveDownloader.cs index f0c13dbe..e64bedef 100644 --- a/Wabbajack.Server/Services/ArchiveDownloader.cs +++ b/Wabbajack.Server/Services/ArchiveDownloader.cs @@ -46,6 +46,8 @@ namespace Wabbajack.Server.Services if (nextDownload == null) break; + _logger.LogInformation($"Checking for previously archived {nextDownload.Archive.Hash}"); + if (nextDownload.Archive.Hash != default && _archiveMaintainer.HaveArchive(nextDownload.Archive.Hash)) { await nextDownload.Finish(_sql); @@ -61,7 +63,8 @@ namespace Wabbajack.Server.Services try { _logger.Log(LogLevel.Information, $"Downloading {nextDownload.Archive.State.PrimaryKeyString}"); - await _discord.Send(Channel.Spam, new DiscordMessage {Content = $"Downloading {nextDownload.Archive.State.PrimaryKeyString}"}); + if (!(nextDownload.Archive.State is GameFileSourceDownloader.State)) + await _discord.Send(Channel.Spam, new DiscordMessage {Content = $"Downloading {nextDownload.Archive.State.PrimaryKeyString}"}); await DownloadDispatcher.PrepareAll(new[] {nextDownload.Archive.State}); await using var tempPath = new TempFile(); @@ -90,7 +93,9 @@ namespace Wabbajack.Server.Services _logger.Log(LogLevel.Information, $"Finished Archiving {nextDownload.Archive.State.PrimaryKeyString}"); await nextDownload.Finish(_sql); - await _discord.Send(Channel.Spam, new DiscordMessage {Content = $"Finished downloading {nextDownload.Archive.State.PrimaryKeyString}"}); + + if (!(nextDownload.Archive.State is GameFileSourceDownloader.State)) + await _discord.Send(Channel.Spam, new DiscordMessage {Content = $"Finished downloading {nextDownload.Archive.State.PrimaryKeyString}"}); } diff --git a/Wabbajack.Server/Services/ListValidator.cs b/Wabbajack.Server/Services/ListValidator.cs index bf20b41b..3294fa9b 100644 --- a/Wabbajack.Server/Services/ListValidator.cs +++ b/Wabbajack.Server/Services/ListValidator.cs @@ -102,7 +102,7 @@ namespace Wabbajack.Server.Services { new DiscordEmbed { - Description = + Title = $"Number of failures in {summary.Name} (`{summary.MachineURL}`) was {oldSummary.Summary.Failed} is now {summary.Failed}", Url = new Uri( $"https://build.wabbajack.org/lists/status/{summary.MachineURL}.html") @@ -120,7 +120,10 @@ namespace Wabbajack.Server.Services { new DiscordEmbed { - Description = $"{summary.Name} (`{summary.MachineURL}`) is now passing.", + Title = $"{summary.Name} (`{summary.MachineURL}`) is now passing.", + Url = new Uri( + $"https://build.wabbajack.org/lists/status/{summary.MachineURL}.html") + } } }); diff --git a/Wabbajack.Test/SanityTests.cs b/Wabbajack.Test/SanityTests.cs index 4cfd9568..22ec2a8b 100644 --- a/Wabbajack.Test/SanityTests.cs +++ b/Wabbajack.Test/SanityTests.cs @@ -473,6 +473,42 @@ namespace Wabbajack.Test await utils.VerifyInstalledFile(mod, @"Data\scripts\test.pex"); } + [Fact] + public async Task CanSourceFilesFromTheGameFiles() + { + + var profile = utils.AddProfile(); + var mod = await utils.AddMod(); + + Game.SkyrimSpecialEdition.MetaData().CanSourceFrom = new[] {Game.Morrowind, Game.Skyrim}; + + // Morrowind file with different name + var mwFile = Game.Morrowind.MetaData().GameLocation().Combine("Data Files", "Bloodmoon.esm"); + var testMW = await utils.AddModFile(mod, @"Data\MW\Bm.esm"); + await mwFile.CopyToAsync(testMW); + + // Skyrim file with same name + var skyrimFile = Game.Skyrim.MetaData().GameLocation().Combine("Data", "Update.esm"); + var testSky = await utils.AddModFile(mod, @"Data\Skyrim\Update.esm"); + await skyrimFile.CopyToAsync(testSky); + + // Same game, but patched ata + + var pdata = utils.RandomData(1024); + var testSkySE = await utils.AddModFile(mod, @"Data\SkyrimSE\Update.esm"); + await testSkySE.WriteAllBytesAsync(pdata); + + + await utils.Configure(); + + await CompileAndInstall(profile, useGameFiles: true); + + await utils.VerifyInstalledFile(mod, @"Data\MW\Bm.esm"); + await utils.VerifyInstalledFile(mod, @"Data\Skyrim\Update.esm"); + await utils.VerifyInstalledFile(mod, @"Data\SkyrimSE\Update.esm"); + + } + /// /// Issue #861 : https://github.com/wabbajack-tools/wabbajack/issues/861