支持加载 XSD
This commit is contained in:
parent
289f6551c1
commit
ce26ef298e
@ -37,11 +37,6 @@ namespace HashCalculator.GUI
|
|||||||
|
|
||||||
public static AssetEntry? TryParse(XElement element)
|
public static AssetEntry? TryParse(XElement element)
|
||||||
{
|
{
|
||||||
if (element == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.Name.Namespace != ModXml.EalaAsset)
|
if (element.Name.Namespace != ModXml.EalaAsset)
|
||||||
{
|
{
|
||||||
TracerListener.WriteLine($"Unknown namespace: {element.Name.Namespace}");
|
TracerListener.WriteLine($"Unknown namespace: {element.Name.Namespace}");
|
||||||
@ -59,6 +54,27 @@ namespace HashCalculator.GUI
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AssetEntry? TryParseXsd(XElement element)
|
||||||
|
{
|
||||||
|
if (element.Name.Namespace != ModXsd.Schema)
|
||||||
|
{
|
||||||
|
TracerListener.WriteLine($"Unknown namespace: {element.Name.Namespace}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var type = element.Attribute("name")?.Value
|
||||||
|
?? throw new NotSupportedException();
|
||||||
|
return new AssetEntry(element.Name.LocalName, type);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
TracerListener.WriteLine($"Failed to parse element: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public AssetEntry(XElement element)
|
public AssetEntry(XElement element)
|
||||||
{
|
{
|
||||||
if (element == null)
|
if (element == null)
|
||||||
@ -107,6 +123,18 @@ namespace HashCalculator.GUI
|
|||||||
TypeIdString = $"{TypeId:x8} ({TypeId,10})";
|
TypeIdString = $"{TypeId:x8} ({TypeId,10})";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AssetEntry(string kind, string type)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
Name = string.Empty;
|
||||||
|
NameString = $"<{kind} {Type}>";
|
||||||
|
DisplayLabels = Array.Empty<string>();
|
||||||
|
InstanceId = 0;
|
||||||
|
TypeId = SageHash.CalculateBinaryHash(Type);
|
||||||
|
InstanceIdString = string.Empty;
|
||||||
|
TypeIdString = $"{TypeId:x8} ({TypeId,10})";
|
||||||
|
}
|
||||||
|
|
||||||
public bool Equals(AssetEntry? entry)
|
public bool Equals(AssetEntry? entry)
|
||||||
{
|
{
|
||||||
return this == entry;
|
return this == entry;
|
||||||
|
@ -46,14 +46,14 @@ namespace HashCalculator.GUI
|
|||||||
ViewModel.StatusText = string.Empty;
|
ViewModel.StatusText = string.Empty;
|
||||||
return;
|
return;
|
||||||
case InputEntryType.BigFile:
|
case InputEntryType.BigFile:
|
||||||
await LoadBig(selected.Value).ConfigureAwait(true);
|
await LoadBig(selected.Value);
|
||||||
return;
|
return;
|
||||||
case InputEntryType.ManifestFile:
|
case InputEntryType.ManifestFile:
|
||||||
await LoadManifestFromFile(selected.Value).ConfigureAwait(true);
|
await LoadManifestFromFile(selected.Value);
|
||||||
return;
|
return;
|
||||||
case InputEntryType.XmlFile:
|
case InputEntryType.XmlFile:
|
||||||
ViewModel.IsXml = true;
|
case InputEntryType.XsdFile:
|
||||||
await LoadXml(selected.Value).ConfigureAwait(true);
|
await LoadXml(selected);
|
||||||
return;
|
return;
|
||||||
default:
|
default:
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
@ -125,9 +125,8 @@ namespace HashCalculator.GUI
|
|||||||
var first = bigViewModel.AllManifests.FirstOrDefault();
|
var first = bigViewModel.AllManifests.FirstOrDefault();
|
||||||
if (first != null)
|
if (first != null)
|
||||||
{
|
{
|
||||||
var name = VirtualFileSystem.GetFileNameWithoutExtension(first.Value);
|
var name = VirtualFileSystem.GetFileNameWithoutExtension(first.Value).ToLowerInvariant();
|
||||||
if (name.StartsWith("mod", StringComparison.OrdinalIgnoreCase)
|
if (new[] { "mod", "mapmetadata_mod", "static", "global" }.Any(name.StartsWith))
|
||||||
|| name.StartsWith("mapmetadata", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
bigViewModel.SelectedItem = first;
|
bigViewModel.SelectedItem = first;
|
||||||
await bigViewModel.SelectCommand.ExecuteTask(ViewModel).ConfigureAwait(true);
|
await bigViewModel.SelectCommand.ExecuteTask(ViewModel).ConfigureAwait(true);
|
||||||
@ -189,7 +188,7 @@ namespace HashCalculator.GUI
|
|||||||
tokenSource.Cancel();
|
tokenSource.Cancel();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await task.ConfigureAwait(true);
|
await task;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@ -197,11 +196,21 @@ namespace HashCalculator.GUI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadXml(string path)
|
private async Task LoadXml(InputEntry entry)
|
||||||
{
|
{
|
||||||
await CancelLoadingXml().ConfigureAwait(true);
|
await CancelLoadingXml();
|
||||||
|
var path = entry.Text;
|
||||||
|
var type = entry.Type;
|
||||||
using var tokenSource = new CancellationTokenSource();
|
using var tokenSource = new CancellationTokenSource();
|
||||||
var task = LoadXmlInternal(path, tokenSource.Token);
|
var modXml = type switch
|
||||||
|
{
|
||||||
|
InputEntryType.XmlFile => LoadModXml(path, tokenSource.Token),
|
||||||
|
InputEntryType.XsdFile => LoadModXsd(path, tokenSource.Token),
|
||||||
|
_ => throw new NotSupportedException(type.ToString())
|
||||||
|
};
|
||||||
|
ViewModel.IsXml = true;
|
||||||
|
ViewModel.IsXsd = type == InputEntryType.XsdFile;
|
||||||
|
var task = LoadXmlInternal(await modXml, tokenSource.Token);
|
||||||
_lastXmlTask = (tokenSource, task).ToTuple();
|
_lastXmlTask = (tokenSource, task).ToTuple();
|
||||||
|
|
||||||
await ExceptionWrapepr(async () =>
|
await ExceptionWrapepr(async () =>
|
||||||
@ -229,23 +238,32 @@ namespace HashCalculator.GUI
|
|||||||
}, "SAGE FastHash 计算器 - XML 加载失败…", "XML 文件加载失败,也许,你选择的 XML 文件并不是红警3使用的那种……\r\n").ConfigureAwait(true);
|
}, "SAGE FastHash 计算器 - XML 加载失败…", "XML 文件加载失败,也许,你选择的 XML 文件并不是红警3使用的那种……\r\n").ConfigureAwait(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadXmlInternal(string path, CancellationToken token)
|
private async Task<IModXml> LoadModXml(string path, CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
if (ModXml.LocateSdkFromRegistry() is not { } sdkRoot)
|
if (ModXml.LocateSdkFromRegistry() is not { } sdkRoot)
|
||||||
{
|
{
|
||||||
var studio = await RequestOpenFile("选择 Mod Studio", ("EALAModStudio.exe", null));
|
var studio = await RequestOpenFile("选择 Mod Studio", ("EALAModStudio.exe", null));
|
||||||
sdkRoot = Path.GetDirectoryName(studio);
|
sdkRoot = Path.GetDirectoryName(studio);
|
||||||
}
|
}
|
||||||
var modXml = new ModXml(path, sdkRoot, token);
|
var modXml = new ModXml(path, sdkRoot, cancelToken);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await FindCsf(modXml.BaseDirectory).ConfigureAwait(true);
|
await FindCsf(modXml.BaseDirectory, cancelToken);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
TracerListener.WriteLine($"[ModXml] Failed to automatically find and load CSF: {exception}");
|
TracerListener.WriteLine($"[ModXml] Failed to automatically find and load CSF: {exception}");
|
||||||
}
|
}
|
||||||
|
return modXml;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<IModXml> LoadModXsd(string path, CancellationToken token)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IModXml>(new ModXsd(path, token));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadXmlInternal(IModXml modXml, CancellationToken token)
|
||||||
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var start = DateTimeOffset.UtcNow;
|
var start = DateTimeOffset.UtcNow;
|
||||||
@ -333,9 +351,9 @@ namespace HashCalculator.GUI
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task FindCsf(DirectoryInfo baseDirectory)
|
private static Task FindCsf(DirectoryInfo baseDirectory, CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
return Task.Run(() =>
|
return Task.Run(async () =>
|
||||||
{
|
{
|
||||||
if (!baseDirectory.Exists)
|
if (!baseDirectory.Exists)
|
||||||
{
|
{
|
||||||
@ -355,6 +373,7 @@ namespace HashCalculator.GUI
|
|||||||
from data in directories.GetDirectories("Data")
|
from data in directories.GetDirectories("Data")
|
||||||
from csf in data.GetFiles("*.csf")
|
from csf in data.GetFiles("*.csf")
|
||||||
select csf;
|
select csf;
|
||||||
|
csfs = csfs.AsParallel().WithCancellation(cancelToken);
|
||||||
var result =
|
var result =
|
||||||
csfs.FirstOrDefault(x => x.Name.Equals("gamestrings.csf", StringComparison.OrdinalIgnoreCase))
|
csfs.FirstOrDefault(x => x.Name.Equals("gamestrings.csf", StringComparison.OrdinalIgnoreCase))
|
||||||
?? csfs.FirstOrDefault();
|
?? csfs.FirstOrDefault();
|
||||||
@ -363,8 +382,13 @@ namespace HashCalculator.GUI
|
|||||||
throw new FileNotFoundException();
|
throw new FileNotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
using var fileStream = result.OpenRead();
|
using var memoryStream = new MemoryStream();
|
||||||
Translator.Provider = new CsfTranslationProvider(fileStream);
|
using (var fileStream = result.OpenRead())
|
||||||
|
{
|
||||||
|
await fileStream.CopyToAsync(memoryStream, cancelToken);
|
||||||
|
memoryStream.Position = 0;
|
||||||
|
}
|
||||||
|
Translator.Provider = new CsfTranslationProvider(memoryStream);
|
||||||
TracerListener.WriteLine($"[ModXml]: Automatically loaded csf from `{result.FullName}`");
|
TracerListener.WriteLine($"[ModXml]: Automatically loaded csf from `{result.FullName}`");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
20
Converters/MultiBooleanConjunctionConverter.cs
Normal file
20
Converters/MultiBooleanConjunctionConverter.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Data;
|
||||||
|
|
||||||
|
namespace HashCalculator.GUI.Converters
|
||||||
|
{
|
||||||
|
class MultiBooleanConjunctionConverter : IMultiValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
return values.Cast<bool>().Aggregate((a, b) => a && b);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
Converters/MultiValueAggregateConverter.cs
Normal file
45
Converters/MultiValueAggregateConverter.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Data;
|
||||||
|
|
||||||
|
namespace HashCalculator.GUI.Converters
|
||||||
|
{
|
||||||
|
class MultiValueAggregateConverter : List<IValueConverter?>, IMultiValueConverter
|
||||||
|
{
|
||||||
|
public IMultiValueConverter? Converter { get; set; }
|
||||||
|
public IValueConverter? PostProcess { get; set; }
|
||||||
|
|
||||||
|
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (Converter is not { } converter)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{nameof(Converter)} of {nameof(MultiValueAggregateConverter)} is null");
|
||||||
|
}
|
||||||
|
if (Count > 0)
|
||||||
|
{
|
||||||
|
values = values.ToArray(); // 复制一份
|
||||||
|
var length = Math.Min(Count, values.Length);
|
||||||
|
for (var i = 0; i < length; ++i)
|
||||||
|
{
|
||||||
|
if (this[i] is IValueConverter c)
|
||||||
|
{
|
||||||
|
values[i] = c.Convert(values[i], targetType, parameter, culture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var result = converter.Convert(values, targetType, parameter, culture);
|
||||||
|
if (PostProcess is not { } postConverter)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return postConverter.Convert(result, targetType, parameter, culture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,17 +8,17 @@ namespace HashCalculator.GUI
|
|||||||
{
|
{
|
||||||
internal class FileSystemSuggestions
|
internal class FileSystemSuggestions
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<string, InputEntryType> Mapping = new Dictionary<string, InputEntryType>(StringComparer.OrdinalIgnoreCase)
|
private static readonly Dictionary<string, InputEntryType> Mapping = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ ".big", InputEntryType.BigFile },
|
{ ".big", InputEntryType.BigFile },
|
||||||
{ ".xml", InputEntryType.XmlFile },
|
{ ".xml", InputEntryType.XmlFile },
|
||||||
{ ".w3x", InputEntryType.XmlFile },
|
{ ".w3x", InputEntryType.XmlFile },
|
||||||
{ ".manifest", InputEntryType.ManifestFile },
|
{ ".manifest", InputEntryType.ManifestFile },
|
||||||
|
{ ".xsd", InputEntryType.XsdFile },
|
||||||
};
|
};
|
||||||
|
|
||||||
private Search _search = new Search();
|
private Search _search = new Search();
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1031")]
|
|
||||||
public IEnumerable<InputEntry> ProvideFileSystemSuggestions(string? path)
|
public IEnumerable<InputEntry> ProvideFileSystemSuggestions(string? path)
|
||||||
{
|
{
|
||||||
var empty = Enumerable.Empty<InputEntry>();
|
var empty = Enumerable.Empty<InputEntry>();
|
||||||
|
15
IModXml.cs
Normal file
15
IModXml.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace HashCalculator.GUI
|
||||||
|
{
|
||||||
|
interface IModXml
|
||||||
|
{
|
||||||
|
public int TotalFilesProcessed { get; }
|
||||||
|
public int TotalAssets { get; }
|
||||||
|
IAsyncEnumerable<AssetEntry> ProcessDocument();
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,4 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using TechnologyAssembler.Core.IO;
|
|
||||||
|
|
||||||
namespace HashCalculator.GUI
|
namespace HashCalculator.GUI
|
||||||
{
|
{
|
||||||
@ -13,18 +8,20 @@ namespace HashCalculator.GUI
|
|||||||
BigFile,
|
BigFile,
|
||||||
ManifestFile,
|
ManifestFile,
|
||||||
XmlFile,
|
XmlFile,
|
||||||
|
XsdFile,
|
||||||
BinaryFile,
|
BinaryFile,
|
||||||
Path,
|
Path,
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class InputEntry
|
internal sealed class InputEntry
|
||||||
{
|
{
|
||||||
public static IReadOnlyDictionary<InputEntryType, string> Descriptions = new Dictionary<InputEntryType, string>
|
public static IReadOnlyDictionary<InputEntryType, string> Descriptions = new Dictionary<InputEntryType, string>
|
||||||
{
|
{
|
||||||
{ InputEntryType.BigFile, "可以尝试读取这个 big 文件里的 manifest 文件" },
|
{ InputEntryType.BigFile, "可以尝试读取这个 big 文件里的 manifest 文件" },
|
||||||
{ InputEntryType.BinaryFile, "可以计算这个文件的哈希值" },
|
{ InputEntryType.BinaryFile, "可以计算这个文件的哈希值" },
|
||||||
{ InputEntryType.ManifestFile, "可以尝试读取这个 manifest 文件,显示各个素材的哈希值" },
|
{ InputEntryType.ManifestFile, "可以尝试读取这个 manifest 文件,显示各个素材的哈希值" },
|
||||||
{ InputEntryType.XmlFile, "可以尝试读取这个XML,显示 XML 里定义的各个素材的哈希值" },
|
{ InputEntryType.XmlFile, "可以尝试读取这个 XML,显示 XML 里定义的各个素材的哈希值" },
|
||||||
|
{ InputEntryType.XsdFile, "可以尝试读取 XSD,显示 XSD 里定义的各个素材的哈希值" },
|
||||||
{ InputEntryType.Path, string.Empty },
|
{ InputEntryType.Path, string.Empty },
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -33,13 +30,13 @@ namespace HashCalculator.GUI
|
|||||||
public string Text { get; }
|
public string Text { get; }
|
||||||
public string Details
|
public string Details
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if(Type == InputEntryType.Text)
|
if (Type == InputEntryType.Text)
|
||||||
{
|
{
|
||||||
var hash = SageHash.CalculateLowercaseHash(Value);
|
var hash = SageHash.CalculateLowercaseHash(Value);
|
||||||
var binaryHash = SageHash.CalculateBinaryHash(Value);
|
var binaryHash = SageHash.CalculateBinaryHash(Value);
|
||||||
return hash == binaryHash
|
return hash == binaryHash
|
||||||
? $"这段文字的哈希值:{hash:x8} ({hash})"
|
? $"这段文字的哈希值:{hash:x8} ({hash})"
|
||||||
: $"这段文字的哈希值:{hash:x8};大小写敏感哈希值 {binaryHash:x8}";
|
: $"这段文字的哈希值:{hash:x8};大小写敏感哈希值 {binaryHash:x8}";
|
||||||
}
|
}
|
||||||
|
104
MainWindow.xaml
104
MainWindow.xaml
@ -6,6 +6,8 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:l="clr-namespace:HashCalculator.GUI"
|
xmlns:l="clr-namespace:HashCalculator.GUI"
|
||||||
|
xmlns:c="clr-namespace:HashCalculator.GUI.Converters"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=System.Runtime"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="SAGE FastHash 哈希计算器"
|
Title="SAGE FastHash 哈希计算器"
|
||||||
Height="600"
|
Height="600"
|
||||||
@ -152,47 +154,54 @@
|
|||||||
Height="25"
|
Height="25"
|
||||||
Margin="0,0,0,5"
|
Margin="0,0,0,5"
|
||||||
>
|
>
|
||||||
<Grid
|
<StackPanel
|
||||||
DockPanel.Dock="Right"
|
DockPanel.Dock="Right"
|
||||||
Width="190"
|
Orientation="Horizontal"
|
||||||
|
d:Visibility="Visible"
|
||||||
Visibility="{Binding IsXml,
|
Visibility="{Binding IsXml,
|
||||||
Converter={StaticResource BooleanToVisibilityConverter}}"
|
Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
Content="{Binding LoadCsfText}"
|
Content="取消加载 XML"
|
||||||
Command="{Binding LoadCsf}"
|
|
||||||
CommandParameter="{Binding ElementName=Self}"
|
|
||||||
Width="92"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
Content="取消加载"
|
|
||||||
Command="{Binding CancelXml}"
|
Command="{Binding CancelXml}"
|
||||||
Width="92"
|
Padding="8,0"
|
||||||
HorizontalAlignment="Right"
|
Margin="0,0,8,0"
|
||||||
Visibility="{Binding IsLoadingXml,
|
Visibility="{Binding IsLoadingXml,
|
||||||
Converter={StaticResource BooleanToVisibilityConverter}}"
|
Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
<Button
|
||||||
<CheckBox
|
Content="{Binding LoadCsfText}"
|
||||||
|
Command="{Binding LoadCsf}"
|
||||||
|
CommandParameter="{Binding ElementName=Self}"
|
||||||
|
Padding="16,0"
|
||||||
|
Visibility="{Binding IsXsd,
|
||||||
|
Converter={StaticResource FalseToVisibilityConverter}}"
|
||||||
|
/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel
|
||||||
DockPanel.Dock="Right"
|
DockPanel.Dock="Right"
|
||||||
Content="只显示 GameObject"
|
Orientation="Horizontal"
|
||||||
IsChecked="{Binding GameObjectOnly}"
|
Visibility="{Binding IsXsd,
|
||||||
Command="{Binding FilterDisplayEntries}"
|
Converter={StaticResource FalseToVisibilityConverter}}"
|
||||||
Padding="4,0"
|
>
|
||||||
Margin="8,0"
|
<CheckBox
|
||||||
VerticalAlignment="Center"
|
Content="只显示 GameObject"
|
||||||
/>
|
IsChecked="{Binding GameObjectOnly}"
|
||||||
<CheckBox
|
Command="{Binding FilterDisplayEntries}"
|
||||||
DockPanel.Dock="Right"
|
Padding="4,0"
|
||||||
Content="搜索类型 ID"
|
Margin="8,0"
|
||||||
IsChecked="{Binding ConsiderTypeId}"
|
VerticalAlignment="Center"
|
||||||
Command="{Binding FilterDisplayEntries}"
|
/>
|
||||||
Padding="4,0"
|
<CheckBox
|
||||||
Margin="8,0"
|
Content="搜索类型 ID"
|
||||||
VerticalAlignment="Center"
|
IsChecked="{Binding ConsiderTypeId}"
|
||||||
Visibility="{Binding GameObjectOnly, Converter={StaticResource FalseToVisibilityConverter}}"
|
Command="{Binding FilterDisplayEntries}"
|
||||||
/>
|
Padding="4,0"
|
||||||
|
Margin="8,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Text="{Binding DisplayEntries.Count, StringFormat=列表里显示了{0}个素材}"
|
Text="{Binding DisplayEntries.Count, StringFormat=列表里显示了{0}个素材}"
|
||||||
Margin="5,0,0,0"
|
Margin="5,0,0,0"
|
||||||
@ -232,6 +241,18 @@
|
|||||||
EnableRowVirtualization="True"
|
EnableRowVirtualization="True"
|
||||||
VirtualizingPanel.VirtualizationMode="Recycling"
|
VirtualizingPanel.VirtualizationMode="Recycling"
|
||||||
>
|
>
|
||||||
|
<DataGrid.Resources>
|
||||||
|
<c:MultiValueAggregateConverter x:Key="IsXmlNotXsd">
|
||||||
|
<c:MultiValueAggregateConverter.Converter>
|
||||||
|
<c:MultiBooleanConjunctionConverter />
|
||||||
|
</c:MultiValueAggregateConverter.Converter>
|
||||||
|
<c:MultiValueAggregateConverter.PostProcess>
|
||||||
|
<BooleanToVisibilityConverter />
|
||||||
|
</c:MultiValueAggregateConverter.PostProcess>
|
||||||
|
<x:Null />
|
||||||
|
<c:BooleanInvertConverter />
|
||||||
|
</c:MultiValueAggregateConverter>
|
||||||
|
</DataGrid.Resources>
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn
|
<DataGridTextColumn
|
||||||
Header="类型/素材名称"
|
Header="类型/素材名称"
|
||||||
@ -241,14 +262,27 @@
|
|||||||
Header="哈希(InstanceId)"
|
Header="哈希(InstanceId)"
|
||||||
Binding="{Binding InstanceIdString}"
|
Binding="{Binding InstanceIdString}"
|
||||||
FontFamily="Courier New"
|
FontFamily="Courier New"
|
||||||
|
Visibility="{Binding Source={x:Reference Name=ReferenceProvider},
|
||||||
|
Path=DataContext.IsXsd,
|
||||||
|
Converter={StaticResource FalseToVisibilityConverter}}"
|
||||||
/>
|
/>
|
||||||
<DataGridTextColumn
|
<DataGridTextColumn
|
||||||
Header="名称"
|
Header="名称"
|
||||||
Binding="{Binding LocalizedNames}"
|
Binding="{Binding LocalizedNames}"
|
||||||
Visibility="{Binding Source={x:Reference Name=ReferenceProvider},
|
>
|
||||||
Path=DataContext.IsXml,
|
<DataGridTextColumn.Visibility>
|
||||||
Converter={StaticResource BooleanToVisibilityConverter}}"
|
<MultiBinding Converter="{StaticResource IsXmlNotXsd}">
|
||||||
/>
|
<Binding
|
||||||
|
Source="{x:Reference Name=ReferenceProvider}"
|
||||||
|
Path="DataContext.IsXml"
|
||||||
|
/>
|
||||||
|
<Binding
|
||||||
|
Source="{x:Reference Name=ReferenceProvider}"
|
||||||
|
Path="DataContext.IsXsd"
|
||||||
|
/>
|
||||||
|
</MultiBinding>
|
||||||
|
</DataGridTextColumn.Visibility>
|
||||||
|
</DataGridTextColumn>
|
||||||
<DataGridTextColumn
|
<DataGridTextColumn
|
||||||
Header="类型 ID"
|
Header="类型 ID"
|
||||||
Binding="{Binding TypeIdString}"
|
Binding="{Binding TypeIdString}"
|
||||||
|
11
ModXml.cs
11
ModXml.cs
@ -1,7 +1,8 @@
|
|||||||
using System;
|
using Microsoft.Win32;
|
||||||
using System.Collections.Generic;
|
using Mvp.Xml.XInclude;
|
||||||
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -9,12 +10,10 @@ using System.Threading.Tasks;
|
|||||||
using System.Threading.Tasks.Dataflow;
|
using System.Threading.Tasks.Dataflow;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Microsoft.Win32;
|
|
||||||
using Mvp.Xml.XInclude;
|
|
||||||
|
|
||||||
namespace HashCalculator.GUI
|
namespace HashCalculator.GUI
|
||||||
{
|
{
|
||||||
public class ModXml
|
public class ModXml : IModXml
|
||||||
{
|
{
|
||||||
protected enum DocumentOption
|
protected enum DocumentOption
|
||||||
{
|
{
|
||||||
|
164
ModXsd.cs
Normal file
164
ModXsd.cs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
using Mvp.Xml.XInclude;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading.Tasks.Dataflow;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
namespace HashCalculator.GUI
|
||||||
|
{
|
||||||
|
public class ModXsd : IModXml
|
||||||
|
{
|
||||||
|
protected enum DocumentOption
|
||||||
|
{
|
||||||
|
Normal,
|
||||||
|
IsEntryPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly IEnumerable<string> NotSupportedAttributes = new[]
|
||||||
|
{
|
||||||
|
"fragid", "set-xml-id", "encoding", "accept", "accept-language"
|
||||||
|
};
|
||||||
|
public static XNamespace Schema = "http://www.w3.org/2001/XMLSchema";
|
||||||
|
public int TotalFilesProcessed => _processed.Count;
|
||||||
|
public int TotalAssets => _assets.Count;
|
||||||
|
private readonly ConcurrentDictionary<string, byte> _processed = new();
|
||||||
|
private readonly ConcurrentDictionary<AssetEntry, string> _assets = new();
|
||||||
|
private readonly BufferBlock<AssetEntry> _entries = new();
|
||||||
|
private readonly string _xmlFullPath;
|
||||||
|
private readonly CancellationToken _token;
|
||||||
|
|
||||||
|
public ModXsd(string xmlPath, CancellationToken token)
|
||||||
|
{
|
||||||
|
_xmlFullPath = Path.GetFullPath(xmlPath);
|
||||||
|
_token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<AssetEntry> ProcessDocument()
|
||||||
|
{
|
||||||
|
if (_processed.Any() || _assets.Any())
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var task = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ProcessDocumentInternal(_xmlFullPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if (!_token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_entries.Complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while (await _entries.OutputAvailableAsync())
|
||||||
|
{
|
||||||
|
yield return _entries.Receive();
|
||||||
|
}
|
||||||
|
|
||||||
|
await task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessDocumentInternal(string fullPath)
|
||||||
|
{
|
||||||
|
_token.ThrowIfCancellationRequested();
|
||||||
|
if (!_processed.TryAdd(fullPath.ToUpperInvariant(), default))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var document = await GetFileAsync(fullPath);
|
||||||
|
var rootName = document.Root?.Name
|
||||||
|
?? throw new InvalidDataException("Document doesn't have a root");
|
||||||
|
if (rootName != Schema + "schema")
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var includes = from include in document.Root.Elements(Schema + "include")
|
||||||
|
let includePath = GetIncludePath(include, fullPath)
|
||||||
|
where includePath != null
|
||||||
|
select Task.Run(() => ProcessDocumentInternal(includePath), _token);
|
||||||
|
var includesTasks = includes.ToArray();
|
||||||
|
|
||||||
|
var items = from element in document.Root.Elements()
|
||||||
|
let name = element.Name
|
||||||
|
where name == Schema + "simpleType" || name == Schema + "complexType"
|
||||||
|
let entry = AssetEntry.TryParseXsd(element)
|
||||||
|
where entry != null
|
||||||
|
select entry;
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (_assets.TryAdd(item, fullPath))
|
||||||
|
{
|
||||||
|
_entries.Post(item);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var previousPath = _assets[item];
|
||||||
|
TracerListener.WriteLine($"[ModXsd] `{fullPath}`: Attempted to add item `{item.Type}:{item.Name}` multiple times - already processed in `{previousPath}`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(includesTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetIncludePath(XElement include, string includerPath)
|
||||||
|
{
|
||||||
|
if (Path.GetDirectoryName(includerPath) is not { } currentDirectory)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (include.Name != Schema + "include")
|
||||||
|
{
|
||||||
|
throw new InvalidDataException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var source = include.Attribute("schemaLocation")?.Value;
|
||||||
|
if (source is null)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var includedSource = Path.GetFullPath(source, currentDirectory);
|
||||||
|
if (!File.Exists(includedSource))
|
||||||
|
{
|
||||||
|
TracerListener.WriteLine($"[ModXsd]: Warning, include path does not exist! It is: {includedSource}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return includedSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<XDocument> GetFileAsync(string normalizedPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var xml = await File.ReadAllBytesAsync(normalizedPath, _token);
|
||||||
|
using var xmlStream = new MemoryStream(xml);
|
||||||
|
using var reader = new XIncludingReader(normalizedPath, xmlStream);
|
||||||
|
|
||||||
|
reader.MoveToContent();
|
||||||
|
return XDocument.Load(reader);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Failed to process XML file: {normalizedPath} - {e.Message}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
ViewModel.cs
39
ViewModel.cs
@ -37,6 +37,23 @@ namespace HashCalculator.GUI
|
|||||||
|
|
||||||
Notify(propertyName);
|
Notify(propertyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void SetField<X>(ref X field, X value, Action onChange, [CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
if (propertyName == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EqualityComparer<X>.Default.Equals(field, value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
field = value;
|
||||||
|
onChange();
|
||||||
|
Notify(propertyName);
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,25 +70,21 @@ namespace HashCalculator.GUI
|
|||||||
public string? Filter
|
public string? Filter
|
||||||
{
|
{
|
||||||
get => _filter;
|
get => _filter;
|
||||||
set
|
set => SetField(ref _filter, value, () => FilterDisplayEntries.Execute(null));
|
||||||
{
|
|
||||||
SetField(ref _filter, value);
|
|
||||||
FilterDisplayEntries.Execute(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _gameObjectOnly;
|
private bool _gameObjectOnly;
|
||||||
public bool GameObjectOnly
|
public bool GameObjectOnly
|
||||||
{
|
{
|
||||||
get => _gameObjectOnly;
|
get => _gameObjectOnly;
|
||||||
set => SetField(ref _gameObjectOnly, value);
|
set => SetField(ref _gameObjectOnly, value, () => ConsiderTypeId &= !value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _considerTypeId;
|
private bool _considerTypeId;
|
||||||
public bool ConsiderTypeId
|
public bool ConsiderTypeId
|
||||||
{
|
{
|
||||||
get => _considerTypeId;
|
get => _considerTypeId;
|
||||||
set => SetField(ref _considerTypeId, value);
|
set => SetField(ref _considerTypeId, value, () => GameObjectOnly &= !value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Command FilterDisplayEntries { get; }
|
public Command FilterDisplayEntries { get; }
|
||||||
@ -95,7 +108,7 @@ namespace HashCalculator.GUI
|
|||||||
public bool IsXml
|
public bool IsXml
|
||||||
{
|
{
|
||||||
get => _isXml;
|
get => _isXml;
|
||||||
set => SetField(ref _isXml, value);
|
set => SetField(ref _isXml, value, () => IsXsd &= value);
|
||||||
}
|
}
|
||||||
private bool _isLoadingXml;
|
private bool _isLoadingXml;
|
||||||
public bool IsLoadingXml
|
public bool IsLoadingXml
|
||||||
@ -103,6 +116,16 @@ namespace HashCalculator.GUI
|
|||||||
get => _isLoadingXml;
|
get => _isLoadingXml;
|
||||||
set => SetField(ref _isLoadingXml, value);
|
set => SetField(ref _isLoadingXml, value);
|
||||||
}
|
}
|
||||||
|
private bool _isXsd;
|
||||||
|
public bool IsXsd
|
||||||
|
{
|
||||||
|
get => _isXsd;
|
||||||
|
set => SetField(ref _isXsd, value, () =>
|
||||||
|
{
|
||||||
|
IsXml |= value;
|
||||||
|
ConsiderTypeId |= value;
|
||||||
|
});
|
||||||
|
}
|
||||||
public Command CancelXml { get; }
|
public Command CancelXml { get; }
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1822")]
|
[SuppressMessage("Microsoft.Performance", "CA1822")]
|
||||||
public string LoadCsfText => $"{(Translator.HasProvider ? "更换" : "加载")} CSF";
|
public string LoadCsfText => $"{(Translator.HasProvider ? "更换" : "加载")} CSF";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user