From 97067a68a5f9f707c970a3f54d4d3203792ca914 Mon Sep 17 00:00:00 2001 From: lanyi Date: Tue, 31 Mar 2020 04:05:57 +0200 Subject: [PATCH] ui, and controller, ecc, ecc --- App.xaml | 162 ++++++++++- App.xaml.cs | 6 + AssetEntry.cs | 54 +++- Controller.cs | 227 +++++++++++++++ Converters/BooleanInvertConverter.cs | 2 + Converters/IsZeroToBooleanConverter.cs | 20 ++ Converters/MultiValueEqualityConverter.cs | 22 ++ .../ValidInputEntryTypeToBooleanConverter.cs | 2 + Converters/ValueConverterAggregate.cs | 2 + CopyableBox.xaml.cs | 20 +- DisposableDispatcherTimer.cs | 38 +++ FileSystemSuggestions.cs | 19 +- HashCalculator.GUI.csproj | 8 +- InputBar.xaml | 19 +- InputBar.xaml.cs | 24 +- MainWindow.xaml | 265 ++++++++--------- MainWindow.xaml.cs | 70 ++--- ModManifest.cs | 19 -- SageHash.cs | 9 + ViewModel.cs | 270 +++++++++--------- 20 files changed, 862 insertions(+), 396 deletions(-) create mode 100644 Controller.cs create mode 100644 Converters/IsZeroToBooleanConverter.cs create mode 100644 Converters/MultiValueEqualityConverter.cs create mode 100644 DisposableDispatcherTimer.cs delete mode 100644 ModManifest.cs diff --git a/App.xaml b/App.xaml index 77bec7c..f4fc817 100644 --- a/App.xaml +++ b/App.xaml @@ -1,9 +1,167 @@  - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/App.xaml.cs b/App.xaml.cs index d9ae60e..bc1c6e7 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -5,6 +5,7 @@ using System.Data; using System.Linq; using System.Threading.Tasks; using System.Windows; +using TechnologyAssembler; namespace HashCalculator.GUI { @@ -13,5 +14,10 @@ namespace HashCalculator.GUI /// public partial class App : Application { + public App() + { + var core = new TechnologyAssemblerCoreModule(); + core.Initialize(); + } } } diff --git a/AssetEntry.cs b/AssetEntry.cs index e504547..3a8bc0c 100644 --- a/AssetEntry.cs +++ b/AssetEntry.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Xml.Linq; +using TechnologyAssembler.Core.Assets; namespace HashCalculator.GUI { @@ -10,10 +12,10 @@ namespace HashCalculator.GUI public string Type { get; } public string Name { get; } public IEnumerable DisplayLabels { get; } - public uint Hash => SageHash.CalculateLowercaseHash(Name); + public uint InstanceId => SageHash.CalculateLowercaseHash(Name); - public string IdString => $"{Type}:{Name}"; - public string HashString => $"{Hash:0X} ({Hash})"; + public string NameString => $"{Type}:{Name}"; + public string InstanceIdString => $"{InstanceId:X8} ({InstanceId})"; public AssetEntry(XElement element) { @@ -38,6 +40,21 @@ namespace HashCalculator.GUI DisplayLabels = labels.Concat(transformLabels).ToArray(); } + public AssetEntry(Asset asset) + { + if(asset is null) + { + throw new ArgumentNullException($"{nameof(asset)} is null"); + } + Type = asset.TypeName; + Name = asset.InstanceName; + DisplayLabels = Enumerable.Empty(); + if(InstanceId != asset.InstanceId || SageHash.CalculateBinaryHash(Type) != asset.TypeId) + { + throw new InvalidDataException(); + } + } + public bool Equals(AssetEntry entry) { return this == entry; @@ -73,7 +90,7 @@ namespace HashCalculator.GUI return true; } - return a.Type == b.Type && a.Hash == b.Hash; + return a.Type == b.Type && a.InstanceId == b.InstanceId; } public static bool operator !=(AssetEntry a, AssetEntry b) @@ -84,7 +101,34 @@ namespace HashCalculator.GUI // override object.GetHashCode public override int GetHashCode() { - return HashCode.Combine(Type, Hash); + return HashCode.Combine(Type, InstanceId); + } + } + + internal class DisplayAssetEntry : IComparable + { + public string Name { get; } + public string InstanceId { get; } + + public DisplayAssetEntry(AssetEntry entry) + { + Name = entry.NameString; + InstanceId = entry.InstanceIdString; + } + + public int CompareTo(DisplayAssetEntry other) + { + return string.CompareOrdinal(Name, other.Name); + } + } + + internal class LocalizedDisplayAssetEntry : DisplayAssetEntry + { + public string LocalizedNames { get; } + + public LocalizedDisplayAssetEntry(AssetEntry entry) : base(entry) + { + LocalizedNames = entry.DisplayLabels.Aggregate((x, y) => $"{x} {y}"); } } } diff --git a/Controller.cs b/Controller.cs new file mode 100644 index 0000000..8f36351 --- /dev/null +++ b/Controller.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +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; + +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; + + public Controller(ViewModel viewModel) + { + ViewModel = viewModel; + } + + [SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")] + public async Task OnMainInputDecided(InputEntry selected) + { + _currentBig?.Dispose(); + _currentBig = null; + ViewModel.Entries.Clear(); + ViewModel.TraceText = string.Empty; + ViewModel.BigEntryInput.AllManifests = null; + + try + { + switch (selected.Type) + { + case InputEntryType.BinaryFile: + ViewModel.StatusText = "请留意一下弹出的窗口("; + CopyableBox.ShowDialog(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: + throw new NotImplementedException(); + default: + throw new NotSupportedException(); + } + } + catch (Exception error) + { + ViewModel.StatusText = "失败…"; + CopyableBox.ShowDialog(_ => + { + return Task.FromResult($"在尝试加载 {selected.Type} `{selected.Value}` 时发生错误:\r\n{error}"); + }); + ViewModel.StatusText = string.Empty; + } + } + + [SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")] + 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) + { + using var provider = new FileSystemProvider(SelectedFileSystemRootPath, null); + await ProcessManifest(path).ConfigureAwait(true); + } + + public async Task ProcessManifest(string path) + { + ViewModel.Entries.Clear(); + ViewModel.TraceText = string.Empty; + 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); + var assets = from asset in manifest.Assets.Values + select new DisplayAssetEntry(new AssetEntry(asset)); + return assets.ToArray(); + }).ConfigureAwait(true); + await AddEntries(entries).ConfigureAwait(true); + ViewModel.StatusText = $"总共加载了{ViewModel.Entries.Count}个素材"; + } + + public async Task AddEntries(IEnumerable entries) + { + var target = ViewModel.Entries; + + int BinarySearch(DisplayAssetEntry entry, int? hint = null) + { + var begin = hint.GetValueOrDefault(0); + var end = target.Count; + while (begin < end) + { + var middle = begin + (end - begin) / 2; + var result = entry.CompareTo(target[middle]); + if (result == 0) + { + return middle; + } + else if (result < 0) + { + end = middle; + } + else if (result > 0) + { + begin = middle + 1; + } + } + return end; + } + + var ordered = await Task.Run(() => + { + var array = entries.ToArray(); + Array.Sort(array); + return array; + }).ConfigureAwait(true); + ViewModel.StatusText = $"正在处理刚刚读取出来的 {ordered.Length} 个素材"; + var hint = 0; + foreach (var entry in ordered) + { + var index = hint = BinarySearch(entry, hint); + target.Insert(index, entry); + } + } + + 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); + } + } +} diff --git a/Converters/BooleanInvertConverter.cs b/Converters/BooleanInvertConverter.cs index cf6b4a4..d42545a 100644 --- a/Converters/BooleanInvertConverter.cs +++ b/Converters/BooleanInvertConverter.cs @@ -1,9 +1,11 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Windows.Data; namespace HashCalculator.GUI.Converters { + [SuppressMessage("Microsoft.Performance", "CA1812")] [ValueConversion(typeof(bool), typeof(bool))] internal class BooleanInvertConverter : IValueConverter { diff --git a/Converters/IsZeroToBooleanConverter.cs b/Converters/IsZeroToBooleanConverter.cs new file mode 100644 index 0000000..0995376 --- /dev/null +++ b/Converters/IsZeroToBooleanConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace HashCalculator.GUI.Converters +{ + [ValueConversion(typeof(int), typeof(bool))] + public class IsZeroToBooleanConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (int)value == 0 ? false : true; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/Converters/MultiValueEqualityConverter.cs b/Converters/MultiValueEqualityConverter.cs new file mode 100644 index 0000000..07729c2 --- /dev/null +++ b/Converters/MultiValueEqualityConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Windows.Data; + +namespace HashCalculator.GUI.Converters +{ + [SuppressMessage("Microsoft.Performance", "CA1812")] + internal class MultiValueEqualityConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + return values.All(x => Equals(x, values.First())); + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/Converters/ValidInputEntryTypeToBooleanConverter.cs b/Converters/ValidInputEntryTypeToBooleanConverter.cs index 2dc5785..2fb8c83 100644 --- a/Converters/ValidInputEntryTypeToBooleanConverter.cs +++ b/Converters/ValidInputEntryTypeToBooleanConverter.cs @@ -1,9 +1,11 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Windows.Data; namespace HashCalculator.GUI.Converters { + [SuppressMessage("Microsoft.Performance", "CA1812")] [ValueConversion(typeof(InputEntry), typeof(bool))] internal class ValidInputEntryTypeToBooleanConverter : IValueConverter { diff --git a/Converters/ValueConverterAggregate.cs b/Converters/ValueConverterAggregate.cs index ac1afcf..3729ea9 100644 --- a/Converters/ValueConverterAggregate.cs +++ b/Converters/ValueConverterAggregate.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Windows.Data; namespace HashCalculator.GUI.Converters { + [SuppressMessage("Microsoft.Performance", "CA1812")] internal class ValueConverterAggregate : List, IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) diff --git a/CopyableBox.xaml.cs b/CopyableBox.xaml.cs index a5c0595..1bc9997 100644 --- a/CopyableBox.xaml.cs +++ b/CopyableBox.xaml.cs @@ -128,32 +128,22 @@ namespace HashCalculator.GUI private async Task InitializationTask(Func> action, Dispatcher dispatcher) { - var utcBegin = DateTimeOffset.UtcNow; - var timer = new DispatcherTimer(TimeSpan.FromMilliseconds(200), DispatcherPriority.Normal, (s, e) => - { - var timeElapsed = DateTimeOffset.UtcNow - utcBegin; - if (((DispatcherTimer)s).IsEnabled && timeElapsed.TotalSeconds > 1) - { - Text = Text = $"{InitialMessage}\r\n目前耗时{timeElapsed},稍微再等一下吧233"; - } - }, dispatcher); - timer.Start(); - var token = _cancellationTokenSource.Token; async Task Action() { try { + using var timer = new DisposableDispatcherTimer(timer => + { + Text = $"{InitialMessage}\r\n目前耗时{timer.TimeSinceCreation},稍微再等一下吧233"; + }, dispatcher, TimeSpan.FromMilliseconds(200), TimeSpan.FromSeconds(1)); + return await action(token).ConfigureAwait(false); } catch (OperationCanceledException) { return "操作已被取消"; } - finally - { - timer.Stop(); - } } Text = await dispatcher.Invoke(Action).ConfigureAwait(true); } diff --git a/DisposableDispatcherTimer.cs b/DisposableDispatcherTimer.cs new file mode 100644 index 0000000..a666da3 --- /dev/null +++ b/DisposableDispatcherTimer.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Threading; + +namespace HashCalculator.GUI +{ + internal class DisposableDispatcherTimer : IDisposable + { + public DispatcherTimer Timer { get; } + public DateTimeOffset CreationTime { get; } = DateTimeOffset.UtcNow; + public TimeSpan TimeSinceCreation => DateTimeOffset.UtcNow - CreationTime; + + public DisposableDispatcherTimer(Action action, Dispatcher dispatcher, TimeSpan interval) : + this(action, dispatcher, interval, TimeSpan.Zero) + { + } + + public DisposableDispatcherTimer(Action action, Dispatcher dispatcher, TimeSpan interval, TimeSpan wait) + { + Timer = new DispatcherTimer(interval, DispatcherPriority.Normal, (s, e) => + { + if (Timer.IsEnabled && (TimeSinceCreation > wait)) + { + action(this); + } + }, dispatcher); + Timer.Start(); + } + + public void Dispose() + { + Timer.Stop(); + } + } +} diff --git a/FileSystemSuggestions.cs b/FileSystemSuggestions.cs index 5910a65..4801518 100644 --- a/FileSystemSuggestions.cs +++ b/FileSystemSuggestions.cs @@ -44,15 +44,16 @@ namespace HashCalculator.GUI var currentFiles = new List(); string? fileName; - string? currentFullPath; + string? currentFullPath = null; try { - currentFullPath = Path.GetFullPath(path); - fileName = Path.GetFileName(path); - if (File.Exists(currentFullPath)) + var currentFile = new FileInfo(path); + fileName = currentFile.Name; + if (currentFile.Exists) { - var type = CheckExtension(currentFullPath); - if(type is InputEntryType entryType) + currentFullPath = currentFile.FullName; + var type = CheckExtension(currentFile); + if (type is InputEntryType entryType) { currentFiles.Add(new InputEntry(entryType, path, currentFullPath)); } @@ -77,7 +78,7 @@ namespace HashCalculator.GUI select file; var supportedFiles = from file in otherFiles - let type = CheckExtension(file.Extension) + let type = CheckExtension(file) where type.HasValue select new InputEntry(type.Value, _search.GetInputStyleName(file), file.FullName); @@ -91,9 +92,9 @@ namespace HashCalculator.GUI return currentFiles.Concat(alternatives); } - private static InputEntryType? CheckExtension(string path) + private static InputEntryType? CheckExtension(FileInfo info) { - if (Mapping.TryGetValue(path, out var type)) + if (Mapping.TryGetValue(info.Extension, out var type)) { return type; } diff --git a/HashCalculator.GUI.csproj b/HashCalculator.GUI.csproj index 6004613..23e51da 100644 --- a/HashCalculator.GUI.csproj +++ b/HashCalculator.GUI.csproj @@ -20,11 +20,11 @@ + + + + - - TechnologyAssembler.Core.dll - true - \ No newline at end of file diff --git a/InputBar.xaml b/InputBar.xaml index 02027dd..b9697de 100644 --- a/InputBar.xaml +++ b/InputBar.xaml @@ -8,7 +8,7 @@ xmlns:c="clr-namespace:HashCalculator.GUI.Converters" mc:Ignorable="d" x:ClassModifier="internal" - x:Name="_this" + x:Name="Self" d:DesignHeight="100" d:DesignWidth="800" > @@ -69,12 +69,17 @@ + + + + + - - - - - - - - - - - - - - - - - + - + - + - -