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 _loadedAssets = new HashSet(); private Tuple? _lastXmlTask = null; public Controller(ViewModel viewModel) { ViewModel = viewModel; } public void StartTrace(Action action) { TracerListener.StartListening(s => action(() => ViewModel.TraceText += s)); } 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; } } private static Task 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[] { 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) { 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(); } var searchFrom = new List { 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(Enumerable.Repeat(entry, 1)); } private void AddEntries(IEnumerable entries) { _loadedAssets.UnionWith(entries); UpdateEntries(); } public static IEnumerable 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 action, string errorTitle, string preErrorMessage) { try { await action().ConfigureAwait(true); } catch (Exception exception) { CopyableBox.ShowDialog(errorTitle, _ => { return Task.FromResult(preErrorMessage + exception); }); } } } }