HashCalculator.GUI/Controller.cs
2022-01-25 13:35:00 +01:00

435 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TechnologyAssembler.Core.Assets;
using TechnologyAssembler.Core.IO;
using TechnologyAssembler.Core.Language.Providers;
namespace HashCalculator.GUI
{
[SuppressMessage("Design", "CA1001:具有可释放字段的类型应该是可释放的", Justification = "<挂起>")]
internal class Controller
{
public const string SelectedFileSystemRootPath = "/selected";
public const string ManifestExtension = ".manifest";
private ViewModel ViewModel { get; }
private BigFileSystemProvider? _currentBig = null;
private readonly HashSet<AssetEntry> _loadedAssets = new HashSet<AssetEntry>();
private Tuple<CancellationTokenSource, Task>? _lastXmlTask = null;
public Controller(ViewModel viewModel)
{
ViewModel = viewModel;
}
public async Task OnMainInputDecided(InputEntry selected)
{
try
{
_currentBig?.Dispose();
_currentBig = null;
ClearEntries();
ViewModel.ClearTraceText.Execute(null);
ViewModel.BigEntryInput.AllManifests = null;
ViewModel.IsXml = false;
switch (selected.Type)
{
case InputEntryType.BinaryFile:
ViewModel.StatusText = "请留意一下弹出的窗口(";
CopyableBox.ShowDialog("SAGE FastHash 计算器", token => CalculateBinaryHash(selected.Value, token));
ViewModel.StatusText = string.Empty;
return;
case InputEntryType.BigFile:
await LoadBig(selected.Value).ConfigureAwait(true);
return;
case InputEntryType.ManifestFile:
await LoadManifestFromFile(selected.Value).ConfigureAwait(true);
return;
case InputEntryType.XmlFile:
ViewModel.IsXml = true;
await LoadXml(selected.Value).ConfigureAwait(true);
return;
default:
throw new NotSupportedException();
}
}
catch (Exception error)
{
ViewModel.StatusText = "失败…";
CopyableBox.ShowDialog("SAGE FastHash 计算器 - 失败!", _ =>
{
return Task.FromResult($"在尝试加载 {selected.Type} `{selected.Value}` 时发生错误:\r\n{error}");
});
ViewModel.StatusText = string.Empty;
}
}
public Task<string?> RequestOpenFile(string title, params (string Extension, string? Description)[] filters)
{
return ViewModel.RequestOpenFile(title, filters);
}
private static Task<string> CalculateBinaryHash(string filePath, CancellationToken cancel)
{
return Task.Run(() =>
{
try
{
using var file = File.OpenRead(filePath);
var array = new byte[file.Length];
cancel.ThrowIfCancellationRequested();
var totalRead = 0;
const int ReadSize = 32 * 1024 * 1024;
while (totalRead < array.Length)
{
totalRead += file.Read(array, totalRead, Math.Min(array.Length - totalRead, ReadSize));
cancel.ThrowIfCancellationRequested();
}
var hashes = new Func<byte[], uint>[]
{
SageHash.CalculateBinaryHash,
SageHash.CalculateLauncherBinaryHash
}.AsParallel().Select(fn => fn(array)).ToArray();
return $"使用 SAGE FastHash 计算出的哈希值:{hashes[0]:x8} (十进制 {hashes[0]}\r\n"
+ "注意这是以大小写敏感模式计算出的哈希值与素材ID的哈希值转换成小写后计算的哈希一般是不一样的\r\n\r\n"
+ "使用 SAGE Launcher FastHash比如说 RA3.exe 用来校验更新补丁文件的哈希)计算出的哈希值:"
+ $"{hashes[1]:x8} (十进制 {hashes[1]}";
}
catch (Exception exception)
{
return exception.ToString();
}
});
}
private async Task LoadBig(string path)
{
ViewModel.StatusText = "正在尝试加载 big 文件…";
var bigViewModel = ViewModel.BigEntryInput;
bigViewModel.Text = string.Empty;
bigViewModel.AllManifests = await Task.Run(() =>
{
using var big = new BigFile(path);
_currentBig = new BigFileSystemProvider(SelectedFileSystemRootPath, big, null);
return FindManifests();
}).ConfigureAwait(true);
var first = bigViewModel.AllManifests.FirstOrDefault();
if (first != null)
{
var name = VirtualFileSystem.GetFileNameWithoutExtension(first.Value);
if (name.StartsWith("mod", StringComparison.OrdinalIgnoreCase)
|| name.StartsWith("mapmetadata", StringComparison.OrdinalIgnoreCase))
{
bigViewModel.SelectedItem = first;
await bigViewModel.SelectCommand.ExecuteTask(ViewModel).ConfigureAwait(true);
ViewModel.StatusText = "big 已加载完毕,并自动选了一个看起来比较合适的 manifest 文件(";
}
}
else
{
ViewModel.StatusText = "big 文件已加载完毕,请选择 big 文件里的 manifest 文件";
}
}
private async Task LoadManifestFromFile(string path)
{
await ExceptionWrapepr(async () =>
{
try
{
using var provider = new FileSystemProvider(SelectedFileSystemRootPath, null);
path = VirtualFileSystem.Combine(SelectedFileSystemRootPath, path);
await ProcessManifest(path).ConfigureAwait(true);
}
catch
{
ViewModel.StatusText = "失败…";
throw;
}
}, "SAGE FastHash 计算器 - Manifest 加载失败…", "Manifest 文件加载失败,也许,你选择的 manifest 文件并不是红警3使用的那种……\r\n").ConfigureAwait(true);
}
public async Task ProcessManifest(string path)
{
ClearEntries();
ViewModel.StatusText = "正在读取 manifest 文件…";
var entries = await Task.Run(() =>
{
if (path.EndsWith(ManifestExtension, StringComparison.OrdinalIgnoreCase))
{
path = path.Remove(path.Length - ManifestExtension.Length);
}
using var manifest = Manifest.Load(path, true);
var assets = from asset in manifest.Assets.Values
select new AssetEntry(asset);
return assets.ToArray();
}).ConfigureAwait(true);
AddEntries(entries);
ViewModel.StatusText = $"Manifest文件读取完毕加载了{_loadedAssets.Count}个素材";
}
public async Task CancelLoadingXml()
{
if (_lastXmlTask is null)
{
return;
}
var (tokenSource, task) = _lastXmlTask;
tokenSource.Cancel();
try
{
await task.ConfigureAwait(true);
}
catch (OperationCanceledException)
{
ViewModel.StatusText = "已经取消了加载";
}
}
private async Task LoadXml(string path)
{
await CancelLoadingXml().ConfigureAwait(true);
using var tokenSource = new CancellationTokenSource();
var task = LoadXmlInternal(path, tokenSource.Token);
_lastXmlTask = (tokenSource, task).ToTuple();
await ExceptionWrapepr(async () =>
{
try
{
ViewModel.IsLoadingXml = true;
await task.ConfigureAwait(true);
}
catch (OperationCanceledException)
{
ViewModel.StatusText = "已经取消了加载";
}
catch
{
ViewModel.StatusText = "失败…";
throw;
}
finally
{
ViewModel.IsLoadingXml = false;
_lastXmlTask = null;
}
}, "SAGE FastHash 计算器 - XML 加载失败…", "XML 文件加载失败,也许,你选择的 XML 文件并不是红警3使用的那种……\r\n").ConfigureAwait(true);
}
private async Task LoadXmlInternal(string path, CancellationToken token)
{
if (ModXml.LocateSdkFromRegistry() is not { } sdkRoot)
{
var studio = await RequestOpenFile("选择 Mod Studio", ("EALAModStudio.exe", null));
sdkRoot = Path.GetDirectoryName(studio);
}
var modXml = new ModXml(path, sdkRoot, token);
try
{
await FindCsf(modXml.BaseDirectory).ConfigureAwait(true);
}
catch (Exception exception)
{
TracerListener.WriteLine($"[ModXml] Failed to automatically find and load CSF: {exception}");
}
token.ThrowIfCancellationRequested();
var start = DateTimeOffset.UtcNow;
var lastMoment = start;
var previousCount = 0;
var preivousSpeed = 0.0;
await foreach (var entry in modXml.ProcessDocument())
{
var now = DateTimeOffset.Now;
var total = modXml.TotalFilesProcessed;
var time = now - start;
var speed = total / time.TotalSeconds;
var dt = (now - lastMoment).TotalSeconds;
double instantSpeed;
if (dt <= 0.1)
{
instantSpeed = preivousSpeed;
}
else
{
var dx = total - previousCount;
instantSpeed = dx / dt;
lastMoment = now;
previousCount = total;
preivousSpeed = instantSpeed;
}
ViewModel.StatusText = $"已读取{total}个文件,平均读取速度{speed,8:N2}文件/秒,瞬时速度{instantSpeed,8:N2}文件/秒";
AddEntry(entry);
}
UpdateEntries();
var totalTime = DateTimeOffset.UtcNow - start;
var fileSpeed = modXml.TotalFilesProcessed / totalTime.TotalSeconds;
var assetSpeed = modXml.TotalAssets / totalTime.TotalSeconds;
var statistics = $"耗时{totalTime:mm\\:ss},共读取{modXml.TotalFilesProcessed}个文件({fileSpeed,1:N2}文件/秒;{assetSpeed,1:N2}素材/秒)";
if (token.IsCancellationRequested)
{
ViewModel.StatusText = $"ModXml 的读取已被取消,共{statistics}";
return;
}
ViewModel.StatusText = $"ModXml 读取完毕~ {statistics}";
if (_loadedAssets.Count != modXml.TotalAssets)
{
var message = $"_loadedAssets.Count ({_loadedAssets.Count}) != modXml.TotalAssets ({modXml.TotalAssets})";
throw new InvalidDataException(message);
}
}
public static async Task LoadCsf(string filePath)
{
await ExceptionWrapepr(async () =>
{
switch (Path.GetExtension(filePath).ToUpperInvariant())
{
case ".BIG":
await LoadCsfFromBig(filePath).ConfigureAwait(true);
return;
case ".CSF":
await Task.Run(() =>
{
using var stream = File.OpenRead(filePath);
Translator.Provider = new CsfTranslationProvider(stream);
}).ConfigureAwait(true);
return;
}
}, "SAGE FastHash 计算器 - CSF 加载失败…", "CSF 加载失败:").ConfigureAwait(true);
}
private static Task LoadCsfFromBig(string bigPath)
{
return Task.Run(() =>
{
using var big = new BigFile(bigPath);
var files = big.GetFiles(string.Empty, "*.csf", VirtualSearchOptionType.AllDirectories);
var file =
files.FirstOrDefault(x => x.Equals("gamestrings.csf", StringComparison.OrdinalIgnoreCase))
?? files.FirstOrDefault();
if (file == null)
{
throw new FileNotFoundException();
}
using var stream = big.OpenStream(file);
Translator.Provider = new CsfTranslationProvider(stream);
});
}
private static Task FindCsf(DirectoryInfo baseDirectory)
{
return Task.Run(() =>
{
if (!baseDirectory.Exists)
{
throw new NotSupportedException();
}
var searchFrom = new List<DirectoryInfo> { baseDirectory };
if (baseDirectory.Parent is { } parent)
{
searchFrom.AddRange(parent.GetDirectories($"{baseDirectory.Name[0]}*"));
searchFrom.Add(parent);
}
var searchDirectories = new[] { "Additional", "Misc" }
.SelectMany(n => searchFrom.SelectMany(x => x.GetDirectories(n)))
.Concat(searchFrom);
var csfs = from directories in searchDirectories
from data in directories.GetDirectories("Data")
from csf in data.GetFiles("*.csf")
select csf;
var result =
csfs.FirstOrDefault(x => x.Name.Equals("gamestrings.csf", StringComparison.OrdinalIgnoreCase))
?? csfs.FirstOrDefault();
if (result == null)
{
throw new FileNotFoundException();
}
using var fileStream = result.OpenRead();
Translator.Provider = new CsfTranslationProvider(fileStream);
TracerListener.WriteLine($"[ModXml]: Automatically loaded csf from `{result.FullName}`");
});
}
public void UpdateEntries()
{
ViewModel.FilterCollection(_loadedAssets);
}
private void ClearEntries()
{
_loadedAssets.Clear();
UpdateEntries();
}
private void AddEntry(AssetEntry entry)
{
_loadedAssets.Add(entry);
ViewModel.AddNewItems(new[] { entry });
}
private void AddEntries(IEnumerable<AssetEntry> entries)
{
_loadedAssets.UnionWith(entries);
UpdateEntries();
}
public static IEnumerable<InputEntry> FindManifests()
{
var manifests = VirtualFileSystem.ListFiles(SelectedFileSystemRootPath, "*.manifest", VirtualSearchOptionType.AllDirectories);
var modManifests = from manifest in manifests
where FileNameStartsWith(manifest, "mod")
select manifest;
var globalDataManifests = from manifest in manifests
where FileNameStartsWith(manifest, "mapmetadata")
select manifest;
var firstManifests = modManifests.Concat(globalDataManifests);
var otherManifests = from manifest in manifests
where !firstManifests.Contains(manifest)
select manifest;
return from manifest in firstManifests.Concat(otherManifests)
select new InputEntry(InputEntryType.ManifestFile, manifest, manifest);
}
private static bool FileNameStartsWith(string path, string what)
{
return VirtualFileSystem.GetFileName(path).StartsWith(what, StringComparison.OrdinalIgnoreCase);
}
private static async Task ExceptionWrapepr(Func<Task> action, string errorTitle, string preErrorMessage)
{
try
{
await action().ConfigureAwait(true);
}
catch (Exception exception)
{
CopyableBox.ShowDialog(errorTitle, _ =>
{
return Task.FromResult(preErrorMessage + exception);
});
}
}
}
}