支持加载 XSD

This commit is contained in:
lanyi 2022-01-25 19:05:37 +01:00
parent 289f6551c1
commit ce26ef298e
11 changed files with 435 additions and 86 deletions

View File

@ -37,11 +37,6 @@ namespace HashCalculator.GUI
public static AssetEntry? TryParse(XElement element)
{
if (element == null)
{
return null;
}
if (element.Name.Namespace != ModXml.EalaAsset)
{
TracerListener.WriteLine($"Unknown namespace: {element.Name.Namespace}");
@ -59,6 +54,27 @@ namespace HashCalculator.GUI
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)
{
if (element == null)
@ -107,6 +123,18 @@ namespace HashCalculator.GUI
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)
{
return this == entry;

View File

@ -46,14 +46,14 @@ namespace HashCalculator.GUI
ViewModel.StatusText = string.Empty;
return;
case InputEntryType.BigFile:
await LoadBig(selected.Value).ConfigureAwait(true);
await LoadBig(selected.Value);
return;
case InputEntryType.ManifestFile:
await LoadManifestFromFile(selected.Value).ConfigureAwait(true);
await LoadManifestFromFile(selected.Value);
return;
case InputEntryType.XmlFile:
ViewModel.IsXml = true;
await LoadXml(selected.Value).ConfigureAwait(true);
case InputEntryType.XsdFile:
await LoadXml(selected);
return;
default:
throw new NotSupportedException();
@ -125,9 +125,8 @@ namespace HashCalculator.GUI
var first = bigViewModel.AllManifests.FirstOrDefault();
if (first != null)
{
var name = VirtualFileSystem.GetFileNameWithoutExtension(first.Value);
if (name.StartsWith("mod", StringComparison.OrdinalIgnoreCase)
|| name.StartsWith("mapmetadata", StringComparison.OrdinalIgnoreCase))
var name = VirtualFileSystem.GetFileNameWithoutExtension(first.Value).ToLowerInvariant();
if (new[] { "mod", "mapmetadata_mod", "static", "global" }.Any(name.StartsWith))
{
bigViewModel.SelectedItem = first;
await bigViewModel.SelectCommand.ExecuteTask(ViewModel).ConfigureAwait(true);
@ -189,7 +188,7 @@ namespace HashCalculator.GUI
tokenSource.Cancel();
try
{
await task.ConfigureAwait(true);
await task;
}
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();
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();
await ExceptionWrapepr(async () =>
@ -229,23 +238,32 @@ namespace HashCalculator.GUI
}, "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)
{
var studio = await RequestOpenFile("选择 Mod Studio", ("EALAModStudio.exe", null));
sdkRoot = Path.GetDirectoryName(studio);
}
var modXml = new ModXml(path, sdkRoot, token);
var modXml = new ModXml(path, sdkRoot, cancelToken);
try
{
await FindCsf(modXml.BaseDirectory).ConfigureAwait(true);
await FindCsf(modXml.BaseDirectory, cancelToken);
}
catch (Exception 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();
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)
{
@ -355,6 +373,7 @@ namespace HashCalculator.GUI
from data in directories.GetDirectories("Data")
from csf in data.GetFiles("*.csf")
select csf;
csfs = csfs.AsParallel().WithCancellation(cancelToken);
var result =
csfs.FirstOrDefault(x => x.Name.Equals("gamestrings.csf", StringComparison.OrdinalIgnoreCase))
?? csfs.FirstOrDefault();
@ -363,8 +382,13 @@ namespace HashCalculator.GUI
throw new FileNotFoundException();
}
using var fileStream = result.OpenRead();
Translator.Provider = new CsfTranslationProvider(fileStream);
using var memoryStream = new MemoryStream();
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}`");
});
}

View 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();
}
}
}

View 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();
}
}
}

View File

@ -8,17 +8,17 @@ namespace HashCalculator.GUI
{
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 },
{ ".xml", InputEntryType.XmlFile },
{ ".w3x", InputEntryType.XmlFile },
{ ".manifest", InputEntryType.ManifestFile },
{ ".xsd", InputEntryType.XsdFile },
};
private Search _search = new Search();
[SuppressMessage("Microsoft.Performance", "CA1031")]
public IEnumerable<InputEntry> ProvideFileSystemSuggestions(string? path)
{
var empty = Enumerable.Empty<InputEntry>();

15
IModXml.cs Normal file
View 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();
}
}

View File

@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using TechnologyAssembler.Core.IO;
using System.Collections.Generic;
namespace HashCalculator.GUI
{
@ -13,6 +8,7 @@ namespace HashCalculator.GUI
BigFile,
ManifestFile,
XmlFile,
XsdFile,
BinaryFile,
Path,
}
@ -25,6 +21,7 @@ namespace HashCalculator.GUI
{ InputEntryType.BinaryFile, "可以计算这个文件的哈希值" },
{ InputEntryType.ManifestFile, "可以尝试读取这个 manifest 文件,显示各个素材的哈希值" },
{ InputEntryType.XmlFile, "可以尝试读取这个 XML显示 XML 里定义的各个素材的哈希值" },
{ InputEntryType.XsdFile, "可以尝试读取 XSD显示 XSD 里定义的各个素材的哈希值" },
{ InputEntryType.Path, string.Empty },
};

View File

@ -6,6 +6,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:HashCalculator.GUI"
xmlns:c="clr-namespace:HashCalculator.GUI.Converters"
xmlns:sys="clr-namespace:System;assembly=System.Runtime"
mc:Ignorable="d"
Title="SAGE FastHash 哈希计算器"
Height="600"
@ -152,30 +154,37 @@
Height="25"
Margin="0,0,0,5"
>
<Grid
<StackPanel
DockPanel.Dock="Right"
Width="190"
Orientation="Horizontal"
d:Visibility="Visible"
Visibility="{Binding IsXml,
Converter={StaticResource BooleanToVisibilityConverter}}"
>
<Button
Content="{Binding LoadCsfText}"
Command="{Binding LoadCsf}"
CommandParameter="{Binding ElementName=Self}"
Width="92"
HorizontalAlignment="Left"
/>
<Button
Content="取消加载"
Content="取消加载 XML"
Command="{Binding CancelXml}"
Width="92"
HorizontalAlignment="Right"
Padding="8,0"
Margin="0,0,8,0"
Visibility="{Binding IsLoadingXml,
Converter={StaticResource BooleanToVisibilityConverter}}"
/>
</Grid>
<CheckBox
<Button
Content="{Binding LoadCsfText}"
Command="{Binding LoadCsf}"
CommandParameter="{Binding ElementName=Self}"
Padding="16,0"
Visibility="{Binding IsXsd,
Converter={StaticResource FalseToVisibilityConverter}}"
/>
</StackPanel>
<StackPanel
DockPanel.Dock="Right"
Orientation="Horizontal"
Visibility="{Binding IsXsd,
Converter={StaticResource FalseToVisibilityConverter}}"
>
<CheckBox
Content="只显示 GameObject"
IsChecked="{Binding GameObjectOnly}"
Command="{Binding FilterDisplayEntries}"
@ -184,15 +193,15 @@
VerticalAlignment="Center"
/>
<CheckBox
DockPanel.Dock="Right"
Content="搜索类型 ID"
IsChecked="{Binding ConsiderTypeId}"
Command="{Binding FilterDisplayEntries}"
Padding="4,0"
Margin="8,0"
VerticalAlignment="Center"
Visibility="{Binding GameObjectOnly, Converter={StaticResource FalseToVisibilityConverter}}"
/>
</StackPanel>
<TextBlock
Text="{Binding DisplayEntries.Count, StringFormat=列表里显示了{0}个素材}"
Margin="5,0,0,0"
@ -232,6 +241,18 @@
EnableRowVirtualization="True"
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>
<DataGridTextColumn
Header="类型/素材名称"
@ -241,14 +262,27 @@
Header="哈希InstanceId"
Binding="{Binding InstanceIdString}"
FontFamily="Courier New"
Visibility="{Binding Source={x:Reference Name=ReferenceProvider},
Path=DataContext.IsXsd,
Converter={StaticResource FalseToVisibilityConverter}}"
/>
<DataGridTextColumn
Header="名称"
Binding="{Binding LocalizedNames}"
Visibility="{Binding Source={x:Reference Name=ReferenceProvider},
Path=DataContext.IsXml,
Converter={StaticResource BooleanToVisibilityConverter}}"
>
<DataGridTextColumn.Visibility>
<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
Header="类型 ID"
Binding="{Binding TypeIdString}"

View File

@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using Microsoft.Win32;
using Mvp.Xml.XInclude;
using System;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
@ -9,12 +10,10 @@ using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Win32;
using Mvp.Xml.XInclude;
namespace HashCalculator.GUI
{
public class ModXml
public class ModXml : IModXml
{
protected enum DocumentOption
{

164
ModXsd.cs Normal file
View 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);
}
}
}
}

View File

@ -37,6 +37,23 @@ namespace HashCalculator.GUI
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
}
@ -53,25 +70,21 @@ namespace HashCalculator.GUI
public string? Filter
{
get => _filter;
set
{
SetField(ref _filter, value);
FilterDisplayEntries.Execute(null);
}
set => SetField(ref _filter, value, () => FilterDisplayEntries.Execute(null));
}
private bool _gameObjectOnly;
public bool GameObjectOnly
{
get => _gameObjectOnly;
set => SetField(ref _gameObjectOnly, value);
set => SetField(ref _gameObjectOnly, value, () => ConsiderTypeId &= !value);
}
private bool _considerTypeId;
public bool ConsiderTypeId
{
get => _considerTypeId;
set => SetField(ref _considerTypeId, value);
set => SetField(ref _considerTypeId, value, () => GameObjectOnly &= !value);
}
public Command FilterDisplayEntries { get; }
@ -95,7 +108,7 @@ namespace HashCalculator.GUI
public bool IsXml
{
get => _isXml;
set => SetField(ref _isXml, value);
set => SetField(ref _isXml, value, () => IsXsd &= value);
}
private bool _isLoadingXml;
public bool IsLoadingXml
@ -103,6 +116,16 @@ namespace HashCalculator.GUI
get => _isLoadingXml;
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; }
[SuppressMessage("Microsoft.Performance", "CA1822")]
public string LoadCsfText => $"{(Translator.HasProvider ? "" : "")} CSF";