initial work on hashes, view models and xml parsing

This commit is contained in:
lanyi 2020-03-26 02:37:10 +01:00
parent a51aa6958b
commit bda455b7e3
8 changed files with 623 additions and 80 deletions

92
AssetEntry.cs Normal file
View File

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace HashCalculator.GUI
{
public class AssetEntry : IEquatable<AssetEntry>
{
public string Type { get; }
public string Name { get; }
public IEnumerable<string> DisplayLabels { get; }
public uint Hash => SageHash.CalculateLowercaseHash(Name);
public AssetEntry(XElement element)
{
if (element == null)
{
throw new ArgumentNullException($"{nameof(element)} is null");
}
if (element.Name.Namespace != ModXml.EalaAsset)
{
throw new NotSupportedException();
}
Type = element.Name.LocalName;
var id = element.Attribute("id")?.Value;
if (id == null)
{
throw new NotSupportedException();
}
Name = id;
var labels = from name in element.Elements(ModXml.EalaAsset + "DisplayName")
select name.Value;
var transformLabels = from name in element.Elements(ModXml.EalaAsset + "DisplayNameTransformed")
select name.Value;
DisplayLabels = labels.Concat(transformLabels).ToArray();
}
public bool Equals(AssetEntry entry)
{
return this == entry;
}
// override object.Equals
public override bool Equals(object obj)
{
//
// See the full list of guidelines at
// http://go.microsoft.com/fwlink/?LinkID=85237
// and also the guidance for operator== at
// http://go.microsoft.com/fwlink/?LinkId=85238
//
if (!(obj is AssetEntry entry))
{
return false;
}
return this == entry;
}
public static bool operator ==(AssetEntry a, AssetEntry b)
{
if (a is null || b is null)
{
return a is null == b is null;
}
if (ReferenceEquals(a, b))
{
return true;
}
return a.Type == b.Type && a.Hash == b.Hash;
}
public static bool operator !=(AssetEntry a, AssetEntry b)
{
return !(a == b);
}
// override object.GetHashCode
public override int GetHashCode()
{
return HashCode.Combine(Type, Hash);
}
}
}

View File

@ -8,13 +8,16 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<Platforms>AnyCPU;x86</Platforms> <Platforms>AnyCPU;x86</Platforms>
<UserSecretsId>bf77c300-44f6-46ea-be94-f50d6993b55b</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.0" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8"> <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Mvp.Xml" Version="2.3.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -14,7 +14,7 @@
<RowDefinition/> <RowDefinition/>
<RowDefinition Height="130" /> <RowDefinition Height="130" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel Grid.Row="0"> <VirtualizingStackPanel Grid.Row="0">
<Grid Height="25"> <Grid Height="25">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition /> <ColumnDefinition />
@ -31,7 +31,7 @@
HintText="Select some big file" HintText="Select some big file"
Text="{Binding Text}" Text="{Binding Text}"
Collection="{Binding Items}" Collection="{Binding Items}"
TextChanged="OnTextChanged" TextChanged="OnMainInputTextChanged"
/> />
</Grid> </Grid>
<Button x:Name="button" Content="浏览文件" <Button x:Name="button" Content="浏览文件"
@ -52,19 +52,22 @@
Margin="0,10,0,0" Margin="0,10,0,0"
Height="25" VerticalAlignment="Top" Height="25" VerticalAlignment="Top"
/> />
<DataGrid x:Name="dataGrid"> <DataGrid
x:Name="DataGrid"
AutoGenerateColumns="False"
ScrollViewer.CanContentScroll="True"
EnableRowVirtualization="True"
VirtualizingPanel.VirtualizationMode="Recycling"
>
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Asset ID" Width="200"/> <DataGridTextColumn Header="Asset ID" Width="200" Binding="{Binding AssetId}"/>
<DataGridTextColumn Header="哈希" Width="*"/> <DataGridTextColumn Header="哈希" Width="*" Binding="{Binding Hash}"/>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</StackPanel> </VirtualizingStackPanel>
<TextBlock x:Name="textBlock" Grid.Row="1" TextWrapping="Wrap"> <TextBlock x:Name="textBlock" Grid.Row="1" TextWrapping="Wrap">
本工具基于 <l:ShellLink NavigateUri="https://github.com/Qibbi">Qibbi</l:ShellLink> 提供的 TechnologyAssembler 制作<LineBreak /> 本工具基于 <l:ShellLink NavigateUri="https://github.com/Qibbi">Qibbi</l:ShellLink> 提供的 TechnologyAssembler 制作<LineBreak />
此外使用了 <l:ShellLink NavigateUri="https://github.com/vain0x">vain0x</l:ShellLink> 的 此外使用了 <l:ShellLink NavigateUri="https://github.com/bgrainger">Bradley Grainger</l:ShellLink> 的
<l:ShellLink NavigateUri="https://github.com/vain0x/DotNetKit.Wpf.AutoCompleteComboBox">DotNetKit.Wpf.AutoCompleteComboBox</l:ShellLink>
组合框控件<LineBreak />
使用了 <l:ShellLink NavigateUri="https://github.com/bgrainger">Bradley Grainger</l:ShellLink> 的
<l:ShellLink NavigateUri="https://github.com/bgrainger/IndexRange">IndexRange</l:ShellLink> <l:ShellLink NavigateUri="https://github.com/bgrainger/IndexRange">IndexRange</l:ShellLink>
从而能在 .NET Standard 2.0 上使用 C# 8.0 的末尾索引操作符<LineBreak /> 从而能在 .NET Standard 2.0 上使用 C# 8.0 的末尾索引操作符<LineBreak />
<LineBreak /> <LineBreak />

View File

@ -1,9 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
@ -23,85 +20,97 @@ namespace HashCalculator.GUI
/// </summary> /// </summary>
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
private readonly FileSystemSuggestions _suggestions = new FileSystemSuggestions(); [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
private readonly ViewModel _model = new ViewModel();
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
DataContext = _model; var x = new ModXml(@"D:\Users\lanyi\Desktop\RA3Mods\ARSDK2\Mods\Armor Rush\DATA\mod.xml");
var xxx = Enumerable.Repeat(new { AssetId = "正在准备", Hash = "正在准备" }, 1);
x.ProcessDocument().ContinueWith(async txx =>
{
try
{
var xx = await txx.ConfigureAwait(true);
if (x.Errors.Any())
{
MessageBox.Show("Errors: \r\n" + x.Errors.Aggregate((x, y) => $"{x}\r\n{y}"));
} }
private void OnTextChanged(object sender, TextChangedEventArgs e) Dispatcher.Invoke(() =>
{ {
DataGrid.ItemsSource = from y in xx
select new { AssetId = $"{y.Type}:{y.Name}", y.Hash };
});
}
catch (Exception e)
{
Dispatcher.Invoke(() =>
{
DataGrid.ItemsSource = Enumerable.Repeat(new { AssetId = $"错误 {e}", Hash = e.ToString() }, 1);
});
}
}, TaskScheduler.Default);
/*_ = Task.Run(async () =>
{
var xxx = Enumerable.Repeat(new { AssetId = "正在准备", Hash = "正在准备" }, 1);
await Dispatcher.Invoke(async () =>
{
try
{
var xx = await x.ProcessDocument().ConfigureAwait(true);
DataGrid.ItemsSource = from y in xx
select new { AssetId = $"{y.Type}:{y.Name}", y.Hash };
}
catch (Exception e)
{
DataGrid.ItemsSource = Enumerable.Repeat(new { AssetId = $"错误 {e}", Hash = e.ToString() }, 1);
}
}).ConfigureAwait(true);
});*/
}
private void OnMainInputTextChanged(object sender, TextChangedEventArgs e)
{
/*var mainInput = _viewModel.MainInput;
if (sender is InputBar) if (sender is InputBar)
{ {
var inputValue = _model.Text; var inputValue = mainInput.Text;
if(inputValue != null) if(inputValue != null)
{ {
_model.Items = _suggestions.ProvideFileSystemSuggestions(inputValue) mainInput.Items = _suggestions.ProvideFileSystemSuggestions(inputValue)
.Prepend(new InputEntry(InputEntryType.Text, inputValue, inputValue)); .Prepend(new InputEntry(InputEntryType.Text, inputValue, inputValue));
} }
} }*/
}
} }
internal class ViewModel : INotifyPropertyChanged private void OnBigInputTextChanged(object sender, TextChangedEventArgs e)
{ {
#region INotifyPropertyChanged /*var mainInput = _viewModel.MainInput;
public event PropertyChangedEventHandler? PropertyChanged; if (sender is InputBar)
private void Notify(string name)
{ {
System.Diagnostics.Debug.WriteLine($"Updating {name}"); var inputValue = mainInput.Text;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); if (inputValue != null)
}
private void SetField<X>(ref X field, X value, [CallerMemberName] string? propertyName = null)
{ {
if (propertyName == null) mainInput.Items = _suggestions.ProvideFileSystemSuggestions(inputValue)
.Prepend(new InputEntry(InputEntryType.Text, inputValue, inputValue));
}
}*/
}
private void OnAssetInputTextChanged(object sender, TextChangedEventArgs e)
{ {
throw new InvalidOperationException(); /*var mainInput = _viewModel.MainInput;
} if (sender is InputBar)
if (EqualityComparer<X>.Default.Equals(field, value))
{ {
return; var inputValue = mainInput.Text;
} if (inputValue != null)
field = value;
Notify(propertyName);
}
#endregion
private IEnumerable<InputEntry>? _items;
public IEnumerable<InputEntry>? Items
{ {
get => _items; mainInput.Items = _suggestions.ProvideFileSystemSuggestions(inputValue)
set => SetField(ref _items, value); .Prepend(new InputEntry(InputEntryType.Text, inputValue, inputValue));
} }
}*/
private InputEntry? selectedItem;
public InputEntry? SelectedItem
{
get { return selectedItem; }
set { SetField(ref selectedItem, value); }
}
private string? selectedValue;
public string? SelectedValue
{
get { return selectedValue; }
set { SetField(ref selectedValue, value); }
}
private string? _text;
public string? Text
{
get { return _text; }
set { SetField(ref _text, value); }
} }
} }
} }

271
ModXml.cs Normal file
View File

@ -0,0 +1,271 @@
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Win32;
using Mvp.Xml.XInclude;
namespace HashCalculator.GUI
{
public class ModXml
{
protected enum DocumentOption
{
Normal,
IsEntryPoint
}
const string RegistryPath = @"Microsoft\Windows\CurrentVersion\Uninstall\{F6A3F605-7B10-4939-8D3D-4594332C1649}";
public static readonly IEnumerable<string> NotSupportedAttributes = new[]
{
"fragid", "set-xml-id", "encoding", "accept", "accept-language"
};
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;
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 ModUriResolver _uriResolver;
private readonly string _xmlFullPath;
public ModXml(string xmlPath)
{
var baseDirectory = new DirectoryInfo(FindBaseDirectory(xmlPath));
var allMods = baseDirectory.Parent;
if (!allMods.Name.Equals("Mods", StringComparison.OrdinalIgnoreCase))
{
throw new DirectoryNotFoundException();
}
var allModsParent = allMods.Parent;
using var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
if (!(hklm.GetValue(RegistryPath) is string sdkRootPath))
{
SdkNotFound = true;
sdkRootPath = allModsParent.FullName;
}
IReadOnlyCollection<string> GetPaths(string name) => new string[]
{
sdkRootPath,
allModsParent.FullName,
Path.Combine(baseDirectory.FullName, name),
Path.Combine(sdkRootPath, "Mods"),
allMods.FullName,
Path.Combine(sdkRootPath, name.Equals("Data", StringComparison.OrdinalIgnoreCase) ? "SageXml" : name)
};
_uriResolver = new ModUriResolver(new Dictionary<string, IReadOnlyCollection<string>>(StringComparer.OrdinalIgnoreCase)
{
{ "Art", GetPaths("Art") },
{ "Audio", GetPaths("Audio") },
{ "Data", GetPaths("Data") },
});
_xmlFullPath = Path.GetFullPath(xmlPath);
}
public Task<ICollection<AssetEntry>> ProcessDocument()
{
if (_processed.Any() || _assets.Any() || _errors.Any())
{
throw new InvalidOperationException();
}
return Task.Run(() =>
{
var includes = ProcessDocumentInternal(_xmlFullPath).AsParallel();
while (includes.Any())
{
includes = from include in includes
from newInclude in ProcessDocumentInternal(include)
select newInclude;
}
return _assets.Keys;
});
}
private string[] ProcessDocumentInternal(string fullPath)
{
if (!_processed.TryAdd(fullPath.ToUpperInvariant(), default))
{
return Array.Empty<string>();
}
var document = GetFile(fullPath);
if (document.Root.Name != EalaAsset + "AssetDeclaration" && document.Root.Name != "AssetDeclaration")
{
throw new NotSupportedException();
}
var items = from element in document.Root.Elements()
where element.Attribute("id") != null
select new AssetEntry(element);
foreach (var item in items)
{
if (!_assets.TryAdd(item, default))
{
_errors.Enqueue($"Attempted to add item `{item.Type}:{item.Name}` multiple times");
}
}
var includes = from include in document.Root.Elements(EalaAsset + "Includes").Elements()
let includePath = GetIncludePath(include, fullPath)
where includePath != null
select includePath;
return includes.ToArray();
}
private string? GetIncludePath(XElement include, string includerPath)
{
if (include.Name != EalaAsset + "Include")
{
throw new InvalidDataException();
}
if (include.Attribute("type")?.Value == "reference")
{
return null;
}
var source = include.Attribute("source")?.Value;
if (source == null)
{
throw new InvalidDataException();
}
string? includedSource;
try
{
includedSource = _uriResolver.ResolveUri(new Uri(includerPath), source).LocalPath;
}
catch (FileNotFoundException error)
{
_errors.Enqueue(error.Message);
return null;
}
return includedSource;
}
private XDocument GetFile(string normalizedPath)
{
using var reader = new XIncludingReader(normalizedPath, _uriResolver)
{
// I dont know why but looks like it need to be set again
// Otherwise it wont work
XmlResolver = _uriResolver
};
reader.MoveToContent();
return XDocument.Load(reader);
}
private static string FindBaseDirectory(string currentPath)
{
var file = new FileInfo(currentPath);
if (File.Exists(Path.Combine(file.DirectoryName, "Data", "mod.xml")))
{
return file.DirectoryName;
}
return FindBaseDirectory(file.DirectoryName);
}
}
public class ModUriResolver : XmlResolver
{
protected IReadOnlyDictionary<string, IReadOnlyCollection<string>> Paths { get; }
public ModUriResolver(IReadOnlyDictionary<string, IReadOnlyCollection<string>> paths)
{
Paths = paths;
}
public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn)
{
if (absoluteUri == null)
{
throw new ArgumentNullException($"{nameof(absoluteUri)} is null");
}
if (ofObjectToReturn != null && ofObjectToReturn != typeof(Stream))
{
throw new NotImplementedException();
}
return File.OpenRead(absoluteUri.LocalPath);
}
[SuppressMessage("Globalization", "CA1303:请不要将文本作为本地化参数传递", Justification = "<挂起>")]
public override Uri ResolveUri(Uri baseUri, string relativeUri)
{
if (baseUri == null)
{
if (!Path.IsPathRooted(relativeUri))
{
throw new ArgumentException($"{nameof(baseUri)} is null and {nameof(relativeUri)} is not rooted");
}
return new Uri(relativeUri);
}
if (relativeUri == null)
{
throw new ArgumentNullException($"{nameof(relativeUri)} is null");
}
return new Uri(Path.GetFullPath(ResolvePath(relativeUri, Path.GetDirectoryName(baseUri.LocalPath))));
}
public override Task<object> GetEntityAsync(Uri absoluteUri, string role, Type ofObjectToReturn)
{
return base.GetEntityAsync(absoluteUri, role, ofObjectToReturn);
}
public override bool SupportsType(Uri absoluteUri, Type type)
{
return base.SupportsType(absoluteUri, type);
}
[SuppressMessage("Globalization", "CA1303:请不要将文本作为本地化参数传递", Justification = "<挂起>")]
private string ResolvePath(string href, string currentDirectory)
{
var splitted = href.Split(new[] { ':' }, 2);
if (splitted.Length > 1)
{
var prefix = splitted[0];
var path = splitted[1];
if (Paths.TryGetValue(prefix, out var collection))
{
foreach (var directory in collection)
{
if (prefix.Equals("Art", StringComparison.OrdinalIgnoreCase))
{
var artPath = Path.Combine(directory, path.Substring(0, 2), path);
if (File.Exists(artPath))
{
return artPath;
}
}
var realPath = Path.Combine(directory, path);
if (File.Exists(realPath))
{
return realPath;
}
}
throw new FileNotFoundException("Cannot find the file specified", href);
}
throw new FileNotFoundException($"Unknown prefix: {prefix}", href);
}
var relativePath = Path.Combine(currentDirectory, href);
if (!File.Exists(relativePath))
{
throw new FileNotFoundException("Cannot find the file specified", href);
}
return relativePath;
}
}
}

33
Model.cs Normal file
View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace HashCalculator.GUI
{
internal class Model
{
private readonly ViewModel _viewModel;
private readonly FileSystemSuggestions _suggestions = new FileSystemSuggestions();
public Model(ViewModel viewModel)
{
_viewModel = viewModel;
}
public void UpdateMainInputSuggestions()
{
var mainInput = _viewModel.MainInput;
var inputValue = mainInput.Text;
if (inputValue != null)
{
mainInput.Items = _suggestions.ProvideFileSystemSuggestions(inputValue)
.Prepend(new InputEntry(InputEntryType.Text, inputValue, inputValue));
}
}
}
}

49
SageHash.cs Normal file
View File

@ -0,0 +1,49 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using TechnologyAssembler.Core.Hashing;
using System.Xml;
namespace HashCalculator.GUI
{
public static class SageHash
{
public static uint CalculateBinaryHash(byte[] content)
{
if (content == null)
{
throw new ArgumentNullException($"{nameof(content)} is null");
}
return FastHash.GetHashCode(content);
}
public static uint CalculateBinaryHash(string content)
{
if (content == null)
{
throw new ArgumentNullException($"{nameof(content)} is null");
}
return CalculateBinaryHash(Encoding.ASCII.GetBytes(content));
}
[SuppressMessage("Globalization", "CA1308:将字符串规范化为大写", Justification = "<挂起>")]
public static uint CalculateLowercaseHash(string content)
{
if(content == null)
{
throw new ArgumentNullException($"{nameof(content)} is null");
}
return CalculateBinaryHash(content.ToLowerInvariant());
}
public static uint CalculateFileHash(string path)
{
if(path == null)
{
throw new ArgumentNullException($"{nameof(path)} is null");
}
return CalculateBinaryHash(File.ReadAllBytes(path));
}
}
}

83
ViewModel.cs Normal file
View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace HashCalculator.GUI
{
internal abstract class NotifyPropertyChanged : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
protected void Notify(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
protected void SetField<X>(ref X field, X value, [CallerMemberName] string? propertyName = null)
{
if (propertyName == null)
{
throw new InvalidOperationException();
}
if (EqualityComparer<X>.Default.Equals(field, value))
{
return;
}
field = value;
Notify(propertyName);
}
#endregion
}
internal class ViewModel : NotifyPropertyChanged
{
public InputBarViewModel MainInput { get; } = new InputBarViewModel();
public InputBarViewModel BigEntryInput { get; } = new InputBarViewModel();
public InputBarViewModel AssetIdInput { get; } = new InputBarViewModel();
public Model Model { get; }
public ViewModel()
{
Model = new Model(this);
}
}
internal class InputBarViewModel : NotifyPropertyChanged
{
private IEnumerable<InputEntry>? _items;
public IEnumerable<InputEntry>? Items
{
get => _items;
set => SetField(ref _items, value);
}
private InputEntry? selectedItem;
public InputEntry? SelectedItem
{
get { return selectedItem; }
set { SetField(ref selectedItem, value); }
}
private string? selectedValue;
public string? SelectedValue
{
get { return selectedValue; }
set { SetField(ref selectedValue, value); }
}
private string? _text;
public string? Text
{
get { return _text; }
set { SetField(ref _text, value); }
}
}
}