diff --git a/App.xaml b/App.xaml
index f4fc817..0b82fe9 100644
--- a/App.xaml
+++ b/App.xaml
@@ -5,6 +5,7 @@
xmlns:c="clr-namespace:HashCalculator.GUI.Converters"
StartupUri="MainWindow.xaml">
+
@@ -38,10 +39,6 @@
Value="#202020"
/>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/App.xaml.cs b/App.xaml.cs
index bc1c6e7..c229593 100644
--- a/App.xaml.cs
+++ b/App.xaml.cs
@@ -18,6 +18,7 @@ namespace HashCalculator.GUI
{
var core = new TechnologyAssemblerCoreModule();
core.Initialize();
+ DispatcherUnhandledException += (s, e) => Program.ErrorBox($"SAGE FastHash 计算器遇上了未处理的错误:{e.Exception}");
}
}
}
diff --git a/AssetEntry.cs b/AssetEntry.cs
index 3a8bc0c..fad7918 100644
--- a/AssetEntry.cs
+++ b/AssetEntry.cs
@@ -2,20 +2,37 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Threading;
using System.Xml.Linq;
using TechnologyAssembler.Core.Assets;
namespace HashCalculator.GUI
{
- public class AssetEntry : IEquatable
+ public class AssetEntry : IEquatable, IComparable
{
public string Type { get; }
public string Name { get; }
+ public string NameString { get; }
public IEnumerable DisplayLabels { get; }
public uint InstanceId => SageHash.CalculateLowercaseHash(Name);
- public string NameString => $"{Type}:{Name}";
- public string InstanceIdString => $"{InstanceId:X8} ({InstanceId})";
+ public string InstanceIdString => $"{InstanceId:x8} ({InstanceId,10})";
+
+ public string LocalizedNames
+ {
+ get
+ {
+ if (!Translator.HasProvider)
+ {
+ return string.Empty;
+ }
+ if (!DisplayLabels.Any())
+ {
+ return "N/A";
+ }
+ return DisplayLabels.Select(Translator.Translate).Aggregate((x, y) => $"{x} {y}");
+ }
+ }
public AssetEntry(XElement element)
{
@@ -32,6 +49,7 @@ namespace HashCalculator.GUI
Type = element.Name.LocalName;
var id = element.Attribute("id")?.Value;
Name = id ?? throw new NotSupportedException();
+ NameString = $"{Type}:{Name}";
var labels = from name in element.Elements(ModXml.EalaAsset + "DisplayName")
select name.Value;
@@ -42,14 +60,15 @@ namespace HashCalculator.GUI
public AssetEntry(Asset asset)
{
- if(asset is null)
+ 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)
+ NameString = $"{Type}:{Name}";
+ DisplayLabels = Array.Empty();
+ if (InstanceId != asset.InstanceId || SageHash.CalculateBinaryHash(Type) != asset.TypeId)
{
throw new InvalidDataException();
}
@@ -103,32 +122,34 @@ namespace HashCalculator.GUI
{
return HashCode.Combine(Type, InstanceId);
}
- }
- internal class DisplayAssetEntry : IComparable
- {
- public string Name { get; }
- public string InstanceId { get; }
-
- public DisplayAssetEntry(AssetEntry entry)
+ public int CompareTo(AssetEntry other)
{
- Name = entry.NameString;
- InstanceId = entry.InstanceIdString;
+ if (other is null)
+ {
+ throw new ArgumentNullException($"{nameof(other)} is null");
+ }
+ return string.CompareOrdinal(NameString, other.NameString);
}
- public int CompareTo(DisplayAssetEntry other)
+ public static bool operator <(AssetEntry left, AssetEntry right)
{
- return string.CompareOrdinal(Name, other.Name);
+ return left is null ? right is object : left.CompareTo(right) < 0;
}
- }
- internal class LocalizedDisplayAssetEntry : DisplayAssetEntry
- {
- public string LocalizedNames { get; }
-
- public LocalizedDisplayAssetEntry(AssetEntry entry) : base(entry)
+ public static bool operator <=(AssetEntry left, AssetEntry right)
{
- LocalizedNames = entry.DisplayLabels.Aggregate((x, y) => $"{x} {y}");
+ return left is null || left.CompareTo(right) <= 0;
+ }
+
+ public static bool operator >(AssetEntry left, AssetEntry right)
+ {
+ return left is object && left.CompareTo(right) > 0;
+ }
+
+ public static bool operator >=(AssetEntry left, AssetEntry right)
+ {
+ return left is null ? right is null : left.CompareTo(right) >= 0;
}
}
}
diff --git a/Command.cs b/Command.cs
index 209ce4e..de446aa 100644
--- a/Command.cs
+++ b/Command.cs
@@ -98,12 +98,12 @@ namespace HashCalculator.GUI
});
}
- public bool CanExecute(object parameter)
+ public bool CanExecute(object? parameter)
{
return _canExecute;
}
- public void Execute(object parameter)
+ public void Execute(object? parameter)
{
if (!_canExecute)
{
diff --git a/Controller.cs b/Controller.cs
index 8f36351..0f6264a 100644
--- a/Controller.cs
+++ b/Controller.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
@@ -8,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using TechnologyAssembler.Core.IO;
using TechnologyAssembler.Core.Assets;
+using TechnologyAssembler.Core.Language.Providers;
namespace HashCalculator.GUI
{
@@ -19,28 +21,36 @@ namespace HashCalculator.GUI
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));
+ }
+
[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
{
+ _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(token => CalculateBinaryHash(selected.Value, token));
+ CopyableBox.ShowDialog("SAGE FastHash 计算器", token => CalculateBinaryHash(selected.Value, token));
ViewModel.StatusText = ViewModel.SuggestionString(string.Empty);
return;
case InputEntryType.BigFile:
@@ -50,7 +60,9 @@ namespace HashCalculator.GUI
await LoadManifestFromFile(selected.Value).ConfigureAwait(true);
return;
case InputEntryType.XmlFile:
- throw new NotImplementedException();
+ ViewModel.IsXml = true;
+ await LoadXml(selected.Value).ConfigureAwait(true);
+ return;
default:
throw new NotSupportedException();
}
@@ -58,7 +70,7 @@ namespace HashCalculator.GUI
catch (Exception error)
{
ViewModel.StatusText = "失败…";
- CopyableBox.ShowDialog(_ =>
+ CopyableBox.ShowDialog("SAGE FastHash 计算器 - 失败!", _ =>
{
return Task.FromResult($"在尝试加载 {selected.Type} `{selected.Value}` 时发生错误:\r\n{error}");
});
@@ -90,10 +102,10 @@ namespace HashCalculator.GUI
SageHash.CalculateLauncherBinaryHash
}.AsParallel().Select(fn => fn(array)).ToArray();
- return $"使用 SAGE FastHash 计算出的哈希值:{hashes[0]:X8} (十进制 {hashes[0]})\r\n"
+ 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]})";
+ + $"{hashes[1]:x8} (十进制 {hashes[1]})";
}
catch (Exception exception)
{
@@ -134,73 +146,252 @@ namespace HashCalculator.GUI
private async Task LoadManifestFromFile(string path)
{
- using var provider = new FileSystemProvider(SelectedFileSystemRootPath, null);
- await ProcessManifest(path).ConfigureAwait(true);
+ 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)
{
- ViewModel.Entries.Clear();
- ViewModel.TraceText = string.Empty;
- ViewModel.StatusText = "正在加载 manifest 文件…";
+ 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);
+ using var manifest = Manifest.Load(path, true);
var assets = from asset in manifest.Assets.Values
- select new DisplayAssetEntry(new AssetEntry(asset));
+ select new AssetEntry(asset);
return assets.ToArray();
}).ConfigureAwait(true);
- await AddEntries(entries).ConfigureAwait(true);
- ViewModel.StatusText = $"总共加载了{ViewModel.Entries.Count}个素材";
+ AddEntries(entries);
+ ViewModel.StatusText = $"Manifest文件读取完毕,加载了{_loadedAssets.Count}个素材";
}
- public async Task AddEntries(IEnumerable entries)
+ public async Task CancelLoadingXml()
{
- var target = ViewModel.Entries;
-
- int BinarySearch(DisplayAssetEntry entry, int? hint = null)
+ if (_lastXmlTask is null)
{
- var begin = hint.GetValueOrDefault(0);
- var end = target.Count;
- while (begin < end)
+ 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
{
- 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;
- }
+ ViewModel.IsLoadingXml = true;
+ await task.ConfigureAwait(true);
}
- return end;
+ 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;
}
- var ordered = await Task.Run(() =>
+ ViewModel.StatusText = $"ModXml 读取完毕~ {statistics}";
+ if (_loadedAssets.Count != modXml.TotalAssets)
{
- 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);
+ 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 entries)
+ {
+ _loadedAssets.UnionWith(entries);
+ UpdateEntries();
+ }
+
public static IEnumerable FindManifests()
{
var manifests = VirtualFileSystem.ListFiles(SelectedFileSystemRootPath, "*.manifest", VirtualSearchOptionType.AllDirectories);
@@ -223,5 +414,21 @@ namespace HashCalculator.GUI
{
return VirtualFileSystem.GetFileName(path).StartsWith(what, StringComparison.OrdinalIgnoreCase);
}
+
+ [SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
+ 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);
+ });
+ }
+ }
}
}
diff --git a/Converters/ValueConverterAggregate.cs b/Converters/ValueConverterAggregate.cs
index 3729ea9..d738740 100644
--- a/Converters/ValueConverterAggregate.cs
+++ b/Converters/ValueConverterAggregate.cs
@@ -10,7 +10,7 @@ namespace HashCalculator.GUI.Converters
[SuppressMessage("Microsoft.Performance", "CA1812")]
internal class ValueConverterAggregate : List, IValueConverter
{
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture)
{
return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture));
}
diff --git a/CopyableBox.xaml b/CopyableBox.xaml
index c2044cc..433c91d 100644
--- a/CopyableBox.xaml
+++ b/CopyableBox.xaml
@@ -8,8 +8,9 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:HashCalculator.GUI"
mc:Ignorable="d"
- Title="CopyableBox" Height="350" Width="600"
+ Title="{Binding Title}" Height="350" Width="600"
Closing="ClosingHandler"
+ Style="{StaticResource CommonStyle}"
>
diff --git a/CopyableBox.xaml.cs b/CopyableBox.xaml.cs
index 1bc9997..f5437e2 100644
--- a/CopyableBox.xaml.cs
+++ b/CopyableBox.xaml.cs
@@ -27,10 +27,11 @@ namespace HashCalculator.GUI
{
private CopyableBoxViewModel ViewModel => (CopyableBoxViewModel)DataContext;
- public static void ShowDialog(Func> action)
+ public static void ShowDialog(string title, Func> action)
{
using var box = new CopyableBox();
- box.ViewModel.Initialize(action, box.Dispatcher);
+ box.ViewModel.Initialize(title, action, box.Dispatcher);
+ box.Owner = App.Current.MainWindow;
box.ShowDialog();
}
@@ -67,7 +68,14 @@ namespace HashCalculator.GUI
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private Task? _task;
- public const string InitialMessage = "正在加载…… 关闭窗口就可以取消加载(";
+ private string _title = "我居然没有名字qwq";
+ public string Title
+ {
+ get => _title;
+ private set => SetField(ref _title, value);
+ }
+
+ private const string InitialMessage = "正在加载…… 关闭窗口就可以取消加载(";
private string _text = InitialMessage;
public string Text
@@ -121,8 +129,9 @@ namespace HashCalculator.GUI
_cancellationTokenSource.Dispose();
}
- public void Initialize(Func> action, Dispatcher dispatcher)
+ public void Initialize(string title, Func> action, Dispatcher dispatcher)
{
+ Title = title;
_task = InitializationTask(action, dispatcher);
}
diff --git a/HashCalculator.GUI.csproj b/HashCalculator.GUI.csproj
index 23e51da..a60b453 100644
--- a/HashCalculator.GUI.csproj
+++ b/HashCalculator.GUI.csproj
@@ -9,22 +9,32 @@
true
AnyCPU;x86
bf77c300-44f6-46ea-be94-f50d6993b55b
+ HashCalculator.GUI.Program
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
+
+
+ TechnologyAssembler.Core.dll
+
+
+
+
+
+ %(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)
+
+
+
\ No newline at end of file
diff --git a/InputBar.xaml b/InputBar.xaml
index b9697de..ee07d4c 100644
--- a/InputBar.xaml
+++ b/InputBar.xaml
@@ -12,11 +12,6 @@
d:DesignHeight="100" d:DesignWidth="800"
>
-
-
-
-
-