435 lines
18 KiB
C#
435 lines
18 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.Diagnostics.CodeAnalysis;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using TechnologyAssembler.Core.IO;
|
||
using TechnologyAssembler.Core.Assets;
|
||
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 void StartTrace(Action<Action> action)
|
||
{
|
||
TracerListener.StartListening(s => action(() => ViewModel.TraceText += s));
|
||
}
|
||
|
||
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
|
||
public async Task OnMainInputDecided(InputEntry selected)
|
||
{
|
||
try
|
||
{
|
||
_currentBig?.Dispose();
|
||
_currentBig = null;
|
||
ClearEntries();
|
||
ViewModel.TraceText = string.Empty;
|
||
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 = ViewModel.SuggestionString(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;
|
||
}
|
||
}
|
||
|
||
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
|
||
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 = "已经取消了加载";
|
||
}
|
||
}
|
||
|
||
[SuppressMessage("Reliability", "CA2000:丢失范围之前释放对象", Justification = "<挂起>")]
|
||
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);
|
||
}
|
||
|
||
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
|
||
private async Task LoadXmlInternal(string path, CancellationToken token)
|
||
{
|
||
var modXml = new ModXml(path, 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();
|
||
}
|
||
|
||
baseDirectory.GetFiles("*.csf");
|
||
var parent = baseDirectory.Parent;
|
||
var searchDirectories =
|
||
parent.GetDirectories($"{baseDirectory.Name[0]}*")
|
||
.Prepend(parent.Parent)
|
||
.Prepend(baseDirectory);
|
||
searchDirectories = searchDirectories
|
||
.Concat(searchDirectories.SelectMany(x => x.GetDirectories("Additional")));
|
||
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(Enumerable.Repeat(entry, 1));
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
|
||
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);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|