支持加载 XSD
This commit is contained in:
		
							parent
							
								
									289f6551c1
								
							
						
					
					
						commit
						ce26ef298e
					
				@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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}`");
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
    {
 | 
			
		||||
        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
									
								
							
							
						
						
									
										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.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using TechnologyAssembler.Core.IO;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
 | 
			
		||||
namespace HashCalculator.GUI
 | 
			
		||||
{
 | 
			
		||||
@ -13,18 +8,20 @@ namespace HashCalculator.GUI
 | 
			
		||||
        BigFile,
 | 
			
		||||
        ManifestFile,
 | 
			
		||||
        XmlFile,
 | 
			
		||||
        XsdFile,
 | 
			
		||||
        BinaryFile,
 | 
			
		||||
        Path,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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.BinaryFile, "可以计算这个文件的哈希值" },
 | 
			
		||||
            { InputEntryType.ManifestFile, "可以尝试读取这个 manifest 文件,显示各个素材的哈希值" },
 | 
			
		||||
            { InputEntryType.XmlFile, "可以尝试读取这个XML,显示 XML 里定义的各个素材的哈希值" },
 | 
			
		||||
            { InputEntryType.XmlFile, "可以尝试读取这个 XML,显示 XML 里定义的各个素材的哈希值" },
 | 
			
		||||
            { InputEntryType.XsdFile, "可以尝试读取 XSD,显示 XSD 里定义的各个素材的哈希值" },
 | 
			
		||||
            { InputEntryType.Path, string.Empty },
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
@ -33,13 +30,13 @@ namespace HashCalculator.GUI
 | 
			
		||||
        public string Text { get; }
 | 
			
		||||
        public string Details
 | 
			
		||||
        {
 | 
			
		||||
            get 
 | 
			
		||||
            get
 | 
			
		||||
            {
 | 
			
		||||
                if(Type == InputEntryType.Text)
 | 
			
		||||
                if (Type == InputEntryType.Text)
 | 
			
		||||
                {
 | 
			
		||||
                    var hash = SageHash.CalculateLowercaseHash(Value);
 | 
			
		||||
                    var binaryHash = SageHash.CalculateBinaryHash(Value);
 | 
			
		||||
                    return hash == binaryHash 
 | 
			
		||||
                    return hash == binaryHash
 | 
			
		||||
                        ? $"这段文字的哈希值:{hash:x8} ({hash})"
 | 
			
		||||
                        : $"这段文字的哈希值:{hash:x8};大小写敏感哈希值 {binaryHash:x8}";
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										104
									
								
								MainWindow.xaml
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								MainWindow.xaml
									
									
									
									
									
								
							@ -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,47 +154,54 @@
 | 
			
		||||
                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"
 | 
			
		||||
                    Content="只显示 GameObject"
 | 
			
		||||
                    IsChecked="{Binding GameObjectOnly}"
 | 
			
		||||
                    Command="{Binding FilterDisplayEntries}"
 | 
			
		||||
                    Padding="4,0"
 | 
			
		||||
                    Margin="8,0"
 | 
			
		||||
                    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}}"
 | 
			
		||||
                />
 | 
			
		||||
                    Orientation="Horizontal"
 | 
			
		||||
                    Visibility="{Binding IsXsd,
 | 
			
		||||
                                         Converter={StaticResource FalseToVisibilityConverter}}"
 | 
			
		||||
                >
 | 
			
		||||
                    <CheckBox 
 | 
			
		||||
                        Content="只显示 GameObject"
 | 
			
		||||
                        IsChecked="{Binding GameObjectOnly}"
 | 
			
		||||
                        Command="{Binding FilterDisplayEntries}"
 | 
			
		||||
                        Padding="4,0"
 | 
			
		||||
                        Margin="8,0"
 | 
			
		||||
                        VerticalAlignment="Center"
 | 
			
		||||
                    />
 | 
			
		||||
                    <CheckBox 
 | 
			
		||||
                        Content="搜索类型 ID"
 | 
			
		||||
                        IsChecked="{Binding ConsiderTypeId}"
 | 
			
		||||
                        Command="{Binding FilterDisplayEntries}"
 | 
			
		||||
                        Padding="4,0"
 | 
			
		||||
                        Margin="8,0"
 | 
			
		||||
                        VerticalAlignment="Center"
 | 
			
		||||
                    />
 | 
			
		||||
                </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}"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										11
									
								
								ModXml.cs
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								ModXml.cs
									
									
									
									
									
								
							@ -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
									
								
							
							
						
						
									
										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);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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";
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user