This commit is contained in:
lanyi 2020-04-03 04:48:57 +02:00
parent 97067a68a5
commit 1e8889aecc
19 changed files with 1156 additions and 212 deletions

204
App.xaml
View File

@ -5,6 +5,7 @@
xmlns:c="clr-namespace:HashCalculator.GUI.Converters"
StartupUri="MainWindow.xaml">
<Application.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<c:ValueConverterAggregate x:Key="ValidInputEntryToVisibilityConverter">
<c:ValidInputEntryTypeToBooleanConverter />
<BooleanToVisibilityConverter />
@ -38,10 +39,6 @@
Value="#202020"
/>
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="l:MainWindow"
/>
<Style
TargetType="l:ShellLink"
>
@ -126,10 +123,6 @@
BasedOn="{StaticResource ButtonStyle}"
TargetType="Button"
/>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="CheckBox"
/>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type DataGrid}"
@ -163,5 +156,200 @@
>
<Setter Property="Background" Value="#181818"/>
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="CheckBox"
>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<Grid
x:Name="TemplateRoot"
Background="Transparent"
SnapsToDevicePixels="True"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border
x:Name="CheckBoxBorder"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="1"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
>
<Grid x:Name="MarkGrid">
<Path
x:Name="OptionMark"
Data="F1M9.97498,1.22334L4.6983,9.09834 4.52164,9.09834 0,5.19331 1.27664,3.52165 4.255,6.08833 8.33331,1.52588E-05 9.97498,1.22334z"
Fill="LightGray"
Margin="1"
Opacity="0"
Stretch="None"
/>
<Rectangle
x:Name="IndeterminateMark"
Fill="LightGray"
Margin="2"
Opacity="0"
/>
</Grid>
</Border>
<ContentPresenter
x:Name="ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Grid.Column="1"
ContentStringFormat="{TemplateBinding ContentStringFormat}"
Focusable="False"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
/>
</Grid>
<ControlTemplate.Triggers>
<Trigger
Property="HasContent"
Value="True"
>
<Setter Property="FocusVisualStyle">
<Setter.Value>
<Style>
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle
Margin="14,0,0,0"
SnapsToDevicePixels="True"
Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"
StrokeThickness="1"
StrokeDashArray="1 2"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
<Setter
Property="Padding"
Value="4,-1,0,0"
/>
</Trigger>
<Trigger
Property="IsMouseOver"
Value="True"
>
<Setter
Property="Background"
TargetName="CheckBoxBorder"
Value="#80F3F9FF"
/>
<Setter
Property="BorderBrush"
TargetName="CheckBoxBorder"
Value="#FF5593FF"
/>
<Setter
Property="Fill"
TargetName="OptionMark"
Value="LightGray"
/>
<Setter
Property="Fill"
TargetName="IndeterminateMark"
Value="LightGray"
/>
</Trigger>
<Trigger
Property="IsEnabled"
Value="False"
>
<Setter
Property="Background"
TargetName="CheckBoxBorder"
Value="#FFE6E6E6"
/>
<Setter
Property="BorderBrush"
TargetName="CheckBoxBorder"
Value="#FFBCBCBC"
/>
<Setter
Property="Fill"
TargetName="OptionMark"
Value="#FF707070"
/>
<Setter
Property="Fill"
TargetName="IndeterminateMark"
Value="#FF707070"
/>
</Trigger>
<Trigger
Property="IsPressed"
Value="True"
>
<Setter
Property="Background"
TargetName="CheckBoxBorder"
Value="#80D9ECFF"
/>
<Setter
Property="BorderBrush"
TargetName="CheckBoxBorder"
Value="#FF3C77DD"
/>
<Setter
Property="Fill"
TargetName="OptionMark"
Value="LightGray"
/>
<Setter
Property="Fill"
TargetName="IndeterminateMark"
Value="LightGray"
/>
</Trigger>
<Trigger
Property="IsChecked"
Value="True"
>
<Setter
Property="Opacity"
TargetName="OptionMark"
Value="1"
/>
<Setter
Property="Opacity"
TargetName="IndeterminateMark"
Value="0"
/>
</Trigger>
<Trigger
Property="IsChecked"
Value="{x:Null}"
>
<Setter
Property="Opacity"
TargetName="OptionMark"
Value="0"
/>
<Setter
Property="Opacity"
TargetName="IndeterminateMark"
Value="1"
/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Resources>
</Application>

View File

@ -18,6 +18,7 @@ namespace HashCalculator.GUI
{
var core = new TechnologyAssemblerCoreModule();
core.Initialize();
DispatcherUnhandledException += (s, e) => Program.ErrorBox($"SAGE FastHash 计算器遇上了未处理的错误:{e.Exception}");
}
}
}

View File

@ -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<AssetEntry>
public class AssetEntry : IEquatable<AssetEntry>, IComparable<AssetEntry>
{
public string Type { get; }
public string Name { get; }
public string NameString { get; }
public IEnumerable<string> 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;
@ -48,7 +66,8 @@ namespace HashCalculator.GUI
}
Type = asset.TypeName;
Name = asset.InstanceName;
DisplayLabels = Enumerable.Empty<string>();
NameString = $"{Type}:{Name}";
DisplayLabels = Array.Empty<string>();
if (InstanceId != asset.InstanceId || SageHash.CalculateBinaryHash(Type) != asset.TypeId)
{
throw new InvalidDataException();
@ -103,32 +122,34 @@ namespace HashCalculator.GUI
{
return HashCode.Combine(Type, InstanceId);
}
public int CompareTo(AssetEntry other)
{
if (other is null)
{
throw new ArgumentNullException($"{nameof(other)} is null");
}
return string.CompareOrdinal(NameString, other.NameString);
}
internal class DisplayAssetEntry : IComparable<DisplayAssetEntry>
public static bool operator <(AssetEntry left, AssetEntry right)
{
public string Name { get; }
public string InstanceId { get; }
public DisplayAssetEntry(AssetEntry entry)
{
Name = entry.NameString;
InstanceId = entry.InstanceIdString;
return left is null ? right is object : left.CompareTo(right) < 0;
}
public int CompareTo(DisplayAssetEntry other)
public static bool operator <=(AssetEntry left, AssetEntry right)
{
return string.CompareOrdinal(Name, other.Name);
}
return left is null || left.CompareTo(right) <= 0;
}
internal class LocalizedDisplayAssetEntry : DisplayAssetEntry
public static bool operator >(AssetEntry left, AssetEntry right)
{
public string LocalizedNames { get; }
return left is object && left.CompareTo(right) > 0;
}
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 ? right is null : left.CompareTo(right) >= 0;
}
}
}

View File

@ -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)
{

View File

@ -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<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)
{
_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)
{
@ -133,72 +145,251 @@ namespace HashCalculator.GUI
}
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)
{
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<DisplayAssetEntry> 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)
{
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;
return;
}
var ordered = await Task.Run(() =>
var (tokenSource, task) = _lastXmlTask;
tokenSource.Cancel();
try
{
var array = entries.ToArray();
Array.Sort(array);
return array;
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);
ViewModel.StatusText = $"正在处理刚刚读取出来的 {ordered.Length} 个素材";
var hint = 0;
foreach (var entry in ordered)
{
var index = hint = BinarySearch(entry, hint);
target.Insert(index, entry);
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()
@ -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<Task> action, string errorTitle, string preErrorMessage)
{
try
{
await action().ConfigureAwait(true);
}
catch (Exception exception)
{
CopyableBox.ShowDialog(errorTitle, _ =>
{
return Task.FromResult(preErrorMessage + exception);
});
}
}
}
}

View File

@ -10,7 +10,7 @@ namespace HashCalculator.GUI.Converters
[SuppressMessage("Microsoft.Performance", "CA1812")]
internal class ValueConverterAggregate : List<IValueConverter>, 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));
}

View File

@ -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}"
>
<Window.DataContext>
<local:CopyableBoxViewModel />

View File

@ -27,10 +27,11 @@ namespace HashCalculator.GUI
{
private CopyableBoxViewModel ViewModel => (CopyableBoxViewModel)DataContext;
public static void ShowDialog(Func<CancellationToken, Task<string>> action)
public static void ShowDialog(string title, Func<CancellationToken, Task<string>> 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<CancellationToken, Task<string>> action, Dispatcher dispatcher)
public void Initialize(string title, Func<CancellationToken, Task<string>> action, Dispatcher dispatcher)
{
Title = title;
_task = InitializationTask(action, dispatcher);
}

View File

@ -9,22 +9,32 @@
<UseWPF>true</UseWPF>
<Platforms>AnyCPU;x86</Platforms>
<UserSecretsId>bf77c300-44f6-46ea-be94-f50d6993b55b</UserSecretsId>
<StartupObject>HashCalculator.GUI.Program</StartupObject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="1.1.0" />
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.0" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Mvp.Xml" Version="2.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TechnologyAssembler.Core\TechnologyAssembler.Core.csproj" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.11.0" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Windows.Presentation" />
<Reference Include="TechnologyAssembler.Core">
<HintPath>TechnologyAssembler.Core.dll</HintPath>
</Reference>
</ItemGroup>
<Target Name="CustomAfterResolveReferences" AfterTargets="AfterResolveReferences">
<ItemGroup>
<EmbeddedResource Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'">
<LogicalName>%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
</Target>
</Project>

View File

@ -12,11 +12,6 @@
d:DesignHeight="100" d:DesignWidth="800"
>
<UserControl.Resources>
<c:ValueConverterAggregate x:Key="NullToVisibilityConverter">
<c:NullToBooleanConverter />
<c:BooleanInvertConverter />
<BooleanToVisibilityConverter />
</c:ValueConverterAggregate>
<Style
x:Key="BlankListBoxContainerStyle"
TargetType="{x:Type ListBoxItem}"

View File

@ -12,6 +12,7 @@
Height="600"
Width="600"
Style="{StaticResource CommonStyle}"
Initialized="OnInitialized"
>
<Window.DataContext>
<l:ViewModel />
@ -43,7 +44,8 @@
Content="浏览文件"
Command="{Binding MainInput.BrowseCommand}"
CommandParameter="{Binding ElementName=Self}"
Visibility="{Binding MainInput.SelectedItem, Converter={StaticResource InvalidInputEntryToVisibilityConverter}}"
Visibility="{Binding MainInput.SelectedItem,
Converter={StaticResource InvalidInputEntryToVisibilityConverter}}"
Grid.Column="1"
Margin="0"
/>
@ -51,7 +53,8 @@
Content="确认"
Command="{Binding MainInput.SelectCommand}"
CommandParameter="{Binding}"
Visibility="{Binding MainInput.SelectedItem, Converter={StaticResource ValidInputEntryToVisibilityConverter}}"
Visibility="{Binding MainInput.SelectedItem,
Converter={StaticResource ValidInputEntryToVisibilityConverter}}"
Grid.Column="1"
Margin="0"
Foreground="#20FF30"
@ -62,7 +65,8 @@
DockPanel.Dock="Top"
Height="25"
Margin="0,10,0,0"
Visibility="{Binding BigEntryInput.AllManifests, Converter={StaticResource NotNullToVisibilityConverter}}"
Visibility="{Binding BigEntryInput.AllManifests,
Converter={StaticResource NotNullToVisibilityConverter}}"
>
<Grid.ColumnDefinitions>
<ColumnDefinition />
@ -116,98 +120,162 @@
</Grid>
<Grid
DockPanel.Dock="Top"
Height="50"
Margin="0,10"
Height="25"
Margin="0,5,0,0"
>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock
Text="{Binding StatusText}"
Grid.Row="0"
Margin="10,0"
VerticalAlignment="Center"
/>
<TextBlock
Text="{Binding Entries.Count, StringFormat=目前加载了{0}个素材}"
Grid.Row="1"
Margin="10,5,0,5"
Width="160"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Visibility="{Binding Entries.Count, Converter={StaticResource NonZeroToVisibilityConverter}}"
/>
<CheckBox
Content="只显示 GameObject"
IsChecked="True"
Grid.Row="1"
Margin="0,5,242,5"
Width="155"
HorizontalAlignment="Right"
Margin="5,0,0,0"
VerticalAlignment="Center"
/>
</Grid>
<DockPanel
DockPanel.Dock="Top"
Height="25"
Margin="0,0,0,5"
>
<Grid
DockPanel.Dock="Right"
Width="190"
Visibility="{Binding IsXml,
Converter={StaticResource BooleanToVisibilityConverter}}"
>
<Button
Content="加载 csf/mod.str"
Grid.Row="1"
Width="140"
Margin="0,0,97,0"
HorizontalAlignment="Right"
Content="{Binding LoadCsfText}"
Command="{Binding LoadCsf}"
CommandParameter="{Binding ElementName=Self}"
Width="92"
HorizontalAlignment="Left"
/>
<Button
Content="取消加载"
Grid.Row="1"
Command="{Binding CancelXml}"
Width="92"
HorizontalAlignment="Right"
Visibility="{Binding IsLoadingXml,
Converter={StaticResource BooleanToVisibilityConverter}}"
/>
</Grid>
<CheckBox
DockPanel.Dock="Right"
Content="只显示 GameObject"
IsChecked="{Binding GameObjectOnly}"
Command="{Binding FilterDisplayEntries}"
Width="155"
VerticalAlignment="Center"
/>
<TextBlock
Text="{Binding DisplayEntries.Count, StringFormat=列表里显示了{0}个素材}"
Margin="5,0,0,0"
Width="160"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Visibility="{Binding DisplayEntries.Count,
Converter={StaticResource NonZeroToVisibilityConverter}}"
/>
</DockPanel>
<Grid
DockPanel.Dock="Top"
Height="25"
>
<TextBox></TextBox>
<TextBox
Text="{Binding Filter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
VerticalContentAlignment="Center"
/>
<TextBlock
Text="过滤Asset ID可选"
Text="可以按照素材名称或 Instance ID 来查找素材,加载 CSF 之后还可以按照单位名称来查找"
Foreground="#B0B0B0"
Padding="5,0"
VerticalAlignment="Center"
Visibility="{Binding Filter,
Converter={StaticResource NullToVisibilityConverter}}"
IsHitTestVisible="False"
/>
</Grid>
<FrameworkElement x:Name="ReferenceProvider" Visibility="Collapsed"/>
<DataGrid
ItemsSource="{Binding Entries}"
x:Name="DataGrid"
ItemsSource="{Binding DisplayEntries}"
IsReadOnly="True"
AutoGenerateColumns="False"
ScrollViewer.CanContentScroll="True"
VerticalScrollBarVisibility="Auto"
EnableRowVirtualization="True"
VirtualizingPanel.VirtualizationMode="Recycling"
>
<DataGrid.Columns>
<DataGridTextColumn
Header="类型/素材名称"
Binding="{Binding NameString}"
/>
<DataGridTextColumn
Header="哈希InstanceId"
Binding="{Binding InstanceIdString}"
FontFamily="Courier New"
/>
<DataGridTextColumn
Header="名称"
Binding="{Binding LocalizedNames}"
Visibility="{Binding Source={x:Reference Name=ReferenceProvider},
Path=DataContext.IsXml,
Converter={StaticResource BooleanToVisibilityConverter}}"
/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
<StackPanel
<ScrollViewer
Grid.Row="1"
Margin="0,10,0,0"
ScrollViewer.CanContentScroll="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
ScrollChanged="OnButtomScrollViewerScrollChanged"
>
<StackPanel>
<TextBlock TextWrapping="Wrap">
本工具基于 <l:ShellLink NavigateUri="https://github.com/Qibbi">Qibbi</l:ShellLink> 提供的 TechnologyAssembler 制作<LineBreak />
此外使用了 <l:ShellLink NavigateUri="https://github.com/bgrainger">Bradley Grainger</l:ShellLink> 的
使用了 <l:ShellLink NavigateUri="https://github.com/bgrainger">Bradley Grainger</l:ShellLink> 的
<l:ShellLink NavigateUri="https://github.com/bgrainger/IndexRange">IndexRange</l:ShellLink>
从而能在 .NET Standard 2.0 上使用 C# 8.0 的末尾索引操作符<LineBreak />
此外还使用了 <l:ShellLink NavigateUri="http://mvpxml.codeplex.com/">Mvp.Xml</l:ShellLink> 来解析 XML 文件<LineBreak />
<LineBreak />
假如对本工具有任何疑问或者建议的话,可以来到<l:ShellLink NavigateUri="https://tieba.baidu.com/ra3">红警3吧</l:ShellLink>发帖寻找岚依(
假如对本工具有任何疑问或者建议的话,可以来到<l:ShellLink NavigateUri="https://tieba.baidu.com/ra3">红警3吧</l:ShellLink>发帖寻找岚依(
</TextBlock>
<TextBlock />
<TextBlock
Text="Tracer 输出:"
Margin="0,5,0,0"
Visibility="{Binding TraceText,
Converter={StaticResource NotNullToVisibilityConverter}}"
/>
<TextBox
Text="{Binding TraceText, Mode=OneWay}"
Visibility="{Binding TraceText,
Converter={StaticResource NotNullToVisibilityConverter}}"
TextWrapping="Wrap"
IsReadOnly="True"
BorderBrush="Transparent"
/>
<StackPanel
FlowDirection="RightToLeft"
Orientation="Horizontal"
Height="25"
HorizontalAlignment="Right"
Visibility="{Binding TraceText,
Converter={StaticResource NotNullToVisibilityConverter}}"
>
<Button
Content="清除输出"
Command="{Binding ClearTraceText}"
Width="92"
Grid.Row="1"
/>
<TextBlock
VerticalAlignment="Center"
Margin="10,0"
Visibility="{Binding SuggestClearFilter, Converter={StaticResource BooleanToVisibilityConverter}}"
>
假如觉得有点卡的话,可以试试点击右边的这个按钮
</TextBlock>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Window>

View File

@ -1,17 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace HashCalculator.GUI
{
@ -26,11 +16,15 @@ namespace HashCalculator.GUI
public MainWindow()
{
InitializeComponent();
Translator.ProviderChanged += (s, e) => Dispatcher.Invoke(() =>
{
ViewModel.NotifyCsfChange();
});
}
private void OnButtomScrollViewerScrollChanged(object sender, ScrollChangedEventArgs e)
{
var scrollViewer = (ScrollViewer)e.Source;
var scrollViewer = (ScrollViewer)sender;
// User scroll event : set or unset auto-scroll mode
if (e.ExtentHeightChange == 0)
{ // Content unchanged : user scroll event
@ -53,5 +47,10 @@ namespace HashCalculator.GUI
scrollViewer.ScrollToVerticalOffset(scrollViewer.ExtentHeight);
}
}
private void OnInitialized(object sender, EventArgs e)
{
ViewModel.StartTracerListener(s => Dispatcher.BeginInvoke(s));
}
}
}

View File

@ -4,7 +4,9 @@ using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Win32;
@ -28,17 +30,21 @@ namespace HashCalculator.GUI
public static XNamespace XInclude { get; } = "http://www.w3.org/2001/XInclude";
public static XNamespace EalaAsset { get; } = "uri:ea.com:eala:asset";
public bool SdkNotFound { get; }
public IReadOnlyCollection<string> Errors => _errors;
public DirectoryInfo BaseDirectory { get; }
public int TotalFilesProcessed => _processed.Count;
public int TotalAssets => _assets.Count;
private readonly ConcurrentDictionary<string, byte> _processed = new ConcurrentDictionary<string, byte>();
private readonly ConcurrentDictionary<AssetEntry, byte> _assets = new ConcurrentDictionary<AssetEntry, byte>();
private readonly ConcurrentQueue<string> _errors = new ConcurrentQueue<string>();
private readonly ConcurrentDictionary<AssetEntry, string> _assets = new ConcurrentDictionary<AssetEntry, string>();
private readonly ModUriResolver _uriResolver;
private readonly string _xmlFullPath;
public ModXml(string xmlPath)
private readonly CancellationToken _token;
private readonly BufferBlock<AssetEntry> _entries;
public ModXml(string xmlPath, CancellationToken token)
{
var baseDirectory = new DirectoryInfo(FindBaseDirectory(xmlPath));
var allMods = baseDirectory.Parent;
BaseDirectory = new DirectoryInfo(FindBaseDirectory(xmlPath));
var allMods = BaseDirectory.Parent;
if (!allMods.Name.Equals("Mods", StringComparison.OrdinalIgnoreCase))
{
throw new DirectoryNotFoundException();
@ -55,7 +61,7 @@ namespace HashCalculator.GUI
{
sdkRootPath,
allModsParent.FullName,
Path.Combine(baseDirectory.FullName, name),
Path.Combine(BaseDirectory.FullName, name),
Path.Combine(sdkRootPath, "Mods"),
allMods.FullName,
Path.Combine(sdkRootPath, name.Equals("Data", StringComparison.OrdinalIgnoreCase) ? "SageXml" : name)
@ -69,31 +75,54 @@ namespace HashCalculator.GUI
});
_xmlFullPath = Path.GetFullPath(xmlPath);
_token = token;
_entries = new BufferBlock<AssetEntry>();
}
public Task<ICollection<AssetEntry>> ProcessDocument()
public async IAsyncEnumerable<AssetEntry> ProcessDocument()
{
if (_processed.Any() || _assets.Any() || _errors.Any())
if (_processed.Any() || _assets.Any())
{
throw new InvalidOperationException();
}
return Task.Run(() =>
var task = Task.Run(() =>
{
var includes = ProcessDocumentInternal(_xmlFullPath).AsParallel();
try
{
var includes = ProcessDocumentInternal(_xmlFullPath);
while (includes.Any())
{
includes = from include in includes
var result = from include in includes.AsParallel()
from newInclude in ProcessDocumentInternal(include)
select newInclude;
includes = result.ToArray();
}
}
catch
{
if (!_token.IsCancellationRequested)
{
throw;
}
}
finally
{
_entries.Complete();
}
});
while(await _entries.OutputAvailableAsync().ConfigureAwait(false))
{
yield return _entries.Receive();
}
return _assets.Keys;
});
await task.ConfigureAwait(false);
}
private string[] ProcessDocumentInternal(string fullPath)
{
_token.ThrowIfCancellationRequested();
if (!_processed.TryAdd(fullPath.ToUpperInvariant(), default))
{
return Array.Empty<string>();
@ -110,9 +139,14 @@ namespace HashCalculator.GUI
select new AssetEntry(element);
foreach (var item in items)
{
if (!_assets.TryAdd(item, default))
if (_assets.TryAdd(item, fullPath))
{
_errors.Enqueue($"Attempted to add item `{item.Type}:{item.Name}` multiple times");
_entries.Post(item);
}
else
{
var previousPath = _assets[item];
TracerListener.WriteLine($"[ModXml] `{fullPath}`: Attempted to add item `{item.Type}:{item.Name}` multiple times - already processed in `{previousPath}`");
}
}
@ -148,7 +182,7 @@ namespace HashCalculator.GUI
}
catch (FileNotFoundException error)
{
_errors.Enqueue(error.Message);
TracerListener.WriteLine($"[ModXml]: {error}");
return null;
}

75
Program.cs Normal file
View File

@ -0,0 +1,75 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
namespace HashCalculator.GUI
{
public static class Program
{
[STAThread]
public static void Main()
{
try
{
AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
App.Main();
}
catch(Exception exception)
{
ErrorBox($"发生了无法处理的错误:\r\n{exception.ToString()}\r\n可以尝试在百度红警3吧联系岚依");
throw;
}
}
private static Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
{
var executingAssembly = Assembly.GetExecutingAssembly();
var assemblyName = new AssemblyName(args.Name);
var nameString = assemblyName.Name;
var paths = new[] { $"{nameString}.dll" }.AsEnumerable();
const string resourceExtenstion = ".resources";
if (nameString.EndsWith(resourceExtenstion, StringComparison.OrdinalIgnoreCase))
{
paths = paths.Append(nameString.Insert(nameString.Length - resourceExtenstion.Length, ".g"));
}
if (!assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture))
{
paths = paths.Select(x => $"{assemblyName.CultureInfo}\\{x}").Concat(paths);
}
foreach(var path in paths)
{
using var stream = executingAssembly.GetManifestResourceStream(path);
if (stream == null)
{
continue;
}
using (stream)
{
var assemblyRawBytes = new byte[stream.Length];
stream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length);
return Assembly.Load(assemblyRawBytes);
}
}
var errorMessage = $"Failed to load assembly {nameString}";
throw new ApplicationException(errorMessage);
}
public static void ErrorBox(string text)
{
_ = NativeMethods.MessageBox(IntPtr.Zero, text, null, 0x00000010L);
}
}
internal static class NativeMethods
{
[DllImport("User32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
public static extern int MessageBox(IntPtr hWnd, string lpText, string? lpCaption, ulong uType);
}
}

Binary file not shown.

34
TracerListener.cs Normal file
View File

@ -0,0 +1,34 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using TechnologyAssembler.Core.Diagnostics;
namespace HashCalculator.GUI
{
internal static class TracerListener
{
private static Action<string>? _onData = null;
[SuppressMessage("Globalization", "CA1308:将字符串规范化为大写", Justification = "<挂起>")]
[SuppressMessage("Globalization", "CA1303:请不要将文本作为本地化参数传递", Justification = "<挂起>")]
public static void StartListening(Action<string> action)
{
var original = Interlocked.CompareExchange(ref _onData, action, null);
if(original != null)
{
throw new InvalidOperationException("Action already set");
}
Tracer.TraceWrite = new TraceWriteDelegate((s, t, m) =>
{
var type = $"{t} ".ToLowerInvariant().Substring(0, 4);
WriteLine($"[{s}] {type}: {m}");
});
}
public static void WriteLine(string message)
{
Interlocked.CompareExchange(ref _onData, null, null)?.Invoke($"{message}\r\n");
}
}
}

66
Translator.cs Normal file
View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Threading;
using TechnologyAssembler.Core.Language;
namespace HashCalculator.GUI
{
internal static class Translator
{
private static ITranslationProvider? _provider;
public static ITranslationProvider? Provider
{
get => Interlocked.CompareExchange(ref _provider, null, null);
set
{
var provider = value is null
? null
: new CachedCsfTranslationProvider(value);
Interlocked.Exchange(ref _provider, provider);
ProviderChanged?.Invoke(null, null);
}
}
public static bool HasProvider => Provider != null;
public static EventHandler? ProviderChanged;
public static string Translate(string what)
{
var provider = Provider;
if(provider is null)
{
return $"NoProvider:{what}";
}
return provider.GetString(what);
}
}
internal class CachedCsfTranslationProvider : ITranslationProvider
{
private readonly ITranslationProvider _provider;
private readonly ConcurrentDictionary<string, string> _cache = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public string Name => $"Cached{_provider.Name}";
public CachedCsfTranslationProvider(ITranslationProvider provider)
{
_provider = provider;
}
public string GetString(string str)
{
if(!_cache.TryGetValue(str, out var value))
{
value = _provider.GetString(str);
_cache.TryAdd(str, value);
}
return value;
}
public string GetPluralString(string str, string strPlural, long count)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,15 +1,13 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Win32;
using TechnologyAssembler.Core.IO;
@ -48,16 +46,38 @@ namespace HashCalculator.GUI
internal class ViewModel : NotifyPropertyChanged
{
private static readonly Random _random = new Random();
public Action<Action<Action>> OnStartTraceListener { get; }
public MainInputViewModel MainInput { get; }
public BigInputViewModel BigEntryInput { get; }
private ObservableCollection<DisplayAssetEntry> _entries = new ObservableCollection<DisplayAssetEntry>();
public ObservableCollection<DisplayAssetEntry> Entries
private string? _filter;
public string? Filter
{
get => _entries;
set => SetField(ref _entries, value);
get => _filter;
set
{
SetField(ref _filter, value);
FilterDisplayEntries.Execute(null);
}
}
private bool _gameObjectOnly;
public bool GameObjectOnly
{
get => _gameObjectOnly;
set => SetField(ref _gameObjectOnly, value);
}
public Command FilterDisplayEntries { get; }
private int _totalCount;
public int TotalCount
{
get => _totalCount;
set => SetField(ref _totalCount, value);
}
public ObservableSortedCollection<AssetEntry> DisplayEntries { get; } = new ObservableSortedCollection<AssetEntry>();
private string _statusText = SuggestionString("不知道该显示些什么呢……");
public string StatusText
@ -66,18 +86,92 @@ namespace HashCalculator.GUI
set => SetField(ref _statusText, value);
}
private bool _isXml;
public bool IsXml
{
get => _isXml;
set => SetField(ref _isXml, value);
}
private bool _isLoadingXml;
public bool IsLoadingXml
{
get => _isLoadingXml;
set => SetField(ref _isLoadingXml, value);
}
public Command CancelXml { get; }
[SuppressMessage("Microsoft.Performance", "CA1822")]
public string LoadCsfText => $"{(Translator.HasProvider ? "" : "")} CSF";
public Command<MainWindow> LoadCsf { get; }
private string _traceText = string.Empty;
public string TraceText
{
get => _traceText;
set => SetField(ref _traceText, value);
set
{
SetField(ref _traceText, value);
SuggestClearFilter = TraceText.Length > 32768;
}
}
private bool _suggestClearFilter;
public bool SuggestClearFilter
{
get => _suggestClearFilter;
set => SetField(ref _suggestClearFilter, value);
}
public Command ClearTraceText { get; }
public ViewModel()
{
var controller = new Controller(this);
OnStartTraceListener = controller.StartTrace;
MainInput = new MainInputViewModel(controller);
BigEntryInput = new BigInputViewModel(controller);
CancelXml = new Command(async () =>
{
try
{
CancelXml.CanExecuteValue = false;
await controller.CancelLoadingXml().ConfigureAwait(true);
}
finally
{
CancelXml.CanExecuteValue = true;
}
});
LoadCsf = new Command<MainWindow>(async window =>
{
try
{
LoadCsf.CanExecuteValue = false;
var dialog = new OpenFileDialog
{
Multiselect = false,
ValidateNames = true,
Filter = "CSF / BIG 文件|*.csf;*.big"
};
if (dialog.ShowDialog(window) == true)
{
await Controller.LoadCsf(dialog.FileName).ConfigureAwait(true);
}
}
finally
{
LoadCsf.CanExecuteValue = true;
}
});
FilterDisplayEntries = new Command(() =>
{
controller.UpdateEntries();
return Task.CompletedTask;
});
ClearTraceText = new Command(() =>
{
TraceText = string.Empty;
return Task.CompletedTask;
});
}
public static string SuggestionString(string original)
@ -92,8 +186,74 @@ namespace HashCalculator.GUI
{
return "本来以为两小时就能写完这个小工具没想到写了两个星期开始怀疑自己的智商orz";
}
if (generated.IndexOf('7') < 5)
{
return "小提示:在下方的素材列表里,选择一行或多行之后,直接按 Ctrl+C 就可以复制内容~";
}
if (generated.IndexOf('2') == 3)
{
return "温馨提示:请多留意一下自己的重工,不要让它卖自己";
}
return original;
}
public void StartTracerListener(Action<Action> action)
{
OnStartTraceListener(action);
}
public void NotifyCsfChange()
{
Notify(nameof(LoadCsfText));
FilterDisplayEntries.Execute(null);
}
public void AddNewItems(IEnumerable<AssetEntry> items)
{
DisplayEntries.SortedAddItems(items.Where(FilterDisplayEntry));
}
public void FilterCollection(HashSet<AssetEntry> orderedSource)
{
DisplayEntries.SortedRemoveItems(x => !orderedSource.Contains(x) || !FilterDisplayEntry(x));
DisplayEntries.SortedAddItems(orderedSource.Where(FilterDisplayEntry));
}
private bool FilterDisplayEntry(AssetEntry entry)
{
if(GameObjectOnly)
{
if(entry.Type != "GameObject")
{
return false;
}
}
if (string.IsNullOrEmpty(Filter))
{
return true;
}
foreach (var chunk in Filter!.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
{
if (entry.NameString.IndexOf(chunk, StringComparison.OrdinalIgnoreCase) != -1)
{
return true;
}
if (entry.InstanceIdString.IndexOf(chunk, StringComparison.OrdinalIgnoreCase) != -1)
{
return true;
}
if (Translator.HasProvider)
{
if (entry.LocalizedNames.IndexOf(chunk, StringComparison.CurrentCultureIgnoreCase) != -1)
{
return true;
}
}
}
return false;
}
}
internal class InputBarViewModel : NotifyPropertyChanged
@ -256,11 +416,14 @@ namespace HashCalculator.GUI
{
base.Text = value;
if (value != SelectedItem?.ToString())
{
if(AllManifests != null)
{
UpdateList(value);
}
}
}
}
private InputEntry? _lastProcessedManifest;
public InputEntry? LastProcessedManifest
@ -277,6 +440,7 @@ namespace HashCalculator.GUI
{
SetField(ref _allManifests, value);
Items = value;
SelectedItem = null;
LastProcessedManifest = null;
Text = null;
}
@ -300,7 +464,7 @@ namespace HashCalculator.GUI
}
catch (Exception exception)
{
CopyableBox.ShowDialog(_ =>
CopyableBox.ShowDialog("SAGE FastHash 计算器的错误" , _ =>
{
return Task.FromResult($"在加载 manifest 时发生错误:{exception}");
});
@ -332,4 +496,76 @@ namespace HashCalculator.GUI
}
}
}
internal class ObservableSortedCollection<T> : ObservableCollection<T> where T : IComparable<T>
{
private readonly HashSet<T> _set = new HashSet<T>();
public void SortedAddItems(IEnumerable<T> items)
{
try
{
items = from item in items
where !_set.Contains(item)
select item;
foreach (var item in items)
{
_set.Add(item);
base.Insert(BinarySearch(item), item);
}
}
catch
{
_set.Clear();
base.Clear();
throw;
}
}
public void SortedRemoveItems(Func<T, bool> ifRemove)
{
try
{
foreach(var i in Enumerable.Range(0, base.Count).Reverse())
{
var item = base[i];
if (ifRemove(item))
{
_set.Remove(item);
base.RemoveAt(i);
}
}
}
catch
{
_set.Clear();
base.Clear();
throw;
}
}
private int BinarySearch(T entry)
{
var begin = 0;
var end = base.Count;
while (begin < end)
{
var middle = begin + (end - begin) / 2;
var result = entry.CompareTo(base[middle]);
if (result == 0)
{
return middle;
}
else if (result < 0)
{
end = middle;
}
else if (result > 0)
{
begin = middle + 1;
}
}
return end;
}
}
}

BIN
log.txt Normal file

Binary file not shown.