diff --git a/Moder.Core/App.axaml b/Moder.Core/App.axaml new file mode 100644 index 0000000..8af1637 --- /dev/null +++ b/Moder.Core/App.axaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + Segoe Fluent Icons + + + + + + \ No newline at end of file diff --git a/Moder.Core/App.axaml.cs b/Moder.Core/App.axaml.cs new file mode 100644 index 0000000..3f706cd --- /dev/null +++ b/Moder.Core/App.axaml.cs @@ -0,0 +1,205 @@ +using System.Diagnostics; +using System.Runtime.Versioning; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using HotAvalonia; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Moder.Core.Extensions; +using Moder.Core.Infrastructure.FileSort; +using Moder.Core.Services; +using Moder.Core.Services.Config; +using Moder.Core.Services.FileNativeService; +using Moder.Core.Services.GameResources; +using Moder.Core.Services.GameResources.Base; +using Moder.Core.Services.GameResources.Localization; +using Moder.Core.Services.GameResources.Modifiers; +using Moder.Core.Views; +using Moder.Core.Views.Game; +using Moder.Core.Views.Menus; +using Moder.Core.ViewsModel; +using Moder.Core.ViewsModel.Game; +using Moder.Core.ViewsModel.Menus; +using Moder.Hosting; +using NLog; +using NLog.Extensions.Logging; + +namespace Moder.Core; + +public class App : Application +{ + public const string AppVersion = "0.1.0-alpha"; + public const string CodeRepositoryUrl = "https://github.com/ModerCore/Moder"; + public static new App Current => (App)Application.Current!; + public static IServiceProvider Services => Current._serviceProvider; + public static string AppConfigFolder { get; } = + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Moder", + "Configs" + ); + + public static string AssetsFolder { get; } = Path.Combine(Environment.CurrentDirectory, "Assets"); + public static string ParserRulesFolder { get; } = Path.Combine(AssetsFolder, "ParserRules"); + + private IHost? _host; + + public App() + { + InitializeApp(); + } + + private static void InitializeApp() + { + if (!Directory.Exists(AppConfigFolder)) + { + Directory.CreateDirectory(AppConfigFolder); + } + + if (!Directory.Exists(ParserRulesFolder)) + { + Directory.CreateDirectory(ParserRulesFolder); + } + } + + public override void Initialize() + { + this.EnableHotReload(); + AvaloniaXamlLoader.Load(this); + } + + private IServiceProvider _serviceProvider = null!; + + public override async void OnFrameworkInitializationCompleted() + { + var builder = CreateHostBuilder(); + var host = builder.Build(); + _host = host; + _serviceProvider = host.Services; + var settingService = Services.GetRequiredService(); + RequestedThemeVariant = settingService.AppTheme.ToThemeVariant(); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Avoid duplicate validations from both Avalonia and the CommunityToolkit. + // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins + DisableAvaloniaDataAnnotationValidation(); + desktop.MainWindow = _serviceProvider.GetRequiredService(); + desktop.Exit += (_, _) => + { + _serviceProvider.GetRequiredService().SaveChanged(); + _host.StopAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult(); + _host.Dispose(); + _host = null; + + // 在退出时刷新日志 + LogManager.Flush(); + }; + } + + base.OnFrameworkInitializationCompleted(); + + await _host.RunAsync(); + } + + private static HostApplicationBuilder CreateHostBuilder() + { + var settings = new HostApplicationBuilderSettings + { + Args = Environment.GetCommandLineArgs(), + ApplicationName = "Moder" + }; + +#if DEBUG + settings.EnvironmentName = "Development"; +#else + settings.EnvironmentName = "Production"; +#endif + var builder = Host.CreateApplicationBuilder(settings); + + builder.Services.AttachLoggerToAvaloniaLogger(); + builder.Logging.ClearProviders(); + builder.Logging.AddNLog(builder.Configuration); + LogManager.Configuration = new NLogLoggingConfiguration(builder.Configuration.GetSection("NLog")); + + // View, ViewModel + builder.Services.AddViewSingleton(); + builder.Services.AddViewTransient(); + builder.Services.AddViewSingleton(); + builder.Services.AddViewSingleton(); + builder.Services.AddViewSingleton(); + builder.Services.AddViewTransient(); + builder.Services.AddViewTransient(); + builder.Services.AddViewSingleton(); + builder.Services.AddTransient(); + + builder.Services.AddSingleton(_ => AppSettingService.Load()); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + // 本地化文本相关服务 + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + // 修饰符相关服务 + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + AddPlatformNativeServices(builder.Services); + + return builder; + } + + private static void AddPlatformNativeServices(IServiceCollection builder) + { +#if WINDOWS + Debug.Assert(OperatingSystem.IsWindows()); + AddWindowsServices(builder); +#elif LINUX + AddLinuxServices(builder); +#endif + } + +#if WINDOWS + [SupportedOSPlatform("windows")] + private static void AddWindowsServices(IServiceCollection builder) + { + builder.AddSingleton(); + builder.AddSingleton(); + } +#elif LINUX + [SupportedOSPlatform("linux")] + private static void AddLinuxServices(IServiceCollection builder) + { + builder.AddSingleton(); + builder.AddSingleton(); + } +#endif + + private static void DisableAvaloniaDataAnnotationValidation() + { + // Get an array of plugins to remove + var dataValidationPluginsToRemove = BindingPlugins + .DataValidators.OfType() + .ToArray(); + + // remove each entry found + foreach (var plugin in dataValidationPluginsToRemove) + { + BindingPlugins.DataValidators.Remove(plugin); + } + } +} diff --git a/Moder.Core/App.xaml b/Moder.Core/App.xaml deleted file mode 100644 index e1eb8cb..0000000 --- a/Moder.Core/App.xaml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/Moder.Core/App.xaml.cs b/Moder.Core/App.xaml.cs deleted file mode 100644 index 90999b6..0000000 --- a/Moder.Core/App.xaml.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.UI.Dispatching; -using Microsoft.UI.Xaml; -using Moder.Core.Helper; -using Moder.Core.Services.Config; -using NLog; - -namespace Moder.Core; - -/// -/// Provides application-specific behavior to supplement the default Application class. -/// -public partial class App : Application -{ - public const string AppVersion = "0.1.0-alpha"; - public static new App Current => (App)Application.Current; - - public IServiceProvider Services => Current._serviceProvider; - - public Views.MainWindow MainWindow - { - get => field ?? throw new InvalidOperationException(); - private set; - } = null!; - - /// - /// 在 UI线程上运行的调度器队列 - /// - public DispatcherQueue DispatcherQueue => MainWindow.DispatcherQueue; - public XamlRoot XamlRoot => MainWindow.Content.XamlRoot; - - public static string ConfigFolder { get; } = Path.Combine(Environment.CurrentDirectory, "Configs"); - public static string ParserRulesFolder { get; } = - Path.Combine(Environment.CurrentDirectory, "Assets", "ParserRules"); - - private readonly IServiceProvider _serviceProvider; - private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - - /// - /// Initializes the singleton application object. This is the first line of authored code - /// executed, and as such is the logical equivalent of main() or WinMain(). - /// - public App(IServiceProvider serviceProvider, IHostApplicationLifetime lifetime) - { - _serviceProvider = serviceProvider; - UnhandledException += (_, args) => Log.Error(args.Exception, "Unhandled exception"); - InitializeComponent(); - - InitializeApp(); - // 在应用程序退出时,刷新所有待处理的日志 - lifetime.ApplicationStopped.Register(LogManager.Flush); - } - - private static void InitializeApp() - { - if (!Directory.Exists(ConfigFolder)) - { - Directory.CreateDirectory(ConfigFolder); - } - - if (!Directory.Exists(ParserRulesFolder)) - { - Directory.CreateDirectory(ParserRulesFolder); - } - } - - /// - /// Invoked when the application is launched. - /// - /// Details about the launch request and process. - protected override void OnLaunched(LaunchActivatedEventArgs args) - { - MainWindow = _serviceProvider.GetRequiredService(); - WindowHelper.SetAppTheme(_serviceProvider.GetRequiredService().AppThemeMode); - MainWindow.Activate(); - } -} diff --git a/Moder.Core/Assets/CodeEditor/Grammars/paradox.tmLanguage.json b/Moder.Core/Assets/CodeEditor/Grammars/paradox.tmLanguage.json new file mode 100644 index 0000000..91cfb12 --- /dev/null +++ b/Moder.Core/Assets/CodeEditor/Grammars/paradox.tmLanguage.json @@ -0,0 +1,309 @@ +{ + "name": "hoi4", + "scopeName": "source.hoi4", + "uuid": "2edebb0f-3089-4abb-8b82-9974e337ad9f", + "foldingStartMarker": "^\\s*#", + "foldingStopMarker": "(?!^[^#])", + "patterns": [ + { + "include": "#namespace" + }, + { + "comment": "This is the main entry-point", + "include": "#code" + } + ], + "repository": { + "namespace": { + "patterns": [ + { + "name": "meta.namespace.paradox", + "match": "^\\s*[^@]?((namespace)\\s[=]\\s([\\w.]+))", + "captures": { + "1": { + "name": "meta.namespace.identifier.txt" + }, + "2": { + "name": "keyword.other.namespace.txt" + }, + "3": { + "name": "entity.name.type.namespace.txt" + } + } + } + ] + }, + "comment": { + "patterns": [ + { + "name": "comment.line.number-sign.paradox", + "comment": "A line starting with # is a comment", + "begin": "#", + "captures": { + "1": { + "name": "punctuation.definition.comment.paradox" + } + }, + "end": "$\\n?" + } + ] + }, + "constants": { + "patterns": [ + { + "name": "constant.prefdef.paradox", + "comment": "A RHS script variable (... = @variable)", + "match": "(@\\w+)" + }, + { + "name": "constant.language.paradox", + "match": "\\b(yes|no)\\b" + }, + { + "name": "constant.numeric.paradox", + "comment": "A RHS number, either integer or float, positive or negative", + "match": "(?|<|<=|>=)\\s?{)" + }, + { + "name": "variable.lhs.paradox", + "match": "(\\w|\\.|-|:)+(?=\\s?(=|>|<|<=|>=))" + }, + { + "name": "variable.bracket.keywords", + "match": "\\{\\s+[\\w+\\s+]+\\}" + }, + { + "name": "variable.language.description.paradox", + "match": "\\b(NOT_USED_3)\\b" + } + ] + }, + "block": { + "patterns": [ + { + "comment": "Two brackets around the inside of the block, which just contains #code", + "begin": "(?<==)\\s*{", + "beginCaptures": { + "0": { + "name": "punctuation.section.block.begin.paradox" + } + }, + "end": "}", + "endCaptures": { + "0": { + "name": "punctuation.section.block.end.paradox" + } + }, + "name": "meta.block.paradox", + "patterns": [ + { + "include": "#code" + } + ] + } + ] + }, + "rhs" : { + "patterns": [ + { + "comment": "RHS is either a constant value", + "include": "#constants" + }, + { + "comment": "Or a generic string", + "include": "#strings" + } + ] + }, + "variables": { + "patterns": [ + { + "comment": "LHS for value, but not block (>keyword =< something)", + "begin": "[^\\S\\n]*(?|<=|>=)[^\\S\\n]*[^{\\s])", + "end": "[^\\S\\n]*(=|<|>|<=|>=)", + "contentName": "variable.paradox", + "patterns": [ + { + "comment": "LHS is a keyword", + "include": "#keywords" + } + ] + }, + { + "comment": "RHS for value (keyword = >something<)", + "begin": "[^\\S\\n]*(?<==|<|>|<=|>=)[^\\S\\n]*", + "end": "[\\s]+", + "contentName": "variable.language.rhs.paradox", + "patterns": [ + { + "include": "#rhs" + } + ] + }, + { + "comment": "LHS for block not value (>keyword =< { ... })", + "begin": "[^\\S\\n]*(? \ No newline at end of file diff --git a/Moder.Core/Assets/ParserRules/CountryTag.txt b/Moder.Core/Assets/ParserRules/CountryTag.txt deleted file mode 100644 index ac5257f..0000000 --- a/Moder.Core/Assets/ParserRules/CountryTag.txt +++ /dev/null @@ -1,5 +0,0 @@ -add_core_of -owner -add_claim_by -controller -transfer_state \ No newline at end of file diff --git a/Moder.Core/Assets/ParserRules/PositiveModifier.txt b/Moder.Core/Assets/ParserRules/PositiveModifier.txt deleted file mode 100644 index 635ab2b..0000000 --- a/Moder.Core/Assets/ParserRules/PositiveModifier.txt +++ /dev/null @@ -1,11 +0,0 @@ -offence -offence_factor -defence -defence_factor -planning_speed -max_planning -naval_hit_chance -naval_retreat_speed -recon_factor -army_infantry_attack_factor -army_infantry_defence_factor \ No newline at end of file diff --git a/Moder.Core/Assets/ParserRules/ReversedModifier.txt b/Moder.Core/Assets/ParserRules/ReversedModifier.txt deleted file mode 100644 index 61e50d1..0000000 --- a/Moder.Core/Assets/ParserRules/ReversedModifier.txt +++ /dev/null @@ -1 +0,0 @@ -supply_consumption_factor \ No newline at end of file diff --git a/Moder.Core/Assets/logo.ico b/Moder.Core/Assets/logo.ico deleted file mode 100644 index a2bfb90..0000000 Binary files a/Moder.Core/Assets/logo.ico and /dev/null differ diff --git a/Moder.Core/Assets/logo.svg b/Moder.Core/Assets/logo.svg new file mode 100644 index 0000000..2928423 --- /dev/null +++ b/Moder.Core/Assets/logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Moder.Core/Behaviors/AutoCompleteZeroMinimumPrefixLengthDropdownBehaviour.cs b/Moder.Core/Behaviors/AutoCompleteZeroMinimumPrefixLengthDropdownBehaviour.cs new file mode 100644 index 0000000..d83d82d --- /dev/null +++ b/Moder.Core/Behaviors/AutoCompleteZeroMinimumPrefixLengthDropdownBehaviour.cs @@ -0,0 +1,100 @@ +using Avalonia.Controls; +using Avalonia.Xaml.Interactivity; + +namespace Moder.Core.Behaviors; + +public sealed class AutoCompleteZeroMinimumPrefixLengthDropdownBehaviour : Behavior +{ + protected override void OnAttached() + { + if (AssociatedObject is not null) + { + AssociatedObject.KeyUp += OnKeyUp; + AssociatedObject.DropDownOpening += DropDownOpening; + AssociatedObject.PointerReleased += PointerReleased; + } + + base.OnAttached(); + } + + protected override void OnDetaching() + { + if (AssociatedObject is not null) + { + AssociatedObject.KeyUp -= OnKeyUp; + AssociatedObject.DropDownOpening -= DropDownOpening; + AssociatedObject.PointerReleased -= PointerReleased; + } + + base.OnDetaching(); + } + + //have to use KeyUp as AutoCompleteBox eats some of the KeyDown events + private void OnKeyUp(object? sender, Avalonia.Input.KeyEventArgs e) + { + if ((e.Key == Avalonia.Input.Key.Down || e.Key == Avalonia.Input.Key.F4)) + { + if (string.IsNullOrEmpty(AssociatedObject?.Text)) + { + ShowDropdown(); + } + } + } + + private void DropDownOpening(object? sender, System.ComponentModel.CancelEventArgs e) + { + var prop = AssociatedObject + ?.GetType() + .GetProperty( + "TextBox", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic + ); + var tb = (TextBox?)prop?.GetValue(AssociatedObject); + if (tb is not null && tb.IsReadOnly) + { + e.Cancel = true; + } + } + + private void PointerReleased(object? sender, Avalonia.Input.PointerReleasedEventArgs e) + { + if (string.IsNullOrEmpty(AssociatedObject?.Text)) + { + ShowDropdown(); + } + } + + private void ShowDropdown() + { + if (AssociatedObject is not null && !AssociatedObject.IsDropDownOpen) + { + typeof(AutoCompleteBox) + .GetMethod( + "PopulateDropDown", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance + ) + ?.Invoke(AssociatedObject, [AssociatedObject, EventArgs.Empty]); + typeof(AutoCompleteBox) + .GetMethod( + "OpeningDropDown", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance + ) + ?.Invoke(AssociatedObject, [false]); + + if (!AssociatedObject.IsDropDownOpen) + { + //We *must* set the field and not the property as we need to avoid the changed event being raised (which prevents the dropdown opening). + var ipc = typeof(AutoCompleteBox).GetField( + "_ignorePropertyChange", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance + ); + if ((bool?)ipc?.GetValue(AssociatedObject) == false) + { + ipc.SetValue(AssociatedObject, true); + } + + AssociatedObject.SetCurrentValue(AutoCompleteBox.IsDropDownOpenProperty, true); + } + } + } +} diff --git a/Moder.Core/Controls/BaseLeaf/BaseLeaf.cs b/Moder.Core/Controls/BaseLeaf/BaseLeaf.cs deleted file mode 100644 index a699deb..0000000 --- a/Moder.Core/Controls/BaseLeaf/BaseLeaf.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System.Windows.Input; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Moder.Core.Models; -using Moder.Core.Models.Vo; - -// ReSharper disable once CheckNamespace -namespace Moder.Core.Controls; - -public sealed partial class BaseLeaf : Control -{ - public static readonly DependencyProperty KeyProperty = DependencyProperty.Register( - nameof(Key), - typeof(string), - typeof(BaseLeaf), - new PropertyMetadata(null) - ); - - public static readonly DependencyProperty SlotContentProperty = DependencyProperty.Register( - nameof(SlotContent), - typeof(object), - typeof(BaseLeaf), - new PropertyMetadata(null) - ); - - public static readonly DependencyProperty TypeProperty = DependencyProperty.Register( - nameof(Type), - typeof(string), - typeof(BaseLeaf), - new PropertyMetadata(null) - ); - - public static readonly DependencyProperty LeafContextProperty = DependencyProperty.Register( - nameof(ObservableGameValue), - typeof(ObservableGameValue), - typeof(BaseLeaf), - new PropertyMetadata(null) - ); - - public static readonly DependencyProperty AddCommandProperty = DependencyProperty.Register( - nameof(AddCommand), - typeof(ICommand), - typeof(BaseLeaf), - new PropertyMetadata(null) - ); - - public static readonly DependencyProperty RemoveCommandProperty = DependencyProperty.Register( - nameof(RemoveCommand), - typeof(ICommand), - typeof(BaseLeaf), - new PropertyMetadata(null) - ); - - public static readonly DependencyProperty GameVoTypeProperty = DependencyProperty.Register( - nameof(GameVoType), - typeof(IReadOnlyList), - typeof(BaseLeaf), - new PropertyMetadata(null) - ); - - public IReadOnlyList GameVoType - { - get => (IReadOnlyList)GetValue(GameVoTypeProperty); - private set => SetValue(GameVoTypeProperty, value); - } - - public ObservableGameValue? LeafContext - { - get => (ObservableGameValue?)GetValue(LeafContextProperty); - set - { - SetValue(LeafContextProperty, value); - if (value is not null) - { - RemoveCommand = value.RemoveSelfInParentCommand; - AddCommand = value.AddAdjacentValueCommand; - Type = value.TypeString; - Key = value.Key; - GameVoType = value.VoTypes; - } - } - } - - public ICommand? AddCommand - { - get => (ICommand?)GetValue(AddCommandProperty); - set => SetValue(AddCommandProperty, value); - } - - public ICommand? RemoveCommand - { - get => (ICommand?)GetValue(RemoveCommandProperty); - set => SetValue(RemoveCommandProperty, value); - } - - public string? Type - { - get => (string?)GetValue(TypeProperty); - set => SetValue(TypeProperty, value); - } - - public object? SlotContent - { - get => GetValue(SlotContentProperty); - set => SetValue(SlotContentProperty, value); - } - - public string? Key - { - get => (string?)GetValue(KeyProperty); - set => SetValue(KeyProperty, value); - } - - public BaseLeaf() - { - DefaultStyleKey = typeof(BaseLeaf); - } -} diff --git a/Moder.Core/Controls/BaseLeaf/BaseLeaf.xaml b/Moder.Core/Controls/BaseLeaf/BaseLeaf.xaml deleted file mode 100644 index 04f4a56..0000000 --- a/Moder.Core/Controls/BaseLeaf/BaseLeaf.xaml +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - + diff --git a/Moder.Core/Controls/DirectorySelector.axaml.cs b/Moder.Core/Controls/DirectorySelector.axaml.cs new file mode 100644 index 0000000..7a489ab --- /dev/null +++ b/Moder.Core/Controls/DirectorySelector.axaml.cs @@ -0,0 +1,50 @@ +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Data; + +namespace Moder.Core.Controls; + +public sealed class DirectorySelector : TemplatedControl +{ + public string SelectorCaption + { + get => GetValue(SelectorCaptionProperty); + set => SetValue(SelectorCaptionProperty, value); + } + public static readonly StyledProperty SelectorCaptionProperty = AvaloniaProperty.Register< + DirectorySelector, + string + >(nameof(SelectorCaption)); + + public string DirectoryPath + { + get => GetValue(DirectoryPathProperty); + set => SetValue(DirectoryPathProperty, value); + } + public static readonly StyledProperty DirectoryPathProperty = AvaloniaProperty.Register< + DirectorySelector, + string + >(nameof(DirectoryPath), enableDataValidation: true, defaultBindingMode: BindingMode.TwoWay); + + public ICommand SelectDirectoryCommand + { + get => GetValue(SelectDirectoryCommandProperty); + set => SetValue(SelectDirectoryCommandProperty, value); + } + public static readonly StyledProperty SelectDirectoryCommandProperty = + AvaloniaProperty.Register(nameof(SelectDirectoryCommand)); + + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error + ) + { + if (property == DirectoryPathProperty) + { + DataValidationErrors.SetError(this, error); + } + } +} diff --git a/Moder.Core/Editor/ParadoxRegistryOptions.cs b/Moder.Core/Editor/ParadoxRegistryOptions.cs new file mode 100644 index 0000000..2b6d6e7 --- /dev/null +++ b/Moder.Core/Editor/ParadoxRegistryOptions.cs @@ -0,0 +1,62 @@ +using Avalonia.Styling; +using TextMateSharp.Internal.Grammars.Reader; +using TextMateSharp.Internal.Themes.Reader; +using TextMateSharp.Internal.Types; +using TextMateSharp.Registry; +using TextMateSharp.Themes; + +namespace Moder.Core.Editor; + +public sealed class ParadoxRegistryOptions(ThemeVariant? theme) : IRegistryOptions +{ + private static string ThemesFolderPath => Path.Combine(App.AssetsFolder, "CodeEditor", "Themes"); + private static string GrammarsFolderPath => Path.Combine(App.AssetsFolder, "CodeEditor", "Grammars"); + + public IRawTheme GetTheme(string scopeName) + { + if (string.IsNullOrWhiteSpace(scopeName)) + { + return GetDefaultTheme(); + } + + var path = Path.Combine(ThemesFolderPath, scopeName); + if (!File.Exists(path)) + { + return GetDefaultTheme(); + } + + return ThemeReader.ReadThemeSync(File.OpenText(path)); + } + + public IRawGrammar GetGrammar(string scopeName) + { + return GrammarReader.ReadGrammarSync( + File.OpenText(Path.Combine(GrammarsFolderPath, "paradox.tmLanguage.json")) + ); + } + + public ICollection? GetInjections(string scopeName) + { + return null; + } + + public IRawTheme GetDefaultTheme() + { + return ThemeReader.ReadThemeSync(File.OpenText(Path.Combine(ThemesFolderPath, GetThemeFileName()))); + } + + private string GetThemeFileName() + { + if (theme == ThemeVariant.Dark) + { + return "dark_plus.json"; + } + + if (theme == ThemeVariant.Light) + { + return "light_plus.json"; + } + + return "dark_plus.json"; + } +} diff --git a/Moder.Core/Encodings.cs b/Moder.Core/Encodings.cs new file mode 100644 index 0000000..f42badb --- /dev/null +++ b/Moder.Core/Encodings.cs @@ -0,0 +1,8 @@ +using System.Text; + +namespace Moder.Core; + +public static class Encodings +{ + public static readonly UTF8Encoding Utf8NotBom = new(false); +} \ No newline at end of file diff --git a/Moder.Core/Extensions/EnumExtensions.cs b/Moder.Core/Extensions/EnumExtensions.cs index c7dfa47..869b781 100644 --- a/Moder.Core/Extensions/EnumExtensions.cs +++ b/Moder.Core/Extensions/EnumExtensions.cs @@ -1,5 +1,7 @@ using System.Globalization; +using Avalonia.Styling; using Moder.Core.Models; +using Moder.Core.Models.Game; namespace Moder.Core.Extensions; @@ -68,4 +70,15 @@ private static GameLanguage GetSystemLanguage() return GameLanguage.English; } + + public static ThemeVariant ToThemeVariant(this ThemeMode type) + { + return type switch + { + ThemeMode.Light => ThemeVariant.Light, + ThemeMode.Dark => ThemeVariant.Dark, + ThemeMode.Default => ThemeVariant.Default, + _ => ThemeVariant.Default + }; + } } diff --git a/Moder.Core/Extensions/ParserExtensions.cs b/Moder.Core/Extensions/ParserExtensions.cs index e8dbf6c..a96432c 100644 --- a/Moder.Core/Extensions/ParserExtensions.cs +++ b/Moder.Core/Extensions/ParserExtensions.cs @@ -1,7 +1,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using Microsoft.FSharp.Collections; -using Moder.Core.Models; +using Moder.Core.Models.Game; using ParadoxPower.Parser; using ParadoxPower.Process; using ParadoxPower.Utilities; @@ -44,17 +44,6 @@ public static GameValueType ToLocalValueType(this Types.Value value) throw new InvalidEnumArgumentException(nameof(value)); } - public static string GetKey(this Child child) - { - var key = child.GetKeyOrNull(); - if (key is null) - { - throw new InvalidOperationException("这个 child 不存在 key"); - } - - return key; - } - public static string? GetKeyOrNull(this Child child) { if (child.IsLeafChild) diff --git a/Moder.Core/Extensions/ServiceCollectionExtensions.cs b/Moder.Core/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..878ef1f --- /dev/null +++ b/Moder.Core/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,30 @@ +using Avalonia.Controls; +using Microsoft.Extensions.DependencyInjection; + +namespace Moder.Core.Extensions; + +public static class ServiceCollectionExtensions +{ + public static void AddViewSingleton(this IServiceCollection services) + where TView : Control, new() + where TViewModel : class + { + services.AddSingleton(); + services.AddSingleton(); + } + + public static void AddViewTransient(this IServiceCollection services) + where TView : Control, new() + where TViewModel : class + { + if ( + typeof(IDisposable).IsAssignableFrom(typeof(TView)) + || typeof(IDisposable).IsAssignableFrom(typeof(TViewModel)) + ) + { + throw new InvalidOperationException("使用 Transient 注入时不能继承 IDisposable 接口"); + } + services.AddTransient(); + services.AddTransient(); + } +} diff --git a/Moder.Core/Helper/GameValueTypeConverterHelper.cs b/Moder.Core/Helper/GameValueTypeConverterHelper.cs deleted file mode 100644 index 9187506..0000000 --- a/Moder.Core/Helper/GameValueTypeConverterHelper.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Moder.Core.Models; - -namespace Moder.Core.Helper; - -public static class GameValueTypeConverterHelper -{ - public static GameValueType GetTypeForString(string value) - { - if (int.TryParse(value, out _)) - { - return GameValueType.Int; - } - - if (double.TryParse(value, out _)) - { - return GameValueType.Float; - } - - if ( - value.Equals("yes", StringComparison.OrdinalIgnoreCase) - || value.Equals("no", StringComparison.OrdinalIgnoreCase) - ) - { - return GameValueType.Bool; - } - - if (value.Length >= 2 && value[0] == '"' && value[^1] == '"') - { - return GameValueType.StringWithQuotation; - } - - return GameValueType.String; - } -} \ No newline at end of file diff --git a/Moder.Core/Helper/ModifierHelper.cs b/Moder.Core/Helper/ModifierHelper.cs deleted file mode 100644 index 0355ae9..0000000 --- a/Moder.Core/Helper/ModifierHelper.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Moder.Core.Models.Modifiers; -using ParadoxPower.Process; - -namespace Moder.Core.Helper; - -public static class ModifierHelper -{ - public static ModifierCollection ParseModifier(Node modifierNode) - { - var list = new List(modifierNode.AllArray.Length); - foreach (var child in modifierNode.AllArray) - { - if (child.IsLeafChild) - { - var modifier = LeafModifier.FromLeaf(child.leaf); - list.Add(modifier); - } - else if (child.IsNodeChild) - { - var node = child.node; - var modifier = new NodeModifier(node.Key, node.Leaves.Select(LeafModifier.FromLeaf)); - list.Add(modifier); - } - } - - return new ModifierCollection(modifierNode.Key, list); - } -} diff --git a/Moder.Core/Helper/ValueConverterHelper.cs b/Moder.Core/Helper/ValueConverterHelper.cs deleted file mode 100644 index af5b58d..0000000 --- a/Moder.Core/Helper/ValueConverterHelper.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Moder.Core.Models; -using ParadoxPower.Parser; - -namespace Moder.Core.Helper; - -public static class ValueConverterHelper -{ - public static Types.Value ToValueType(GameValueType type, string value) - { - return type switch - { - GameValueType.Bool => Types.Value.NewBool(bool.Parse(value)), - GameValueType.Float => Types.Value.NewFloat(decimal.Parse(value)), - GameValueType.Int => Types.Value.NewInt(int.Parse(value)), - GameValueType.String => Types.Value.NewStringValue(value), - GameValueType.StringWithQuotation => Types.Value.NewQStringValue(value), - GameValueType.None => throw new ArgumentException(), - GameValueType.Comment => throw new InvalidOperationException($"{nameof(GameValueType.Comment)} 不支持转换为 {nameof(Types.Value)}"), - _ => throw new ArgumentException() - }; - } -} \ No newline at end of file diff --git a/Moder.Core/Helper/WindowHelper.cs b/Moder.Core/Helper/WindowHelper.cs deleted file mode 100644 index 05c32d9..0000000 --- a/Moder.Core/Helper/WindowHelper.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.UI.Composition.SystemBackdrops; -using Microsoft.UI.Xaml.Media; -using Moder.Core.Models; -using Moder.Core.Services.Config; -using Moder.Core.Views; -using Windows.Storage.Pickers; -using Microsoft.UI.Xaml; -using WinUIEx; -using SystemBackdrop = Microsoft.UI.Xaml.Media.SystemBackdrop; - -namespace Moder.Core.Helper; - -public static class WindowHelper -{ - public static void SetSystemBackdropTypeByConfig() - { - SetSystemBackdropTypeByConfig(App.Current.MainWindow); - } - - public static void SetSystemBackdropTypeByConfig(MainWindow window) - { - var settings = App.Current.Services.GetRequiredService(); - SystemBackdrop? backdrop; - - switch (settings.WindowBackdropType) - { - case WindowBackdropType.Default: - backdrop = MicaController.IsSupported() ? new MicaBackdrop { Kind = MicaKind.Base } : null; - break; - case WindowBackdropType.Mica: - backdrop = new MicaBackdrop { Kind = MicaKind.Base }; - break; - case WindowBackdropType.MicaAlt: - backdrop = new MicaBackdrop { Kind = MicaKind.BaseAlt }; - break; - case WindowBackdropType.Acrylic: - backdrop = new DesktopAcrylicBackdrop(); - break; - case WindowBackdropType.None: - backdrop = null; - break; - default: - throw new ArgumentOutOfRangeException(); - } - - window.SystemBackdrop = backdrop; - } - - public static FolderPicker CreateFolderPicker() - { - var folderPicker = new FolderPicker(); - WinRT.Interop.InitializeWithWindow.Initialize(folderPicker, App.Current.MainWindow.GetWindowHandle()); - // 不设置 FileTypeFilter 在某些 Windows 版本上会报错 - folderPicker.FileTypeFilter.Add("*"); - - return folderPicker; - } - - public static void SetAppTheme(ElementTheme theme) - { - var window = App.Current.MainWindow; - if (window.Content is FrameworkElement root) - { - root.RequestedTheme = theme; - } - } -} diff --git a/Moder.Core/Infrastructure/FileSort/IFileSortComparer.cs b/Moder.Core/Infrastructure/FileSort/IFileSortComparer.cs new file mode 100644 index 0000000..0eb6732 --- /dev/null +++ b/Moder.Core/Infrastructure/FileSort/IFileSortComparer.cs @@ -0,0 +1,6 @@ +namespace Moder.Core.Infrastructure.FileSort; + +/// +/// 文件树排序接口 +/// +public interface IFileSortComparer : IComparer; diff --git a/Moder.Core/Infrastructure/FileSort/LinuxFileSortComparer.cs b/Moder.Core/Infrastructure/FileSort/LinuxFileSortComparer.cs new file mode 100644 index 0000000..b95b137 --- /dev/null +++ b/Moder.Core/Infrastructure/FileSort/LinuxFileSortComparer.cs @@ -0,0 +1,256 @@ +// Optimized by Richard Deeming +// Original code by Vasian Cepa +// https://madebits.github.io/#r/msnet-numeric-sort.md + +#if LINUX +namespace Moder.Core.Infrastructure.FileSort; + +public sealed class LinuxFileSortComparer : IFileSortComparer +{ + public int Compare(string? s1, string? s2) + { + return Compare(s1, s2, false); + } + + // zeroesFirst 参数为 true 时, 字符串前带有前导零的数字按默认顺序排列, 与 Windows 资源管理器中的顺序相同 + // 001 + // 01 + // 1 + // 002 + // 02 + // 2 + // 为 false 时: + // 001 + // 002 + // 01 + // 02 + // 1 + // 2 + private static int Compare(string? s1, string? s2, bool zeroesFirst) + { + if (string.IsNullOrEmpty(s1)) + { + if (string.IsNullOrEmpty(s2)) + { + return 0; + } + + return -1; + } + + if (string.IsNullOrEmpty(s2)) + { + return 1; + } + + var s1Length = s1.Length; + var s2Length = s2.Length; + + var sp1 = char.IsLetterOrDigit(s1[0]); + var sp2 = char.IsLetterOrDigit(s2[0]); + + if (sp1 && !sp2) + { + return 1; + } + + if (!sp1 && sp2) + { + return -1; + } + + char c1, + c2; + int i1 = 0, + i2 = 0; + int r; + bool letter1, + letter2; + + while (true) + { + c1 = s1[i1]; + c2 = s2[i2]; + + sp1 = char.IsDigit(c1); + sp2 = char.IsDigit(c2); + + if (!sp1 && !sp2) + { + if (c1 != c2) + { + letter1 = char.IsLetter(c1); + letter2 = char.IsLetter(c2); + + if (letter1 && letter2) + { + c1 = char.ToUpper(c1); + c2 = char.ToUpper(c2); + + r = c1 - c2; + if (0 != r) + { + return r; + } + } + else if (!letter1 && !letter2) + { + r = c1 - c2; + if (0 != r) + { + return r; + } + } + else if (letter1) + { + return 1; + } + else if (letter2) + { + return -1; + } + } + } + else if (sp1 && sp2) + { + r = CompareNumbers(s1, s1Length, ref i1, s2, s2Length, ref i2, zeroesFirst); + if (0 != r) + { + return r; + } + } + else if (sp1) + { + return -1; + } + else if (sp2) + { + return 1; + } + + i1++; + i2++; + + if (i1 >= s1Length) + { + if (i2 >= s2Length) + { + return 0; + } + + return -1; + } + else if (i2 >= s2Length) + { + return 1; + } + } + } + + private static int CompareNumbers( + string s1, + int s1Length, + ref int i1, + string s2, + int s2Length, + ref int i2, + bool zeroesFirst + ) + { + int nzStart1 = i1, + nzStart2 = i2; + int end1 = i1, + end2 = i2; + + ScanNumber(s1, s1Length, i1, ref nzStart1, ref end1); + ScanNumber(s2, s2Length, i2, ref nzStart2, ref end2); + + var start1 = i1; + i1 = end1 - 1; + var start2 = i2; + i2 = end2 - 1; + + if (zeroesFirst) + { + var zl1 = nzStart1 - start1; + var zl2 = nzStart2 - start2; + if (zl1 > zl2) + { + return -1; + } + + if (zl1 < zl2) + { + return 1; + } + } + + var length1 = end2 - nzStart2; + var length2 = end1 - nzStart1; + + if (length1 == length2) + { + int r; + for (int j1 = nzStart1, j2 = nzStart2; j1 <= i1; j1++, j2++) + { + r = s1[j1] - s2[j2]; + if (0 != r) + { + return r; + } + } + + length1 = end1 - start1; + length2 = end2 - start2; + + if (length1 == length2) + { + return 0; + } + } + + if (length1 > length2) + { + return -1; + } + + return 1; + } + + private static void ScanNumber(string s, int length, int start, ref int nzStart, ref int end) + { + nzStart = start; + end = start; + + var countZeros = true; + var c = s[end]; + + while (true) + { + if (countZeros) + { + if ('0' == c) + { + nzStart++; + } + else + { + countZeros = false; + } + } + + end++; + if (end >= length) + { + break; + } + + c = s[end]; + if (!char.IsDigit(c)) + { + break; + } + } + } +} +#endif diff --git a/Moder.Core/Infrastructure/FileSort/WindowsFileSortComparer.cs b/Moder.Core/Infrastructure/FileSort/WindowsFileSortComparer.cs new file mode 100644 index 0000000..df51aae --- /dev/null +++ b/Moder.Core/Infrastructure/FileSort/WindowsFileSortComparer.cs @@ -0,0 +1,29 @@ +#if WINDOWS +using System.Runtime.Versioning; + +namespace Moder.Core.Infrastructure.FileSort; + +[SupportedOSPlatform("windows")] +public sealed class WindowsFileSortComparer : IFileSortComparer +{ + // CSharp 实现 https://www.codeproject.com/Articles/11016/Numeric-String-Sort-in-C?msg=1183262#xx1183262xx + public int Compare(string? x, string? y) + { + if (string.IsNullOrEmpty(x)) + { + if (string.IsNullOrEmpty(y)) + { + return 0; + } + + return -1; + } + if (string.IsNullOrEmpty(y)) + { + return 1; + } + + return Vanara.PInvoke.ShlwApi.StrCmpLogicalW(x, y); + } +} +#endif diff --git a/Moder.Core/Helper/FileSystemSafeWatcher.cs b/Moder.Core/Infrastructure/FileSystemSafeWatcher.cs similarity index 99% rename from Moder.Core/Helper/FileSystemSafeWatcher.cs rename to Moder.Core/Infrastructure/FileSystemSafeWatcher.cs index 8d2f2bc..83b74aa 100644 --- a/Moder.Core/Helper/FileSystemSafeWatcher.cs +++ b/Moder.Core/Infrastructure/FileSystemSafeWatcher.cs @@ -7,7 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.Timers; -namespace Moder.Core.Helper; +namespace Moder.Core.Infrastructure; // DISCLAIMER: Use this code at your own risk. // No support is provided and this code has NOT been tested. @@ -28,7 +28,7 @@ namespace Moder.Core.Helper; /// /// 如果使用 , 会多次触发 Changed 事件, 在拷贝文件时, 也会多次触发 Changed 事件 /// -public sealed partial class FileSystemSafeWatcher : IDisposable +public sealed class FileSystemSafeWatcher : IDisposable { private readonly FileSystemWatcher _fileSystemWatcher; diff --git a/Moder.Core/Infrastructure/IClosed.cs b/Moder.Core/Infrastructure/IClosed.cs new file mode 100644 index 0000000..3ed34c5 --- /dev/null +++ b/Moder.Core/Infrastructure/IClosed.cs @@ -0,0 +1,15 @@ +namespace Moder.Core.Infrastructure; + +/// +/// 与 的作用一样, 但为了能使用 Ioc 容器管理 Transient 注入的 , +/// 所以绕开 单独定义一个接口. +///

+/// 注意: 只有在 TabView 中使用的类才应该使用此接口来释放资源, 其他类仍应该使用 +///

+public interface IClosed +{ + /// + /// 释放资源 + /// + public void Close(); +} diff --git a/Moder.Core/Infrastructure/ITabViewItem.cs b/Moder.Core/Infrastructure/ITabViewItem.cs new file mode 100644 index 0000000..a32655a --- /dev/null +++ b/Moder.Core/Infrastructure/ITabViewItem.cs @@ -0,0 +1,18 @@ +namespace Moder.Core.Infrastructure; + +public interface ITabViewItem +{ + // TODO: 添加Icon + public string Header { get; } + + /// + /// 唯一识别字符串, 用来在 TabView 中判断是否存在 + /// + public string Id { get; } + public string ToolTip { get; } + + public bool Equals(ITabViewItem? other) + { + return Id == other?.Id; + } +} diff --git a/Moder.Core/Infrastructure/Interaction.cs b/Moder.Core/Infrastructure/Interaction.cs new file mode 100644 index 0000000..f79909b --- /dev/null +++ b/Moder.Core/Infrastructure/Interaction.cs @@ -0,0 +1,58 @@ +using System.Windows.Input; + +namespace Moder.Core.Infrastructure; + +/// +/// Simple implementation of Interaction pattern from ReactiveUI framework. +/// https://www.reactiveui.net/docs/handbook/interactions/ +/// +public sealed class Interaction : IDisposable, ICommand +{ + // this is a reference to the registered interaction handler. + private Func>? _handler; + + /// + /// Performs the requested interaction . Returns the result provided by the View + /// + /// The input parameter + /// The result of the interaction + /// + public Task HandleAsync(TInput input) + { + if (_handler is null) + { + throw new InvalidOperationException("Handler wasn't registered"); + } + + return _handler(input); + } + + /// + /// Registers a handler to our Interaction + /// + /// the handler to register + /// a disposable object to clean up memory if not in use anymore/> + /// + public IDisposable RegisterHandler(Func> handler) + { + if (_handler is not null) + { + throw new InvalidOperationException("Handler was already registered"); + } + + _handler = handler; + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + return this; + } + + public void Dispose() + { + _handler = null; + } + + public bool CanExecute(object? parameter) => _handler is not null; + + public void Execute(object? parameter) => HandleAsync((TInput?)parameter!); + + public event EventHandler? CanExecuteChanged; +} \ No newline at end of file diff --git a/Moder.Core/Helper/ModifierMergeManager.cs b/Moder.Core/Infrastructure/ModifierMergeManager.cs similarity index 95% rename from Moder.Core/Helper/ModifierMergeManager.cs rename to Moder.Core/Infrastructure/ModifierMergeManager.cs index d259b40..b408fce 100644 --- a/Moder.Core/Helper/ModifierMergeManager.cs +++ b/Moder.Core/Infrastructure/ModifierMergeManager.cs @@ -1,10 +1,10 @@ using System.Globalization; using System.Runtime.InteropServices; -using Moder.Core.Models; -using Moder.Core.Models.Modifiers; +using Moder.Core.Models.Game; +using Moder.Core.Models.Game.Modifiers; using NLog; -namespace Moder.Core.Helper; +namespace Moder.Core.Infrastructure; /// /// 修饰符合并管理器 @@ -17,6 +17,13 @@ public sealed class ModifierMergeManager private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + public void Clear() + { + _leafModifiers.Clear(); + _nodeModifiers.Clear(); + _customEffectTooltipLocalizationKeys.Clear(); + } + public void Add(IModifier modifier) { if (modifier.Type == ModifierType.Leaf) @@ -205,4 +212,4 @@ out var exists refValue = value; } } -} +} \ No newline at end of file diff --git a/Moder.Core/Infrastructure/Parser/LocalizationFormatInfo.cs b/Moder.Core/Infrastructure/Parser/LocalizationFormatInfo.cs new file mode 100644 index 0000000..7baf172 --- /dev/null +++ b/Moder.Core/Infrastructure/Parser/LocalizationFormatInfo.cs @@ -0,0 +1,7 @@ +namespace Moder.Core.Infrastructure.Parser; + +public sealed class LocalizationFormatInfo(string text, LocalizationFormatType type) +{ + public string Text => text; + public LocalizationFormatType Type => type; +} \ No newline at end of file diff --git a/Moder.Core/Parser/LocalizationFormatParser.cs b/Moder.Core/Infrastructure/Parser/LocalizationFormatParser.cs similarity index 57% rename from Moder.Core/Parser/LocalizationFormatParser.cs rename to Moder.Core/Infrastructure/Parser/LocalizationFormatParser.cs index af6c6a3..354545c 100644 --- a/Moder.Core/Parser/LocalizationFormatParser.cs +++ b/Moder.Core/Infrastructure/Parser/LocalizationFormatParser.cs @@ -2,38 +2,38 @@ using Pidgin; using static Pidgin.Parser; -namespace Moder.Core.Parser; +namespace Moder.Core.Infrastructure.Parser; public static class LocalizationFormatParser { private static readonly Parser CharExcept = Parser.Token(c => c != '$'); - private static readonly Parser PlaceholderParser = Char('$') + private static readonly Parser PlaceholderParser = Char('$') .Then(CharExcept.AtLeastOnceString()) .Before(Char('$')) - .Map(placeholder => new LocalizationFormat(placeholder, LocalizationFormatType.Placeholder)); + .Map(placeholder => new LocalizationFormatInfo(placeholder, LocalizationFormatType.Placeholder)); - private static readonly Parser TextWithColorParser = + private static readonly Parser TextWithColorParser = Parser.Token(c => c != '§').AtLeastOnceString().Optional() .Between(Char('§'), String("§!")) - .Map(text => new LocalizationFormat( + .Map(text => new LocalizationFormatInfo( text.HasValue ? text.Value : string.Empty, LocalizationFormatType.TextWithColor )); - private static readonly Parser TextParser = + private static readonly Parser TextParser = from text in Try(String("$$").WithResult('$')).Or(AnyCharExcept('$', '§')).AtLeastOnceString() - select new LocalizationFormat(text, LocalizationFormatType.Text); + select new LocalizationFormatInfo(text, LocalizationFormatType.Text); - private static readonly Parser> LocalizationTextParser = TextParser + private static readonly Parser> LocalizationTextParser = TextParser .Or(PlaceholderParser) .Or(TextWithColorParser) .Many(); - public static bool TryParse(string input, [NotNullWhen(true)] out IEnumerable? result) + public static bool TryParse(string input, [NotNullWhen(true)] out IEnumerable? formats) { var parseResult = LocalizationTextParser.Parse(input); - result = parseResult.Success ? parseResult.Value : null; + formats = parseResult.Success ? parseResult.Value : null; return parseResult.Success; } diff --git a/Moder.Core/Parser/LocalizationFormatType.cs b/Moder.Core/Infrastructure/Parser/LocalizationFormatType.cs similarity index 67% rename from Moder.Core/Parser/LocalizationFormatType.cs rename to Moder.Core/Infrastructure/Parser/LocalizationFormatType.cs index 9793b45..b2dec71 100644 --- a/Moder.Core/Parser/LocalizationFormatType.cs +++ b/Moder.Core/Infrastructure/Parser/LocalizationFormatType.cs @@ -1,4 +1,4 @@ -namespace Moder.Core.Parser; +namespace Moder.Core.Infrastructure.Parser; public enum LocalizationFormatType : byte { diff --git a/Moder.Core/Parser/TextParser.cs b/Moder.Core/Infrastructure/Parser/TextParser.cs similarity index 97% rename from Moder.Core/Parser/TextParser.cs rename to Moder.Core/Infrastructure/Parser/TextParser.cs index 3af12cd..a3b5ef6 100644 --- a/Moder.Core/Parser/TextParser.cs +++ b/Moder.Core/Infrastructure/Parser/TextParser.cs @@ -3,7 +3,7 @@ using ParadoxPower.CSharp; using ParadoxPower.Process; -namespace Moder.Core.Parser; +namespace Moder.Core.Infrastructure.Parser; public class TextParser { diff --git a/Moder.Core/Infrastructure/Unit.cs b/Moder.Core/Infrastructure/Unit.cs new file mode 100644 index 0000000..9da41f2 --- /dev/null +++ b/Moder.Core/Infrastructure/Unit.cs @@ -0,0 +1,8 @@ +namespace Moder.Core.Infrastructure; + +public sealed class Unit +{ + private Unit() { } + + public static readonly Unit Value = new(); +} diff --git a/Moder.Core/Messages/CompleteAppInitializeMessage.cs b/Moder.Core/Messages/CompleteAppInitializeMessage.cs new file mode 100644 index 0000000..6d4470b --- /dev/null +++ b/Moder.Core/Messages/CompleteAppInitializeMessage.cs @@ -0,0 +1,5 @@ +namespace Moder.Core.Messages; + +public sealed record CompleteAppInitializeMessage; + +public sealed record CompleteAppSettingsMessage; \ No newline at end of file diff --git a/Moder.Core/Messages/CompleteWorkFolderSelectMessage.cs b/Moder.Core/Messages/CompleteWorkFolderSelectMessage.cs deleted file mode 100644 index cd8cc21..0000000 --- a/Moder.Core/Messages/CompleteWorkFolderSelectMessage.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Moder.Core.Messages; - -public sealed record CompleteWorkFolderSelectMessage; \ No newline at end of file diff --git a/Moder.Core/Messages/OpenFileMessage.cs b/Moder.Core/Messages/OpenFileMessage.cs deleted file mode 100644 index e63620b..0000000 --- a/Moder.Core/Messages/OpenFileMessage.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Moder.Core.ViewsModels.Menus; - -namespace Moder.Core.Messages; - -public sealed record OpenFileMessage(SystemFileItem FileItem); \ No newline at end of file diff --git a/Moder.Core/Messages/SelectedTraitChangedMessage.cs b/Moder.Core/Messages/SelectedTraitChangedMessage.cs deleted file mode 100644 index 7e0af67..0000000 --- a/Moder.Core/Messages/SelectedTraitChangedMessage.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Moder.Core.Models.Vo; - -namespace Moder.Core.Messages; - -public sealed class SelectedTraitChangedMessage(bool isAdded, TraitVo trait) -{ - public bool IsAdded => isAdded; - public TraitVo Trait => trait; -} \ No newline at end of file diff --git a/Moder.Core/Messages/SyncSideWorkSelectedItemMessage.cs b/Moder.Core/Messages/SyncSideWorkSelectedItemMessage.cs index b6b3d95..55431fc 100644 --- a/Moder.Core/Messages/SyncSideWorkSelectedItemMessage.cs +++ b/Moder.Core/Messages/SyncSideWorkSelectedItemMessage.cs @@ -1,4 +1,4 @@ -using Moder.Core.ViewsModels.Menus; +using Moder.Core.Models; namespace Moder.Core.Messages; diff --git a/Moder.Core/Models/AppThemeInfo.cs b/Moder.Core/Models/AppThemeInfo.cs new file mode 100644 index 0000000..b9a6db7 --- /dev/null +++ b/Moder.Core/Models/AppThemeInfo.cs @@ -0,0 +1,7 @@ +namespace Moder.Core.Models; + +public sealed class AppThemeInfo(string displayName, ThemeMode mode) +{ + public string DisplayName { get; } = displayName; + public ThemeMode Mode { get; } = mode; +} diff --git a/Moder.Core/Models/Building/BuildingInfo.cs b/Moder.Core/Models/Building/BuildingInfo.cs deleted file mode 100644 index da270a1..0000000 --- a/Moder.Core/Models/Building/BuildingInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -// ReSharper disable once CheckNamespace -namespace Moder.Core.Models; - -public sealed class BuildingInfo(string name, byte? maxLevel) -{ - public string Name { get; } = name; - public byte? MaxLevel { get; } = maxLevel; -} \ No newline at end of file diff --git a/Moder.Core/Models/Building/BuildingLeafVo.cs b/Moder.Core/Models/Building/BuildingLeafVo.cs deleted file mode 100644 index ea33d62..0000000 --- a/Moder.Core/Models/Building/BuildingLeafVo.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Moder.Core.Services.GameResources; -using Moder.Language.Strings; - -// ReSharper disable once CheckNamespace -namespace Moder.Core.Models.Vo; - -public partial class BuildingLeafVo(string key, string value, GameValueType type, NodeVo parent) - : IntLeafVo(key, value, type, parent) -{ - public string BuildingName => $"{LocalisationService.GetValue(Key)} {Resource.Common_MaxLevel}: {MaxBuildingLevel}"; - public byte MaxBuildingLevel => GetMaxBuildingLevel(); - - private static readonly BuildingsService Service = - App.Current.Services.GetRequiredService(); - - private byte GetMaxBuildingLevel() - { - if (Service.TryGetBuildingInfo(Key, out var buildingInfo) && buildingInfo.MaxLevel.HasValue) - { - return buildingInfo.MaxLevel.Value; - } - - return byte.MaxValue; - } -} diff --git a/Moder.Core/Models/CharacterTypeInfo.cs b/Moder.Core/Models/CharacterTypeInfo.cs new file mode 100644 index 0000000..e2b5a4f --- /dev/null +++ b/Moder.Core/Models/CharacterTypeInfo.cs @@ -0,0 +1,13 @@ +namespace Moder.Core.Models; + +public sealed class CharacterTypeInfo(string displayName, string keyword) +{ + /// + /// 显示在 UI 上的名称 + /// + public string DisplayName { get; } = displayName; + /// + /// 在代码中的关键字 + /// + public string Keyword { get; } = keyword; +} \ No newline at end of file diff --git a/Moder.Core/Models/ColorTextInfo.cs b/Moder.Core/Models/ColorTextInfo.cs new file mode 100644 index 0000000..eb9b1a3 --- /dev/null +++ b/Moder.Core/Models/ColorTextInfo.cs @@ -0,0 +1,9 @@ +using Avalonia.Media; + +namespace Moder.Core.Models; + +public sealed class ColorTextInfo(string text, IBrush brush) +{ + public string DisplayText { get; } = text; + public IBrush Brush { get; } = brush; +} diff --git a/Moder.Core/Models/CountryTag/CountryTagLeafVo.cs b/Moder.Core/Models/CountryTag/CountryTagLeafVo.cs deleted file mode 100644 index 2c9a137..0000000 --- a/Moder.Core/Models/CountryTag/CountryTagLeafVo.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Moder.Core.Services.GameResources; - -// ReSharper disable once CheckNamespace -namespace Moder.Core.Models.Vo; - -public sealed partial class CountryTagLeafVo(string key, string value, GameValueType type, NodeVo parent) - : LeafVo(key, value, type, parent) -{ - public static IReadOnlyCollection CountryTags => CountryTagService.CountryTags; - - public override string Value - { - get => LeafValue; - set - { - SetProperty(ref LeafValue, value); - OnPropertyChanged(nameof(CountryName)); - } - } - - public string CountryName => LocalisationService.GetValue(Value); - - private static readonly CountryTagService CountryTagService = - App.Current.Services.GetRequiredService(); -} diff --git a/Moder.Core/Models/Character/Skill.cs b/Moder.Core/Models/Game/Character/Skill.cs similarity index 75% rename from Moder.Core/Models/Character/Skill.cs rename to Moder.Core/Models/Game/Character/Skill.cs index 1b2cd32..1712db7 100644 --- a/Moder.Core/Models/Character/Skill.cs +++ b/Moder.Core/Models/Game/Character/Skill.cs @@ -1,16 +1,16 @@ -namespace Moder.Core.Models.Character; +namespace Moder.Core.Models.Game.Character; /// /// 技能的相关信息 /// public sealed class Skill { - public CharacterSkillType Type { get; } + public SkillCharacterType Type { get; } public ushort MaxValue { get; } private readonly SkillModifier[] _modifiers; - public Skill(CharacterSkillType type, ushort maxValue, IEnumerable modifiers) + public Skill(SkillCharacterType type, ushort maxValue, IEnumerable modifiers) { Type = type; MaxValue = maxValue; diff --git a/Moder.Core/Models/Character/CharacterSkillType.cs b/Moder.Core/Models/Game/Character/SkillCharacterType.cs similarity index 57% rename from Moder.Core/Models/Character/CharacterSkillType.cs rename to Moder.Core/Models/Game/Character/SkillCharacterType.cs index f00a5a2..222e376 100644 --- a/Moder.Core/Models/Character/CharacterSkillType.cs +++ b/Moder.Core/Models/Game/Character/SkillCharacterType.cs @@ -1,17 +1,17 @@ using Ardalis.SmartEnum; -namespace Moder.Core.Models.Character; +namespace Moder.Core.Models.Game.Character; /// /// 技能信息中的人物职业类型, 例如 Navy, CorpsCommander, FieldMarshal, Name 为 type 中可以使用的值 /// -public sealed class CharacterSkillType : SmartEnum +public sealed class SkillCharacterType : SmartEnum { - public static readonly CharacterSkillType Navy = new("navy", 0); - public static readonly CharacterSkillType CorpsCommander = new("corps_commander", 1); - public static readonly CharacterSkillType FieldMarshal = new("field_marshal", 2); + public static readonly SkillCharacterType Navy = new("navy", 0); + public static readonly SkillCharacterType CorpsCommander = new("corps_commander", 1); + public static readonly SkillCharacterType FieldMarshal = new("field_marshal", 2); - public static CharacterSkillType FromCharacterType(string? characterType) + public static SkillCharacterType FromCharacterType(string? characterType) { return characterType switch { @@ -22,6 +22,6 @@ public static CharacterSkillType FromCharacterType(string? characterType) }; } - private CharacterSkillType(string name, byte value) + private SkillCharacterType(string name, byte value) : base(name, value) { } } diff --git a/Moder.Core/Models/Character/SkillInfo.cs b/Moder.Core/Models/Game/Character/SkillInfo.cs similarity index 56% rename from Moder.Core/Models/Character/SkillInfo.cs rename to Moder.Core/Models/Game/Character/SkillInfo.cs index b00b30c..cae1849 100644 --- a/Moder.Core/Models/Character/SkillInfo.cs +++ b/Moder.Core/Models/Game/Character/SkillInfo.cs @@ -1,4 +1,4 @@ -namespace Moder.Core.Models.Character; +namespace Moder.Core.Models.Game.Character; /// /// 存储某一项属性(攻击, 防御等)的每一级别的信息 @@ -18,14 +18,14 @@ public void Add(Skill skill) _skills.Add(skill); } - public ushort? GetMaxValue(CharacterSkillType skillType) + public ushort? GetMaxValue(SkillCharacterType type) { - return _skills.Find(skill => skill.Type == skillType)?.MaxValue; + return _skills.Find(skill => skill.Type == type)?.MaxValue; } - public SkillModifier GetModifierDescription(CharacterSkillType skillType, ushort level) + public SkillModifier GetModifierDescription(SkillCharacterType type, ushort level) { - return _skills.Find(skill => skill.Type == skillType)?.GetModifier(level) + return _skills.Find(skill => skill.Type == type)?.GetModifier(level) ?? new SkillModifier(level, []); } } diff --git a/Moder.Core/Models/Character/SkillModifier.cs b/Moder.Core/Models/Game/Character/SkillModifier.cs similarity index 79% rename from Moder.Core/Models/Character/SkillModifier.cs rename to Moder.Core/Models/Game/Character/SkillModifier.cs index 7877555..d281828 100644 --- a/Moder.Core/Models/Character/SkillModifier.cs +++ b/Moder.Core/Models/Game/Character/SkillModifier.cs @@ -1,7 +1,7 @@ -using Moder.Core.Models.Modifiers; +using Moder.Core.Models.Game.Modifiers; using ParadoxPower.Process; -namespace Moder.Core.Models.Character; +namespace Moder.Core.Models.Game.Character; public sealed class SkillModifier { diff --git a/Moder.Core/Models/Character/SkillType.cs b/Moder.Core/Models/Game/Character/SkillType.cs similarity index 81% rename from Moder.Core/Models/Character/SkillType.cs rename to Moder.Core/Models/Game/Character/SkillType.cs index 4907827..8c0cd84 100644 --- a/Moder.Core/Models/Character/SkillType.cs +++ b/Moder.Core/Models/Game/Character/SkillType.cs @@ -1,4 +1,4 @@ -namespace Moder.Core.Models.Character; +namespace Moder.Core.Models.Game.Character; /// /// 技能类型, 比如攻击, 防御等等 diff --git a/Moder.Core/Models/Character/Trait.cs b/Moder.Core/Models/Game/Character/Trait.cs similarity index 86% rename from Moder.Core/Models/Character/Trait.cs rename to Moder.Core/Models/Game/Character/Trait.cs index 3e72187..ff0684a 100644 --- a/Moder.Core/Models/Character/Trait.cs +++ b/Moder.Core/Models/Game/Character/Trait.cs @@ -1,6 +1,6 @@ -using Moder.Core.Models.Modifiers; +using Moder.Core.Models.Game.Modifiers; -namespace Moder.Core.Models.Character; +namespace Moder.Core.Models.Game.Character; /// /// 人物特质 diff --git a/Moder.Core/Models/Character/TraitType.cs b/Moder.Core/Models/Game/Character/TraitType.cs similarity index 85% rename from Moder.Core/Models/Character/TraitType.cs rename to Moder.Core/Models/Game/Character/TraitType.cs index 1b4aafc..dfb921a 100644 --- a/Moder.Core/Models/Character/TraitType.cs +++ b/Moder.Core/Models/Game/Character/TraitType.cs @@ -1,4 +1,4 @@ -namespace Moder.Core.Models.Character; +namespace Moder.Core.Models.Game.Character; [Flags] public enum TraitType : byte diff --git a/Moder.Core/Models/GameLanguage.cs b/Moder.Core/Models/Game/GameLanguage.cs similarity index 80% rename from Moder.Core/Models/GameLanguage.cs rename to Moder.Core/Models/Game/GameLanguage.cs index 9424efd..539d3de 100644 --- a/Moder.Core/Models/GameLanguage.cs +++ b/Moder.Core/Models/Game/GameLanguage.cs @@ -1,4 +1,4 @@ -namespace Moder.Core.Models; +namespace Moder.Core.Models.Game; public enum GameLanguage : byte { diff --git a/Moder.Core/Models/GameLanguageInfo.cs b/Moder.Core/Models/Game/GameLanguageInfo.cs similarity index 83% rename from Moder.Core/Models/GameLanguageInfo.cs rename to Moder.Core/Models/Game/GameLanguageInfo.cs index ed0951c..3d59ad9 100644 --- a/Moder.Core/Models/GameLanguageInfo.cs +++ b/Moder.Core/Models/Game/GameLanguageInfo.cs @@ -1,4 +1,4 @@ -namespace Moder.Core.Models; +namespace Moder.Core.Models.Game; public sealed class GameLanguageInfo(string displayName, GameLanguage type) { diff --git a/Moder.Core/Models/GameValueType.cs b/Moder.Core/Models/Game/GameValueType.cs similarity index 78% rename from Moder.Core/Models/GameValueType.cs rename to Moder.Core/Models/Game/GameValueType.cs index 4fcee98..448c1db 100644 --- a/Moder.Core/Models/GameValueType.cs +++ b/Moder.Core/Models/Game/GameValueType.cs @@ -1,4 +1,4 @@ -namespace Moder.Core.Models; +namespace Moder.Core.Models.Game; public enum GameValueType : byte { diff --git a/Moder.Core/Models/LocalizationTextColor.cs b/Moder.Core/Models/Game/LocalizationTextColor.cs similarity index 71% rename from Moder.Core/Models/LocalizationTextColor.cs rename to Moder.Core/Models/Game/LocalizationTextColor.cs index f06cbc9..91ec3c1 100644 --- a/Moder.Core/Models/LocalizationTextColor.cs +++ b/Moder.Core/Models/Game/LocalizationTextColor.cs @@ -1,6 +1,6 @@ -using Windows.UI; +using Avalonia.Media; -namespace Moder.Core.Models; +namespace Moder.Core.Models.Game; public sealed class LocalizationTextColor(char key, Color color) { diff --git a/Moder.Core/Models/Modifiers/IModifier.cs b/Moder.Core/Models/Game/Modifiers/IModifier.cs similarity index 68% rename from Moder.Core/Models/Modifiers/IModifier.cs rename to Moder.Core/Models/Game/Modifiers/IModifier.cs index 6dc39fd..9f1f8bd 100644 --- a/Moder.Core/Models/Modifiers/IModifier.cs +++ b/Moder.Core/Models/Game/Modifiers/IModifier.cs @@ -1,4 +1,4 @@ -namespace Moder.Core.Models.Modifiers; +namespace Moder.Core.Models.Game.Modifiers; public interface IModifier { diff --git a/Moder.Core/Models/Modifiers/LeafModifier.cs b/Moder.Core/Models/Game/Modifiers/LeafModifier.cs similarity index 92% rename from Moder.Core/Models/Modifiers/LeafModifier.cs rename to Moder.Core/Models/Game/Modifiers/LeafModifier.cs index c521c5d..f1600d4 100644 --- a/Moder.Core/Models/Modifiers/LeafModifier.cs +++ b/Moder.Core/Models/Game/Modifiers/LeafModifier.cs @@ -2,7 +2,7 @@ using Moder.Core.Extensions; using ParadoxPower.Process; -namespace Moder.Core.Models.Modifiers; +namespace Moder.Core.Models.Game.Modifiers; [DebuggerDisplay("{Key} = {Value}")] public sealed class LeafModifier : IModifier, IEquatable @@ -19,7 +19,7 @@ public sealed class LeafModifier : IModifier, IEquatable public const string CustomModifierTooltipKey = "custom_modifier_tooltip"; /// - /// 从 构建一个叶子修饰符, 属性被设置为 leaf.Key + /// 从 构建一个叶子修饰符 /// /// 叶子 /// diff --git a/Moder.Core/Models/Modifiers/ModifierCollection.cs b/Moder.Core/Models/Game/Modifiers/ModifierCollection.cs similarity index 90% rename from Moder.Core/Models/Modifiers/ModifierCollection.cs rename to Moder.Core/Models/Game/Modifiers/ModifierCollection.cs index caa203b..3980c7a 100644 --- a/Moder.Core/Models/Modifiers/ModifierCollection.cs +++ b/Moder.Core/Models/Game/Modifiers/ModifierCollection.cs @@ -1,4 +1,4 @@ -namespace Moder.Core.Models.Modifiers; +namespace Moder.Core.Models.Game.Modifiers; /// /// Modifier 集合, 只有需要 Modifier 节点的 Key 时才会使用到这个类 diff --git a/Moder.Core/Models/Modifiers/ModifierEffectType.cs b/Moder.Core/Models/Game/Modifiers/ModifierEffectType.cs similarity index 80% rename from Moder.Core/Models/Modifiers/ModifierEffectType.cs rename to Moder.Core/Models/Game/Modifiers/ModifierEffectType.cs index b5c3d8c..84a7160 100644 --- a/Moder.Core/Models/Modifiers/ModifierEffectType.cs +++ b/Moder.Core/Models/Game/Modifiers/ModifierEffectType.cs @@ -1,4 +1,4 @@ -namespace Moder.Core.Models.Modifiers; +namespace Moder.Core.Models.Game.Modifiers; public enum ModifierEffectType : byte { diff --git a/Moder.Core/Models/Modifiers/ModifierType.cs b/Moder.Core/Models/Game/Modifiers/ModifierType.cs similarity index 53% rename from Moder.Core/Models/Modifiers/ModifierType.cs rename to Moder.Core/Models/Game/Modifiers/ModifierType.cs index b33963a..89e627f 100644 --- a/Moder.Core/Models/Modifiers/ModifierType.cs +++ b/Moder.Core/Models/Game/Modifiers/ModifierType.cs @@ -1,4 +1,4 @@ -namespace Moder.Core.Models.Modifiers; +namespace Moder.Core.Models.Game.Modifiers; public enum ModifierType : byte { diff --git a/Moder.Core/Models/Modifiers/NodeModifier.cs b/Moder.Core/Models/Game/Modifiers/NodeModifier.cs similarity index 87% rename from Moder.Core/Models/Modifiers/NodeModifier.cs rename to Moder.Core/Models/Game/Modifiers/NodeModifier.cs index 7887d91..a67df2f 100644 --- a/Moder.Core/Models/Modifiers/NodeModifier.cs +++ b/Moder.Core/Models/Game/Modifiers/NodeModifier.cs @@ -1,4 +1,4 @@ -namespace Moder.Core.Models.Modifiers; +namespace Moder.Core.Models.Game.Modifiers; public sealed class NodeModifier : IModifier { diff --git a/Moder.Core/Models/Game/ObservableGameValue.cs b/Moder.Core/Models/Game/ObservableGameValue.cs new file mode 100644 index 0000000..cd0ecc1 --- /dev/null +++ b/Moder.Core/Models/Game/ObservableGameValue.cs @@ -0,0 +1,119 @@ +// using System.ComponentModel; +// using System.Diagnostics; +// using CommunityToolkit.Mvvm.ComponentModel; +// using CommunityToolkit.Mvvm.Input; +// using CommunityToolkit.WinUI; +// using EnumsNET; +// using Microsoft.Extensions.DependencyInjection; +// using Microsoft.UI.Xaml.Controls; +// using Moder.Core.Models.Vo; +// using Moder.Core.Services; +// using NLog; +// using ParadoxPower.Process; +// +// namespace Moder.Core.Models; +// +// public abstract partial class ObservableGameValue(string key, NodeVo? parent) : ObservableObject +// { +// public string Key { get; } = key; +// public bool IsChanged { get; private set; } +// public NodeVo? Parent { get; } = parent; +// public string TypeString => Type.ToString(); +// public IReadOnlyList VoTypes => Enums.GetValues(); +// public GameValueType Type { get; init; } +// +// public IRelayCommand RemoveSelfInParentCommand => +// _removeSelfInParentCommand ??= new RelayCommand(RemoveSelfInParent); +// private RelayCommand? _removeSelfInParentCommand; +// +// public IRelayCommand AddAdjacentValueCommand => +// _addAdjacentValueCommand ??= new RelayCommand(AddAdjacentValue); +// private RelayCommand? _addAdjacentValueCommand; +// +// private static readonly Logger Log = LogManager.GetCurrentClassLogger(); +// protected static readonly LeafConverterService ConverterService = +// App.Current.Services.GetRequiredService(); +// +// public abstract Child[] ToRawChildren(); +// +// protected override void OnPropertyChanged(PropertyChangedEventArgs e) +// { +// IsChanged = true; +// base.OnPropertyChanged(e); +// } +// +// private void RemoveSelfInParent() +// { +// Debug.Assert(Parent != null, "Parent cannot be null"); +// if (Parent is null) +// { +// Log.Warn("删除节点失败, 父节点为空"); +// return; +// } +// +// Parent.Remove(this); +// // 如果父节点下没有其他子节点,则删除父节点 +// // if (Parent.Children.Count == 0 && Parent.Parent is not null) +// // { +// // Parent.Parent.Remove(Parent); +// // } +// } +// +// private void AddAdjacentValue(StackPanel? value) +// { +// if (value is null) +// { +// return; +// } +// +// if (Parent is null) +// { +// Log.Warn("添加相邻节点失败, 父节点为空"); +// return; +// } +// +// var addedKeywordTextBox = value.FindChild(box => box.Name == "NewKeywordTextBox"); +// var addedValueTextBox = value.FindChild(box => box.Name == "NewValueTextBox"); +// var typeComboBox = value.FindChild(box => box.Name == "TypeComboBox"); +// Debug.Assert( +// addedKeywordTextBox is not null && addedValueTextBox is not null, +// "添加相邻节点失败, 未找到TextBox" +// ); +// Debug.Assert(typeComboBox is not null, "添加相邻节点失败, 未找到ComboBox"); +// +// if (typeComboBox.SelectedItem is null) +// { +// Log.Warn("添加相邻节点失败, 未选择类型"); +// return; +// } +// +// var newKeyword = addedKeywordTextBox.Text; +// var newValue = addedValueTextBox.Text; +// var voType = (GameVoType)typeComboBox.SelectedItem; +// +// if ( +// string.IsNullOrWhiteSpace(newKeyword) +// || (voType == GameVoType.Leaf && string.IsNullOrWhiteSpace(newValue)) +// ) +// { +// Log.Warn("添加相邻节点失败, 输入值为空"); +// return; +// } +// +// var index = Parent.Children.IndexOf(this) + 1; +// ObservableGameValue newObservableGameValue = voType switch +// { +// GameVoType.Node => new NodeVo(newKeyword, Parent), +// GameVoType.Leaf => ConverterService.GetSpecificLeafVo(newKeyword, newValue, Parent), +// GameVoType.LeafValues => new LeafValuesVo(newKeyword, [newValue], Parent), +// GameVoType.Comment => new CommentVo(newKeyword, Parent), +// _ => throw new ArgumentOutOfRangeException() +// }; +// Parent.Children.Insert(index, newObservableGameValue); +// +// addedKeywordTextBox.Text = string.Empty; +// addedValueTextBox.Text = string.Empty; +// +// Log.Info("添加相邻节点成功, 关键字: {Keyword}, 值: {Value}, 父节点: {Parent}", newKeyword, newValue, Parent.Key); +// } +// } diff --git a/Moder.Core/Models/SpriteInfo.cs b/Moder.Core/Models/Game/SpriteInfo.cs similarity index 84% rename from Moder.Core/Models/SpriteInfo.cs rename to Moder.Core/Models/Game/SpriteInfo.cs index b9c77b7..fcbfff9 100644 --- a/Moder.Core/Models/SpriteInfo.cs +++ b/Moder.Core/Models/Game/SpriteInfo.cs @@ -1,4 +1,4 @@ -namespace Moder.Core.Models; +namespace Moder.Core.Models.Game; public sealed class SpriteInfo(string name, string path) { diff --git a/Moder.Core/Models/ObservableGameValue.cs b/Moder.Core/Models/ObservableGameValue.cs deleted file mode 100644 index 7247590..0000000 --- a/Moder.Core/Models/ObservableGameValue.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System.ComponentModel; -using System.Diagnostics; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.WinUI; -using EnumsNET; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.UI.Xaml.Controls; -using Moder.Core.Models.Vo; -using Moder.Core.Services; -using NLog; -using ParadoxPower.Process; - -namespace Moder.Core.Models; - -public abstract partial class ObservableGameValue(string key, NodeVo? parent) : ObservableObject -{ - public string Key { get; } = key; - public bool IsChanged { get; private set; } - public NodeVo? Parent { get; } = parent; - public string TypeString => Type.ToString(); - public IReadOnlyList VoTypes => Enums.GetValues(); - public GameValueType Type { get; init; } - - public IRelayCommand RemoveSelfInParentCommand => - _removeSelfInParentCommand ??= new RelayCommand(RemoveSelfInParent); - private RelayCommand? _removeSelfInParentCommand; - - public IRelayCommand AddAdjacentValueCommand => - _addAdjacentValueCommand ??= new RelayCommand(AddAdjacentValue); - private RelayCommand? _addAdjacentValueCommand; - - private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - protected static readonly LeafConverterService ConverterService = - App.Current.Services.GetRequiredService(); - - public abstract Child[] ToRawChildren(); - - protected override void OnPropertyChanged(PropertyChangedEventArgs e) - { - IsChanged = true; - base.OnPropertyChanged(e); - } - - private void RemoveSelfInParent() - { - Debug.Assert(Parent != null, "Parent cannot be null"); - if (Parent is null) - { - Log.Warn("删除节点失败, 父节点为空"); - return; - } - - Parent.Remove(this); - // 如果父节点下没有其他子节点,则删除父节点 - // if (Parent.Children.Count == 0 && Parent.Parent is not null) - // { - // Parent.Parent.Remove(Parent); - // } - } - - private void AddAdjacentValue(StackPanel? value) - { - if (value is null) - { - return; - } - - if (Parent is null) - { - Log.Warn("添加相邻节点失败, 父节点为空"); - return; - } - - var addedKeywordTextBox = value.FindChild(box => box.Name == "NewKeywordTextBox"); - var addedValueTextBox = value.FindChild(box => box.Name == "NewValueTextBox"); - var typeComboBox = value.FindChild(box => box.Name == "TypeComboBox"); - Debug.Assert( - addedKeywordTextBox is not null && addedValueTextBox is not null, - "添加相邻节点失败, 未找到TextBox" - ); - Debug.Assert(typeComboBox is not null, "添加相邻节点失败, 未找到ComboBox"); - - if (typeComboBox.SelectedItem is null) - { - Log.Warn("添加相邻节点失败, 未选择类型"); - return; - } - - var newKeyword = addedKeywordTextBox.Text; - var newValue = addedValueTextBox.Text; - var voType = (GameVoType)typeComboBox.SelectedItem; - - if ( - string.IsNullOrWhiteSpace(newKeyword) - || (voType == GameVoType.Leaf && string.IsNullOrWhiteSpace(newValue)) - ) - { - Log.Warn("添加相邻节点失败, 输入值为空"); - return; - } - - var index = Parent.Children.IndexOf(this) + 1; - ObservableGameValue newObservableGameValue = voType switch - { - GameVoType.Node => new NodeVo(newKeyword, Parent), - GameVoType.Leaf => ConverterService.GetSpecificLeafVo(newKeyword, newValue, Parent), - GameVoType.LeafValues => new LeafValuesVo(newKeyword, [newValue], Parent), - GameVoType.Comment => new CommentVo(newKeyword, Parent), - _ => throw new ArgumentOutOfRangeException() - }; - Parent.Children.Insert(index, newObservableGameValue); - - addedKeywordTextBox.Text = string.Empty; - addedValueTextBox.Text = string.Empty; - - Log.Info("添加相邻节点成功, 关键字: {Keyword}, 值: {Value}, 父节点: {Parent}", newKeyword, newValue, Parent.Key); - } -} diff --git a/Moder.Core/Models/Ore/ResourcesLeafVo.cs b/Moder.Core/Models/Ore/ResourcesLeafVo.cs deleted file mode 100644 index 0cf229c..0000000 --- a/Moder.Core/Models/Ore/ResourcesLeafVo.cs +++ /dev/null @@ -1,8 +0,0 @@ -// ReSharper disable once CheckNamespace -namespace Moder.Core.Models.Vo; - -public sealed partial class ResourcesLeafVo(string key, string value, GameValueType type, NodeVo parent) - : IntLeafVo(key, value, type, parent) -{ - public string Name => LocalisationService.GetValue($"state_resource_{Key}"); -} diff --git a/Moder.Core/Models/StateCategory/StateCategory.cs b/Moder.Core/Models/StateCategory/StateCategory.cs deleted file mode 100644 index e3928e3..0000000 --- a/Moder.Core/Models/StateCategory/StateCategory.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Moder.Core.Services.GameResources; - -// ReSharper disable once CheckNamespace -namespace Moder.Core.Models; - -public sealed class StateCategory(string typeName, byte? localBuildingSlots) -{ - public string TypeName { get; } = typeName; - public byte? LocalBuildingSlots { get; } = localBuildingSlots; - - public string TypeNameDescription => LocalisationService.GetValue(TypeName); - public string LocalBuildingSlotsDescription => LocalBuildingSlots.HasValue ? $"[{LocalBuildingSlots}]" : "[?]"; - - private static readonly LocalisationService LocalisationService = App - .Current.Services.GetRequiredService(); -} diff --git a/Moder.Core/Models/StateCategory/StateCategoryLeafVo.cs b/Moder.Core/Models/StateCategory/StateCategoryLeafVo.cs deleted file mode 100644 index c5c8ac5..0000000 --- a/Moder.Core/Models/StateCategory/StateCategoryLeafVo.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Moder.Core.Services.GameResources; -using Moder.Language.Strings; - -// ReSharper disable once CheckNamespace -namespace Moder.Core.Models.Vo; - -public sealed partial class StateCategoryLeafVo : LeafVo -{ - public StateCategoryLeafVo(string key, string value, GameValueType type, NodeVo? parent) - : base(key, value, type, parent) - { - StateCategory.OnResourceChanged += (_, _) => - App.Current.DispatcherQueue.TryEnqueue(() => OnPropertyChanged(nameof(StateCategories))); - } - - public override string Value - { - get => LeafValue; - set - { - SetProperty(ref LeafValue, value); - OnPropertyChanged(nameof(StateCategoryUiDescription)); - } - } - - public string StateCategoryUiDescription => GetStateCategoryUiDescription(); - - public IReadOnlyCollection StateCategories => StateCategory.StateCategories; - - private static readonly StateCategoryService StateCategory = - App.Current.Services.GetRequiredService(); - - private string GetStateCategoryUiDescription() - { - return StateCategory.TryGetValue(Value, out var stateCategory) - ? $"{stateCategory.TypeNameDescription} {Resource.StateFile_BuildingsSlot} [{stateCategory.LocalBuildingSlots}]" - : $"{Resource.Common_Unknown} {Key}"; - } -} diff --git a/Moder.Core/Models/StateName/StateNameLeafVo.cs b/Moder.Core/Models/StateName/StateNameLeafVo.cs deleted file mode 100644 index 142ca2d..0000000 --- a/Moder.Core/Models/StateName/StateNameLeafVo.cs +++ /dev/null @@ -1,20 +0,0 @@ - - -// ReSharper disable once CheckNamespace -namespace Moder.Core.Models.Vo; - -public sealed partial class StateNameLeafVo(string key, string value, GameValueType type, NodeVo parent) - : LeafVo(key, value, type, parent) -{ - public override string Value - { - get => LeafValue; - set - { - SetProperty(ref LeafValue, value); - OnPropertyChanged(nameof(LocalisedName)); - } - } - - public string LocalisedName => LocalisationService.GetValue(Value); -} diff --git a/Moder.Core/ViewsModels/Menus/SystemFileItem.cs b/Moder.Core/Models/SystemFileItem.cs similarity index 53% rename from Moder.Core/ViewsModels/Menus/SystemFileItem.cs rename to Moder.Core/Models/SystemFileItem.cs index b8cf4cc..ca2ab4f 100644 --- a/Moder.Core/ViewsModels/Menus/SystemFileItem.cs +++ b/Moder.Core/Models/SystemFileItem.cs @@ -1,16 +1,19 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Threading; using CommunityToolkit.Mvvm.Input; +using FluentAvalonia.UI.Controls; using Microsoft.Extensions.DependencyInjection; -using Microsoft.UI.Xaml.Controls; using Moder.Core.Services; +using Moder.Core.Services.Config; +using Moder.Core.Services.FileNativeService; using Moder.Core.Views.Menus; +using Moder.Language.Strings; +using MsBox.Avalonia; +using MsBox.Avalonia.Enums; using NLog; -using Vanara.PInvoke; -using Vanara.Windows.Shell; -using Windows.Storage; -using Windows.System; -namespace Moder.Core.ViewsModels.Menus; +namespace Moder.Core.Models; public sealed partial class SystemFileItem { @@ -29,7 +32,11 @@ public sealed partial class SystemFileItem private static readonly Logger Log = LogManager.GetCurrentClassLogger(); private static readonly MessageBoxService MessageBoxService = - App.Current.Services.GetRequiredService(); + App.Services.GetRequiredService(); + private static readonly IFileNativeService FileNativeService = + App.Services.GetRequiredService(); + private static readonly AppSettingService AppSettingService = + App.Services.GetRequiredService(); public SystemFileItem(string fullPath, bool isFile, SystemFileItem? parent) { @@ -66,12 +73,12 @@ public void InsertChild(int index, SystemFileItem child) throw new ArgumentException("Child's parent should be this"); } - App.Current.DispatcherQueue.TryEnqueue(() => _children.Insert(index, child)); + Dispatcher.UIThread.Post(() => _children.Insert(index, child)); } public void RemoveChild(SystemFileItem child) { - App.Current.DispatcherQueue.TryEnqueue(() => _children.Remove(child)); + Dispatcher.UIThread.Post(() => _children.Remove(child)); } public override string ToString() @@ -80,31 +87,9 @@ public override string ToString() } [RelayCommand] - private async Task ShowInExplorerAsync() + private void ShowInExplorer() { - string? folder; - IStorageItem selectedItem; - if (IsFile) - { - selectedItem = await StorageFile.GetFileFromPathAsync(FullPath); - folder = Path.GetDirectoryName(FullPath); - } - else - { - selectedItem = await StorageFolder.GetFolderFromPathAsync(FullPath); - folder = Directory.GetParent(FullPath)?.FullName; - } - - if (folder is null) - { - Log.Warn("在资源管理器中打开失败,无法获取路径:{FullPath}", FullPath); - return; - } - - await Launcher.LaunchFolderPathAsync( - folder, - new FolderLauncherOptions { ItemsToSelect = { selectedItem } } - ); + _ = FileNativeService.TryShowInExplorer(FullPath, IsFile, out _); } [RelayCommand] @@ -112,11 +97,11 @@ private async Task RenameAsync() { var dialog = new ContentDialog { - XamlRoot = App.Current.XamlRoot, - Title = "重命名", - PrimaryButtonText = "确定", - CloseButtonText = "取消" + Title = Resource.Common_Rename, + PrimaryButtonText = Resource.Common_Ok, + CloseButtonText = Resource.Common_Cancel, }; + var view = new RenameFileControlView(dialog, this); dialog.Content = view; @@ -146,7 +131,15 @@ private async Task RenameAsync() return; } - Rename(newPath); + try + { + Rename(newPath); + } + catch (Exception e) + { + Log.Error(e, "重命名文件或文件夹时发生错误"); + await MessageBoxService.ErrorAsync(Resource.RenameFile_ErrorOccurs); + } } private void Rename(string newPath) @@ -162,27 +155,49 @@ private void Rename(string newPath) } [RelayCommand] - private async Task DeleteFile() + private async Task CopyPath() { - var dialog = new ContentDialog + await CopyToClipboard(FullPath).ConfigureAwait(false); + } + + [RelayCommand] + private async Task CopyAsRelativePath() + { + var relativePath = Path.GetRelativePath(AppSettingService.ModRootFolderPath, FullPath); + await CopyToClipboard(relativePath).ConfigureAwait(false); + } + + private static async Task CopyToClipboard(string path) + { + var app = (ClassicDesktopStyleApplicationLifetime?)App.Current.ApplicationLifetime; + if (app?.MainWindow?.Clipboard is null) { - XamlRoot = App.Current.MainWindow.Content.XamlRoot, - Title = IsFile ? $"确认删除 '{Name}' 吗?" : $"确认删除 '{Name}' 及其内容吗?", - Content = "您可以从回收站还原此文件", - PrimaryButtonText = "移动到回收站", - CloseButtonText = "取消" - }; + Log.Warn("无法复制文件路径,剪切板不可用"); + return; + } + + await app.MainWindow.Clipboard.SetTextAsync(path); + } + + [RelayCommand] + private async Task DeleteFile() + { + var text = IsFile + ? string.Format(Resource.DeleteFile_EnsureFile, Name) + : string.Format(Resource.DeleteFile_EnsureFolder, Name); + text += $"\n\n{Resource.DeleteFile_CanFindBack}"; + var dialog = MessageBoxManager.GetMessageBoxStandard(Resource.Common_Delete, text, ButtonEnum.YesNo); var result = await dialog.ShowAsync(); - if (result == ContentDialogResult.Primary) + if (result == ButtonResult.Yes) { - if (TryMoveToRecycleBin(FullPath, out var errorMessage, out var errorCode)) + if (FileNativeService.TryMoveToRecycleBin(FullPath, out var errorMessage, out var errorCode)) { Parent?._children.Remove(this); } else { - await MessageBoxService.ErrorAsync($"删除失败, 原因: {errorMessage}"); + await MessageBoxService.ErrorAsync($"{Resource.DeleteFile_Failed}{errorMessage}"); Log.Warn( "删除文件或文件夹失败:{FullPath}, 错误信息: {ErrorMessage} 错误代码: {Code}", FullPath, @@ -192,43 +207,4 @@ private async Task DeleteFile() } } } - - /// - /// 尝试将文件或文件夹移动到回收站 - /// - /// 文件或文件夹路径 - /// 错误信息 - /// 错误代码 - /// 成功返回 true, 失败返回 false - private static bool TryMoveToRecycleBin( - string fileOrDirectoryPath, - out string? errorMessage, - out int errorCode - ) - { - // 可以使用 dynamic - // from https://learn.microsoft.com/en-us/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants - - if (!Path.Exists(fileOrDirectoryPath)) - { - errorMessage = "文件或文件夹不存在"; - errorCode = 0; - return false; - } - - using var operation = new ShellFileOperations(); - operation.Options = - ShellFileOperations.OperationFlags.RecycleOnDelete - | ShellFileOperations.OperationFlags.NoConfirmation; - operation.QueueDeleteOperation(new ShellItem(fileOrDirectoryPath)); - - var result = default(HRESULT); - operation.PostDeleteItem += (_, args) => result = args.Result; - operation.PerformOperations(); - - errorMessage = result.FormatMessage(); - errorCode = result.Code; - - return result.Succeeded; - } } diff --git a/Moder.Core/Models/ThemeMode.cs b/Moder.Core/Models/ThemeMode.cs new file mode 100644 index 0000000..6b6fd07 --- /dev/null +++ b/Moder.Core/Models/ThemeMode.cs @@ -0,0 +1,8 @@ +namespace Moder.Core.Models; + +public enum ThemeMode : byte +{ + Default, + Light, + Dark +} diff --git a/Moder.Core/Models/ThemeModeInfo.cs b/Moder.Core/Models/ThemeModeInfo.cs deleted file mode 100644 index 8cbbec1..0000000 --- a/Moder.Core/Models/ThemeModeInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.UI.Xaml; - -namespace Moder.Core.Models; - -public sealed class ThemeModeInfo(string name, ElementTheme mode) -{ - public string Name { get; } = name; - public ElementTheme Mode { get; } = mode; -} \ No newline at end of file diff --git a/Moder.Core/Models/Vo/BackdropTypeItemVo.cs b/Moder.Core/Models/Vo/BackdropTypeItemVo.cs deleted file mode 100644 index ed594ed..0000000 --- a/Moder.Core/Models/Vo/BackdropTypeItemVo.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Moder.Core.Models.Vo; - -public sealed class BackdropTypeItemVo(string text, WindowBackdropType backdrop) -{ - public string Text { get; } = text; - public WindowBackdropType Backdrop { get; } = backdrop; -} diff --git a/Moder.Core/Models/Vo/CommentVo.cs b/Moder.Core/Models/Vo/CommentVo.cs deleted file mode 100644 index c9d6d45..0000000 --- a/Moder.Core/Models/Vo/CommentVo.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Threading; -using ParadoxPower.Process; -using ParadoxPower.Utilities; - -namespace Moder.Core.Models.Vo; - -public sealed partial class CommentVo : ObservableGameValue -{ - public string Comment { get; set; } - - public CommentVo(string comment, NodeVo? parent) - : base(string.Empty, parent) - { - Comment = comment; - Type = GameValueType.Comment; - } - - private static int _commentLine; - - /// - /// 将注释转换为多个 , 以换行符做分隔符, 每个 对应文件中的一行注释 - /// - /// 数组, 每个元素对应一行注释 - public override Child[] ToRawChildren() - { - // 两种情况: 单行注释, 多行注释 - // 单行注释直接返回一个Child数组 - // 多行注释按换行符分割, 每个 Child 对应一个注释行 - - // 当TextBox未被选中时, 换行符为\r\n, 否则为\r, 并且按下回车键时添加的也是\r, 所以这里统一用\r分割 - if (Comment.Contains('\r')) - { - var comments = Comment.Split('\r'); - return Array.ConvertAll(comments, comment => ToRawChild(comment.Trim('\n'))); - } - return [ToRawChild(Comment)]; - } - - private static Child ToRawChild(string comment) - { - // 赋值Line是为了保证多行注释重新写入时,不会重叠在一起, 例如 - // #victory_points = { - // # 11386 1 - // #} - // 如果不赋值Line,则会导致多行注释被合并成一行, 变成 - // #victory_points = { # 11386 1 # } - // 控制 Comment 的格式化代码在 Prints.fs printKeyValue 方法中 - var line = Interlocked.Increment(ref _commentLine); - return Child.NewCommentChild( - new Comment(new Position.Range(0, new Position.pos(line, 0), new Position.pos(line, 0)), comment) - ); - } -} diff --git a/Moder.Core/Models/Vo/FloatLeafVo.cs b/Moder.Core/Models/Vo/FloatLeafVo.cs deleted file mode 100644 index d6b8018..0000000 --- a/Moder.Core/Models/Vo/FloatLeafVo.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Globalization; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace Moder.Core.Models.Vo; - -public partial class FloatLeafVo(string key, string value, GameValueType type, NodeVo parent) - : LeafVo(key, value, type, parent) -{ - [ObservableProperty] - private double _numberValue = double.TryParse(value, out var number) ? number : 0D; - - partial void OnNumberValueChanged(double value) - { - Value = value.ToString(CultureInfo.InvariantCulture); - } -} diff --git a/Moder.Core/Models/Vo/GameVoType.cs b/Moder.Core/Models/Vo/GameVoType.cs deleted file mode 100644 index 4357926..0000000 --- a/Moder.Core/Models/Vo/GameVoType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Moder.Core.Models.Vo; - -public enum GameVoType : byte -{ - Node, - Leaf, - LeafValues, - Comment -} \ No newline at end of file diff --git a/Moder.Core/Models/Vo/IntLeafVo.cs b/Moder.Core/Models/Vo/IntLeafVo.cs deleted file mode 100644 index 00856d9..0000000 --- a/Moder.Core/Models/Vo/IntLeafVo.cs +++ /dev/null @@ -1,14 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; - -namespace Moder.Core.Models.Vo; - -public partial class IntLeafVo(string key, string value, GameValueType type, NodeVo parent) : LeafVo(key, value, type, parent) -{ - [ObservableProperty] - private int _numberValue = int.TryParse(value, out var amount) ? amount : 0; - - partial void OnNumberValueChanged(int value) - { - Value = value.ToString(); - } -} diff --git a/Moder.Core/Models/Vo/LeafValuesVo.cs b/Moder.Core/Models/Vo/LeafValuesVo.cs deleted file mode 100644 index e2bf4a2..0000000 --- a/Moder.Core/Models/Vo/LeafValuesVo.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Collections.ObjectModel; -using CommunityToolkit.Mvvm.Input; -using Microsoft.UI.Xaml.Controls; -using Moder.Core.Extensions; -using Moder.Core.Helper; -using ParadoxPower.Process; - -namespace Moder.Core.Models.Vo; - -public sealed partial class LeafValuesVo : ObservableGameValue -{ - public ObservableCollection Values { get; } - - public LeafValuesVo(string key, IEnumerable values, NodeVo parent) - : base(key, parent) - { - var valuesArray = values.Select(value => value.Value).ToArray(); - Values = new ObservableCollection(valuesArray.Select(value => value.ToRawString())); - Type = valuesArray[0].ToLocalValueType(); - } - - /// - /// values 必须至少有一个值 - /// - /// - /// - /// - public LeafValuesVo(string key, IReadOnlyCollection values, NodeVo parent) - : base(key, parent) - { - if (values.Count < 1) - { - throw new ArgumentException("values 必须至少有一个值"); - } - - Values = new ObservableCollection(values); - Type = GameValueTypeConverterHelper.GetTypeForString(Values[0]); - } - - public Child[] ToLeafValues() - { - var leafValues = new Child[Values.Count]; - for (var index = 0; index < Values.Count; index++) - { - leafValues[index] = Child.NewLeafValueChild( - LeafValue.Create(ValueConverterHelper.ToValueType(Type, Values[index])) - ); - } - - return leafValues; - } - - public override Child[] ToRawChildren() - { - var leafValues = new Node(Key) { AllArray = ToLeafValues() }; - return [Child.NewNodeChild(leafValues)]; - } - - [RelayCommand] - private void RemoveValue(string? value) - { - if (value is null) - { - return; - } - - Values.Remove(value); - } - - [RelayCommand] - private void AddValue(TextBox textBox) - { - var value = textBox.Text.Trim(); - if (string.IsNullOrWhiteSpace(value)) - { - return; - } - - Values.Add(value); - - textBox.Text = string.Empty; - } -} diff --git a/Moder.Core/Models/Vo/LeafVo.cs b/Moder.Core/Models/Vo/LeafVo.cs deleted file mode 100644 index 6a173ca..0000000 --- a/Moder.Core/Models/Vo/LeafVo.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Moder.Core.Helper; -using Moder.Core.Services.GameResources; -using ParadoxPower.Parser; -using ParadoxPower.Process; -using ParadoxPower.Utilities; - -namespace Moder.Core.Models.Vo; - -public partial class LeafVo : ObservableGameValue -{ - public virtual string Value - { - get => LeafValue; - set => SetProperty(ref LeafValue, value); - } - protected string LeafValue; - - protected static readonly LocalisationService LocalisationService = - App.Current.Services.GetRequiredService(); - - public LeafVo(string key, string value, GameValueType type, NodeVo? parent) - : base(key, parent) - { - LeafValue = value; - Type = type; - } - - public Types.Value ToRawValue() - { - return ValueConverterHelper.ToValueType(Type, Value); - } - - public override Child[] ToRawChildren() - { - return [Child.NewLeafChild(new Leaf(Key, ToRawValue(), Position.Range.Zero, Types.Operator.Equals))]; - } -} diff --git a/Moder.Core/Models/Vo/NodeVo.cs b/Moder.Core/Models/Vo/NodeVo.cs deleted file mode 100644 index 6960d50..0000000 --- a/Moder.Core/Models/Vo/NodeVo.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System.Collections.ObjectModel; -using System.Diagnostics; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Microsoft.UI.Xaml; -using ParadoxPower.Process; - -namespace Moder.Core.Models.Vo; - -public partial class NodeVo(string key, NodeVo? parent) : ObservableGameValue(key, parent) -{ - public ObservableCollection Children { get; } = []; - public Visibility AddedValueTextBoxVisibility => - SelectedVoType == GameVoType.Node ? Visibility.Collapsed : Visibility.Visible; - - [ObservableProperty] - private string _addedKey = string.Empty; - - [ObservableProperty] - private string _addedValue = string.Empty; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(AddedValueTextBoxVisibility))] - private GameVoType? _selectedVoType; - - public void Add(ObservableGameValue child) - { - Children.Add(child); - } - - public void Remove(ObservableGameValue child) - { - var isRemoved = Children.Remove(child); - Debug.Assert(isRemoved, "Failed to remove child from NodeVo."); - } - - public override Child[] ToRawChildren() - { - var node = new Node(Key); - var children = new List(Children.Count); - foreach (var child in Children) - { - children.AddRange(child.ToRawChildren()); - } - node.AllArray = children.ToArray(); - return [Child.NewNodeChild(node)]; - } - - [RelayCommand] - private void AddChildValue() - { - InternalAddValue(AddType.AddChild); - } - - [RelayCommand] - private void AddAdjacentValueForNode() - { - InternalAddValue(AddType.AddAdjacent); - } - - private void InternalAddValue(AddType type) - { - if (SelectedVoType is null || string.IsNullOrWhiteSpace(AddedKey)) - { - return; - } - - if (SelectedVoType == GameVoType.Leaf && string.IsNullOrWhiteSpace(AddedValue)) - { - return; - } - - var parent = type switch - { - AddType.AddChild => this, - AddType.AddAdjacent => Parent, - _ => throw new ArgumentOutOfRangeException(nameof(type), type, "无效枚举值") - }; - if (parent is null) - { - return; - } - - ObservableGameValue child = SelectedVoType switch - { - GameVoType.Node => new NodeVo(AddedKey, parent), - GameVoType.Leaf => ConverterService.GetSpecificLeafVo(AddedKey, AddedValue, parent), - GameVoType.LeafValues => new LeafValuesVo(AddedKey, [AddedValue], parent), - _ => throw new ArgumentOutOfRangeException() - }; - - switch (type) - { - case AddType.AddChild: - Children.Insert(0, child); - break; - case AddType.AddAdjacent: - Debug.Assert(Parent is not null, "Parent is null."); - Parent?.Children.Insert(Parent.Children.IndexOf(this) + 1, child); - break; - default: - throw new ArgumentOutOfRangeException(nameof(type), type, "无效枚举值"); - } - } - - private enum AddType : byte - { - AddChild, - AddAdjacent - } -} diff --git a/Moder.Core/Models/Vo/TraitVo.cs b/Moder.Core/Models/Vo/TraitVo.cs index dbd8954..2df8e69 100644 --- a/Moder.Core/Models/Vo/TraitVo.cs +++ b/Moder.Core/Models/Vo/TraitVo.cs @@ -1,134 +1,61 @@ -using System.Diagnostics; -using System.Text; +using System.Collections; using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Messaging; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Documents; -using Microsoft.UI.Xaml.Media; -using Moder.Core.Messages; -using Moder.Core.Models.Character; -using Moder.Core.Parser; -using Moder.Core.Services; -using Moder.Core.Services.GameResources; -using Moder.Language.Strings; +using Moder.Core.Models.Game.Character; namespace Moder.Core.Models.Vo; -public sealed partial class TraitVo : ObservableObject, - IEquatable +public sealed partial class TraitVo : ObservableObject, IEquatable { public string Name => Trait.Name; public Trait Trait { get; } public string LocalisationName { get; } - public TextBlock Description => GetDescription(); - - private static readonly ModifierService ModifierService = - App.Current.Services.GetRequiredService(); - private static readonly LocalisationService LocalisationService = - App.Current.Services.GetRequiredService(); - private static readonly SpriteService SpriteService = - App.Current.Services.GetRequiredService(); - - private static readonly string Separator = new('-', 25); - private static readonly ImageSource? UnknownImage = GetUnknownImageSource(); - - private static ImageSource? GetUnknownImageSource() - { - if (SpriteService.TryGetImageSource("GFX_trait_unknown", out var source)) - { - return source; - } - - return null; - } + // private static readonly ModifierService ModifierService = + // App.Current.Services.GetRequiredService(); + // private static readonly LocalisationService LocalisationService = + // App.Current.Services.GetRequiredService(); + // private static readonly SpriteService SpriteService = + // App.Current.Services.GetRequiredService(); + // + // private static readonly string Separator = new('-', 25); + // private static readonly ImageSource? UnknownImage = GetUnknownImageSource(); + + // private static ImageSource? GetUnknownImageSource() + // { + // if (SpriteService.TryGetImageSource("GFX_trait_unknown", out var source)) + // { + // return source; + // } + // + // return null; + // } /// - /// 是否已选择, 当值改变时, 发送 通知 + /// 是否已选择 /// [ObservableProperty] - private bool _isSelected; + public partial bool IsSelected { get; set; } public TraitVo(Trait trait, string localisationName) { Trait = trait; LocalisationName = localisationName; - _imageSource = new Lazy(GetImageSource); + // _imageSource = new Lazy(GetImageSource); } - public ImageSource? ImageSource => _imageSource.Value; - private readonly Lazy _imageSource; + // public ImageSource? ImageSource => _imageSource.Value; + // private readonly Lazy _imageSource; - partial void OnIsSelectedChanged(bool value) - { - WeakReferenceMessenger.Default.Send(new SelectedTraitChangedMessage(value, this)); - } - - private ImageSource? GetImageSource() - { - if (SpriteService.TryGetImageSource($"GFX_trait_{Name}", out var source)) - { - return source; - } - - return UnknownImage; - } - - private TextBlock GetDescription() - { - var textBox = new TextBlock(); - foreach (var inline in ModifierService.GetModifierInlines(Trait.AllModifiers)) - { - textBox.Inlines.Add(inline); - } - - if (textBox.Inlines.Count == 0) - { - textBox.Inlines.Add(new Run { Text = Resource.ModifierDisplay_Empty }); - } - - textBox.Inlines.Add(new LineBreak()); - textBox.Inlines.Add(new Run { Text = Separator }); - textBox.Inlines.Add(new LineBreak()); - - var traitDesc = LocalisationService.GetValue($"{Trait.Name}_desc"); - - foreach (var chars in GetCleanText(traitDesc).Chunk(15)) - { - textBox.Inlines.Add(new Run { Text = new string(chars) }); - textBox.Inlines.Add(new LineBreak()); - } - - if (textBox.Inlines[^1] is LineBreak) - { - textBox.Inlines.RemoveAt(textBox.Inlines.Count - 1); - } - return textBox; - } - - private static string GetCleanText(string rawText) - { - string text; - if (LocalizationFormatParser.TryParse(rawText, out var formats)) - { - var sb = new StringBuilder(); - foreach (var format in formats) - { - sb.Append( - format.Type == LocalizationFormatType.TextWithColor ? format.Text[1..] : format.Text - ); - } - text = sb.ToString(); - } - else - { - text = rawText; - } - - return text; - } + // private ImageSource? GetImageSource() + // { + // if (SpriteService.TryGetImageSource($"GFX_trait_{Name}", out var source)) + // { + // return source; + // } + // + // return UnknownImage; + // } public bool Equals(TraitVo? other) { @@ -154,4 +81,36 @@ public override int GetHashCode() { return Name.GetHashCode(); } + + public sealed class Comparer : IComparer, IComparer + { + public static readonly IComparer Default = new Comparer(); + + private Comparer() { } + + public int Compare(TraitVo? x, TraitVo? y) + { + if (ReferenceEquals(x, y)) + { + return 0; + } + + if (y is null) + { + return 1; + } + + if (x is null) + { + return -1; + } + + return string.Compare(x.LocalisationName, y.LocalisationName, StringComparison.CurrentCulture); + } + + public int Compare(object? x, object? y) + { + return Compare(x as TraitVo, y as TraitVo); + } + } } diff --git a/Moder.Core/Models/WindowBackdropType.cs b/Moder.Core/Models/WindowBackdropType.cs index de8a3c8..532842b 100644 --- a/Moder.Core/Models/WindowBackdropType.cs +++ b/Moder.Core/Models/WindowBackdropType.cs @@ -1,31 +1,31 @@ -using Microsoft.UI.Xaml.Media; - -namespace Moder.Core.Models; - -public enum WindowBackdropType : byte -{ - /// - /// 默认值为 , 当不支持 时切换到 - /// - Default, - - /// - /// null - /// - None, - - /// - /// - /// - Acrylic, - - /// - /// - /// - Mica, - - /// - /// , 设置为 BaseAlt - /// - MicaAlt -} +// using Microsoft.UI.Xaml.Media; +// +// namespace Moder.Core.Models; +// +// public enum WindowBackdropType : byte +// { +// /// +// /// 默认值为 , 当不支持 时切换到 +// /// +// Default, +// +// /// +// /// null +// /// +// None, +// +// /// +// /// +// /// +// Acrylic, +// +// /// +// /// +// /// +// Mica, +// +// /// +// /// , 设置为 BaseAlt +// /// +// MicaAlt +// } diff --git a/Moder.Core/Moder.Core.csproj b/Moder.Core/Moder.Core.csproj index 093cfc1..234d56c 100644 --- a/Moder.Core/Moder.Core.csproj +++ b/Moder.Core/Moder.Core.csproj @@ -1,66 +1,39 @@  WinExe - net9.0-windows10.0.22621.0 - 10.0.17763.0 - Moder.Core + net9.0 + enable + true app.manifest - x86;x64;ARM64 + true + nullable preview - win-x86;win-x64;win-arm64 - win10-x86;win10-x64;win10-arm64 - - true - true logo.ico - - - true - DISABLE_XAML_GENERATED_MAIN - Moder.Core.Program - nullable - enable - true - True - None - - true - - true - partial - false + Debug;Release;Debug-Linux + AnyCPU - - - - - - - - - PreserveNewest - - - - - - - - - - - - - + + + + + + + + + None + All + + + + + + + - - - - all @@ -69,78 +42,77 @@ - - - - + - - - - - - - - - + + $(DefineConstants);ENABLE_XAML_HOT_RELOAD + + + + + + + + $(DefineConstants);WINDOWS + + + $(DefineConstants);LINUX;DEBUG + - - + + - + + PreserveNewest + + + PreserveNewest + + PreserveNewest - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - + + - - + + - - - true - + + + + MainWindow.axaml + Code + + + SideBarControlView.axaml + Code + + + AppInitializeControlView.axaml + Code + + + MainControlView.axaml + Code + + - - PreserveNewest - + + + + + + + - \ No newline at end of file + diff --git a/Moder.Core/Package.appxmanifest b/Moder.Core/Package.appxmanifest deleted file mode 100644 index 557b555..0000000 --- a/Moder.Core/Package.appxmanifest +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - Moder.Core - QWQ - Assets\StoreLogo.png - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Moder.Core/Parser/LocalizationFormat.cs b/Moder.Core/Parser/LocalizationFormat.cs deleted file mode 100644 index d435289..0000000 --- a/Moder.Core/Parser/LocalizationFormat.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Moder.Core.Parser; - -public sealed class LocalizationFormat(string text, LocalizationFormatType type) -{ - public string Text => text; - public LocalizationFormatType Type => type; -} \ No newline at end of file diff --git a/Moder.Core/Program.cs b/Moder.Core/Program.cs index a8772b0..19ff24b 100644 --- a/Moder.Core/Program.cs +++ b/Moder.Core/Program.cs @@ -1,110 +1,26 @@ -using System.Runtime.InteropServices; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Moder.Core.Services; -using Moder.Core.Services.Config; -using Moder.Core.Services.GameResources; -using Moder.Core.Services.ParserRules; -using Moder.Core.Views; -using Moder.Core.Views.Game; -using Moder.Core.Views.Menus; -using Moder.Core.ViewsModels; -using Moder.Core.ViewsModels.Game; -using Moder.Core.ViewsModels.Menus; -using Moder.Hosting.WinUI; -using NLog.Extensions.Logging; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Versioning; +using Avalonia; namespace Moder.Core; -#if !DISABLE_XAML_GENERATED_MAIN -#error "This project only works with custom Main entry point. Must set DISABLE_XAML_GENERATED_MAIN to True." -#endif - -public static partial class Program +public static class Program { - /// - /// Ensures that the process can run XAML, and provides a deterministic error if a - /// check fails. Otherwise, it quietly does nothing. - /// - [LibraryImport("Microsoft.ui.xaml.dll")] - private static partial void XamlCheckProcessRequirements(); - [STAThread] - private static void Main(string[] args) + [SupportedOSPlatform("windows")] + [SupportedOSPlatform("linux")] + [RequiresDynamicCode("Calls Microsoft.Extensions.Hosting.Host.CreateApplicationBuilder()")] + // Macos需要有一个同步的Main方法 + public static void Main(string[] args) { - XamlCheckProcessRequirements(); - - var settings = new HostApplicationBuilderSettings { Args = args, ApplicationName = "Moder" }; - -#if DEBUG - settings.EnvironmentName = "Development"; -#else - settings.EnvironmentName = "Production"; -#endif - var builder = Host.CreateApplicationBuilder(settings); - - // View, ViewModel - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - - builder.Logging.ClearProviders(); - builder.Logging.AddNLog(builder.Configuration); - NLog.LogManager.Configuration = new NLogLoggingConfiguration( - builder.Configuration.GetSection("NLog") - ); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(_ => GlobalSettingService.Load()); - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - // 游戏内资源 - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - // Setup and provision the hosting context for the User Interface - // service. - ((IHostApplicationBuilder)builder).Properties.Add( - HostingExtensions.HostingContextKey, - new HostingContext() { IsLifetimeLinked = true } - ); - - // Add the WinUI User Interface hosted service as early as possible to - // allow the UI to start showing up while you continue setting up other - // services not required for the UI. - var host = builder.ConfigureWinUI().Build(); + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } - // Finally start the host. This will block until the application - // lifetime is terminated through CTRL+C, closing the UI windows or - // programmatically. - host.Run(); + // ReSharper disable once MemberCanBePrivate.Global + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + { + return AppBuilder.Configure().UsePlatformDetect().WithInterFont().LogToTrace(); } } diff --git a/Moder.Core/Properties/launchSettings.json b/Moder.Core/Properties/launchSettings.json deleted file mode 100644 index 539c169..0000000 --- a/Moder.Core/Properties/launchSettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "profiles": { - "Moder.Core (Package)": { - "commandName": "MsixPackage" - }, - "Moder.Core (Unpackaged)": { - "commandName": "Project" - } - } -} \ No newline at end of file diff --git a/Moder.Core/Resources/AppThemeResource.axaml b/Moder.Core/Resources/AppThemeResource.axaml new file mode 100644 index 0000000..41cebd0 --- /dev/null +++ b/Moder.Core/Resources/AppThemeResource.axaml @@ -0,0 +1,14 @@ + + + + + #fafafa + #1e1e1e + + + #252526 + #f5f5f5 + + + + diff --git a/Moder.Core/Resources/AppThemeStyleSetter.axaml b/Moder.Core/Resources/AppThemeStyleSetter.axaml new file mode 100644 index 0000000..c497e11 --- /dev/null +++ b/Moder.Core/Resources/AppThemeStyleSetter.axaml @@ -0,0 +1,6 @@ + + + diff --git a/Moder.Core/Services/AppResourcesService.cs b/Moder.Core/Services/AppResourcesService.cs new file mode 100644 index 0000000..3ac39b5 --- /dev/null +++ b/Moder.Core/Services/AppResourcesService.cs @@ -0,0 +1,8 @@ +using Moder.Core.Models.Game.Character; + +namespace Moder.Core.Services; + +public sealed class AppResourcesService +{ + public SkillCharacterType? CurrentSelectedCharacterType { get; set; } +} \ No newline at end of file diff --git a/Moder.Core/Services/Config/GlobalSettingService.cs b/Moder.Core/Services/Config/AppSettingService.cs similarity index 75% rename from Moder.Core/Services/Config/GlobalSettingService.cs rename to Moder.Core/Services/Config/AppSettingService.cs index 3c380b8..1bda654 100644 --- a/Moder.Core/Services/Config/GlobalSettingService.cs +++ b/Moder.Core/Services/Config/AppSettingService.cs @@ -1,12 +1,12 @@ using MemoryPack; -using Microsoft.UI.Xaml; using Moder.Core.Models; +using Moder.Core.Models.Game; using NLog; namespace Moder.Core.Services.Config; [MemoryPackable] -public sealed partial class GlobalSettingService +public sealed partial class AppSettingService { [MemoryPackOrder(0)] public string ModRootFolderPath @@ -23,32 +23,25 @@ public string GameRootFolderPath } = string.Empty; [MemoryPackOrder(2)] - public ElementTheme AppThemeMode - { - get; - set => SetProperty(ref field, value); - } = ElementTheme.Default; - - [MemoryPackOrder(3)] public GameLanguage GameLanguage { get; set => SetProperty(ref field, value); } = GameLanguage.Default; - [MemoryPackOrder(4)] - public WindowBackdropType WindowBackdropType + [MemoryPackOrder(3)] + public string AppLanguageCode { get; set => SetProperty(ref field, value); - } = WindowBackdropType.Default; - - [MemoryPackOrder(5)] - public string AppLanguage + } = AppLanguageInfo.Default; + + [MemoryPackOrder(4)] + public ThemeMode AppTheme { get; set => SetProperty(ref field, value); - } = AppLanguageInfo.Default; + } = ThemeMode.Light; [MemoryPackIgnore] public bool IsChanged { get; private set; } @@ -57,11 +50,11 @@ public string AppLanguage public bool IsUnchanged => !IsChanged; private const string ConfigFileName = "globalSettings.bin"; - private static string ConfigFilePath => Path.Combine(App.ConfigFolder, ConfigFileName); + private static string ConfigFilePath => Path.Combine(App.AppConfigFolder, ConfigFileName); private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - private GlobalSettingService() { } + private AppSettingService() { } private void SetProperty(ref T field, T newValue) { @@ -91,21 +84,21 @@ public void SaveChanged() Log.Info("配置文件保存完成"); } - public static GlobalSettingService Load() + public static AppSettingService Load() { if (!File.Exists(ConfigFilePath)) { - return new GlobalSettingService(); + return new AppSettingService(); } using var reader = File.OpenRead(ConfigFilePath); var array = new Span(new byte[reader.Length]); _ = reader.Read(array); - var result = MemoryPackSerializer.Deserialize(array); + var result = MemoryPackSerializer.Deserialize(array); if (result is null) { - result = new GlobalSettingService(); + result = new AppSettingService(); } else { diff --git a/Moder.Core/Services/FileNativeService/IFileNativeService.cs b/Moder.Core/Services/FileNativeService/IFileNativeService.cs new file mode 100644 index 0000000..e2eb4a1 --- /dev/null +++ b/Moder.Core/Services/FileNativeService/IFileNativeService.cs @@ -0,0 +1,22 @@ +namespace Moder.Core.Services.FileNativeService; + +public interface IFileNativeService +{ + /// + /// 尝试将文件或文件夹移动到回收站 + /// + /// 文件或文件夹路径 + /// 错误信息 + /// 错误代码 + /// 成功返回 true, 失败返回 false + public bool TryMoveToRecycleBin(string fileOrDirectoryPath, out string? errorMessage, out int errorCode); + + /// + /// 尝试在资源管理器中显示文件或文件夹 + /// + /// 文件或文件夹路径 + /// fileOrDirectoryPath是文件路径时, 为true, 否则为false + /// 错误信息 + /// 成功返回 true, 失败返回 false + public bool TryShowInExplorer(string fileOrDirectoryPath, bool isFile, out string? errorMessage); +} diff --git a/Moder.Core/Services/FileNativeService/LinuxFileNativeService.cs b/Moder.Core/Services/FileNativeService/LinuxFileNativeService.cs new file mode 100644 index 0000000..5c8ec9e --- /dev/null +++ b/Moder.Core/Services/FileNativeService/LinuxFileNativeService.cs @@ -0,0 +1,123 @@ +#if LINUX +using System.Diagnostics; +using System.Web; + +namespace Moder.Core.Services.FileNativeService; + +public sealed class LinuxFileNativeService : IFileNativeService +{ + // XDG 规范 + // 在 Ubuntu 下测试 + // https://cgit.freedesktop.org/xdg/xdg-specs/plain/trash/trash-spec.xml + private const string TrashDir = ".local/share/Trash"; + private const string FilesDir = "files"; + private const string InfoDir = "info"; + + public bool TryMoveToRecycleBin(string fileOrDirectoryPath, out string? errorMessage, out int errorCode) + { + try + { + if (!Path.Exists(fileOrDirectoryPath)) + { + errorMessage = "文件或文件夹不存在"; + errorCode = 1; + return false; + } + + return TryMoveToRecycleBinCore(fileOrDirectoryPath, out errorMessage, out errorCode); + } + catch (Exception e) + { + errorMessage = e.Message; + errorCode = 1; + } + + return false; + } + + /// + public bool TryShowInExplorer(string fileOrDirectoryPath, bool isFile, out string? errorMessage) + { + if (isFile) + { + // 不打开文件, 只打开文件所属文件夹 + // TODO: 可以使用 Dolphin 或 Nautilus 直接打开文件夹并选中对应文件 + fileOrDirectoryPath = Path.GetDirectoryName(fileOrDirectoryPath) ?? fileOrDirectoryPath; + } + var startInfo = new ProcessStartInfo("xdg-open") { Arguments = fileOrDirectoryPath }; + + using var process = Process.Start(startInfo); + + errorMessage = null; + return true; + } + + private static bool TryMoveToRecycleBinCore( + string fileOrDirectoryPath, + out string? errorMessage, + out int errorCode + ) + { + var isFile = File.Exists(fileOrDirectoryPath); + + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var trashPath = Path.Combine(homeDir, TrashDir); + var trashFilesPath = Path.Combine(trashPath, FilesDir); + var trashInfoPath = Path.Combine(trashPath, InfoDir); + + // Ensure the trash directories exist + Directory.CreateDirectory(trashFilesPath); + Directory.CreateDirectory(trashInfoPath); + + var fileOrDirectoryName = Path.GetFileName(fileOrDirectoryPath); + + var destPath = Path.Combine(trashFilesPath, fileOrDirectoryName); + var uniqueFileOrDirectoryName = GetUniqueName(destPath); + + // Create .trashinfo metadata file + var infoFilePath = Path.Combine( + trashInfoPath, + $"{Path.GetFileName(uniqueFileOrDirectoryName)}.trashinfo" + ); + CreateTrashInfoFile(infoFilePath, fileOrDirectoryPath); + + if (isFile) + { + File.Move(fileOrDirectoryPath, uniqueFileOrDirectoryName); + } + else + { + Directory.Move(fileOrDirectoryPath, uniqueFileOrDirectoryName); + } + + errorMessage = null; + errorCode = 0; + return true; + } + + private static string GetUniqueName(string basePath) + { + var uniquePath = basePath; + var counter = 1; + while (Path.Exists(uniquePath)) + { + uniquePath = $"{basePath}.{counter}"; + counter++; + } + + return uniquePath; + } + + private static void CreateTrashInfoFile(string infoFilePath, string originalPath) + { + var path = HttpUtility.UrlEncode(originalPath, Encodings.Utf8NotBom); + // 不转也能正常工作, Ubuntu 是转了的 + path = path.Replace("%2f", "/"); + + using StreamWriter writer = new(infoFilePath, false, Encodings.Utf8NotBom); + writer.WriteLine("[Trash Info]"); + writer.WriteLine($"Path={path}"); + writer.WriteLine($"DeletionDate={DateTime.Now:s}"); + } +} +#endif diff --git a/Moder.Core/Services/FileNativeService/WindowsFileNativeService.cs b/Moder.Core/Services/FileNativeService/WindowsFileNativeService.cs new file mode 100644 index 0000000..69fc744 --- /dev/null +++ b/Moder.Core/Services/FileNativeService/WindowsFileNativeService.cs @@ -0,0 +1,79 @@ +#if WINDOWS +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Versioning; +using Moder.Language.Strings; +using NLog; +using Vanara.PInvoke; +using Vanara.Windows.Shell; + +namespace Moder.Core.Services.FileNativeService; + +public sealed class WindowsFileNativeService : IFileNativeService +{ + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + /// + [SupportedOSPlatform("windows")] + public bool TryMoveToRecycleBin( + string fileOrDirectoryPath, + [NotNullWhen(false)] out string? errorMessage, + out int errorCode + ) + { + // 可以使用 dynamic + // from https://learn.microsoft.com/en-us/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants + + if (!Path.Exists(fileOrDirectoryPath)) + { + errorMessage = Resource.FileManager_NotExist; + errorCode = 0; + return false; + } + + using var operation = new ShellFileOperations(); + operation.Options = + ShellFileOperations.OperationFlags.RecycleOnDelete + | ShellFileOperations.OperationFlags.NoConfirmation; + using var item = new ShellItem(fileOrDirectoryPath); + operation.QueueDeleteOperation(item); + + var result = default(HRESULT); + operation.PostDeleteItem += (_, args) => result = args.Result; + operation.PerformOperations(); + + errorMessage = result.FormatMessage(); + errorCode = result.Code; + + return result.Succeeded; + } + + /// + public bool TryShowInExplorer( + string fileOrDirectoryPath, + bool isFile, + [NotNullWhen(false)] out string? errorMessage + ) + { + try + { + var startInfo = new ProcessStartInfo("explorer.exe") + { + UseShellExecute = true, + Arguments = $"/select, \"{fileOrDirectoryPath}\"" + }; + + using var process = Process.Start(startInfo); + + errorMessage = null; + return true; + } + catch (Exception e) + { + Log.Error(e, "在文件资源管理器中打开失败, path:{Path}", fileOrDirectoryPath); + errorMessage = e.Message; + return false; + } + } +} +#endif diff --git a/Moder.Core/Services/GameModDescriptorService.cs b/Moder.Core/Services/GameModDescriptorService.cs index cc9e7d2..533486d 100644 --- a/Moder.Core/Services/GameModDescriptorService.cs +++ b/Moder.Core/Services/GameModDescriptorService.cs @@ -1,5 +1,5 @@ using System.Collections.Frozen; -using Moder.Core.Parser; +using Moder.Core.Infrastructure.Parser; using Moder.Core.Services.Config; using NLog; @@ -25,7 +25,7 @@ public sealed class GameModDescriptorService /// /// 当文件不存在时 /// - public GameModDescriptorService(GlobalSettingService settingService) + public GameModDescriptorService(AppSettingService settingService) { var logger = LogManager.GetCurrentClassLogger(); var descriptorFilePath = Path.Combine(settingService.ModRootFolderPath, FileName); @@ -60,6 +60,6 @@ public GameModDescriptorService(GlobalSettingService settingService) break; } } - _replacePaths = replacePathList.ToFrozenSet(); + _replacePaths = replacePathList.ToFrozenSet(StringComparer.OrdinalIgnoreCase); } -} \ No newline at end of file +} diff --git a/Moder.Core/Services/GameResources/Base/CommonResourcesService.cs b/Moder.Core/Services/GameResources/Base/CommonResourcesService.cs index 6c30c0e..e2b42aa 100644 --- a/Moder.Core/Services/GameResources/Base/CommonResourcesService.cs +++ b/Moder.Core/Services/GameResources/Base/CommonResourcesService.cs @@ -1,5 +1,5 @@ using Moder.Core.Extensions; -using Moder.Core.Parser; +using Moder.Core.Infrastructure.Parser; using ParadoxPower.Process; namespace Moder.Core.Services.GameResources.Base; diff --git a/Moder.Core/Services/GameResources/GameResourcesWatcherService.cs b/Moder.Core/Services/GameResources/Base/GameResourcesWatcherService.cs similarity index 93% rename from Moder.Core/Services/GameResources/GameResourcesWatcherService.cs rename to Moder.Core/Services/GameResources/Base/GameResourcesWatcherService.cs index 76211d4..992e5aa 100644 --- a/Moder.Core/Services/GameResources/GameResourcesWatcherService.cs +++ b/Moder.Core/Services/GameResources/Base/GameResourcesWatcherService.cs @@ -1,15 +1,14 @@ using EnumsNET; -using Moder.Core.Helper; +using Moder.Core.Infrastructure; using Moder.Core.Services.Config; -using Moder.Core.Services.GameResources.Base; using NLog; -namespace Moder.Core.Services.GameResources; +namespace Moder.Core.Services.GameResources.Base; /// /// 用来监听游戏资源的改变, 如注册的国家标签, 资源, 建筑物 /// -public sealed partial class GameResourcesWatcherService : IDisposable +public sealed class GameResourcesWatcherService : IDisposable { /// /// key: 资源文件夹路径, value: 监听器列表 @@ -17,6 +16,9 @@ public sealed partial class GameResourcesWatcherService : IDisposable private readonly Dictionary> _watchedPaths = new(8); private readonly FileSystemSafeWatcher _watcher; + //TODO: 重构, 可以用这个类监测资源服务的变化并发出通知 + //监听者???, 消息总线? + /// /// 待监听文件夹列表, 其中的文件夹被创建或从其他名称重命名后, 会被自动监听, 然后被移除 /// @@ -27,9 +29,9 @@ public sealed partial class GameResourcesWatcherService : IDisposable bool includeSubFolders )> _waitingWatchFolders = new(8); private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - private readonly GlobalSettingService _settingService; + private readonly AppSettingService _settingService; - public GameResourcesWatcherService(GlobalSettingService settingService) + public GameResourcesWatcherService(AppSettingService settingService) { _settingService = settingService; _watcher = new FileSystemSafeWatcher(_settingService.ModRootFolderPath, "*.*"); diff --git a/Moder.Core/Services/GameResources/Base/ResourcesService.cs b/Moder.Core/Services/GameResources/Base/ResourcesService.cs index 83831d6..c71c582 100644 --- a/Moder.Core/Services/GameResources/Base/ResourcesService.cs +++ b/Moder.Core/Services/GameResources/Base/ResourcesService.cs @@ -16,7 +16,7 @@ public abstract partial class ResourcesService : protected readonly Dictionary Resources; protected readonly Logger Log; - private readonly GlobalSettingService _settingService; + private readonly AppSettingService _settingService; private readonly string _serviceName = typeof(TType).Name; private readonly string _folderOrFileRelativePath; @@ -30,10 +30,10 @@ protected ResourcesService(string folderOrFileRelativePath, WatcherFilter filter { _folderOrFileRelativePath = folderOrFileRelativePath; Log = LogManager.GetLogger(typeof(TType).FullName); - _settingService = App.Current.Services.GetRequiredService(); + _settingService = App.Services.GetRequiredService(); - var gameResourcesPathService = App.Current.Services.GetRequiredService(); - var watcherService = App.Current.Services.GetRequiredService(); + var gameResourcesPathService = App.Services.GetRequiredService(); + var watcherService = App.Services.GetRequiredService(); var isFolderPath = pathType == PathType.Folder; var filePaths = isFolderPath diff --git a/Moder.Core/Services/GameResources/BuildingsService.cs b/Moder.Core/Services/GameResources/BuildingsService.cs deleted file mode 100644 index 31ad63b..0000000 --- a/Moder.Core/Services/GameResources/BuildingsService.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Collections.Frozen; -using System.Diagnostics.CodeAnalysis; -using Moder.Core.Models; -using Moder.Core.Services.GameResources.Base; -using ParadoxPower.CSharpExtensions; -using ParadoxPower.Process; - -namespace Moder.Core.Services.GameResources; - -public sealed class BuildingsService - : CommonResourcesService> -{ - private Dictionary>.ValueCollection Buildings => - Resources.Values; - private const string BuildingsKeyword = "buildings"; - - public BuildingsService() - : base(Path.Combine(Keywords.Common, BuildingsKeyword), WatcherFilter.Text) { } - - public bool Contains(string buildingType) - { - foreach (var building in Buildings) - { - if (building.ContainsKey(buildingType)) - { - return true; - } - } - return false; - } - - public bool TryGetBuildingInfo(string buildingType, [NotNullWhen(true)] out BuildingInfo? buildingInfo) - { - foreach (var building in Buildings) - { - if (building.TryGetValue(buildingType, out buildingInfo)) - { - return true; - } - } - - buildingInfo = null; - return false; - } - - protected override FrozenDictionary? ParseFileToContent(Node rootNode) - { - if (!rootNode.TryGetNode(BuildingsKeyword, out var buildingsNode)) - { - Log.Warn("buildings node not found"); - return null; - } - - var buildings = ParseBuildingNode(buildingsNode.Nodes); - return buildings; - } - - private FrozenDictionary ParseBuildingNode(IEnumerable buildingNodes) - { - var buildings = new Dictionary(8); - foreach (var buildingNode in buildingNodes) - { - ParseBuildingNodeToDictionary(buildingNode, buildings); - } - - return buildings.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); - } - - private void ParseBuildingNodeToDictionary( - Node buildingNode, - Dictionary buildings - ) - { - byte? maxLevel = null; - var levelCapNode = buildingNode.Nodes.FirstOrDefault(node => - StringComparer.OrdinalIgnoreCase.Equals(node.Key, "level_cap") - ); - if (levelCapNode is null) - { - Log.Warn("建筑 {Building} 的 level_cap node 未找到", buildingNode.Key); - return; - } - - foreach (var levelPropertyLeaf in levelCapNode.Leaves) - { - if ( - levelPropertyLeaf.Key.Equals("state_max", StringComparison.OrdinalIgnoreCase) - || levelPropertyLeaf.Key.Equals("province_max", StringComparison.OrdinalIgnoreCase) - ) - { - if (byte.TryParse(levelPropertyLeaf.ValueText, out var value)) - { - maxLevel = value; - } - break; - } - } - - if (!maxLevel.HasValue) - { - Log.Warn("{Building} 的 最大等级 信息未找到", buildingNode.Key); - } - buildings[buildingNode.Key] = new BuildingInfo(buildingNode.Key, maxLevel); - } -} diff --git a/Moder.Core/Services/GameResources/CharacterSkillService.cs b/Moder.Core/Services/GameResources/CharacterSkillService.cs index 5137c59..d77b92a 100644 --- a/Moder.Core/Services/GameResources/CharacterSkillService.cs +++ b/Moder.Core/Services/GameResources/CharacterSkillService.cs @@ -1,8 +1,6 @@ using MethodTimer; -using Microsoft.UI.Xaml.Documents; -using Moder.Core.Models.Character; +using Moder.Core.Models.Game.Character; using Moder.Core.Services.GameResources.Base; -using Moder.Language.Strings; using ParadoxPower.Process; namespace Moder.Core.Services.GameResources; @@ -10,45 +8,24 @@ namespace Moder.Core.Services.GameResources; /// /// 用于提供人物技能的信息, 如最大值, 修正等 /// -public sealed class CharacterSkillService : CommonResourcesService> +[method: Time("加载人物技能信息")] +public sealed class CharacterSkillService() + : CommonResourcesService( + Path.Combine(Keywords.Common, "unit_leader"), + WatcherFilter.Text + ) { - private readonly ModifierService _modifierService; - private const ushort DefaultSkillMaxValue = 1; + public IEnumerable Skills => Resources.Values.SelectMany(s => s); - private IEnumerable Skills => Resources.Values.SelectMany(s => s); - - [Time("加载人物技能信息")] - public CharacterSkillService(ModifierService modifierService) - : base(Path.Combine(Keywords.Common, "unit_leader"), WatcherFilter.Text) - { - _modifierService = modifierService; - } + private const ushort DefaultSkillMaxValue = 1; - public ushort GetMaxSkillValue(SkillType skillType, CharacterSkillType characterSkillType) + public ushort GetMaxSkillValue(SkillType skillType, SkillCharacterType skillCharacterType) { - return Skills.FirstOrDefault(skill => skill.SkillType == skillType)?.GetMaxValue(characterSkillType) + return Skills.FirstOrDefault(skill => skill.SkillType == skillType)?.GetMaxValue(skillCharacterType) ?? DefaultSkillMaxValue; } - public IEnumerable GetSkillModifierDescription( - SkillType skillType, - CharacterSkillType characterSkillType, - ushort level - ) - { - var skillModifier = Skills - .FirstOrDefault(skill => skill.SkillType == skillType) - ?.GetModifierDescription(characterSkillType, level); - - if (skillModifier is null || skillModifier.Modifiers.Count == 0) - { - return [new Run { Text = Resource.CharacterEditor_None }]; - } - - return _modifierService.GetModifierInlines(skillModifier.Modifiers); - } - - protected override List? ParseFileToContent(Node rootNode) + protected override SkillInfo[]? ParseFileToContent(Node rootNode) { var skills = new List(); @@ -91,13 +68,13 @@ ushort level skills.Add(ParseSkills(node, skillType)); } - return skills; + return skills.ToArray(); } private SkillInfo ParseSkills(Node node, SkillType skillType) { - var skillMap = new Dictionary(3); - var skillModifiers = new Dictionary>(3); + var skillMap = new Dictionary(3); + var skillModifiers = new Dictionary>(3); foreach (var skillInfoNode in node.Nodes) { @@ -109,7 +86,7 @@ private SkillInfo ParseSkills(Node node, SkillType skillType) continue; } - if (!CharacterSkillType.TryFromName(skillTypeLeaf.ValueText, true, out var type)) + if (!SkillCharacterType.TryFromName(skillTypeLeaf.ValueText, true, out var type)) { continue; } diff --git a/Moder.Core/Services/GameResources/CharacterTraitsService.cs b/Moder.Core/Services/GameResources/CharacterTraitsService.cs index 15c33d8..7e8f761 100644 --- a/Moder.Core/Services/GameResources/CharacterTraitsService.cs +++ b/Moder.Core/Services/GameResources/CharacterTraitsService.cs @@ -3,10 +3,10 @@ using System.Runtime.InteropServices; using MethodTimer; using Moder.Core.Extensions; -using Moder.Core.Helper; -using Moder.Core.Models.Character; -using Moder.Core.Models.Modifiers; +using Moder.Core.Models.Game.Character; +using Moder.Core.Models.Game.Modifiers; using Moder.Core.Services.GameResources.Base; +using Moder.Core.Services.GameResources.Localization; using ParadoxPower.Process; namespace Moder.Core.Services.GameResources; @@ -14,6 +14,10 @@ namespace Moder.Core.Services.GameResources; public sealed class CharacterTraitsService : CommonResourcesService> { + public IEnumerable GetAllTraits() => _allTraitsLazy.Value; + + private Lazy> _allTraitsLazy; + private readonly LocalizationService _localizationService; private Dictionary>.ValueCollection Traits => Resources.Values; /// @@ -28,9 +32,39 @@ public sealed class CharacterTraitsService "sub_unit_modifiers" ]; + private static readonly string[] SkillModifierKeywords = + [ + "attack_skill", + "defense_skill", + "planning_skill", + "logistics_skill", + "maneuvering_skill", + "coordination_skill" + ]; + + private static readonly string[] SkillFactorModifierKeywords = + [ + "skill_factor", + "attack_skill_factor", + "defense_skill_factor", + "planning_skill_factor", + "logistics_skill_factor", + "maneuvering_skill_factor", + "coordination_skill_factor" + ]; + [Time("加载人物特质")] - public CharacterTraitsService() - : base(Path.Combine(Keywords.Common, "unit_leader"), WatcherFilter.Text) { } + public CharacterTraitsService(LocalizationService localizationService) + : base(Path.Combine(Keywords.Common, "unit_leader"), WatcherFilter.Text) + { + _localizationService = localizationService; + + _allTraitsLazy = GetAllTraitsLazy(); + OnResourceChanged += (_, _) => _allTraitsLazy = GetAllTraitsLazy(); + } + + private Lazy> GetAllTraitsLazy() => + new(() => Traits.SelectMany(trait => trait.Values).ToArray()); public bool TryGetTrait(string name, [NotNullWhen(true)] out Trait? trait) { @@ -46,7 +80,10 @@ public bool TryGetTrait(string name, [NotNullWhen(true)] out Trait? trait) return false; } - public IEnumerable GetAllTraits() => Traits.SelectMany(trait => trait.Values); + public string GetLocalizationName(Trait trait) + { + return _localizationService.GetValue(trait.Name); + } protected override FrozenDictionary? ParseFileToContent(Node rootNode) { @@ -74,27 +111,6 @@ public bool TryGetTrait(string name, [NotNullWhen(true)] out Trait? trait) return dictionary.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); } - private static readonly string[] SkillModifierKeywords = - [ - "attack_skill", - "defense_skill", - "planning_skill", - "logistics_skill", - "maneuvering_skill", - "coordination_skill" - ]; - - private static readonly string[] SkillFactorModifierKeywords = - [ - "skill_factor", - "attack_skill_factor", - "defense_skill_factor", - "planning_skill_factor", - "logistics_skill_factor", - "maneuvering_skill_factor", - "coordination_skill_factor" - ]; - /// /// /// @@ -131,7 +147,7 @@ private ReadOnlySpan ParseTraitsNode(Node traitsNode) && Array.Exists(ModifierNodeKeys, s => StringComparer.OrdinalIgnoreCase.Equals(s, key)) ) { - modifiers.Add(ModifierHelper.ParseModifier(traitAttribute.node)); + modifiers.Add(ParseModifier(traitAttribute.node)); } else if ( traitAttribute.IsLeafChild @@ -245,4 +261,25 @@ private static bool IsSkillModifier(Child traitAttribute) ) ); } + + private static ModifierCollection ParseModifier(Node modifierNode) + { + var list = new List(modifierNode.AllArray.Length); + foreach (var child in modifierNode.AllArray) + { + if (child.IsLeafChild) + { + var modifier = LeafModifier.FromLeaf(child.leaf); + list.Add(modifier); + } + else if (child.IsNodeChild) + { + var node = child.node; + var modifier = new NodeModifier(node.Key, node.Leaves.Select(LeafModifier.FromLeaf)); + list.Add(modifier); + } + } + + return new ModifierCollection(modifierNode.Key, list); + } } diff --git a/Moder.Core/Services/GameResources/CountryTagService.cs b/Moder.Core/Services/GameResources/CountryTagService.cs deleted file mode 100644 index 9a30a4f..0000000 --- a/Moder.Core/Services/GameResources/CountryTagService.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Collections.Frozen; -using Moder.Core.Services.GameResources.Base; -using ParadoxPower.Process; - -namespace Moder.Core.Services.GameResources; - -public sealed class CountryTagService : CommonResourcesService> -{ - /// - /// 在游戏内注册的国家标签 - /// - public IReadOnlyCollection CountryTags => _countryTagsLazy.Value; - - private Lazy> _countryTagsLazy; - - public CountryTagService() - : base(Path.Combine(Keywords.Common, "country_tags"), WatcherFilter.Text) - { - _countryTagsLazy = new Lazy>(GetCountryTags); - OnResourceChanged += (_, _) => - { - _countryTagsLazy = new Lazy>(GetCountryTags); - Log.Debug("Country tags changed, 已重置"); - }; - } - - private string[] GetCountryTags() - { - return Resources.Values.SelectMany(set => set.Items).ToArray(); - } - - protected override FrozenSet? ParseFileToContent(Node rootNode) - { - var leaves = rootNode.Leaves.ToArray(); - // 不加载临时标签 - if ( - Array.Exists( - leaves, - leaf => - leaf.Key.Equals("dynamic_tags", StringComparison.OrdinalIgnoreCase) - && leaf.ValueText.Equals("yes", StringComparison.OrdinalIgnoreCase) - ) - ) - { - return null; - } - - var countryTags = new HashSet(leaves.Length); - foreach (var leaf in leaves) - { - var countryTag = leaf.Key; - // 国家标签长度必须为 3 - if (countryTag.Length != 3) - { - continue; - } - countryTags.Add(countryTag); - } - return countryTags.ToFrozenSet(StringComparer.OrdinalIgnoreCase); - } -} diff --git a/Moder.Core/Services/GameResources/GameResourcesPathService.cs b/Moder.Core/Services/GameResources/GameResourcesPathService.cs index 6ffd77f..931abb6 100644 --- a/Moder.Core/Services/GameResources/GameResourcesPathService.cs +++ b/Moder.Core/Services/GameResources/GameResourcesPathService.cs @@ -6,11 +6,12 @@ namespace Moder.Core.Services.GameResources; public sealed class GameResourcesPathService { - private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - private readonly GlobalSettingService _settingService; + private readonly AppSettingService _settingService; private readonly GameModDescriptorService _descriptor; - public GameResourcesPathService(GlobalSettingService settingService, GameModDescriptorService descriptor) + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + public GameResourcesPathService(AppSettingService settingService, GameModDescriptorService descriptor) { _settingService = settingService; _descriptor = descriptor; diff --git a/Moder.Core/Services/GameResources/Localization/LocalizationFormatService.cs b/Moder.Core/Services/GameResources/Localization/LocalizationFormatService.cs new file mode 100644 index 0000000..834ab31 --- /dev/null +++ b/Moder.Core/Services/GameResources/Localization/LocalizationFormatService.cs @@ -0,0 +1,66 @@ +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Moder.Core.Infrastructure.Parser; +using Moder.Core.Models; + +namespace Moder.Core.Services.GameResources.Localization; + +public sealed class LocalizationFormatService(LocalizationTextColorsService localizationTextColorsService) +{ + /// + /// 从文本中获取颜色信息, 返回的集合中不包含 类型的文本, 如果解析失败, 则统一使用黑色 + /// + /// 文本 + /// 一个集合, 包含非占位符的所有文本颜色信息 + public IReadOnlyCollection GetColorText(string text) + { + var result = new List(4); + + if (LocalizationFormatParser.TryParse(text, out var formats)) + { + foreach (var format in formats) + { + if (format.Type != LocalizationFormatType.Placeholder) + { + result.Add(GetColorText(format)); + } + } + } + else + { + result.Add(new ColorTextInfo(text, Brushes.Black)); + } + + return result; + } + + /// + /// 尝试将文本解析为 , 并使用 中指定的颜色, 如果颜色不存在, 则使用默认颜色 + /// + /// 文本格式信息 + /// + public ColorTextInfo GetColorText(LocalizationFormatInfo format) + { + if (format.Type == LocalizationFormatType.TextWithColor) + { + if (string.IsNullOrEmpty(format.Text)) + { + return new ColorTextInfo(string.Empty, Brushes.Black); + } + + if (localizationTextColorsService.TryGetColor(format.Text[0], out var colorInfo)) + { + if (!_colorBrushes.TryGetValue(format.Text[0], out var brush)) + { + brush = new ImmutableSolidColorBrush(colorInfo.Color); + _colorBrushes.Add(format.Text[0], brush); + } + return new ColorTextInfo(format.Text[1..], brush); + } + } + + return new ColorTextInfo(format.Text, Brushes.Black); + } + + private readonly Dictionary _colorBrushes = []; +} diff --git a/Moder.Core/Services/GameResources/Localization/LocalizationKeyMappingService.cs b/Moder.Core/Services/GameResources/Localization/LocalizationKeyMappingService.cs new file mode 100644 index 0000000..58e637c --- /dev/null +++ b/Moder.Core/Services/GameResources/Localization/LocalizationKeyMappingService.cs @@ -0,0 +1,57 @@ +using System.Diagnostics.CodeAnalysis; +using EnumsNET; +using Moder.Core.Models.Game.Character; + +namespace Moder.Core.Services.GameResources.Localization; + +/// +/// 用来解决脚本关键字与本地化文本中的键不一致的问题 +/// +public sealed class LocalizationKeyMappingService +{ + /// + /// 当调用方法查找Key对应的本地化文本时,如果字典内存在Key, 则使用Key对应的Value进行查询 + /// + private readonly Dictionary _localisationKeyMapping = + new(StringComparer.OrdinalIgnoreCase); + + public LocalizationKeyMappingService() + { + // 添加特性中技能的本地化映射 + // 6种技能类型, attack, defense, planning, logistics, maneuvering, coordination + foreach ( + var skillType in Enums + .GetNames() + .Where(name => !name.Equals("level", StringComparison.OrdinalIgnoreCase)) + ) + { + AddKeyMapping($"{skillType}_skill", $"trait_bonus_{skillType}"); + + AddKeyMapping( + $"{skillType}_skill_factor", + // FACTOR 中是 Defence, 技能加成中就是 Defense, 不理解为什么要这样写 + skillType == "Defense" + ? "BOOST_DEFENCE_FACTOR" + : $"boost_{skillType}_factor" + ); + } + + // 突破 + AddKeyMapping("breakthrough_factor", "MODIFIER_BREAKTHROUGH"); + } + + /// + /// 添加映射 + /// + /// 原始键 + /// 映射键 + public void AddKeyMapping(string rawKey, string mappingKey) + { + _localisationKeyMapping[rawKey] = mappingKey; + } + + public bool TryGetValue(string key, [NotNullWhen(true)] out string? mappingKey) + { + return _localisationKeyMapping.TryGetValue(key, out mappingKey); + } +} diff --git a/Moder.Core/Services/GameResources/LocalisationService.cs b/Moder.Core/Services/GameResources/Localization/LocalizationService.cs similarity index 66% rename from Moder.Core/Services/GameResources/LocalisationService.cs rename to Moder.Core/Services/GameResources/Localization/LocalizationService.cs index 9676d52..4fb4ac3 100644 --- a/Moder.Core/Services/GameResources/LocalisationService.cs +++ b/Moder.Core/Services/GameResources/Localization/LocalizationService.cs @@ -8,28 +8,27 @@ using ParadoxPower.CSharp; using ParadoxPower.Localisation; -namespace Moder.Core.Services.GameResources; +namespace Moder.Core.Services.GameResources.Localization; -public sealed class LocalisationService - : ResourcesService, YAMLLocalisationParser.LocFile> +public sealed class LocalizationService + : ResourcesService, YAMLLocalisationParser.LocFile> { private Dictionary>.ValueCollection Localisations => Resources.Values; - private readonly LocalisationKeyMappingService _localisationKeyMapping; + private readonly LocalizationKeyMappingService _localizationKeyMapping; [Time("加载本地化文件")] - public LocalisationService(LocalisationKeyMappingService localisationKeyMapping) + public LocalizationService(LocalizationKeyMappingService localizationKeyMapping) : base( Path.Combine( "localisation", - App.Current.Services.GetRequiredService() - .GameLanguage.ToGameLocalizationLanguage() + App.Services.GetRequiredService().GameLanguage.ToGameLocalizationLanguage() ), WatcherFilter.LocalizationFiles, PathType.Folder ) { - _localisationKeyMapping = localisationKeyMapping; + _localizationKeyMapping = localizationKeyMapping; } /// @@ -67,56 +66,21 @@ public string GetValueInAll(string key) } /// - /// 查找本地化字符串, 先尝试在 中查找 Key 是否有替换的 Key + /// 查找本地化字符串, 先尝试在 中查找 Key 是否有替换的 Key /// /// /// /// public bool TryGetValueInAll(string key, [NotNullWhen(true)] out string? value) { - if (_localisationKeyMapping.TryGetValue(key, out var config)) + if (_localizationKeyMapping.TryGetValue(key, out var mappingKey)) { - key = config.LocalisationKey; + key = mappingKey; } return TryGetValue(key, out value); } - public string GetModifier(string modifier) - { - if (TryGetValueInAll(modifier, out var value)) - { - return value; - } - - if (TryGetValue($"MODIFIER_{modifier}", out value)) - { - return value; - } - - if (TryGetValue($"MODIFIER_NAVAL_{modifier}", out value)) - { - return value; - } - - if (TryGetValue($"MODIFIER_UNIT_LEADER_{modifier}", out value)) - { - return value; - } - - if (TryGetValue($"MODIFIER_ARMY_LEADER_{modifier}", out value)) - { - return value; - } - - return modifier; - } - - public bool TryGetModifierTt(string modifier, [NotNullWhen(true)] out string? result) - { - return TryGetValue($"{modifier}_tt", out result); - } - protected override FrozenDictionary ParseFileToContent( YAMLLocalisationParser.LocFile result ) diff --git a/Moder.Core/Services/GameResources/LocalizationTextColorsService.cs b/Moder.Core/Services/GameResources/Localization/LocalizationTextColorsService.cs similarity index 70% rename from Moder.Core/Services/GameResources/LocalizationTextColorsService.cs rename to Moder.Core/Services/GameResources/Localization/LocalizationTextColorsService.cs index d47ece8..6d99117 100644 --- a/Moder.Core/Services/GameResources/LocalizationTextColorsService.cs +++ b/Moder.Core/Services/GameResources/Localization/LocalizationTextColorsService.cs @@ -1,12 +1,12 @@ using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; +using Avalonia.Media; using MethodTimer; -using Moder.Core.Models; +using Moder.Core.Models.Game; using Moder.Core.Services.GameResources.Base; using ParadoxPower.Process; -using Windows.UI; -namespace Moder.Core.Services.GameResources; +namespace Moder.Core.Services.GameResources.Localization; public sealed class LocalizationTextColorsService : CommonResourcesService> @@ -52,17 +52,29 @@ public bool TryGetColor(char key, [NotNullWhen(true)] out LocalizationTextColor? foreach (var textColorNode in textColorsNode.Nodes) { var key = textColorNode.Key[0]; - var color = textColorNode - .LeafValues.Select(value => byte.TryParse(value.ValueText, out var result) ? result : (byte)0) - .ToArray(); + var colorBytes = new List(3); - if (color.Length != 3) + foreach (var leafValue in textColorNode.LeafValues) + { + if (byte.TryParse(leafValue.ValueText, out var colorByte)) + { + colorBytes.Add(colorByte); + } + else + { + Log.Warn("颜色 {Key} 的值 {Value} 不是数字", textColorNode.Key, colorByte); + } + } + if (colorBytes.Count != 3) { Log.Warn("颜色 {Key} 的长度不正确", textColorNode.Key); continue; } - colors[key] = new LocalizationTextColor(key, Color.FromArgb(255, color[0], color[1], color[2])); + colors[key] = new LocalizationTextColor( + key, + Color.FromRgb(colorBytes[0], colorBytes[1], colorBytes[2]) + ); } return colors.ToFrozenDictionary(); diff --git a/Moder.Core/Services/GameResources/Modifiers/ModifierDisplayService.cs b/Moder.Core/Services/GameResources/Modifiers/ModifierDisplayService.cs new file mode 100644 index 0000000..fb3c051 --- /dev/null +++ b/Moder.Core/Services/GameResources/Modifiers/ModifierDisplayService.cs @@ -0,0 +1,239 @@ +using Avalonia.Controls.Documents; +using Moder.Core.Models.Game; +using Moder.Core.Models.Game.Character; +using Moder.Core.Models.Game.Modifiers; +using Moder.Core.Services.GameResources.Localization; +using Moder.Language.Strings; +using NLog; + +namespace Moder.Core.Services.GameResources.Modifiers; + +public sealed class ModifierDisplayService +{ + private readonly LocalizationFormatService _localisationFormatService; + private readonly LocalizationService _localizationService; + private readonly ModifierService _modifierService; + private readonly TerrainService _terrainService; + private readonly LocalizationKeyMappingService _localisationKeyMappingService; + private readonly CharacterSkillService _characterSkillService; + + private const string NodeModifierChildrenPrefix = " "; + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + public ModifierDisplayService( + LocalizationFormatService localisationFormatService, + LocalizationService localizationService, + ModifierService modifierService, + LocalizationKeyMappingService localisationKeyMappingService, + TerrainService terrainService, + CharacterSkillService characterSkillService + ) + { + _localisationFormatService = localisationFormatService; + _localizationService = localizationService; + _modifierService = modifierService; + _localisationKeyMappingService = localisationKeyMappingService; + _terrainService = terrainService; + _characterSkillService = characterSkillService; + } + + public IEnumerable GetSkillModifierDescription( + SkillType skillType, + SkillCharacterType skillCharacterType, + ushort level + ) + { + var skillModifier = _characterSkillService + .Skills.FirstOrDefault(skill => skill.SkillType == skillType) + ?.GetModifierDescription(skillCharacterType, level); + + if (skillModifier is null || skillModifier.Modifiers.Count == 0) + { + return [new Run { Text = Resource.CharacterEditor_None }]; + } + + return GetDescription(skillModifier.Modifiers); + } + + public IReadOnlyCollection GetDescription(IEnumerable modifiers) + { + var inlines = new List(8); + + foreach (var modifier in modifiers) + { + IEnumerable addedInlines; + switch (modifier.Type) + { + case ModifierType.Leaf: + { + var leafModifier = (LeafModifier)modifier; + if (IsCustomToolTip(leafModifier.Key)) + { + var name = _localizationService.GetValue(leafModifier.Value); + addedInlines = _localisationFormatService + .GetColorText(name) + .Select(colorTextInfo => new Run(colorTextInfo.DisplayText) + { + Foreground = colorTextInfo.Brush + }); + } + else + { + addedInlines = GetDescriptionForLeaf(leafModifier); + } + + break; + } + case ModifierType.Node: + { + var nodeModifier = (NodeModifier)modifier; + addedInlines = GetModifierDescriptionForNode(nodeModifier); + break; + } + default: + continue; + } + + inlines.AddRange(addedInlines); + inlines.Add(new LineBreak()); + } + + RemoveLastLineBreak(inlines); + return inlines; + } + + private static bool IsCustomToolTip(string modifierKey) + { + return StringComparer.OrdinalIgnoreCase.Equals(modifierKey, LeafModifier.CustomEffectTooltipKey) + || StringComparer.OrdinalIgnoreCase.Equals(modifierKey, LeafModifier.CustomModifierTooltipKey); + } + + private List GetDescriptionForLeaf(LeafModifier modifier) + { + var modifierKey = _localisationKeyMappingService.TryGetValue(modifier.Key, out var mappingKey) + ? mappingKey + : modifier.Key; + var inlines = new List(4); + GetModifierColorTextFromText(modifierKey, inlines); + + if (modifier.ValueType is GameValueType.Int or GameValueType.Float) + { + var modifierFormat = _modifierService.TryGetLocalizationFormat(modifierKey, out var result) + ? result + : string.Empty; + inlines.Add(GetRun(modifier, modifierFormat)); + } + else + { + inlines.Add(new Run { Text = modifier.Value }); + } + return inlines; + } + + private void GetModifierColorTextFromText(string modifierKey, List inlines) + { + var modifierName = _modifierService.GetLocalizationName(modifierKey); + foreach (var colorTextInfo in _localisationFormatService.GetColorText(modifierName)) + { + inlines.Add(new Run(colorTextInfo.DisplayText) { Foreground = colorTextInfo.Brush }); + } + } + + private List GetModifierDescriptionForNode(NodeModifier nodeModifier) + { + if (_terrainService.Contains(nodeModifier.Key)) + { + return GetTerrainModifierDescription(nodeModifier); + } + + return GetDescriptionForUnknownNode(nodeModifier); + } + + /// + /// 获取地形修饰符的描述 + /// + /// + /// + private List GetTerrainModifierDescription(NodeModifier nodeModifier) + { + return GetDescriptionForNode( + nodeModifier, + leafModifier => + { + var modifierName = _localizationService.GetValue($"STAT_ADJUSTER_{leafModifier.Key}"); + var modifierFormat = _localizationService.GetValue($"STAT_ADJUSTER_{leafModifier.Key}_DIFF"); + return + [ + new Run { Text = $"{NodeModifierChildrenPrefix}{modifierName}" }, + GetRun(leafModifier, modifierFormat) + ]; + } + ); + } + + /// + /// 从 中获取LeafModifier.Value, 并使用modifierFormat设置值的格式 + /// + /// + /// + /// + private Run GetRun(LeafModifier modifier, string modifierFormat) + { + return new Run + { + Text = _modifierService.GetDisplayValue(modifier, modifierFormat), + Foreground = _modifierService.GetModifierBrush(modifier, modifierFormat) + }; + } + + private List GetDescriptionForUnknownNode(NodeModifier nodeModifier) + { + Log.Warn("未知的节点修饰符: {Name}", nodeModifier.Key); + return GetDescriptionForNode( + nodeModifier, + leafModifier => + { + var runs = GetDescriptionForLeaf(leafModifier); + foreach (var run in runs) + { + run.Text = $"{NodeModifierChildrenPrefix}{run.Text}"; + } + + return runs; + } + ); + } + + private List GetDescriptionForNode( + NodeModifier nodeModifier, + Func> func + ) + { + var inlines = new List(nodeModifier.Modifiers.Count * 3) + { + new Run { Text = $"{_localizationService.GetValue(nodeModifier.Key)}:" }, + new LineBreak() + }; + + foreach (var leafModifier in nodeModifier.Modifiers) + { + inlines.AddRange(func(leafModifier)); + inlines.Add(new LineBreak()); + } + + RemoveLastLineBreak(inlines); + return inlines; + } + + /// + /// 移除末尾多余的换行 + /// + /// 一段文本的 集合 + private static void RemoveLastLineBreak(List inlines) + { + if (inlines.Count != 0 && inlines[^1] is LineBreak) + { + inlines.RemoveAt(inlines.Count - 1); + } + } +} diff --git a/Moder.Core/Services/GameResources/Modifiers/ModifierService.cs b/Moder.Core/Services/GameResources/Modifiers/ModifierService.cs new file mode 100644 index 0000000..72ad774 --- /dev/null +++ b/Moder.Core/Services/GameResources/Modifiers/ModifierService.cs @@ -0,0 +1,177 @@ +using System.Diagnostics.CodeAnalysis; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Moder.Core.Models.Game; +using Moder.Core.Models.Game.Modifiers; +using Moder.Core.Services.GameResources.Localization; +using NLog; + +namespace Moder.Core.Services.GameResources.Modifiers; + +public sealed class ModifierService +{ + private readonly LocalizationService _localizationService; + + public ModifierService(LocalizationService localizationService) + { + _localizationService = localizationService; + } + + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private static readonly ImmutableSolidColorBrush Yellow = new(Color.FromRgb(255, 189, 0)); + + public IBrush GetModifierBrush(LeafModifier leafModifier, string modifierFormat) + { + var value = double.Parse(leafModifier.Value); + if (value == 0.0) + { + return Yellow; + } + + var modifierType = GetModifierType(leafModifier.Key, modifierFormat); + if (modifierType == ModifierEffectType.Unknown) + { + return Brushes.Black; + } + + if (value > 0.0) + { + if (modifierType == ModifierEffectType.Positive) + { + return Brushes.Green; + } + + if (modifierType == ModifierEffectType.Negative) + { + return Brushes.Red; + } + } + else + { + if (modifierType == ModifierEffectType.Positive) + { + return Brushes.Red; + } + + if (modifierType == ModifierEffectType.Negative) + { + return Brushes.Green; + } + } + + return Brushes.Black; + } + + public bool TryGetLocalizationName(string modifierKey, [NotNullWhen(true)] out string? value) + { + if (_localizationService.TryGetValueInAll(modifierKey, out value)) + { + return true; + } + + if (_localizationService.TryGetValue($"MODIFIER_{modifierKey}", out value)) + { + return true; + } + + if (_localizationService.TryGetValue($"MODIFIER_NAVAL_{modifierKey}", out value)) + { + return true; + } + + if (_localizationService.TryGetValue($"MODIFIER_UNIT_LEADER_{modifierKey}", out value)) + { + return true; + } + + if (_localizationService.TryGetValue($"MODIFIER_ARMY_LEADER_{modifierKey}", out value)) + { + return true; + } + + return false; + } + + public string GetLocalizationName(string modifierKey) + { + if (TryGetLocalizationName(modifierKey, out var value)) + { + return value; + } + + return modifierKey; + } + + public bool TryGetLocalizationFormat(string modifier, [NotNullWhen(true)] out string? result) + { + if (_localizationService.TryGetValue($"{modifier}_tt", out result)) + { + return true; + } + + return _localizationService.TryGetValue(modifier, out result); + } + + private static ModifierEffectType GetModifierType(string modifierName, string modifierFormat) + { + // TODO: 重新支持从数据库中定义修饰符 + // if (_modifierTypes.TryGetValue(modifierName, out var modifierType)) + // { + // return modifierType; + // } + + for (var index = modifierFormat.Length - 1; index >= 0; index--) + { + var c = modifierFormat[index]; + switch (c) + { + case '+': + return ModifierEffectType.Positive; + case '-': + return ModifierEffectType.Negative; + } + } + + return ModifierEffectType.Unknown; + } + + /// + /// 获取 Modifier 数值的显示值 + /// + /// 包含关键字和对应值的修饰符对象 + /// 修饰符对应的格式化设置文本, 为空时使用百分比格式 + /// 应用modifierDisplayFormat格式的LeafModifier.Value的的显示值 + public string GetDisplayValue(LeafModifier leafModifier, string modifierDisplayFormat) + { + if (leafModifier.ValueType is GameValueType.Int or GameValueType.Float) + { + var value = double.Parse(leafModifier.Value); + var sign = leafModifier.Value.StartsWith('-') ? string.Empty : "+"; + + var displayDigits = GetDisplayDigits(modifierDisplayFormat); + var isPercentage = + string.IsNullOrEmpty(modifierDisplayFormat) || modifierDisplayFormat.Contains('%'); + var format = isPercentage ? 'P' : 'F'; + + return $"{sign}{value.ToString($"{format}{displayDigits}")}"; + } + + return leafModifier.Value; + } + + private static char GetDisplayDigits(string modifierDescription) + { + var displayDigits = '1'; + for (var i = modifierDescription.Length - 1; i >= 0; i--) + { + var c = modifierDescription[i]; + if (char.IsDigit(c)) + { + displayDigits = c; + break; + } + } + + return displayDigits; + } +} diff --git a/Moder.Core/Services/GameResources/OreService.cs b/Moder.Core/Services/GameResources/OreService.cs deleted file mode 100644 index 57d0cb9..0000000 --- a/Moder.Core/Services/GameResources/OreService.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Moder.Core.Services.GameResources.Base; -using ParadoxPower.CSharpExtensions; -using ParadoxPower.Process; - -namespace Moder.Core.Services.GameResources; - -/// -/// 游戏内定义的资源 -/// -/// -/// Resource 现在指代的是游戏资源, 游戏内的只好用 Ore 来代替了. -/// -/// 单例模式 -public sealed class OreService : CommonResourcesService -{ - private const string ResourcesKeyword = "resources"; - private Dictionary.ValueCollection Ores => Resources.Values; - - public OreService() - : base(Path.Combine(Keywords.Common, ResourcesKeyword), WatcherFilter.Text) { } - - public bool Contains(string resource) - { - foreach (var ore in Ores) - { - if (Array.Exists(ore, x => StringComparer.OrdinalIgnoreCase.Equals(x, resource))) - { - return true; - } - } - - return false; - } - - protected override string[] ParseFileToContent(Node rootNode) - { - // 一般来说, 每个资源文件只会有一个 resources 节点 - var ores = new List(1); - - if (rootNode.TryGetNode(ResourcesKeyword, out var resourcesNode)) - { - foreach (var resource in resourcesNode.Nodes) - { - ores.Add(resource.Key); - } - } - else - { - Log.Warn("未找到 resources 节点"); - } - return ores.ToArray(); - } -} diff --git a/Moder.Core/Services/GameResources/SpriteService.cs b/Moder.Core/Services/GameResources/SpriteService.cs deleted file mode 100644 index c3e1ade..0000000 --- a/Moder.Core/Services/GameResources/SpriteService.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System.Collections.Frozen; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices.WindowsRuntime; -using MethodTimer; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Media.Imaging; -using Moder.Core.Extensions; -using Moder.Core.Models; -using Moder.Core.Services.GameResources.Base; -using ParadoxPower.Process; -using Pfim; - -namespace Moder.Core.Services.GameResources; - -public sealed class SpriteService - : CommonResourcesService> -{ - private readonly GameResourcesPathService _resourcesPathService; - - [Time("加载界面图片")] - public SpriteService(GameResourcesPathService resourcesPathService) - : base("interface", WatcherFilter.GfxFiles) - { - _resourcesPathService = resourcesPathService; - } - - private Dictionary>.ValueCollection Sprites => - Resources.Values; - - public bool TryGetSpriteInfo(string spriteTypeName, [NotNullWhen(true)] out SpriteInfo? info) - { - foreach (var sprite in Sprites) - { - if (sprite.TryGetValue(spriteTypeName, out info)) - { - return true; - } - } - - info = null; - return false; - } - - public bool TryGetImageSource(string spriteTypeName, [NotNullWhen(true)] out ImageSource? imageSource) - { - imageSource = null; - if (!TryGetSpriteInfo(spriteTypeName, out var info)) - { - return false; - } - - try - { - using var image = Pfimage.FromFile( - _resourcesPathService.GetFilePathPriorModByRelativePath(info.Path) - ); - //BUG: 部分图片无法正常显示 - var source = new WriteableBitmap(image.Width, image.Height); - using var stream = source.PixelBuffer.AsStream(); - stream.Write(image.Data); - stream.Flush(); - - imageSource = source; - return true; - } - catch (Exception e) - { - Log.Warn(e, "读取图片失败, name:{Name}, path:{Path}", info.Name, info.Path); - return false; - } - } - - protected override FrozenDictionary? ParseFileToContent(Node rootNode) - { - var sprites = new Dictionary(16); - - foreach (var child in rootNode.AllArray) - { - if (!child.IsNodeWithKey("spriteTypes", out var spriteTypes)) - { - continue; - } - - foreach ( - var spriteType in spriteTypes.Nodes.Where(node => - StringComparer.OrdinalIgnoreCase.Equals("spriteType", node.Key) - ) - ) - { - ParseSpriteTypeNodeToDictionary(spriteType, sprites); - } - } - - return sprites.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); - } - - private static void ParseSpriteTypeNodeToDictionary( - Node spriteTypeNode, - Dictionary sprites - ) - { - string? spriteTypeName = null; - string? textureFilePath = null; - foreach (var leaf in spriteTypeNode.Leaves) - { - if (StringComparer.OrdinalIgnoreCase.Equals("name", leaf.Key)) - { - spriteTypeName = leaf.ValueText; - } - else if (StringComparer.OrdinalIgnoreCase.Equals("texturefile", leaf.Key)) - { - textureFilePath = leaf.ValueText; - } - } - - if (spriteTypeName is null || textureFilePath is null) - { - return; - } - - sprites[spriteTypeName] = new SpriteInfo(spriteTypeName, textureFilePath); - } -} diff --git a/Moder.Core/Services/GameResources/StateCategoryService.cs b/Moder.Core/Services/GameResources/StateCategoryService.cs deleted file mode 100644 index aafa47f..0000000 --- a/Moder.Core/Services/GameResources/StateCategoryService.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Collections.Frozen; -using System.Diagnostics.CodeAnalysis; -using Moder.Core.Models; -using Moder.Core.Services.GameResources.Base; -using ParadoxPower.CSharpExtensions; -using ParadoxPower.Process; - -namespace Moder.Core.Services.GameResources; - -public sealed class StateCategoryService - : CommonResourcesService> -{ - public IReadOnlyList StateCategories => _lazyStateCategories.Value; - private Lazy> _lazyStateCategories; - - private Dictionary< - string, - FrozenDictionary - >.ValueCollection StateCategoriesResource => Resources.Values; - - public StateCategoryService() - : base(Path.Combine(Keywords.Common, "state_category"), WatcherFilter.Text) - { - _lazyStateCategories = GetStateCategoriesLazy(); - OnResourceChanged += (_, _) => - { - _lazyStateCategories = GetStateCategoriesLazy(); - }; - } - - private Lazy> GetStateCategoriesLazy() - { - return new Lazy>(() => - { - var sortedArray = StateCategoriesResource.SelectMany(item => item.Values).ToArray(); - Array.Sort(sortedArray, (x, y) => x.LocalBuildingSlots < y.LocalBuildingSlots ? -1 : 1); - return sortedArray; - }); - } - - public bool ContainsCategory(string categoryName) - { - foreach (var stateCategory in StateCategoriesResource) - { - if (stateCategory.ContainsKey(categoryName)) - { - return true; - } - } - - return false; - } - - public bool TryGetValue(string categoryName, [NotNullWhen(true)] out StateCategory? category) - { - foreach (var stateCategory in StateCategoriesResource) - { - if (stateCategory.TryGetValue(categoryName, out category)) - { - return true; - } - } - - category = null; - return false; - } - - protected override FrozenDictionary? ParseFileToContent(Node rootNode) - { - var stateCategories = new Dictionary(8); - if (!rootNode.TryGetNode("state_categories", out var stateCategoriesNode)) - { - Log.Warn("文件: {FileName} 中未找到 state_categories 节点", rootNode.Position.FileName); - return null; - } - - foreach (var typeNode in stateCategoriesNode.Nodes) - { - byte? localBuildingSlots = null; - var typeName = typeNode.Key; - - if (typeNode.TryGetLeaf("local_building_slots", out var localBuildingSlotsLeaf)) - { - if (byte.TryParse(localBuildingSlotsLeaf.ValueText, out var value)) - { - localBuildingSlots = value; - } - else - { - Log.Warn( - "文件: {FileName} 中 local_building_slots 解析失败, 文本: {Text}", - typeNode.Position.FileName, - localBuildingSlotsLeaf.ValueText - ); - } - } - else - { - Log.Warn("文件: {FileName} 中未找到 local_building_slots", typeNode.Position.FileName); - } - - stateCategories.Add(typeName, new StateCategory(typeName, localBuildingSlots)); - } - - return stateCategories.ToFrozenDictionary(); - } -} diff --git a/Moder.Core/Services/GameResources/TerrainService.cs b/Moder.Core/Services/GameResources/TerrainService.cs index f4eabc3..3d53823 100644 --- a/Moder.Core/Services/GameResources/TerrainService.cs +++ b/Moder.Core/Services/GameResources/TerrainService.cs @@ -12,12 +12,26 @@ public sealed class TerrainService : CommonResourcesService>.ValueCollection Terrains => Resources.Values; + /// + /// 未在文件中定义的地形 + /// + private readonly FrozenSet _unitTerrain; + [Time("加载地形资源")] public TerrainService() - : base(Path.Combine(Keywords.Common, "terrain"), WatcherFilter.Text) { } + : base(Path.Combine(Keywords.Common, "terrain"), WatcherFilter.Text) + { + //TODO: 从数据库读取 + _unitTerrain = ["fort", "river"]; + } public bool Contains(string terrainName) { + if (_unitTerrain.Contains(terrainName)) + { + return true; + } + foreach (var terrain in Terrains) { if (terrain.Contains(terrainName)) diff --git a/Moder.Core/Services/GlobalResourceService.cs b/Moder.Core/Services/GlobalResourceService.cs deleted file mode 100644 index 1065bf8..0000000 --- a/Moder.Core/Services/GlobalResourceService.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Moder.Core.Models.Character; -using Moder.Core.ViewsModels.Menus; - -namespace Moder.Core.Services; - -public sealed class GlobalResourceService -{ - public CharacterSkillType CurrentSelectSelectSkillType - { - get => - _currentSelectSelectSkillType - ?? throw new InvalidOperationException("未设置 CurrentSelectSelectSkillType"); - set => _currentSelectSelectSkillType = value; - } - private CharacterSkillType? _currentSelectSelectSkillType; - - private SystemFileItem? _currentSelectFileItem; - - /// - /// 弹出 , 如果未设置则抛出异常 - /// - /// - /// 未设置CurrentSelectFileItem - public SystemFileItem PopCurrentSelectFileItem() - { - var item = _currentSelectFileItem ?? throw new InvalidOperationException("未设置CurrentSelectFileItem"); - _currentSelectFileItem = null; - return item; - } - - public void SetCurrentSelectFileItem(SystemFileItem fileItem) - { - _currentSelectFileItem = fileItem; - } -} diff --git a/Moder.Core/Services/LeafConverterService.cs b/Moder.Core/Services/LeafConverterService.cs deleted file mode 100644 index e962068..0000000 --- a/Moder.Core/Services/LeafConverterService.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Moder.Core.Helper; -using Moder.Core.Models; -using Moder.Core.Models.Vo; -using Moder.Core.Services.GameResources; -using Moder.Core.Services.ParserRules; - -namespace Moder.Core.Services; - -public sealed class LeafConverterService( - CountryTagConsumerService countryTagConsumer, - BuildingsService buildingsService -) -{ - private readonly CountryTagConsumerService _countryTagConsumer = countryTagConsumer; - private readonly BuildingsService _buildingsService = buildingsService; - - public LeafVo GetSpecificLeafVo(string key, string value, NodeVo parentNodeVo) - { - return GetSpecificLeafVo( - key, - value, - GameValueTypeConverterHelper.GetTypeForString(value), - parentNodeVo - ); - } - - public LeafVo GetSpecificLeafVo(string leafKey, string leafValue, GameValueType type, NodeVo parentNodeVo) - { - LeafVo leafVo; - - if (_countryTagConsumer.IsKeyword(leafKey)) - { - leafVo = new CountryTagLeafVo(leafKey, leafValue, type, parentNodeVo); - } - else if (leafKey.Equals("name", StringComparison.OrdinalIgnoreCase)) - { - leafVo = new StateNameLeafVo(leafKey, leafValue, type, parentNodeVo); - } - else if (parentNodeVo.Key.Equals("resources", StringComparison.OrdinalIgnoreCase)) - { - leafVo = new ResourcesLeafVo(leafKey, leafValue, type, parentNodeVo); - } - else if (leafKey.Equals("state_category", StringComparison.OrdinalIgnoreCase)) - { - leafVo = new StateCategoryLeafVo(leafKey, leafValue, type, parentNodeVo); - } - // 当父节点是 buildings 时, 子节点就可以为建筑物, 在这里, nodeVo 即为 leaf 的父节点 - else if ( - ( - parentNodeVo.Key.Equals("buildings", StringComparison.OrdinalIgnoreCase) - // province 中的建筑物 - || parentNodeVo.Parent?.Key.Equals("buildings", StringComparison.OrdinalIgnoreCase) == true - ) && _buildingsService.Contains(leafKey) - ) - { - leafVo = new BuildingLeafVo(leafKey, leafValue, type, parentNodeVo); - } - else - { - if (type == GameValueType.Int) - { - leafVo = new IntLeafVo(leafKey, leafValue, type, parentNodeVo); - } - else if (type == GameValueType.Float) - { - leafVo = new FloatLeafVo(leafKey, leafValue, type, parentNodeVo); - } - else - { - leafVo = new LeafVo(leafKey, leafValue, type, parentNodeVo); - } - } - - return leafVo; - } -} diff --git a/Moder.Core/Services/LocalisationKeyMappingConfig.cs b/Moder.Core/Services/LocalisationKeyMappingConfig.cs deleted file mode 100644 index 09d626c..0000000 --- a/Moder.Core/Services/LocalisationKeyMappingConfig.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Moder.Core.Services; - -public sealed class LocalisationKeyMappingConfig -{ - public string LocalisationKey { get; } - - /// - /// 当本地化文本中存在占位符时, 该占位符对应的Key - /// - /// 例如: 本地化文本为 trait_bonus_attack:"进攻:$VAL|+=0$" 占位符为 VAL - /// - /// - /// 大小写敏感 - /// - public string ValuePlaceholderKey { get; } - - public LocalisationKeyMappingConfig(string localisationKey, string valuePlaceholderKey = "") - { - LocalisationKey = localisationKey; - ValuePlaceholderKey = valuePlaceholderKey; - } - - public bool ExistsValuePlaceholder => ValuePlaceholderKey != string.Empty; -} \ No newline at end of file diff --git a/Moder.Core/Services/LocalisationKeyMappingService.cs b/Moder.Core/Services/LocalisationKeyMappingService.cs deleted file mode 100644 index fdc263b..0000000 --- a/Moder.Core/Services/LocalisationKeyMappingService.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using EnumsNET; -using Moder.Core.Models.Character; - -namespace Moder.Core.Services; - -/// -/// 用来解决脚本关键字与本地化文本中的键不一致的问题 -/// -public sealed class LocalisationKeyMappingService -{ - /// - /// 当调用方法查找Key对应的本地化文本时,如果字典内存在Key, 则使用Key对应的Value进行查询 - /// - private readonly Dictionary _localisationKeyMapping = - new(StringComparer.OrdinalIgnoreCase); - - public LocalisationKeyMappingService() - { - const string skillValuePlaceholderKey = "VAL"; - // 添加特性中技能的本地化映射 - // 6种技能类型, attack, defense, planning, logistics, maneuvering, coordination - foreach ( - var skillType in Enums - .GetNames() - .Where(name => !name.Equals("level", StringComparison.OrdinalIgnoreCase)) - ) - { - AddKeyMapping( - $"{skillType}_skill", - new LocalisationKeyMappingConfig($"trait_bonus_{skillType}", skillValuePlaceholderKey) - ); - - AddKeyMapping( - $"{skillType}_skill_factor", - // FACTOR 中是 Defence, 技能加成中就是 Defense, 不理解为什么要这样写 - new LocalisationKeyMappingConfig( - skillType == "Defense" ? "BOOST_DEFENCE_FACTOR" : $"boost_{skillType}_factor", - skillValuePlaceholderKey - ) - ); - } - - // 突破 - AddKeyMapping("breakthrough_factor", new LocalisationKeyMappingConfig("MODIFIER_BREAKTHROUGH")); - } - - /// - /// 添加映射 - /// - /// 原始键 - /// 配置 - private void AddKeyMapping(string key, LocalisationKeyMappingConfig config) - { - _localisationKeyMapping[key] = config; - } - - public bool TryGetValue(string key, [NotNullWhen(true)] out LocalisationKeyMappingConfig? value) - { - return _localisationKeyMapping.TryGetValue(key, out value); - } -} diff --git a/Moder.Core/Services/LocalizationFormatService.cs b/Moder.Core/Services/LocalizationFormatService.cs deleted file mode 100644 index ca38afc..0000000 --- a/Moder.Core/Services/LocalizationFormatService.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Microsoft.UI.Xaml.Documents; -using Microsoft.UI.Xaml.Media; -using Moder.Core.Parser; -using Moder.Core.Services.GameResources; - -namespace Moder.Core.Services; - -public class LocalizationFormatService -{ - private readonly LocalizationTextColorsService _localizationTextColorsService; - - public LocalizationFormatService(LocalizationTextColorsService localizationTextColorsService) - { - _localizationTextColorsService = localizationTextColorsService; - } - - public IEnumerable GetTextWithColor(string text) - { - var result = new List(4); - - if (LocalizationFormatParser.TryParse(text, out var formats)) - { - result.AddRange(formats.Select(GetTextRun)); - } - else - { - result.Add(new Run { Text = text }); - } - - return result; - } - - /// - /// 尝试获取 文本, 并使用 中指定的颜色, 如果颜色不存在, 则使用默认颜色 - /// - /// 文本格式信息 - /// - public Run GetTextRun(LocalizationFormat format) - { - Brush? foregroundBrush = null; - var run = new Run { Text = format.Text }; - - if (format.Type == LocalizationFormatType.TextWithColor) - { - if (format.Text.Length == 0) - { - return run; - } - if (_localizationTextColorsService.TryGetColor(format.Text[0], out var color)) - { - foregroundBrush = new SolidColorBrush(color.Color); - run.Text = format.Text[1..]; - } - } - - if (foregroundBrush is not null) - { - run.Foreground = foregroundBrush; - } - - return run; - } -} diff --git a/Moder.Core/Services/MessageBoxService.cs b/Moder.Core/Services/MessageBoxService.cs index 43d89dd..df5ad15 100644 --- a/Moder.Core/Services/MessageBoxService.cs +++ b/Moder.Core/Services/MessageBoxService.cs @@ -1,5 +1,6 @@ -using Microsoft.UI.Xaml.Controls; using Moder.Language.Strings; +using MsBox.Avalonia; +using MsBox.Avalonia.Enums; namespace Moder.Core.Services; @@ -7,25 +8,34 @@ public sealed class MessageBoxService { public async Task WarnAsync(string message) { - var dialog = new ContentDialog - { - XamlRoot = App.Current.XamlRoot, - Title = Resource.Common_Warning, - Content = message, - CloseButtonText = Resource.Common_Ok - }; + var dialog = MessageBoxManager.GetMessageBoxStandard( + Resource.Common_Warning, + message, + ButtonEnum.Ok, + Icon.Warning + ); await dialog.ShowAsync(); } public async Task ErrorAsync(string message) { - var dialog = new ContentDialog - { - XamlRoot = App.Current.XamlRoot, - Title = Resource.Common_Error, - Content = message, - CloseButtonText = Resource.Common_Ok - }; + var dialog = MessageBoxManager.GetMessageBoxStandard( + Resource.Common_Error, + message, + ButtonEnum.Ok, + Icon.Error + ); await dialog.ShowAsync(); } -} \ No newline at end of file + + public async Task InfoAsync(string message) + { + var dialog = MessageBoxManager.GetMessageBoxStandard( + Resource.Common_Tip, + message, + ButtonEnum.Ok, + Icon.Info + ); + await dialog.ShowAsync(); + } +} diff --git a/Moder.Core/Services/ModifierService.cs b/Moder.Core/Services/ModifierService.cs deleted file mode 100644 index 9e6617d..0000000 --- a/Moder.Core/Services/ModifierService.cs +++ /dev/null @@ -1,396 +0,0 @@ -using System.Collections.Frozen; -using MethodTimer; -using Microsoft.UI; -using Microsoft.UI.Xaml.Documents; -using Microsoft.UI.Xaml.Media; -using Moder.Core.Models; -using Moder.Core.Models.Modifiers; -using Moder.Core.Parser; -using Moder.Core.Services.GameResources; -using NLog; -using Windows.UI; - -namespace Moder.Core.Services; - -public sealed class ModifierService -{ - private readonly LocalisationService _localisationService; - private readonly TerrainService _terrainService; - private readonly LocalisationKeyMappingService _localisationKeyMappingService; - private readonly LocalizationFormatService _localisationFormatService; - - /// - /// 无法在本地化文件中判断类型的修饰符, 在文件中手动设置 - /// - private readonly FrozenDictionary _modifierTypes; - - private static readonly string[] UnitTerrain = ["fort", "river"]; - private static readonly Color Yellow = Color.FromArgb(255, 255, 189, 0); - private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - - public ModifierService( - LocalisationService localisationService, - TerrainService terrainService, - LocalisationKeyMappingService localisationKeyMappingService, - LocalizationFormatService localisationFormatService - ) - { - _localisationService = localisationService; - _terrainService = terrainService; - _localisationKeyMappingService = localisationKeyMappingService; - _localisationFormatService = localisationFormatService; - _modifierTypes = ReadModifierTypes(); - } - - [Time("读取文件中的修饰符类型")] - private static FrozenDictionary ReadModifierTypes() - { - var positiveFilePath = Path.Combine(App.ParserRulesFolder, "PositiveModifier.txt"); - var reversedFilePath = Path.Combine(App.ParserRulesFolder, "ReversedModifier.txt"); - var positives = File.Exists(positiveFilePath) ? File.ReadAllLines(positiveFilePath) : []; - var reversedModifiers = File.Exists(reversedFilePath) ? File.ReadAllLines(reversedFilePath) : []; - - var modifierTypes = new Dictionary( - positives.Length + reversedModifiers.Length - ); - foreach (var positive in positives) - { - if (!string.IsNullOrWhiteSpace(positive)) - { - modifierTypes.Add(positive.Trim(), ModifierEffectType.Positive); - } - } - foreach (var modifier in reversedModifiers) - { - if (!string.IsNullOrWhiteSpace(modifier)) - { - modifierTypes.Add(modifier.Trim(), ModifierEffectType.Negative); - } - } - - return modifierTypes.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); - } - - private Color GetModifierColor(LeafModifier leafModifier, string modifierFormat) - { - var value = double.Parse(leafModifier.Value); - if (value == 0.0) - { - return Yellow; - } - - var modifierType = GetModifierType(leafModifier.Key, modifierFormat); - if (modifierType == ModifierEffectType.Unknown) - { - return Colors.Black; - } - - if (value > 0.0) - { - if (modifierType == ModifierEffectType.Positive) - { - return Colors.Green; - } - - if (modifierType == ModifierEffectType.Negative) - { - return Colors.Red; - } - - return Colors.Black; - } - - if (value < 0.0) - { - if (modifierType == ModifierEffectType.Positive) - { - return Colors.Red; - } - - if (modifierType == ModifierEffectType.Negative) - { - return Colors.Green; - } - - return Colors.Black; - } - - return Colors.Black; - } - - private ModifierEffectType GetModifierType(string modifierName, string modifierFormat) - { - if (_modifierTypes.TryGetValue(modifierName, out var modifierType)) - { - return modifierType; - } - - for (var index = modifierFormat.Length - 1; index >= 0; index--) - { - var c = modifierFormat[index]; - if (c == '+') - { - return ModifierEffectType.Positive; - } - - if (c == '-') - { - return ModifierEffectType.Negative; - } - } - - return ModifierEffectType.Unknown; - } - - public IReadOnlyCollection GetModifierInlines(IEnumerable modifiers) - { - var inlines = new List(8); - - foreach (var modifier in modifiers) - { - IEnumerable addedInlines; - if (modifier.Type == ModifierType.Leaf) - { - var leafModifier = (LeafModifier)modifier; - if ( - StringComparer.OrdinalIgnoreCase.Equals( - leafModifier.Key, - LeafModifier.CustomEffectTooltipKey - ) - || - StringComparer.OrdinalIgnoreCase.Equals( - leafModifier.Key, - LeafModifier.CustomModifierTooltipKey - ) - ) - { - addedInlines = _localisationFormatService.GetTextWithColor( - _localisationService.GetValue(leafModifier.Value) - ); - } - else - { - addedInlines = GetModifierInlinesForLeaf(leafModifier); - } - } - else if (modifier.Type == ModifierType.Node) - { - var nodeModifier = (NodeModifier)modifier; - addedInlines = GetModifierInlinesForNode(nodeModifier); - } - else - { - continue; - } - - inlines.AddRange(addedInlines); - inlines.Add(new LineBreak()); - } - - if (inlines.Count != 0 && inlines[^1] is LineBreak) - { - inlines.RemoveAt(inlines.Count - 1); - } - return inlines; - } - - private List GetModifierInlinesForLeaf(LeafModifier modifier) - { - if (_localisationKeyMappingService.TryGetValue(modifier.Key, out var config)) - { - return GetModifierDisplayMessageForMapping(modifier, config); - } - - return GetModifierDisplayMessageUniversal(modifier); - } - - private List GetModifierDisplayMessageForMapping( - LeafModifier modifier, - LocalisationKeyMappingConfig config - ) - { - if (config.ExistsValuePlaceholder) - { - var inlineTexts = new List(4); - var localisationName = _localisationService.GetValue(config.LocalisationKey); - if (LocalizationFormatParser.TryParse(localisationName, out var result)) - { - ParseModifierFormatToInlineTexts(inlineTexts, result, config, modifier); - } - else - { - inlineTexts.Add(new Run { Text = localisationName }); - Log.Warn("无法解析本地化格式: {Format}", localisationName); - } - - return inlineTexts; - } - - return GetModifierDisplayMessageUniversal(modifier); - } - - private void ParseModifierFormatToInlineTexts( - List inlineTexts, - IEnumerable localizationFormats, - LocalisationKeyMappingConfig config, - LeafModifier modifier - ) - { - foreach (var localizationFormat in localizationFormats) - { - if (localizationFormat.Type == LocalizationFormatType.Placeholder) - { - if (localizationFormat.Text.Contains(config.ValuePlaceholderKey)) - { - var value = GetModifierDisplayValue(modifier, localizationFormat.Text); - inlineTexts.Add( - new Run - { - Text = value, - Foreground = new SolidColorBrush( - GetModifierColor(modifier, localizationFormat.Text) - ) - } - ); - } - else - { - // 如果是占位符且不包含 ValuePlaceholderKey, 则有可能是其他本地化值的键 - inlineTexts.Add( - new Run { Text = _localisationService.GetValue(localizationFormat.Text) } - ); - } - } - else if (localizationFormat.Type == LocalizationFormatType.Text) - { - inlineTexts.Add(new Run { Text = localizationFormat.Text }); - } - else if (localizationFormat.Type == LocalizationFormatType.TextWithColor) - { - inlineTexts.Add(_localisationFormatService.GetTextRun(localizationFormat)); - } - } - } - - private List GetModifierDisplayMessageUniversal(LeafModifier modifier) - { - var inlines = new List(4); - var modifierName = _localisationService.GetModifier(modifier.Key); - inlines.AddRange(_localisationFormatService.GetTextWithColor(modifierName)); - - if (modifier.ValueType is GameValueType.Int or GameValueType.Float) - { - var modifierFormat = _localisationService.TryGetModifierTt(modifier.Key, out var result) - ? result - : string.Empty; - var value = GetModifierDisplayValue(modifier, modifierFormat); - inlines.Add( - new Run - { - Text = value, - Foreground = new SolidColorBrush(GetModifierColor(modifier, modifierFormat)) - } - ); - } - else - { - inlines.Add(new Run { Text = modifier.Value }); - } - return inlines; - } - - private List GetModifierInlinesForNode(NodeModifier nodeModifier) - { - if ( - _terrainService.Contains(nodeModifier.Key) - || Array.Exists(UnitTerrain, element => element == nodeModifier.Key) - ) - { - return GetTerrainModifierInlines(nodeModifier); - } - - Log.Warn("未知的节点修饰符: {Name}", nodeModifier.Key); - var inlines = new List(nodeModifier.Modifiers.Count * 3); - foreach (var leafModifier in nodeModifier.Modifiers) - { - inlines.AddRange(GetModifierInlinesForLeaf(leafModifier)); - } - return inlines; - } - - /// - /// 获取地形修饰符的描述 - /// - /// - /// - private List GetTerrainModifierInlines(NodeModifier nodeModifier) - { - var inlines = new List(4); - var terrainName = _localisationService.GetValue(nodeModifier.Key); - inlines.Add(new Run { Text = $"{terrainName}: " }); - inlines.Add(new LineBreak()); - - for (var index = 0; index < nodeModifier.Modifiers.Count; index++) - { - var modifier = nodeModifier.Modifiers[index]; - // TODO: 转为 LocalisationKeyMappingService - var modifierDescription = _localisationService.GetValue($"STAT_ADJUSTER_{modifier.Key}_DIFF"); - var modifierName = _localisationService.GetValue($"STAT_ADJUSTER_{modifier.Key}"); - var color = GetModifierColor(modifier, modifierDescription); - inlines.Add( - new Run - { - Text = $" {modifierName}{GetModifierDisplayValue(modifier, modifierDescription)}", - Foreground = new SolidColorBrush(color) - } - ); - - if (index != nodeModifier.Modifiers.Count - 1) - { - inlines.Add(new LineBreak()); - } - } - - return inlines; - } - - /// - /// 获取 Modifier 数值的显示值 - /// - /// - /// - /// - private static string GetModifierDisplayValue(LeafModifier leafModifier, string modifierDisplayFormat) - { - if (leafModifier.ValueType is GameValueType.Int or GameValueType.Float) - { - var value = double.Parse(leafModifier.Value); - var sign = leafModifier.Value.StartsWith('-') ? string.Empty : "+"; - - var displayDigits = GetModifierDisplayDigits(modifierDisplayFormat); - var isPercentage = - string.IsNullOrEmpty(modifierDisplayFormat) || modifierDisplayFormat.Contains('%'); - var format = isPercentage ? 'P' : 'F'; - - return $"{sign}{value.ToString($"{format}{displayDigits}")}"; - } - - return leafModifier.Value; - } - - private static char GetModifierDisplayDigits(string modifierDescription) - { - var displayDigits = '1'; - for (var i = modifierDescription.Length - 1; i >= 0; i--) - { - var c = modifierDescription[i]; - if (char.IsDigit(c)) - { - displayDigits = c; - break; - } - } - - return displayDigits; - } -} diff --git a/Moder.Core/Services/ParserRules/CountryTagConsumerService.cs b/Moder.Core/Services/ParserRules/CountryTagConsumerService.cs deleted file mode 100644 index 14a80bd..0000000 --- a/Moder.Core/Services/ParserRules/CountryTagConsumerService.cs +++ /dev/null @@ -1,38 +0,0 @@ -using MethodTimer; - -namespace Moder.Core.Services.ParserRules; - -public class CountryTagConsumerService -{ - private readonly string[] _keywords; - private const string FileName = "CountryTag.txt"; - - public CountryTagConsumerService() - { - var configFilePath = Path.Combine(App.ParserRulesFolder, FileName); - if (!File.Exists(configFilePath)) - { - _keywords = []; - return; - } - - _keywords = ReadKeywordsInFile(configFilePath); - } - - [Time("加载解析规则 CountryTag.txt")] - private static string[] ReadKeywordsInFile(string configFilePath) - { - var lines = File.ReadAllLines(configFilePath); - return lines.Select(keyword => keyword.Trim()).ToArray(); - } - - /// - /// 检查 keyword 是否是使用 Country Tag 的关键字 - /// - /// - /// - public bool IsKeyword(string keyword) - { - return Array.FindIndex(_keywords, k => k.Equals(keyword, StringComparison.OrdinalIgnoreCase)) != -1; - } -} diff --git a/Moder.Core/Services/TabViewNavigationService.cs b/Moder.Core/Services/TabViewNavigationService.cs new file mode 100644 index 0000000..0f200c9 --- /dev/null +++ b/Moder.Core/Services/TabViewNavigationService.cs @@ -0,0 +1,67 @@ +using System.Collections.ObjectModel; +using Avalonia.Controls; +using FluentAvalonia.UI.Controls; +using Microsoft.Extensions.DependencyInjection; +using Moder.Core.Infrastructure; +using NLog; + +namespace Moder.Core.Services; + +public sealed class TabViewNavigationService +{ + private TabView TabView => + _tabView ?? throw new InvalidOperationException("TabViewNavigationService 未初始化"); + private TabView? _tabView; + private readonly ObservableCollection _openedTabFileItems = []; + + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + public void Initialize(TabView tabView) + { + _tabView = tabView; + tabView.TabItems = _openedTabFileItems; + } + + public void AddTab(ITabViewItem content) + { + var openedTabFileItem = _openedTabFileItems.FirstOrDefault(item => + { + var tabViewItem = item.Content as ITabViewItem; + return tabViewItem?.Equals(content) == true; + }); + + AddTabCore(openedTabFileItem, () => content); + } + + public void AddSingleTabFromIoc() + where TType : class, ITabViewItem + { + var openedTabFileItem = _openedTabFileItems.FirstOrDefault(item => item.Content is TType); + + AddTabCore(openedTabFileItem, () => App.Services.GetRequiredService()); + } + + private void AddTabCore(TabViewItem? tabViewItem, Func action) + { + if (tabViewItem is null) + { + var content = action(); + tabViewItem = new TabViewItem { Header = content.Header, Content = content }; + ToolTip.SetTip(tabViewItem, content.ToolTip); + + _openedTabFileItems.Add(tabViewItem); + } + + TabView.SelectedItem = tabViewItem; + } + + public bool RemoveTab(TabViewItem content) + { + if (content.Content is IClosed closed) + { + closed.Close(); + Log.Debug("释放 {Content}", content.Content.GetType().Name); + } + return _openedTabFileItems.Remove(content); + } +} diff --git a/Moder.Core/Themes/AppDarkTheme.xaml b/Moder.Core/Themes/AppDarkTheme.xaml deleted file mode 100644 index f4e5d34..0000000 --- a/Moder.Core/Themes/AppDarkTheme.xaml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Moder.Core/Themes/AppLightTheme.xaml b/Moder.Core/Themes/AppLightTheme.xaml deleted file mode 100644 index 86b325c..0000000 --- a/Moder.Core/Themes/AppLightTheme.xaml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Moder.Core/Themes/Generic.xaml b/Moder.Core/Themes/Generic.xaml deleted file mode 100644 index b33cc62..0000000 --- a/Moder.Core/Themes/Generic.xaml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/Moder.Core/Views/Game/CharacterEditorControlView.axaml b/Moder.Core/Views/Game/CharacterEditorControlView.axaml new file mode 100644 index 0000000..a55425d --- /dev/null +++ b/Moder.Core/Views/Game/CharacterEditorControlView.axaml @@ -0,0 +1,164 @@ + + + + + + + + + + + +