diff --git a/AssetEntry.cs b/AssetEntry.cs index 091f075..b85fc1b 100644 --- a/AssetEntry.cs +++ b/AssetEntry.cs @@ -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; diff --git a/Controller.cs b/Controller.cs index 37d427c..54c57e4 100644 --- a/Controller.cs +++ b/Controller.cs @@ -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}`"); }); } diff --git a/Converters/MultiBooleanConjunctionConverter.cs b/Converters/MultiBooleanConjunctionConverter.cs new file mode 100644 index 0000000..2e6d3a2 --- /dev/null +++ b/Converters/MultiBooleanConjunctionConverter.cs @@ -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(); + } + } +} diff --git a/Converters/MultiValueAggregateConverter.cs b/Converters/MultiValueAggregateConverter.cs new file mode 100644 index 0000000..78f79a7 --- /dev/null +++ b/Converters/MultiValueAggregateConverter.cs @@ -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(); + } + } +} diff --git a/FileSystemSuggestions.cs b/FileSystemSuggestions.cs index 4801518..6e343c9 100644 --- a/FileSystemSuggestions.cs +++ b/FileSystemSuggestions.cs @@ -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>(); diff --git a/IModXml.cs b/IModXml.cs new file mode 100644 index 0000000..dab48e6 --- /dev/null +++ b/IModXml.cs @@ -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(); + } +} diff --git a/InputEntry.cs b/InputEntry.cs index 0fb2335..056680e 100644 --- a/InputEntry.cs +++ b/InputEntry.cs @@ -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}"; } diff --git a/MainWindow.xaml b/MainWindow.xaml index 78e9d30..d2338d9 100644 --- a/MainWindow.xaml +++ b/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}" diff --git a/ModXml.cs b/ModXml.cs index 34b6447..c7518df 100644 --- a/ModXml.cs +++ b/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 { diff --git a/ModXsd.cs b/ModXsd.cs new file mode 100644 index 0000000..d247f5c --- /dev/null +++ b/ModXsd.cs @@ -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); + } + } + } +} diff --git a/ViewModel.cs b/ViewModel.cs index 9537a0c..49f38e4 100644 --- a/ViewModel.cs +++ b/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";