mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Implement BSA install optimization
This commit is contained in:
parent
83cf5a3436
commit
f93f52b1c9
@ -65,7 +65,6 @@ public class ResourceMonitor : IDisposable
|
|||||||
var used = new HashSet<ulong>();
|
var used = new HashSet<ulong>();
|
||||||
foreach (var resource in _resources)
|
foreach (var resource in _resources)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Resource {Name}: {Jobs}", resource.Name, resource.Jobs.Count());
|
|
||||||
foreach (var job in resource.Jobs)
|
foreach (var job in resource.Jobs)
|
||||||
{
|
{
|
||||||
used.Add(job.ID);
|
used.Add(job.ID);
|
||||||
|
@ -39,12 +39,13 @@ public abstract class AInstaller<T>
|
|||||||
where T : AInstaller<T>
|
where T : AInstaller<T>
|
||||||
{
|
{
|
||||||
private const int _limitMS = 100;
|
private const int _limitMS = 100;
|
||||||
|
public static RelativePath BSACreationDir = "TEMP_BSA_FILES".ToRelativePath();
|
||||||
private static readonly Regex NoDeleteRegex = new(@"(?i)[\\\/]\[NoDelete\]", RegexOptions.Compiled);
|
private static readonly Regex NoDeleteRegex = new(@"(?i)[\\\/]\[NoDelete\]", RegexOptions.Compiled);
|
||||||
|
|
||||||
protected readonly InstallerConfiguration _configuration;
|
protected readonly InstallerConfiguration _configuration;
|
||||||
protected readonly DownloadDispatcher _downloadDispatcher;
|
protected readonly DownloadDispatcher _downloadDispatcher;
|
||||||
private readonly FileExtractor.FileExtractor _extractor;
|
private readonly FileExtractor.FileExtractor _extractor;
|
||||||
private readonly FileHashCache _fileHashCache;
|
protected readonly FileHashCache FileHashCache;
|
||||||
protected readonly IGameLocator _gameLocator;
|
protected readonly IGameLocator _gameLocator;
|
||||||
private readonly DTOSerializer _jsonSerializer;
|
private readonly DTOSerializer _jsonSerializer;
|
||||||
protected readonly ILogger<T> _logger;
|
protected readonly ILogger<T> _logger;
|
||||||
@ -81,7 +82,7 @@ public abstract class AInstaller<T>
|
|||||||
_extractor = extractor;
|
_extractor = extractor;
|
||||||
_jsonSerializer = jsonSerializer;
|
_jsonSerializer = jsonSerializer;
|
||||||
_vfs = vfs;
|
_vfs = vfs;
|
||||||
_fileHashCache = fileHashCache;
|
FileHashCache = fileHashCache;
|
||||||
_downloadDispatcher = downloadDispatcher;
|
_downloadDispatcher = downloadDispatcher;
|
||||||
_parallelOptions = parallelOptions;
|
_parallelOptions = parallelOptions;
|
||||||
_gameLocator = gameLocator;
|
_gameLocator = gameLocator;
|
||||||
@ -114,7 +115,9 @@ public abstract class AInstaller<T>
|
|||||||
{
|
{
|
||||||
Interlocked.Add(ref _currentStepProgress, stepProgress);
|
Interlocked.Add(ref _currentStepProgress, stepProgress);
|
||||||
|
|
||||||
OnStatusUpdate?.Invoke(new StatusUpdate(_statusCategory, _statusText, Percent.FactoryPutInRange(_currentStep, MaxSteps), Percent.FactoryPutInRange(_currentStepProgress, MaxStepProgress)));
|
OnStatusUpdate?.Invoke(new StatusUpdate(_statusCategory, _statusText,
|
||||||
|
Percent.FactoryPutInRange(_currentStep, MaxSteps),
|
||||||
|
Percent.FactoryPutInRange(_currentStepProgress, MaxStepProgress)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Task<bool> Begin(CancellationToken token);
|
public abstract Task<bool> Begin(CancellationToken token);
|
||||||
@ -124,7 +127,7 @@ public abstract class AInstaller<T>
|
|||||||
ExtractedModlistFolder = _manager.CreateFolder();
|
ExtractedModlistFolder = _manager.CreateFolder();
|
||||||
await using var stream = _configuration.ModlistArchive.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
await using var stream = _configuration.ModlistArchive.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
using var archive = new ZipArchive(stream, ZipArchiveMode.Read);
|
using var archive = new ZipArchive(stream, ZipArchiveMode.Read);
|
||||||
NextStep(Consts.StepPreparing,"Extracting Modlist", archive.Entries.Count);
|
NextStep(Consts.StepPreparing, "Extracting Modlist", archive.Entries.Count);
|
||||||
foreach (var entry in archive.Entries)
|
foreach (var entry in archive.Entries)
|
||||||
{
|
{
|
||||||
var path = entry.FullName.ToRelativePath().RelativeTo(ExtractedModlistFolder);
|
var path = entry.FullName.ToRelativePath().RelativeTo(ExtractedModlistFolder);
|
||||||
@ -186,7 +189,7 @@ public abstract class AInstaller<T>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected async Task PrimeVFS()
|
protected async Task PrimeVFS()
|
||||||
{
|
{
|
||||||
NextStep(Consts.StepPreparing,"Priming VFS", 0);
|
NextStep(Consts.StepPreparing, "Priming VFS", 0);
|
||||||
_vfs.AddKnown(_configuration.ModList.Directives.OfType<FromArchive>().Select(d => d.ArchiveHashPath),
|
_vfs.AddKnown(_configuration.ModList.Directives.OfType<FromArchive>().Select(d => d.ArchiveHashPath),
|
||||||
HashedArchives);
|
HashedArchives);
|
||||||
await _vfs.BackfillMissing();
|
await _vfs.BackfillMissing();
|
||||||
@ -218,7 +221,8 @@ public abstract class AInstaller<T>
|
|||||||
await _vfs.Extract(grouped.Keys.ToHashSet(), async (vf, sf) =>
|
await _vfs.Extract(grouped.Keys.ToHashSet(), async (vf, sf) =>
|
||||||
{
|
{
|
||||||
var directives = grouped[vf];
|
var directives = grouped[vf];
|
||||||
using var job = await _limiter.Begin($"Installing files from {vf.Name}", directives.Sum(f => f.VF.Size), token);
|
using var job = await _limiter.Begin($"Installing files from {vf.Name}", directives.Sum(f => f.VF.Size),
|
||||||
|
token);
|
||||||
foreach (var directive in directives)
|
foreach (var directive in directives)
|
||||||
{
|
{
|
||||||
var file = directive.Directive;
|
var file = directive.Directive;
|
||||||
@ -268,7 +272,7 @@ public abstract class AInstaller<T>
|
|||||||
throw new Exception($"No handler for {directive}");
|
throw new Exception($"No handler for {directive}");
|
||||||
}
|
}
|
||||||
|
|
||||||
await job.Report((int)directive.VF.Size, token);
|
await job.Report((int) directive.VF.Size, token);
|
||||||
}
|
}
|
||||||
}, token);
|
}, token);
|
||||||
}
|
}
|
||||||
@ -357,7 +361,8 @@ public abstract class AInstaller<T>
|
|||||||
|
|
||||||
if (hash != archive.Hash)
|
if (hash != archive.Hash)
|
||||||
{
|
{
|
||||||
_logger.LogError("Downloaded hash {Downloaded} does not match expected hash: {Expected}", hash, archive.Hash);
|
_logger.LogError("Downloaded hash {Downloaded} does not match expected hash: {Expected}", hash,
|
||||||
|
archive.Hash);
|
||||||
if (destination!.Value.FileExists())
|
if (destination!.Value.FileExists())
|
||||||
{
|
{
|
||||||
destination!.Value.Delete();
|
destination!.Value.Delete();
|
||||||
@ -367,7 +372,7 @@ public abstract class AInstaller<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hash != default)
|
if (hash != default)
|
||||||
_fileHashCache.FileHashWriteCache(destination.Value, hash);
|
FileHashCache.FileHashWriteCache(destination.Value, hash);
|
||||||
|
|
||||||
if (result == DownloadResult.Update)
|
if (result == DownloadResult.Update)
|
||||||
await destination.Value.MoveToAsync(destination.Value.Parent.Combine(archive.Hash.ToHex()), true,
|
await destination.Value.MoveToAsync(destination.Value.Parent.Combine(archive.Hash.ToHex()), true,
|
||||||
@ -406,7 +411,7 @@ public abstract class AInstaller<T>
|
|||||||
.PMapAll(async e =>
|
.PMapAll(async e =>
|
||||||
{
|
{
|
||||||
UpdateProgress(1);
|
UpdateProgress(1);
|
||||||
return (await _fileHashCache.FileHashCachedAsync(e, token), e);
|
return (await FileHashCache.FileHashCachedAsync(e, token), e);
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@ -427,19 +432,46 @@ public abstract class AInstaller<T>
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Optimizing ModList directives");
|
_logger.LogInformation("Optimizing ModList directives");
|
||||||
|
|
||||||
|
|
||||||
var indexed = ModList.Directives.ToDictionary(d => d.To);
|
var indexed = ModList.Directives.ToDictionary(d => d.To);
|
||||||
|
|
||||||
|
var bsasToBuild = await ModList.Directives
|
||||||
|
.OfType<CreateBSA>()
|
||||||
|
.PMapAll(async b =>
|
||||||
|
{
|
||||||
|
var file = _configuration.Install.Combine(b.To);
|
||||||
|
if (!file.FileExists())
|
||||||
|
return (true, b);
|
||||||
|
return (b.Hash != await FileHashCache.FileHashCachedAsync(file, token), b);
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var bsasToNotBuild = bsasToBuild
|
||||||
|
.Where(b => b.Item1 == false).Select(t => t.b.TempID).ToHashSet();
|
||||||
|
|
||||||
|
var bsaPathsToNotBuild = bsasToBuild
|
||||||
|
.Where(b => b.Item1 == false).Select(t => t.b.To.RelativeTo(_configuration.Install))
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
indexed = indexed.Values
|
||||||
|
.Where(d =>
|
||||||
|
{
|
||||||
|
return d switch
|
||||||
|
{
|
||||||
|
CreateBSA bsa => !bsasToNotBuild.Contains(bsa.TempID),
|
||||||
|
FromArchive a when a.To.StartsWith($"{BSACreationDir}") => !bsasToNotBuild.Any(b =>
|
||||||
|
a.To.RelativeTo(_configuration.Install).InFolder(_configuration.Install.Combine(BSACreationDir, b))),
|
||||||
|
_ => true
|
||||||
|
};
|
||||||
|
}).ToDictionary(d => d.To);
|
||||||
|
|
||||||
|
|
||||||
var profileFolder = _configuration.Install.Combine("profiles");
|
var profileFolder = _configuration.Install.Combine("profiles");
|
||||||
var savePath = (RelativePath) "saves";
|
var savePath = (RelativePath) "saves";
|
||||||
|
|
||||||
var existingFiles = _configuration.Install.EnumerateFiles().ToList();
|
NextStep(Consts.StepPreparing, "Looking for files to delete", 0);
|
||||||
NextStep(Consts.StepPreparing, "Looking for files to delete", existingFiles.Count);
|
await _configuration.Install.EnumerateFiles()
|
||||||
await existingFiles
|
|
||||||
.PDoAll(async f =>
|
.PDoAll(async f =>
|
||||||
{
|
{
|
||||||
UpdateProgress(1);
|
|
||||||
var relativeTo = f.RelativeTo(_configuration.Install);
|
var relativeTo = f.RelativeTo(_configuration.Install);
|
||||||
if (indexed.ContainsKey(relativeTo) || f.InFolder(_configuration.Downloads))
|
if (indexed.ContainsKey(relativeTo) || f.InFolder(_configuration.Downloads))
|
||||||
return;
|
return;
|
||||||
@ -449,17 +481,18 @@ public abstract class AInstaller<T>
|
|||||||
if (NoDeleteRegex.IsMatch(f.ToString()))
|
if (NoDeleteRegex.IsMatch(f.ToString()))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_logger.LogTrace("Deleting {relativeTo} it's not part of this ModList", relativeTo);
|
if (bsaPathsToNotBuild.Contains(f))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_logger.LogInformation("Deleting {RelativePath} it's not part of this ModList", relativeTo);
|
||||||
f.Delete();
|
f.Delete();
|
||||||
});
|
});
|
||||||
|
|
||||||
_logger.LogInformation("Cleaning empty folders");
|
_logger.LogInformation("Cleaning empty folders");
|
||||||
NextStep(Consts.StepPreparing, "Cleaning empty folders", indexed.Keys.Count);
|
var expectedFolders = indexed.Keys
|
||||||
var expectedFolders = (indexed.Keys
|
|
||||||
.Select(f => f.RelativeTo(_configuration.Install))
|
.Select(f => f.RelativeTo(_configuration.Install))
|
||||||
// We ignore the last part of the path, so we need a dummy file name
|
// We ignore the last part of the path, so we need a dummy file name
|
||||||
.Append(_configuration.Downloads.Combine("_"))
|
.Append(_configuration.Downloads.Combine("_"))
|
||||||
.OnEach(_ => UpdateProgress(1))
|
|
||||||
.Where(f => f.InFolder(_configuration.Install))
|
.Where(f => f.InFolder(_configuration.Install))
|
||||||
.SelectMany(path =>
|
.SelectMany(path =>
|
||||||
{
|
{
|
||||||
@ -468,28 +501,30 @@ public abstract class AInstaller<T>
|
|||||||
var split = ((string) path.RelativeTo(_configuration.Install)).Split('\\');
|
var split = ((string) path.RelativeTo(_configuration.Install)).Split('\\');
|
||||||
return Enumerable.Range(1, split.Length - 1).Select(t => string.Join("\\", split.Take(t)));
|
return Enumerable.Range(1, split.Length - 1).Select(t => string.Join("\\", split.Take(t)));
|
||||||
})
|
})
|
||||||
.ToList())
|
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.Select(p => _configuration.Install.Combine(p))
|
.Select(p => _configuration.Install.Combine(p))
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var toDelete = _configuration.Install.EnumerateDirectories()
|
var toDelete = _configuration.Install.EnumerateDirectories(true)
|
||||||
.Where(p => !expectedFolders.Contains(p))
|
.Where(p => !expectedFolders.Contains(p))
|
||||||
.OrderByDescending(p => p.ToString().Length)
|
.OrderByDescending(p => p.ToString().Length)
|
||||||
.ToList();
|
.ToList();
|
||||||
foreach (var dir in toDelete) dir.DeleteDirectory(true);
|
foreach (var dir in toDelete)
|
||||||
|
{
|
||||||
|
dir.DeleteDirectory(dontDeleteIfNotEmpty: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
// ignored because it's not worth throwing a fit over
|
// ignored because it's not worth throwing a fit over
|
||||||
_logger.LogWarning("Error when trying to clean empty folders. This doesn't really matter.");
|
_logger.LogInformation("Error when trying to clean empty folders. This doesn't really matter.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingfiles = _configuration.Install.EnumerateFiles().ToHashSet();
|
var existingfiles = _configuration.Install.EnumerateFiles().ToHashSet();
|
||||||
|
|
||||||
NextStep(Consts.StepPreparing, "Removing redundant directives", indexed.Count);
|
NextStep(Consts.StepPreparing, "Looking for unmodified files", 0);
|
||||||
await indexed.Values.PMapAll<Directive, Directive?>(async d =>
|
await indexed.Values.PMapAll<Directive, Directive?>(async d =>
|
||||||
{
|
{
|
||||||
// Bit backwards, but we want to return null for
|
// Bit backwards, but we want to return null for
|
||||||
@ -498,17 +533,18 @@ public abstract class AInstaller<T>
|
|||||||
var path = _configuration.Install.Combine(d.To);
|
var path = _configuration.Install.Combine(d.To);
|
||||||
if (!existingfiles.Contains(path)) return null;
|
if (!existingfiles.Contains(path)) return null;
|
||||||
|
|
||||||
return await _fileHashCache.FileHashCachedAsync(path, token) == d.Hash ? d : null;
|
return await FileHashCache.FileHashCachedAsync(path, token) == d.Hash ? d : null;
|
||||||
})
|
})
|
||||||
.Do(d =>
|
.Do(d =>
|
||||||
{
|
{
|
||||||
UpdateProgress(1);
|
if (d != null)
|
||||||
if (d != null) indexed.Remove(d.To);
|
{
|
||||||
|
indexed.Remove(d.To);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_logger.LogInformation("Optimized {optimized} directives to {indexed} required", ModList.Directives.Length,
|
NextStep(Consts.StepPreparing, "Updating ModList", 0);
|
||||||
indexed.Count);
|
_logger.LogInformation("Optimized {From} directives to {To} required", ModList.Directives.Length, indexed.Count);
|
||||||
NextStep(Consts.StepPreparing, "Finalizing modlist optimization", 0);
|
|
||||||
var requiredArchives = indexed.Values.OfType<FromArchive>()
|
var requiredArchives = indexed.Values.OfType<FromArchive>()
|
||||||
.GroupBy(d => d.ArchiveHashPath.Hash)
|
.GroupBy(d => d.ArchiveHashPath.Hash)
|
||||||
.Select(d => d.Key)
|
.Select(d => d.Key)
|
||||||
|
@ -33,7 +33,6 @@ namespace Wabbajack.Installer;
|
|||||||
|
|
||||||
public class StandardInstaller : AInstaller<StandardInstaller>
|
public class StandardInstaller : AInstaller<StandardInstaller>
|
||||||
{
|
{
|
||||||
public static RelativePath BSACreationDir = "TEMP_BSA_FILES".ToRelativePath();
|
|
||||||
|
|
||||||
public StandardInstaller(ILogger<StandardInstaller> logger,
|
public StandardInstaller(ILogger<StandardInstaller> logger,
|
||||||
InstallerConfiguration config,
|
InstallerConfiguration config,
|
||||||
@ -260,11 +259,14 @@ public class StandardInstaller : AInstaller<StandardInstaller>
|
|||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
_logger.LogInformation("Writing {bsaTo}", bsa.To);
|
_logger.LogInformation("Writing {bsaTo}", bsa.To);
|
||||||
await using var outStream = _configuration.Install.Combine(bsa.To)
|
var outPath = _configuration.Install.Combine(bsa.To);
|
||||||
.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
await using var outStream = outPath.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
await a.Build(outStream, token);
|
await a.Build(outStream, token);
|
||||||
streams.Do(s => s.Dispose());
|
streams.Do(s => s.Dispose());
|
||||||
|
|
||||||
|
FileHashCache.FileHashWriteCache(outPath, bsa.Hash);
|
||||||
|
|
||||||
|
|
||||||
sourceDir.DeleteDirectory();
|
sourceDir.DeleteDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,4 +178,9 @@ public readonly struct RelativePath : IPath, IEquatable<RelativePath>, IComparab
|
|||||||
{
|
{
|
||||||
return Parts[^1].StartsWith(mrkinn);
|
return Parts[^1].StartsWith(mrkinn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool StartsWith(string s)
|
||||||
|
{
|
||||||
|
return ToString().StartsWith(s);
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user