2021-09-27 12:42:46 +00:00
|
|
|
using System;
|
|
|
|
using System.Collections.Concurrent;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Diagnostics;
|
|
|
|
using System.IO;
|
|
|
|
using System.IO.Compression;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Threading;
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
using Wabbajack.Common;
|
|
|
|
using Wabbajack.Compiler.CompilationSteps;
|
|
|
|
using Wabbajack.Downloaders;
|
2021-10-13 03:59:54 +00:00
|
|
|
using Wabbajack.Downloaders.GameFile;
|
2021-09-27 12:42:46 +00:00
|
|
|
using Wabbajack.DTOs;
|
|
|
|
using Wabbajack.DTOs.Directives;
|
|
|
|
using Wabbajack.DTOs.DownloadStates;
|
|
|
|
using Wabbajack.DTOs.JsonConverters;
|
|
|
|
using Wabbajack.Hashing.xxHash64;
|
|
|
|
using Wabbajack.Installer;
|
|
|
|
using Wabbajack.Networking.WabbajackClientApi;
|
|
|
|
using Wabbajack.Paths;
|
|
|
|
using Wabbajack.Paths.IO;
|
2021-10-15 04:20:27 +00:00
|
|
|
using Wabbajack.RateLimiter;
|
2021-09-27 12:42:46 +00:00
|
|
|
using Wabbajack.VFS;
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
namespace Wabbajack.Compiler;
|
|
|
|
|
|
|
|
public abstract class ACompiler
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
protected readonly DownloadDispatcher _dispatcher;
|
|
|
|
private readonly DTOSerializer _dtos;
|
|
|
|
private readonly FileExtractor.FileExtractor _extractor;
|
|
|
|
private readonly FileHashCache _hashCache;
|
|
|
|
public readonly IGameLocator _locator;
|
|
|
|
protected internal readonly ILogger _logger;
|
|
|
|
private readonly TemporaryFileManager _manager;
|
|
|
|
public readonly ParallelOptions _parallelOptions;
|
|
|
|
public readonly IBinaryPatchCache _patchCache;
|
|
|
|
private readonly AbsolutePath _stagingFolder;
|
|
|
|
private readonly Stopwatch _updateStopWatch = new();
|
|
|
|
protected readonly Context _vfs;
|
|
|
|
protected readonly Client _wjClient;
|
|
|
|
public readonly IResource<ACompiler> CompilerLimiter;
|
|
|
|
private int _currentStep;
|
|
|
|
private long _currentStepProgress;
|
|
|
|
|
|
|
|
private long _maxStepProgress;
|
|
|
|
public ConcurrentDictionary<PatchedFromArchive, VirtualFile[]> _patchOptions;
|
|
|
|
public CompilerSettings _settings;
|
|
|
|
|
|
|
|
public ConcurrentDictionary<Directive, RawSourceFile> _sourceFileLinks;
|
|
|
|
private string _statusText;
|
2022-01-27 07:20:48 +00:00
|
|
|
private string _statusCategory;
|
2021-10-23 16:51:17 +00:00
|
|
|
public List<IndexedArchive> IndexedArchives = new();
|
|
|
|
|
|
|
|
public Dictionary<Hash, IEnumerable<VirtualFile>> IndexedFiles = new();
|
|
|
|
|
|
|
|
public ModList ModList = new();
|
|
|
|
public AbsolutePath ModListImage;
|
|
|
|
|
|
|
|
public ACompiler(ILogger logger, FileExtractor.FileExtractor extractor, FileHashCache hashCache, Context vfs,
|
|
|
|
TemporaryFileManager manager, CompilerSettings settings,
|
|
|
|
ParallelOptions parallelOptions, DownloadDispatcher dispatcher, Client wjClient, IGameLocator locator,
|
|
|
|
DTOSerializer dtos, IResource<ACompiler> compilerLimiter,
|
|
|
|
IBinaryPatchCache patchCache)
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
CompilerLimiter = compilerLimiter;
|
|
|
|
_logger = logger;
|
|
|
|
_extractor = extractor;
|
|
|
|
_hashCache = hashCache;
|
|
|
|
_vfs = vfs;
|
|
|
|
_manager = manager;
|
|
|
|
_settings = settings;
|
|
|
|
_stagingFolder = _manager.CreateFolder().Path;
|
|
|
|
_parallelOptions = parallelOptions;
|
|
|
|
_sourceFileLinks = new ConcurrentDictionary<Directive, RawSourceFile>();
|
|
|
|
_dispatcher = dispatcher;
|
|
|
|
_wjClient = wjClient;
|
|
|
|
_locator = locator;
|
|
|
|
_dtos = dtos;
|
|
|
|
_patchOptions = new ConcurrentDictionary<PatchedFromArchive, VirtualFile[]>();
|
|
|
|
_sourceFileLinks = new ConcurrentDictionary<Directive, RawSourceFile>();
|
|
|
|
_patchCache = patchCache;
|
|
|
|
_updateStopWatch = new Stopwatch();
|
|
|
|
}
|
2021-10-15 04:20:27 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
protected long MaxSteps { get; set; }
|
2021-10-15 04:20:27 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public CompilerSettings Settings
|
|
|
|
{
|
|
|
|
get => _settings;
|
|
|
|
set => _settings = value;
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public Dictionary<Game, HashSet<Hash>> GameHashes { get; set; } = new();
|
|
|
|
public Dictionary<Hash, Game[]> GamesWithHashes { get; set; } = new();
|
2022-05-26 04:58:11 +00:00
|
|
|
public ILookup<Hash,Archive> GameFiles { get; private set; }
|
2021-10-12 03:49:01 +00:00
|
|
|
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public bool IgnoreMissingFiles { get; set; }
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public List<Archive> SelectedArchives { get; protected set; } = new();
|
|
|
|
public List<Directive> InstallDirectives { get; protected set; } = new();
|
|
|
|
public List<RawSourceFile> AllFiles { get; protected set; } = new();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public Dictionary<AbsolutePath, IndexedArchive> ArchivesByFullPath { get; set; } = new();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public event EventHandler<StatusUpdate> OnStatusUpdate;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2022-01-27 07:20:48 +00:00
|
|
|
public void NextStep(string statusCategory, string statusText, long maxStepProgress = 1)
|
2021-10-23 16:51:17 +00:00
|
|
|
{
|
|
|
|
_updateStopWatch.Restart();
|
|
|
|
_maxStepProgress = maxStepProgress;
|
|
|
|
_currentStep += 1;
|
|
|
|
_statusText = statusText;
|
2022-01-27 07:20:48 +00:00
|
|
|
_statusCategory = statusCategory;
|
2021-10-23 16:51:17 +00:00
|
|
|
_logger.LogInformation("Compiler Step: {Step}", statusText);
|
|
|
|
|
|
|
|
if (OnStatusUpdate != null)
|
2022-01-27 07:20:48 +00:00
|
|
|
OnStatusUpdate(this, new StatusUpdate(statusCategory, $"[{_currentStep}/{MaxSteps}] " + statusText,
|
2021-10-23 16:51:17 +00:00
|
|
|
Percent.FactoryPutInRange(_currentStep, MaxSteps),
|
|
|
|
Percent.Zero));
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public void UpdateProgress(long stepProgress)
|
|
|
|
{
|
|
|
|
Interlocked.Add(ref _currentStepProgress, stepProgress);
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
lock (_updateStopWatch)
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
if (_updateStopWatch.ElapsedMilliseconds < 100) return;
|
|
|
|
_updateStopWatch.Restart();
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
if (OnStatusUpdate != null)
|
2022-01-27 07:20:48 +00:00
|
|
|
OnStatusUpdate(this, new StatusUpdate(_statusCategory, _statusText, Percent.FactoryPutInRange(_currentStep, MaxSteps),
|
2021-10-23 16:51:17 +00:00
|
|
|
Percent.FactoryPutInRange(_currentStepProgress, _maxStepProgress)));
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public abstract Task<bool> Begin(CancellationToken token);
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
internal RelativePath IncludeId()
|
|
|
|
{
|
|
|
|
return Guid.NewGuid().ToString().ToRelativePath();
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
internal async Task<RelativePath> IncludeFile(byte[] data)
|
|
|
|
{
|
|
|
|
var id = IncludeId();
|
|
|
|
await _stagingFolder.Combine(id).WriteAllBytesAsync(data);
|
|
|
|
return id;
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
internal async Task<RelativePath> IncludeFile(Stream data)
|
|
|
|
{
|
|
|
|
var id = IncludeId();
|
|
|
|
await using var os = _stagingFolder.Combine(id).Open(FileMode.Create, FileAccess.Write);
|
|
|
|
await data.CopyToAsync(os);
|
|
|
|
return id;
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
internal AbsolutePath IncludeFile(out RelativePath id)
|
|
|
|
{
|
|
|
|
id = IncludeId();
|
|
|
|
return _stagingFolder.Combine(id);
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
internal async Task<RelativePath> IncludeFile(string data)
|
|
|
|
{
|
|
|
|
var id = IncludeId();
|
|
|
|
await _stagingFolder.Combine(id).WriteAllTextAsync(data);
|
|
|
|
return id;
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
internal async Task<RelativePath> IncludeFile(Stream data, CancellationToken token)
|
|
|
|
{
|
|
|
|
var id = IncludeId();
|
|
|
|
await _stagingFolder.Combine(id).WriteAllAsync(data, token);
|
|
|
|
return id;
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
internal async Task<RelativePath> IncludeFile(AbsolutePath data, CancellationToken token)
|
|
|
|
{
|
|
|
|
await using var stream = data.Open(FileMode.Open);
|
|
|
|
return await IncludeFile(stream, token);
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
internal async Task<(RelativePath, AbsolutePath)> IncludeString(string str)
|
|
|
|
{
|
|
|
|
var id = IncludeId();
|
|
|
|
var fullPath = _stagingFolder.Combine(id);
|
|
|
|
await fullPath.WriteAllTextAsync(str);
|
|
|
|
return (id, fullPath);
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public async Task<bool> GatherMetaData()
|
|
|
|
{
|
|
|
|
_logger.LogInformation("Getting meta data for {count} archives", SelectedArchives.Count);
|
2022-01-27 07:20:48 +00:00
|
|
|
NextStep("Building", "Gathering Metadata", SelectedArchives.Count);
|
2021-10-23 16:51:17 +00:00
|
|
|
await SelectedArchives.PDoAll(CompilerLimiter, async a =>
|
|
|
|
{
|
|
|
|
UpdateProgress(1);
|
|
|
|
await _dispatcher.FillInMetadata(a);
|
|
|
|
});
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
return true;
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
protected async Task IndexGameFileHashes()
|
|
|
|
{
|
2022-01-27 07:20:48 +00:00
|
|
|
NextStep("Compiling", "Indexing Game Files");
|
2022-05-26 04:58:11 +00:00
|
|
|
var gameFiles = new List<Archive>();
|
2021-10-23 16:51:17 +00:00
|
|
|
if (_settings.UseGamePaths)
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
//taking the games in Settings.IncludedGames + currently compiling game so you can eg
|
|
|
|
//include the stock game files if you are compiling for a VR game (ex: Skyrim + SkyrimVR)
|
|
|
|
foreach (var ag in _settings.OtherGames.Append(_settings.Game).Distinct())
|
2021-09-27 12:42:46 +00:00
|
|
|
try
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
if (!_locator.TryFindLocation(ag, out var path))
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
_logger.LogWarning("Game {game} was to be used in compilation but it is not installed", ag);
|
|
|
|
return;
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
var mainFile = ag.MetaData().MainExecutable!.Value.RelativeTo(path);
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
if (!mainFile.FileExists())
|
|
|
|
_logger.LogWarning("Main file {file} for {game} does not exist", mainFile, ag);
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
var versionInfo = FileVersionInfo.GetVersionInfo(mainFile.ToString());
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
var files = await _wjClient.GetGameArchives(ag, versionInfo.FileVersion ?? "0.0.0.0");
|
2022-05-26 04:58:11 +00:00
|
|
|
gameFiles.AddRange(files);
|
2021-10-15 04:20:27 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
_logger.LogInformation($"Including {files.Length} stock game files from {ag} as download sources");
|
|
|
|
GameHashes[ag] = files.Select(f => f.Hash).ToHashSet();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
IndexedArchives.AddRange(files.Select(f =>
|
|
|
|
{
|
|
|
|
var state = (GameFileSource) f.State;
|
|
|
|
return new IndexedArchive(
|
|
|
|
_vfs.Index.ByRootPath[path.Combine(state.GameFile)])
|
|
|
|
{
|
|
|
|
Name = state.GameFile.ToString().Replace("/", "_").Replace("\\", "_"),
|
|
|
|
State = state
|
|
|
|
};
|
|
|
|
}));
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
catch (Exception e)
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
_logger.LogCritical(e, "Unable to find existing game files for {game}, skipping.", ag);
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
GamesWithHashes = GameHashes.SelectMany(g => g.Value.Select(h => (g, h)))
|
|
|
|
.GroupBy(gh => gh.h)
|
|
|
|
.ToDictionary(gh => gh.Key, gh => gh.Select(p => p.g.Key).ToArray());
|
|
|
|
}
|
2022-05-26 04:58:11 +00:00
|
|
|
|
|
|
|
GameFiles = gameFiles.ToLookup(f => f.Hash);
|
2021-10-23 16:51:17 +00:00
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2022-05-26 04:58:11 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
protected async Task CleanInvalidArchivesAndFillState()
|
|
|
|
{
|
2022-01-27 07:20:48 +00:00
|
|
|
NextStep("Compiling", "Cleaning Invalid Archives");
|
2022-05-29 04:19:25 +00:00
|
|
|
var remove = await IndexedArchives.PKeepAll(CompilerLimiter, async a =>
|
2021-10-23 16:51:17 +00:00
|
|
|
{
|
|
|
|
try
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
var resolved = await ResolveArchive(a);
|
2022-05-29 04:19:25 +00:00
|
|
|
if (resolved == null) return a;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
a.State = resolved.State;
|
2022-05-29 04:19:25 +00:00
|
|
|
return default;
|
2021-10-23 16:51:17 +00:00
|
|
|
}
|
|
|
|
catch (Exception ex)
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2022-05-27 05:41:11 +00:00
|
|
|
_logger.LogWarning(ex, "While resolving archive {Archive}", a.Name);
|
2022-05-29 04:19:25 +00:00
|
|
|
return a;
|
2021-10-23 16:51:17 +00:00
|
|
|
}
|
2022-05-29 04:19:25 +00:00
|
|
|
}).ToHashSet();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
if (remove.Count == 0) return;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
_logger.LogWarning(
|
|
|
|
"Removing {count} archives from the compilation state, this is probably not an issue but reference this if you have compilation failures",
|
|
|
|
remove.Count);
|
|
|
|
remove.Do(r => _logger.LogWarning("Resolution failed for: ({size} {hash}) {path}", r.File.Size, r.File.Hash,
|
|
|
|
r.File.FullPath));
|
|
|
|
IndexedArchives.RemoveAll(a => remove.Contains(a));
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
protected async Task InferMetas(CancellationToken token)
|
|
|
|
{
|
|
|
|
async Task<bool> HasInvalidMeta(AbsolutePath filename)
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
var metaName = filename.WithExtension(Ext.Meta);
|
|
|
|
if (!metaName.FileExists()) return true;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
try
|
|
|
|
{
|
|
|
|
var ini = metaName.LoadIniFile();
|
|
|
|
return await _dispatcher.ResolveArchive(ini["General"].ToDictionary(d => d.KeyName, d => d.Value)) ==
|
|
|
|
null;
|
|
|
|
}
|
|
|
|
catch (Exception e)
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
_logger.LogCritical(e, $"Exception while checking meta {filename}");
|
2022-05-28 22:53:52 +00:00
|
|
|
return true;
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
2021-10-23 16:51:17 +00:00
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
var toFind = await _settings.Downloads.EnumerateFiles()
|
|
|
|
.Where(f => f.Extension != Ext.Meta)
|
|
|
|
.PMapAll(CompilerLimiter, async f => await HasInvalidMeta(f) ? f : default)
|
|
|
|
.Where(f => f != default)
|
|
|
|
.Where(f => f.FileExists())
|
|
|
|
.ToList();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2022-01-27 07:20:48 +00:00
|
|
|
NextStep("Initializing", "InferMetas", toFind.Count);
|
2021-10-23 16:51:17 +00:00
|
|
|
if (toFind.Count == 0) return;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
_logger.LogInformation("Attempting to infer {count} metas from the server.", toFind.Count);
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
await toFind.PDoAll(async f =>
|
|
|
|
{
|
|
|
|
UpdateProgress(1);
|
|
|
|
var vf = _vfs.Index.ByRootPath[f];
|
|
|
|
|
|
|
|
var archives = await _wjClient.GetArchivesForHash(vf.Hash);
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
Archive? a = null;
|
|
|
|
foreach (var archive in archives)
|
|
|
|
if (await _dispatcher.Verify(archive, token))
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
a = archive;
|
|
|
|
break;
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
if (a == null)
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
await vf.AbsoluteName.WithExtension(Ext.Meta).WriteAllLinesAsync(
|
|
|
|
new[]
|
|
|
|
{
|
|
|
|
"[General]",
|
|
|
|
"unknownArchive=true"
|
|
|
|
}, token);
|
|
|
|
_logger.LogWarning("Could not infer meta for {archive} {hash}", f, vf.Hash);
|
2021-09-27 12:42:46 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
_logger.LogInformation($"Inferred .meta for {vf.FullPath.FileName}, writing to disk");
|
|
|
|
await vf.AbsoluteName.WithExtension(Ext.Meta)
|
|
|
|
.WriteAllTextAsync(_dispatcher.MetaIniSection(a), token);
|
|
|
|
});
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
protected async Task ExportModList(CancellationToken token)
|
|
|
|
{
|
2022-01-27 07:20:48 +00:00
|
|
|
NextStep("Finalizing", "Exporting Modlist");
|
2021-10-23 16:51:17 +00:00
|
|
|
_logger.LogInformation("Exporting ModList to {location}", _settings.OutputFile);
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
// Modify readme and ModList image to relative paths if they exist
|
|
|
|
if (_settings.ModListImage.FileExists()) ModList.Image = (RelativePath) "modlist-image.png";
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
await using (var of = _stagingFolder.Combine("modlist").Open(FileMode.Create, FileAccess.Write))
|
|
|
|
{
|
|
|
|
await _dtos.Serialize(ModList, of);
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
await _wjClient.SendModListDefinition(ModList);
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
_settings.OutputFile.Delete();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
await using (var fs = _settings.OutputFile.Open(FileMode.Create, FileAccess.Write))
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
using var za = new ZipArchive(fs, ZipArchiveMode.Create);
|
|
|
|
|
|
|
|
foreach (var f in _stagingFolder.EnumerateFiles())
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
var ze = za.CreateEntry((string) f.FileName);
|
|
|
|
await using var os = ze.Open();
|
|
|
|
await using var ins = f.Open(FileMode.Open);
|
|
|
|
await ins.CopyToAsync(os, token);
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
// Copy in modimage
|
|
|
|
if (_settings.ModListImage.FileExists())
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
var ze = za.CreateEntry((string) ModList.Image);
|
|
|
|
await using var os = ze.Open();
|
|
|
|
await using var ins = _settings.ModListImage.Open(FileMode.Open);
|
|
|
|
await ins.CopyToAsync(os, token);
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
_logger.LogInformation("Exporting Modlist metadata");
|
|
|
|
var outputFileHash = await _hashCache.FileHashCachedAsync(_settings.OutputFile, token);
|
|
|
|
if (outputFileHash == default)
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
_logger.LogCritical("Unable to hash Modlist Output File");
|
|
|
|
return;
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
var metadata = new DownloadMetadata
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
Size = _settings.OutputFile.Size(),
|
|
|
|
Hash = outputFileHash,
|
|
|
|
NumberOfArchives = ModList.Archives.Length,
|
|
|
|
SizeOfArchives = ModList.Archives.Sum(a => a.Size),
|
|
|
|
NumberOfInstalledFiles = ModList.Directives.Length,
|
|
|
|
SizeOfInstalledFiles = ModList.Directives.Sum(a => a.Size)
|
|
|
|
};
|
|
|
|
await using var metajson = _settings.OutputFile.WithExtension(new Extension(".meta.json"))
|
|
|
|
.Open(FileMode.Create, FileAccess.Write);
|
|
|
|
await _dtos.Serialize(metadata, metajson);
|
|
|
|
|
|
|
|
_logger.LogInformation("Removing ModList staging folder");
|
|
|
|
_stagingFolder.DeleteDirectory();
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Fills in the Patch fields in files that require them
|
|
|
|
/// </summary>
|
|
|
|
protected async Task BuildPatches(CancellationToken token)
|
|
|
|
{
|
|
|
|
var toBuild = InstallDirectives.OfType<PatchedFromArchive>()
|
|
|
|
.Where(p => _patchOptions.GetValueOrDefault(p, Array.Empty<VirtualFile>()).Length > 0)
|
|
|
|
.SelectMany(p => _patchOptions[p].Select(c => new PatchedFromArchive
|
|
|
|
{
|
|
|
|
To = p.To,
|
|
|
|
Hash = p.Hash,
|
|
|
|
ArchiveHashPath = c.MakeRelativePaths(),
|
|
|
|
Size = p.Size
|
|
|
|
}))
|
|
|
|
.ToArray();
|
|
|
|
|
2022-01-27 07:20:48 +00:00
|
|
|
NextStep("Compiling","Generating Patches", toBuild.Length);
|
2021-10-23 16:51:17 +00:00
|
|
|
if (toBuild.Length == 0) return;
|
|
|
|
|
|
|
|
// Extract all the source files
|
|
|
|
var indexed = toBuild.GroupBy(f => _vfs.Index.FileForArchiveHashPath(f.ArchiveHashPath))
|
|
|
|
.ToDictionary(f => f.Key);
|
|
|
|
await _vfs.Extract(indexed.Keys.ToHashSet(),
|
|
|
|
async (vf, sf) =>
|
2021-10-15 04:20:27 +00:00
|
|
|
{
|
|
|
|
UpdateProgress(1);
|
2021-10-23 16:51:17 +00:00
|
|
|
// For each, extract the destination
|
|
|
|
var matches = indexed[vf];
|
|
|
|
foreach (var match in matches)
|
|
|
|
{
|
|
|
|
var destFile = FindDestFile(match.To);
|
|
|
|
// Build the patch
|
|
|
|
await _vfs.Extract(new[] {destFile}.ToHashSet(),
|
|
|
|
async (destvf, destsfn) =>
|
|
|
|
{
|
|
|
|
_logger.LogInformation("Patching {from} {to}", destFile, match.To);
|
|
|
|
await using var srcStream = await sf.GetStream();
|
|
|
|
await using var destStream = await destsfn.GetStream();
|
2022-05-27 05:41:11 +00:00
|
|
|
using var _ = await CompilerLimiter.Begin($"Patching {match.To}", 100, token);
|
2021-10-23 16:51:17 +00:00
|
|
|
var patchSize =
|
|
|
|
await _patchCache.CreatePatch(srcStream, vf.Hash, destStream, destvf.Hash);
|
|
|
|
_logger.LogInformation("Patch size {patchSize} for {to}", patchSize, match.To);
|
|
|
|
}, token);
|
|
|
|
}
|
|
|
|
}, token);
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
// Load in the patches
|
|
|
|
await InstallDirectives.OfType<PatchedFromArchive>()
|
|
|
|
.Where(p => p.PatchID == default)
|
|
|
|
.PDoAll(CompilerLimiter, async pfa =>
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
var patches = await _patchOptions[pfa]
|
|
|
|
.SelectAsync(async c => (await _patchCache.GetPatch(c.Hash, pfa.Hash), c))
|
|
|
|
.ToList();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
// Pick the best patch
|
|
|
|
if (patches.All(p => p.Item1 != null))
|
|
|
|
{
|
|
|
|
var (patch, file) = IncludePatches.PickPatch(this, patches);
|
|
|
|
pfa.FromHash = file.Hash;
|
|
|
|
pfa.ArchiveHashPath = file.MakeRelativePaths();
|
|
|
|
pfa.PatchID = await IncludeFile(await patch.cache.GetData(patch));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
var firstFailedPatch =
|
|
|
|
InstallDirectives.OfType<PatchedFromArchive>().FirstOrDefault(f => f.PatchID == default);
|
|
|
|
if (firstFailedPatch != null)
|
|
|
|
{
|
|
|
|
_logger.LogCritical("Missing data from failed patch, starting data dump");
|
|
|
|
_logger.LogCritical("Dest File: {to}", firstFailedPatch.To);
|
|
|
|
_logger.LogCritical("Options ({count}):", _patchOptions[firstFailedPatch].Length);
|
|
|
|
foreach (var choice in _patchOptions[firstFailedPatch]) _logger.LogCritical(" {path}", choice.FullPath);
|
|
|
|
|
|
|
|
_logger.LogCritical(
|
|
|
|
"Missing patches after generation, this should not happen. First failure: {path}", firstFailedPatch.To);
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
2021-10-23 16:51:17 +00:00
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
private VirtualFile FindDestFile(RelativePath to)
|
|
|
|
{
|
|
|
|
var abs = to.RelativeTo(_settings.Source);
|
|
|
|
if (abs.FileExists()) return _vfs.Index.ByRootPath[abs];
|
|
|
|
|
|
|
|
if (to.InFolder(Consts.BSACreationDir))
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
var bsaId = (RelativePath) ((string) to).Split('\\')[1];
|
|
|
|
var bsa = InstallDirectives.OfType<CreateBSA>().First(b => b.TempID == bsaId);
|
|
|
|
var find = (RelativePath) Path.Combine(((string) to).Split('\\').Skip(2).ToArray());
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
return _vfs.Index.ByRootPath[_settings.Source.Combine(bsa.To)].Children.First(c => c.RelativeName == find);
|
|
|
|
}
|
2021-10-13 03:59:54 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
throw new ArgumentException($"Couldn't load data for {to}");
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public async Task GenerateManifest()
|
|
|
|
{
|
2022-01-27 07:20:48 +00:00
|
|
|
NextStep("Finalizing", "Generating Manifest");
|
2021-10-23 16:51:17 +00:00
|
|
|
var manifest = new Manifest(ModList);
|
|
|
|
await using var of = _settings.OutputFile.Open(FileMode.Create, FileAccess.Write);
|
|
|
|
await _dtos.Serialize(manifest, of);
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public async Task GatherArchives()
|
|
|
|
{
|
2022-01-27 07:20:48 +00:00
|
|
|
NextStep("Building", "Gathering Archives");
|
2021-10-23 16:51:17 +00:00
|
|
|
_logger.LogInformation("Building a list of archives based on the files required");
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
var hashes = InstallDirectives.OfType<FromArchive>()
|
|
|
|
.Select(a => a.ArchiveHashPath.Hash)
|
|
|
|
.Distinct();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
var archives = IndexedArchives.OrderByDescending(f => f.File.LastModified)
|
|
|
|
.GroupBy(f => f.File.Hash)
|
|
|
|
.ToDictionary(f => f.Key, f => f.First());
|
|
|
|
|
|
|
|
SelectedArchives.Clear();
|
|
|
|
SelectedArchives.AddRange(await hashes.PMapAll(CompilerLimiter, hash =>
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
UpdateProgress(1);
|
|
|
|
return ResolveArchive(hash, archives);
|
|
|
|
}).ToList());
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public async Task<Archive> ResolveArchive(Hash hash, IDictionary<Hash, IndexedArchive> archives)
|
|
|
|
{
|
|
|
|
if (archives.TryGetValue(hash, out var found)) return await ResolveArchive(found);
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
throw new ArgumentException($"No match found for Archive sha: {hash.ToBase64()} this shouldn't happen");
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public async Task<Archive?> ResolveArchive(IndexedArchive archive)
|
|
|
|
{
|
|
|
|
if (archive.State == null && archive.IniData == null)
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
_logger.LogWarning(
|
|
|
|
"No download metadata found for {archive}, please use MO2 to query info or add a .meta file and try again.",
|
|
|
|
archive.Name);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
IDownloadState? state;
|
|
|
|
if (archive.State == null)
|
|
|
|
{
|
|
|
|
state = await _dispatcher.ResolveArchive(archive.IniData!["General"]
|
|
|
|
.ToDictionary(d => d.KeyName, d => d.Value));
|
|
|
|
|
|
|
|
if (state == null)
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
_logger.LogWarning("{archive} could not be handled by any of the downloaders", archive.Name);
|
|
|
|
return null;
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
}
|
2021-10-23 16:51:17 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
state = archive.State;
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
var result = new Archive
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
State = state!,
|
|
|
|
Name = archive.Name ?? "",
|
|
|
|
Hash = archive.File.Hash,
|
|
|
|
Size = archive.File.Size
|
|
|
|
};
|
|
|
|
|
|
|
|
var token = new CancellationTokenSource();
|
|
|
|
token.CancelAfter(_settings.MaxVerificationTime);
|
|
|
|
if (!await _dispatcher.Verify(result, token.Token))
|
|
|
|
_logger.LogWarning(
|
|
|
|
"Unable to resolve link for {Archive}. If this is hosted on the Nexus the file may have been removed.",
|
|
|
|
result.State!.PrimaryKeyString);
|
|
|
|
|
|
|
|
result.Meta = "[General]\n" + string.Join("\n", _dispatcher.MetaIni(result));
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async Task<Directive> RunStack(IEnumerable<ICompilationStep> stack, RawSourceFile source)
|
|
|
|
{
|
|
|
|
foreach (var step in stack)
|
|
|
|
{
|
|
|
|
var result = await step.Run(source);
|
|
|
|
if (result != null) return result;
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
throw new InvalidDataException("Data fell out of the compilation stack");
|
|
|
|
}
|
|
|
|
|
|
|
|
public abstract IEnumerable<ICompilationStep> GetStack();
|
|
|
|
public abstract IEnumerable<ICompilationStep> MakeStack();
|
|
|
|
|
|
|
|
public void PrintNoMatches(ICollection<NoMatch> noMatches)
|
|
|
|
{
|
|
|
|
const int max = 10;
|
|
|
|
if (noMatches.Count > 0)
|
|
|
|
foreach (var file in noMatches)
|
|
|
|
_logger.LogWarning(" {fileTo} - {fileReason}", file.To, file.Reason);
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
protected async Task InlineFiles(CancellationToken token)
|
|
|
|
{
|
|
|
|
var grouped = ModList.Directives.OfType<InlineFile>()
|
|
|
|
.Where(f => f.SourceDataID == default)
|
|
|
|
.GroupBy(f => _sourceFileLinks[f].File)
|
|
|
|
.ToDictionary(k => k.Key);
|
|
|
|
|
2022-01-27 07:20:48 +00:00
|
|
|
NextStep("Building", "Inlining Files");
|
2021-10-23 16:51:17 +00:00
|
|
|
if (grouped.Count == 0) return;
|
|
|
|
await _vfs.Extract(grouped.Keys.ToHashSet(), async (vf, sfn) =>
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
UpdateProgress(1);
|
|
|
|
await using var stream = await sfn.GetStream();
|
|
|
|
var id = await IncludeFile(stream, token);
|
|
|
|
foreach (var file in grouped[vf]) file.SourceDataID = id;
|
|
|
|
}, token);
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
|
|
|
public bool CheckForNoMatchExit(ICollection<NoMatch> noMatches)
|
|
|
|
{
|
|
|
|
if (noMatches.Count > 0)
|
|
|
|
{
|
|
|
|
_logger.LogCritical("Exiting due to no way to compile these files");
|
|
|
|
return true;
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
2021-10-23 16:51:17 +00:00
|
|
|
|
|
|
|
return false;
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
}
|