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);
 | ||
|                 });
 | ||
|             }
 | ||
|         }
 | ||
|     }
 | ||
| }
 |