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/Controls.axaml b/Moder.Core/Controls/Controls.axaml
new file mode 100644
index 0000000..3647941
--- /dev/null
+++ b/Moder.Core/Controls/Controls.axaml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/Moder.Core/Controls/DirectorySelector.axaml b/Moder.Core/Controls/DirectorySelector.axaml
new file mode 100644
index 0000000..604eb84
--- /dev/null
+++ b/Moder.Core/Controls/DirectorySelector.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Moder.Core/Views/Game/CharacterEditorControlView.axaml.cs b/Moder.Core/Views/Game/CharacterEditorControlView.axaml.cs
new file mode 100644
index 0000000..9501359
--- /dev/null
+++ b/Moder.Core/Views/Game/CharacterEditorControlView.axaml.cs
@@ -0,0 +1,127 @@
+using Avalonia.Controls;
+using Avalonia.Media;
+using AvaloniaEdit.TextMate;
+using Microsoft.Extensions.DependencyInjection;
+using Moder.Core.Editor;
+using Moder.Core.Infrastructure;
+using Moder.Core.ViewsModel.Game;
+using Moder.Language.Strings;
+
+namespace Moder.Core.Views.Game;
+
+public sealed partial class CharacterEditorControlView : UserControl, ITabViewItem, IClosed
+{
+ public string Header => Resource.Menu_CharacterEditor;
+ public string Id => nameof(CharacterEditorControlView);
+ public string ToolTip => Header;
+
+ private CharacterEditorControlViewModel ViewModel { get; }
+
+ public CharacterEditorControlView()
+ {
+ InitializeComponent();
+ ViewModel = App.Services.GetRequiredService();
+ DataContext = ViewModel;
+
+ InitializeTextEditor();
+ ViewModel.PropertyChanged += (_, args) =>
+ {
+ if (args.PropertyName == nameof(ViewModel.GeneratedText))
+ {
+ Editor.Text = ViewModel.GeneratedText;
+ }
+ };
+ }
+
+ // TODO: 状态栏
+ private void InitializeTextEditor()
+ {
+ var options = new ParadoxRegistryOptions(App.Current.ActualThemeVariant);
+ Editor.Options.HighlightCurrentLine = true;
+ var installation = Editor.InstallTextMate(options);
+
+ installation.SetGrammar("source.hoi4");
+
+ ApplyThemeColorsToEditor(installation);
+ }
+
+ private void ApplyThemeColorsToEditor(TextMate.Installation installation)
+ {
+ ApplyBrushAction(installation, "editor.background", brush => Editor.Background = brush);
+ ApplyBrushAction(installation, "editor.foreground", brush => Editor.Foreground = brush);
+
+ if (
+ !ApplyBrushAction(
+ installation,
+ "editor.selectionBackground",
+ brush => Editor.TextArea.SelectionBrush = brush
+ )
+ )
+ {
+ if (App.Current.TryGetResource("TextAreaSelectionBrush", out var resourceObject))
+ {
+ if (resourceObject is IBrush brush)
+ {
+ Editor.TextArea.SelectionBrush = brush;
+ }
+ }
+ }
+
+ if (
+ !ApplyBrushAction(
+ installation,
+ "editor.lineHighlightBackground",
+ brush =>
+ {
+ Editor.TextArea.TextView.CurrentLineBackground = brush;
+ Editor.TextArea.TextView.CurrentLineBorder = new Pen(brush);
+ }
+ )
+ )
+ {
+ Editor.TextArea.TextView.SetDefaultHighlightLineColors();
+ }
+
+ if (
+ !ApplyBrushAction(
+ installation,
+ "editorLineNumber.foreground",
+ brush => Editor.LineNumbersForeground = brush
+ )
+ )
+ {
+ Editor.LineNumbersForeground = Editor.Foreground;
+ }
+ }
+
+ private static bool ApplyBrushAction(
+ TextMate.Installation e,
+ string colorKeyNameFromJson,
+ Action applyColorAction
+ )
+ {
+ if (!e.TryGetThemeColor(colorKeyNameFromJson, out var colorString))
+ {
+ return false;
+ }
+
+ if (!Color.TryParse(colorString, out var color))
+ {
+ return false;
+ }
+
+ var colorBrush = new SolidColorBrush(color);
+ applyColorAction(colorBrush);
+ return true;
+ }
+
+ private void TextMateInstallationOnAppliedTheme(object sender, TextMate.Installation e)
+ {
+ ApplyThemeColorsToEditor(e);
+ }
+
+ public void Close()
+ {
+ ViewModel.Close();
+ }
+}
diff --git a/Moder.Core/Views/Game/CharacterEditorControlView.xaml b/Moder.Core/Views/Game/CharacterEditorControlView.xaml
deleted file mode 100644
index e7704c4..0000000
--- a/Moder.Core/Views/Game/CharacterEditorControlView.xaml
+++ /dev/null
@@ -1,191 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Moder.Core/Views/Game/CharacterEditorControlView.xaml.cs b/Moder.Core/Views/Game/CharacterEditorControlView.xaml.cs
deleted file mode 100644
index 78b18a4..0000000
--- a/Moder.Core/Views/Game/CharacterEditorControlView.xaml.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using CommunityToolkit.Mvvm.Messaging;
-using Microsoft.Extensions.DependencyInjection;
-using Moder.Core.Messages;
-using Moder.Core.ViewsModels.Game;
-
-namespace Moder.Core.Views.Game;
-
-// 不能使用 Ioc 容器管理, 因为需要使用 IDisposable 接口释放资源
-public sealed partial class CharacterEditorControlView : IDisposable
-{
- public CharacterEditorControlViewModel ViewModel { get; }
-
- public CharacterEditorControlView()
- {
- InitializeComponent();
-
- ViewModel = App.Current.Services.GetRequiredService();
- ViewModel.LevelModifierDescription = LevelModifierDescriptionTextBlock.Inlines;
- ViewModel.AttackModifierDescription = AttackModifierDescriptionTextBlock.Inlines;
- ViewModel.DefenseModifierDescription = DefenseModifierDescriptionTextBlock.Inlines;
- ViewModel.PlanningModifierDescription = PlanningModifierDescriptionTextBlock.Inlines;
- ViewModel.LogisticsModifierDescription = LogisticsModifierDescriptionTextBlock.Inlines;
- ViewModel.ManeuveringModifierDescription = ManeuveringModifierDescriptionTextBlock.Inlines;
- ViewModel.CoordinationModifierDescription = CoordinationModifierDescriptionTextBlock.Inlines;
-
- ViewModel.InitializeSkillDefaultValue();
-
- WeakReferenceMessenger.Default.Register(this, OnLanguageChanged);
- }
-
- private void OnLanguageChanged(object recipient, AppLanguageChangedMessage message)
- {
- Bindings.Update();
- }
-
- public void Dispose()
- {
- ReleaseResources();
- GC.SuppressFinalize(this);
- }
-
- ~CharacterEditorControlView()
- {
- ReleaseResources();
- }
-
- private void ReleaseResources()
- {
- ViewModel.Close();
- WeakReferenceMessenger.Default.UnregisterAll(this);
- }
-}
\ No newline at end of file
diff --git a/Moder.Core/Views/Game/StateFileControlView.xaml b/Moder.Core/Views/Game/StateFileControlView.xaml
deleted file mode 100644
index 85c6a5f..0000000
--- a/Moder.Core/Views/Game/StateFileControlView.xaml
+++ /dev/null
@@ -1,349 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Moder.Core/Views/Game/StateFileControlView.xaml.cs b/Moder.Core/Views/Game/StateFileControlView.xaml.cs
deleted file mode 100644
index 80d7fe0..0000000
--- a/Moder.Core/Views/Game/StateFileControlView.xaml.cs
+++ /dev/null
@@ -1,145 +0,0 @@
-using System.Collections;
-using System.Diagnostics;
-using CommunityToolkit.WinUI;
-using Microsoft.UI.Xaml;
-using Microsoft.UI.Xaml.Controls;
-using Microsoft.UI.Xaml.Media;
-using Moder.Core.Controls;
-using Moder.Core.Models;
-using Moder.Core.Models.Vo;
-using Moder.Core.Services.GameResources;
-using Moder.Core.ViewsModels.Game;
-using Vanara.PInvoke;
-using Windows.Foundation;
-using WinUIEx;
-
-namespace Moder.Core.Views.Game;
-
-public sealed partial class StateFileControlView : IFileView
-{
- public string Title => ViewModel.Title;
- public string FullPath => ViewModel.FullPath;
- public StateFileControlViewModel ViewModel => (StateFileControlViewModel)DataContext;
-
- private readonly CountryTagService _countryTagService;
- private readonly DispatcherTimer _timer;
-
- public StateFileControlView(StateFileControlViewModel model, CountryTagService countryTagService)
- {
- _countryTagService = countryTagService;
- _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) };
- _timer.Tick += (_, _) =>
- {
- User32.GetCursorPos(out var point);
- User32.ScreenToClient(App.Current.MainWindow.GetWindowHandle(), ref point);
-
- var resolutionScale = MainTreeView.XamlRoot.RasterizationScale;
- var elements = VisualTreeHelper.FindElementsInHostCoordinates(
- new Point(point.X / resolutionScale, point.Y / resolutionScale),
- MainTreeView
- );
- if (
- elements.Any(element =>
- {
- if (element is BaseLeaf)
- {
- return true;
- }
-
- if (element is TreeViewItem { Content: LeafValuesVo or LeafVo })
- {
- return true;
- }
-
- return false;
- })
- )
- {
- MainTreeView.CanReorderItems = false;
- }
- else
- {
- MainTreeView.CanReorderItems = true;
- }
- };
-
- InitializeComponent();
- DataContext = model;
- }
-
- private async void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- // 确保 ItemsSource 不为空, 避免抛出异常
- if (sender is ListView { ItemsSource: ICollection { Count: > 0 } } listView)
- {
- await listView.SmoothScrollIntoViewWithIndexAsync(
- listView.SelectedIndex,
- ScrollItemPlacement.Center,
- false,
- true
- );
- }
- }
-
- private void AutoSuggestBox_OnSuggestionChosen(
- AutoSuggestBox sender,
- AutoSuggestBoxSuggestionChosenEventArgs args
- )
- {
- Debug.Assert(args.SelectedItem is StateCategory);
-
- var stateCategory = (StateCategory)args.SelectedItem;
- sender.Text = stateCategory.TypeName;
- }
-
- private void CountryTagAutoSuggestBox_OnTextChanged(
- AutoSuggestBox sender,
- AutoSuggestBoxTextChangedEventArgs args
- )
- {
- if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
- {
- var suggestions = SearchCountryTag(sender.Text);
-
- sender.ItemsSource = suggestions.Length > 0 ? suggestions : ["未找到"];
- }
- }
-
- private string[] SearchCountryTag(string query)
- {
- var countryTags = _countryTagService.CountryTags;
- if (string.IsNullOrWhiteSpace(query))
- {
- return countryTags.ToArray();
- }
-
- var suggestions = new List(16);
-
- foreach (var countryTag in countryTags)
- {
- if (countryTag.Contains(query, StringComparison.OrdinalIgnoreCase))
- {
- suggestions.Add(countryTag);
- }
- }
-
- return suggestions
- .OrderByDescending(countryTag =>
- countryTag.StartsWith(query, StringComparison.CurrentCultureIgnoreCase)
- )
- .ToArray();
- }
-
- private void TreeView_OnDragItemsCompleted(TreeView sender, TreeViewDragItemsCompletedEventArgs args)
- {
- _timer.Stop();
- // _logger.LogDebug("DragItemsCompleted");
- }
-
- private void TreeView_OnDragItemsStarting(TreeView sender, TreeViewDragItemsStartingEventArgs args)
- {
- _timer.Start();
-
- // TODO: 不能加入到 Leaf, LeafValue 中 | 加入一个 Node, 离开一个 Node, 调整位置
- }
-}
diff --git a/Moder.Core/Views/Game/StateFileDataTemplateSelector.cs b/Moder.Core/Views/Game/StateFileDataTemplateSelector.cs
deleted file mode 100644
index 2aca0c1..0000000
--- a/Moder.Core/Views/Game/StateFileDataTemplateSelector.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using System.Diagnostics;
-using Microsoft.UI.Xaml;
-using Microsoft.UI.Xaml.Controls;
-using Moder.Core.Models.Vo;
-
-namespace Moder.Core.Views.Game;
-
-public partial class StateFileDataTemplateSelector : DataTemplateSelector
-{
- // 每个 DataTemplate 都需要在 XAML 中声明
- public DataTemplate Node { get; set; } = null!;
- public DataTemplate Leaf { get; set; } = null!;
- public DataTemplate IntLeaf { get; set; } = null!;
- public DataTemplate FloatLeaf { get; set; } = null!;
- public DataTemplate LeafValues { get; set; } = null!;
- public DataTemplate Comment { get; set; } = null!;
- public DataTemplate StateCategoryLeaf { get; set; } = null!;
- public DataTemplate BuildingLeaf { get; set; } = null!;
- public DataTemplate NameLeaf { get; set; } = null!;
- public DataTemplate CountryTagLeaf { get; set; } = null!;
- public DataTemplate ResourcesLeaf { get; set; } = null!;
-
- protected override DataTemplate SelectTemplateCore(object item)
- {
- AssertTemplatesIsNotNull();
-
- return item switch
- {
- StateCategoryLeafVo => StateCategoryLeaf,
- StateNameLeafVo => NameLeaf,
- BuildingLeafVo => BuildingLeaf,
- CountryTagLeafVo => CountryTagLeaf,
- ResourcesLeafVo => ResourcesLeaf,
- IntLeafVo => IntLeaf,
- FloatLeafVo => FloatLeaf,
-
- NodeVo => Node,
- LeafVo => Leaf,
- LeafValuesVo => LeafValues,
- CommentVo => Comment,
- _ => throw new ArgumentException("未知对象", nameof(item))
- };
- }
-
- [Conditional("DEBUG")]
- private void AssertTemplatesIsNotNull()
- {
- foreach (
- var propertyInfo in typeof(StateFileDataTemplateSelector)
- .GetProperties()
- .Where(info => info.PropertyType == typeof(DataTemplate))
- )
- {
- var template = propertyInfo.GetValue(this) as DataTemplate;
- Debug.Assert(template is not null, propertyInfo.Name + " is null");
- }
- }
-}
diff --git a/Moder.Core/Views/Game/TraitSelectionWindowView.axaml b/Moder.Core/Views/Game/TraitSelectionWindowView.axaml
new file mode 100644
index 0000000..ea21396
--- /dev/null
+++ b/Moder.Core/Views/Game/TraitSelectionWindowView.axaml
@@ -0,0 +1,77 @@
+
+
+
+ M11.5,2.75 C16.3324916,2.75 20.25,6.66750844 20.25,11.5 C20.25,13.6461673 19.4773285,15.6118676 18.1949905,17.1340957 L25.0303301,23.9696699 C25.3232233,24.2625631 25.3232233,24.7374369 25.0303301,25.0303301 C24.7640635,25.2965966 24.3473998,25.3208027 24.0537883,25.1029482 L23.9696699,25.0303301 L17.1340957,18.1949905 C15.6118676,19.4773285 13.6461673,20.25 11.5,20.25 C6.66750844,20.25 2.75,16.3324916 2.75,11.5 C2.75,6.66750844 6.66750844,2.75 11.5,2.75 Z M11.5,4.25 C7.49593556,4.25 4.25,7.49593556 4.25,11.5 C4.25,15.5040644 7.49593556,18.75 11.5,18.75 C15.5040644,18.75 18.75,15.5040644 18.75,11.5 C18.75,7.49593556 15.5040644,4.25 11.5,4.25 Z
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Moder.Core/Views/Game/TraitSelectionWindowView.axaml.cs b/Moder.Core/Views/Game/TraitSelectionWindowView.axaml.cs
new file mode 100644
index 0000000..d47467a
--- /dev/null
+++ b/Moder.Core/Views/Game/TraitSelectionWindowView.axaml.cs
@@ -0,0 +1,129 @@
+using Avalonia.Controls;
+using Avalonia.Controls.Documents;
+using Avalonia.Input;
+using Avalonia.Media;
+using Microsoft.Extensions.DependencyInjection;
+using Moder.Core.Models.Vo;
+using Moder.Core.Services.GameResources.Modifiers;
+using Moder.Core.ViewsModel.Game;
+using Moder.Language.Strings;
+using NLog;
+
+namespace Moder.Core.Views.Game;
+
+public sealed partial class TraitSelectionWindowView : Window, IDisposable
+{
+ public IEnumerable SelectedTraits => _viewModel.SelectedTraits;
+
+ private readonly IBrush _pointerOverBrush;
+ private readonly TraitSelectionWindowViewModel _viewModel;
+
+ // private Flyout _modifierDescriptionFlyout;
+
+ // private readonly Timer _showModifierToolTipTimer;
+ private readonly ModifierDisplayService _modifierDisplayService;
+
+ private static readonly Logger Log = LogManager.GetCurrentClassLogger();
+
+ public TraitSelectionWindowView()
+ {
+ InitializeComponent();
+
+ _modifierDisplayService = App.Services.GetRequiredService();
+ // _modifierDescriptionFlyout = new Flyout { Placement = PlacementMode.Bottom };
+ _viewModel = App.Services.GetRequiredService();
+ DataContext = _viewModel;
+
+ // 因为在选择特质时, 用户无法改变主题, 所以可以直接缓存 Brush
+ if (
+ App.Current.Styles.TryGetResource(
+ "SystemControlHighlightListLowBrush",
+ ActualThemeVariant,
+ out var value
+ ) && value is IBrush brush
+ )
+ {
+ _pointerOverBrush = brush;
+ }
+ else
+ {
+ _pointerOverBrush = Brushes.WhiteSmoke;
+ }
+ }
+
+ public void SyncSelectedTraits(IEnumerable selectedTraits)
+ {
+ _viewModel.SyncSelectedTraits(selectedTraits);
+ }
+
+ private void Border_OnPointerEntered(object? sender, PointerEventArgs e)
+ {
+ var border = (Border?)sender;
+ if (border is null)
+ {
+ return;
+ }
+
+ if (border.Tag is not TraitVo traitVo)
+ {
+ Log.Error("无法从 Tag 中获取 TraitVo");
+ return;
+ }
+
+ border.Background = _pointerOverBrush;
+
+ if (ToolTip.GetTip(border) is not null)
+ {
+ return;
+ }
+
+ var toolTip = new TextBlock { Inlines = [], FontSize = 15 };
+ var inlines = _modifierDisplayService.GetDescription(traitVo.Trait.AllModifiers);
+ if (inlines.Count == 0)
+ {
+ toolTip.Inlines.Add(new Run { Text = Resource.CharacterEditor_None });
+ }
+ else
+ {
+ toolTip.Inlines.AddRange(inlines);
+ }
+ ToolTip.SetTip(border, toolTip);
+ }
+
+ private void Border_OnPointerExited(object? sender, PointerEventArgs e)
+ {
+ var border = (Border?)sender;
+ if (border is null)
+ {
+ return;
+ }
+
+ border.Background = Brushes.Transparent;
+ }
+
+ private void Border_OnPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ var border = (Border?)sender;
+ if (border?.Tag is not TraitVo traitVo)
+ {
+ Log.Error("无法从 Tag 中获取 TraitVo");
+ return;
+ }
+
+ if (traitVo.IsSelected)
+ {
+ traitVo.IsSelected = false;
+ _viewModel.UpdateModifiersDescriptionOnRemove(traitVo);
+ }
+ else
+ {
+ traitVo.IsSelected = true;
+ _viewModel.UpdateModifiersDescriptionOnAdd(traitVo);
+ }
+ }
+
+ public void Dispose()
+ {
+ _viewModel.Dispose();
+ }
+}
diff --git a/Moder.Core/Views/Game/TraitsSelectionWindowView.xaml b/Moder.Core/Views/Game/TraitsSelectionWindowView.xaml
deleted file mode 100644
index edaf69d..0000000
--- a/Moder.Core/Views/Game/TraitsSelectionWindowView.xaml
+++ /dev/null
@@ -1,89 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Moder.Core/Views/Game/TraitsSelectionWindowView.xaml.cs b/Moder.Core/Views/Game/TraitsSelectionWindowView.xaml.cs
deleted file mode 100644
index da43050..0000000
--- a/Moder.Core/Views/Game/TraitsSelectionWindowView.xaml.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using System.Timers;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.UI;
-using Microsoft.UI.Xaml;
-using Microsoft.UI.Xaml.Controls;
-using Microsoft.UI.Xaml.Controls.Primitives;
-using Microsoft.UI.Xaml.Input;
-using Microsoft.UI.Xaml.Media;
-using Moder.Core.Models.Vo;
-using Moder.Core.ViewsModels.Game;
-
-namespace Moder.Core.Views.Game;
-
-public sealed partial class TraitsSelectionWindowView : IDisposable
-{
- public TraitsSelectionWindowViewModel ViewModel { get; }
-
- private readonly Timer _showModifierToolTipTimer;
- private DependencyObject? _displayTarget;
- private TraitVo? _traitVo;
- private readonly FlyoutShowOptions _flyoutShowOptions = new() { ShowMode = FlyoutShowMode.Transient };
-
- private readonly SolidColorBrush _whiteSmokeBrush = new(Colors.WhiteSmoke);
- private readonly SolidColorBrush _transparentBrush = new(Colors.Transparent);
-
- public TraitsSelectionWindowView()
- {
- ViewModel = App.Current.Services.GetRequiredService();
- InitializeComponent();
-
- ViewModel.TraitsModifierDescription = TraitsModifierDescriptionTextBlock.Inlines;
- _showModifierToolTipTimer = new Timer(TimeSpan.FromMilliseconds(300)) { AutoReset = false };
-
- _showModifierToolTipTimer.Elapsed += (_, _) =>
- {
- if (_displayTarget is null || _traitVo is null)
- {
- return;
- }
-
- App.Current.DispatcherQueue.TryEnqueue(() =>
- {
- ModifierToolTip.Content = _traitVo.Description;
- ModifierToolTip.ShowAt(_displayTarget, _flyoutShowOptions);
- });
- };
- }
-
- public void Dispose()
- {
- _showModifierToolTipTimer.Dispose();
- ViewModel.Close();
- }
-
- private void Border_OnPointerEntered(object sender, PointerRoutedEventArgs e)
- {
- var border = (Border)sender;
- border.Background = _whiteSmokeBrush;
- _traitVo = (TraitVo)border.DataContext;
- _displayTarget = border;
- _showModifierToolTipTimer.Start();
- }
-
- private void Border_OnPointerExited(object sender, PointerRoutedEventArgs e)
- {
- _showModifierToolTipTimer.Stop();
- var border = (Border)sender;
- border.Background = _transparentBrush;
- ModifierToolTip.Hide();
- }
-
- private void Border_OnPointerPressed(object sender, PointerRoutedEventArgs e)
- {
- var border = (Border)sender;
- var trait = (TraitVo)border.DataContext;
- trait.IsSelected = !trait.IsSelected;
- }
-}
diff --git a/Moder.Core/Views/IFileView.cs b/Moder.Core/Views/IFileView.cs
deleted file mode 100644
index 75b4498..0000000
--- a/Moder.Core/Views/IFileView.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Moder.Core.Views;
-
-///
-/// 可视化文件接口
-///
-public interface IFileView
-{
- public string Title { get; }
- public string FullPath { get; }
-}
\ No newline at end of file
diff --git a/Moder.Core/Views/MainWindow.axaml b/Moder.Core/Views/MainWindow.axaml
new file mode 100644
index 0000000..e0dba77
--- /dev/null
+++ b/Moder.Core/Views/MainWindow.axaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Moder.Core/Views/MainWindow.axaml.cs b/Moder.Core/Views/MainWindow.axaml.cs
new file mode 100644
index 0000000..8971e57
--- /dev/null
+++ b/Moder.Core/Views/MainWindow.axaml.cs
@@ -0,0 +1,52 @@
+using Avalonia.Controls;
+using CommunityToolkit.Mvvm.Messaging;
+using Microsoft.Extensions.DependencyInjection;
+using Moder.Core.Messages;
+using Moder.Core.Services.Config;
+using Moder.Core.ViewsModel;
+using NLog;
+
+namespace Moder.Core.Views;
+
+public sealed partial class MainWindow : Window
+{
+ private static readonly Logger Log = LogManager.GetCurrentClassLogger();
+
+ public MainWindow()
+ {
+ var settingService = App.Services.GetRequiredService();
+ Log.Info("App Config path: {Path}", App.AppConfigFolder);
+ InitializeComponent();
+
+ DataContext = App.Services.GetRequiredService();
+
+ if (string.IsNullOrEmpty(settingService.GameRootFolderPath))
+ {
+ Log.Info("开始初始化设置");
+ NavigateTo(typeof(Menus.AppInitializeControlView));
+ }
+ else
+ {
+ NavigateTo(typeof(Menus.MainControlView));
+ }
+
+ WeakReferenceMessenger.Default.Register(
+ this,
+ (_, _) =>
+ {
+ NavigateTo(typeof(Menus.MainControlView));
+ }
+ );
+ }
+
+ private void NavigateTo(Type view)
+ {
+ if (MainContentControl.Content is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+
+ MainContentControl.Content = App.Services.GetRequiredService(view);
+ Log.Info("导航到 {View}", view.Name);
+ }
+}
diff --git a/Moder.Core/Views/MainWindow.xaml b/Moder.Core/Views/MainWindow.xaml
deleted file mode 100644
index 3e3f663..0000000
--- a/Moder.Core/Views/MainWindow.xaml
+++ /dev/null
@@ -1,106 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Moder.Core/Views/MainWindow.xaml.cs b/Moder.Core/Views/MainWindow.xaml.cs
deleted file mode 100644
index 4b722ce..0000000
--- a/Moder.Core/Views/MainWindow.xaml.cs
+++ /dev/null
@@ -1,316 +0,0 @@
-using System.Diagnostics;
-using System.Globalization;
-using CommunityToolkit.Mvvm.Messaging;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.UI.Input;
-using Microsoft.UI.Windowing;
-using Microsoft.UI.Xaml;
-using Microsoft.UI.Xaml.Controls;
-using Moder.Core.Helper;
-using Moder.Core.Messages;
-using Moder.Core.Services.Config;
-using Moder.Core.Views.Game;
-using Moder.Core.Views.Menus;
-using Moder.Core.ViewsModels;
-using Moder.Core.ViewsModels.Menus;
-using NLog;
-using Windows.Foundation;
-using Moder.Core.Models;
-
-namespace Moder.Core.Views;
-
-public sealed partial class MainWindow
-{
- public MainWindowViewModel ViewModel { get; }
-
- private readonly GlobalSettingService _settings;
- private readonly IServiceProvider _serviceProvider;
- private static readonly Logger Log = LogManager.GetCurrentClassLogger();
-
- ///
- /// 缓存已打开的文件标签页, 避免在侧边栏中查询
- ///
- private readonly List _openedTabFileItems = new(8);
-
- private string _selectedSideFileItemFullPath = string.Empty;
-
- public MainWindow(
- MainWindowViewModel model,
- GlobalSettingService settings,
- IServiceProvider serviceProvider
- )
- {
- ViewModel = model;
- _settings = settings;
- _serviceProvider = serviceProvider;
- InitializeComponent();
-
- SetAppLanguage();
- AppWindow.SetIcon(Path.Combine(AppContext.BaseDirectory, "Assets/logo.ico"));
- ExtendsContentIntoTitleBar = true;
- SetTitleBar(AppTitleBar);
- AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Standard;
- WindowHelper.SetSystemBackdropTypeByConfig(this);
-
- AppTitleBar.Loaded += AppTitleBarOnLoaded;
- AppTitleBar.SizeChanged += AppTitleBarOnSizeChanged;
-
- if (string.IsNullOrEmpty(settings.ModRootFolderPath))
- {
- SideContentControl.Content = _serviceProvider.GetRequiredService();
- }
- else
- {
- SideContentControl.Content = _serviceProvider.GetRequiredService();
- }
-
- WeakReferenceMessenger.Default.Register(
- this,
- (_, _) =>
- SideContentControl.Content = _serviceProvider.GetRequiredService()
- );
-
- WeakReferenceMessenger.Default.Register(this, OnOpenFile);
-
- WeakReferenceMessenger.Default.Register(
- this,
- (_, _) =>
- {
- Bindings.Update();
- }
- );
- }
-
- private void SetAppLanguage()
- {
- if (_settings.AppLanguage != AppLanguageInfo.Default)
- {
- CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(_settings.AppLanguage);
- }
- }
-
- private void OnOpenFile(object sender, OpenFileMessage message)
- {
- _selectedSideFileItemFullPath = message.FileItem.FullPath;
-
- // 如果文件已经打开,则切换到已打开的标签页
- // 如果文件未打开,则打开新的标签页
- var openedTab = MainTabView.TabItems.FirstOrDefault(item =>
- {
- if (item is not TabViewItem tabViewItem)
- {
- return false;
- }
-
- var view = tabViewItem.Content as IFileView;
- return view?.FullPath == message.FileItem.FullPath;
- });
-
- if (openedTab is null)
- {
- // 打开新的标签页
- var content = GetContent(message.FileItem);
- var newTab = new TabViewItem { Content = content, Header = message.FileItem.Name };
- ToolTipService.SetToolTip(newTab, message.FileItem.FullPath);
- NavigateToNewTab(newTab);
-
- _openedTabFileItems.Add(message.FileItem);
- }
- else
- {
- // 切换到已打开的标签页
- MainTabView.SelectedItem = openedTab;
- }
- }
-
- private IFileView GetContent(SystemFileItem fileItem)
- {
- var relativePath = Path.GetRelativePath(_settings.ModRootFolderPath, fileItem.FullPath);
- if (relativePath.Contains("states"))
- {
- return _serviceProvider.GetRequiredService();
- }
-
- return _serviceProvider.GetRequiredService();
- }
-
- private void AppTitleBarOnSizeChanged(object sender, SizeChangedEventArgs e)
- {
- SetRegionsForCustomTitleBar();
- }
-
- private void AppTitleBarOnLoaded(object sender, RoutedEventArgs e)
- {
- SetRegionsForCustomTitleBar();
- }
-
- private void SetRegionsForCustomTitleBar()
- {
- var scaleAdjustment = AppTitleBar.XamlRoot.RasterizationScale;
- RightPaddingColumn.Width = new GridLength(AppWindow.TitleBar.RightInset / scaleAdjustment);
- LeftPaddingColumn.Width = new GridLength(AppWindow.TitleBar.LeftInset / scaleAdjustment);
- var transform = SettingsButton.TransformToVisual(null);
- var bounds = transform.TransformBounds(
- new Rect(
- 0,
- 0,
- SettingsButton.ActualWidth + CharacterEditorButton.ActualWidth,
- SettingsButton.ActualHeight + CharacterEditorButton.ActualHeight
- )
- );
- var settingsButtonRect = GetRect(bounds, scaleAdjustment);
-
- var rectArray = new[] { settingsButtonRect };
-
- var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(AppWindow.Id);
- nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rectArray);
- }
-
- private static Windows.Graphics.RectInt32 GetRect(Rect bounds, double scale)
- {
- return new Windows.Graphics.RectInt32(
- _X: (int)Math.Round(bounds.X * scale),
- _Y: (int)Math.Round(bounds.Y * scale),
- _Width: (int)Math.Round(bounds.Width * scale),
- _Height: (int)Math.Round(bounds.Height * scale)
- );
- }
-
- private void MainWindow_OnClosed(object sender, WindowEventArgs args)
- {
- _settings.SaveChanged();
- }
-
- private void MainTabView_OnTabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args)
- {
- var isRemoved = sender.TabItems.Remove(args.Tab);
- Debug.Assert(isRemoved);
-
- var tab = args.Tab.Content;
- // 关闭文件标签页时,从缓存列表中移除对应的文件并同步侧边栏选中项
- if (tab is IFileView fileView)
- {
- var index = _openedTabFileItems.FindIndex(item => item.FullPath == fileView.FullPath);
- if (index == -1)
- {
- Log.Warn("未在标签文件缓存列表中找到指定文件, Path: {Path}", fileView.FullPath);
- }
- else
- {
- _openedTabFileItems.RemoveAt(index);
- if (sender.TabItems.Count == 0)
- {
- WeakReferenceMessenger.Default.Send(new SyncSideWorkSelectedItemMessage(null));
- }
- }
- }
- else if (tab is SettingsControlView settings)
- {
- settings.SaveChanged();
- }
-
- if (tab is IDisposable disposable)
- {
- disposable.Dispose();
- }
- }
-
- private void MainTabView_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- if (MainTabView.SelectedItem is not TabViewItem currentTab)
- {
- return;
- }
-
- // 如果切换到的标签页不是文件标签页 (比如设置标签页),则清空侧边栏选中项
- if (currentTab.Content is not IFileView currentFileView)
- {
- ClearSideWorkSelectState();
- return;
- }
-
- // 切换标签页时,同步侧边栏选中项
- if (_selectedSideFileItemFullPath != currentFileView.FullPath)
- {
- _selectedSideFileItemFullPath = currentFileView.FullPath;
- var target = _openedTabFileItems.Find(item => item.FullPath == currentFileView.FullPath);
- Debug.Assert(target is not null, "在标签文件缓存列表中未找到目标文件");
- WeakReferenceMessenger.Default.Send(new SyncSideWorkSelectedItemMessage(target));
- }
- }
-
- private void ClearSideWorkSelectState()
- {
- WeakReferenceMessenger.Default.Send(new SyncSideWorkSelectedItemMessage(null));
- _selectedSideFileItemFullPath = string.Empty;
- }
-
- ///
- /// 打开设置标签页
- ///
- private void TitleBarSettingsButton_OnClick(object sender, RoutedEventArgs e)
- {
- var isSelected = MainTabView.SelectedItem is TabViewItem { Content: SettingsControlView };
- if (isSelected)
- {
- return;
- }
-
- var existingSettingsTab = MainTabView
- .TabItems.Cast()
- .FirstOrDefault(item => item.Content is SettingsControlView);
-
- if (existingSettingsTab is not null)
- {
- MainTabView.SelectedItem = existingSettingsTab;
- return;
- }
-
- var settingsView = _serviceProvider.GetRequiredService();
- var settingsTab = new TabViewItem
- {
- Content = settingsView,
- Header = Language.Strings.Resource.Menu_Settings,
- IconSource = new FontIconSource { Glyph = "\uE713" }
- };
- NavigateToNewTab(settingsTab);
- }
-
- private void CharacterEditorButton_OnClick(object sender, RoutedEventArgs e)
- {
- var isSelected = MainTabView.SelectedItem is TabViewItem { Content: CharacterEditorControlView };
- if (isSelected)
- {
- return;
- }
-
- var existingTab = MainTabView
- .TabItems.Cast()
- .FirstOrDefault(item => item.Content is CharacterEditorControlView);
-
- if (existingTab is not null)
- {
- MainTabView.SelectedItem = existingTab;
- return;
- }
-
- var editorView = new CharacterEditorControlView();
- var editorTab = new TabViewItem
- {
- Content = editorView,
- Header = Language.Strings.Resource.Menu_CharacterEditor,
- IconSource = new FontIconSource { Glyph = "\uE70F" }
- };
- NavigateToNewTab(editorTab);
- }
-
- ///
- /// 添加标签页并切换到新标签页, 在此方法运行之后,会触发 方法
- ///
- /// 添加的标签页
- private void NavigateToNewTab(TabViewItem tab)
- {
- MainTabView.TabItems.Add(tab);
- MainTabView.SelectedItem = tab;
- }
-}
diff --git a/Moder.Core/Views/Menus/AppInitializeControlView.axaml b/Moder.Core/Views/Menus/AppInitializeControlView.axaml
new file mode 100644
index 0000000..4a09852
--- /dev/null
+++ b/Moder.Core/Views/Menus/AppInitializeControlView.axaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
diff --git a/Moder.Core/Views/Menus/AppInitializeControlView.axaml.cs b/Moder.Core/Views/Menus/AppInitializeControlView.axaml.cs
new file mode 100644
index 0000000..5e366f0
--- /dev/null
+++ b/Moder.Core/Views/Menus/AppInitializeControlView.axaml.cs
@@ -0,0 +1,47 @@
+using Avalonia.Controls;
+using Avalonia.Platform.Storage;
+using Microsoft.Extensions.DependencyInjection;
+using AppInitializeControlViewModel = Moder.Core.ViewsModel.Menus.AppInitializeControlViewModel;
+
+namespace Moder.Core.Views.Menus;
+
+public sealed partial class AppInitializeControlView : UserControl
+{
+ private IDisposable? _selectFolderInteractionDisposable;
+
+ public AppInitializeControlView()
+ {
+ InitializeComponent();
+
+ var viewModel = App.Services.GetRequiredService();
+ DataContext = viewModel;
+ }
+
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ _selectFolderInteractionDisposable?.Dispose();
+
+ if (DataContext is AppInitializeControlViewModel viewModel)
+ {
+ _selectFolderInteractionDisposable = viewModel.SelectFolderInteraction.RegisterHandler(Handler);
+ }
+
+ base.OnDataContextChanged(e);
+ }
+
+ private async Task Handler(string title)
+ {
+ var topLevel = TopLevel.GetTopLevel(this);
+ if (topLevel is null)
+ {
+ return string.Empty;
+ }
+
+ var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(
+ new FolderPickerOpenOptions { Title = title, AllowMultiple = false }
+ );
+ var result = folders.Count > 0 ? folders[0].TryGetLocalPath() ?? string.Empty : string.Empty;
+
+ return result;
+ }
+}
diff --git a/Moder.Core/Views/Menus/AppSettingsView.axaml b/Moder.Core/Views/Menus/AppSettingsView.axaml
new file mode 100644
index 0000000..939fc00
--- /dev/null
+++ b/Moder.Core/Views/Menus/AppSettingsView.axaml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Moder.Core/Views/Menus/AppSettingsView.axaml.cs b/Moder.Core/Views/Menus/AppSettingsView.axaml.cs
new file mode 100644
index 0000000..05f4306
--- /dev/null
+++ b/Moder.Core/Views/Menus/AppSettingsView.axaml.cs
@@ -0,0 +1,60 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Platform.Storage;
+using Microsoft.Extensions.DependencyInjection;
+using Moder.Core.Infrastructure;
+using Moder.Core.ViewsModel.Menus;
+using Moder.Language.Strings;
+
+namespace Moder.Core.Views.Menus;
+
+public sealed partial class AppSettingsView : UserControl, ITabViewItem
+{
+ private IDisposable? _selectFolderInteractionDisposable;
+
+ public AppSettingsView()
+ {
+ InitializeComponent();
+ ViewModel = App.Services.GetRequiredService();
+ DataContext = ViewModel;
+ }
+
+ public string Header => Resource.Menu_Settings;
+ public string Id => nameof(AppSettingsView);
+ public string ToolTip => Header;
+
+ private AppSettingsViewModel ViewModel { get; }
+
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ _selectFolderInteractionDisposable?.Dispose();
+
+ if (DataContext is AppSettingsViewModel viewModel)
+ {
+ _selectFolderInteractionDisposable = viewModel.SelectFolderInteraction.RegisterHandler(Handler);
+ }
+
+ base.OnDataContextChanged(e);
+ }
+
+ private async Task Handler(string title)
+ {
+ var topLevel = TopLevel.GetTopLevel(this);
+ if (topLevel is null)
+ {
+ return string.Empty;
+ }
+
+ var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(
+ new FolderPickerOpenOptions { Title = title, AllowMultiple = false }
+ );
+ var result = folders.Count > 0 ? folders[0].TryGetLocalPath() ?? string.Empty : string.Empty;
+
+ return result;
+ }
+
+ private void SettingsExpanderItem_OnClick(object? sender, RoutedEventArgs e)
+ {
+ _ = TopLevel.GetTopLevel(this)?.Launcher.LaunchUriAsync(new Uri(App.CodeRepositoryUrl));
+ }
+}
diff --git a/Moder.Core/Views/Menus/MainControlView.axaml b/Moder.Core/Views/Menus/MainControlView.axaml
new file mode 100644
index 0000000..591ecb3
--- /dev/null
+++ b/Moder.Core/Views/Menus/MainControlView.axaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Moder.Core/Views/Menus/MainControlView.axaml.cs b/Moder.Core/Views/Menus/MainControlView.axaml.cs
new file mode 100644
index 0000000..f6976ad
--- /dev/null
+++ b/Moder.Core/Views/Menus/MainControlView.axaml.cs
@@ -0,0 +1,19 @@
+using Avalonia.Controls;
+using Microsoft.Extensions.DependencyInjection;
+using Moder.Core.ViewsModel.Menus;
+
+namespace Moder.Core.Views.Menus;
+
+public sealed partial class MainControlView : UserControl
+{
+ public MainControlView()
+ {
+ InitializeComponent();
+ DataContext = App.Services.GetRequiredService();
+
+ SideBarControl.Content = App.Services.GetRequiredService();
+ WorkSpaceControl.Content = App.Services.GetRequiredService();
+
+ StatusBarControl.Content = App.Services.GetRequiredService();
+ }
+}
diff --git a/Moder.Core/Views/Menus/NotSupportInfoControlView.axaml b/Moder.Core/Views/Menus/NotSupportInfoControlView.axaml
new file mode 100644
index 0000000..5f91864
--- /dev/null
+++ b/Moder.Core/Views/Menus/NotSupportInfoControlView.axaml
@@ -0,0 +1,11 @@
+
+ Welcome to Avalonia!
+
diff --git a/Moder.Core/Views/Menus/NotSupportInfoControlView.axaml.cs b/Moder.Core/Views/Menus/NotSupportInfoControlView.axaml.cs
new file mode 100644
index 0000000..8ce41f1
--- /dev/null
+++ b/Moder.Core/Views/Menus/NotSupportInfoControlView.axaml.cs
@@ -0,0 +1,20 @@
+using Avalonia.Controls;
+using Moder.Core.Infrastructure;
+using Moder.Core.Models;
+
+namespace Moder.Core.Views.Menus;
+
+public sealed partial class NotSupportInfoControlView : UserControl, ITabViewItem
+{
+ public NotSupportInfoControlView(SystemFileItem item)
+ {
+ InitializeComponent();
+ Header = item.Name;
+ Id = item.FullPath;
+ ToolTip = item.FullPath;
+ }
+
+ public string Header { get; }
+ public string Id { get; }
+ public string ToolTip { get; }
+}
diff --git a/Moder.Core/Views/Menus/OpenFolderControlView.xaml b/Moder.Core/Views/Menus/OpenFolderControlView.xaml
deleted file mode 100644
index 2212e4d..0000000
--- a/Moder.Core/Views/Menus/OpenFolderControlView.xaml
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Moder.Core/Views/Menus/OpenFolderControlView.xaml.cs b/Moder.Core/Views/Menus/OpenFolderControlView.xaml.cs
deleted file mode 100644
index 03d1255..0000000
--- a/Moder.Core/Views/Menus/OpenFolderControlView.xaml.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Microsoft.UI.Xaml.Controls;
-using Moder.Core.ViewsModels.Menus;
-
-// To learn more about WinUI, the WinUI project structure,
-// and more about our project templates, see: http://aka.ms/winui-project-info.
-
-namespace Moder.Core.Views.Menus;
-
-public sealed partial class OpenFolderControlView : UserControl
-{
- public OpenFolderControlViewModel ViewModel => (OpenFolderControlViewModel)DataContext;
-
- public OpenFolderControlView(OpenFolderControlViewModel model)
- {
- this.InitializeComponent();
- DataContext = model;
- }
-}
\ No newline at end of file
diff --git a/Moder.Core/Views/Menus/RenameFileControlView.axaml b/Moder.Core/Views/Menus/RenameFileControlView.axaml
new file mode 100644
index 0000000..630776a
--- /dev/null
+++ b/Moder.Core/Views/Menus/RenameFileControlView.axaml
@@ -0,0 +1,16 @@
+
+
+
diff --git a/Moder.Core/Views/Menus/RenameFileControlView.xaml.cs b/Moder.Core/Views/Menus/RenameFileControlView.axaml.cs
similarity index 54%
rename from Moder.Core/Views/Menus/RenameFileControlView.xaml.cs
rename to Moder.Core/Views/Menus/RenameFileControlView.axaml.cs
index c8c39a1..6d674b6 100644
--- a/Moder.Core/Views/Menus/RenameFileControlView.xaml.cs
+++ b/Moder.Core/Views/Menus/RenameFileControlView.axaml.cs
@@ -1,21 +1,20 @@
-using Microsoft.UI.Xaml.Controls;
-using Moder.Core.ViewsModels.Menus;
+using Avalonia.Controls;
+using FluentAvalonia.UI.Controls;
+using Moder.Core.Models;
+using Moder.Core.ViewsModel.Menus;
namespace Moder.Core.Views.Menus;
public sealed partial class RenameFileControlView : UserControl
{
+ public string NewName => ViewModel.NewName;
public bool IsInvalid => !ViewModel.IsValid;
- public string NewName => NewNameTextBox.Text;
private RenameFileControlViewModel ViewModel { get; }
public RenameFileControlView(ContentDialog dialog, SystemFileItem fileItem)
{
- ViewModel = new RenameFileControlViewModel(dialog, fileItem);
InitializeComponent();
-
- // 在 ViewModel 里设置 SelectionLength 不生效, 在这里设置一下
- NewNameTextBox.Text = ViewModel.NewName;
- NewNameTextBox.SelectionLength = ViewModel.SelectionLength;
+ ViewModel = new RenameFileControlViewModel(dialog, fileItem);
+ DataContext = ViewModel;
}
}
diff --git a/Moder.Core/Views/Menus/RenameFileControlView.xaml b/Moder.Core/Views/Menus/RenameFileControlView.xaml
deleted file mode 100644
index 09adda8..0000000
--- a/Moder.Core/Views/Menus/RenameFileControlView.xaml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
- False
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Moder.Core/Views/Menus/SettingsControlView.xaml b/Moder.Core/Views/Menus/SettingsControlView.xaml
deleted file mode 100644
index b8a14a7..0000000
--- a/Moder.Core/Views/Menus/SettingsControlView.xaml
+++ /dev/null
@@ -1,137 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Moder.Core/Views/Menus/SettingsControlView.xaml.cs b/Moder.Core/Views/Menus/SettingsControlView.xaml.cs
deleted file mode 100644
index 8c72731..0000000
--- a/Moder.Core/Views/Menus/SettingsControlView.xaml.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using CommunityToolkit.Mvvm.Messaging;
-using CommunityToolkit.WinUI.Controls;
-using Microsoft.UI.Xaml;
-using Moder.Core.Messages;
-using Moder.Core.Services.Config;
-using Moder.Core.ViewsModels.Menus;
-using Windows.System;
-
-namespace Moder.Core.Views.Menus;
-
-public sealed partial class SettingsControlView : IDisposable
-{
- public static string RuntimeInfo => $"Runtime: .NET {Environment.Version.ToString()}";
-
- public SettingsControlViewModel ViewModel => (SettingsControlViewModel)DataContext;
- private readonly GlobalSettingService _globalSettingService;
-
- public SettingsControlView(
- SettingsControlViewModel settingsViewModel,
- GlobalSettingService globalSettingService
- )
- {
- _globalSettingService = globalSettingService;
- InitializeComponent();
-
- DataContext = settingsViewModel;
-
- WeakReferenceMessenger.Default.Register(this, (_, _) => Bindings.Update());
- }
-
- private async void OnRootPathCardClicked(object sender, RoutedEventArgs e)
- {
- var card = (SettingsCard)sender;
- await Launcher.LaunchFolderPathAsync(card.Description.ToString());
- }
-
- private async void OnGitHubUrlCardClicked(object sender, RoutedEventArgs e)
- {
- var card = (SettingsCard)sender;
- await Launcher.LaunchUriAsync(
- new Uri(card.Description.ToString() ?? throw new InvalidOperationException())
- );
- }
-
- ///
- /// 如果有更改,保存更改
- ///
- public void SaveChanged()
- {
- _globalSettingService.SaveChanged();
- }
-
- public void Dispose()
- {
- WeakReferenceMessenger.Default.UnregisterAll(this);
- }
-}
diff --git a/Moder.Core/Views/Menus/SideBarControlView.axaml b/Moder.Core/Views/Menus/SideBarControlView.axaml
new file mode 100644
index 0000000..ba8750d
--- /dev/null
+++ b/Moder.Core/Views/Menus/SideBarControlView.axaml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Moder.Core/Views/Menus/SideBarControlView.axaml.cs b/Moder.Core/Views/Menus/SideBarControlView.axaml.cs
new file mode 100644
index 0000000..77dfb94
--- /dev/null
+++ b/Moder.Core/Views/Menus/SideBarControlView.axaml.cs
@@ -0,0 +1,95 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.VisualTree;
+using FluentAvalonia.UI.Controls;
+using Microsoft.Extensions.DependencyInjection;
+using Moder.Core.Models;
+using Moder.Core.Services;
+using Moder.Core.ViewsModel.Menus;
+
+namespace Moder.Core.Views.Menus;
+
+public sealed partial class SideBarControlView : UserControl
+{
+ // BUG: 第一次右键选择菜单项时文件树有可能会滚动到顶部
+ // BUG: 右键选中效果会和滚动条重合, 在实现拉伸文件树时尝试修复
+ private readonly FAMenuFlyout _contextMenu;
+ private TreeViewItem? _lastSelectedTreeViewItem;
+ private readonly Thickness _rightSelectedItemThickness = new(0.65);
+ private readonly TabViewNavigationService _tabViewNavigation;
+
+ public SideBarControlView()
+ {
+ InitializeComponent();
+ _tabViewNavigation = App.Services.GetRequiredService();
+ _contextMenu = Resources["ContextMenu"] as FAMenuFlyout ?? throw new InvalidOperationException();
+
+ DataContext = App.Services.GetRequiredService();
+
+ FileTreeView.AutoScrollToSelectedItem = true;
+ FileTreeView.AddHandler(PointerPressedEvent, FileTreeView_OnPointerPressed, RoutingStrategies.Tunnel);
+ }
+
+ private void FileTreeView_OnPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (e.Pointer.Type != PointerType.Mouse)
+ {
+ return;
+ }
+
+ // 无论左键右键都清理上次右键选中的项的选中效果
+ ClearTreeViewItemRightSelectEffect();
+
+ var point = e.GetCurrentPoint(FileTreeView);
+
+ if (point.Properties.PointerUpdateKind == PointerUpdateKind.RightButtonPressed)
+ {
+ e.Handled = true;
+
+ var treeViewItem = ((Control?)e.Source)
+ ?.GetVisualAncestors()
+ .OfType()
+ .FirstOrDefault();
+ if (treeViewItem is null)
+ {
+ return;
+ }
+
+ _contextMenu.ShowAt(treeViewItem, true);
+ _lastSelectedTreeViewItem = treeViewItem;
+ treeViewItem.BorderThickness = _rightSelectedItemThickness;
+ treeViewItem.BorderBrush = Brushes.CornflowerBlue;
+ }
+ }
+
+ private void ClearTreeViewItemRightSelectEffect()
+ {
+ if (_lastSelectedTreeViewItem is not null)
+ {
+ _lastSelectedTreeViewItem.BorderBrush = Brushes.Transparent;
+ }
+ }
+
+ private void TreeView_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (e.AddedItems.Count == 0)
+ {
+ return;
+ }
+
+ if (e.AddedItems[0] is not SystemFileItem item)
+ {
+ return;
+ }
+
+ if (item.IsFolder)
+ {
+ return;
+ }
+
+ _tabViewNavigation.AddTab(new NotSupportInfoControlView(item));
+ }
+}
diff --git a/Moder.Core/Views/Menus/SideWorkSpaceControlView.xaml b/Moder.Core/Views/Menus/SideWorkSpaceControlView.xaml
deleted file mode 100644
index e607405..0000000
--- a/Moder.Core/Views/Menus/SideWorkSpaceControlView.xaml
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Moder.Core/Views/Menus/SideWorkSpaceControlView.xaml.cs b/Moder.Core/Views/Menus/SideWorkSpaceControlView.xaml.cs
deleted file mode 100644
index fbca200..0000000
--- a/Moder.Core/Views/Menus/SideWorkSpaceControlView.xaml.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using System.Diagnostics;
-using CommunityToolkit.Mvvm.Messaging;
-using Microsoft.UI.Xaml.Controls;
-using Microsoft.UI.Xaml.Input;
-using Microsoft.UI.Xaml.Media;
-using Moder.Core.Messages;
-using Moder.Core.Services;
-using Moder.Core.ViewsModels.Menus;
-using Microsoft.UI;
-using NLog;
-
-namespace Moder.Core.Views.Menus;
-
-public sealed partial class SideWorkSpaceControlView : UserControl
-{
- // TODO: 本地化文本无法及时刷新, 需重启软件
- public SideWorkSpaceControlViewModel ViewModel => (SideWorkSpaceControlViewModel)DataContext;
- private static readonly Logger Log = LogManager.GetCurrentClassLogger();
- private readonly GlobalResourceService _resourceService;
- private TreeViewItem? _lastSelectedItem;
-
- public SideWorkSpaceControlView(
- SideWorkSpaceControlViewModel model,
- GlobalResourceService resourceService
- )
- {
- _resourceService = resourceService;
- InitializeComponent();
-
- DataContext = model;
-
- WeakReferenceMessenger.Default.Register(
- this,
- (_, message) =>
- {
- AssertNeedSync(message);
- FileTreeView.SelectedItem = message.TargetItem;
- Log.Debug("侧边栏同步选中项为: {SelectedItem}", message.TargetItem?.Name);
- }
- );
- }
-
- [Conditional("DEBUG")]
- private void AssertNeedSync(SyncSideWorkSelectedItemMessage message)
- {
- // 未选中文件且触发此事件时, 是打开了非文件视图, 比如设置标签页
- // 此时 FileTreeView.SelectedItem 和 message.TargetItem 都为 null, 所以我们先做一个判断
- if (FileTreeView.SelectedItem is not null)
- {
- Debug.Assert(FileTreeView.SelectedItem != message.TargetItem);
- }
- }
-
- private void FileTreeView_OnSelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args)
- {
- ClearTreeViewItemRightSelectEffect();
-
- // 当用户切换选中的 FileTreeView 时, 会触发两次 SelectionChanged 事件, 第一次是离开当前选中项, 第二次是进入新选中项
- // 这里不处理第一次的离开事件, 只处理第二次的进入事件
- if (args.AddedItems.Count != 1)
- {
- return;
- }
-
- if (args.AddedItems[0] is SystemFileItem { IsFile: true } file)
- {
- Log.Info("文件: {File}", file.Name);
- // TODO: 这样做只能打开一个文件
- _resourceService.SetCurrentSelectFileItem(file);
-
- WeakReferenceMessenger.Default.Send(new OpenFileMessage(file));
- }
- }
-
- private void TreeViewItem_OnRightTapped(object sender, RightTappedRoutedEventArgs e)
- {
- ClearTreeViewItemRightSelectEffect();
-
- var item = (TreeViewItem)sender;
- _lastSelectedItem = item;
- item.BorderBrush = new SolidColorBrush(Colors.CornflowerBlue);
- }
-
- private void ClearTreeViewItemRightSelectEffect()
- {
- if (_lastSelectedItem is not null)
- {
- _lastSelectedItem.BorderBrush = new SolidColorBrush(Colors.Transparent);
- }
- }
-}
diff --git a/Moder.Core/Views/Menus/StatusBarControlView.axaml b/Moder.Core/Views/Menus/StatusBarControlView.axaml
new file mode 100644
index 0000000..06f5f9d
--- /dev/null
+++ b/Moder.Core/Views/Menus/StatusBarControlView.axaml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Moder.Core/Views/Menus/StatusBarControlView.axaml.cs b/Moder.Core/Views/Menus/StatusBarControlView.axaml.cs
new file mode 100644
index 0000000..dea995b
--- /dev/null
+++ b/Moder.Core/Views/Menus/StatusBarControlView.axaml.cs
@@ -0,0 +1,15 @@
+using Avalonia.Controls;
+using Microsoft.Extensions.DependencyInjection;
+using Moder.Core.ViewsModel.Menus;
+
+namespace Moder.Core.Views.Menus;
+
+public sealed partial class StatusBarControlView : UserControl
+{
+ public StatusBarControlView()
+ {
+ InitializeComponent();
+
+ DataContext = App.Services.GetRequiredService();
+ }
+}
\ No newline at end of file
diff --git a/Moder.Core/Views/Menus/WorkSpaceControlView.axaml b/Moder.Core/Views/Menus/WorkSpaceControlView.axaml
new file mode 100644
index 0000000..35ea831
--- /dev/null
+++ b/Moder.Core/Views/Menus/WorkSpaceControlView.axaml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/Moder.Core/Views/Menus/WorkSpaceControlView.axaml.cs b/Moder.Core/Views/Menus/WorkSpaceControlView.axaml.cs
new file mode 100644
index 0000000..3adac92
--- /dev/null
+++ b/Moder.Core/Views/Menus/WorkSpaceControlView.axaml.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics;
+using Avalonia.Controls;
+using FluentAvalonia.UI.Controls;
+using Microsoft.Extensions.DependencyInjection;
+using Moder.Core.Services;
+
+namespace Moder.Core.Views.Menus;
+
+public sealed partial class WorkSpaceControlView : UserControl
+{
+ private readonly TabViewNavigationService _tabService;
+
+ public WorkSpaceControlView()
+ {
+ InitializeComponent();
+
+ _tabService = App.Services.GetRequiredService();
+ _tabService.Initialize(MainTabView);
+ }
+
+ private void MainTabView_OnTabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args)
+ {
+ var isRemoved = _tabService.RemoveTab(args.Tab);
+ Debug.Assert(isRemoved);
+ }
+}
diff --git a/Moder.Core/Views/NotSupportInfoControlView.xaml b/Moder.Core/Views/NotSupportInfoControlView.xaml
deleted file mode 100644
index fb9c1f2..0000000
--- a/Moder.Core/Views/NotSupportInfoControlView.xaml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
diff --git a/Moder.Core/Views/NotSupportInfoControlView.xaml.cs b/Moder.Core/Views/NotSupportInfoControlView.xaml.cs
deleted file mode 100644
index 1ac9324..0000000
--- a/Moder.Core/Views/NotSupportInfoControlView.xaml.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Moder.Core.Services;
-
-namespace Moder.Core.Views;
-
-public sealed partial class NotSupportInfoControlView : IFileView
-{
- public NotSupportInfoControlView(GlobalResourceService resourceService)
- {
- this.InitializeComponent();
-
- FullPath = resourceService.PopCurrentSelectFileItem().FullPath;
- }
-
- public string Title => "不支持";
- public string FullPath { get; }
-}
\ No newline at end of file
diff --git a/Moder.Core/ViewsModel/DesignData.cs b/Moder.Core/ViewsModel/DesignData.cs
new file mode 100644
index 0000000..415c74d
--- /dev/null
+++ b/Moder.Core/ViewsModel/DesignData.cs
@@ -0,0 +1,11 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Moder.Core.ViewsModel;
+
+public static class DesignData
+{
+ public static MainWindowViewModel MainWindowViewModel { get; } =
+ App.Services.GetRequiredService();
+ public static Menus.AppInitializeControlViewModel AppInitializeControlViewModel { get; } =
+ App.Services.GetRequiredService();
+}
diff --git a/Moder.Core/ViewsModels/Game/CharacterEditorControlViewModel.cs b/Moder.Core/ViewsModel/Game/CharacterEditorControlViewModel.cs
similarity index 59%
rename from Moder.Core/ViewsModels/Game/CharacterEditorControlViewModel.cs
rename to Moder.Core/ViewsModel/Game/CharacterEditorControlViewModel.cs
index d7b3591..ee17449 100644
--- a/Moder.Core/ViewsModels/Game/CharacterEditorControlViewModel.cs
+++ b/Moder.Core/ViewsModel/Game/CharacterEditorControlViewModel.cs
@@ -1,226 +1,248 @@
-using System.ComponentModel;
-using System.Runtime.InteropServices;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Controls.Documents;
+using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
-using Microsoft.UI.Xaml.Controls;
-using Microsoft.UI.Xaml.Documents;
using Moder.Core.Extensions;
-using Moder.Core.Models.Character;
+using Moder.Core.Infrastructure;
+using Moder.Core.Infrastructure.Parser;
+using Moder.Core.Models;
+using Moder.Core.Models.Game.Character;
using Moder.Core.Models.Vo;
-using Moder.Core.Parser;
using Moder.Core.Services;
using Moder.Core.Services.Config;
using Moder.Core.Services.GameResources;
using Moder.Core.Services.GameResources.Base;
+using Moder.Core.Services.GameResources.Modifiers;
using Moder.Core.Views.Game;
using Moder.Language.Strings;
using NLog;
using ParadoxPower.CSharpExtensions;
using ParadoxPower.Process;
-namespace Moder.Core.ViewsModels.Game;
+namespace Moder.Core.ViewsModel.Game;
-public sealed partial class CharacterEditorControlViewModel : ObservableObject
+public sealed partial class CharacterEditorControlViewModel : ObservableValidator, IClosed
{
- public ComboBoxItem[] CharactersType { get; } =
- [
- new() { Content = "将军 (corps_commander)", Tag = "corps_commander" },
- new() { Content = "陆军元帅 (field_marshal)", Tag = "field_marshal" },
- new() { Content = "海军将领 (navy_leader)", Tag = "navy_leader" }
- ];
+ [ObservableProperty]
+ public partial ushort Level { get; set; }
- public IEnumerable CharacterFiles =>
- Directory
- .GetFiles(CharactersFolder, "*.txt", SearchOption.TopDirectoryOnly)
- .Select(filePath => Path.GetFileName(filePath));
+ [ObservableProperty]
+ public partial ushort LevelMaxValue { get; set; } = 10;
- private string CharactersFolder =>
- Path.Combine(_globalSettingService.ModRootFolderPath, Keywords.Common, "characters");
+ [ObservableProperty]
+ public partial ushort AttackMaxValue { get; set; }
[ObservableProperty]
- private string _generatedText = string.Empty;
+ public partial ushort Attack { get; set; }
[ObservableProperty]
- private string _selectedCharacterFile = string.Empty;
+ public partial ushort DefenseMaxValue { get; set; }
[ObservableProperty]
- private ushort _levelMaxValue;
+ public partial ushort Defense { get; set; }
- public InlineCollection LevelModifierDescription { get; set; } = null!;
+ [ObservableProperty]
+ public partial ushort PlanningMaxValue { get; set; }
[ObservableProperty]
- private ushort _level;
+ public partial ushort Planning { get; set; }
[ObservableProperty]
- private ushort _attackMaxValue;
+ public partial ushort LogisticsMaxValue { get; set; }
- public InlineCollection AttackModifierDescription { get; set; } = null!;
+ [ObservableProperty]
+ public partial ushort Logistics { get; set; }
[ObservableProperty]
- private ushort _attack;
+ public partial ushort ManeuveringMaxValue { get; set; }
[ObservableProperty]
- private ushort _defenseMaxValue;
+ public partial ushort Maneuvering { get; set; }
- public InlineCollection DefenseModifierDescription { get; set; } = null!;
+ [ObservableProperty]
+ public partial ushort CoordinationMaxValue { get; set; }
[ObservableProperty]
- private ushort _defense;
+ public partial ushort Coordination { get; set; }
[ObservableProperty]
- private ushort _planningMaxValue;
+ public partial string GeneratedText { get; set; } = string.Empty;
- public InlineCollection PlanningModifierDescription { get; set; } = null!;
+ [Required(
+ ErrorMessageResourceType = typeof(Resource),
+ ErrorMessageResourceName = "UIErrorMessage_Required"
+ )]
+ public string Name
+ {
+ get;
+ set => SetProperty(ref field, value, true);
+ } = string.Empty;
[ObservableProperty]
- private ushort _planning;
+ public partial string LocalizedName { get; set; } = string.Empty;
[ObservableProperty]
- private ushort _logisticsMaxValue;
-
- public InlineCollection LogisticsModifierDescription { get; set; } = null!;
+ public partial string ImageKey { get; set; } = string.Empty;
[ObservableProperty]
- private ushort _logistics;
+ public partial string SelectedCharacterFile { get; set; } = string.Empty;
- [ObservableProperty]
- private ushort _maneuveringMaxValue;
+ public InlineCollection LevelModifierDescription { get; } = [];
- public InlineCollection ManeuveringModifierDescription { get; set; } = null!;
+ public InlineCollection AttackModifierDescription { get; } = [];
- [ObservableProperty]
- private ushort _maneuvering;
+ public InlineCollection DefenseModifierDescription { get; } = [];
- [ObservableProperty]
- private ushort _coordinationMaxValue;
+ public InlineCollection PlanningModifierDescription { get; } = [];
- public InlineCollection CoordinationModifierDescription { get; set; } = null!;
+ public InlineCollection LogisticsModifierDescription { get; } = [];
- [ObservableProperty]
- private ushort _coordination;
+ public InlineCollection ManeuveringModifierDescription { get; } = [];
- [ObservableProperty]
- private string _name = string.Empty;
+ public InlineCollection CoordinationModifierDescription { get; } = [];
- [ObservableProperty]
- private string _localizedName = string.Empty;
+ public IEnumerable CharacterFiles =>
+ Directory
+ .GetFiles(CharactersFolder, "*.txt", SearchOption.TopDirectoryOnly)
+ .Select(filePath => Path.GetFileName(filePath));
- [ObservableProperty]
- private int _selectedCharacterTypeIndex;
+ private string CharactersFolder =>
+ Path.Combine(_appSettingService.ModRootFolderPath, Keywords.Common, "characters");
+
+ public CharacterTypeInfo[] CharactersType { get; } =
+ [
+ new("将军 (corps_commander)", "corps_commander"),
+ new("陆军元帅 (field_marshal)", "field_marshal"),
+ new("海军将领 (navy_leader)", "navy_leader")
+ ];
[ObservableProperty]
- private string _imageKey = string.Empty;
- private IEnumerable _selectedTraits = [];
+ [NotifyPropertyChangedFor(nameof(IsSelectedNavy))]
+ public partial CharacterTypeInfo SelectedCharacterType { get; set; }
- private bool IsSelectedNavy => SelectedCharacterTypeCode == "navy_leader";
- private string SelectedCharacterTypeCode =>
- CharactersType[SelectedCharacterTypeIndex].Tag.ToString()
- ?? throw new InvalidOperationException("未设置 CharactersType 的 null");
- private CharacterSkillType SelectedCharacterSkillType =>
- CharacterSkillType.FromCharacterType(SelectedCharacterTypeCode);
+ public bool IsSelectedNavy => SelectedCharacterType.Keyword == "navy_leader";
- private readonly GlobalSettingService _globalSettingService;
- private readonly MessageBoxService _messageBoxService;
+ private IEnumerable _selectedTraits = [];
+ private readonly AppSettingService _appSettingService;
private readonly CharacterSkillService _characterSkillService;
- private readonly GlobalResourceService _globalResourceService;
+ private readonly ModifierDisplayService _modifierDisplayService;
+ private readonly MessageBoxService _messageBoxService;
+ private readonly AppResourcesService _appResourcesService;
+
+ private static readonly Logger Log = LogManager.GetCurrentClassLogger();
+
///
/// 是否已初始化, 防止在初始化期间多次调用生成人物方法
///
private bool _isInitialized;
- private static readonly Logger Log = LogManager.GetCurrentClassLogger();
+ private SkillCharacterType SelectedSkillCharacterType =>
+ SkillCharacterType.FromCharacterType(SelectedCharacterType.Keyword);
public CharacterEditorControlViewModel(
- GlobalSettingService globalSettingService,
- MessageBoxService messageBoxService,
+ AppSettingService appSettingService,
CharacterSkillService characterSkillService,
- GlobalResourceService globalResourceService
+ ModifierDisplayService modifierDisplayService,
+ MessageBoxService messageBoxService,
+ AppResourcesService appResourcesService
)
{
- _globalSettingService = globalSettingService;
- _messageBoxService = messageBoxService;
+ _appSettingService = appSettingService;
_characterSkillService = characterSkillService;
- _globalResourceService = globalResourceService;
+ _modifierDisplayService = modifierDisplayService;
+ _messageBoxService = messageBoxService;
+ _appResourcesService = appResourcesService;
+ SelectedCharacterType = CharactersType[0];
- SetSkillsMaxValue();
_characterSkillService.OnResourceChanged += OnResourceChanged;
+ SetSkillsMaxValue();
+
+ InitializeSkillDefaultValue();
+ _isInitialized = true;
}
private void OnResourceChanged(object? sender, ResourceChangedEventArgs e)
{
- App.Current.DispatcherQueue.TryEnqueue(() =>
+ Dispatcher.UIThread.Post(() =>
{
SetSkillsMaxValue();
ResetSkillsModifierDescription();
});
}
- public void InitializeSkillDefaultValue()
+ private void SetSkillsMaxValue()
{
- Level = 1;
- Attack = 1;
- Defense = 1;
- Planning = 1;
- Logistics = 1;
- Maneuvering = 1;
- Coordination = 1;
+ var type = SelectedSkillCharacterType;
+ LevelMaxValue = _characterSkillService.GetMaxSkillValue(SkillType.Level, type);
+ AttackMaxValue = _characterSkillService.GetMaxSkillValue(SkillType.Attack, type);
+ DefenseMaxValue = _characterSkillService.GetMaxSkillValue(SkillType.Defense, type);
+ if (IsSelectedNavy)
+ {
+ ManeuveringMaxValue = _characterSkillService.GetMaxSkillValue(SkillType.Maneuvering, type);
+ CoordinationMaxValue = _characterSkillService.GetMaxSkillValue(SkillType.Coordination, type);
+ PlanningMaxValue = 1;
+ LogisticsMaxValue = 1;
+ }
+ else
+ {
+ PlanningMaxValue = _characterSkillService.GetMaxSkillValue(SkillType.Planning, type);
+ LogisticsMaxValue = _characterSkillService.GetMaxSkillValue(SkillType.Logistics, type);
+ ManeuveringMaxValue = 1;
+ CoordinationMaxValue = 1;
+ }
+ }
- _isInitialized = true;
+ private void ResetSkillsModifierDescription()
+ {
+ OnLevelChanged(Level);
+ OnAttackChanged(Attack);
+ OnDefenseChanged(Defense);
+ OnPlanningChanged(Planning);
+ OnCoordinationChanged(Coordination);
+ OnLogisticsChanged(Logistics);
+ OnManeuveringChanged(Maneuvering);
}
- partial void OnLevelChanged(ushort value)
+ //BUG: 切换时描述会丢失
+ // 等框架修复或者换个解决方案
+ partial void OnSelectedCharacterTypeChanged(CharacterTypeInfo value)
{
- var description = _characterSkillService.GetSkillModifierDescription(
- SkillType.Level,
- SelectedCharacterSkillType,
- value
- );
+ _appResourcesService.CurrentSelectedCharacterType = SelectedSkillCharacterType;
+ // 这样描述可以正常显示
+ Dispatcher.UIThread.Post(() =>
+ {
+ SetSkillsMaxValue();
+ ResetSkillsModifierDescription();
+ });
+ }
- AddModifierDescription(LevelModifierDescription, description);
+ partial void OnLevelChanged(ushort value)
+ {
+ AddModifierDescription(SkillType.Level, value, LevelModifierDescription);
}
partial void OnAttackChanged(ushort value)
{
- var description = _characterSkillService.GetSkillModifierDescription(
- SkillType.Attack,
- SelectedCharacterSkillType,
- value
- );
-
- AddModifierDescription(AttackModifierDescription, description);
+ AddModifierDescription(SkillType.Attack, value, AttackModifierDescription);
}
partial void OnDefenseChanged(ushort value)
{
- var description = _characterSkillService.GetSkillModifierDescription(
- SkillType.Defense,
- SelectedCharacterSkillType,
- value
- );
-
- AddModifierDescription(DefenseModifierDescription, description);
+ AddModifierDescription(SkillType.Defense, value, DefenseModifierDescription);
}
partial void OnPlanningChanged(ushort value)
{
- var description = _characterSkillService.GetSkillModifierDescription(
- SkillType.Planning,
- SelectedCharacterSkillType,
- value
- );
-
- AddModifierDescription(PlanningModifierDescription, description);
+ AddModifierDescription(SkillType.Planning, value, PlanningModifierDescription);
}
partial void OnLogisticsChanged(ushort value)
{
- var description = _characterSkillService.GetSkillModifierDescription(
- SkillType.Logistics,
- SelectedCharacterSkillType,
- value
- );
-
- AddModifierDescription(LogisticsModifierDescription, description);
+ AddModifierDescription(SkillType.Logistics, value, LogisticsModifierDescription);
}
partial void OnManeuveringChanged(ushort value)
@@ -230,13 +252,7 @@ partial void OnManeuveringChanged(ushort value)
return;
}
- var description = _characterSkillService.GetSkillModifierDescription(
- SkillType.Maneuvering,
- SelectedCharacterSkillType,
- value
- );
-
- AddModifierDescription(ManeuveringModifierDescription, description);
+ AddModifierDescription(SkillType.Maneuvering, value, ManeuveringModifierDescription);
}
partial void OnCoordinationChanged(ushort value)
@@ -246,73 +262,63 @@ partial void OnCoordinationChanged(ushort value)
return;
}
- var description = _characterSkillService.GetSkillModifierDescription(
- SkillType.Coordination,
- SelectedCharacterSkillType,
- value
- );
-
- AddModifierDescription(CoordinationModifierDescription, description);
+ AddModifierDescription(SkillType.Coordination, value, CoordinationModifierDescription);
}
- private static void AddModifierDescription(InlineCollection collection, IEnumerable inlines)
+ private void AddModifierDescription(SkillType skillType, ushort value, InlineCollection? collection)
{
- collection.Clear();
- try
- {
- foreach (var inline in inlines)
- {
- collection.Add(inline);
- }
- }
- catch (COMException e)
+ if (collection is null)
{
- Log.Error(e);
+ Log.Warn("技能: {Type} 修饰符 InlineCollection 未设置", skillType);
+ return;
}
- }
- partial void OnSelectedCharacterTypeIndexChanged(int value)
- {
- SetSkillsMaxValue();
- ResetSkillsModifierDescription();
+ var descriptions = _modifierDisplayService.GetSkillModifierDescription(
+ skillType,
+ SelectedSkillCharacterType,
+ value
+ );
+
+ collection.Clear();
+ collection.AddRange(descriptions);
}
- private void SetSkillsMaxValue()
+ private void InitializeSkillDefaultValue()
{
- var type = SelectedCharacterSkillType;
- LevelMaxValue = _characterSkillService.GetMaxSkillValue(SkillType.Level, type);
- AttackMaxValue = _characterSkillService.GetMaxSkillValue(SkillType.Attack, type);
- DefenseMaxValue = _characterSkillService.GetMaxSkillValue(SkillType.Defense, type);
- if (IsSelectedNavy)
- {
- ManeuveringMaxValue = _characterSkillService.GetMaxSkillValue(SkillType.Maneuvering, type);
- CoordinationMaxValue = _characterSkillService.GetMaxSkillValue(SkillType.Coordination, type);
- PlanningMaxValue = 1;
- LogisticsMaxValue = 1;
- }
- else
- {
- PlanningMaxValue = _characterSkillService.GetMaxSkillValue(SkillType.Planning, type);
- LogisticsMaxValue = _characterSkillService.GetMaxSkillValue(SkillType.Logistics, type);
- ManeuveringMaxValue = 1;
- CoordinationMaxValue = 1;
- }
+ Level = 1;
+ Attack = 1;
+ Defense = 1;
+ Planning = 1;
+ Logistics = 1;
+ Maneuvering = 1;
+ Coordination = 1;
+
+ _isInitialized = true;
+
+ Debug.Assert(
+ LevelModifierDescription is not null
+ && AttackModifierDescription is not null
+ && CoordinationModifierDescription is not null
+ );
}
- private void ResetSkillsModifierDescription()
+ [RelayCommand]
+ private async Task OpenTraitsSelectionWindow()
{
- OnLevelChanged(Level);
- OnAttackChanged(Attack);
- OnDefenseChanged(Defense);
- OnPlanningChanged(Planning);
- OnCoordinationChanged(Coordination);
- OnLogisticsChanged(Logistics);
- OnManeuveringChanged(Maneuvering);
+ using var window = new TraitSelectionWindowView();
+ window.SyncSelectedTraits(_selectedTraits);
+ var lifetime = (IClassicDesktopStyleApplicationLifetime?)App.Current.ApplicationLifetime;
+ Debug.Assert(lifetime?.MainWindow is not null);
+
+ await window.ShowDialog(lifetime.MainWindow);
+ _selectedTraits = window.SelectedTraits;
+ RefreshGeneratedText();
}
[RelayCommand]
private async Task SaveAsync()
{
+ ValidateAllProperties();
if (string.IsNullOrEmpty(SelectedCharacterFile))
{
await _messageBoxService.WarnAsync(Resource.CharacterEditor_MissingCharacterFileNameTip);
@@ -321,7 +327,7 @@ private async Task SaveAsync()
if (string.IsNullOrEmpty(Name))
{
- await _messageBoxService.WarnAsync(Resource.CharacterEditor_MissingRequiredInfoTip);
+ await _messageBoxService.WarnAsync(Resource.UIErrorMessage_MissingRequiredInfoTip);
return;
}
@@ -368,6 +374,7 @@ await _messageBoxService.ErrorAsync(
Log.Info("保存成功");
}
+ // TODO: 生成的代码应该加一些加成的注释和特质的本地化名称?
private Node GetGeneratedCharacterNode()
{
var newCharacterNode = Node.Create(Name);
@@ -375,7 +382,7 @@ private Node GetGeneratedCharacterNode()
AddCharacterImage(newCharacterNode);
- var characterTypeNode = newCharacterNode.AddNodeChild(SelectedCharacterTypeCode);
+ var characterTypeNode = newCharacterNode.AddNodeChild(SelectedCharacterType.Keyword);
characterTypeNode.AllArray = GetCharacterSkills();
AddTraits(characterTypeNode);
@@ -427,39 +434,24 @@ private Child[] GetCharacterSkills()
return array;
}
- [RelayCommand]
- private async Task OpenTraitsSelectionWindow()
+ public void Close()
{
- _globalResourceService.CurrentSelectSelectSkillType = SelectedCharacterSkillType;
-
- using var window = new TraitsSelectionWindowView();
- var dialog = new ContentDialog
- {
- XamlRoot = App.Current.XamlRoot,
- Content = window,
- DefaultButton = ContentDialogButton.Primary,
- PrimaryButtonText = Resource.Common_Ok,
- CloseButtonText = Resource.Common_Close
- };
-
- window.ViewModel.SyncSelectedTraits(_selectedTraits);
- var result = await dialog.ShowAsync();
- if (result == ContentDialogResult.Primary)
- {
- var selectedTraits = window.ViewModel.Traits.Cast().Where(trait => trait.IsSelected);
- _selectedTraits = selectedTraits;
- OnPropertyChanged(nameof(_selectedTraits));
- }
+ _characterSkillService.OnResourceChanged -= OnResourceChanged;
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
- base.OnPropertyChanged(e);
-
if (e.PropertyName != nameof(GeneratedText) && _isInitialized)
{
- GeneratedText = GetGeneratedText();
+ RefreshGeneratedText();
}
+
+ base.OnPropertyChanged(e);
+ }
+
+ private void RefreshGeneratedText()
+ {
+ GeneratedText = GetGeneratedText();
}
private string GetGeneratedText()
@@ -471,9 +463,4 @@ private string GetGeneratedText()
return GetGeneratedCharacterNode().PrintRaw();
}
-
- public void Close()
- {
- _characterSkillService.OnResourceChanged -= OnResourceChanged;
- }
}
diff --git a/Moder.Core/ViewsModel/Game/TraitSelectionWindowViewModel.cs b/Moder.Core/ViewsModel/Game/TraitSelectionWindowViewModel.cs
new file mode 100644
index 0000000..fb055d5
--- /dev/null
+++ b/Moder.Core/ViewsModel/Game/TraitSelectionWindowViewModel.cs
@@ -0,0 +1,203 @@
+using System.Collections.ObjectModel;
+using System.Reactive.Linq;
+using Avalonia.Controls.Documents;
+using Avalonia.Threading;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using DynamicData;
+using DynamicData.Binding;
+using EnumsNET;
+using Moder.Core.Infrastructure;
+using Moder.Core.Models.Game.Character;
+using Moder.Core.Models.Game.Modifiers;
+using Moder.Core.Models.Vo;
+using Moder.Core.Services;
+using Moder.Core.Services.GameResources;
+using Moder.Core.Services.GameResources.Modifiers;
+using NLog;
+
+namespace Moder.Core.ViewsModel.Game;
+
+public sealed partial class TraitSelectionWindowViewModel : ObservableObject, IDisposable
+{
+ [ObservableProperty]
+ public partial string SearchText { get; set; } = string.Empty;
+ public ReadOnlyObservableCollection Traits { get; }
+ public IEnumerable SelectedTraits => _traits.Items.Where(x => x.IsSelected);
+ private readonly SourceList _traits = new();
+ public InlineCollection TraitsModifierDescription { get; } = [];
+
+ private readonly AppResourcesService _appResourcesService;
+ private readonly ModifierDisplayService _modifierDisplayService;
+ private readonly ModifierMergeManager _modifierMergeManager = new();
+ private readonly ModifierService _modifierService;
+ private readonly IDisposable _cleanUp;
+
+ private static readonly Logger Log = LogManager.GetCurrentClassLogger();
+
+ public TraitSelectionWindowViewModel(
+ CharacterTraitsService characterTraitsService,
+ AppResourcesService appResourcesService,
+ ModifierDisplayService modifierDisplayService,
+ ModifierService modifierService
+ )
+ {
+ _appResourcesService = appResourcesService;
+ _modifierDisplayService = modifierDisplayService;
+ _modifierService = modifierService;
+
+ _traits.AddRange(
+ characterTraitsService
+ .GetAllTraits()
+ .Where(FilterTraitsByCharacterType)
+ .Select(trait => new TraitVo(trait, characterTraitsService.GetLocalizationName(trait)))
+ );
+
+ _cleanUp = _traits
+ .Connect()
+ .AutoRefreshOnObservable(_ =>
+ this.WhenValueChanged(vm => vm.SearchText).Throttle(TimeSpan.FromMilliseconds(160))
+ )
+ .Sort(TraitVo.Comparer.Default)
+ .Filter(FilterTraitsBySearchText)
+ .Bind(out var traits)
+ .Subscribe();
+ Traits = traits;
+ }
+
+ private bool FilterTraitsBySearchText(TraitVo traitVo)
+ {
+ if (string.IsNullOrEmpty(SearchText))
+ {
+ return true;
+ }
+
+ if (
+ traitVo.Trait.AllModifiers.Any(modifier =>
+ {
+ if (modifier.Key.Contains(SearchText))
+ {
+ return true;
+ }
+
+ if (IsContainsSearchTextInLocalizationModifierName(modifier))
+ {
+ return true;
+ }
+
+ if (modifier is NodeModifier nodeModifier)
+ {
+ return nodeModifier.Modifiers.Any(IsContainsSearchTextInLocalizationModifierName);
+ }
+
+ return false;
+ })
+ )
+ {
+ return true;
+ }
+
+ return traitVo.LocalisationName.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
+ || traitVo.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private bool IsContainsSearchTextInLocalizationModifierName(IModifier modifier)
+ {
+ return _modifierService.TryGetLocalizationName(modifier.Key, out var modifierName)
+ && modifierName.Contains(SearchText, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private bool FilterTraitsByCharacterType(Trait trait)
+ {
+ if (_appResourcesService.CurrentSelectedCharacterType == SkillCharacterType.Navy)
+ {
+ if (trait.Type.HasAnyFlags(TraitType.Navy))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ if (trait.Type == TraitType.Navy)
+ {
+ return false;
+ }
+
+ // TODO: 暂不支持间谍
+ if (trait.Type == TraitType.Operative)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ [RelayCommand]
+ private void ClearTraits()
+ {
+ foreach (var trait in _traits.Items)
+ {
+ trait.IsSelected = false;
+ }
+ TraitsModifierDescription.Clear();
+ _modifierMergeManager.Clear();
+ }
+
+ public void UpdateModifiersDescriptionOnAdd(TraitVo traitVo)
+ {
+ _modifierMergeManager.AddRange(traitVo.Trait.AllModifiers);
+ UpdateModifiersDescriptionCore();
+ }
+
+ public void UpdateModifiersDescriptionOnRemove(TraitVo traitVo)
+ {
+ _modifierMergeManager.RemoveAll(traitVo.Trait.AllModifiers);
+ UpdateModifiersDescriptionCore();
+ }
+
+ private void UpdateModifiersDescriptionCore()
+ {
+ var mergedModifiers = _modifierMergeManager.GetMergedModifiers();
+ var addedModifiers = _modifierDisplayService.GetDescription(mergedModifiers);
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ TraitsModifierDescription.Clear();
+ TraitsModifierDescription.AddRange(addedModifiers);
+ });
+ }
+
+ public void SyncSelectedTraits(IEnumerable selectedTraits)
+ {
+ // 因为有可能因为特质文件改变导致选择的特质数量发生变化,因此这里需要过滤一下
+ var selectedTraitNames = selectedTraits.Select(trait => trait.Name).ToHashSet();
+ if (selectedTraitNames.Count == 0)
+ {
+ return;
+ }
+
+ var traitVos = new List(8);
+ foreach (var trait in _traits.Items)
+ {
+ if (selectedTraitNames.Contains(trait.Name))
+ {
+ trait.IsSelected = true;
+ traitVos.Add(trait);
+ }
+ }
+ UpdateModifiersDescriptionOnAdd(traitVos);
+ }
+
+ private void UpdateModifiersDescriptionOnAdd(IEnumerable traitVos)
+ {
+ _modifierMergeManager.AddRange(traitVos.SelectMany(traitVo => traitVo.Trait.AllModifiers));
+ UpdateModifiersDescriptionCore();
+ }
+
+ public void Dispose()
+ {
+ _traits.Dispose();
+ _cleanUp.Dispose();
+ }
+}
diff --git a/Moder.Core/ViewsModel/MainWindowViewModel.cs b/Moder.Core/ViewsModel/MainWindowViewModel.cs
new file mode 100644
index 0000000..7b36688
--- /dev/null
+++ b/Moder.Core/ViewsModel/MainWindowViewModel.cs
@@ -0,0 +1,25 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.Extensions.DependencyInjection;
+using Moder.Core.Services;
+using Moder.Core.Views.Game;
+using Moder.Core.Views.Menus;
+
+namespace Moder.Core.ViewsModel;
+
+public sealed partial class MainWindowViewModel : ObservableObject
+{
+ [RelayCommand]
+ private void OpenCharacterEditor()
+ {
+ var tabview = App.Services.GetRequiredService