diff --git a/src/MaaWpfGui/App.xaml.cs b/src/MaaWpfGui/App.xaml.cs index 7051957dec..88945248a5 100644 --- a/src/MaaWpfGui/App.xaml.cs +++ b/src/MaaWpfGui/App.xaml.cs @@ -18,37 +18,36 @@ using MaaWpfGui.WineCompat; using MaaWpfGui.WineCompat.FontConfig; using Serilog; -namespace MaaWpfGui +namespace MaaWpfGui; + +/// +/// App.xaml 的交互逻辑 +/// +public partial class App : Application { - /// - /// App.xaml 的交互逻辑 - /// - public partial class App : Application + private static readonly ILogger _logger = Log.ForContext(); + + public void Hyperlink_Click(object sender, RoutedEventArgs e) { - private static readonly ILogger _logger = Log.ForContext(); - - public void Hyperlink_Click(object sender, RoutedEventArgs e) + Hyperlink link = sender as Hyperlink; + if (!string.IsNullOrEmpty(link?.NavigateUri?.AbsoluteUri)) { - Hyperlink link = sender as Hyperlink; - if (!string.IsNullOrEmpty(link?.NavigateUri?.AbsoluteUri)) + Process.Start(new ProcessStartInfo { - Process.Start(new ProcessStartInfo - { - FileName = link.NavigateUri.AbsoluteUri, - UseShellExecute = true, - }); - } - } - - protected override void OnStartup(StartupEventArgs e) - { - if (WineRuntimeInformation.IsRunningUnderWine && MaaDesktopIntegration.Available) - { - // override buintin font map as early as possible - FontConfigIntegration.Install(); - } - - base.OnStartup(e); + FileName = link.NavigateUri.AbsoluteUri, + UseShellExecute = true, + }); } } + + protected override void OnStartup(StartupEventArgs e) + { + if (WineRuntimeInformation.IsRunningUnderWine && MaaDesktopIntegration.Available) + { + // override buintin font map as early as possible + FontConfigIntegration.Install(); + } + + base.OnStartup(e); + } } diff --git a/src/MaaWpfGui/Configuration/Factory/PropertyChangedEventDetailArgs.cs b/src/MaaWpfGui/Configuration/Factory/PropertyChangedEventDetailArgs.cs index 0f4fabc499..c137c3ad21 100644 --- a/src/MaaWpfGui/Configuration/Factory/PropertyChangedEventDetailArgs.cs +++ b/src/MaaWpfGui/Configuration/Factory/PropertyChangedEventDetailArgs.cs @@ -14,21 +14,20 @@ #nullable enable using System.ComponentModel; -namespace MaaWpfGui.Configuration.Factory +namespace MaaWpfGui.Configuration.Factory; + +// https://github.com/dotnet/runtime/issues/27252 +public class PropertyChangedEventDetailArgs : PropertyChangedEventArgs { - // https://github.com/dotnet/runtime/issues/27252 - public class PropertyChangedEventDetailArgs : PropertyChangedEventArgs + public PropertyChangedEventDetailArgs(string propertyName, object oldValue, object newValue) + : base(propertyName) { - public PropertyChangedEventDetailArgs(string propertyName, object oldValue, object newValue) - : base(propertyName) - { - OldValue = oldValue; - NewValue = newValue; - } - - // ReSharper disable once UnusedAutoPropertyAccessor.Global - public object OldValue { get; private set; } - - public object NewValue { get; private set; } + OldValue = oldValue; + NewValue = newValue; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Global + public object OldValue { get; private set; } + + public object NewValue { get; private set; } } diff --git a/src/MaaWpfGui/Configuration/Global/Timer.cs b/src/MaaWpfGui/Configuration/Global/Timer.cs index 90ebbcd5a4..4161ab11c4 100644 --- a/src/MaaWpfGui/Configuration/Global/Timer.cs +++ b/src/MaaWpfGui/Configuration/Global/Timer.cs @@ -14,37 +14,36 @@ using System.Text.Json.Serialization; using MaaWpfGui.Configuration.Factory; -namespace MaaWpfGui.Configuration.Global +namespace MaaWpfGui.Configuration.Global; + +public class Timer { - public class Timer + [JsonConstructor] + public Timer(bool enable, string config, int hour, int minute) { - [JsonConstructor] - public Timer(bool enable, string config, int hour, int minute) - { - Enable = enable; - Config = config; - Hour = hour; - Minute = minute; - } - - public Timer() - { - Enable = false; - Config = ConfigFactory.Root.Current; - Hour = 0; - Minute = 0; - } - - [JsonInclude] - public bool Enable { get; set; } - - [JsonInclude] - public string Config { get; set; } - - [JsonInclude] - public int Hour { get; set; } - - [JsonInclude] - public int Minute { get; set; } + Enable = enable; + Config = config; + Hour = hour; + Minute = minute; } + + public Timer() + { + Enable = false; + Config = ConfigFactory.Root.Current; + Hour = 0; + Minute = 0; + } + + [JsonInclude] + public bool Enable { get; set; } + + [JsonInclude] + public string Config { get; set; } + + [JsonInclude] + public int Hour { get; set; } + + [JsonInclude] + public int Minute { get; set; } } diff --git a/src/MaaWpfGui/Constants/AchievementIds.cs b/src/MaaWpfGui/Constants/AchievementIds.cs index badcb950de..7282bac16f 100644 --- a/src/MaaWpfGui/Constants/AchievementIds.cs +++ b/src/MaaWpfGui/Constants/AchievementIds.cs @@ -11,141 +11,140 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Constants +namespace MaaWpfGui.Constants; + +public static class AchievementIds { - public static class AchievementIds - { - #region 基础使用类 + #region 基础使用类 - public const string SanitySpenderGroup = "SanitySpender"; - public const string SanitySpender1 = "SanitySpender1"; - public const string SanitySpender2 = "SanitySpender2"; - public const string SanitySpender3 = "SanitySpender3"; + public const string SanitySpenderGroup = "SanitySpender"; + public const string SanitySpender1 = "SanitySpender1"; + public const string SanitySpender2 = "SanitySpender2"; + public const string SanitySpender3 = "SanitySpender3"; - public const string SanitySaverGroup = "SanitySaver"; - public const string SanitySaver1 = "SanitySaver1"; - public const string SanitySaver2 = "SanitySaver2"; - public const string SanitySaver3 = "SanitySaver3"; + public const string SanitySaverGroup = "SanitySaver"; + public const string SanitySaver1 = "SanitySaver1"; + public const string SanitySaver2 = "SanitySaver2"; + public const string SanitySaver3 = "SanitySaver3"; - public const string RoguelikeGamePassGroup = "RoguelikeGamePass"; - public const string RoguelikeGamePass1 = "RoguelikeGamePass1"; - public const string RoguelikeGamePass2 = "RoguelikeGamePass2"; - public const string RoguelikeGamePass3 = "RoguelikeGamePass3"; + public const string RoguelikeGamePassGroup = "RoguelikeGamePass"; + public const string RoguelikeGamePass1 = "RoguelikeGamePass1"; + public const string RoguelikeGamePass2 = "RoguelikeGamePass2"; + public const string RoguelikeGamePass3 = "RoguelikeGamePass3"; - public const string RoguelikeGroup = "Roguelike"; - public const string RoguelikeRetreat = "RoguelikeRetreat"; - public const string RoguelikeGoldMax = "RoguelikeGoldMax"; + public const string RoguelikeGroup = "Roguelike"; + public const string RoguelikeRetreat = "RoguelikeRetreat"; + public const string RoguelikeGoldMax = "RoguelikeGoldMax"; - public const string RoguelikeNGroup = "RoguelikeN"; - public const string RoguelikeN04 = "RoguelikeN04"; - public const string RoguelikeN08 = "RoguelikeN08"; - public const string RoguelikeN12 = "RoguelikeN12"; - public const string RoguelikeN15 = "RoguelikeN15"; + public const string RoguelikeNGroup = "RoguelikeN"; + public const string RoguelikeN04 = "RoguelikeN04"; + public const string RoguelikeN08 = "RoguelikeN08"; + public const string RoguelikeN12 = "RoguelikeN12"; + public const string RoguelikeN15 = "RoguelikeN15"; - public const string FirstLaunch = "FirstLaunch"; - public const string SanityExpire = "SanityExpire"; - public const string OverLimitAgent = "OverLimitAgent"; + public const string FirstLaunch = "FirstLaunch"; + public const string SanityExpire = "SanityExpire"; + public const string OverLimitAgent = "OverLimitAgent"; - #endregion + #endregion - #region 功能探索类 + #region 功能探索类 - public const string ScheduleMasterGroup = "ScheduleMaster"; - public const string ScheduleMaster1 = "ScheduleMaster1"; - public const string ScheduleMaster2 = "ScheduleMaster2"; + public const string ScheduleMasterGroup = "ScheduleMaster"; + public const string ScheduleMaster1 = "ScheduleMaster1"; + public const string ScheduleMaster2 = "ScheduleMaster2"; - public const string MirrorChyanGroup = "MirrorChyan"; - public const string MirrorChyanFirstUse = "MirrorChyanFirstUse"; - public const string MirrorChyanCdkError = "MirrorChyanCdkError"; + public const string MirrorChyanGroup = "MirrorChyan"; + public const string MirrorChyanFirstUse = "MirrorChyanFirstUse"; + public const string MirrorChyanCdkError = "MirrorChyanCdkError"; - public const string MosquitoLeg = "MosquitoLeg"; + public const string MosquitoLeg = "MosquitoLeg"; - public const string PioneerGroup = "Pioneer"; - public const string Pioneer1 = "Pioneer1"; - public const string Pioneer2 = "Pioneer2"; - public const string Pioneer3 = "Pioneer3"; + public const string PioneerGroup = "Pioneer"; + public const string Pioneer1 = "Pioneer1"; + public const string Pioneer2 = "Pioneer2"; + public const string Pioneer3 = "Pioneer3"; - public const string RealGacha = "RealGacha"; - public const string PeekScreen = "PeekScreen"; - public const string CustomizationMaster = "CustomizationMaster"; + public const string RealGacha = "RealGacha"; + public const string PeekScreen = "PeekScreen"; + public const string CustomizationMaster = "CustomizationMaster"; - #endregion + #endregion - #region 代理关卡类 + #region 代理关卡类 - public const string MapOutdated = "MapOutdated"; + public const string MapOutdated = "MapOutdated"; - public const string UseCopilotGroup = "UseCopilot"; - public const string UseCopilot1 = "UseCopilot1"; - public const string UseCopilot2 = "UseCopilot2"; - public const string UseCopilot3 = "UseCopilot3"; - public const string CopilotLikeGroup = "CopilotLikeGiven"; - public const string CopilotLikeGiven1 = "CopilotLikeGiven1"; - public const string CopilotLikeGiven2 = "CopilotLikeGiven2"; - public const string CopilotLikeGiven3 = "CopilotLikeGiven3"; - public const string CopilotError = "CopilotError"; + public const string UseCopilotGroup = "UseCopilot"; + public const string UseCopilot1 = "UseCopilot1"; + public const string UseCopilot2 = "UseCopilot2"; + public const string UseCopilot3 = "UseCopilot3"; + public const string CopilotLikeGroup = "CopilotLikeGiven"; + public const string CopilotLikeGiven1 = "CopilotLikeGiven1"; + public const string CopilotLikeGiven2 = "CopilotLikeGiven2"; + public const string CopilotLikeGiven3 = "CopilotLikeGiven3"; + public const string CopilotError = "CopilotError"; - public const string Irreplaceable = "Irreplaceable"; + public const string Irreplaceable = "Irreplaceable"; - #endregion + #endregion - #region 搞笑/梗成就 + #region 搞笑/梗成就 - public const string QuickCloser = "QuickCloser"; - public const string TacticalRetreat = "TacticalRetreat"; - public const string Martian = "Martian"; + public const string QuickCloser = "QuickCloser"; + public const string TacticalRetreat = "TacticalRetreat"; + public const string Martian = "Martian"; - public const string RecruitGroup = "Recruit"; - public const string RecruitNoSixStar = "RecruitNoSixStar"; - public const string RecruitNoSixStarStreak = "RecruitNoSixStarStreak"; + public const string RecruitGroup = "Recruit"; + public const string RecruitNoSixStar = "RecruitNoSixStar"; + public const string RecruitNoSixStarStreak = "RecruitNoSixStarStreak"; - public const string SnapshotChallengeGroup = "SnapshotChallenge"; // 累计型成就 - public const string SnapshotChallenge1 = "SnapshotChallenge1"; // 999 - public const string SnapshotChallenge2 = "SnapshotChallenge2"; // 800 - public const string SnapshotChallenge3 = "SnapshotChallenge3"; // 400 - public const string SnapshotChallenge4 = "SnapshotChallenge4"; // 100 - public const string SnapshotChallenge5 = "SnapshotChallenge5"; // 10 - public const string SnapshotChallenge6 = "SnapshotChallenge6"; // 5 + public const string SnapshotChallengeGroup = "SnapshotChallenge"; // 累计型成就 + public const string SnapshotChallenge1 = "SnapshotChallenge1"; // 999 + public const string SnapshotChallenge2 = "SnapshotChallenge2"; // 800 + public const string SnapshotChallenge3 = "SnapshotChallenge3"; // 400 + public const string SnapshotChallenge4 = "SnapshotChallenge4"; // 100 + public const string SnapshotChallenge5 = "SnapshotChallenge5"; // 10 + public const string SnapshotChallenge6 = "SnapshotChallenge6"; // 5 - #endregion + #endregion - #region BUG 相关 + #region BUG 相关 - public const string CongratulationError = "CongratulationError"; - public const string UnexpectedCrash = "UnexpectedCrash"; - public const string ProblemFeedback = "ProblemFeedback"; - public const string CdnTorture = "CdnTorture"; + public const string CongratulationError = "CongratulationError"; + public const string UnexpectedCrash = "UnexpectedCrash"; + public const string ProblemFeedback = "ProblemFeedback"; + public const string CdnTorture = "CdnTorture"; - #endregion + #endregion - #region 用户行为习惯类 + #region 用户行为习惯类 - public const string MissionStartCount = "MissionStartCount"; - public const string MissionStartCountCustomDataKey = "LastStartDate"; - public const string LongTaskTimeout = "LongTaskTimeout"; - public const string ProxyOnline3Hours = "ProxyOnline3Hours"; - public const string TaskStartCancel = "TaskStartCancel"; - public const string AfkWatcher = "AfkWatcher"; + public const string MissionStartCount = "MissionStartCount"; + public const string MissionStartCountCustomDataKey = "LastStartDate"; + public const string LongTaskTimeout = "LongTaskTimeout"; + public const string ProxyOnline3Hours = "ProxyOnline3Hours"; + public const string TaskStartCancel = "TaskStartCancel"; + public const string AfkWatcher = "AfkWatcher"; - public const string UseDailyGroup = "UseDaily"; - public const string UseDailyCustomDataKey = "LastLaunchDate"; - public const string UseDaily1 = "UseDaily1"; // 7 天 - public const string UseDaily2 = "UseDaily2"; // 30 天 - public const string UseDaily3 = "UseDaily3"; // 365 天 + public const string UseDailyGroup = "UseDaily"; + public const string UseDailyCustomDataKey = "LastLaunchDate"; + public const string UseDaily1 = "UseDaily1"; // 7 天 + public const string UseDaily2 = "UseDaily2"; // 30 天 + public const string UseDaily3 = "UseDaily3"; // 365 天 - #endregion + #endregion - #region 彩蛋类 + #region 彩蛋类 - public const string Rules = "Rules"; - public const string VersionClick = "VersionClick"; - public const string Lucky = "Lucky"; + public const string Rules = "Rules"; + public const string VersionClick = "VersionClick"; + public const string Lucky = "Lucky"; - public const string LoginGroup = "Login"; - public const string AprilFools = "AprilFools"; - public const string MidnightLaunch = "MidnightLaunch"; - public const string LunarNewYear = "LunarNewYear"; + public const string LoginGroup = "Login"; + public const string AprilFools = "AprilFools"; + public const string MidnightLaunch = "MidnightLaunch"; + public const string LunarNewYear = "LunarNewYear"; - #endregion - } + #endregion } diff --git a/src/MaaWpfGui/Constants/ConfigurationKeys.cs b/src/MaaWpfGui/Constants/ConfigurationKeys.cs index 96888268bf..a8de457062 100644 --- a/src/MaaWpfGui/Constants/ConfigurationKeys.cs +++ b/src/MaaWpfGui/Constants/ConfigurationKeys.cs @@ -13,327 +13,326 @@ using JetBrains.Annotations; -namespace MaaWpfGui.Constants +namespace MaaWpfGui.Constants; + +/// +/// MaaWpfGui configuration keys +/// +public static class ConfigurationKeys { - /// - /// MaaWpfGui configuration keys - /// - public static class ConfigurationKeys - { - public const string CurrentConfiguration = "Current"; - public const string DefaultConfiguration = "Default"; - public const string GlobalConfiguration = "Global"; - public const string ConfigurationMap = "Configurations"; + public const string CurrentConfiguration = "Current"; + public const string DefaultConfiguration = "Default"; + public const string GlobalConfiguration = "Global"; + public const string ConfigurationMap = "Configurations"; - [UsedImplicitly] - public const string ConfigurationData = "Data"; + [UsedImplicitly] + public const string ConfigurationData = "Data"; - [UsedImplicitly] - public const string ConfigurationCron = "Cron"; + [UsedImplicitly] + public const string ConfigurationCron = "Cron"; - public const string Localization = "GUI.Localization"; - public const string OperNameLanguage = "GUI.OperNameLanguage"; - public const string UseTray = "GUI.UseTray"; - public const string MinimizeToTray = "GUI.MinimizeToTray"; - public const string HideCloseButton = "GUI.HideCloseButton"; - public const string WindowTitleScrollable = "GUI.WindowTitleScrollable"; + public const string Localization = "GUI.Localization"; + public const string OperNameLanguage = "GUI.OperNameLanguage"; + public const string UseTray = "GUI.UseTray"; + public const string MinimizeToTray = "GUI.MinimizeToTray"; + public const string HideCloseButton = "GUI.HideCloseButton"; + public const string WindowTitleScrollable = "GUI.WindowTitleScrollable"; - // public const string UseLogItemDateFormat = "GUI.UseLogItemDateFormat"; 是否使用自定义的时间格式 - public const string LogItemDateFormat = "GUI.LogItemDateFormatString"; - public const string WindowPlacement = "GUI.Placement"; - public const string LoadWindowPlacement = "GUI.Placement.Load"; - public const string SaveWindowPlacement = "GUI.Placement.SaveOnClosing"; - public const string UseAlternateStage = "GUI.UseAlternateStage"; - public const string AllowUseStoneSave = "GUI.AllowUseStoneSave"; - public const string HideUnavailableStage = "GUI.HideUnavailableStage"; - public const string HideSeries = "GUI.HideSeries"; - public const string CustomStageCode = "GUI.CustomStageCode"; - public const string InverseClearMode = "GUI.InverseClearMode"; - public const string WindowTitlePrefix = "GUI.WindowTitlePrefix"; - public const string WindowTitleSelectShowList = "GUI.WindowTitleSelectShowList"; - public const string SoberLanguage = "GUI.SoberLanguage"; - public const string Cheers = "GUI.Cheers"; - public const string Hangover = "GUI.Hangover"; - public const string LastBuyWineTime = "GUI.LastBuyWineTime"; - public const string CustomCulture = "GUI.CustomCulture"; + // public const string UseLogItemDateFormat = "GUI.UseLogItemDateFormat"; 是否使用自定义的时间格式 + public const string LogItemDateFormat = "GUI.LogItemDateFormatString"; + public const string WindowPlacement = "GUI.Placement"; + public const string LoadWindowPlacement = "GUI.Placement.Load"; + public const string SaveWindowPlacement = "GUI.Placement.SaveOnClosing"; + public const string UseAlternateStage = "GUI.UseAlternateStage"; + public const string AllowUseStoneSave = "GUI.AllowUseStoneSave"; + public const string HideUnavailableStage = "GUI.HideUnavailableStage"; + public const string HideSeries = "GUI.HideSeries"; + public const string CustomStageCode = "GUI.CustomStageCode"; + public const string InverseClearMode = "GUI.InverseClearMode"; + public const string WindowTitlePrefix = "GUI.WindowTitlePrefix"; + public const string WindowTitleSelectShowList = "GUI.WindowTitleSelectShowList"; + public const string SoberLanguage = "GUI.SoberLanguage"; + public const string Cheers = "GUI.Cheers"; + public const string Hangover = "GUI.Hangover"; + public const string LastBuyWineTime = "GUI.LastBuyWineTime"; + public const string CustomCulture = "GUI.CustomCulture"; - public const string BackgroundImagePath = "GUI.Background.ImagePath"; - public const string BackgroundImageStretchMode = "GUI.Background.StretchMode"; - public const string BackgroundOpacity = "GUI.Background.Opacity"; - public const string BackgroundBlurEffectRadius = "GUI.Background.BlurEffectRadius"; + public const string BackgroundImagePath = "GUI.Background.ImagePath"; + public const string BackgroundImageStretchMode = "GUI.Background.StretchMode"; + public const string BackgroundOpacity = "GUI.Background.Opacity"; + public const string BackgroundBlurEffectRadius = "GUI.Background.BlurEffectRadius"; - public const string AddressHistory = "Connect.AddressHistory"; - public const string AutoDetect = "Connect.AutoDetect"; - public const string AlwaysAutoDetect = "Connect.AlwaysAutoDetect"; - public const string MumuBridgeConnection = "Connect.MumuBridgeConnection"; - public const string ConnectAddress = "Connect.Address"; - public const string AdbPath = "Connect.AdbPath"; - public const string ConnectConfig = "Connect.ConnectConfig"; - public const string MuMu12ExtrasEnabled = "Connect.MuMu12Extras.Enabled"; - public const string MuMu12EmulatorPath = "Connect.MuMu12EmulatorPath"; - public const string MuMu12Index = "Connect.MuMu12Index"; - public const string MuMu12Display = "Connect.MuMu12Display"; - public const string LdPlayerExtrasEnabled = "Connect.LdPlayerExtras.Enabled"; - public const string LdPlayerEmulatorPath = "Connect.LdPlayerEmulatorPath"; - public const string LdPlayerManualSetIndex = "Connect.LdPlayerManualSetIndex"; - public const string LdPlayerIndex = "Connect.LdPlayerIndex"; - public const string RetryOnAdbDisconnected = "Connect.RetryOnDisconnected"; - public const string AllowAdbRestart = "Connect.AllowADBRestart"; - public const string AllowAdbHardRestart = "Connect.AllowADBHardRestart"; - public const string AdbLiteEnabled = "Connect.AdbLiteEnabled"; - public const string KillAdbOnExit = "Connect.KillAdbOnExit"; - public const string TouchMode = "Connect.TouchMode"; - public const string AdbReplaced = "Connect.AdbReplaced"; + public const string AddressHistory = "Connect.AddressHistory"; + public const string AutoDetect = "Connect.AutoDetect"; + public const string AlwaysAutoDetect = "Connect.AlwaysAutoDetect"; + public const string MumuBridgeConnection = "Connect.MumuBridgeConnection"; + public const string ConnectAddress = "Connect.Address"; + public const string AdbPath = "Connect.AdbPath"; + public const string ConnectConfig = "Connect.ConnectConfig"; + public const string MuMu12ExtrasEnabled = "Connect.MuMu12Extras.Enabled"; + public const string MuMu12EmulatorPath = "Connect.MuMu12EmulatorPath"; + public const string MuMu12Index = "Connect.MuMu12Index"; + public const string MuMu12Display = "Connect.MuMu12Display"; + public const string LdPlayerExtrasEnabled = "Connect.LdPlayerExtras.Enabled"; + public const string LdPlayerEmulatorPath = "Connect.LdPlayerEmulatorPath"; + public const string LdPlayerManualSetIndex = "Connect.LdPlayerManualSetIndex"; + public const string LdPlayerIndex = "Connect.LdPlayerIndex"; + public const string RetryOnAdbDisconnected = "Connect.RetryOnDisconnected"; + public const string AllowAdbRestart = "Connect.AllowADBRestart"; + public const string AllowAdbHardRestart = "Connect.AllowADBHardRestart"; + public const string AdbLiteEnabled = "Connect.AdbLiteEnabled"; + public const string KillAdbOnExit = "Connect.KillAdbOnExit"; + public const string TouchMode = "Connect.TouchMode"; + public const string AdbReplaced = "Connect.AdbReplaced"; - public const string StartGame = "Start.StartGame"; - public const string ClientType = "Start.ClientType"; - public const string AccountName = "Start.AccountName"; - public const string RunDirectly = "Start.RunDirectly"; - public const string MinimizeDirectly = "Start.MinimizeDirectly"; - public const string StartEmulator = "Start.OpenEmulatorAfterLaunch"; - public const string EmulatorPath = "Start.EmulatorPath"; - public const string EmulatorAddCommand = "Start.EmulatorAddCommand"; - public const string EmulatorWaitSeconds = "Start.EmulatorWaitSeconds"; + public const string StartGame = "Start.StartGame"; + public const string ClientType = "Start.ClientType"; + public const string AccountName = "Start.AccountName"; + public const string RunDirectly = "Start.RunDirectly"; + public const string MinimizeDirectly = "Start.MinimizeDirectly"; + public const string StartEmulator = "Start.OpenEmulatorAfterLaunch"; + public const string EmulatorPath = "Start.EmulatorPath"; + public const string EmulatorAddCommand = "Start.EmulatorAddCommand"; + public const string EmulatorWaitSeconds = "Start.EmulatorWaitSeconds"; - // 仅在刷理智的任务中会生效,前缀需要修改 - public const string AutoRestartOnDrop = "Start.AutoRestartOnDrop"; - public const string StartsWithScript = "Start.StartsWithScript"; - public const string EndsWithScript = "Start.EndsWithScript"; - public const string CopilotWithScript = "Start.CopilotWithScript"; - public const string ManualStopWithScript = "Start.ManualStopWithScript"; - public const string BlockSleep = "Start.BlockSleep"; - public const string BlockSleepWithScreenOn = "Start.BlockSleepWithScreenOn"; + // 仅在刷理智的任务中会生效,前缀需要修改 + public const string AutoRestartOnDrop = "Start.AutoRestartOnDrop"; + public const string StartsWithScript = "Start.StartsWithScript"; + public const string EndsWithScript = "Start.EndsWithScript"; + public const string CopilotWithScript = "Start.CopilotWithScript"; + public const string ManualStopWithScript = "Start.ManualStopWithScript"; + public const string BlockSleep = "Start.BlockSleep"; + public const string BlockSleepWithScreenOn = "Start.BlockSleepWithScreenOn"; - public const string ChooseLevel3 = "Recruit.ChooseLevel3"; - public const string ChooseLevel4 = "Recruit.ChooseLevel4"; - public const string ChooseLevel5 = "Recruit.ChooseLevel5"; - public const string ChooseLevel6 = "Recruit.ChooseLevel6"; - public const string AutoSetTime = "Recruit.AutoSetTime"; - public const string RecruitmentShowPotential = "Recruit.ShowPotential"; + public const string ChooseLevel3 = "Recruit.ChooseLevel3"; + public const string ChooseLevel4 = "Recruit.ChooseLevel4"; + public const string ChooseLevel5 = "Recruit.ChooseLevel5"; + public const string ChooseLevel6 = "Recruit.ChooseLevel6"; + public const string AutoSetTime = "Recruit.AutoSetTime"; + public const string RecruitmentShowPotential = "Recruit.ShowPotential"; - public const string DepotResult = "Depot.DepotResult"; + public const string DepotResult = "Depot.DepotResult"; - public const string InfrastMode = "Infrast.InfrastMode"; - public const string DormThreshold = "Infrast.DormThreshold"; - public const string UsesOfDrones = "Infrast.UsesOfDrones"; - public const string InfrastReceptionMessageBoardReceive = "Infrast.ReceptionMessageBoardReceive"; - public const string InfrastReceptionClueExchange = "Infrast.ReceptionClueExchange"; - public const string ContinueTraining = "Infrast.ContinueTraining"; - public const string DefaultInfrast = "Infrast.DefaultInfrast"; - public const string IsCustomInfrastFileReadOnly = "Infrast.IsCustomInfrastFileReadOnly"; // 已废弃 - public const string DormFilterNotStationedEnabled = "Infrast.DormFilterNotStationedEnabled"; - public const string DormTrustEnabled = "Infrast.DormTrustEnabled"; - public const string OriginiumShardAutoReplenishment = "Infrast.OriginiumShardAutoReplenishment"; - public const string CustomInfrastEnabled = "Infrast.CustomInfrastEnabled"; - public const string CustomInfrastFile = "Infrast.CustomInfrastFile"; - public const string CustomInfrastPlanIndex = "Infrast.CustomInfrastPlanIndex"; - public const string CustomInfrastPlanShowInFightSettings = "Infrast.CustomInfrastPlanShowInFightSettings"; + public const string InfrastMode = "Infrast.InfrastMode"; + public const string DormThreshold = "Infrast.DormThreshold"; + public const string UsesOfDrones = "Infrast.UsesOfDrones"; + public const string InfrastReceptionMessageBoardReceive = "Infrast.ReceptionMessageBoardReceive"; + public const string InfrastReceptionClueExchange = "Infrast.ReceptionClueExchange"; + public const string ContinueTraining = "Infrast.ContinueTraining"; + public const string DefaultInfrast = "Infrast.DefaultInfrast"; + public const string IsCustomInfrastFileReadOnly = "Infrast.IsCustomInfrastFileReadOnly"; // 已废弃 + public const string DormFilterNotStationedEnabled = "Infrast.DormFilterNotStationedEnabled"; + public const string DormTrustEnabled = "Infrast.DormTrustEnabled"; + public const string OriginiumShardAutoReplenishment = "Infrast.OriginiumShardAutoReplenishment"; + public const string CustomInfrastEnabled = "Infrast.CustomInfrastEnabled"; + public const string CustomInfrastFile = "Infrast.CustomInfrastFile"; + public const string CustomInfrastPlanIndex = "Infrast.CustomInfrastPlanIndex"; + public const string CustomInfrastPlanShowInFightSettings = "Infrast.CustomInfrastPlanShowInFightSettings"; - public const string UseRemainingSanityStage = "Fight.UseRemainingSanityStage"; - public const string UseExpiringMedicine = "Fight.UseExpiringMedicine"; - public const string RemainingSanityStage = "Fight.RemainingSanityStage"; + public const string UseRemainingSanityStage = "Fight.UseRemainingSanityStage"; + public const string UseExpiringMedicine = "Fight.UseExpiringMedicine"; + public const string RemainingSanityStage = "Fight.RemainingSanityStage"; - public const string RoguelikeTheme = "Roguelike.RoguelikeTheme"; - public const string RoguelikeDifficulty = "Roguelike.Difficulty"; - public const string RoguelikeMode = "Roguelike.Mode"; - public const string RoguelikeSquad = "Roguelike.Squad"; - public const string RoguelikeCollectibleModeSquad = "Roguelike.CollectibleModeSquad"; - public const string RoguelikeRoles = "Roguelike.Roles"; - public const string RoguelikeCoreChar = "Roguelike.CoreChar"; - public const string RoguelikeStartWithEliteTwo = "Roguelike.RoguelikeStartWithEliteTwo"; - public const string RoguelikeOnlyStartWithEliteTwo = "Roguelike.RoguelikeOnlyStartWithEliteTwo"; - public const string RoguelikeStartWithSelectList = "Roguelike.RoguelikeStartWithSelectList"; - public const string Roguelike3FirstFloorFoldartal = "Roguelike.Roguelike3FirstFloorFoldartal"; - public const string Roguelike3FirstFloorFoldartals = "Roguelike.Roguelike3StartFloorFoldartal"; - public const string Roguelike3NewSquad2StartingFoldartal = "Roguelike.Roguelike3NewSquad2StartingFoldartal"; - public const string Roguelike3NewSquad2StartingFoldartals = "Roguelike.Roguelike3NewSquad2StartingFoldartals"; - public const string RoguelikeExpectedCollapsalParadigms = "Roguelike.RoguelikeExpectedCollapsalParadigms"; - public const string RoguelikeUseSupportUnit = "Roguelike.RoguelikeUseSupportUnit"; - public const string RoguelikeEnableNonfriendSupport = "Roguelike.RoguelikeEnableNonfriendSupport"; - public const string RoguelikeDelayAbortUntilCombatComplete = "Roguelike.RoguelikeDelayAbortUntilCombatComplete"; - public const string RoguelikeStartsCount = "Roguelike.StartsCount"; - public const string RoguelikeInvestmentEnabled = "Roguelike.InvestmentEnabled"; - public const string RoguelikeInvestmentEnterSecondFloor = "Roguelike.InvestmentEnterSecondFloor"; - public const string RoguelikeCollectibleModeShopping = "Roguelike.CollectibleModeShopping"; - public const string RoguelikeRefreshTraderWithDice = "Roguelike.RefreshTraderWithDice"; - public const string RoguelikeInvestsCount = "Roguelike.InvestsCount"; - public const string RoguelikeStopWhenInvestmentFull = "Roguelike.StopWhenInvestmentFull"; - public const string RoguelikeDeploymentWithPause = "Roguelike.DeploymentWithPause"; - public const string RoguelikeStopAtFinalBoss = "Roguelike.ExitAtFinalBoss"; - public const string RoguelikeStopAtMaxLevel = "Roguelike.StopAtMaxLevel"; - public const string RoguelikeStartWithSeed = "Roguelike.StartWithSeed"; - public const string RoguelikeMonthlySquadAutoIterate = "Roguelike.MonthlySquadAutoIterate"; - public const string RoguelikeMonthlySquadCheckComms = "Roguelike.MonthlySquadCheckComms"; - public const string RoguelikeDeepExplorationAutoIterate = "Roguelike.DeepExplorationAutoIterate"; - public const string ReclamationTheme = "Reclamation.Theme"; - public const string ReclamationMode = "Reclamation.Mode"; - public const string ReclamationToolToCraft = "Reclamation.ToolToCraft"; - public const string ReclamationIncrementMode = "Reclamation.ReclamationIncrementMode"; - public const string ReclamationMaxCraftCountPerRound = "Reclamation.ReclamationMaxCraftCountPerRound"; - public const string ReclamationClearStore = "Reclamation.ReclamationClearStore"; + public const string RoguelikeTheme = "Roguelike.RoguelikeTheme"; + public const string RoguelikeDifficulty = "Roguelike.Difficulty"; + public const string RoguelikeMode = "Roguelike.Mode"; + public const string RoguelikeSquad = "Roguelike.Squad"; + public const string RoguelikeCollectibleModeSquad = "Roguelike.CollectibleModeSquad"; + public const string RoguelikeRoles = "Roguelike.Roles"; + public const string RoguelikeCoreChar = "Roguelike.CoreChar"; + public const string RoguelikeStartWithEliteTwo = "Roguelike.RoguelikeStartWithEliteTwo"; + public const string RoguelikeOnlyStartWithEliteTwo = "Roguelike.RoguelikeOnlyStartWithEliteTwo"; + public const string RoguelikeStartWithSelectList = "Roguelike.RoguelikeStartWithSelectList"; + public const string Roguelike3FirstFloorFoldartal = "Roguelike.Roguelike3FirstFloorFoldartal"; + public const string Roguelike3FirstFloorFoldartals = "Roguelike.Roguelike3StartFloorFoldartal"; + public const string Roguelike3NewSquad2StartingFoldartal = "Roguelike.Roguelike3NewSquad2StartingFoldartal"; + public const string Roguelike3NewSquad2StartingFoldartals = "Roguelike.Roguelike3NewSquad2StartingFoldartals"; + public const string RoguelikeExpectedCollapsalParadigms = "Roguelike.RoguelikeExpectedCollapsalParadigms"; + public const string RoguelikeUseSupportUnit = "Roguelike.RoguelikeUseSupportUnit"; + public const string RoguelikeEnableNonfriendSupport = "Roguelike.RoguelikeEnableNonfriendSupport"; + public const string RoguelikeDelayAbortUntilCombatComplete = "Roguelike.RoguelikeDelayAbortUntilCombatComplete"; + public const string RoguelikeStartsCount = "Roguelike.StartsCount"; + public const string RoguelikeInvestmentEnabled = "Roguelike.InvestmentEnabled"; + public const string RoguelikeInvestmentEnterSecondFloor = "Roguelike.InvestmentEnterSecondFloor"; + public const string RoguelikeCollectibleModeShopping = "Roguelike.CollectibleModeShopping"; + public const string RoguelikeRefreshTraderWithDice = "Roguelike.RefreshTraderWithDice"; + public const string RoguelikeInvestsCount = "Roguelike.InvestsCount"; + public const string RoguelikeStopWhenInvestmentFull = "Roguelike.StopWhenInvestmentFull"; + public const string RoguelikeDeploymentWithPause = "Roguelike.DeploymentWithPause"; + public const string RoguelikeStopAtFinalBoss = "Roguelike.ExitAtFinalBoss"; + public const string RoguelikeStopAtMaxLevel = "Roguelike.StopAtMaxLevel"; + public const string RoguelikeStartWithSeed = "Roguelike.StartWithSeed"; + public const string RoguelikeMonthlySquadAutoIterate = "Roguelike.MonthlySquadAutoIterate"; + public const string RoguelikeMonthlySquadCheckComms = "Roguelike.MonthlySquadCheckComms"; + public const string RoguelikeDeepExplorationAutoIterate = "Roguelike.DeepExplorationAutoIterate"; + public const string ReclamationTheme = "Reclamation.Theme"; + public const string ReclamationMode = "Reclamation.Mode"; + public const string ReclamationToolToCraft = "Reclamation.ToolToCraft"; + public const string ReclamationIncrementMode = "Reclamation.ReclamationIncrementMode"; + public const string ReclamationMaxCraftCountPerRound = "Reclamation.ReclamationMaxCraftCountPerRound"; + public const string ReclamationClearStore = "Reclamation.ReclamationClearStore"; - public const string RecruitMaxTimes = "AutoRecruit.MaxTimes"; - public const string AutoRecruitFirstList = "AutoRecruit.AutoRecruitFirstList"; - public const string RefreshLevel3 = "AutoRecruit.RefreshLevel3"; - public const string ForceRefresh = "AutoRecruit.ForceRefresh"; - public const string SelectExtraTags = "AutoRecruit.SelectExtraTags"; - public const string NotChooseLevel1 = "AutoRecruit.NotChooseLevel1"; - public const string RecruitChooseLevel3 = "AutoRecruit.ChooseLevel3"; - public const string ChooseLevel3Time = "AutoRecruit.ChooseLevel3.Time"; - public const string RecruitChooseLevel4 = "AutoRecruit.ChooseLevel4"; - public const string ChooseLevel4Time = "AutoRecruit.ChooseLevel4.Time"; - public const string RecruitChooseLevel5 = "AutoRecruit.ChooseLevel5"; - public const string ChooseLevel5Time = "AutoRecruit.ChooseLevel5.Time"; + public const string RecruitMaxTimes = "AutoRecruit.MaxTimes"; + public const string AutoRecruitFirstList = "AutoRecruit.AutoRecruitFirstList"; + public const string RefreshLevel3 = "AutoRecruit.RefreshLevel3"; + public const string ForceRefresh = "AutoRecruit.ForceRefresh"; + public const string SelectExtraTags = "AutoRecruit.SelectExtraTags"; + public const string NotChooseLevel1 = "AutoRecruit.NotChooseLevel1"; + public const string RecruitChooseLevel3 = "AutoRecruit.ChooseLevel3"; + public const string ChooseLevel3Time = "AutoRecruit.ChooseLevel3.Time"; + public const string RecruitChooseLevel4 = "AutoRecruit.ChooseLevel4"; + public const string ChooseLevel4Time = "AutoRecruit.ChooseLevel4.Time"; + public const string RecruitChooseLevel5 = "AutoRecruit.ChooseLevel5"; + public const string ChooseLevel5Time = "AutoRecruit.ChooseLevel5.Time"; - public const string LastCreditFightTaskTime = "Visit.LastCreditFightTaskTime"; - public const string CreditFightTaskEnabled = "Visit.CreditFightTaskEnabled"; - public const string CreditFightSelectFormation = "Visit.CreditFightSelectFormation"; + public const string LastCreditFightTaskTime = "Visit.LastCreditFightTaskTime"; + public const string CreditFightTaskEnabled = "Visit.CreditFightTaskEnabled"; + public const string CreditFightSelectFormation = "Visit.CreditFightSelectFormation"; - public const string LastCreditVisitFriendsTime = "Mall.LastCreditVisitFriendsTime"; - public const string CreditVisitOnceADay = "Mall.CreditVisitOnceADay"; - public const string CreditVisitFriendsEnabled = "Mall.CreditVisitFriendsEnabled"; - public const string CreditShopping = "Mall.CreditShopping"; - public const string CreditFirstListNew = "Mall.CreditFirstListNew"; - public const string CreditBlackListNew = "Mall.CreditBlackListNew"; - public const string CreditForceShoppingIfCreditFull = "Mall.CreditForceShoppingIfCreditFull"; - public const string CreditOnlyBuyDiscount = "Mall.CreditOnlyBuyDiscount"; - public const string CreditReserveMaxCredit = "Mall.CreidtReserveMaxCredit"; + public const string LastCreditVisitFriendsTime = "Mall.LastCreditVisitFriendsTime"; + public const string CreditVisitOnceADay = "Mall.CreditVisitOnceADay"; + public const string CreditVisitFriendsEnabled = "Mall.CreditVisitFriendsEnabled"; + public const string CreditShopping = "Mall.CreditShopping"; + public const string CreditFirstListNew = "Mall.CreditFirstListNew"; + public const string CreditBlackListNew = "Mall.CreditBlackListNew"; + public const string CreditForceShoppingIfCreditFull = "Mall.CreditForceShoppingIfCreditFull"; + public const string CreditOnlyBuyDiscount = "Mall.CreditOnlyBuyDiscount"; + public const string CreditReserveMaxCredit = "Mall.CreidtReserveMaxCredit"; - public const string ReceiveAward = "Mission.ReceiveAward"; - public const string ReceiveMail = "Mission.ReceiveMail"; - public const string ReceiveFreeGacha = "Mission.ReceiveFreeRecruit"; - public const string ReceiveOrundum = "Mission.ReceiveOrundum"; - public const string ReceiveMining = "Mission.ReceiveMining"; - public const string ReceiveSpecialAccess = "Mission.ReceiveSpecialAccess"; - public const string CopilotAddUserAdditional = "Copilot.AddUserAdditional"; - public const string CopilotUserAdditional = "Copilot.UserAdditional"; - public const string CopilotSelectFormation = "Copilot.SelectFormation"; - public const string CopilotLoopTimes = "Copilot.LoopTimes"; - public const string CopilotTaskList = "Copilot.CopilotTaskList"; - public const string UpdateProxy = "VersionUpdate.Proxy"; - public const string ProxyType = "VersionUpdate.ProxyType"; - public const string VersionType = "VersionUpdate.VersionType"; + public const string ReceiveAward = "Mission.ReceiveAward"; + public const string ReceiveMail = "Mission.ReceiveMail"; + public const string ReceiveFreeGacha = "Mission.ReceiveFreeRecruit"; + public const string ReceiveOrundum = "Mission.ReceiveOrundum"; + public const string ReceiveMining = "Mission.ReceiveMining"; + public const string ReceiveSpecialAccess = "Mission.ReceiveSpecialAccess"; + public const string CopilotAddUserAdditional = "Copilot.AddUserAdditional"; + public const string CopilotUserAdditional = "Copilot.UserAdditional"; + public const string CopilotSelectFormation = "Copilot.SelectFormation"; + public const string CopilotLoopTimes = "Copilot.LoopTimes"; + public const string CopilotTaskList = "Copilot.CopilotTaskList"; + public const string UpdateProxy = "VersionUpdate.Proxy"; + public const string ProxyType = "VersionUpdate.ProxyType"; + public const string VersionType = "VersionUpdate.VersionType"; - // MirrorChyanCdk 迁到新配置的话就直接叫 UpdateSource,现在本体更新也加上了 MirrorChyan - public const string UpdateSource = "VersionUpdate.ResourceUpdateSource"; - public const string ForceGithubGlobalSource = "VersionUpdate.UpdateSource.ForceGithubGlobalSource"; - public const string MirrorChyanCdk = "VersionUpdate.ResourceUpdateSource.MirrorChyanCdk"; - public const string MirrorChyanCdkExpiredTime = "VersionUpdate.UpdateSource.MirrorChyanCdkExpired"; - public const string StartupUpdateCheck = "VersionUpdate.StartupUpdateCheck"; - public const string UpdateAutoCheck = "VersionUpdate.ScheduledUpdateCheck"; - public const string ResourceApi = "VersionUpdate.ResourceApi"; - public const string AllowNightlyUpdates = "VersionUpdate.AllowNightlyUpdates"; - public const string HasAcknowledgedNightlyWarning = "VersionUpdate.HasAcknowledgedNightlyWarning"; + // MirrorChyanCdk 迁到新配置的话就直接叫 UpdateSource,现在本体更新也加上了 MirrorChyan + public const string UpdateSource = "VersionUpdate.ResourceUpdateSource"; + public const string ForceGithubGlobalSource = "VersionUpdate.UpdateSource.ForceGithubGlobalSource"; + public const string MirrorChyanCdk = "VersionUpdate.ResourceUpdateSource.MirrorChyanCdk"; + public const string MirrorChyanCdkExpiredTime = "VersionUpdate.UpdateSource.MirrorChyanCdkExpired"; + public const string StartupUpdateCheck = "VersionUpdate.StartupUpdateCheck"; + public const string UpdateAutoCheck = "VersionUpdate.ScheduledUpdateCheck"; + public const string ResourceApi = "VersionUpdate.ResourceApi"; + public const string AllowNightlyUpdates = "VersionUpdate.AllowNightlyUpdates"; + public const string HasAcknowledgedNightlyWarning = "VersionUpdate.HasAcknowledgedNightlyWarning"; - // 这个已经废弃了,还要留着吗? - [UsedImplicitly] - public const string UseAria2 = "VersionUpdate.UseAria2"; + // 这个已经废弃了,还要留着吗? + [UsedImplicitly] + public const string UseAria2 = "VersionUpdate.UseAria2"; - public const string AutoDownloadUpdatePackage = "VersionUpdate.AutoDownloadUpdatePackage"; - public const string AutoInstallUpdatePackage = "VersionUpdate.AutoInstallUpdatePackage"; + public const string AutoDownloadUpdatePackage = "VersionUpdate.AutoDownloadUpdatePackage"; + public const string AutoInstallUpdatePackage = "VersionUpdate.AutoInstallUpdatePackage"; - public const string PenguinId = "Penguin.Id"; - public const string IsDrGrandet = "Penguin.IsDrGrandet"; - public const string EnablePenguin = "Penguin.EnablePenguin"; + public const string PenguinId = "Penguin.Id"; + public const string IsDrGrandet = "Penguin.IsDrGrandet"; + public const string EnablePenguin = "Penguin.EnablePenguin"; - public const string EnableYituliu = "Yituliu.EnableYituliu"; + public const string EnableYituliu = "Yituliu.EnableYituliu"; - public const string TaskTimeoutMinutes = "TimeOut.Timer.TaskTimeoutMinutes"; - public const string ReminderIntervalMinutes = "TimeOut.Timer.ReminderIntervalMinutes"; + public const string TaskTimeoutMinutes = "TimeOut.Timer.TaskTimeoutMinutes"; + public const string ReminderIntervalMinutes = "TimeOut.Timer.ReminderIntervalMinutes"; - public const string BluestacksConfigPath = "Bluestacks.Config.Path"; - public const string BluestacksConfigKeyword = "Bluestacks.Config.Keyword"; - public const string BluestacksConfigError = "Bluestacks.Config.Error"; + public const string BluestacksConfigPath = "Bluestacks.Config.Path"; + public const string BluestacksConfigKeyword = "Bluestacks.Config.Keyword"; + public const string BluestacksConfigError = "Bluestacks.Config.Error"; - public const string PostActions = "MainFunction.PostActions"; - public const string MainFunctionInverseMode = "MainFunction.InverseMode"; - public const string Stage1 = "MainFunction.Stage1"; - public const string Stage2 = "MainFunction.Stage2"; - public const string Stage3 = "MainFunction.Stage3"; - public const string Stage4 = "MainFunction.Stage4"; - public const string UseCustomAnnihilation = "MainFunction.Annihilation.UseCustom"; - public const string AnnihilationStage = "MainFunction.Annihilation.Stage"; - public const string UseMedicine = "MainFunction.UseMedicine"; - public const string UseMedicineQuantity = "MainFunction.UseMedicine.Quantity"; - public const string UseStone = "MainFunction.UseStone"; - public const string UseStoneQuantity = "MainFunction.UseStone.Quantity"; - public const string TimesLimited = "MainFunction.TimesLimited"; - public const string TimesLimitedQuantity = "MainFunction.TimesLimited.Quantity"; - public const string SeriesQuantity = "MainFunction.Series.Quantity"; - public const string DropsEnable = "MainFunction.Drops.Enable"; - public const string DropsItemId = "MainFunction.Drops.ItemId"; - public const string DropsItemName = "MainFunction.Drops.ItemName"; - public const string DropsQuantity = "MainFunction.Drops.Quantity"; + public const string PostActions = "MainFunction.PostActions"; + public const string MainFunctionInverseMode = "MainFunction.InverseMode"; + public const string Stage1 = "MainFunction.Stage1"; + public const string Stage2 = "MainFunction.Stage2"; + public const string Stage3 = "MainFunction.Stage3"; + public const string Stage4 = "MainFunction.Stage4"; + public const string UseCustomAnnihilation = "MainFunction.Annihilation.UseCustom"; + public const string AnnihilationStage = "MainFunction.Annihilation.Stage"; + public const string UseMedicine = "MainFunction.UseMedicine"; + public const string UseMedicineQuantity = "MainFunction.UseMedicine.Quantity"; + public const string UseStone = "MainFunction.UseStone"; + public const string UseStoneQuantity = "MainFunction.UseStone.Quantity"; + public const string TimesLimited = "MainFunction.TimesLimited"; + public const string TimesLimitedQuantity = "MainFunction.TimesLimited.Quantity"; + public const string SeriesQuantity = "MainFunction.Series.Quantity"; + public const string DropsEnable = "MainFunction.Drops.Enable"; + public const string DropsItemId = "MainFunction.Drops.ItemId"; + public const string DropsItemName = "MainFunction.Drops.ItemName"; + public const string DropsQuantity = "MainFunction.Drops.Quantity"; - public const string RemoteControlGetTaskEndpointUri = "RemoteControl.RemoteControlGetTaskEndpointUri"; - public const string RemoteControlReportStatusUri = "RemoteControl.RemoteControlReportStatusUri"; - public const string RemoteControlUserIdentity = "RemoteControl.RemoteControlUserIdentity"; - public const string RemoteControlDeviceIdentity = "RemoteControl.RemoteControlDeviceIdentity"; - public const string RemoteControlPollIntervalMs = "RemoteControl.RemoteControlPollIntervalMs"; + public const string RemoteControlGetTaskEndpointUri = "RemoteControl.RemoteControlGetTaskEndpointUri"; + public const string RemoteControlReportStatusUri = "RemoteControl.RemoteControlReportStatusUri"; + public const string RemoteControlUserIdentity = "RemoteControl.RemoteControlUserIdentity"; + public const string RemoteControlDeviceIdentity = "RemoteControl.RemoteControlDeviceIdentity"; + public const string RemoteControlPollIntervalMs = "RemoteControl.RemoteControlPollIntervalMs"; - public const string ExternalNotificationEnabled = "ExternalNotification.Enabled"; - public const string ExternalNotificationSendWhenComplete = "ExternalNotification.SendWhenComplete"; - public const string ExternalNotificationEnableDetails = "ExternalNotification.EnableDetails"; - public const string ExternalNotificationSendWhenError = "ExternalNotification.SendWhenError"; - public const string ExternalNotificationSendWhenTimeout = "ExternalNotification.SendWhenTimeout"; - public const string ExternalNotificationSmtpServer = "ExternalNotification.Smtp.Server"; - public const string ExternalNotificationSmtpPort = "ExternalNotification.Smtp.Port"; - public const string ExternalNotificationSmtpUser = "ExternalNotification.Smtp.User"; - public const string ExternalNotificationSmtpPassword = "ExternalNotification.Smtp.Password"; - public const string ExternalNotificationSmtpUseSsl = "ExternalNotification.Smtp.UseSsl"; - public const string ExternalNotificationSmtpRequiresAuthentication = "ExternalNotification.Smtp.RequiresAuthentication"; - public const string ExternalNotificationSmtpFrom = "ExternalNotification.Smtp.From"; - public const string ExternalNotificationSmtpTo = "ExternalNotification.Smtp.To"; - public const string ExternalNotificationServerChanSendKey = "ExternalNotification.ServerChan.SendKey"; - public const string ExternalNotificationDiscordBotToken = "ExternalNotification.Discord.BotToken"; - public const string ExternalNotificationDiscordUserId = "ExternalNotification.Discord.UserId"; - public const string ExternalNotificationDiscordWebhookUrl = "ExternalNotification.Discord.WebhookUrl"; - public const string ExternalNotificationTelegramBotToken = "ExternalNotification.Telegram.BotToken"; - public const string ExternalNotificationTelegramChatId = "ExternalNotification.Telegram.ChatId"; - public const string ExternalNotificationTelegramTopicId = "ExternalNotification.TelegramTopicId"; - public const string ExternalNotificationBarkSendKey = "ExternalNotification.Bark.SendKey"; - public const string ExternalNotificationBarkServer = "ExternalNotification.Bark.Server"; - public const string ExternalNotificationQmsgServer = "ExternalNotification.Qmsg.Server"; - public const string ExternalNotificationQmsgKey = "ExternalNotification.Qmsg.Key"; - public const string ExternalNotificationQmsgUser = "ExternalNotification.Qmsg.User"; - public const string ExternalNotificationQmsgBot = "ExternalNotification.Qmsg.Bot"; - public const string ExternalNotificationCustomWebhookUrl = "ExternalNotification.CustomWebhook.Url"; - public const string ExternalNotificationCustomWebhookBody = "ExternalNotification.CustomWebhook.Body"; + public const string ExternalNotificationEnabled = "ExternalNotification.Enabled"; + public const string ExternalNotificationSendWhenComplete = "ExternalNotification.SendWhenComplete"; + public const string ExternalNotificationEnableDetails = "ExternalNotification.EnableDetails"; + public const string ExternalNotificationSendWhenError = "ExternalNotification.SendWhenError"; + public const string ExternalNotificationSendWhenTimeout = "ExternalNotification.SendWhenTimeout"; + public const string ExternalNotificationSmtpServer = "ExternalNotification.Smtp.Server"; + public const string ExternalNotificationSmtpPort = "ExternalNotification.Smtp.Port"; + public const string ExternalNotificationSmtpUser = "ExternalNotification.Smtp.User"; + public const string ExternalNotificationSmtpPassword = "ExternalNotification.Smtp.Password"; + public const string ExternalNotificationSmtpUseSsl = "ExternalNotification.Smtp.UseSsl"; + public const string ExternalNotificationSmtpRequiresAuthentication = "ExternalNotification.Smtp.RequiresAuthentication"; + public const string ExternalNotificationSmtpFrom = "ExternalNotification.Smtp.From"; + public const string ExternalNotificationSmtpTo = "ExternalNotification.Smtp.To"; + public const string ExternalNotificationServerChanSendKey = "ExternalNotification.ServerChan.SendKey"; + public const string ExternalNotificationDiscordBotToken = "ExternalNotification.Discord.BotToken"; + public const string ExternalNotificationDiscordUserId = "ExternalNotification.Discord.UserId"; + public const string ExternalNotificationDiscordWebhookUrl = "ExternalNotification.Discord.WebhookUrl"; + public const string ExternalNotificationTelegramBotToken = "ExternalNotification.Telegram.BotToken"; + public const string ExternalNotificationTelegramChatId = "ExternalNotification.Telegram.ChatId"; + public const string ExternalNotificationTelegramTopicId = "ExternalNotification.TelegramTopicId"; + public const string ExternalNotificationBarkSendKey = "ExternalNotification.Bark.SendKey"; + public const string ExternalNotificationBarkServer = "ExternalNotification.Bark.Server"; + public const string ExternalNotificationQmsgServer = "ExternalNotification.Qmsg.Server"; + public const string ExternalNotificationQmsgKey = "ExternalNotification.Qmsg.Key"; + public const string ExternalNotificationQmsgUser = "ExternalNotification.Qmsg.User"; + public const string ExternalNotificationQmsgBot = "ExternalNotification.Qmsg.Bot"; + public const string ExternalNotificationCustomWebhookUrl = "ExternalNotification.CustomWebhook.Url"; + public const string ExternalNotificationCustomWebhookBody = "ExternalNotification.CustomWebhook.Body"; - public const string PerformanceUseGpu = "Performance.UseGpu"; - public const string PerformancePreferredGpuDescription = "Performance.PreferredGpuDescription"; - public const string PerformancePreferredGpuInstancePath = "Performance.PreferredGpuInstancePath"; - public const string PerformanceAllowDeprecatedGpu = "Performance.AllowDeprecatedGpu"; + public const string PerformanceUseGpu = "Performance.UseGpu"; + public const string PerformancePreferredGpuDescription = "Performance.PreferredGpuDescription"; + public const string PerformancePreferredGpuInstancePath = "Performance.PreferredGpuInstancePath"; + public const string PerformanceAllowDeprecatedGpu = "Performance.AllowDeprecatedGpu"; - // The following should not be modified manually - public const string VersionName = "VersionUpdate.name"; + // The following should not be modified manually + public const string VersionName = "VersionUpdate.name"; - public const string VersionUpdateBody = "VersionUpdate.body"; - public const string VersionUpdateIsFirstBoot = "VersionUpdate.isfirstboot"; - public const string VersionUpdatePackage = "VersionUpdate.package"; - public const string VersionUpdateDoNotShowUpdate = "VersionUpdate.doNotShowUpdate"; + public const string VersionUpdateBody = "VersionUpdate.body"; + public const string VersionUpdateIsFirstBoot = "VersionUpdate.isfirstboot"; + public const string VersionUpdatePackage = "VersionUpdate.package"; + public const string VersionUpdateDoNotShowUpdate = "VersionUpdate.doNotShowUpdate"; - public const string OperBoxData = "OperBox.Data"; + public const string OperBoxData = "OperBox.Data"; - public const string GachaShowDisclaimerNoMore = "Gacha.ShowDisclaimerNoMore"; + public const string GachaShowDisclaimerNoMore = "Gacha.ShowDisclaimerNoMore"; - public const string PeepTargetFps = "Peep.TargetFps"; + public const string PeepTargetFps = "Peep.TargetFps"; - public const string GuideStepIndex = "Guide.StepIndex"; + public const string GuideStepIndex = "Guide.StepIndex"; - public const string ForceScheduledStart = "Timer.ForceScheduledStart"; - public const string ShowWindowBeforeForceScheduledStart = "Timer.ShowWindowBeforeForceScheduledStart"; - public const string CustomConfig = "Timer.CustomConfig"; + public const string ForceScheduledStart = "Timer.ForceScheduledStart"; + public const string ShowWindowBeforeForceScheduledStart = "Timer.ShowWindowBeforeForceScheduledStart"; + public const string CustomConfig = "Timer.CustomConfig"; - public const string DebugTaskName = "Debug.TaskName"; + public const string DebugTaskName = "Debug.TaskName"; - public const string MiniGameTaskName = "MiniGame.TaskName"; + public const string MiniGameTaskName = "MiniGame.TaskName"; - public const string AchievementPopupDisabled = "Achievement.PopupDisabled"; - public const string AchievementPopupAutoClose = "Achievement.PopupAutoClose"; + public const string AchievementPopupDisabled = "Achievement.PopupDisabled"; + public const string AchievementPopupAutoClose = "Achievement.PopupAutoClose"; - // public const string AnnouncementInfo = "Announcement.AnnouncementInfo";// 已迁移 - // public const string DoNotRemindThisAnnouncementAgain = "Announcement.DoNotRemindThisAnnouncementAgain";// 已迁移 - // public const string DoNotShowAnnouncement = "Announcement.DoNotShowAnnouncement";// 已迁移 - } + // public const string AnnouncementInfo = "Announcement.AnnouncementInfo";// 已迁移 + // public const string DoNotRemindThisAnnouncementAgain = "Announcement.DoNotRemindThisAnnouncementAgain";// 已迁移 + // public const string DoNotShowAnnouncement = "Announcement.DoNotShowAnnouncement";// 已迁移 } diff --git a/src/MaaWpfGui/Constants/JsonDataKey.cs b/src/MaaWpfGui/Constants/JsonDataKey.cs index 5f71467b58..70e7e82ae7 100644 --- a/src/MaaWpfGui/Constants/JsonDataKey.cs +++ b/src/MaaWpfGui/Constants/JsonDataKey.cs @@ -11,11 +11,10 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Constants +namespace MaaWpfGui.Constants; + +public static class JsonDataKey { - public static class JsonDataKey - { - public const string OperBoxData = "OperBoxData"; - public const string DepotData = "DepotData"; - } + public const string OperBoxData = "OperBoxData"; + public const string DepotData = "DepotData"; } diff --git a/src/MaaWpfGui/Constants/MaaUrls.cs b/src/MaaWpfGui/Constants/MaaUrls.cs index 3cc3a45d63..9f99ebaaa8 100644 --- a/src/MaaWpfGui/Constants/MaaUrls.cs +++ b/src/MaaWpfGui/Constants/MaaUrls.cs @@ -11,93 +11,91 @@ // but WITHOUT ANY WARRANTY // -using System.Collections.Generic; using MaaWpfGui.Helper; -namespace MaaWpfGui.Constants +namespace MaaWpfGui.Constants; + +public static class MaaUrls { - public static class MaaUrls + public const string MaaPlus = "https://maa.plus"; + + public const string Bilibili = "https://space.bilibili.com/3493274731940507"; + + public const string BilibiliVideo = "https://www.bilibili.com/video/"; + + public const string GitHub = "https://github.com/MaaAssistantArknights/MaaAssistantArknights"; + + public const string ResourceRepository = "https://github.com/MaaAssistantArknights/MaaResource"; + + public const string GitHubIssues = "https://github.com/MaaAssistantArknights/MaaAssistantArknights/issues"; + + public const string Telegram = "https://t.me/+Mgc2Zngr-hs3ZjU1"; + + public const string Discord = "https://discord.gg/23DfZ9uA4V"; + + public const string PrtsPlus = "https://prts.plus"; + + public const string PrtsPlusCopilotGet = "https://prts.maa.plus/copilot/get/"; + + public const string PrtsPlusCopilotRating = "https://prts.maa.plus/copilot/rating"; + + public const string PrtsPlusCopilotSetGet = "https://prts.maa.plus/set/get?id="; + + public const string MapPrts = "https://map.ark-nights.com/areas?coord_override=maa"; + + public const string MaaApi = "https://api.maa.plus/MaaAssistantArknights/api/"; + public const string MaaApi2 = "https://api2.maa.plus/MaaAssistantArknights/api/"; + + public const string QqGroups = "https://api.maa.plus/MaaAssistantArknights/api/qqgroup/index.html"; + + public const string QqChannel = "https://pd.qq.com/s/4j1ju9z47"; + + public const string GoogleAdbDownloadUrl = "https://dl.google.com/android/repository/platform-tools-latest-windows.zip"; + public const string AdbMaaMirrorDownloadUrl = "https://api.maa.plus/MaaAssistantArknights/api/binaries/adb-windows.zip"; + public const string AdbMaaMirror2DownloadUrl = "https://api2.maa.plus/MaaAssistantArknights/api/binaries/adb-windows.zip"; + public const string GoogleAdbFilename = "adb-windows.zip"; + + private static string Language => ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Localization, LocalizationHelper.DefaultLanguage); + + private const string MaaDocs = "https://docs.maa.plus"; + + // 常见问题 + public static string HelpUri => $"{MaaDocs}/{Language}/manual/faq.html"; + + // YostarEN resolution info + public static string YostarENResolution => $"{MaaDocs}/{Language}/"; + + // 外服适配教程 + public static string OverseasAdaptation => $"{MaaDocs}/{Language}/develop/overseas-client-adaptation.html"; + + // 基建排班协议文档 + public static string CustomInfrastGenerator => $"{MaaDocs}/{Language}/protocol/base-scheduling-schema.html"; + + // 远程控制协议文档 + public static readonly string RemoteControlDocument = $"{MaaDocs}/{Language}/protocol/remote-control-schema.html"; + + public static string NewIssueUri => Language switch { - public const string MaaPlus = "https://maa.plus"; + "zh-cn" => $"{GitHubIssues}/new?assignees=&labels=bug&template=cn-bug-report.yaml", + "zh-tw" => $"{GitHubIssues}/new?assignees=&labels=bug&template=cn-bug-report.yaml", + _ => $"{GitHubIssues}/new?assignees=&labels=bug&template=en-bug-report.yaml", + }; - public const string Bilibili = "https://space.bilibili.com/3493274731940507"; + // 资源更新更新源 + public const string GithubResourceUpdate = "https://github.com/MaaAssistantArknights/MaaResource/archive/refs/heads/main.zip"; - public const string BilibiliVideo = "https://www.bilibili.com/video/"; + // MirrorChyan + public const string MirrorChyanDomain = "https://mirrorchyan.com"; + public const string MirrorChyanWebsite = $"{MirrorChyanDomain}?source=maawpfgui-settings"; + public const string MirrorChyanAppUpdate = $"{MirrorChyanDomain}/api/resources/MAA/latest"; + public const string MirrorChyanResourceUpdate = $"{MirrorChyanDomain}/api/resources/MaaResource/latest"; + public const string MirrorChyanManualUpdate = $"{MirrorChyanDomain}/zh/projects?rid=MAA&source=maawpfgui-manualupdate"; - public const string GitHub = "https://github.com/MaaAssistantArknights/MaaAssistantArknights"; - - public const string ResourceRepository = "https://github.com/MaaAssistantArknights/MaaResource"; - - public const string GitHubIssues = "https://github.com/MaaAssistantArknights/MaaAssistantArknights/issues"; - - public const string Telegram = "https://t.me/+Mgc2Zngr-hs3ZjU1"; - - public const string Discord = "https://discord.gg/23DfZ9uA4V"; - - public const string PrtsPlus = "https://prts.plus"; - - public const string PrtsPlusCopilotGet = "https://prts.maa.plus/copilot/get/"; - - public const string PrtsPlusCopilotRating = "https://prts.maa.plus/copilot/rating"; - - public const string PrtsPlusCopilotSetGet = "https://prts.maa.plus/set/get?id="; - - public const string MapPrts = "https://map.ark-nights.com/areas?coord_override=maa"; - - public const string MaaApi = "https://api.maa.plus/MaaAssistantArknights/api/"; - public const string MaaApi2 = "https://api2.maa.plus/MaaAssistantArknights/api/"; - - public const string QqGroups = "https://api.maa.plus/MaaAssistantArknights/api/qqgroup/index.html"; - - public const string QqChannel = "https://pd.qq.com/s/4j1ju9z47"; - - public const string GoogleAdbDownloadUrl = "https://dl.google.com/android/repository/platform-tools-latest-windows.zip"; - public const string AdbMaaMirrorDownloadUrl = "https://api.maa.plus/MaaAssistantArknights/api/binaries/adb-windows.zip"; - public const string AdbMaaMirror2DownloadUrl = "https://api2.maa.plus/MaaAssistantArknights/api/binaries/adb-windows.zip"; - public const string GoogleAdbFilename = "adb-windows.zip"; - - private static string Language => ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Localization, LocalizationHelper.DefaultLanguage); - - private const string MaaDocs = "https://docs.maa.plus"; - - // 常见问题 - public static string HelpUri => $"{MaaDocs}/{Language}/manual/faq.html"; - - // YostarEN resolution info - public static string YostarENResolution => $"{MaaDocs}/{Language}/"; - - // 外服适配教程 - public static string OverseasAdaptation => $"{MaaDocs}/{Language}/develop/overseas-client-adaptation.html"; - - // 基建排班协议文档 - public static string CustomInfrastGenerator => $"{MaaDocs}/{Language}/protocol/base-scheduling-schema.html"; - - // 远程控制协议文档 - public static readonly string RemoteControlDocument = $"{MaaDocs}/{Language}/protocol/remote-control-schema.html"; - - public static string NewIssueUri => Language switch - { - "zh-cn" => $"{GitHubIssues}/new?assignees=&labels=bug&template=cn-bug-report.yaml", - "zh-tw" => $"{GitHubIssues}/new?assignees=&labels=bug&template=cn-bug-report.yaml", - _ => $"{GitHubIssues}/new?assignees=&labels=bug&template=en-bug-report.yaml", - }; - - // 资源更新更新源 - public const string GithubResourceUpdate = "https://github.com/MaaAssistantArknights/MaaResource/archive/refs/heads/main.zip"; - - // MirrorChyan - public const string MirrorChyanDomain = "https://mirrorchyan.com"; - public const string MirrorChyanWebsite = $"{MirrorChyanDomain}?source=maawpfgui-settings"; - public const string MirrorChyanAppUpdate = $"{MirrorChyanDomain}/api/resources/MAA/latest"; - public const string MirrorChyanResourceUpdate = $"{MirrorChyanDomain}/api/resources/MaaResource/latest"; - public const string MirrorChyanManualUpdate = $"{MirrorChyanDomain}/zh/projects?rid=MAA&source=maawpfgui-manualupdate"; - - // 企鹅物流 - public const string PenguinIoDomain = "https://penguin-stats.io"; - public static readonly string[] PenguinBackupDomains = - [ - /*"https://penguin-stats.alvorna.com",*/ - "https://penguin-stats.cn" - ]; - } + // 企鹅物流 + public const string PenguinIoDomain = "https://penguin-stats.io"; + public static readonly string[] PenguinBackupDomains = + [ + /*"https://penguin-stats.alvorna.com",*/ + "https://penguin-stats.cn" + ]; } diff --git a/src/MaaWpfGui/Constants/UILogColor.cs b/src/MaaWpfGui/Constants/UILogColor.cs index 085332fee7..9cec93a75d 100644 --- a/src/MaaWpfGui/Constants/UILogColor.cs +++ b/src/MaaWpfGui/Constants/UILogColor.cs @@ -13,183 +13,182 @@ using JetBrains.Annotations; -namespace MaaWpfGui.Constants +namespace MaaWpfGui.Constants; + +/// +/// The recommended colors for logs. +/// +public static class UiLogColor { + public const string Text = "PrimaryTextBrush"; + /// - /// The recommended colors for logs. + /// The recommended color for success logs. /// - public static class UiLogColor - { - public const string Text = "PrimaryTextBrush"; + public const string Success = "SuccessLogBrush"; - /// - /// The recommended color for success logs. - /// - public const string Success = "SuccessLogBrush"; + /// + /// The recommended color for error logs. + /// + public const string Error = "ErrorLogBrush"; - /// - /// The recommended color for error logs. - /// - public const string Error = "ErrorLogBrush"; + /// + /// The recommended color for warning logs. + /// + public const string Warning = "WarningLogBrush"; - /// - /// The recommended color for warning logs. - /// - public const string Warning = "WarningLogBrush"; + /// + /// The recommended color for info logs. + /// + public const string Info = "InfoLogBrush"; - /// - /// The recommended color for info logs. - /// - public const string Info = "InfoLogBrush"; + /// + /// The recommended color for trace logs. + /// + public const string Trace = "TraceLogBrush"; - /// - /// The recommended color for trace logs. - /// - public const string Trace = "TraceLogBrush"; + /// + /// The recommended color for message logs. + /// + public const string Message = "MessageLogBrush"; - /// - /// The recommended color for message logs. - /// - public const string Message = "MessageLogBrush"; + /// + /// The recommended color for rare operator logs. + /// + public const string RareOperator = "RareOperatorLogBrush"; - /// - /// The recommended color for rare operator logs. - /// - public const string RareOperator = "RareOperatorLogBrush"; + /// + /// The recommended color for robot operator logs. + /// + [UsedImplicitly] + public const string RobotOperator = "RobotOperatorLogBrush"; - /// - /// The recommended color for robot operator logs. - /// - [UsedImplicitly] - public const string RobotOperator = "RobotOperatorLogBrush"; + #region 公招 - #region 公招 + /// + /// The recommended color for 1-star operators (also used for robot operators). + /// + public const string Star1Operator = "Star1OperatorLogBrush"; - /// - /// The recommended color for 1-star operators (also used for robot operators). - /// - public const string Star1Operator = "Star1OperatorLogBrush"; + /// + /// The recommended color for 1-star operators when potential is full. + /// + public const string Star1OperatorPotentialFull = "Star1OperatorLogBrushPotentialFull"; - /// - /// The recommended color for 1-star operators when potential is full. - /// - public const string Star1OperatorPotentialFull = "Star1OperatorLogBrushPotentialFull"; + /// + /// The recommended color for 2-star operators. + /// + public const string Star2Operator = "Star2OperatorLogBrush"; - /// - /// The recommended color for 2-star operators. - /// - public const string Star2Operator = "Star2OperatorLogBrush"; + /// + /// The recommended color for 2-star operators when potential is full. + /// + public const string Star2OperatorPotentialFull = "Star2OperatorLogBrushPotentialFull"; - /// - /// The recommended color for 2-star operators when potential is full. - /// - public const string Star2OperatorPotentialFull = "Star2OperatorLogBrushPotentialFull"; + /// + /// The recommended color for 3-star operators. + /// + public const string Star3Operator = "Star3OperatorLogBrush"; - /// - /// The recommended color for 3-star operators. - /// - public const string Star3Operator = "Star3OperatorLogBrush"; + /// + /// The recommended color for 3-star operators when potential is full. + /// + public const string Star3OperatorPotentialFull = "Star3OperatorLogBrushPotentialFull"; - /// - /// The recommended color for 3-star operators when potential is full. - /// - public const string Star3OperatorPotentialFull = "Star3OperatorLogBrushPotentialFull"; + /// + /// The recommended color for 4-star operators. + /// + public const string Star4Operator = "Star4OperatorLogBrush"; - /// - /// The recommended color for 4-star operators. - /// - public const string Star4Operator = "Star4OperatorLogBrush"; + /// + /// The recommended color for 4-star operators when potential is full. + /// + public const string Star4OperatorPotentialFull = "Star4OperatorLogBrushPotentialFull"; - /// - /// The recommended color for 4-star operators when potential is full. - /// - public const string Star4OperatorPotentialFull = "Star4OperatorLogBrushPotentialFull"; + /// + /// The recommended color for 5-star operators. + /// + public const string Star5Operator = "Star5OperatorLogBrush"; - /// - /// The recommended color for 5-star operators. - /// - public const string Star5Operator = "Star5OperatorLogBrush"; + /// + /// The recommended color for 5-star operators when potential is full. + /// + public const string Star5OperatorPotentialFull = "Star5OperatorLogBrushPotentialFull"; - /// - /// The recommended color for 5-star operators when potential is full. - /// - public const string Star5OperatorPotentialFull = "Star5OperatorLogBrushPotentialFull"; + /// + /// The recommended color for 6-star operators. + /// + public const string Star6Operator = "Star6OperatorLogBrush"; - /// - /// The recommended color for 6-star operators. - /// - public const string Star6Operator = "Star6OperatorLogBrush"; + /// + /// The recommended color for 6-star operators when potential is full. + /// + public const string Star6OperatorPotentialFull = "Star6OperatorLogBrushPotentialFull"; - /// - /// The recommended color for 6-star operators when potential is full. - /// - public const string Star6OperatorPotentialFull = "Star6OperatorLogBrushPotentialFull"; + #endregion - #endregion + /// + /// The recommended color for file downloading or downloaded or download failed. + /// + public const string Download = "DownloadLogBrush"; - /// - /// The recommended color for file downloading or downloaded or download failed. - /// - public const string Download = "DownloadLogBrush"; + /// + /// The recommended color for MuMu special screenshot. + /// + public const string MuMuSpecialScreenshot = "MuMuSpecialScreenshot"; - /// - /// The recommended color for MuMu special screenshot. - /// - public const string MuMuSpecialScreenshot = "MuMuSpecialScreenshot"; + /// + /// The recommended color for LD special screenshot. + /// + public const string LdSpecialScreenshot = "LdSpecialScreenshot"; - /// - /// The recommended color for LD special screenshot. - /// - public const string LdSpecialScreenshot = "LdSpecialScreenshot"; + // I.S. Colors - // I.S. Colors + /// + /// The recommended color for success fights. + /// + public const string SuccessIS = "SuccessIS"; - /// - /// The recommended color for success fights. - /// - public const string SuccessIS = "SuccessIS"; + /// + /// The recommended color for Safe House events. + /// + public const string SafehouseIS = "SafehouseIS"; - /// - /// The recommended color for Safe House events. - /// - public const string SafehouseIS = "SafehouseIS"; + /// + /// The recommended color for Trader events. + /// + public const string TraderIS = "TraderIS"; - /// - /// The recommended color for Trader events. - /// - public const string TraderIS = "TraderIS"; + /// + /// The recommended color for standard events. + /// + public const string EventIS = "EventIS"; - /// - /// The recommended color for standard events. - /// - public const string EventIS = "EventIS"; + /// + /// The recommended color for truth events. + /// + public const string TruthIS = "TruthIS"; - /// - /// The recommended color for truth events. - /// - public const string TruthIS = "TruthIS"; + /// + /// The recommended color for standard fights. + /// + public const string CombatIS = "CombatIS"; - /// - /// The recommended color for standard fights. - /// - public const string CombatIS = "CombatIS"; + /// + /// The recommended color for emergency fights. + /// + public const string EmergencyIS = "EmergencyIS"; - /// - /// The recommended color for emergency fights. - /// - public const string EmergencyIS = "EmergencyIS"; + /// + /// The recommended color for boss fights. + /// + public const string BossIS = "BossIS"; - /// - /// The recommended color for boss fights. - /// - public const string BossIS = "BossIS"; + /// + /// The recommended color for abandoned. + /// + public const string ExplorationAbandonedIS = "ExplorationAbandonedIS"; - /// - /// The recommended color for abandoned. - /// - public const string ExplorationAbandonedIS = "ExplorationAbandonedIS"; - - // 颜色在MaaWpfGui\Res\Themes中定义 - // Brush are defined in MaaWpfGui\Res\Themes - } + // 颜色在MaaWpfGui\Res\Themes中定义 + // Brush are defined in MaaWpfGui\Res\Themes } diff --git a/src/MaaWpfGui/Extensions/ComboBoxExtensions.cs b/src/MaaWpfGui/Extensions/ComboBoxExtensions.cs index 5463592022..c51ae8aa31 100644 --- a/src/MaaWpfGui/Extensions/ComboBoxExtensions.cs +++ b/src/MaaWpfGui/Extensions/ComboBoxExtensions.cs @@ -17,111 +17,109 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Input; using Serilog; -using static System.Net.Mime.MediaTypeNames; -namespace MaaWpfGui.Extensions +namespace MaaWpfGui.Extensions; + +/// +/// Extensions +/// +public static class ComboBoxExtensions { - /// - /// Extensions - /// - public static class ComboBoxExtensions - { - private static readonly ILogger _logger = Log.ForContext("SourceContext", "ComboBoxExtensions"); - private const string InputTag = "TextInput"; - private const string SelectionTag = "Selection"; + private static readonly ILogger _logger = Log.ForContext("SourceContext", "ComboBoxExtensions"); + private const string InputTag = "TextInput"; + private const string SelectionTag = "Selection"; - /// - /// Make searchable - /// - /// Target - public static void MakeComboBoxSearchable(this ComboBox targetComboBox) + /// + /// Make searchable + /// + /// Target + public static void MakeComboBoxSearchable(this ComboBox targetComboBox) + { + if (targetComboBox?.Template.FindName("PART_EditableTextBox", targetComboBox) is not TextBox targetTextBox) { - if (targetComboBox?.Template.FindName("PART_EditableTextBox", targetComboBox) is not TextBox targetTextBox) + return; + } + + targetComboBox.Items.IsLiveFiltering = true; + targetComboBox.StaysOpenOnEdit = true; + targetComboBox.IsEditable = true; + targetComboBox.IsTextSearchEnabled = false; + + targetComboBox.Tag = SelectionTag; + + targetTextBox.PreviewKeyDown += (_, ev) => + { + if (ev.Key is Key.Enter or Key.Return or Key.Tab) { return; } - targetComboBox.Items.IsLiveFiltering = true; - targetComboBox.StaysOpenOnEdit = true; - targetComboBox.IsEditable = true; - targetComboBox.IsTextSearchEnabled = false; - - targetComboBox.Tag = SelectionTag; - - targetTextBox.PreviewKeyDown += (_, ev) => + if (targetComboBox.Tag is SelectionTag) { - if (ev.Key is Key.Enter or Key.Return or Key.Tab) - { - return; - } + var text = targetComboBox.SelectedItem?.ToString() ?? string.Empty; + _logger.Debug("Switching to input mode with text: {Text}", text); + targetComboBox.SelectedItem = null; + targetTextBox.Text = text; + targetTextBox.Select(text.Length, 0); + } - if (targetComboBox.Tag is SelectionTag) - { - var text = targetComboBox.SelectedItem?.ToString() ?? string.Empty; - _logger.Debug("Switching to input mode with text: {Text}", text); - targetComboBox.SelectedItem = null; - targetTextBox.Text = text; - targetTextBox.Select(text.Length, 0); - } + // switch to input mode + targetComboBox.Tag = InputTag; + targetComboBox.IsDropDownOpen = true; + }; - // switch to input mode - targetComboBox.Tag = InputTag; - targetComboBox.IsDropDownOpen = true; - }; - - targetTextBox.TextChanged += (_, _) => + targetTextBox.TextChanged += (_, _) => + { + if (targetComboBox.Tag is SelectionTag) { - if (targetComboBox.Tag is SelectionTag) - { - return; - } + return; + } - var searchTerm = targetTextBox.Text; - _logger.Debug("Searching for: {SearchTerm}", searchTerm); + var searchTerm = targetTextBox.Text; + _logger.Debug("Searching for: {SearchTerm}", searchTerm); - // 如果文字完全匹配某个选项,恢复完整列表 - object exactMatchItem = targetComboBox.ItemsSource.Cast().FirstOrDefault(obj => obj?.ToString() == searchTerm); + // 如果文字完全匹配某个选项,恢复完整列表 + object exactMatchItem = targetComboBox.ItemsSource.Cast().FirstOrDefault(obj => obj?.ToString() == searchTerm); - if (exactMatchItem != null) - { - targetComboBox.Items.Filter = null; - targetComboBox.SelectedItem = exactMatchItem; - targetComboBox.Dispatcher.BeginInvoke(new Action(() => - { - targetComboBox.UpdateLayout(); - if (targetComboBox.ItemContainerGenerator.ContainerFromItem(exactMatchItem) is FrameworkElement element) - { - element.BringIntoView(); - } - }), System.Windows.Threading.DispatcherPriority.Background); - } - else - { - targetComboBox.Items.Filter = item => item?.ToString()?.Contains(searchTerm) ?? false; - } - }; - - targetComboBox.SelectionChanged += (_, _) => - { - if (targetComboBox.SelectedItem != null) - { - targetComboBox.Tag = SelectionTag; - } - - targetComboBox.Dispatcher.BeginInvoke(new Action(() => - { - targetTextBox.Select(targetTextBox.Text.Length, 0); - }), System.Windows.Threading.DispatcherPriority.Background); - }; - - targetComboBox.DropDownOpened += (_, _) => + if (exactMatchItem != null) { targetComboBox.Items.Filter = null; + targetComboBox.SelectedItem = exactMatchItem; targetComboBox.Dispatcher.BeginInvoke(new Action(() => { - targetTextBox.Select(targetTextBox.Text.Length, 0); + targetComboBox.UpdateLayout(); + if (targetComboBox.ItemContainerGenerator.ContainerFromItem(exactMatchItem) is FrameworkElement element) + { + element.BringIntoView(); + } }), System.Windows.Threading.DispatcherPriority.Background); - }; - } + } + else + { + targetComboBox.Items.Filter = item => item?.ToString()?.Contains(searchTerm) ?? false; + } + }; + + targetComboBox.SelectionChanged += (_, _) => + { + if (targetComboBox.SelectedItem != null) + { + targetComboBox.Tag = SelectionTag; + } + + targetComboBox.Dispatcher.BeginInvoke(new Action(() => + { + targetTextBox.Select(targetTextBox.Text.Length, 0); + }), System.Windows.Threading.DispatcherPriority.Background); + }; + + targetComboBox.DropDownOpened += (_, _) => + { + targetComboBox.Items.Filter = null; + targetComboBox.Dispatcher.BeginInvoke(new Action(() => + { + targetTextBox.Select(targetTextBox.Text.Length, 0); + }), System.Windows.Threading.DispatcherPriority.Background); + }; } } diff --git a/src/MaaWpfGui/Extensions/DateTimeExtension.cs b/src/MaaWpfGui/Extensions/DateTimeExtension.cs index 072872d190..d25053c601 100644 --- a/src/MaaWpfGui/Extensions/DateTimeExtension.cs +++ b/src/MaaWpfGui/Extensions/DateTimeExtension.cs @@ -15,72 +15,70 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using MaaWpfGui.Constants; using MaaWpfGui.Helper; -namespace MaaWpfGui.Extensions +namespace MaaWpfGui.Extensions; + +public static class DateTimeExtension { - public static class DateTimeExtension + private const int YjDayStartHour = 4; + + private static string ClientType => ConfigurationHelper.GetValue(ConfigurationKeys.ClientType, string.Empty); + + private static readonly Dictionary _clientTypeTimezone = new() { - private const int YjDayStartHour = 4; + { string.Empty, 8 }, + { "Official", 8 }, + { "Bilibili", 8 }, + { "txwy", 8 }, + { "YoStarEN", -7 }, + { "YoStarJP", 9 }, + { "YoStarKR", 9 }, + }; - private static string ClientType => ConfigurationHelper.GetValue(ConfigurationKeys.ClientType, string.Empty); + public static DateTime ToYjDateTime(this DateTime dt) + { + return dt.AddHours(_clientTypeTimezone[ClientType] - YjDayStartHour); + } - private static readonly Dictionary _clientTypeTimezone = new() + public static DateTime ToYjDate(this DateTime dt) + { + return dt.ToYjDateTime().Date; + } + + public static string ToFormattedString(this DateTime dt) + { + return dt.ToString("yyyy/MM/dd HH:mm:ss", DateTimeFormatInfo.InvariantInfo); + } + + public static bool IsAprilFoolsDay(this DateTime dt) + { + return dt is { Month: 4, Day: 1 }; + } + + public static CultureInfo CustomCultureInfo => LocalizationHelper.CustomCultureInfo; + + public static string ToLocalTimeString(this DateTime dt, string? format = null) + { + var localTime = dt.Kind switch { - { string.Empty, 8 }, - { "Official", 8 }, - { "Bilibili", 8 }, - { "txwy", 8 }, - { "YoStarEN", -7 }, - { "YoStarJP", 9 }, - { "YoStarKR", 9 }, + DateTimeKind.Utc => dt.ToLocalTime(), + DateTimeKind.Local => dt, + _ => DateTime.SpecifyKind(dt, DateTimeKind.Utc).ToLocalTime(), }; - public static DateTime ToYjDateTime(this DateTime dt) + if (!string.IsNullOrEmpty(format)) { - return dt.AddHours(_clientTypeTimezone[ClientType] - YjDayStartHour); + return localTime.ToString(format, CustomCultureInfo); } - public static DateTime ToYjDate(this DateTime dt) - { - return dt.ToYjDateTime().Date; - } + var dateTimeFormat = LocalizationHelper.FormatDateTime(localTime); + return localTime.ToString($"{dateTimeFormat} HH:mm:ss", CustomCultureInfo); + } - public static string ToFormattedString(this DateTime dt) - { - return dt.ToString("yyyy/MM/dd HH:mm:ss", DateTimeFormatInfo.InvariantInfo); - } - - public static bool IsAprilFoolsDay(this DateTime dt) - { - return dt is { Month: 4, Day: 1 }; - } - - public static CultureInfo CustomCultureInfo => LocalizationHelper.CustomCultureInfo; - - public static string ToLocalTimeString(this DateTime dt, string? format = null) - { - var localTime = dt.Kind switch - { - DateTimeKind.Utc => dt.ToLocalTime(), - DateTimeKind.Local => dt, - _ => DateTime.SpecifyKind(dt, DateTimeKind.Utc).ToLocalTime(), - }; - - if (!string.IsNullOrEmpty(format)) - { - return localTime.ToString(format, CustomCultureInfo); - } - - var dateTimeFormat = LocalizationHelper.FormatDateTime(localTime); - return localTime.ToString($"{dateTimeFormat} HH:mm:ss", CustomCultureInfo); - } - - public static DateTime ToDateTime(this System.Runtime.InteropServices.ComTypes.FILETIME filetime) - { - return DateTime.FromFileTime(((long)filetime.dwHighDateTime << 32) | (uint)filetime.dwLowDateTime); - } + public static DateTime ToDateTime(this System.Runtime.InteropServices.ComTypes.FILETIME filetime) + { + return DateTime.FromFileTime(((long)filetime.dwHighDateTime << 32) | (uint)filetime.dwLowDateTime); } } diff --git a/src/MaaWpfGui/Extensions/HttpResponseLoggingExtension.cs b/src/MaaWpfGui/Extensions/HttpResponseLoggingExtension.cs index b5465fe912..d96c1070b4 100644 --- a/src/MaaWpfGui/Extensions/HttpResponseLoggingExtension.cs +++ b/src/MaaWpfGui/Extensions/HttpResponseLoggingExtension.cs @@ -16,30 +16,29 @@ using System.Net; using System.Net.Http; using Serilog; -namespace MaaWpfGui.Extensions +namespace MaaWpfGui.Extensions; + +public static class HttpResponseLoggingExtension { - public static class HttpResponseLoggingExtension + private static readonly ILogger _logger = Serilog.Log.ForContext("SourceContext", "HttpResponseLoggingExtension"); + + public static void Log(this HttpResponseMessage response, UriPartial uriPartial = UriPartial.Query, double? elapsedMs = null) { - private static readonly ILogger _logger = Serilog.Log.ForContext("SourceContext", "HttpResponseLoggingExtension"); + var method = response?.RequestMessage?.Method; + var uri = response?.RequestMessage?.RequestUri; + var statusCode = response?.StatusCode.ToString(); + var etag = response?.Headers.ETag?.Tag; + var lastModified = response?.Content?.Headers?.LastModified?.ToString("R"); // RFC1123 - public static void Log(this HttpResponseMessage response, UriPartial uriPartial = UriPartial.Query, double? elapsedMs = null) + if (response != null && (response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.NotModified)) { - var method = response?.RequestMessage?.Method; - var uri = response?.RequestMessage?.RequestUri; - var statusCode = response?.StatusCode.ToString(); - var etag = response?.Headers.ETag?.Tag; - var lastModified = response?.Content?.Headers?.LastModified?.ToString("R"); // RFC1123 - - if (response != null && (response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.NotModified)) - { - _logger.Information("HTTP: {StatusCode} {Method} {Url} {ETag} {LastModified} {Elapsed:F3}ms", - statusCode, method, uri?.GetLeftPart(uriPartial), etag, lastModified, elapsedMs); - } - else - { - _logger.Warning("HTTP: {StatusCode} {Method} {Url} {ETag} {LastModified} {Elapsed:F3}ms", - statusCode, method, uri?.GetLeftPart(uriPartial), etag, lastModified, elapsedMs); - } + _logger.Information("HTTP: {StatusCode} {Method} {Url} {ETag} {LastModified} {Elapsed:F3}ms", + statusCode, method, uri?.GetLeftPart(uriPartial), etag, lastModified, elapsedMs); + } + else + { + _logger.Warning("HTTP: {StatusCode} {Method} {Url} {ETag} {LastModified} {Elapsed:F3}ms", + statusCode, method, uri?.GetLeftPart(uriPartial), etag, lastModified, elapsedMs); } } } diff --git a/src/MaaWpfGui/Extensions/NumberExtensions.cs b/src/MaaWpfGui/Extensions/NumberExtensions.cs index c6777b4e25..9abfe1e0f9 100644 --- a/src/MaaWpfGui/Extensions/NumberExtensions.cs +++ b/src/MaaWpfGui/Extensions/NumberExtensions.cs @@ -12,26 +12,25 @@ // #nullable enable -namespace MaaWpfGui.Extensions -{ - public static class NumberExtensions - { - public static string FormatNumber(this int n, bool abbreviateK = true) => n switch - { - >= 1_000_000_000 => $"{n / 1_000_000_000.0:#.#}B", - >= 1_000_000 => $"{n / 1_000_000.0:#.#}M", - >= 10_000 => $"{n / 1_000.0:#.#}k", - >= 1_000 when abbreviateK => $"{n / 1_000.0:#.#}k", - _ => $"{n}", - }; +namespace MaaWpfGui.Extensions; - public static string FormatNumber(this long n, bool abbreviateK = true) => n switch - { - >= 1_000_000_000 => $"{n / 1_000_000_000.0:#.#}B", - >= 1_000_000 => $"{n / 1_000_000.0:#.#}M", - >= 10_000 => $"{n / 1_000.0:#.#}k", - >= 1_000 when abbreviateK => $"{n / 1_000.0:#.#}k", - _ => $"{n}", - }; - } +public static class NumberExtensions +{ + public static string FormatNumber(this int n, bool abbreviateK = true) => n switch + { + >= 1_000_000_000 => $"{n / 1_000_000_000.0:#.#}B", + >= 1_000_000 => $"{n / 1_000_000.0:#.#}M", + >= 10_000 => $"{n / 1_000.0:#.#}k", + >= 1_000 when abbreviateK => $"{n / 1_000.0:#.#}k", + _ => $"{n}", + }; + + public static string FormatNumber(this long n, bool abbreviateK = true) => n switch + { + >= 1_000_000_000 => $"{n / 1_000_000_000.0:#.#}B", + >= 1_000_000 => $"{n / 1_000_000.0:#.#}M", + >= 10_000 => $"{n / 1_000.0:#.#}k", + >= 1_000 when abbreviateK => $"{n / 1_000.0:#.#}k", + _ => $"{n}", + }; } diff --git a/src/MaaWpfGui/Extensions/StringExtensions.cs b/src/MaaWpfGui/Extensions/StringExtensions.cs index 78eb4cc922..e0d8f9b840 100644 --- a/src/MaaWpfGui/Extensions/StringExtensions.cs +++ b/src/MaaWpfGui/Extensions/StringExtensions.cs @@ -15,39 +15,38 @@ using System; using System.Security.Cryptography; using System.Text; -namespace MaaWpfGui.Extensions +namespace MaaWpfGui.Extensions; + +public static class StringExtensions { - public static class StringExtensions + /// + /// Mask the string with a visible start and end. + /// + /// The string to be masked. + /// The number of characters to remain visible at the start of the string. + /// The number of characters to remain visible at the end of the string. + /// The character used for masking. + /// A masked string with the specified number of visible characters at the start and end. + public static string Mask(this string input, int visibleStartLength = 3, int visibleEndLength = 3, char maskChar = '*') { - /// - /// Mask the string with a visible start and end. - /// - /// The string to be masked. - /// The number of characters to remain visible at the start of the string. - /// The number of characters to remain visible at the end of the string. - /// The character used for masking. - /// A masked string with the specified number of visible characters at the start and end. - public static string Mask(this string input, int visibleStartLength = 3, int visibleEndLength = 3, char maskChar = '*') + if (input.Length <= visibleStartLength + visibleEndLength) { - if (input.Length <= visibleStartLength + visibleEndLength) - { - return new(maskChar, input.Length); - } - - var maskLength = input.Length - visibleStartLength - visibleEndLength; - var maskedPart = new string(maskChar, maskLength); - return input[..visibleStartLength] + maskedPart + input[^visibleEndLength..]; + return new(maskChar, input.Length); } - /// - /// Get the stable hash of the string. - /// - /// input text - /// stable hash - public static string StableHash(this string input) - { - var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input)); - return BitConverter.ToString(hash).Replace("-", string.Empty); - } + var maskLength = input.Length - visibleStartLength - visibleEndLength; + var maskedPart = new string(maskChar, maskLength); + return input[..visibleStartLength] + maskedPart + input[^visibleEndLength..]; + } + + /// + /// Get the stable hash of the string. + /// + /// input text + /// stable hash + public static string StableHash(this string input) + { + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input)); + return BitConverter.ToString(hash).Replace("-", string.Empty); } } diff --git a/src/MaaWpfGui/Extensions/UIBehaviors/ClipboardInterceptor.cs b/src/MaaWpfGui/Extensions/UIBehaviors/ClipboardInterceptor.cs index e6febf5459..30c7af6f09 100644 --- a/src/MaaWpfGui/Extensions/UIBehaviors/ClipboardInterceptor.cs +++ b/src/MaaWpfGui/Extensions/UIBehaviors/ClipboardInterceptor.cs @@ -17,257 +17,256 @@ using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; -namespace MaaWpfGui.Extensions.UIBehaviors +namespace MaaWpfGui.Extensions.UIBehaviors; + +public static class ClipboardInterceptor { - public static class ClipboardInterceptor + public static readonly DependencyProperty EnableSafeClipboardProperty = + DependencyProperty.RegisterAttached( + "EnableSafeClipboard", + typeof(bool), + typeof(ClipboardInterceptor), + new(false, OnEnableSafeClipboardChanged)); + + public static void SetEnableSafeClipboard(DependencyObject element, bool value) + => element.SetValue(EnableSafeClipboardProperty, value); + + public static bool GetEnableSafeClipboard(DependencyObject element) + => (bool)element.GetValue(EnableSafeClipboardProperty); + + private static void OnEnableSafeClipboardChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - public static readonly DependencyProperty EnableSafeClipboardProperty = - DependencyProperty.RegisterAttached( - "EnableSafeClipboard", - typeof(bool), - typeof(ClipboardInterceptor), - new(false, OnEnableSafeClipboardChanged)); - - public static void SetEnableSafeClipboard(DependencyObject element, bool value) - => element.SetValue(EnableSafeClipboardProperty, value); - - public static bool GetEnableSafeClipboard(DependencyObject element) - => (bool)element.GetValue(EnableSafeClipboardProperty); - - private static void OnEnableSafeClipboardChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + switch (d) { - switch (d) - { - case TextBox tb when (bool)e.NewValue: - AddCommandBindingsToTextBox(tb); - break; - case RichTextBox rtb when (bool)e.NewValue: - AddCommandBindingsToRichTextBox(rtb); - break; - case DataGrid dg when (bool)e.NewValue: - AddCommandBindingsToDataGrid(dg); - break; - case ComboBox cb when (bool)e.NewValue && cb.IsEditable: - cb.Loaded += OnComboBoxLoaded; - break; - } - } - - private static void AddCommandBindingsToTextBox(TextBox tb) - { - tb.CommandBindings.Add(new(ApplicationCommands.Copy, OnCopyTextBox)); - tb.CommandBindings.Add(new(ApplicationCommands.Cut, OnCutTextBox)); - tb.CommandBindings.Add(new(ApplicationCommands.Paste, OnPasteTextBox)); - } - - private static void AddCommandBindingsToRichTextBox(RichTextBox rtb) - { - rtb.CommandBindings.Add(new(ApplicationCommands.Copy, OnCopyRichTextBox)); - rtb.CommandBindings.Add(new(ApplicationCommands.Cut, OnCutRichTextBox)); - rtb.CommandBindings.Add(new(ApplicationCommands.Paste, OnPasteRichTextBox)); - } - - private static void AddCommandBindingsToDataGrid(DataGrid dg) - { - dg.CommandBindings.Add(new(ApplicationCommands.Copy, OnCopyDataGrid)); - } - - private static void OnComboBoxLoaded(object sender, RoutedEventArgs e) - { - if (sender is not ComboBox cb) - { - return; - } - - if (cb.Template.FindName("PART_EditableTextBox", cb) is TextBox textBox) - { - AddCommandBindingsToTextBox(textBox); - } - } - - private static void OnCopyTextBox(object sender, ExecutedRoutedEventArgs e) - { - if (sender is not TextBox { SelectionLength: > 0 } tb) - { - return; - } - - try - { - System.Windows.Forms.Clipboard.Clear(); - System.Windows.Forms.Clipboard.SetDataObject(tb.SelectedText, true); - } - catch - { - // ignored - } - - e.Handled = true; - } - - private static void OnCutTextBox(object sender, ExecutedRoutedEventArgs e) - { - if (sender is not TextBox { SelectionLength: > 0 } tb) - { - return; - } - - try - { - System.Windows.Forms.Clipboard.Clear(); - System.Windows.Forms.Clipboard.SetDataObject(tb.SelectedText, true); - } - catch - { - // ignored - } - - tb.SelectedText = string.Empty; - e.Handled = true; - } - - private static void OnPasteTextBox(object sender, ExecutedRoutedEventArgs e) - { - if (sender is not TextBox tb) - { - return; - } - - if (System.Windows.Forms.Clipboard.ContainsText()) - { - var pasteText = System.Windows.Forms.Clipboard.GetText(); - - var start = tb.SelectionStart; - - tb.SelectedText = pasteText; - tb.CaretIndex = start + pasteText.Length; - tb.SelectionLength = 0; - } - - e.Handled = true; - } - - private static void OnCopyRichTextBox(object sender, ExecutedRoutedEventArgs e) - { - if (sender is not RichTextBox rtb) - { - return; - } - - var textRange = new TextRange(rtb.Selection.Start, rtb.Selection.End); - if (string.IsNullOrEmpty(textRange.Text)) - { - return; - } - - try - { - System.Windows.Forms.Clipboard.Clear(); - System.Windows.Forms.Clipboard.SetDataObject(textRange.Text, true); - } - catch - { - // ignored - } - - e.Handled = true; - } - - private static void OnCutRichTextBox(object sender, ExecutedRoutedEventArgs e) - { - if (sender is not RichTextBox rtb) - { - return; - } - - var selection = new TextRange(rtb.Selection.Start, rtb.Selection.End); - if (string.IsNullOrEmpty(selection.Text)) - { - return; - } - - try - { - System.Windows.Forms.Clipboard.Clear(); - System.Windows.Forms.Clipboard.SetDataObject(selection.Text, true); - } - catch - { - // ignored - } - - selection.Text = string.Empty; - e.Handled = true; - } - - private static void OnPasteRichTextBox(object sender, ExecutedRoutedEventArgs e) - { - if (sender is not RichTextBox rtb) - { - return; - } - - if (!System.Windows.Forms.Clipboard.ContainsText()) - { - return; - } - - var pasteText = System.Windows.Forms.Clipboard.GetText(); - - var selection = rtb.Selection; - selection.Text = pasteText; - - var caretPos = selection.End; - rtb.CaretPosition = caretPos; - rtb.Selection.Select(caretPos, caretPos); - - e.Handled = true; - } - - private static void OnCopyDataGrid(object sender, ExecutedRoutedEventArgs e) - { - if (sender is not DataGrid dg) - { - return; - } - - // 获取选中单元格内容,拼成制表符分隔的文本 - var selectedCells = dg.SelectedCells; - if (selectedCells == null || selectedCells.Count == 0) - { - return; - } - - var sb = new System.Text.StringBuilder(); - var rowGroups = selectedCells.GroupBy(c => c.Item); - - foreach (var row in rowGroups) - { - var rowText = string.Join("\t", row.Select(cell => - { - if (cell.Column.GetCellContent(cell.Item) is TextBlock tb) - { - return tb.Text; - } - - return string.Empty; - })); - sb.AppendLine(rowText); - } - - var sbStr = sb.ToString().TrimEnd('\r', '\n'); - - try - { - System.Windows.Forms.Clipboard.Clear(); - System.Windows.Forms.Clipboard.SetDataObject(sbStr, true); - } - catch - { - // ignored - } - - e.Handled = true; + case TextBox tb when (bool)e.NewValue: + AddCommandBindingsToTextBox(tb); + break; + case RichTextBox rtb when (bool)e.NewValue: + AddCommandBindingsToRichTextBox(rtb); + break; + case DataGrid dg when (bool)e.NewValue: + AddCommandBindingsToDataGrid(dg); + break; + case ComboBox cb when (bool)e.NewValue && cb.IsEditable: + cb.Loaded += OnComboBoxLoaded; + break; } } + + private static void AddCommandBindingsToTextBox(TextBox tb) + { + tb.CommandBindings.Add(new(ApplicationCommands.Copy, OnCopyTextBox)); + tb.CommandBindings.Add(new(ApplicationCommands.Cut, OnCutTextBox)); + tb.CommandBindings.Add(new(ApplicationCommands.Paste, OnPasteTextBox)); + } + + private static void AddCommandBindingsToRichTextBox(RichTextBox rtb) + { + rtb.CommandBindings.Add(new(ApplicationCommands.Copy, OnCopyRichTextBox)); + rtb.CommandBindings.Add(new(ApplicationCommands.Cut, OnCutRichTextBox)); + rtb.CommandBindings.Add(new(ApplicationCommands.Paste, OnPasteRichTextBox)); + } + + private static void AddCommandBindingsToDataGrid(DataGrid dg) + { + dg.CommandBindings.Add(new(ApplicationCommands.Copy, OnCopyDataGrid)); + } + + private static void OnComboBoxLoaded(object sender, RoutedEventArgs e) + { + if (sender is not ComboBox cb) + { + return; + } + + if (cb.Template.FindName("PART_EditableTextBox", cb) is TextBox textBox) + { + AddCommandBindingsToTextBox(textBox); + } + } + + private static void OnCopyTextBox(object sender, ExecutedRoutedEventArgs e) + { + if (sender is not TextBox { SelectionLength: > 0 } tb) + { + return; + } + + try + { + System.Windows.Forms.Clipboard.Clear(); + System.Windows.Forms.Clipboard.SetDataObject(tb.SelectedText, true); + } + catch + { + // ignored + } + + e.Handled = true; + } + + private static void OnCutTextBox(object sender, ExecutedRoutedEventArgs e) + { + if (sender is not TextBox { SelectionLength: > 0 } tb) + { + return; + } + + try + { + System.Windows.Forms.Clipboard.Clear(); + System.Windows.Forms.Clipboard.SetDataObject(tb.SelectedText, true); + } + catch + { + // ignored + } + + tb.SelectedText = string.Empty; + e.Handled = true; + } + + private static void OnPasteTextBox(object sender, ExecutedRoutedEventArgs e) + { + if (sender is not TextBox tb) + { + return; + } + + if (System.Windows.Forms.Clipboard.ContainsText()) + { + var pasteText = System.Windows.Forms.Clipboard.GetText(); + + var start = tb.SelectionStart; + + tb.SelectedText = pasteText; + tb.CaretIndex = start + pasteText.Length; + tb.SelectionLength = 0; + } + + e.Handled = true; + } + + private static void OnCopyRichTextBox(object sender, ExecutedRoutedEventArgs e) + { + if (sender is not RichTextBox rtb) + { + return; + } + + var textRange = new TextRange(rtb.Selection.Start, rtb.Selection.End); + if (string.IsNullOrEmpty(textRange.Text)) + { + return; + } + + try + { + System.Windows.Forms.Clipboard.Clear(); + System.Windows.Forms.Clipboard.SetDataObject(textRange.Text, true); + } + catch + { + // ignored + } + + e.Handled = true; + } + + private static void OnCutRichTextBox(object sender, ExecutedRoutedEventArgs e) + { + if (sender is not RichTextBox rtb) + { + return; + } + + var selection = new TextRange(rtb.Selection.Start, rtb.Selection.End); + if (string.IsNullOrEmpty(selection.Text)) + { + return; + } + + try + { + System.Windows.Forms.Clipboard.Clear(); + System.Windows.Forms.Clipboard.SetDataObject(selection.Text, true); + } + catch + { + // ignored + } + + selection.Text = string.Empty; + e.Handled = true; + } + + private static void OnPasteRichTextBox(object sender, ExecutedRoutedEventArgs e) + { + if (sender is not RichTextBox rtb) + { + return; + } + + if (!System.Windows.Forms.Clipboard.ContainsText()) + { + return; + } + + var pasteText = System.Windows.Forms.Clipboard.GetText(); + + var selection = rtb.Selection; + selection.Text = pasteText; + + var caretPos = selection.End; + rtb.CaretPosition = caretPos; + rtb.Selection.Select(caretPos, caretPos); + + e.Handled = true; + } + + private static void OnCopyDataGrid(object sender, ExecutedRoutedEventArgs e) + { + if (sender is not DataGrid dg) + { + return; + } + + // 获取选中单元格内容,拼成制表符分隔的文本 + var selectedCells = dg.SelectedCells; + if (selectedCells == null || selectedCells.Count == 0) + { + return; + } + + var sb = new System.Text.StringBuilder(); + var rowGroups = selectedCells.GroupBy(c => c.Item); + + foreach (var row in rowGroups) + { + var rowText = string.Join("\t", row.Select(cell => + { + if (cell.Column.GetCellContent(cell.Item) is TextBlock tb) + { + return tb.Text; + } + + return string.Empty; + })); + sb.AppendLine(rowText); + } + + var sbStr = sb.ToString().TrimEnd('\r', '\n'); + + try + { + System.Windows.Forms.Clipboard.Clear(); + System.Windows.Forms.Clipboard.SetDataObject(sbStr, true); + } + catch + { + // ignored + } + + e.Handled = true; + } } diff --git a/src/MaaWpfGui/Extensions/UIBehaviors/ResourceReferenceHelper.cs b/src/MaaWpfGui/Extensions/UIBehaviors/ResourceReferenceHelper.cs index ae0df218e2..981608a3fb 100644 --- a/src/MaaWpfGui/Extensions/UIBehaviors/ResourceReferenceHelper.cs +++ b/src/MaaWpfGui/Extensions/UIBehaviors/ResourceReferenceHelper.cs @@ -14,88 +14,87 @@ using System.Windows; using System.Windows.Controls; -namespace MaaWpfGui.Extensions.UIBehaviors +namespace MaaWpfGui.Extensions.UIBehaviors; + +public static class ResourceReferenceHelper { - public static class ResourceReferenceHelper - { - #region Foreground - public static readonly DependencyProperty ForegroundResourceKeyProperty = - DependencyProperty.RegisterAttached( - "ForegroundResourceKey", - typeof(string), - typeof(ResourceReferenceHelper), - new PropertyMetadata(null, (d, e) => + #region Foreground + public static readonly DependencyProperty ForegroundResourceKeyProperty = + DependencyProperty.RegisterAttached( + "ForegroundResourceKey", + typeof(string), + typeof(ResourceReferenceHelper), + new PropertyMetadata(null, (d, e) => + { + if (d is FrameworkElement fe && e.NewValue is string key) { - if (d is FrameworkElement fe && e.NewValue is string key) - { - fe.SetResourceReference(Control.ForegroundProperty, key); - } - })); + fe.SetResourceReference(Control.ForegroundProperty, key); + } + })); - public static void SetForegroundResourceKey(DependencyObject d, string value) => d.SetValue(ForegroundResourceKeyProperty, value); + public static void SetForegroundResourceKey(DependencyObject d, string value) => d.SetValue(ForegroundResourceKeyProperty, value); - public static string GetForegroundResourceKey(DependencyObject d) => (string)d.GetValue(ForegroundResourceKeyProperty); - #endregion + public static string GetForegroundResourceKey(DependencyObject d) => (string)d.GetValue(ForegroundResourceKeyProperty); + #endregion - #region Background - public static readonly DependencyProperty BackgroundResourceKeyProperty = - DependencyProperty.RegisterAttached( - "BackgroundResourceKey", - typeof(string), - typeof(ResourceReferenceHelper), - new PropertyMetadata(null, (d, e) => + #region Background + public static readonly DependencyProperty BackgroundResourceKeyProperty = + DependencyProperty.RegisterAttached( + "BackgroundResourceKey", + typeof(string), + typeof(ResourceReferenceHelper), + new PropertyMetadata(null, (d, e) => + { + if (d is FrameworkElement fe && e.NewValue is string key) { - if (d is FrameworkElement fe && e.NewValue is string key) - { - fe.SetResourceReference(Control.BackgroundProperty, key); - } - })); + fe.SetResourceReference(Control.BackgroundProperty, key); + } + })); - public static void SetBackgroundResourceKey(DependencyObject d, string value) => d.SetValue(BackgroundResourceKeyProperty, value); + public static void SetBackgroundResourceKey(DependencyObject d, string value) => d.SetValue(BackgroundResourceKeyProperty, value); - public static string GetBackgroundResourceKey(DependencyObject d) => (string)d.GetValue(BackgroundResourceKeyProperty); - #endregion + public static string GetBackgroundResourceKey(DependencyObject d) => (string)d.GetValue(BackgroundResourceKeyProperty); + #endregion - #region BorderBrush - public static readonly DependencyProperty BorderBrushResourceKeyProperty = - DependencyProperty.RegisterAttached( - "BorderBrushResourceKey", - typeof(string), - typeof(ResourceReferenceHelper), - new PropertyMetadata(null, (d, e) => + #region BorderBrush + public static readonly DependencyProperty BorderBrushResourceKeyProperty = + DependencyProperty.RegisterAttached( + "BorderBrushResourceKey", + typeof(string), + typeof(ResourceReferenceHelper), + new PropertyMetadata(null, (d, e) => + { + if (d is Border border && e.NewValue is string key) { - if (d is Border border && e.NewValue is string key) - { - border.SetResourceReference(Border.BorderBrushProperty, key); - } - })); + border.SetResourceReference(Border.BorderBrushProperty, key); + } + })); - public static void SetBorderBrushResourceKey(DependencyObject d, string value) => - d.SetValue(BorderBrushResourceKeyProperty, value); + public static void SetBorderBrushResourceKey(DependencyObject d, string value) => + d.SetValue(BorderBrushResourceKeyProperty, value); - public static string GetBorderBrushResourceKey(DependencyObject d) => - (string)d.GetValue(BorderBrushResourceKeyProperty); - #endregion + public static string GetBorderBrushResourceKey(DependencyObject d) => + (string)d.GetValue(BorderBrushResourceKeyProperty); + #endregion - #region Fill - public static readonly DependencyProperty FillResourceKeyProperty = - DependencyProperty.RegisterAttached( - "FillResourceKey", - typeof(string), - typeof(ResourceReferenceHelper), - new PropertyMetadata(null, (d, e) => + #region Fill + public static readonly DependencyProperty FillResourceKeyProperty = + DependencyProperty.RegisterAttached( + "FillResourceKey", + typeof(string), + typeof(ResourceReferenceHelper), + new PropertyMetadata(null, (d, e) => + { + if (d is System.Windows.Shapes.Shape shape && e.NewValue is string key) { - if (d is System.Windows.Shapes.Shape shape && e.NewValue is string key) - { - shape.SetResourceReference(System.Windows.Shapes.Shape.FillProperty, key); - } - })); + shape.SetResourceReference(System.Windows.Shapes.Shape.FillProperty, key); + } + })); - public static void SetFillResourceKey(DependencyObject d, string value) => - d.SetValue(FillResourceKeyProperty, value); + public static void SetFillResourceKey(DependencyObject d, string value) => + d.SetValue(FillResourceKeyProperty, value); - public static string GetFillResourceKey(DependencyObject d) => - (string)d.GetValue(FillResourceKeyProperty); - #endregion - } + public static string GetFillResourceKey(DependencyObject d) => + (string)d.GetValue(FillResourceKeyProperty); + #endregion } diff --git a/src/MaaWpfGui/Extensions/Win32Extension.cs b/src/MaaWpfGui/Extensions/Win32Extension.cs index 85550cfec0..dd332dd465 100644 --- a/src/MaaWpfGui/Extensions/Win32Extension.cs +++ b/src/MaaWpfGui/Extensions/Win32Extension.cs @@ -18,13 +18,12 @@ using System.Text; using System.Threading.Tasks; using Windows.Win32.Foundation; -namespace MaaWpfGui.Extensions +namespace MaaWpfGui.Extensions; + +internal static class Win32Extension { - internal static class Win32Extension + public static long AsLong(this LUID luid) { - public static long AsLong(this LUID luid) - { - return ((long)luid.HighPart << 32) | luid.LowPart; - } + return ((long)luid.HighPart << 32) | luid.LowPart; } } diff --git a/src/MaaWpfGui/Helper/AchievementTrackerHelper.cs b/src/MaaWpfGui/Helper/AchievementTrackerHelper.cs index 944aef2876..bc5ab5399b 100644 --- a/src/MaaWpfGui/Helper/AchievementTrackerHelper.cs +++ b/src/MaaWpfGui/Helper/AchievementTrackerHelper.cs @@ -31,596 +31,595 @@ using MaaWpfGui.ViewModels.UserControl.Settings; using Newtonsoft.Json.Linq; using Stylet; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +public class AchievementTrackerHelper : PropertyChangedBase { - public class AchievementTrackerHelper : PropertyChangedBase + public AchievementTrackerHelper() { - public AchievementTrackerHelper() + InitializeAchievements(); + Load(); + Sort(); + if (Instances.MainWindowManager is not null) { - InitializeAchievements(); - Load(); - Sort(); - if (Instances.MainWindowManager is not null) + Instances.MainWindowManager.WindowRestored += (_, _) => { - Instances.MainWindowManager.WindowRestored += (_, _) => - { - TryShowPendingGrowls(); - }; - } - - SearchCmd = new RelayCommand(Search); + TryShowPendingGrowls(); + }; } - public static AchievementTrackerHelper Instance { get; } = new(); + SearchCmd = new RelayCommand(Search); + } - private Dictionary _achievements = []; + public static AchievementTrackerHelper Instance { get; } = new(); - public Dictionary Achievements + private Dictionary _achievements = []; + + public Dictionary Achievements + { + get => _achievements; + set { - get => _achievements; - set - { - SetAndNotify(ref _achievements, value); - NotifyOfPropertyChange(nameof(UnlockedCount)); - NotifyOfPropertyChange(nameof(VisibleAchievements)); - } - } - - // 已解锁及非隐藏的未解锁成就 - public Dictionary VisibleAchievements - { - get => Achievements - .Where(kv => kv.Value.IsVisibleInSearch && (kv.Value.IsUnlocked || !kv.Value.IsHidden)) - .ToDictionary(kv => kv.Key, kv => kv.Value); - } - - public ICommand SearchCmd { get; } - - public void Search(string text = "") - { - text = text.Trim(); - if (string.IsNullOrWhiteSpace(text)) - { - foreach (var achievement in Achievements) - { - achievement.Value.IsVisibleInSearch = true; - } - } - else - { - foreach (var (_, value) in Achievements) - { - value.IsVisibleInSearch = value.Title.Contains(text) || value.Description.Contains(text) || value.Conditions.Contains(text); - } - } - + SetAndNotify(ref _achievements, value); + NotifyOfPropertyChange(nameof(UnlockedCount)); NotifyOfPropertyChange(nameof(VisibleAchievements)); } + } - public int UnlockedCount => Achievements.Count(a => a.Value.IsUnlocked); + // 已解锁及非隐藏的未解锁成就 + public Dictionary VisibleAchievements + { + get => Achievements + .Where(kv => kv.Value.IsVisibleInSearch && (kv.Value.IsUnlocked || !kv.Value.IsHidden)) + .ToDictionary(kv => kv.Key, kv => kv.Value); + } - private void Load() + public ICommand SearchCmd { get; } + + public void Search(string text = "") + { + text = text.Trim(); + if (string.IsNullOrWhiteSpace(text)) { - var loadAchievements = JsonDataHelper.Get("Achievement", new Dictionary()) ?? []; - - // 从中读取 IsUnlocked UnlockedTime Progress CustomData 等有关成就状态的信息,覆盖 _achievements 里现有的默认值 - foreach (var (id, saved) in loadAchievements) + foreach (var achievement in Achievements) { - if (!_achievements.TryGetValue(id, out var achievement)) - { - // EDIT:存个屁,不存了 - // 有可能是高版本移到低版本来的,虽然有点抽象,但存一下 - // RegisterAchievement(saved); - continue; - } - - achievement.IsUnlocked = saved.IsUnlocked; - achievement.UnlockedTime = saved.UnlockedTime; - achievement.Progress = saved.Progress; - achievement.CustomData = saved.CustomData; + achievement.Value.IsVisibleInSearch = true; + } + } + else + { + foreach (var (_, value) in Achievements) + { + value.IsVisibleInSearch = value.Title.Contains(text) || value.Description.Contains(text) || value.Conditions.Contains(text); } } - private bool _allowSave = true; + NotifyOfPropertyChange(nameof(VisibleAchievements)); + } - public void Save() - { - if (!_allowSave) - { - return; - } + public int UnlockedCount => Achievements.Count(a => a.Value.IsUnlocked); - Sort(); + private void Load() + { + var loadAchievements = JsonDataHelper.Get("Achievement", new Dictionary()) ?? []; - JsonDataHelper.Set("Achievement", _achievements); - } - - private void Sort() - { - Achievements = _achievements - .OrderByDescending(kv => kv.Value.IsUnlocked) // 已解锁优先 - .ThenByDescending(kv => kv.Value.IsNewUnlock) // 新解锁的排前面 - .ThenBy(kv => kv.Value.Category) // 按类别分组 - .ThenBy(kv => kv.Value.Group) - .ThenBy(kv => kv.Value.Id) // 最后按 Id - .ToDictionary(kv => kv.Key, kv => kv.Value); - } - - public void RegisterAchievement(Achievement achievement) - { - if (!_achievements.TryAdd(achievement.Id, achievement)) - { - return; - } - - // 尝试从相同组的成就中找最大已达成进度 - var similarProgress = _achievements.Values - .Where(a => a.Id != achievement.Id && a.Id.StartsWith(GetGroupPrefix(achievement.Id))) - .Select(a => a.Progress) - .DefaultIfEmpty(0) - .Max(); - - // 如果新成就进度是0,且存在类似成就的进度,就追溯赋值 - if (achievement.Progress == 0 && similarProgress > 0) - { - achievement.Progress = similarProgress; - CheckProgressUnlock(achievement); - } - } - - private static string GetGroupPrefix(string id) - { - // 例如 SanitySpender1 => SanitySpender - return new([.. id.TakeWhile(char.IsLetter)]); - } - - public void Unlock(string id, bool staysOpen = true) - { - if (!_achievements.TryGetValue(id, out var achievement) || achievement.IsUnlocked) - { - return; - } - - achievement.IsUnlocked = true; - achievement.UnlockedTime = DateTime.UtcNow; - achievement.IsNewUnlock = true; - Save(); - - var growlInfo = new GrowlInfo - { - IsCustom = true, - Message = $"{LocalizationHelper.GetString("AchievementCelebrate")}: {achievement.Title}\n{achievement.Description}", - StaysOpen = staysOpen && !SettingsViewModel.AchievementSettings.AchievementPopupAutoClose, - WaitTime = 15, - IconKey = "HangoverGeometry", - IconBrushKey = achievement.MedalBrushKey, - }; - ShowInfo(growlInfo); - } - - public async Task UnlockAll() - { - _allowSave = false; // 禁止保存,避免重复触发 Save - foreach (var achievement in _achievements.Values.Where(a => !a.IsUnlocked)) - { - if (!SettingsViewModel.AchievementSettings.AchievementPopupDisabled) - { - await Task.Delay(100); - } - - Unlock(achievement.Id, false); - } - - _allowSave = true; - Save(); - } - - public void Lock(string id) + // 从中读取 IsUnlocked UnlockedTime Progress CustomData 等有关成就状态的信息,覆盖 _achievements 里现有的默认值 + foreach (var (id, saved) in loadAchievements) { if (!_achievements.TryGetValue(id, out var achievement)) { - return; + // EDIT:存个屁,不存了 + // 有可能是高版本移到低版本来的,虽然有点抽象,但存一下 + // RegisterAchievement(saved); + continue; } - achievement.UnlockedTime = null; - achievement.Progress = 0; - achievement.CustomData = null; - achievement.IsNewUnlock = false; + achievement.IsUnlocked = saved.IsUnlocked; + achievement.UnlockedTime = saved.UnlockedTime; + achievement.Progress = saved.Progress; + achievement.CustomData = saved.CustomData; + } + } - if (achievement.IsUnlocked) - { - achievement.IsUnlocked = false; - NotifyOfPropertyChange(nameof(UnlockedCount)); - } + private bool _allowSave = true; - Save(); + public void Save() + { + if (!_allowSave) + { + return; } - public void LockAll() - { - _allowSave = false; // 禁止保存,避免重复触发 Save - foreach (var achievement in _achievements.Values) - { - Lock(achievement.Id); - } + Sort(); - _allowSave = true; - Save(); + JsonDataHelper.Set("Achievement", _achievements); + } + + private void Sort() + { + Achievements = _achievements + .OrderByDescending(kv => kv.Value.IsUnlocked) // 已解锁优先 + .ThenByDescending(kv => kv.Value.IsNewUnlock) // 新解锁的排前面 + .ThenBy(kv => kv.Value.Category) // 按类别分组 + .ThenBy(kv => kv.Value.Group) + .ThenBy(kv => kv.Value.Id) // 最后按 Id + .ToDictionary(kv => kv.Key, kv => kv.Value); + } + + public void RegisterAchievement(Achievement achievement) + { + if (!_achievements.TryAdd(achievement.Id, achievement)) + { + return; } - private static readonly List _pending = []; + // 尝试从相同组的成就中找最大已达成进度 + var similarProgress = _achievements.Values + .Where(a => a.Id != achievement.Id && a.Id.StartsWith(GetGroupPrefix(achievement.Id))) + .Select(a => a.Progress) + .DefaultIfEmpty(0) + .Max(); - public static void ShowInfo(GrowlInfo info) + // 如果新成就进度是0,且存在类似成就的进度,就追溯赋值 + if (achievement.Progress == 0 && similarProgress > 0) { - // 检查是否禁用了成就通知 - if (SettingsViewModel.AchievementSettings.AchievementPopupDisabled) - { - return; - } - - Execute.OnUIThread(() => - { - var win = Instances.MainWindowManager.GetWindowIfVisible(); - if (win == null) - { - _pending.Add(info); - return; - } - - Growl.Info(info); - }); - } - - public static void TryShowPendingGrowls() - { - foreach (var info in _pending) - { - Growl.Info(info); - } - - _pending.Clear(); - } - - private void CheckProgressUnlock(Achievement achievement) - { - if (achievement.IsUnlocked || - achievement.Target == 0 || - achievement.Progress < achievement.Target) - { - return; - } - - Unlock(achievement.Id); - } - - public void AddProgress(string id, int amount = 1) - { - if (!_achievements.TryGetValue(id, out var achievement)) - { - return; - } - - achievement.Progress += amount; + achievement.Progress = similarProgress; CheckProgressUnlock(achievement); - Save(); + } + } + + private static string GetGroupPrefix(string id) + { + // 例如 SanitySpender1 => SanitySpender + return new([.. id.TakeWhile(char.IsLetter)]); + } + + public void Unlock(string id, bool staysOpen = true) + { + if (!_achievements.TryGetValue(id, out var achievement) || achievement.IsUnlocked) + { + return; } - public void AddProgressToGroup(string groupPrefix, int amount = 1) + achievement.IsUnlocked = true; + achievement.UnlockedTime = DateTime.UtcNow; + achievement.IsNewUnlock = true; + Save(); + + var growlInfo = new GrowlInfo { - foreach (var achievement in _achievements.Values.Where(achievement => achievement.Group == groupPrefix)) + IsCustom = true, + Message = $"{LocalizationHelper.GetString("AchievementCelebrate")}: {achievement.Title}\n{achievement.Description}", + StaysOpen = staysOpen && !SettingsViewModel.AchievementSettings.AchievementPopupAutoClose, + WaitTime = 15, + IconKey = "HangoverGeometry", + IconBrushKey = achievement.MedalBrushKey, + }; + ShowInfo(growlInfo); + } + + public async Task UnlockAll() + { + _allowSave = false; // 禁止保存,避免重复触发 Save + foreach (var achievement in _achievements.Values.Where(a => !a.IsUnlocked)) + { + if (!SettingsViewModel.AchievementSettings.AchievementPopupDisabled) { - AddProgress(achievement.Id, amount); + await Task.Delay(100); } + + Unlock(achievement.Id, false); } - public void SetProgress(string id, int progress) + _allowSave = true; + Save(); + } + + public void Lock(string id) + { + if (!_achievements.TryGetValue(id, out var achievement)) { - if (!_achievements.TryGetValue(id, out var achievement)) + return; + } + + achievement.UnlockedTime = null; + achievement.Progress = 0; + achievement.CustomData = null; + achievement.IsNewUnlock = false; + + if (achievement.IsUnlocked) + { + achievement.IsUnlocked = false; + NotifyOfPropertyChange(nameof(UnlockedCount)); + } + + Save(); + } + + public void LockAll() + { + _allowSave = false; // 禁止保存,避免重复触发 Save + foreach (var achievement in _achievements.Values) + { + Lock(achievement.Id); + } + + _allowSave = true; + Save(); + } + + private static readonly List _pending = []; + + public static void ShowInfo(GrowlInfo info) + { + // 检查是否禁用了成就通知 + if (SettingsViewModel.AchievementSettings.AchievementPopupDisabled) + { + return; + } + + Execute.OnUIThread(() => + { + var win = Instances.MainWindowManager.GetWindowIfVisible(); + if (win == null) { + _pending.Add(info); return; } - achievement.Progress = progress; - CheckProgressUnlock(achievement); - Save(); - } + Growl.Info(info); + }); + } - public void SetProgressToGroup(string groupPrefix, int progress) + public static void TryShowPendingGrowls() + { + foreach (var info in _pending) { - foreach (var achievement in _achievements.Values.Where(achievement => achievement.Group == groupPrefix)) - { - SetProgress(achievement.Id, progress); - } + Growl.Info(info); } - public Dictionary GetAchievementCustomData(string id) + _pending.Clear(); + } + + private void CheckProgressUnlock(Achievement achievement) + { + if (achievement.IsUnlocked || + achievement.Target == 0 || + achievement.Progress < achievement.Target) { - if (!_achievements.TryGetValue(id, out var achievement) || achievement.CustomData == null) - { - return []; - } - - return achievement.CustomData; + return; } - public JToken? GetAchievementCustomData(string id, string key) + Unlock(achievement.Id); + } + + public void AddProgress(string id, int amount = 1) + { + if (!_achievements.TryGetValue(id, out var achievement)) { - if (!_achievements.TryGetValue(id, out var achievement) || achievement.CustomData == null) - { - return null; - } - - achievement.CustomData.TryGetValue(key, out var value); - return value; + return; } - public void SetAchievementCustomData(string id, string key, JToken value) + achievement.Progress += amount; + CheckProgressUnlock(achievement); + Save(); + } + + public void AddProgressToGroup(string groupPrefix, int amount = 1) + { + foreach (var achievement in _achievements.Values.Where(achievement => achievement.Group == groupPrefix)) { - if (!_achievements.TryGetValue(id, out var achievement)) - { - return; - } + AddProgress(achievement.Id, amount); + } + } - achievement.CustomData ??= []; - achievement.CustomData[key] = value; - Save(); + public void SetProgress(string id, int progress) + { + if (!_achievements.TryGetValue(id, out var achievement)) + { + return; } - public bool IsUnlocked(string id) => _achievements.TryGetValue(id, out var a) && a.IsUnlocked; + achievement.Progress = progress; + CheckProgressUnlock(achievement); + Save(); + } - public Achievement? Get(string id) => _achievements.GetValueOrDefault(id); + public void SetProgressToGroup(string groupPrefix, int progress) + { + foreach (var achievement in _achievements.Values.Where(achievement => achievement.Group == groupPrefix)) + { + SetProgress(achievement.Id, progress); + } + } - #region 工厂函数 + public Dictionary GetAchievementCustomData(string id) + { + if (!_achievements.TryGetValue(id, out var achievement) || achievement.CustomData == null) + { + return []; + } - private static Achievement BasicUsage(string id, string group = "", int? target = null, bool isHidden = false, bool isRare = false) - => new() { Id = id, Group = group, Target = target ?? 0, IsHidden = isHidden, Category = AchievementCategory.BasicUsage, IsRare = isRare }; + return achievement.CustomData; + } - private static Achievement FeatureExploration(string id, string group = "", int? target = null, bool isHidden = false, bool isRare = false) - => new() { Id = id, Group = group, Target = target ?? 0, IsHidden = isHidden, Category = AchievementCategory.FeatureExploration, IsRare = isRare }; + public JToken? GetAchievementCustomData(string id, string key) + { + if (!_achievements.TryGetValue(id, out var achievement) || achievement.CustomData == null) + { + return null; + } - private static Achievement AutoBattle(string id, string group = "", int? target = null, bool isHidden = false, bool isRare = false) - => new() { Id = id, Group = group, Target = target ?? 0, IsHidden = isHidden, Category = AchievementCategory.AutoBattle, IsRare = isRare }; + achievement.CustomData.TryGetValue(key, out var value); + return value; + } - private static Achievement Humor(string id, string group = "", int? target = null, bool isHidden = false, bool isRare = false) - => new() { Id = id, Group = group, Target = target ?? 0, IsHidden = isHidden, Category = AchievementCategory.Humor, IsRare = isRare }; + public void SetAchievementCustomData(string id, string key, JToken value) + { + if (!_achievements.TryGetValue(id, out var achievement)) + { + return; + } - private static Achievement BugRelated(string id, string group = "", int? target = null, bool isHidden = false, bool isRare = false) - => new() { Id = id, Group = group, Target = target ?? 0, IsHidden = isHidden, Category = AchievementCategory.BugRelated, IsRare = isRare }; + achievement.CustomData ??= []; + achievement.CustomData[key] = value; + Save(); + } - private static Achievement Behavior(string id, string group = "", int? target = null, bool isHidden = false, bool isRare = false) - => new() { Id = id, Group = group, Target = target ?? 0, IsHidden = isHidden, Category = AchievementCategory.Behavior, IsRare = isRare }; + public bool IsUnlocked(string id) => _achievements.TryGetValue(id, out var a) && a.IsUnlocked; - private static Achievement EasterEgg(string id, string group = "", int? target = null, bool isHidden = false, bool isRare = false) - => new() { Id = id, Group = group, Target = target ?? 0, IsHidden = isHidden, Category = AchievementCategory.EasterEgg, IsRare = isRare }; + public Achievement? Get(string id) => _achievements.GetValueOrDefault(id); + #region 工厂函数 + + private static Achievement BasicUsage(string id, string group = "", int? target = null, bool isHidden = false, bool isRare = false) + => new() { Id = id, Group = group, Target = target ?? 0, IsHidden = isHidden, Category = AchievementCategory.BasicUsage, IsRare = isRare }; + + private static Achievement FeatureExploration(string id, string group = "", int? target = null, bool isHidden = false, bool isRare = false) + => new() { Id = id, Group = group, Target = target ?? 0, IsHidden = isHidden, Category = AchievementCategory.FeatureExploration, IsRare = isRare }; + + private static Achievement AutoBattle(string id, string group = "", int? target = null, bool isHidden = false, bool isRare = false) + => new() { Id = id, Group = group, Target = target ?? 0, IsHidden = isHidden, Category = AchievementCategory.AutoBattle, IsRare = isRare }; + + private static Achievement Humor(string id, string group = "", int? target = null, bool isHidden = false, bool isRare = false) + => new() { Id = id, Group = group, Target = target ?? 0, IsHidden = isHidden, Category = AchievementCategory.Humor, IsRare = isRare }; + + private static Achievement BugRelated(string id, string group = "", int? target = null, bool isHidden = false, bool isRare = false) + => new() { Id = id, Group = group, Target = target ?? 0, IsHidden = isHidden, Category = AchievementCategory.BugRelated, IsRare = isRare }; + + private static Achievement Behavior(string id, string group = "", int? target = null, bool isHidden = false, bool isRare = false) + => new() { Id = id, Group = group, Target = target ?? 0, IsHidden = isHidden, Category = AchievementCategory.Behavior, IsRare = isRare }; + + private static Achievement EasterEgg(string id, string group = "", int? target = null, bool isHidden = false, bool isRare = false) + => new() { Id = id, Group = group, Target = target ?? 0, IsHidden = isHidden, Category = AchievementCategory.EasterEgg, IsRare = isRare }; + + #endregion + + private readonly Achievement[] _allAchievements = + [ + + #region 基础使用类 + BasicUsage(id: AchievementIds.SanitySpender1, group: AchievementIds.SanitySpenderGroup, target: 10), // 刷理智次数 + BasicUsage(id: AchievementIds.SanitySpender2, group: AchievementIds.SanitySpenderGroup, target: 100), + BasicUsage(id: AchievementIds.SanitySpender3, group: AchievementIds.SanitySpenderGroup, target: 1000), + + BasicUsage(id: AchievementIds.SanitySaver1, group: AchievementIds.SanitySaverGroup, target: 1), // 使用理智药数 + BasicUsage(id: AchievementIds.SanitySaver2, group: AchievementIds.SanitySaverGroup, target: 10), + BasicUsage(id: AchievementIds.SanitySaver3, group: AchievementIds.SanitySaverGroup, target: 50), + + BasicUsage(id: AchievementIds.RoguelikeGamePass1, group: AchievementIds.RoguelikeGamePassGroup, target: 1), // 使用牛牛通关肉鸽 + BasicUsage(id: AchievementIds.RoguelikeGamePass2, group: AchievementIds.RoguelikeGamePassGroup, target: 5), + BasicUsage(id: AchievementIds.RoguelikeGamePass3, group: AchievementIds.RoguelikeGamePassGroup, target: 10), + + BasicUsage(id: AchievementIds.RoguelikeN04, group: AchievementIds.RoguelikeNGroup), // 肉鸽 难度 通关 + BasicUsage(id: AchievementIds.RoguelikeN08, group: AchievementIds.RoguelikeNGroup), + BasicUsage(id: AchievementIds.RoguelikeN12, group: AchievementIds.RoguelikeNGroup), + BasicUsage(id: AchievementIds.RoguelikeN15, group: AchievementIds.RoguelikeNGroup, isRare: true), + + BasicUsage(id: AchievementIds.RoguelikeRetreat, group: AchievementIds.RoguelikeGroup, target: 100), // 牛牛放弃探索 100 次 + BasicUsage(id: AchievementIds.RoguelikeGoldMax, group: AchievementIds.RoguelikeGroup, target: 999), // 肉鸽源石锭到达 999 + + BasicUsage(id: AchievementIds.FirstLaunch), // 首次启动 + BasicUsage(id: AchievementIds.SanityExpire, target: 8), // 单次消耗 8 瓶快过期的理智药 + BasicUsage(id: AchievementIds.OverLimitAgent, target: 100, isHidden: true), // 单次代理 100 关 #endregion - private readonly Achievement[] _allAchievements = - [ + #region 功能探索类 + FeatureExploration(id: AchievementIds.ScheduleMaster1, group: AchievementIds.ScheduleMasterGroup, target: 1), // 定时执行 + FeatureExploration(id: AchievementIds.ScheduleMaster2, group: AchievementIds.ScheduleMasterGroup, target: 100), - #region 基础使用类 - BasicUsage(id: AchievementIds.SanitySpender1, group: AchievementIds.SanitySpenderGroup, target: 10), // 刷理智次数 - BasicUsage(id: AchievementIds.SanitySpender2, group: AchievementIds.SanitySpenderGroup, target: 100), - BasicUsage(id: AchievementIds.SanitySpender3, group: AchievementIds.SanitySpenderGroup, target: 1000), + FeatureExploration(id: AchievementIds.MirrorChyanFirstUse, group: AchievementIds.MirrorChyanGroup, isHidden: true), // 第一次成功使用 MirrorChyan 下载 + FeatureExploration(id: AchievementIds.MirrorChyanCdkError, group: AchievementIds.MirrorChyanGroup, isHidden: true), // MirrorChyan CDK 错误 - BasicUsage(id: AchievementIds.SanitySaver1, group: AchievementIds.SanitySaverGroup, target: 1), // 使用理智药数 - BasicUsage(id: AchievementIds.SanitySaver2, group: AchievementIds.SanitySaverGroup, target: 10), - BasicUsage(id: AchievementIds.SanitySaver3, group: AchievementIds.SanitySaverGroup, target: 50), + FeatureExploration(id: AchievementIds.Pioneer1, group: AchievementIds.PioneerGroup), // 将 MAA 更新至公测版 + FeatureExploration(id: AchievementIds.Pioneer2, group: AchievementIds.PioneerGroup, isHidden: true), // 将 MAA 更新至内测版(隐藏) + FeatureExploration(id: AchievementIds.Pioneer3, group: AchievementIds.PioneerGroup, isHidden: true), // 使用未发布版本的 MAA(隐藏) - BasicUsage(id: AchievementIds.RoguelikeGamePass1, group: AchievementIds.RoguelikeGamePassGroup, target: 1), // 使用牛牛通关肉鸽 - BasicUsage(id: AchievementIds.RoguelikeGamePass2, group: AchievementIds.RoguelikeGamePassGroup, target: 5), - BasicUsage(id: AchievementIds.RoguelikeGamePass3, group: AchievementIds.RoguelikeGamePassGroup, target: 10), + FeatureExploration(id: AchievementIds.MosquitoLeg, target: 5), // 使用「借助战打 OF-1」功能超过 5 次 + FeatureExploration(id: AchievementIds.RealGacha, isHidden: true), // 真正的抽卡 + FeatureExploration(id: AchievementIds.PeekScreen, isHidden: true), // 窥屏 + FeatureExploration(id: AchievementIds.CustomizationMaster, isHidden: true), // 自定义背景 + #endregion - BasicUsage(id: AchievementIds.RoguelikeN04, group: AchievementIds.RoguelikeNGroup), // 肉鸽 难度 通关 - BasicUsage(id: AchievementIds.RoguelikeN08, group: AchievementIds.RoguelikeNGroup), - BasicUsage(id: AchievementIds.RoguelikeN12, group: AchievementIds.RoguelikeNGroup), - BasicUsage(id: AchievementIds.RoguelikeN15, group: AchievementIds.RoguelikeNGroup, isRare: true), + #region 自动战斗 + AutoBattle(id: AchievementIds.UseCopilot1, group: AchievementIds.UseCopilotGroup, target: 1), // 自动战斗 + AutoBattle(id: AchievementIds.UseCopilot2, group: AchievementIds.UseCopilotGroup, target: 10), + AutoBattle(id: AchievementIds.UseCopilot3, group: AchievementIds.UseCopilotGroup, target: 100), - BasicUsage(id: AchievementIds.RoguelikeRetreat, group: AchievementIds.RoguelikeGroup, target: 100), // 牛牛放弃探索 100 次 - BasicUsage(id: AchievementIds.RoguelikeGoldMax, group: AchievementIds.RoguelikeGroup, target: 999), // 肉鸽源石锭到达 999 + AutoBattle(id: AchievementIds.CopilotLikeGiven1, group: AchievementIds.CopilotLikeGroup, target: 1), // 点赞 1 次 + AutoBattle(id: AchievementIds.CopilotLikeGiven2, group: AchievementIds.CopilotLikeGroup, target: 10), // 点赞 10 次 + AutoBattle(id: AchievementIds.CopilotLikeGiven3, group: AchievementIds.CopilotLikeGroup, target: 50), // 点赞 50 次 - BasicUsage(id: AchievementIds.FirstLaunch), // 首次启动 - BasicUsage(id: AchievementIds.SanityExpire, target: 8), // 单次消耗 8 瓶快过期的理智药 - BasicUsage(id: AchievementIds.OverLimitAgent, target: 100, isHidden: true), // 单次代理 100 关 - #endregion + AutoBattle(id: AchievementIds.CopilotError), // 代理作战出现失误 + AutoBattle(id: AchievementIds.MapOutdated, isHidden: true), // 提示需要更新地图资源 + AutoBattle(id: AchievementIds.Irreplaceable, isHidden: true), // 自动编队缺少至少两名干员 + #endregion - #region 功能探索类 - FeatureExploration(id: AchievementIds.ScheduleMaster1, group: AchievementIds.ScheduleMasterGroup, target: 1), // 定时执行 - FeatureExploration(id: AchievementIds.ScheduleMaster2, group: AchievementIds.ScheduleMasterGroup, target: 100), + #region 搞笑/梗类成就 + Humor(id: AchievementIds.SnapshotChallenge1, group: AchievementIds.SnapshotChallengeGroup, isHidden: true), // 平均截图用时超过 800ms(高 ping 战士) + Humor(id: AchievementIds.SnapshotChallenge2, group: AchievementIds.SnapshotChallengeGroup, isHidden: true), // 平均截图用时在 400ms 到 800ms 之间(是不是有点太慢了) + Humor(id: AchievementIds.SnapshotChallenge3, group: AchievementIds.SnapshotChallengeGroup), // 平均截图用时小于 400ms(截图挑战 · Normal) + Humor(id: AchievementIds.SnapshotChallenge4, group: AchievementIds.SnapshotChallengeGroup), // 平均截图用时小于 100ms(截图挑战 · Fast) + Humor(id: AchievementIds.SnapshotChallenge5, group: AchievementIds.SnapshotChallengeGroup), // 平均截图用时小于 10ms(截图挑战 · Ultra) + Humor(id: AchievementIds.SnapshotChallenge6, group: AchievementIds.SnapshotChallengeGroup, isHidden: true, isRare: true), // 平均截图用时小于 5ms - FeatureExploration(id: AchievementIds.MirrorChyanFirstUse, group: AchievementIds.MirrorChyanGroup, isHidden: true), // 第一次成功使用 MirrorChyan 下载 - FeatureExploration(id: AchievementIds.MirrorChyanCdkError, group: AchievementIds.MirrorChyanGroup, isHidden: true), // MirrorChyan CDK 错误 + Humor(id: AchievementIds.QuickCloser, isHidden: true), // 快速关闭弹窗 + Humor(id: AchievementIds.TacticalRetreat), // 停止任务 + Humor(id: AchievementIds.Martian, isHidden: true), // 90 天没更新 - FeatureExploration(id: AchievementIds.Pioneer1, group: AchievementIds.PioneerGroup), // 将 MAA 更新至公测版 - FeatureExploration(id: AchievementIds.Pioneer2, group: AchievementIds.PioneerGroup, isHidden: true), // 将 MAA 更新至内测版(隐藏) - FeatureExploration(id: AchievementIds.Pioneer3, group: AchievementIds.PioneerGroup, isHidden: true), // 使用未发布版本的 MAA(隐藏) + Humor(id: AchievementIds.RecruitNoSixStar, group: AchievementIds.RecruitGroup, target: 500), // 公招中累计 500 次没出现六星tag + Humor(id: AchievementIds.RecruitNoSixStarStreak, group: AchievementIds.RecruitGroup, target: 500, isHidden: true), // 公招中连续 500 次没出现六星tag + #endregion - FeatureExploration(id: AchievementIds.MosquitoLeg, target: 5), // 使用「借助战打 OF-1」功能超过 5 次 - FeatureExploration(id: AchievementIds.RealGacha, isHidden: true), // 真正的抽卡 - FeatureExploration(id: AchievementIds.PeekScreen, isHidden: true), // 窥屏 - FeatureExploration(id: AchievementIds.CustomizationMaster, isHidden: true), // 自定义背景 - #endregion + #region BUG 相关 + BugRelated(id: AchievementIds.CongratulationError, isHidden: true), // 喜报 + BugRelated(id: AchievementIds.UnexpectedCrash, isHidden: true), // 不速之客 + BugRelated(id: AchievementIds.ProblemFeedback), // 问题反馈 + BugRelated(id: AchievementIds.CdnTorture, target: 3), // 下载资源失败超过3次 + #endregion - #region 自动战斗 - AutoBattle(id: AchievementIds.UseCopilot1, group: AchievementIds.UseCopilotGroup, target: 1), // 自动战斗 - AutoBattle(id: AchievementIds.UseCopilot2, group: AchievementIds.UseCopilotGroup, target: 10), - AutoBattle(id: AchievementIds.UseCopilot3, group: AchievementIds.UseCopilotGroup, target: 100), + #region 习惯 行为 时长类 + Behavior(id: AchievementIds.MissionStartCount, target: 3), // 一天内开始任务超过 3 次 + Behavior(id: AchievementIds.LongTaskTimeout), // 触发超时提醒 + Behavior(id: AchievementIds.ProxyOnline3Hours, isHidden: true), // 使用代理功能连续在线超过 3 小时 + Behavior(id: AchievementIds.TaskStartCancel, isHidden: true), // 在开始任务后马上又停止 + Behavior(id: AchievementIds.AfkWatcher), // 窗口尺寸最小化后长时间不操作 - AutoBattle(id: AchievementIds.CopilotLikeGiven1, group: AchievementIds.CopilotLikeGroup, target: 1), // 点赞 1 次 - AutoBattle(id: AchievementIds.CopilotLikeGiven2, group: AchievementIds.CopilotLikeGroup, target: 10), // 点赞 10 次 - AutoBattle(id: AchievementIds.CopilotLikeGiven3, group: AchievementIds.CopilotLikeGroup, target: 50), // 点赞 50 次 + Behavior(id: AchievementIds.UseDaily1, group: AchievementIds.UseDailyGroup, target: 7), // 连续使用时间 + Behavior(id: AchievementIds.UseDaily2, group: AchievementIds.UseDailyGroup, target: 30), + Behavior(id: AchievementIds.UseDaily3, group: AchievementIds.UseDailyGroup, target: 365, isRare: true), + #endregion - AutoBattle(id: AchievementIds.CopilotError), // 代理作战出现失误 - AutoBattle(id: AchievementIds.MapOutdated, isHidden: true), // 提示需要更新地图资源 - AutoBattle(id: AchievementIds.Irreplaceable, isHidden: true), // 自动编队缺少至少两名干员 - #endregion + #region 彩蛋类 + EasterEgg(id: AchievementIds.Rules, isHidden: true), // 我会一直注视着你 + EasterEgg(id: AchievementIds.VersionClick, isHidden: true), // 这也能点? - #region 搞笑/梗类成就 - Humor(id: AchievementIds.SnapshotChallenge1, group: AchievementIds.SnapshotChallengeGroup, isHidden: true), // 平均截图用时超过 800ms(高 ping 战士) - Humor(id: AchievementIds.SnapshotChallenge2, group: AchievementIds.SnapshotChallengeGroup, isHidden: true), // 平均截图用时在 400ms 到 800ms 之间(是不是有点太慢了) - Humor(id: AchievementIds.SnapshotChallenge3, group: AchievementIds.SnapshotChallengeGroup), // 平均截图用时小于 400ms(截图挑战 · Normal) - Humor(id: AchievementIds.SnapshotChallenge4, group: AchievementIds.SnapshotChallengeGroup), // 平均截图用时小于 100ms(截图挑战 · Fast) - Humor(id: AchievementIds.SnapshotChallenge5, group: AchievementIds.SnapshotChallengeGroup), // 平均截图用时小于 10ms(截图挑战 · Ultra) - Humor(id: AchievementIds.SnapshotChallenge6, group: AchievementIds.SnapshotChallengeGroup, isHidden: true, isRare: true), // 平均截图用时小于 5ms + EasterEgg(id: AchievementIds.AprilFools, AchievementIds.LoginGroup, isHidden: true), // 愚人节 + EasterEgg(id: AchievementIds.MidnightLaunch, AchievementIds.LoginGroup, isHidden: true), // 0~4 点 + EasterEgg(id: AchievementIds.LunarNewYear, AchievementIds.LoginGroup, isHidden: true), // 春节 - Humor(id: AchievementIds.QuickCloser, isHidden: true), // 快速关闭弹窗 - Humor(id: AchievementIds.TacticalRetreat), // 停止任务 - Humor(id: AchievementIds.Martian, isHidden: true), // 90 天没更新 + EasterEgg(id: AchievementIds.Lucky, isHidden: true, isRare: true), // 启动 MAA 时有极小概率触发 + #endregion + ]; - Humor(id: AchievementIds.RecruitNoSixStar, group: AchievementIds.RecruitGroup, target: 500), // 公招中累计 500 次没出现六星tag - Humor(id: AchievementIds.RecruitNoSixStarStreak, group: AchievementIds.RecruitGroup, target: 500, isHidden: true), // 公招中连续 500 次没出现六星tag - #endregion - - #region BUG 相关 - BugRelated(id: AchievementIds.CongratulationError, isHidden: true), // 喜报 - BugRelated(id: AchievementIds.UnexpectedCrash, isHidden: true), // 不速之客 - BugRelated(id: AchievementIds.ProblemFeedback), // 问题反馈 - BugRelated(id: AchievementIds.CdnTorture, target: 3), // 下载资源失败超过3次 - #endregion - - #region 习惯 行为 时长类 - Behavior(id: AchievementIds.MissionStartCount, target: 3), // 一天内开始任务超过 3 次 - Behavior(id: AchievementIds.LongTaskTimeout), // 触发超时提醒 - Behavior(id: AchievementIds.ProxyOnline3Hours, isHidden: true), // 使用代理功能连续在线超过 3 小时 - Behavior(id: AchievementIds.TaskStartCancel, isHidden: true), // 在开始任务后马上又停止 - Behavior(id: AchievementIds.AfkWatcher), // 窗口尺寸最小化后长时间不操作 - - Behavior(id: AchievementIds.UseDaily1, group: AchievementIds.UseDailyGroup, target: 7), // 连续使用时间 - Behavior(id: AchievementIds.UseDaily2, group: AchievementIds.UseDailyGroup, target: 30), - Behavior(id: AchievementIds.UseDaily3, group: AchievementIds.UseDailyGroup, target: 365, isRare: true), - #endregion - - #region 彩蛋类 - EasterEgg(id: AchievementIds.Rules, isHidden: true), // 我会一直注视着你 - EasterEgg(id: AchievementIds.VersionClick, isHidden: true), // 这也能点? - - EasterEgg(id: AchievementIds.AprilFools, AchievementIds.LoginGroup, isHidden: true), // 愚人节 - EasterEgg(id: AchievementIds.MidnightLaunch, AchievementIds.LoginGroup, isHidden: true), // 0~4 点 - EasterEgg(id: AchievementIds.LunarNewYear, AchievementIds.LoginGroup, isHidden: true), // 春节 - - EasterEgg(id: AchievementIds.Lucky, isHidden: true, isRare: true), // 启动 MAA 时有极小概率触发 - #endregion - ]; - - private void InitializeAchievements() + private void InitializeAchievements() + { + foreach (var achievement in _allAchievements) { - foreach (var achievement in _allAchievements) - { - RegisterAchievement(achievement); - } + RegisterAchievement(achievement); } + } - #region 带有 CustomData 的辅助函数 + #region 带有 CustomData 的辅助函数 - public void MissionStartCountAdd() + public void MissionStartCountAdd() + { + const string Id = AchievementIds.MissionStartCount; + const string Key = AchievementIds.MissionStartCountCustomDataKey; + + var today = DateTime.UtcNow.ToYjDate().Date; + DateTime? lastDate = GetAchievementCustomData(AchievementIds.MissionStartCount, Key)?.ToObject(); + + if (lastDate.HasValue && lastDate.Value == today) { - const string Id = AchievementIds.MissionStartCount; - const string Key = AchievementIds.MissionStartCountCustomDataKey; - - var today = DateTime.UtcNow.ToYjDate().Date; - DateTime? lastDate = GetAchievementCustomData(AchievementIds.MissionStartCount, Key)?.ToObject(); - - if (lastDate.HasValue && lastDate.Value == today) - { - AddProgress(Id); - } - else - { - SetAchievementCustomData(Id, Key, JToken.FromObject(today)); - SetProgress(Id, 1); - } + AddProgress(Id); } - - public void UseDailyAdd() + else { - // group 是不注册的,从第一个成就取 CustomData - const string Id = AchievementIds.UseDaily1; - const string GroupId = AchievementIds.UseDailyGroup; - const string Key = AchievementIds.UseDailyCustomDataKey; - - var today = DateTime.UtcNow.ToYjDate().Date; - DateTime? lastDate = GetAchievementCustomData(Id, Key)?.ToObject(); - if (lastDate.HasValue) - { - var delta = (today - lastDate.Value).TotalDays; - - switch (delta) - { - case 1: - AddProgressToGroup(GroupId); - break; - case > 1: - SetProgressToGroup(GroupId, 1); - break; - } - } - else - { - SetProgressToGroup(GroupId, 1); - } - SetAchievementCustomData(Id, Key, JToken.FromObject(today)); + SetProgress(Id, 1); + } + } + + public void UseDailyAdd() + { + // group 是不注册的,从第一个成就取 CustomData + const string Id = AchievementIds.UseDaily1; + const string GroupId = AchievementIds.UseDailyGroup; + const string Key = AchievementIds.UseDailyCustomDataKey; + + var today = DateTime.UtcNow.ToYjDate().Date; + DateTime? lastDate = GetAchievementCustomData(Id, Key)?.ToObject(); + if (lastDate.HasValue) + { + var delta = (today - lastDate.Value).TotalDays; + + switch (delta) + { + case 1: + AddProgressToGroup(GroupId); + break; + case > 1: + SetProgressToGroup(GroupId, 1); + break; + } + } + else + { + SetProgressToGroup(GroupId, 1); } - #endregion + SetAchievementCustomData(Id, Key, JToken.FromObject(today)); + } - public static class Events + #endregion + + public static class Events + { + /// + /// 启动时触发成就 + /// + public static void Startup() { - /// - /// 启动时触发成就 - /// - public static void Startup() + Instance.Unlock(AchievementIds.FirstLaunch); + var buildTimeInterval = (DateTime.UtcNow - VersionUpdateSettingsUserControlModel.BuildDateTime).TotalDays; + var resourceTimeInterval = (DateTime.UtcNow - SettingsViewModel.VersionUpdateSettings.ResourceDateTime).TotalDays; + var maxTimeInterval = Math.Max(buildTimeInterval, resourceTimeInterval); + if (maxTimeInterval > 90) { - Instance.Unlock(AchievementIds.FirstLaunch); - var buildTimeInterval = (DateTime.UtcNow - VersionUpdateSettingsUserControlModel.BuildDateTime).TotalDays; - var resourceTimeInterval = (DateTime.UtcNow - SettingsViewModel.VersionUpdateSettings.ResourceDateTime).TotalDays; - var maxTimeInterval = Math.Max(buildTimeInterval, resourceTimeInterval); - if (maxTimeInterval > 90) - { - Instance.Unlock(AchievementIds.Martian); - } + Instance.Unlock(AchievementIds.Martian); + } - if (Instances.VersionUpdateViewModel.IsDebugVersion()) - { - Instance.Unlock(AchievementIds.Pioneer3); - } - else if (Instances.VersionUpdateViewModel.IsBetaVersion()) - { - Instance.Unlock(AchievementIds.Pioneer1); - } + if (Instances.VersionUpdateViewModel.IsDebugVersion()) + { + Instance.Unlock(AchievementIds.Pioneer3); + } + else if (Instances.VersionUpdateViewModel.IsBetaVersion()) + { + Instance.Unlock(AchievementIds.Pioneer1); + } - // 内测版要传入 SemVersion 判断,这里就取反判断了 - else if (!Instances.VersionUpdateViewModel.IsStdVersion()) - { - Instance.Unlock(AchievementIds.Pioneer2); - } + // 内测版要传入 SemVersion 判断,这里就取反判断了 + else if (!Instances.VersionUpdateViewModel.IsStdVersion()) + { + Instance.Unlock(AchievementIds.Pioneer2); + } - // 0.066% 概率触发 - if (new Random().NextDouble() < 0.00066) - { - Instance.Unlock(AchievementIds.Lucky); - } + // 0.066% 概率触发 + if (new Random().NextDouble() < 0.00066) + { + Instance.Unlock(AchievementIds.Lucky); + } - var now = DateTime.Now; + var now = DateTime.Now; - // 0~4 点启动 - if (now.Hour is >= 0 and < 4) - { - Instance.Unlock(AchievementIds.MidnightLaunch); - } + // 0~4 点启动 + if (now.Hour is >= 0 and < 4) + { + Instance.Unlock(AchievementIds.MidnightLaunch); + } - // 愚人节启动 - if (now is { Month: 4, Day: 1 }) - { - Instance.Unlock(AchievementIds.AprilFools); - } + // 愚人节启动 + if (now is { Month: 4, Day: 1 }) + { + Instance.Unlock(AchievementIds.AprilFools); + } - // 春节 - ChineseLunisolarCalendar chineseCalendar = new(); - if (chineseCalendar.GetMonth(now) == 1 && chineseCalendar.GetDayOfMonth(now) == 1) - { - Instance.Unlock(AchievementIds.LunarNewYear); - } + // 春节 + ChineseLunisolarCalendar chineseCalendar = new(); + if (chineseCalendar.GetMonth(now) == 1 && chineseCalendar.GetDayOfMonth(now) == 1) + { + Instance.Unlock(AchievementIds.LunarNewYear); } } } diff --git a/src/MaaWpfGui/Helper/ApplicationExtensions.cs b/src/MaaWpfGui/Helper/ApplicationExtensions.cs index 5ab4b61000..de31e73e88 100644 --- a/src/MaaWpfGui/Helper/ApplicationExtensions.cs +++ b/src/MaaWpfGui/Helper/ApplicationExtensions.cs @@ -13,21 +13,20 @@ using System.Windows; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +internal static class ApplicationExtensions { - internal static class ApplicationExtensions + public static bool IsShuttingDown(this Application application) { - public static bool IsShuttingDown(this Application application) + try { - try - { - application.ShutdownMode = application.ShutdownMode; - return false; - } - catch - { - return true; - } + application.ShutdownMode = application.ShutdownMode; + return false; + } + catch + { + return true; } } } diff --git a/src/MaaWpfGui/Helper/BitmapImageExtensions.cs b/src/MaaWpfGui/Helper/BitmapImageExtensions.cs index 9fffc145f4..8f5bcffcc1 100644 --- a/src/MaaWpfGui/Helper/BitmapImageExtensions.cs +++ b/src/MaaWpfGui/Helper/BitmapImageExtensions.cs @@ -15,44 +15,43 @@ using System.IO; using System.Linq; using System.Windows.Media.Imaging; -namespace MaaWpfGui.Helper -{ - public static class BitmapImageExtensions - { - public static bool IsEqual(this BitmapImage image1, BitmapImage image2) - { - if (image1 == null || image2 == null) - { - return false; - } +namespace MaaWpfGui.Helper; - return image1.ToBytes().SequenceEqual(image2.ToBytes()); +public static class BitmapImageExtensions +{ + public static bool IsEqual(this BitmapImage image1, BitmapImage image2) + { + if (image1 == null || image2 == null) + { + return false; } - public static byte[] ToBytes(this BitmapImage image) + return image1.ToBytes().SequenceEqual(image2.ToBytes()); + } + + public static byte[] ToBytes(this BitmapImage image) + { + byte[] data = { }; + if (image == null) { - byte[] data = { }; - if (image == null) - { - return data; - } + return data; + } - try - { - var encoder = new BmpBitmapEncoder(); - encoder.Frames.Add(BitmapFrame.Create(image)); - using MemoryStream ms = new MemoryStream(); - encoder.Save(ms); - data = ms.ToArray(); - - return data; - } - catch - { - // ignored - } + try + { + var encoder = new BmpBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(image)); + using MemoryStream ms = new MemoryStream(); + encoder.Save(ms); + data = ms.ToArray(); return data; } + catch + { + // ignored + } + + return data; } } diff --git a/src/MaaWpfGui/Helper/CheckBoxHelper.cs b/src/MaaWpfGui/Helper/CheckBoxHelper.cs index 7d8bb0c7d8..ab0db1e4c8 100644 --- a/src/MaaWpfGui/Helper/CheckBoxHelper.cs +++ b/src/MaaWpfGui/Helper/CheckBoxHelper.cs @@ -15,25 +15,24 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Input; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +public static class CheckBoxHelper { - public static class CheckBoxHelper + public static void ToggleCheckBoxNullOnRightClick(object sender, MouseButtonEventArgs e) { - public static void ToggleCheckBoxNullOnRightClick(object sender, MouseButtonEventArgs e) + if (e.ChangedButton != MouseButton.Right) { - if (e.ChangedButton != MouseButton.Right) - { - return; - } - - CheckBox checkBox = (CheckBox)sender; - checkBox.IsChecked = checkBox.IsChecked == null ? (bool?)false : null; + return; } - public static void ToggleCheckBoxNullOnLeftClick(object sender, RoutedEventArgs e) - { - CheckBox checkBox = (CheckBox)sender; - checkBox.IsChecked = checkBox.IsChecked == true ? null : (bool?)false; - } + CheckBox checkBox = (CheckBox)sender; + checkBox.IsChecked = checkBox.IsChecked == null ? (bool?)false : null; + } + + public static void ToggleCheckBoxNullOnLeftClick(object sender, RoutedEventArgs e) + { + CheckBox checkBox = (CheckBox)sender; + checkBox.IsChecked = checkBox.IsChecked == true ? null : (bool?)false; } } diff --git a/src/MaaWpfGui/Helper/ConfigurationHelper.cs b/src/MaaWpfGui/Helper/ConfigurationHelper.cs index 95d3290dde..b997fbcf9f 100644 --- a/src/MaaWpfGui/Helper/ConfigurationHelper.cs +++ b/src/MaaWpfGui/Helper/ConfigurationHelper.cs @@ -21,572 +21,571 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Serilog; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +public class ConfigurationHelper { - public class ConfigurationHelper + public static readonly string ConfigFile = Path.Combine(PathsHelper.ConfigDir, "gui.json"); + public static readonly string ConfigBakFile = Path.Combine(PathsHelper.ConfigDir, "gui.json.bak"); + public static readonly string ConfigErrorFile = Path.Combine(PathsHelper.ConfigDir, "gui.error.json"); + + private static ConcurrentDictionary> _kvsMap; + private static string _current = ConfigurationKeys.DefaultConfiguration; + private static ConcurrentDictionary _kvs; + private static ConcurrentDictionary _globalKvs; + + private static readonly object _lock = new(); + + private static readonly ILogger _logger = Log.ForContext(); + + public delegate void ConfigurationUpdateEventHandler(string key, string oldValue, string newValue); + + public static event ConfigurationUpdateEventHandler ConfigurationUpdateEvent; + + private static bool Released { get; set; } + + /// + /// Get a configuration value + /// + /// The config key + /// The default value to return if the key is not existed + /// The config value + public static string GetValue(string key, string defaultValue) { - public static readonly string ConfigFile = Path.Combine(PathsHelper.ConfigDir, "gui.json"); - public static readonly string ConfigBakFile = Path.Combine(PathsHelper.ConfigDir, "gui.json.bak"); - public static readonly string ConfigErrorFile = Path.Combine(PathsHelper.ConfigDir, "gui.error.json"); + var hasValue = _kvs.TryGetValue(key, out var value); - private static ConcurrentDictionary> _kvsMap; - private static string _current = ConfigurationKeys.DefaultConfiguration; - private static ConcurrentDictionary _kvs; - private static ConcurrentDictionary _globalKvs; - - private static readonly object _lock = new(); - - private static readonly ILogger _logger = Log.ForContext(); - - public delegate void ConfigurationUpdateEventHandler(string key, string oldValue, string newValue); - - public static event ConfigurationUpdateEventHandler ConfigurationUpdateEvent; - - private static bool Released { get; set; } - - /// - /// Get a configuration value - /// - /// The config key - /// The default value to return if the key is not existed - /// The config value - public static string GetValue(string key, string defaultValue) + // _logger.Debug("Read configuration key {Key} with default value {DefaultValue}, configuration hit: {HasValue}, configuration value {Value}", key, defaultValue, hasValue, value); + if (hasValue) { - var hasValue = _kvs.TryGetValue(key, out var value); - - // _logger.Debug("Read configuration key {Key} with default value {DefaultValue}, configuration hit: {HasValue}, configuration value {Value}", key, defaultValue, hasValue, value); - if (hasValue) - { - return value; - } - - hasValue = _globalKvs.TryGetValue(key, out value); - if (hasValue) - { - _logger.Information("Read configuration key {Key} with global configuration value {Value}, configuration hit: {HasValue}, configuration value {Value}", key, value, true, value); - SetValue(key, value); - return value; - } - - // return hasValue ? value : defaultValue; - SetValue(key, defaultValue); - return defaultValue; + return value; } - public static int GetValue(string key, int defaultValue) + hasValue = _globalKvs.TryGetValue(key, out value); + if (hasValue) { - var value = GetValue(key, defaultValue.ToString()); - return int.TryParse(value, out var result) ? result : defaultValue; + _logger.Information("Read configuration key {Key} with global configuration value {Value}, configuration hit: {HasValue}, configuration value {Value}", key, value, true, value); + SetValue(key, value); + return value; } - public static long GetValue(string key, long defaultValue) + // return hasValue ? value : defaultValue; + SetValue(key, defaultValue); + return defaultValue; + } + + public static int GetValue(string key, int defaultValue) + { + var value = GetValue(key, defaultValue.ToString()); + return int.TryParse(value, out var result) ? result : defaultValue; + } + + public static long GetValue(string key, long defaultValue) + { + var value = GetValue(key, defaultValue.ToString()); + return long.TryParse(value, out var result) ? result : defaultValue; + } + + public static bool GetValue(string key, bool defaultValue) + { + var value = GetValue(key, defaultValue.ToString()); + return bool.TryParse(value, out var result) ? result : defaultValue; + } + + public static TOut GetValue(string key, TOut defaultValue) + where TOut : struct, Enum + { + var value = GetValue(key, defaultValue.ToString()); + return Enum.TryParse(value, out var result) ? result : defaultValue; + } + + public static string GetGlobalValue(string key, string defaultValue) + { + var hasValue = _globalKvs.TryGetValue(key, out var value); + + // _logger.Debug("Read global configuration key {Key} with default value {DefaultValue}, configuration hit: {HasValue}, configuration value {Value}", key, defaultValue, hasValue, value); + if (hasValue) { - var value = GetValue(key, defaultValue.ToString()); - return long.TryParse(value, out var result) ? result : defaultValue; + return value; } - public static bool GetValue(string key, bool defaultValue) + hasValue = _kvs.TryGetValue(key, out value); + if (hasValue) { - var value = GetValue(key, defaultValue.ToString()); - return bool.TryParse(value, out var result) ? result : defaultValue; + _logger.Information("Read global configuration key {Key} with current configuration value {Value}, configuration hit: {HasValue}, configuration value {Value}", key, value, true, value); + SetGlobalValue(key, value); + DeleteValue(key); + return value; } - public static TOut GetValue(string key, TOut defaultValue) - where TOut : struct, Enum + // 保证有全局配置 + SetGlobalValue(key, defaultValue); + return defaultValue; + } + + public static int GetGlobalValue(string key, int defaultValue) + { + var value = GetGlobalValue(key, defaultValue.ToString()); + return int.TryParse(value, out var result) ? result : defaultValue; + } + + public static long GetGlobalValue(string key, long defaultValue) + { + var value = GetGlobalValue(key, defaultValue.ToString()); + return long.TryParse(value, out var result) ? result : defaultValue; + } + + public static bool GetGlobalValue(string key, bool defaultValue) + { + var value = GetGlobalValue(key, defaultValue.ToString()); + return bool.TryParse(value, out var result) ? result : defaultValue; + } + + public static TOut GetGlobalValue(string key, TOut defaultValue) + where TOut : struct, Enum + { + var value = GetGlobalValue(key, defaultValue.ToString()); + return Enum.TryParse(value, out var result) ? result : defaultValue; + } + + /// + /// Set a configuration value + /// + /// The config key + /// The config value + /// The return value of + public static bool SetValue(string key, string value) + { + var old = string.Empty; + if (!_kvs.TryAdd(key, value)) { - var value = GetValue(key, defaultValue.ToString()); - return Enum.TryParse(value, out var result) ? result : defaultValue; + old = _kvs[key]; + _kvs[key] = value; } - public static string GetGlobalValue(string key, string defaultValue) + var result = Save(); + if (result) { - var hasValue = _globalKvs.TryGetValue(key, out var value); - - // _logger.Debug("Read global configuration key {Key} with default value {DefaultValue}, configuration hit: {HasValue}, configuration value {Value}", key, defaultValue, hasValue, value); - if (hasValue) - { - return value; - } - - hasValue = _kvs.TryGetValue(key, out value); - if (hasValue) - { - _logger.Information("Read global configuration key {Key} with current configuration value {Value}, configuration hit: {HasValue}, configuration value {Value}", key, value, true, value); - SetGlobalValue(key, value); - DeleteValue(key); - return value; - } - - // 保证有全局配置 - SetGlobalValue(key, defaultValue); - return defaultValue; + ConfigurationUpdateEvent?.Invoke(key, old, value); + _logger.Debug("Configuration {Key} has been set to `{Value}`", key, value); + } + else + { + _logger.Warning("Failed to save configuration {Key} to `{Value}`", key, value); } - public static int GetGlobalValue(string key, int defaultValue) + return result; + } + + public static bool SetGlobalValue(string key, string value) + { + var old = string.Empty; + if (!_globalKvs.TryAdd(key, value)) { - var value = GetGlobalValue(key, defaultValue.ToString()); - return int.TryParse(value, out var result) ? result : defaultValue; + old = _globalKvs[key]; + _globalKvs[key] = value; } - public static long GetGlobalValue(string key, long defaultValue) + var result = Save(); + if (result) { - var value = GetGlobalValue(key, defaultValue.ToString()); - return long.TryParse(value, out var result) ? result : defaultValue; + ConfigurationUpdateEvent?.Invoke(key, old, value); + _logger.Debug("Global configuration {Key} has been set to `{Value}`", key, value); + } + else + { + _logger.Warning("Failed to save global configuration {Key} to `{Value}`", key, value); } - public static bool GetGlobalValue(string key, bool defaultValue) + return result; + } + + public static bool ContainsKey(string key, bool isGlobal = false) => isGlobal ? _globalKvs.ContainsKey(key) : _kvs.ContainsKey(key); + + /// + /// Deletes a configuration + /// + /// The configuration key. + /// The return value of . + public static bool DeleteValue(string key) => DeleteValue(key, out _); + + /// + /// Deletes a configuration + /// + /// The configuration key. + /// The old value. + /// The return value of . + public static bool DeleteValue(string key, out string value) + { + value = string.Empty; + if (_kvs.Remove(key, out var kv)) { - var value = GetGlobalValue(key, defaultValue.ToString()); - return bool.TryParse(value, out var result) ? result : defaultValue; + value = kv; } - - public static TOut GetGlobalValue(string key, TOut defaultValue) - where TOut : struct, Enum + else { - var value = GetGlobalValue(key, defaultValue.ToString()); - return Enum.TryParse(value, out var result) ? result : defaultValue; - } - - /// - /// Set a configuration value - /// - /// The config key - /// The config value - /// The return value of - public static bool SetValue(string key, string value) - { - var old = string.Empty; - if (!_kvs.TryAdd(key, value)) - { - old = _kvs[key]; - _kvs[key] = value; - } - - var result = Save(); - if (result) - { - ConfigurationUpdateEvent?.Invoke(key, old, value); - _logger.Debug("Configuration {Key} has been set to `{Value}`", key, value); - } - else - { - _logger.Warning("Failed to save configuration {Key} to `{Value}`", key, value); - } - - return result; - } - - public static bool SetGlobalValue(string key, string value) - { - var old = string.Empty; - if (!_globalKvs.TryAdd(key, value)) - { - old = _globalKvs[key]; - _globalKvs[key] = value; - } - - var result = Save(); - if (result) - { - ConfigurationUpdateEvent?.Invoke(key, old, value); - _logger.Debug("Global configuration {Key} has been set to `{Value}`", key, value); - } - else - { - _logger.Warning("Failed to save global configuration {Key} to `{Value}`", key, value); - } - - return result; - } - - public static bool ContainsKey(string key, bool isGlobal = false) => isGlobal ? _globalKvs.ContainsKey(key) : _kvs.ContainsKey(key); - - /// - /// Deletes a configuration - /// - /// The configuration key. - /// The return value of . - public static bool DeleteValue(string key) => DeleteValue(key, out _); - - /// - /// Deletes a configuration - /// - /// The configuration key. - /// The old value. - /// The return value of . - public static bool DeleteValue(string key, out string value) - { - value = string.Empty; - if (_kvs.Remove(key, out var kv)) - { - value = kv; - } - else - { - return false; - } - - var result = Save(); - if (result) - { - ConfigurationUpdateEvent?.Invoke(key, value, string.Empty); - _logger.Debug("Configuration {Key} has been deleted", key); - } - else - { - _logger.Warning("Failed to save configuration file when deleted {Key}", key); - } - - return result; - } - - public static bool DeleteGlobalValue(string key, out string value) - { - value = string.Empty; - if (_globalKvs.Remove(key, out var kv)) - { - value = kv; - } - else - { - return false; - } - - var result = Save(); - if (result) - { - ConfigurationUpdateEvent?.Invoke(key, value, string.Empty); - _logger.Debug("Global configuration {Key} has been deleted", key); - } - else - { - _logger.Warning("Failed to save global configuration file when deleted {Key}", key); - } - - return result; - } - - private static void EnsureConfigDirectoryExists() - { - if (Directory.Exists("config") is false) - { - Directory.CreateDirectory("config"); - } - } - - private static bool ParseConfig(out JObject parsed) - { - parsed = ParseJsonFile(ConfigFile); - if (parsed is not null) - { - return true; - } - - if (File.Exists(ConfigFile)) - { - _logger.Warning("Failed to load configuration file, copying configuration file to error file"); - File.Copy(ConfigFile, ConfigErrorFile, true); - } - - if (File.Exists(ConfigBakFile)) - { - _logger.Information("trying to use backup file"); - parsed = ParseJsonFile(ConfigBakFile); - if (parsed is not null) - { - _logger.Information("Backup file loaded successfully, copying backup file to configuration file"); - File.Copy(ConfigBakFile, ConfigFile, true); - return true; - } - } - - _logger.Warning("Failed to load configuration file and/or backup file, using default settings"); - - _kvsMap = []; - _current = ConfigurationKeys.DefaultConfiguration; - _kvsMap[_current] = []; - _kvs = _kvsMap[_current]; - _globalKvs = []; return false; } - /// - /// Load configuration file - /// - /// True if success, false if failed - public static bool Load() + var result = Save(); + if (result) { - EnsureConfigDirectoryExists(); + ConfigurationUpdateEvent?.Invoke(key, value, string.Empty); + _logger.Debug("Configuration {Key} has been deleted", key); + } + else + { + _logger.Warning("Failed to save configuration file when deleted {Key}", key); + } - // Load configuration file - if (!ParseConfig(out var parsed)) - { - return false; - } + return result; + } - SetKvOrMigrate(parsed); - if (Save(ConfigBakFile)) - { - _logger.Information("{File} saved", ConfigBakFile); - } - else - { - _logger.Warning("{File} save failed", ConfigBakFile); - } + public static bool DeleteGlobalValue(string key, out string value) + { + value = string.Empty; + if (_globalKvs.Remove(key, out var kv)) + { + value = kv; + } + else + { + return false; + } + var result = Save(); + if (result) + { + ConfigurationUpdateEvent?.Invoke(key, value, string.Empty); + _logger.Debug("Global configuration {Key} has been deleted", key); + } + else + { + _logger.Warning("Failed to save global configuration file when deleted {Key}", key); + } + + return result; + } + + private static void EnsureConfigDirectoryExists() + { + if (Directory.Exists("config") is false) + { + Directory.CreateDirectory("config"); + } + } + + private static bool ParseConfig(out JObject parsed) + { + parsed = ParseJsonFile(ConfigFile); + if (parsed is not null) + { return true; } - private static void SetKvOrMigrate(JObject parsed) + if (File.Exists(ConfigFile)) { - bool hasConfigMap = parsed.ContainsKey(ConfigurationKeys.ConfigurationMap); - bool hasGlobalConfig = parsed.ContainsKey(ConfigurationKeys.GlobalConfiguration); - if (hasConfigMap) - { - // new version - _kvsMap = parsed[ConfigurationKeys.ConfigurationMap]?.ToObject>>() - ?? []; - _current = parsed[ConfigurationKeys.CurrentConfiguration]?.ToString() - ?? ConfigurationKeys.DefaultConfiguration; - } - else - { - // old version - _logger.Information("Configuration file is in old version, migrating to new version"); - - _kvsMap = []; - _current = ConfigurationKeys.DefaultConfiguration; - _kvsMap[_current] = parsed.ToObject>(); - } - - _kvs = _kvsMap[_current]; - - _globalKvs = hasGlobalConfig - ? parsed[ConfigurationKeys.GlobalConfiguration]?.ToObject>() - : new ConcurrentDictionary(); + _logger.Warning("Failed to load configuration file, copying configuration file to error file"); + File.Copy(ConfigFile, ConfigErrorFile, true); } - /// - /// Save configuration file - /// - /// The result of saving process - private static bool Save(string file = null) + if (File.Exists(ConfigBakFile)) { - lock (_lock) + _logger.Information("trying to use backup file"); + parsed = ParseJsonFile(ConfigBakFile); + if (parsed is not null) { - if (Released) - { - _logger.Warning("Attempting to save configuration file after release, this is not allowed"); - return false; - } - - try - { - var sortedKvsMap = new SortedDictionary>( - _kvsMap.ToDictionary( - kvp => kvp.Key, - kvp => new SortedDictionary(kvp.Value))); - - var jsonStr = JsonConvert.SerializeObject( - new SortedDictionary - { - { ConfigurationKeys.ConfigurationMap, sortedKvsMap }, - { ConfigurationKeys.CurrentConfiguration, _current }, - { ConfigurationKeys.GlobalConfiguration, new SortedDictionary(_globalKvs) }, - }, - Formatting.Indented); - - File.WriteAllText(file ?? ConfigFile, jsonStr); - } - catch (Exception e) - { - _logger.Error(e, "Failed to save configuration file."); - return false; - } - + _logger.Information("Backup file loaded successfully, copying backup file to configuration file"); + File.Copy(ConfigBakFile, ConfigFile, true); return true; } } - public static string GetCheckedStorage(string storageKey, string name, string defaultValue) + _logger.Warning("Failed to load configuration file and/or backup file, using default settings"); + + _kvsMap = []; + _current = ConfigurationKeys.DefaultConfiguration; + _kvsMap[_current] = []; + _kvs = _kvsMap[_current]; + _globalKvs = []; + return false; + } + + /// + /// Load configuration file + /// + /// True if success, false if failed + public static bool Load() + { + EnsureConfigDirectoryExists(); + + // Load configuration file + if (!ParseConfig(out var parsed)) { - return GetValue(storageKey + name + ".IsChecked", defaultValue); + return false; } - public static bool SetCheckedStorage(string storageKey, string name, string value) + SetKvOrMigrate(parsed); + if (Save(ConfigBakFile)) { - return SetValue(storageKey + name + ".IsChecked", value); + _logger.Information("{File} saved", ConfigBakFile); + } + else + { + _logger.Warning("{File} save failed", ConfigBakFile); } - public static string GetTimer(int i, string defaultValue) + return true; + } + + private static void SetKvOrMigrate(JObject parsed) + { + bool hasConfigMap = parsed.ContainsKey(ConfigurationKeys.ConfigurationMap); + bool hasGlobalConfig = parsed.ContainsKey(ConfigurationKeys.GlobalConfiguration); + if (hasConfigMap) { - return GetGlobalValue($"Timer.Timer{i + 1}", defaultValue); + // new version + _kvsMap = parsed[ConfigurationKeys.ConfigurationMap]?.ToObject>>() + ?? []; + _current = parsed[ConfigurationKeys.CurrentConfiguration]?.ToString() + ?? ConfigurationKeys.DefaultConfiguration; + } + else + { + // old version + _logger.Information("Configuration file is in old version, migrating to new version"); + + _kvsMap = []; + _current = ConfigurationKeys.DefaultConfiguration; + _kvsMap[_current] = parsed.ToObject>(); } - public static bool SetTimer(int i, string value) - { - return SetGlobalValue($"Timer.Timer{i + 1}", value); - } + _kvs = _kvsMap[_current]; - public static string GetTimerHour(int i, string defaultValue) - { - return GetGlobalValue($"Timer.Timer{i + 1}Hour", defaultValue); - } + _globalKvs = hasGlobalConfig + ? parsed[ConfigurationKeys.GlobalConfiguration]?.ToObject>() + : new ConcurrentDictionary(); + } - public static bool SetTimerHour(int i, string value) + /// + /// Save configuration file + /// + /// The result of saving process + private static bool Save(string file = null) + { + lock (_lock) { - return SetGlobalValue($"Timer.Timer{i + 1}Hour", value); - } - - public static string GetTimerMin(int i, string defaultValue) - { - return GetGlobalValue($"Timer.Timer{i + 1}Min", defaultValue); - } - - public static bool SetTimerMin(int i, string value) - { - return SetGlobalValue($"Timer.Timer{i + 1}Min", value); - } - - public static string GetTimerConfig(int i, string defaultValue) - { - return GetGlobalValue($"Timer.Timer{i + 1}.Config", defaultValue); - } - - public static bool SetTimerConfig(int i, string value) - { - return SetGlobalValue($"Timer.Timer{i + 1}.Config", value); - } - - public static string GetTaskOrder(string task, string defaultValue) - { - return GetValue("TaskQueue.Order." + task, defaultValue); - } - - public static bool SetTaskOrder(string task, string value) - { - return SetValue("TaskQueue.Order." + task, value); - } - - public static int GetSettingOrder(string setting, int defaultValue) - { - var value = GetGlobalValue("Settings.Order." + setting, defaultValue.ToString()); - return int.TryParse(value, out var result) ? result : defaultValue; - } - - public static bool SetSettingOrder(string setting, int value) - { - return SetGlobalValue("Settings.Order." + setting, value.ToString()); - } - - public static void Release() - { - lock (_lock) + if (Released) { - if (Released) - { - return; - } - - if (Save()) - { - _logger.Information("{File} saved", ConfigFile); - } - else - { - _logger.Warning("{File} save failed", ConfigFile); - } + _logger.Warning("Attempting to save configuration file after release, this is not allowed"); + return false; } - Released = true; - } - - private static JObject ParseJsonFile(string filePath) - { - if (File.Exists(filePath) is false) - { - return null; - } - - var str = File.ReadAllText(filePath); - try { - var obj = (JObject)JsonConvert.DeserializeObject(str); - return obj ?? throw new Exception("Failed to parse json file"); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to deserialize json file: {FilePath}", filePath); - } + var sortedKvsMap = new SortedDictionary>( + _kvsMap.ToDictionary( + kvp => kvp.Key, + kvp => new SortedDictionary(kvp.Value))); - return null; - } + var jsonStr = JsonConvert.SerializeObject( + new SortedDictionary + { + { ConfigurationKeys.ConfigurationMap, sortedKvsMap }, + { ConfigurationKeys.CurrentConfiguration, _current }, + { ConfigurationKeys.GlobalConfiguration, new SortedDictionary(_globalKvs) }, + }, + Formatting.Indented); - public static bool SwitchConfiguration(string configName) - { - if (_kvsMap.ContainsKey(configName) is false) + File.WriteAllText(file ?? ConfigFile, jsonStr); + } + catch (Exception e) { - _logger.Warning("Configuration {ConfigName} does not exist", configName); + _logger.Error(e, "Failed to save configuration file."); return false; } - _current = configName; - _kvs = _kvsMap[_current]; return true; } + } - public static bool AddConfiguration(string configName, string copyFrom = null) + public static string GetCheckedStorage(string storageKey, string name, string defaultValue) + { + return GetValue(storageKey + name + ".IsChecked", defaultValue); + } + + public static bool SetCheckedStorage(string storageKey, string name, string value) + { + return SetValue(storageKey + name + ".IsChecked", value); + } + + public static string GetTimer(int i, string defaultValue) + { + return GetGlobalValue($"Timer.Timer{i + 1}", defaultValue); + } + + public static bool SetTimer(int i, string value) + { + return SetGlobalValue($"Timer.Timer{i + 1}", value); + } + + public static string GetTimerHour(int i, string defaultValue) + { + return GetGlobalValue($"Timer.Timer{i + 1}Hour", defaultValue); + } + + public static bool SetTimerHour(int i, string value) + { + return SetGlobalValue($"Timer.Timer{i + 1}Hour", value); + } + + public static string GetTimerMin(int i, string defaultValue) + { + return GetGlobalValue($"Timer.Timer{i + 1}Min", defaultValue); + } + + public static bool SetTimerMin(int i, string value) + { + return SetGlobalValue($"Timer.Timer{i + 1}Min", value); + } + + public static string GetTimerConfig(int i, string defaultValue) + { + return GetGlobalValue($"Timer.Timer{i + 1}.Config", defaultValue); + } + + public static bool SetTimerConfig(int i, string value) + { + return SetGlobalValue($"Timer.Timer{i + 1}.Config", value); + } + + public static string GetTaskOrder(string task, string defaultValue) + { + return GetValue("TaskQueue.Order." + task, defaultValue); + } + + public static bool SetTaskOrder(string task, string value) + { + return SetValue("TaskQueue.Order." + task, value); + } + + public static int GetSettingOrder(string setting, int defaultValue) + { + var value = GetGlobalValue("Settings.Order." + setting, defaultValue.ToString()); + return int.TryParse(value, out var result) ? result : defaultValue; + } + + public static bool SetSettingOrder(string setting, int value) + { + return SetGlobalValue("Settings.Order." + setting, value.ToString()); + } + + public static void Release() + { + lock (_lock) { - if (string.IsNullOrEmpty(configName)) + if (Released) { - return false; + return; } - if (_kvsMap.ContainsKey(configName)) + if (Save()) { - _logger.Warning("Configuration {ConfigName} already exists", configName); - return false; - } - - if (copyFrom is null) - { - _kvsMap[configName] = []; + _logger.Information("{File} saved", ConfigFile); } else { - if (_kvsMap.ContainsKey(copyFrom) is false) - { - _logger.Warning("Configuration {ConfigName} does not exist", copyFrom); - return false; - } - - _kvsMap[configName] = new ConcurrentDictionary(_kvsMap[copyFrom]); + _logger.Warning("{File} save failed", ConfigFile); } - - return true; } - public static bool DeleteConfiguration(string configName) - { - if (_kvsMap.ContainsKey(configName) is false) - { - _logger.Warning("Configuration {ConfigName} does not exist", configName); - return false; - } - - if (_current == configName) - { - _logger.Warning("Configuration {ConfigName} is current configuration, cannot delete", configName); - return false; - } - - _kvsMap.Remove(configName, out _); - return true; - } - - public static List GetConfigurationList() => [.. _kvsMap.Keys.OrderBy(key => key)]; - - public static string GetCurrentConfiguration() => _current; + Released = true; } + + private static JObject ParseJsonFile(string filePath) + { + if (File.Exists(filePath) is false) + { + return null; + } + + var str = File.ReadAllText(filePath); + + try + { + var obj = (JObject)JsonConvert.DeserializeObject(str); + return obj ?? throw new Exception("Failed to parse json file"); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to deserialize json file: {FilePath}", filePath); + } + + return null; + } + + public static bool SwitchConfiguration(string configName) + { + if (_kvsMap.ContainsKey(configName) is false) + { + _logger.Warning("Configuration {ConfigName} does not exist", configName); + return false; + } + + _current = configName; + _kvs = _kvsMap[_current]; + return true; + } + + public static bool AddConfiguration(string configName, string copyFrom = null) + { + if (string.IsNullOrEmpty(configName)) + { + return false; + } + + if (_kvsMap.ContainsKey(configName)) + { + _logger.Warning("Configuration {ConfigName} already exists", configName); + return false; + } + + if (copyFrom is null) + { + _kvsMap[configName] = []; + } + else + { + if (_kvsMap.ContainsKey(copyFrom) is false) + { + _logger.Warning("Configuration {ConfigName} does not exist", copyFrom); + return false; + } + + _kvsMap[configName] = new ConcurrentDictionary(_kvsMap[copyFrom]); + } + + return true; + } + + public static bool DeleteConfiguration(string configName) + { + if (_kvsMap.ContainsKey(configName) is false) + { + _logger.Warning("Configuration {ConfigName} does not exist", configName); + return false; + } + + if (_current == configName) + { + _logger.Warning("Configuration {ConfigName} is current configuration, cannot delete", configName); + return false; + } + + _kvsMap.Remove(configName, out _); + return true; + } + + public static List GetConfigurationList() => [.. _kvsMap.Keys.OrderBy(key => key)]; + + public static string GetCurrentConfiguration() => _current; } diff --git a/src/MaaWpfGui/Helper/DataHelper.cs b/src/MaaWpfGui/Helper/DataHelper.cs index 6030347b01..47244c7362 100644 --- a/src/MaaWpfGui/Helper/DataHelper.cs +++ b/src/MaaWpfGui/Helper/DataHelper.cs @@ -19,461 +19,459 @@ using System.IO; using System.Linq; using MaaWpfGui.Constants; using MaaWpfGui.Constants.Enums; -using MaaWpfGui.Main; using MaaWpfGui.ViewModels.UI; using MaaWpfGui.ViewModels.UserControl.Settings; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +public static class DataHelper { - public static class DataHelper + public static readonly Dictionary ClientDirectoryMapper = new() { - public static readonly Dictionary ClientDirectoryMapper = new() + { "zh-tw", "txwy" }, + { "en-us", "YoStarEN" }, + { "ja-jp", "YoStarJP" }, + { "ko-kr", "YoStarKR" }, + }; + + public static readonly Dictionary ClientLanguageMapper = new() + { + { string.Empty, "zh-cn" }, + { "Official", "zh-cn" }, + { "Bilibili", "zh-cn" }, + { "YoStarEN", "en-us" }, + { "YoStarJP", "ja-jp" }, + { "YoStarKR", "ko-kr" }, + { "txwy", "zh-tw" }, + }; + + /// + /// Gets 储存角色信息的字典 + /// + public static Dictionary Characters { get; } = []; + + public static IReadOnlyDictionary Operators => Characters.Where(oper => oper.Value.IsOperator && !_virtuallyOpers.Contains(oper.Key)).ToDictionary(); + + /// + /// Gets 当前语言与客户端类型下的干员名列表 + /// + public static HashSet CharacterNames { get; } = []; + + public static Dictionary RecruitTags { get; private set; } = []; + + public static List MapData { get; private set; } = []; + + static DataHelper() + { + InitRecruitTag(); + Reload(); + } + + public static void Reload() + { + LoadBattleData(); + LoadMapData(); + } + + private static void LoadBattleData() + { + string filePath = Path.Combine(PathsHelper.ResourceDir, "battle_data.json"); + if (!File.Exists(filePath)) { - { "zh-tw", "txwy" }, - { "en-us", "YoStarEN" }, - { "ja-jp", "YoStarJP" }, - { "ko-kr", "YoStarKR" }, - }; - - public static readonly Dictionary ClientLanguageMapper = new() - { - { string.Empty, "zh-cn" }, - { "Official", "zh-cn" }, - { "Bilibili", "zh-cn" }, - { "YoStarEN", "en-us" }, - { "YoStarJP", "ja-jp" }, - { "YoStarKR", "ko-kr" }, - { "txwy", "zh-tw" }, - }; - - /// - /// Gets 储存角色信息的字典 - /// - public static Dictionary Characters { get; } = []; - - public static IReadOnlyDictionary Operators => Characters.Where(oper => oper.Value.IsOperator && !_virtuallyOpers.Contains(oper.Key)).ToDictionary(); - - /// - /// Gets 当前语言与客户端类型下的干员名列表 - /// - public static HashSet CharacterNames { get; } = []; - - public static Dictionary RecruitTags { get; private set; } = []; - - public static List MapData { get; private set; } = []; - - static DataHelper() - { - InitRecruitTag(); - Reload(); - } - - public static void Reload() - { - LoadBattleData(); - LoadMapData(); - } - - private static void LoadBattleData() - { - string filePath = Path.Combine(PathsHelper.ResourceDir, "battle_data.json"); - if (!File.Exists(filePath)) - { - return; - } - - string jsonText = File.ReadAllText(filePath); - var characterData = JsonConvert.DeserializeObject>(JObject.Parse(jsonText)["chars"]?.ToString() ?? string.Empty) ?? []; - - var characterNamesLangAdd = GetCharacterNamesAddAction(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Localization, LocalizationHelper.DefaultLanguage)); - var characterNamesClientAdd = GetCharacterNamesAddAction(ConfigurationHelper.GetValue(ConfigurationKeys.ClientType, string.Empty)); - - Characters.Clear(); - CharacterNames.Clear(); - foreach (var (key, value) in characterData) - { - value.Id = key; - Characters.Add(key, value); - if (!key.StartsWith("char_")) - { - continue; - } - - characterNamesLangAdd.Invoke(value); - characterNamesClientAdd.Invoke(value); - } - - _nameToCharacterMap = BuildNameIndex(); - } - - private static void InitRecruitTag() - { - var clientType = GameSettingsUserControlModel.Instance.ClientType; - var clientPath = clientType switch - { - "" or "Official" or "Bilibili" => string.Empty, - _ => Path.Combine("global", clientType, "resource"), - }; - - var displayLanguage = GuiSettingsUserControlModel.Instance.Language; - var displayPath = displayLanguage switch - { - "zh-tw" or "en-us" or "ja-jp" or "ko-kr" => Path.Combine("global", ClientDirectoryMapper[displayLanguage], "resource"), - _ => string.Empty, - }; - - var clientTags = ParseRecruit(Path.Combine(PathsHelper.ResourceDir, clientPath, "recruitment.json")); - var displayTags = ParseRecruit(Path.Combine(PathsHelper.ResourceDir, displayPath, "recruitment.json")); - - RecruitTags = clientTags.Keys - .Select(key => new KeyValuePair( - key, - (displayTags.TryGetValue(key, out var displayTag) - ? displayTag - : string.Empty, - clientTags.TryGetValue(key, out var clientTag) - ? clientTag - : string.Empty))) - .Where(i => !string.IsNullOrEmpty(i.Value.ClientName)) - .Select(i => !string.IsNullOrEmpty(i.Value.DisplayName) ? i : new(i.Key, (i.Value.ClientName, i.Value.ClientName))) - .ToDictionary(); return; - - static Dictionary ParseRecruit(string path) - { - Dictionary clientTags = []; - if (!File.Exists(path)) - { - return clientTags; - } - - var jObj = (JObject?)JsonConvert.DeserializeObject(File.ReadAllText(path)); - if (jObj is null || !jObj.ContainsKey("tags") || jObj["tags"] is not JObject tags) - { - return clientTags; - } - - foreach (var item in tags) - { - clientTags.Add(item.Key, item.Value?.ToString() ?? string.Empty); - } - - return clientTags.Where(i => !string.IsNullOrEmpty(i.Value)).ToDictionary(); - } } - private static void LoadMapData() + string jsonText = File.ReadAllText(filePath); + var characterData = JsonConvert.DeserializeObject>(JObject.Parse(jsonText)["chars"]?.ToString() ?? string.Empty) ?? []; + + var characterNamesLangAdd = GetCharacterNamesAddAction(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Localization, LocalizationHelper.DefaultLanguage)); + var characterNamesClientAdd = GetCharacterNamesAddAction(ConfigurationHelper.GetValue(ConfigurationKeys.ClientType, string.Empty)); + + Characters.Clear(); + CharacterNames.Clear(); + foreach (var (key, value) in characterData) { - MapData = []; - var path = Path.Combine(PathsHelper.ResourceDir, "Arknights-Tile-Pos", "overview.json"); + value.Id = key; + Characters.Add(key, value); + if (!key.StartsWith("char_")) + { + continue; + } + + characterNamesLangAdd.Invoke(value); + characterNamesClientAdd.Invoke(value); + } + + _nameToCharacterMap = BuildNameIndex(); + } + + private static void InitRecruitTag() + { + var clientType = GameSettingsUserControlModel.Instance.ClientType; + var clientPath = clientType switch + { + "" or "Official" or "Bilibili" => string.Empty, + _ => Path.Combine("global", clientType, "resource"), + }; + + var displayLanguage = GuiSettingsUserControlModel.Instance.Language; + var displayPath = displayLanguage switch + { + "zh-tw" or "en-us" or "ja-jp" or "ko-kr" => Path.Combine("global", ClientDirectoryMapper[displayLanguage], "resource"), + _ => string.Empty, + }; + + var clientTags = ParseRecruit(Path.Combine(PathsHelper.ResourceDir, clientPath, "recruitment.json")); + var displayTags = ParseRecruit(Path.Combine(PathsHelper.ResourceDir, displayPath, "recruitment.json")); + + RecruitTags = clientTags.Keys + .Select(key => new KeyValuePair( + key, + (displayTags.TryGetValue(key, out var displayTag) + ? displayTag + : string.Empty, + clientTags.TryGetValue(key, out var clientTag) + ? clientTag + : string.Empty))) + .Where(i => !string.IsNullOrEmpty(i.Value.ClientName)) + .Select(i => !string.IsNullOrEmpty(i.Value.DisplayName) ? i : new(i.Key, (i.Value.ClientName, i.Value.ClientName))) + .ToDictionary(); + return; + + static Dictionary ParseRecruit(string path) + { + Dictionary clientTags = []; if (!File.Exists(path)) { - return; + return clientTags; } - try + var jObj = (JObject?)JsonConvert.DeserializeObject(File.ReadAllText(path)); + if (jObj is null || !jObj.ContainsKey("tags") || jObj["tags"] is not JObject tags) { - var jObj = JsonConvert.DeserializeObject>(File.ReadAllText(path)); - if (jObj is not null && jObj.Count > 0) - { - MapData = [.. jObj.Values]; - } - } - catch - { - } - } - - public static MapInfo? FindMap(string mapId) - => MapData.FirstOrDefault(map => map.Code == mapId || map.Name == mapId || map.StageId == mapId || map.LevelId == mapId); - - private static Action GetCharacterNamesAddAction(string str) - { - return str switch - { - "zh-cn" or "Official" or "Bilibili" => - v => CharacterNames.Add(v.Name ?? string.Empty), - "zh-tw" or "txwy" => - v => CharacterNames.Add(v.NameTw ?? string.Empty), - "en-us" or "YoStarEN" => - v => CharacterNames.Add(v.NameEn ?? string.Empty), - "ja-jp" or "YoStarJP" => - v => CharacterNames.Add(v.NameJp ?? string.Empty), - "ko-kr" or "YoStarKR" => - v => CharacterNames.Add(v.NameKr ?? string.Empty), - _ => - v => CharacterNames.Add(v.Name ?? string.Empty), - }; - } - - private static Dictionary _nameToCharacterMap = BuildNameIndex(); - - private static Dictionary BuildNameIndex() - { - var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var character in Characters.Values) - { - TryAdd(character.Name); - TryAdd(character.NameEn); - TryAdd(character.NameJp); - TryAdd(character.NameKr); - TryAdd(character.NameTw); - continue; - - void TryAdd(string? name) - { - if (!string.IsNullOrWhiteSpace(name)) - { - dict.TryAdd(name, character); - } - } + return clientTags; } - return dict; - } - - /// - /// 通过任意服务器中对应的干员名获取干员信息 - /// - /// 任意服务器中的干员名 - /// 对应干员信息 - public static CharacterInfo? GetCharacterByNameOrAlias(string characterName) - { - return _nameToCharacterMap.GetValueOrDefault(characterName); - } - - /// - /// 通过任意服务器中对应的干员名获取指定语言的干员名 - /// - /// 任意服务器中的干员名 - /// 指定语言,默认为当前语言 - /// 指定语言干员名 - public static string? GetLocalizedCharacterName(string? characterName, string? language = null) - { - return string.IsNullOrEmpty(characterName) - ? null - : GetLocalizedCharacterName(GetCharacterByNameOrAlias(characterName), language); - } - - /// - /// 通过干员信息获取指定语言的干员名 - /// - /// 干员信息 - /// 指定语言 - /// 指定语言干员名 - public static string? GetLocalizedCharacterName(CharacterInfo? characterInfo, string? language = null) - { - if (characterInfo?.Name == null) + foreach (var item in tags) { - return null; + clientTags.Add(item.Key, item.Value?.ToString() ?? string.Empty); } - language ??= SettingsViewModel.GuiSettings.OperNameLocalization; - return language switch - { - "zh-cn" => characterInfo.Name, - "zh-tw" => characterInfo.NameTw ?? characterInfo.Name, - "en-us" => characterInfo.NameEn ?? characterInfo.Name, - "ja-jp" => characterInfo.NameJp ?? characterInfo.Name, - "ko-kr" => characterInfo.NameKr ?? characterInfo.Name, - _ => characterInfo.Name, - }; + return clientTags.Where(i => !string.IsNullOrEmpty(i.Value)).ToDictionary(); } - - /// - /// 判断干员在指定客户端中是否可用 - /// - /// 干员信息 - /// 客户端类型 - /// 是否可用 - public static bool IsCharacterAvailableInClient(CharacterInfo? character, string clientType) - { - if (character is null) - { - return false; - } - - return clientType switch - { - "zh-tw" or "txwy" => !character.NameTwUnavailable, - "en-us" or "YoStarEN" => !character.NameEnUnavailable, - "ja-jp" or "YoStarJP" => !character.NameJpUnavailable, - "ko-kr" or "YoStarKR" => !character.NameKrUnavailable, - _ => true, - }; - } - - /// - /// 判断干员在指定客户端中是否可用 - /// - /// 干员名 - /// 客户端类型 - /// 是否可用 - public static bool IsCharacterAvailableInClient(string characterName, string clientType) - { - var character = GetCharacterByNameOrAlias(characterName); - return character != null && IsCharacterAvailableInClient(character, clientType); - } - - /// - /// 通过完整 ID 查询 - /// - /// 干员 id - /// 干员信息 - public static CharacterInfo? GetCharacterById(string id) - { - return Characters.GetValueOrDefault(id); - } - - /// - /// 通过代号查询(如 char_002_amiya 中的 amiya) - /// - /// 干员代号 - /// 干员信息 - public static CharacterInfo? GetCharacterByCodeName(string codeName) - { - return Characters.Values.FirstOrDefault(character => - character.CodeName.Equals(codeName, StringComparison.OrdinalIgnoreCase)); - } - - public class CharacterInfo - { - [JsonIgnore] - public string Id { get; set; } = string.Empty; - - [JsonProperty("name")] - public string? Name { get; set; } - - [JsonProperty("name_en")] - public string? NameEn { get; set; } - - [JsonProperty("name_en_unavailable")] - public bool NameEnUnavailable { get; set; } = false; - - [JsonProperty("name_jp")] - public string? NameJp { get; set; } - - [JsonProperty("name_jp_unavailable")] - public bool NameJpUnavailable { get; set; } = false; - - [JsonProperty("name_kr")] - public string? NameKr { get; set; } - - [JsonProperty("name_kr_unavailable")] - public bool NameKrUnavailable { get; set; } = false; - - [JsonProperty("name_tw")] - public string? NameTw { get; set; } - - [JsonProperty("name_tw_unavailable")] - public bool NameTwUnavailable { get; set; } = false; - - [JsonProperty("position")] - public string? Position { get; set; } - - [JsonProperty("profession")] - public OperatorRole Type { get; set; } = OperatorRole.Unknown; - - [JsonProperty("rangeId")] - public List? RangeId { get; set; } - - [JsonProperty("rarity")] - public int Rarity { get; set; } - - public bool IsOperator => Type is - OperatorRole.Caster or - OperatorRole.Medic or - OperatorRole.Pioneer or - OperatorRole.Sniper or - OperatorRole.Special or - OperatorRole.Support or - OperatorRole.Tank or - OperatorRole.Warrior; - - [JsonIgnore] - public string CodeName => ExtractCodeName(Id); - - private static string ExtractCodeName(string id) - { - if (string.IsNullOrEmpty(id)) - { - return string.Empty; - } - - // 从"char_002_amiya"中提取"amiya" - var parts = id.Split('_'); - return parts.Length >= 3 ? parts[2] : id; - } - } - - public class MapInfo - { - // 1-7 - [JsonProperty("code")] - public string? Code { get; set; } - - // main_01-07#f#-obt-main-level_main_01-07.json - [JsonProperty("filename")] - public string? Filename { get; set; } - - // obt/main/level_main_01-07 - [JsonProperty("levelId")] - public string? LevelId { get; set; } - - // 暴君 - [JsonProperty("name")] - public string? Name { get; set; } - - // main_01-07#f#, #f#是突袭关卡 - [JsonProperty("stageId")] - public string? StageId { get; set; } - - [JsonProperty("height")] - public int Height { get; set; } - - [JsonProperty("width")] - public int Width { get; set; } - } - - /// - /// 未实装干员,但在battle_data中, - /// - private static readonly HashSet _virtuallyOpers = - [ - "char_504_rguard", // 预备干员-近战 - "char_505_rcast", // 预备干员-术师 - "char_506_rmedic", // 预备干员-后勤 - "char_507_rsnipe", // 预备干员-狙击 - "char_508_aguard", // Sharp - "char_509_acast", // Pith - "char_612_accast", // Pith - "char_510_amedic", // Touch - "char_613_acmedc", // Touch - "char_511_asnipe", // Stormeye - "char_611_acnipe", // Stormeye - "char_512_aprot", // 暮落 - "char_513_apionr", // 郁金香 - "char_514_rdfend", // 预备干员-重装 - - // 因为 core 是通过名字来判断的,所以下面干员中如果有和上面重名的不会用到,不过也加上了 - "char_600_cpione", // 预备干员-先锋 4★ - "char_601_cguard", // 预备干员-近卫 4★ - "char_602_cdfend", // 预备干员-重装 4★ - "char_603_csnipe", // 预备干员-狙击 4★ - "char_604_ccast", // 预备干员-术师 4★ - "char_605_cmedic", // 预备干员-医疗 4★ - "char_606_csuppo", // 预备干员-辅助 4★ - "char_607_cspec", // 预备干员-特种 4★ - "char_608_acpion", // 郁金香 6★ - "char_609_acguad", // Sharp 6★ - "char_610_acfend", // Mechanist - "char_614_acsupo", // Raidian - "char_615_acspec", // Misery - - "char_1001_amiya2", // 阿米娅-WARRIOR - "char_1037_amiya3", // 阿米娅-MEDIC - ]; } + + private static void LoadMapData() + { + MapData = []; + var path = Path.Combine(PathsHelper.ResourceDir, "Arknights-Tile-Pos", "overview.json"); + if (!File.Exists(path)) + { + return; + } + + try + { + var jObj = JsonConvert.DeserializeObject>(File.ReadAllText(path)); + if (jObj is not null && jObj.Count > 0) + { + MapData = [.. jObj.Values]; + } + } + catch + { + } + } + + public static MapInfo? FindMap(string mapId) + => MapData.FirstOrDefault(map => map.Code == mapId || map.Name == mapId || map.StageId == mapId || map.LevelId == mapId); + + private static Action GetCharacterNamesAddAction(string str) + { + return str switch + { + "zh-cn" or "Official" or "Bilibili" => + v => CharacterNames.Add(v.Name ?? string.Empty), + "zh-tw" or "txwy" => + v => CharacterNames.Add(v.NameTw ?? string.Empty), + "en-us" or "YoStarEN" => + v => CharacterNames.Add(v.NameEn ?? string.Empty), + "ja-jp" or "YoStarJP" => + v => CharacterNames.Add(v.NameJp ?? string.Empty), + "ko-kr" or "YoStarKR" => + v => CharacterNames.Add(v.NameKr ?? string.Empty), + _ => + v => CharacterNames.Add(v.Name ?? string.Empty), + }; + } + + private static Dictionary _nameToCharacterMap = BuildNameIndex(); + + private static Dictionary BuildNameIndex() + { + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var character in Characters.Values) + { + TryAdd(character.Name); + TryAdd(character.NameEn); + TryAdd(character.NameJp); + TryAdd(character.NameKr); + TryAdd(character.NameTw); + continue; + + void TryAdd(string? name) + { + if (!string.IsNullOrWhiteSpace(name)) + { + dict.TryAdd(name, character); + } + } + } + + return dict; + } + + /// + /// 通过任意服务器中对应的干员名获取干员信息 + /// + /// 任意服务器中的干员名 + /// 对应干员信息 + public static CharacterInfo? GetCharacterByNameOrAlias(string characterName) + { + return _nameToCharacterMap.GetValueOrDefault(characterName); + } + + /// + /// 通过任意服务器中对应的干员名获取指定语言的干员名 + /// + /// 任意服务器中的干员名 + /// 指定语言,默认为当前语言 + /// 指定语言干员名 + public static string? GetLocalizedCharacterName(string? characterName, string? language = null) + { + return string.IsNullOrEmpty(characterName) + ? null + : GetLocalizedCharacterName(GetCharacterByNameOrAlias(characterName), language); + } + + /// + /// 通过干员信息获取指定语言的干员名 + /// + /// 干员信息 + /// 指定语言 + /// 指定语言干员名 + public static string? GetLocalizedCharacterName(CharacterInfo? characterInfo, string? language = null) + { + if (characterInfo?.Name == null) + { + return null; + } + + language ??= SettingsViewModel.GuiSettings.OperNameLocalization; + return language switch + { + "zh-cn" => characterInfo.Name, + "zh-tw" => characterInfo.NameTw ?? characterInfo.Name, + "en-us" => characterInfo.NameEn ?? characterInfo.Name, + "ja-jp" => characterInfo.NameJp ?? characterInfo.Name, + "ko-kr" => characterInfo.NameKr ?? characterInfo.Name, + _ => characterInfo.Name, + }; + } + + /// + /// 判断干员在指定客户端中是否可用 + /// + /// 干员信息 + /// 客户端类型 + /// 是否可用 + public static bool IsCharacterAvailableInClient(CharacterInfo? character, string clientType) + { + if (character is null) + { + return false; + } + + return clientType switch + { + "zh-tw" or "txwy" => !character.NameTwUnavailable, + "en-us" or "YoStarEN" => !character.NameEnUnavailable, + "ja-jp" or "YoStarJP" => !character.NameJpUnavailable, + "ko-kr" or "YoStarKR" => !character.NameKrUnavailable, + _ => true, + }; + } + + /// + /// 判断干员在指定客户端中是否可用 + /// + /// 干员名 + /// 客户端类型 + /// 是否可用 + public static bool IsCharacterAvailableInClient(string characterName, string clientType) + { + var character = GetCharacterByNameOrAlias(characterName); + return character != null && IsCharacterAvailableInClient(character, clientType); + } + + /// + /// 通过完整 ID 查询 + /// + /// 干员 id + /// 干员信息 + public static CharacterInfo? GetCharacterById(string id) + { + return Characters.GetValueOrDefault(id); + } + + /// + /// 通过代号查询(如 char_002_amiya 中的 amiya) + /// + /// 干员代号 + /// 干员信息 + public static CharacterInfo? GetCharacterByCodeName(string codeName) + { + return Characters.Values.FirstOrDefault(character => + character.CodeName.Equals(codeName, StringComparison.OrdinalIgnoreCase)); + } + + public class CharacterInfo + { + [JsonIgnore] + public string Id { get; set; } = string.Empty; + + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("name_en")] + public string? NameEn { get; set; } + + [JsonProperty("name_en_unavailable")] + public bool NameEnUnavailable { get; set; } = false; + + [JsonProperty("name_jp")] + public string? NameJp { get; set; } + + [JsonProperty("name_jp_unavailable")] + public bool NameJpUnavailable { get; set; } = false; + + [JsonProperty("name_kr")] + public string? NameKr { get; set; } + + [JsonProperty("name_kr_unavailable")] + public bool NameKrUnavailable { get; set; } = false; + + [JsonProperty("name_tw")] + public string? NameTw { get; set; } + + [JsonProperty("name_tw_unavailable")] + public bool NameTwUnavailable { get; set; } = false; + + [JsonProperty("position")] + public string? Position { get; set; } + + [JsonProperty("profession")] + public OperatorRole Type { get; set; } = OperatorRole.Unknown; + + [JsonProperty("rangeId")] + public List? RangeId { get; set; } + + [JsonProperty("rarity")] + public int Rarity { get; set; } + + public bool IsOperator => Type is + OperatorRole.Caster or + OperatorRole.Medic or + OperatorRole.Pioneer or + OperatorRole.Sniper or + OperatorRole.Special or + OperatorRole.Support or + OperatorRole.Tank or + OperatorRole.Warrior; + + [JsonIgnore] + public string CodeName => ExtractCodeName(Id); + + private static string ExtractCodeName(string id) + { + if (string.IsNullOrEmpty(id)) + { + return string.Empty; + } + + // 从"char_002_amiya"中提取"amiya" + var parts = id.Split('_'); + return parts.Length >= 3 ? parts[2] : id; + } + } + + public class MapInfo + { + // 1-7 + [JsonProperty("code")] + public string? Code { get; set; } + + // main_01-07#f#-obt-main-level_main_01-07.json + [JsonProperty("filename")] + public string? Filename { get; set; } + + // obt/main/level_main_01-07 + [JsonProperty("levelId")] + public string? LevelId { get; set; } + + // 暴君 + [JsonProperty("name")] + public string? Name { get; set; } + + // main_01-07#f#, #f#是突袭关卡 + [JsonProperty("stageId")] + public string? StageId { get; set; } + + [JsonProperty("height")] + public int Height { get; set; } + + [JsonProperty("width")] + public int Width { get; set; } + } + + /// + /// 未实装干员,但在battle_data中, + /// + private static readonly HashSet _virtuallyOpers = + [ + "char_504_rguard", // 预备干员-近战 + "char_505_rcast", // 预备干员-术师 + "char_506_rmedic", // 预备干员-后勤 + "char_507_rsnipe", // 预备干员-狙击 + "char_508_aguard", // Sharp + "char_509_acast", // Pith + "char_612_accast", // Pith + "char_510_amedic", // Touch + "char_613_acmedc", // Touch + "char_511_asnipe", // Stormeye + "char_611_acnipe", // Stormeye + "char_512_aprot", // 暮落 + "char_513_apionr", // 郁金香 + "char_514_rdfend", // 预备干员-重装 + + // 因为 core 是通过名字来判断的,所以下面干员中如果有和上面重名的不会用到,不过也加上了 + "char_600_cpione", // 预备干员-先锋 4★ + "char_601_cguard", // 预备干员-近卫 4★ + "char_602_cdfend", // 预备干员-重装 4★ + "char_603_csnipe", // 预备干员-狙击 4★ + "char_604_ccast", // 预备干员-术师 4★ + "char_605_cmedic", // 预备干员-医疗 4★ + "char_606_csuppo", // 预备干员-辅助 4★ + "char_607_cspec", // 预备干员-特种 4★ + "char_608_acpion", // 郁金香 6★ + "char_609_acguad", // Sharp 6★ + "char_610_acfend", // Mechanist + "char_614_acsupo", // Raidian + "char_615_acspec", // Misery + + "char_1001_amiya2", // 阿米娅-WARRIOR + "char_1037_amiya3", // 阿米娅-MEDIC + ]; } diff --git a/src/MaaWpfGui/Helper/ETagCache.cs b/src/MaaWpfGui/Helper/ETagCache.cs index 33e0991392..dc84c2aeb1 100644 --- a/src/MaaWpfGui/Helper/ETagCache.cs +++ b/src/MaaWpfGui/Helper/ETagCache.cs @@ -21,133 +21,132 @@ using System.Threading.Tasks; using Newtonsoft.Json; using Serilog; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +public class ETagCache { - public class ETagCache + private static readonly ILogger _logger = Log.ForContext(); + + private static readonly string _etagFile = Path.Combine(PathsHelper.CacheDir, "etag.json"); + private static readonly string _lastModifiedFile = Path.Combine(PathsHelper.CacheDir, "last_modified.json"); + private static Dictionary _etagCache = []; + private static Dictionary _lastModifiedCache = []; + + public static void Load() { - private static readonly ILogger _logger = Log.ForContext(); - - private static readonly string _etagFile = Path.Combine(PathsHelper.CacheDir, "etag.json"); - private static readonly string _lastModifiedFile = Path.Combine(PathsHelper.CacheDir, "last_modified.json"); - private static Dictionary _etagCache = []; - private static Dictionary _lastModifiedCache = []; - - public static void Load() + // ETag + if (File.Exists(_etagFile)) { - // ETag - if (File.Exists(_etagFile)) + try { - try - { - var json = File.ReadAllText(_etagFile); - _etagCache = JsonConvert.DeserializeObject>(json) ?? []; - } - catch (Exception e) - { - _logger.Warning(e, "Failed to load ETag cache"); - _etagCache = []; - } + var json = File.ReadAllText(_etagFile); + _etagCache = JsonConvert.DeserializeObject>(json) ?? []; } - - // Last-Modified - if (File.Exists(_lastModifiedFile)) + catch (Exception e) { - try - { - var json = File.ReadAllText(_lastModifiedFile); - _lastModifiedCache = JsonConvert.DeserializeObject>(json) ?? []; - } - catch (Exception e) - { - _logger.Warning(e, "Failed to load Last-Modified cache"); - _lastModifiedCache = []; - } + _logger.Warning(e, "Failed to load ETag cache"); + _etagCache = []; } } - public static void Save() + // Last-Modified + if (File.Exists(_lastModifiedFile)) { - File.WriteAllText(_etagFile, JsonConvert.SerializeObject(_etagCache, Formatting.Indented)); - File.WriteAllText(_lastModifiedFile, JsonConvert.SerializeObject(_lastModifiedCache, Formatting.Indented)); - } - - public static string GetETag(string? url) => - url != null && _etagCache.TryGetValue(url, out var etag) ? etag : string.Empty; - - public static DateTimeOffset? GetLastModified(string? url) => - url != null && _lastModifiedCache.TryGetValue(url, out var lm) ? lm : null; - - public static void SetETag(string url, string etag) - { - _etagCache[url] = etag; - } - - public static void SetLastModified(string url, DateTimeOffset? dt) - { - if (dt.HasValue) + try { - _lastModifiedCache[url] = dt.Value; + var json = File.ReadAllText(_lastModifiedFile); + _lastModifiedCache = JsonConvert.DeserializeObject>(json) ?? []; + } + catch (Exception e) + { + _logger.Warning(e, "Failed to load Last-Modified cache"); + _lastModifiedCache = []; } } + } - // UPDATE: 重定向会导致 uri 变成其他地址,导致存的 ETag 无法匹配原始地址,所以要传入原始地址 - public static void Set(HttpResponseMessage? response, string uri) + public static void Save() + { + File.WriteAllText(_etagFile, JsonConvert.SerializeObject(_etagCache, Formatting.Indented)); + File.WriteAllText(_lastModifiedFile, JsonConvert.SerializeObject(_lastModifiedCache, Formatting.Indented)); + } + + public static string GetETag(string? url) => + url != null && _etagCache.TryGetValue(url, out var etag) ? etag : string.Empty; + + public static DateTimeOffset? GetLastModified(string? url) => + url != null && _lastModifiedCache.TryGetValue(url, out var lm) ? lm : null; + + public static void SetETag(string url, string etag) + { + _etagCache[url] = etag; + } + + public static void SetLastModified(string url, DateTimeOffset? dt) + { + if (dt.HasValue) { - if (response == null || string.IsNullOrEmpty(uri)) - { - return; - } + _lastModifiedCache[url] = dt.Value; + } + } - var etag = response.Headers.ETag?.Tag; - var lastModified = response.Content?.Headers?.LastModified; + // UPDATE: 重定向会导致 uri 变成其他地址,导致存的 ETag 无法匹配原始地址,所以要传入原始地址 + public static void Set(HttpResponseMessage? response, string uri) + { + if (response == null || string.IsNullOrEmpty(uri)) + { + return; + } + + var etag = response.Headers.ETag?.Tag; + var lastModified = response.Content?.Headers?.LastModified; + + if (!string.IsNullOrEmpty(etag)) + { + SetETag(uri, etag); + } + + if (lastModified.HasValue) + { + SetLastModified(uri, lastModified.Value); + } + + Save(); + } + + public static async Task FetchResponseWithEtag(string url, bool force = false) + { + var headers = new Dictionary + { + { "Accept", "application/octet-stream" }, + { "Connection", "close" }, + }; + + if (!force) + { + var etag = GetETag(url); + var lastModified = GetLastModified(url); if (!string.IsNullOrEmpty(etag)) { - SetETag(uri, etag); + headers["If-None-Match"] = etag; } if (lastModified.HasValue) { - SetLastModified(uri, lastModified.Value); + headers["If-Modified-Since"] = lastModified.Value.ToUniversalTime().ToString("R"); } - - Save(); } - public static async Task FetchResponseWithEtag(string url, bool force = false) + try { - var headers = new Dictionary - { - { "Accept", "application/octet-stream" }, - { "Connection", "close" }, - }; - - if (!force) - { - var etag = GetETag(url); - var lastModified = GetLastModified(url); - - if (!string.IsNullOrEmpty(etag)) - { - headers["If-None-Match"] = etag; - } - - if (lastModified.HasValue) - { - headers["If-Modified-Since"] = lastModified.Value.ToUniversalTime().ToString("R"); - } - } - - try - { - var response = await Instances.HttpService.GetAsync(new Uri(url), headers); - return response; - } - catch (Exception e) - { - _logger.Error(e, "Failed to send GET request to {Uri}", url); - return null; - } + var response = await Instances.HttpService.GetAsync(new Uri(url), headers); + return response; + } + catch (Exception e) + { + _logger.Error(e, "Failed to send GET request to {Uri}", url); + return null; } } } diff --git a/src/MaaWpfGui/Helper/GpuOption.cs b/src/MaaWpfGui/Helper/GpuOption.cs index fb9b994f2e..2353893db3 100644 --- a/src/MaaWpfGui/Helper/GpuOption.cs +++ b/src/MaaWpfGui/Helper/GpuOption.cs @@ -29,514 +29,513 @@ using Windows.Win32.Graphics.Direct3D; using Windows.Win32.Graphics.Direct3D12; using Windows.Win32.Graphics.Dxgi; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +public abstract class GpuOption { - public abstract class GpuOption + public static GpuOption Disable => DisableOption.Instance; + + private static readonly List _unavailableOptions = [Disable]; + + public static bool AllowDeprecatedGpu { - public static GpuOption Disable => DisableOption.Instance; + get => ConfigurationHelper.GetValue(ConfigurationKeys.PerformanceAllowDeprecatedGpu, false); + set => ConfigurationHelper.SetValue(ConfigurationKeys.PerformanceAllowDeprecatedGpu, value.ToString()); + } - private static readonly List _unavailableOptions = [Disable]; + // use string literal to efficiently store uint16 array + // https://github.com/torvalds/linux/blob/v6.10/drivers/gpu/drm/amd/amdgpu/amdgpu_drv.c#L1756 + private static ReadOnlySpan AmdBlacklist => MemoryMarshal.Cast( + /* CHIP_TAHITI */ "\u6780\u6784\u6788\u678A\u6790\u6791\u6792\u6798\u6799\u679A\u679B\u679E\u679F" + + /* CHIP_PITCAIRN */ "\u6800\u6801\u6802\u6806\u6808\u6809\u6810\u6811\u6816\u6817\u6818\u6819" + + /* CHIP_OLAND */ "\u6600\u6601\u6602\u6603\u6604\u6605\u6606\u6607\u6608" + + "\u6610\u6611\u6613\u6617\u6620\u6621\u6623\u6631" + + /* CHIP_VERDE */ "\u6820\u6821\u6822\u6823\u6824\u6825\u6826\u6827\u6828\u6829\u682A\u682B\u682C" + + "\u682D\u682F\u6830\u6831\u6835\u6837\u6838\u6839\u683B\u683D\u683F" + + /* CHIP_HAINAN */ "\u6660\u6663\u6664\u6665\u6667\u666F" + + /* CHIP_KAVERI */ "\u1304\u1305\u1306\u1307\u1309\u130A\u130B\u130C\u130D\u130E\u130F" + + "\u1310\u1311\u1312\u1313\u1315\u1316\u1317\u1318\u131B\u131C\u131D" + + /* CHIP_BONAIRE */ "\u6640\u6641\u6646\u6647\u6649\u6650\u6651\u6658\u665c\u665d\u665f" + + /* CHIP_HAWAII */ "\u67A0\u67A1\u67A2\u67A8\u67A9\u67AA\u67B0\u67B1\u67B8\u67B9\u67BA\u67BE" + + /* CHIP_KABINI */ "\u9830\u9831\u9832\u9833\u9834\u9835\u9836\u9837" + + "\u9838\u9839\u983a\u983b\u983c\u983d\u983e\u983f" + + /* CHIP_MULLINS */ "\u9850\u9851\u9852\u9853\u9854\u9855\u9856\u9857" + + "\u9858\u9859\u985A\u985B\u985C\u985D\u985E\u985F" + + /* CHIP_TOPAZ */ "\u6900\u6901\u6902\u6903\u6907" + + /* CHIP_TONGA */ "\u6920\u6921\u6928\u6929\u692B\u692F\u6930\u6938\u6939" + + /* CHIP_FIJI */ "\u7300\u730F" + + /* CHIP_CARRIZO */ "\u9870\u9874\u9875\u9876\u9877" + + /* CHIP_STONEY */ "\u98E4"); - public static bool AllowDeprecatedGpu + // https://dgpu-docs.intel.com/devices/hardware-table.html#gpus-with-unsupported-drivers + private static ReadOnlySpan IntelBlacklist => MemoryMarshal.Cast( + /* Xe (Tiger Lake, Elkhart Lake, Jasper Lake, Rocket Lake, Alder Lake, Raptor Lake) */ "\uA780\uA781" + + "\uA788\uA789\uA78A\uA782\uA78B\uA783\uA7A0\uA7A1\uA7A8\uA7AA\uA7AB\uA7AC\uA7AD\uA7A9\uA721\u4905\u4907" + + "\u4908\u4909\u4680\u4690\u4688\u468A\u468B\u4682\u4692\u4693\u46D3\u46D4\u46D0\u46D1\u46D2\u4626\u4628" + + "\u462A\u46A2\u46B3\u46C2\u46A3\u46B2\u46C3\u46A0\u46B0\u46C0\u46A6\u46AA\u46A8\u46A1\u46B1\u46C1\u4C8A" + + "\u4C8B\u4C90\u4C9A\u4E71\u4E61\u4E57\u4E55\u4E51\u4557\u4555\u4571\u4551\u4541\u9A59\u9A78\u9A60\u9A70" + + "\u9A68\u9A40\u9A49" + + /* Xe-LPG (Meteor Lake) */ "\u7D40\u7D45\u7D55\u7DD5" + + /* Gen11 */ "\u8A70\u8A71\u8A56\u8A58\u8A5B\u8A5D\u8A54\u8A5A\u8A5C\u8A57\u8A59\u8A50\u8A51\u8A52\u8A53" + + /* Gen9 */ "\u3EA5\u3EA8\u3EA6\u3EA7\u3EA2\u3E90\u3E93\u3E99\u3E9C\u3EA1\u9BA5\u9BA8\u3EA4\u9B21\u9BA0" + + "\u9BA2\u9BA4\u9BAA\u9BAB\u9BAC\u87CA\u3EA3\u9B41\u9BC0\u9BC2\u9BC4\u9BCA\u9BCB\u9BCC\u3E91\u3E92\u3E98" + + "\u3E9B\u9BC5\u9BC8\u3E96\u3E9A\u3E94\u9BC6\u9BE6\u9BF6\u3EA9\u3EA0\u593B\u5923\u5926\u5927\u5917\u5912" + + "\u591B\u5916\u5921\u591A\u591D\u591E\u591C\u87C0\u5913\u5915\u5902\u5906\u590B\u590A\u5908\u590E\u3185" + + "\u3184\u1A85\u5A85\u0A84\u1A84\u5A84\u192A\u1932\u193B\u193A\u193D\u1923\u1926\u1927\u192B\u192D\u1912" + + "\u191B\u1913\u1915\u1917\u191A\u1916\u1921\u191D\u191E\u1902\u1906\u190B\u190A\u190E"); + + // https://download.nvidia.com/XFree86/Linux-x86_64/555.58/README/supportedchips.html + private static ReadOnlySpan NvidiaBlacklist => MemoryMarshal.Cast( + /* Kepler */ "\u0FC6\u0FC8\u0FC9\u0FCD\u0FCE\u0FD1\u0FD2\u0FD3\u0FD4\u0FD5\u0FD8\u0FD9\u0FDF\u0FE0\u0FE1" + + "\u0FE2\u0FE3\u0FE4\u0FE9\u0FEA\u0FEC\u0FED\u0FEE\u0FF6\u0FF8\u0FF9\u0FFA\u0FFB\u0FFC\u0FFD\u0FFE\u0FFF" + + "\u1001\u1004\u1005\u1007\u1008\u100A\u100C\u1021\u1022\u1023\u1024\u1026\u1027\u1028\u1029\u102A\u102D" + + "\u103A\u103C\u1180\u1183\u1184\u1185\u1185\u1187\u1188\u1189\u1189\u118A\u118E\u118F\u1193\u1194\u1195" + + "\u1198\u1199\u1199\u119A\u119D\u119E\u119F\u11A0\u11A1\u11A2\u11A3\u11A7\u11B4\u11B6\u11B7\u11B8\u11BA" + + "\u11BC\u11BD\u11BE\u11C0\u11C2\u11C3\u11C4\u11C5\u11C6\u11C8\u11CB\u11E0\u11E1\u11E2\u11E3\u11E3\u11FA" + + "\u11FC\u1280\u1281\u1282\u1284\u1286\u1287\u1288\u1289\u128B\u1290\u1290\u1291\u1292\u1292\u1293\u1295" + + "\u1295\u1296\u1298\u1299\u1299\u129A\u12B9\u12BA" + + /* Maxwell */ "\u1340\u1341\u1344\u1346\u1347\u1348\u1349\u134B\u134D\u134E\u134F\u137A\u137B\u1380" + + "\u1381\u1382\u1390\u1391\u1392\u1393\u1398\u1399\u139A\u139B\u139C\u139D\u13B0\u13B1\u13B2\u13B3\u13B4" + + "\u13B6\u13B9\u13BA\u13BB\u13BC\u13C0\u13C2\u13D7\u13D8\u13D9\u13DA\u13F0\u13F1\u13F2\u13F3\u13F8\u13F9" + + "\u13FA\u13FB\u1401\u1402\u1406\u1407\u1427\u1430\u1431\u1436\u1617\u1618\u1619\u161A\u1667\u174D\u174E" + + "\u179C\u17C2\u17C8\u17F0\u17F1\u17FD"); + + private static IDXGIFactory4? _factory; + + private static IDXGIFactory4? GetDxgiFactory() + { + if (_factory != null) { - get => ConfigurationHelper.GetValue(ConfigurationKeys.PerformanceAllowDeprecatedGpu, false); - set => ConfigurationHelper.SetValue(ConfigurationKeys.PerformanceAllowDeprecatedGpu, value.ToString()); - } - - // use string literal to efficiently store uint16 array - // https://github.com/torvalds/linux/blob/v6.10/drivers/gpu/drm/amd/amdgpu/amdgpu_drv.c#L1756 - private static ReadOnlySpan AmdBlacklist => MemoryMarshal.Cast( - /* CHIP_TAHITI */ "\u6780\u6784\u6788\u678A\u6790\u6791\u6792\u6798\u6799\u679A\u679B\u679E\u679F" + - /* CHIP_PITCAIRN */ "\u6800\u6801\u6802\u6806\u6808\u6809\u6810\u6811\u6816\u6817\u6818\u6819" + - /* CHIP_OLAND */ "\u6600\u6601\u6602\u6603\u6604\u6605\u6606\u6607\u6608" + - "\u6610\u6611\u6613\u6617\u6620\u6621\u6623\u6631" + - /* CHIP_VERDE */ "\u6820\u6821\u6822\u6823\u6824\u6825\u6826\u6827\u6828\u6829\u682A\u682B\u682C" + - "\u682D\u682F\u6830\u6831\u6835\u6837\u6838\u6839\u683B\u683D\u683F" + - /* CHIP_HAINAN */ "\u6660\u6663\u6664\u6665\u6667\u666F" + - /* CHIP_KAVERI */ "\u1304\u1305\u1306\u1307\u1309\u130A\u130B\u130C\u130D\u130E\u130F" + - "\u1310\u1311\u1312\u1313\u1315\u1316\u1317\u1318\u131B\u131C\u131D" + - /* CHIP_BONAIRE */ "\u6640\u6641\u6646\u6647\u6649\u6650\u6651\u6658\u665c\u665d\u665f" + - /* CHIP_HAWAII */ "\u67A0\u67A1\u67A2\u67A8\u67A9\u67AA\u67B0\u67B1\u67B8\u67B9\u67BA\u67BE" + - /* CHIP_KABINI */ "\u9830\u9831\u9832\u9833\u9834\u9835\u9836\u9837" + - "\u9838\u9839\u983a\u983b\u983c\u983d\u983e\u983f" + - /* CHIP_MULLINS */ "\u9850\u9851\u9852\u9853\u9854\u9855\u9856\u9857" + - "\u9858\u9859\u985A\u985B\u985C\u985D\u985E\u985F" + - /* CHIP_TOPAZ */ "\u6900\u6901\u6902\u6903\u6907" + - /* CHIP_TONGA */ "\u6920\u6921\u6928\u6929\u692B\u692F\u6930\u6938\u6939" + - /* CHIP_FIJI */ "\u7300\u730F" + - /* CHIP_CARRIZO */ "\u9870\u9874\u9875\u9876\u9877" + - /* CHIP_STONEY */ "\u98E4"); - - // https://dgpu-docs.intel.com/devices/hardware-table.html#gpus-with-unsupported-drivers - private static ReadOnlySpan IntelBlacklist => MemoryMarshal.Cast( - /* Xe (Tiger Lake, Elkhart Lake, Jasper Lake, Rocket Lake, Alder Lake, Raptor Lake) */ "\uA780\uA781" + - "\uA788\uA789\uA78A\uA782\uA78B\uA783\uA7A0\uA7A1\uA7A8\uA7AA\uA7AB\uA7AC\uA7AD\uA7A9\uA721\u4905\u4907" + - "\u4908\u4909\u4680\u4690\u4688\u468A\u468B\u4682\u4692\u4693\u46D3\u46D4\u46D0\u46D1\u46D2\u4626\u4628" + - "\u462A\u46A2\u46B3\u46C2\u46A3\u46B2\u46C3\u46A0\u46B0\u46C0\u46A6\u46AA\u46A8\u46A1\u46B1\u46C1\u4C8A" + - "\u4C8B\u4C90\u4C9A\u4E71\u4E61\u4E57\u4E55\u4E51\u4557\u4555\u4571\u4551\u4541\u9A59\u9A78\u9A60\u9A70" + - "\u9A68\u9A40\u9A49" + - /* Xe-LPG (Meteor Lake) */ "\u7D40\u7D45\u7D55\u7DD5" + - /* Gen11 */ "\u8A70\u8A71\u8A56\u8A58\u8A5B\u8A5D\u8A54\u8A5A\u8A5C\u8A57\u8A59\u8A50\u8A51\u8A52\u8A53" + - /* Gen9 */ "\u3EA5\u3EA8\u3EA6\u3EA7\u3EA2\u3E90\u3E93\u3E99\u3E9C\u3EA1\u9BA5\u9BA8\u3EA4\u9B21\u9BA0" + - "\u9BA2\u9BA4\u9BAA\u9BAB\u9BAC\u87CA\u3EA3\u9B41\u9BC0\u9BC2\u9BC4\u9BCA\u9BCB\u9BCC\u3E91\u3E92\u3E98" + - "\u3E9B\u9BC5\u9BC8\u3E96\u3E9A\u3E94\u9BC6\u9BE6\u9BF6\u3EA9\u3EA0\u593B\u5923\u5926\u5927\u5917\u5912" + - "\u591B\u5916\u5921\u591A\u591D\u591E\u591C\u87C0\u5913\u5915\u5902\u5906\u590B\u590A\u5908\u590E\u3185" + - "\u3184\u1A85\u5A85\u0A84\u1A84\u5A84\u192A\u1932\u193B\u193A\u193D\u1923\u1926\u1927\u192B\u192D\u1912" + - "\u191B\u1913\u1915\u1917\u191A\u1916\u1921\u191D\u191E\u1902\u1906\u190B\u190A\u190E"); - - // https://download.nvidia.com/XFree86/Linux-x86_64/555.58/README/supportedchips.html - private static ReadOnlySpan NvidiaBlacklist => MemoryMarshal.Cast( - /* Kepler */ "\u0FC6\u0FC8\u0FC9\u0FCD\u0FCE\u0FD1\u0FD2\u0FD3\u0FD4\u0FD5\u0FD8\u0FD9\u0FDF\u0FE0\u0FE1" + - "\u0FE2\u0FE3\u0FE4\u0FE9\u0FEA\u0FEC\u0FED\u0FEE\u0FF6\u0FF8\u0FF9\u0FFA\u0FFB\u0FFC\u0FFD\u0FFE\u0FFF" + - "\u1001\u1004\u1005\u1007\u1008\u100A\u100C\u1021\u1022\u1023\u1024\u1026\u1027\u1028\u1029\u102A\u102D" + - "\u103A\u103C\u1180\u1183\u1184\u1185\u1185\u1187\u1188\u1189\u1189\u118A\u118E\u118F\u1193\u1194\u1195" + - "\u1198\u1199\u1199\u119A\u119D\u119E\u119F\u11A0\u11A1\u11A2\u11A3\u11A7\u11B4\u11B6\u11B7\u11B8\u11BA" + - "\u11BC\u11BD\u11BE\u11C0\u11C2\u11C3\u11C4\u11C5\u11C6\u11C8\u11CB\u11E0\u11E1\u11E2\u11E3\u11E3\u11FA" + - "\u11FC\u1280\u1281\u1282\u1284\u1286\u1287\u1288\u1289\u128B\u1290\u1290\u1291\u1292\u1292\u1293\u1295" + - "\u1295\u1296\u1298\u1299\u1299\u129A\u12B9\u12BA" + - /* Maxwell */ "\u1340\u1341\u1344\u1346\u1347\u1348\u1349\u134B\u134D\u134E\u134F\u137A\u137B\u1380" + - "\u1381\u1382\u1390\u1391\u1392\u1393\u1398\u1399\u139A\u139B\u139C\u139D\u13B0\u13B1\u13B2\u13B3\u13B4" + - "\u13B6\u13B9\u13BA\u13BB\u13BC\u13C0\u13C2\u13D7\u13D8\u13D9\u13DA\u13F0\u13F1\u13F2\u13F3\u13F8\u13F9" + - "\u13FA\u13FB\u1401\u1402\u1406\u1407\u1427\u1430\u1431\u1436\u1617\u1618\u1619\u161A\u1667\u174D\u174E" + - "\u179C\u17C2\u17C8\u17F0\u17F1\u17FD"); - - private static IDXGIFactory4? _factory; - - private static IDXGIFactory4? GetDxgiFactory() - { - if (_factory != null) + try { - try - { - if (!_factory.IsCurrent()) - { - _factory = null; - } - else - { - return _factory; - } - } - catch (Exception) + if (!_factory.IsCurrent()) { _factory = null; } + else + { + return _factory; + } + } + catch (Exception) + { + _factory = null; + } + } + + var hr = PInvoke.CreateDXGIFactory2(0, typeof(IDXGIFactory4).GUID, out var comObj); + if (hr.Failed) + { + return null; + } + + _factory = (IDXGIFactory4)comObj; + return _factory; + } + + public static List GetGpuOptions() + { + var factory = GetDxgiFactory(); + + if (factory == null) + { + return _unavailableOptions; + } + + uint i = 0; + + var options = new List() { Disable }; + + while (true) + { + var hr = factory.EnumAdapters1(i, out var adapter); + if (hr == HRESULT.DXGI_ERROR_NOT_FOUND) + { + break; } - var hr = PInvoke.CreateDXGIFactory2(0, typeof(IDXGIFactory4).GUID, out var comObj); - if (hr.Failed) + var index = i; + i++; + + var desc = adapter.GetDesc1(); + var instancePath = GetAdapterInstancePath(desc.AdapterLuid); + var driverInfo = GetGpuDriverInformation(desc.Description.ToString(), instancePath); + + if (!CheckGpu(adapter, ref desc, instancePath, driverInfo, out var deprecated)) + { + continue; + } + + if (index == 0) + { + options.Add(new SystemDefaultOption(driverInfo) { IsDeprecated = deprecated }); + } + + var opt = new SpecificGpuOption(index, desc, instancePath ?? string.Empty, driverInfo) { IsDeprecated = deprecated }; + + options.Add(opt); + } + + if (i == 0) + { + return _unavailableOptions; + } + + // show index if multiple adapters with same name installed + var counter = new Dictionary(); + foreach (var option in options.OfType()) + { + counter[option.GpuInfo.Description] = counter.GetValueOrDefault(option.GpuInfo.Description, 0) + 1; + } + + foreach (var option in options.OfType()) + { + if (counter.GetValueOrDefault(option.GpuInfo.Description, 0) > 1) + { + option.ShowIndex = true; + } + } + + return options; + } + + private static bool CheckD3D12Support(IDXGIAdapter1 adapter, bool requireFl12 = false) + { + // using the same feature level as onnxruntime does + var hr = D3D12CreateDevice(adapter, requireFl12 ? D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_12_0 : D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_0, typeof(ID3D12Device).GUID, 0); + + return hr == HRESULT.S_FALSE; + + [DllImport("d3d12.dll", ExactSpelling = true)] + static extern HRESULT D3D12CreateDevice([MarshalAs(UnmanagedType.IUnknown)] object pAdapter, D3D_FEATURE_LEVEL minimumFeatureLevel, in Guid riid, nint ppDevice); + } + + private static unsafe string? GetAdapterInstancePath(LUID luid) + { + try + { + var req = new DISPLAYCONFIG_ADAPTER_NAME + { + header = new DISPLAYCONFIG_DEVICE_INFO_HEADER + { + size = (uint)sizeof(DISPLAYCONFIG_ADAPTER_NAME), + adapterId = luid, + id = 0, + type = DISPLAYCONFIG_DEVICE_INFO_TYPE.DISPLAYCONFIG_DEVICE_INFO_GET_ADAPTER_NAME, + }, + }; + + // var adpName = + PInvoke.DisplayConfigGetDeviceInfo(ref req.header); + + var interfacePath = req.adapterDevicePath.ToString(); + uint size = 0; + var err = PInvoke.CM_Get_Device_Interface_Property(interfacePath, PInvoke.DEVPKEY_Device_InstanceId, out var type, null, ref size, 0); + + if (err != CONFIGRET.CR_BUFFER_SMALL) { return null; } - _factory = (IDXGIFactory4)comObj; - return _factory; - } - - public static List GetGpuOptions() - { - var factory = GetDxgiFactory(); - - if (factory == null) - { - return _unavailableOptions; - } - - uint i = 0; - - var options = new List() { Disable }; - - while (true) - { - var hr = factory.EnumAdapters1(i, out var adapter); - if (hr == HRESULT.DXGI_ERROR_NOT_FOUND) - { - break; - } - - var index = i; - i++; - - var desc = adapter.GetDesc1(); - var instancePath = GetAdapterInstancePath(desc.AdapterLuid); - var driverInfo = GetGpuDriverInformation(desc.Description.ToString(), instancePath); - - if (!CheckGpu(adapter, ref desc, instancePath, driverInfo, out var deprecated)) - { - continue; - } - - if (index == 0) - { - options.Add(new SystemDefaultOption(driverInfo) { IsDeprecated = deprecated }); - } - - var opt = new SpecificGpuOption(index, desc, instancePath ?? string.Empty, driverInfo) { IsDeprecated = deprecated }; - - options.Add(opt); - } - - if (i == 0) - { - return _unavailableOptions; - } - - // show index if multiple adapters with same name installed - var counter = new Dictionary(); - foreach (var option in options.OfType()) - { - counter[option.GpuInfo.Description] = counter.GetValueOrDefault(option.GpuInfo.Description, 0) + 1; - } - - foreach (var option in options.OfType()) - { - if (counter.GetValueOrDefault(option.GpuInfo.Description, 0) > 1) - { - option.ShowIndex = true; - } - } - - return options; - } - - private static bool CheckD3D12Support(IDXGIAdapter1 adapter, bool requireFl12 = false) - { - // using the same feature level as onnxruntime does - var hr = D3D12CreateDevice(adapter, requireFl12 ? D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_12_0 : D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_0, typeof(ID3D12Device).GUID, 0); - - return hr == HRESULT.S_FALSE; - - [DllImport("d3d12.dll", ExactSpelling = true)] - static extern HRESULT D3D12CreateDevice([MarshalAs(UnmanagedType.IUnknown)] object pAdapter, D3D_FEATURE_LEVEL minimumFeatureLevel, in Guid riid, nint ppDevice); - } - - private static unsafe string? GetAdapterInstancePath(LUID luid) - { - try - { - var req = new DISPLAYCONFIG_ADAPTER_NAME - { - header = new DISPLAYCONFIG_DEVICE_INFO_HEADER - { - size = (uint)sizeof(DISPLAYCONFIG_ADAPTER_NAME), - adapterId = luid, - id = 0, - type = DISPLAYCONFIG_DEVICE_INFO_TYPE.DISPLAYCONFIG_DEVICE_INFO_GET_ADAPTER_NAME, - }, - }; - - // var adpName = - PInvoke.DisplayConfigGetDeviceInfo(ref req.header); - - var interfacePath = req.adapterDevicePath.ToString(); - uint size = 0; - var err = PInvoke.CM_Get_Device_Interface_Property(interfacePath, PInvoke.DEVPKEY_Device_InstanceId, out var type, null, ref size, 0); - - if (err != CONFIGRET.CR_BUFFER_SMALL) - { - return null; - } - - if (type != DEVPROPTYPE.DEVPROP_TYPE_STRING) - { - return null; - } - - var buf = ArrayPool.Shared.Rent((int)size); - string? result; - - fixed (byte* ptr = buf) - { - err = PInvoke.CM_Get_Device_Interface_Property(interfacePath, PInvoke.DEVPKEY_Device_InstanceId, out _, ptr, ref size, 0); - if (err != CONFIGRET.CR_SUCCESS) - { - return null; - } - - var cch = (int)(size / 2) - 1; - result = Marshal.PtrToStringUni((nint)ptr, cch); - } - - return result; - } - catch + if (type != DEVPROPTYPE.DEVPROP_TYPE_STRING) { return null; } + + var buf = ArrayPool.Shared.Rent((int)size); + string? result; + + fixed (byte* ptr = buf) + { + err = PInvoke.CM_Get_Device_Interface_Property(interfacePath, PInvoke.DEVPKEY_Device_InstanceId, out _, ptr, ref size, 0); + if (err != CONFIGRET.CR_SUCCESS) + { + return null; + } + + var cch = (int)(size / 2) - 1; + result = Marshal.PtrToStringUni((nint)ptr, cch); + } + + return result; + } + catch + { + return null; + } + } + + private static bool IsIndirectDisplayAdapter(string instancePath) + { + try + { + using var registryKey = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Enum\" + instancePath, false); + + var upperFilters = registryKey?.GetValue("UpperFilters"); + + switch (upperFilters) + { + case null: + return false; + case string[] sa: + return sa.Any(s => s.Equals("IndirectKmd", StringComparison.InvariantCultureIgnoreCase)); + } + } + catch + { + // ignore } - private static bool IsIndirectDisplayAdapter(string instancePath) + return false; + } + + private static unsafe GpuDriverInformation GetGpuDriverInformation(string description, string? instancePath) + { + if (instancePath == null) { - try + return new(description, null, null); + } + + uint devInst; + + fixed (char* ptr = instancePath) + { + var err2 = PInvoke.CM_Locate_DevNode(out devInst, ptr, CM_LOCATE_DEVNODE_FLAGS.CM_LOCATE_DEVNODE_NORMAL); + + if (err2 != CONFIGRET.CR_SUCCESS) { - using var registryKey = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Enum\" + instancePath, false); + return new(description, null, null); + } + } - var upperFilters = registryKey?.GetValue("UpperFilters"); + System.Runtime.InteropServices.ComTypes.FILETIME ft; + uint size = (uint)sizeof(System.Runtime.InteropServices.ComTypes.FILETIME); - switch (upperFilters) + var err = PInvoke.CM_Get_DevNode_Property(devInst, PInvoke.DEVPKEY_Device_DriverDate, out _, (byte*)(&ft), ref size, 0); + + if (err != CONFIGRET.CR_SUCCESS) + { + return new(description, null, null); + } + + var driverDate = ft.ToDateTime().Date; + + size = 0; + err = PInvoke.CM_Get_DevNode_Property(devInst, PInvoke.DEVPKEY_Device_DriverVersion, out _, null, ref size, 0); + + if (err != CONFIGRET.CR_BUFFER_SMALL) + { + return new(description, null, driverDate); + } + + var buf = new byte[size]; + string? driverVersion; + fixed (byte* ptr = buf) + { + // err = + PInvoke.CM_Get_DevNode_Property(devInst, PInvoke.DEVPKEY_Device_DriverVersion, out _, ptr, ref size, 0); + driverVersion = Marshal.PtrToStringUni((nint)ptr); + } + + return new(description, driverVersion, driverDate); + } + + private static bool CheckGpu(IDXGIAdapter1 adapter, ref DXGI_ADAPTER_DESC1 desc, string? instancePath, GpuDriverInformation driverInfo, out bool deprecated) + { + deprecated = false; + if (((uint)desc.Flags & (uint)DXGI_ADAPTER_FLAG.DXGI_ADAPTER_FLAG_SOFTWARE) != 0) + { + // skip software device + return false; + } + + if (!CheckD3D12Support(adapter, requireFl12: false)) + { + // skip adapters without D3D12 support + // onnxruntime uses FL 11_0 for device creation + return false; + } + + if (instancePath != null && IsIndirectDisplayAdapter(instancePath)) + { + // skip virtual adapters (streaming/RDP) + return false; + } + + // check for deprecated GPU + bool IsGpuDeprecated(ref DXGI_ADAPTER_DESC1 desc) + { + // reject GPUs without native D3D12 support (FL 12_0) + if (!CheckD3D12Support(adapter, requireFl12: true)) + { + return true; + } + + // reject drivers that predates DirectML (released alongside with Windows 10 1903, i.e. 2019-05-21) + if (instancePath != null) + { + var devDate = driverInfo.DriverDate; + if (devDate.HasValue && devDate.Value < new DateTime(2019, 5, 21)) { - case null: - return false; - case string[] sa: - return sa.Any(s => s.Equals("IndirectKmd", StringComparison.InvariantCultureIgnoreCase)); + return true; } } - catch + + // reject legacy GPUs + // since we filtered GPUs without FL 12_0, only a few generations will be listed here + // Intel: pre-Xe (Gen9, Gen11) + // AMD: pre-Polaris (Sea Islands, Volcanic Islands, Arctic Islands) + // NVIDIA: maybe pre-Pascal? (Kepler, Maxwell) + ReadOnlySpan devIdBlacklist = desc.VendorId switch { - // ignore + 0x8086 => IntelBlacklist, + 0x1002 => AmdBlacklist, + 0x10de => NvidiaBlacklist, + _ => default, + }; + + return devIdBlacklist.Contains((ushort)desc.DeviceId); + } + + deprecated = IsGpuDeprecated(ref desc); + + return true; + } + + public static GpuOption GetCurrent() + { + var preferredGpuInstancePath = ConfigurationHelper.GetValue(ConfigurationKeys.PerformancePreferredGpuInstancePath, string.Empty); + var preferredGpuDescription = ConfigurationHelper.GetValue(ConfigurationKeys.PerformancePreferredGpuDescription, string.Empty); + + GpuOption result; + if (ConfigurationHelper.GetValue(ConfigurationKeys.PerformanceUseGpu, false)) + { + var options = GetGpuOptions(); + if (ReferenceEquals(options, _unavailableOptions)) + { + return Disable; + } + + result = options.OfType().FirstOrDefault(Disable); + if (preferredGpuInstancePath != string.Empty) + { + result = options.OfType().FirstOrDefault(x => string.Equals(((SpecificGpuOption)x).InstancePath, preferredGpuInstancePath, StringComparison.Ordinal), result); + } + + if (result is SystemDefaultOption && preferredGpuDescription != string.Empty) + { + // instance path lookup failed, fallback to description (name) lookup + result = options.OfType().FirstOrDefault(x => string.Equals(((SpecificGpuOption)x).GpuInfo.Description, preferredGpuDescription, StringComparison.OrdinalIgnoreCase), result); + } + } + else + { + result = Disable; + } + + return result; + } + + public static void SetCurrent(GpuOption option) + { + switch (option) + { + case DisableOption: + ConfigurationHelper.SetValue(ConfigurationKeys.PerformanceUseGpu, "false"); + break; + case SystemDefaultOption: + ConfigurationHelper.SetValue(ConfigurationKeys.PerformanceUseGpu, "true"); + ConfigurationHelper.SetValue(ConfigurationKeys.PerformancePreferredGpuDescription, string.Empty); + ConfigurationHelper.SetValue(ConfigurationKeys.PerformancePreferredGpuInstancePath, string.Empty); + break; + case SpecificGpuOption x: + ConfigurationHelper.SetValue(ConfigurationKeys.PerformanceUseGpu, "true"); + ConfigurationHelper.SetValue(ConfigurationKeys.PerformancePreferredGpuDescription, x.GpuInfo.Description); + ConfigurationHelper.SetValue(ConfigurationKeys.PerformancePreferredGpuInstancePath, x.InstancePath); + break; + } + } + + public record GpuDriverInformation(string Description, string? DriverVersion, DateTime? DriverDate); + + public bool IsDeprecated { get; set; } + + public class DisableOption : GpuOption + { + public static DisableOption Instance { get; } = new(); + + private DisableOption() + { + } + + public override bool Equals(object? obj) + { + return obj is DisableOption; + } + + public override int GetHashCode() => typeof(DisableOption).GetHashCode(); + + public override string ToString() => LocalizationHelper.GetString("GpuOptionDisable"); + } + + public abstract class EnableOption : GpuOption + { + public abstract uint Index { get; } + + public abstract GpuDriverInformation GpuInfo { get; } + } + + public class SystemDefaultOption(GpuDriverInformation info) : EnableOption + { + public override uint Index => 0; + + public override GpuDriverInformation GpuInfo { get; } = info; + + public override bool Equals(object? obj) + { + return obj is SystemDefaultOption; + } + + public override int GetHashCode() => typeof(SystemDefaultOption).GetHashCode(); + + public override string ToString() => LocalizationHelper.GetString("GpuOptionSystemDefault") + $" ({GpuInfo.Description})"; + } + + public class SpecificGpuOption : EnableOption + { + private readonly DXGI_ADAPTER_DESC1 _description; + private readonly uint _index; + private readonly string _instancePath; + + public override GpuDriverInformation GpuInfo { get; } + + public bool ShowIndex { get; set; } + + internal SpecificGpuOption(uint index, DXGI_ADAPTER_DESC1 description, string instancePath, GpuDriverInformation info) + { + _index = index; + _description = description; + _instancePath = instancePath; + GpuInfo = info; + } + + public override uint Index => _index; + + public string InstancePath => _instancePath; + + public override bool Equals(object? obj) + { + if (obj is SpecificGpuOption x) + { + return x._description.AdapterLuid.AsLong() == _description.AdapterLuid.AsLong(); } return false; } - private static unsafe GpuDriverInformation GetGpuDriverInformation(string description, string? instancePath) - { - if (instancePath == null) - { - return new(description, null, null); - } + // ReSharper disable once UsageOfDefaultStructEquality + public override int GetHashCode() => HashCode.Combine(typeof(SpecificGpuOption), _description, _index, _instancePath); - uint devInst; - - fixed (char* ptr = instancePath) - { - var err2 = PInvoke.CM_Locate_DevNode(out devInst, ptr, CM_LOCATE_DEVNODE_FLAGS.CM_LOCATE_DEVNODE_NORMAL); - - if (err2 != CONFIGRET.CR_SUCCESS) - { - return new(description, null, null); - } - } - - System.Runtime.InteropServices.ComTypes.FILETIME ft; - uint size = (uint)sizeof(System.Runtime.InteropServices.ComTypes.FILETIME); - - var err = PInvoke.CM_Get_DevNode_Property(devInst, PInvoke.DEVPKEY_Device_DriverDate, out _, (byte*)(&ft), ref size, 0); - - if (err != CONFIGRET.CR_SUCCESS) - { - return new(description, null, null); - } - - var driverDate = ft.ToDateTime().Date; - - size = 0; - err = PInvoke.CM_Get_DevNode_Property(devInst, PInvoke.DEVPKEY_Device_DriverVersion, out _, null, ref size, 0); - - if (err != CONFIGRET.CR_BUFFER_SMALL) - { - return new(description, null, driverDate); - } - - var buf = new byte[size]; - string? driverVersion; - fixed (byte* ptr = buf) - { - // err = - PInvoke.CM_Get_DevNode_Property(devInst, PInvoke.DEVPKEY_Device_DriverVersion, out _, ptr, ref size, 0); - driverVersion = Marshal.PtrToStringUni((nint)ptr); - } - - return new(description, driverVersion, driverDate); - } - - private static bool CheckGpu(IDXGIAdapter1 adapter, ref DXGI_ADAPTER_DESC1 desc, string? instancePath, GpuDriverInformation driverInfo, out bool deprecated) - { - deprecated = false; - if (((uint)desc.Flags & (uint)DXGI_ADAPTER_FLAG.DXGI_ADAPTER_FLAG_SOFTWARE) != 0) - { - // skip software device - return false; - } - - if (!CheckD3D12Support(adapter, requireFl12: false)) - { - // skip adapters without D3D12 support - // onnxruntime uses FL 11_0 for device creation - return false; - } - - if (instancePath != null && IsIndirectDisplayAdapter(instancePath)) - { - // skip virtual adapters (streaming/RDP) - return false; - } - - // check for deprecated GPU - bool IsGpuDeprecated(ref DXGI_ADAPTER_DESC1 desc) - { - // reject GPUs without native D3D12 support (FL 12_0) - if (!CheckD3D12Support(adapter, requireFl12: true)) - { - return true; - } - - // reject drivers that predates DirectML (released alongside with Windows 10 1903, i.e. 2019-05-21) - if (instancePath != null) - { - var devDate = driverInfo.DriverDate; - if (devDate.HasValue && devDate.Value < new DateTime(2019, 5, 21)) - { - return true; - } - } - - // reject legacy GPUs - // since we filtered GPUs without FL 12_0, only a few generations will be listed here - // Intel: pre-Xe (Gen9, Gen11) - // AMD: pre-Polaris (Sea Islands, Volcanic Islands, Arctic Islands) - // NVIDIA: maybe pre-Pascal? (Kepler, Maxwell) - ReadOnlySpan devIdBlacklist = desc.VendorId switch - { - 0x8086 => IntelBlacklist, - 0x1002 => AmdBlacklist, - 0x10de => NvidiaBlacklist, - _ => default, - }; - - return devIdBlacklist.Contains((ushort)desc.DeviceId); - } - - deprecated = IsGpuDeprecated(ref desc); - - return true; - } - - public static GpuOption GetCurrent() - { - var preferredGpuInstancePath = ConfigurationHelper.GetValue(ConfigurationKeys.PerformancePreferredGpuInstancePath, string.Empty); - var preferredGpuDescription = ConfigurationHelper.GetValue(ConfigurationKeys.PerformancePreferredGpuDescription, string.Empty); - - GpuOption result; - if (ConfigurationHelper.GetValue(ConfigurationKeys.PerformanceUseGpu, false)) - { - var options = GetGpuOptions(); - if (ReferenceEquals(options, _unavailableOptions)) - { - return Disable; - } - - result = options.OfType().FirstOrDefault(Disable); - if (preferredGpuInstancePath != string.Empty) - { - result = options.OfType().FirstOrDefault(x => string.Equals(((SpecificGpuOption)x).InstancePath, preferredGpuInstancePath, StringComparison.Ordinal), result); - } - - if (result is SystemDefaultOption && preferredGpuDescription != string.Empty) - { - // instance path lookup failed, fallback to description (name) lookup - result = options.OfType().FirstOrDefault(x => string.Equals(((SpecificGpuOption)x).GpuInfo.Description, preferredGpuDescription, StringComparison.OrdinalIgnoreCase), result); - } - } - else - { - result = Disable; - } - - return result; - } - - public static void SetCurrent(GpuOption option) - { - switch (option) - { - case DisableOption: - ConfigurationHelper.SetValue(ConfigurationKeys.PerformanceUseGpu, "false"); - break; - case SystemDefaultOption: - ConfigurationHelper.SetValue(ConfigurationKeys.PerformanceUseGpu, "true"); - ConfigurationHelper.SetValue(ConfigurationKeys.PerformancePreferredGpuDescription, string.Empty); - ConfigurationHelper.SetValue(ConfigurationKeys.PerformancePreferredGpuInstancePath, string.Empty); - break; - case SpecificGpuOption x: - ConfigurationHelper.SetValue(ConfigurationKeys.PerformanceUseGpu, "true"); - ConfigurationHelper.SetValue(ConfigurationKeys.PerformancePreferredGpuDescription, x.GpuInfo.Description); - ConfigurationHelper.SetValue(ConfigurationKeys.PerformancePreferredGpuInstancePath, x.InstancePath); - break; - } - } - - public record GpuDriverInformation(string Description, string? DriverVersion, DateTime? DriverDate); - - public bool IsDeprecated { get; set; } - - public class DisableOption : GpuOption - { - public static DisableOption Instance { get; } = new(); - - private DisableOption() - { - } - - public override bool Equals(object? obj) - { - return obj is DisableOption; - } - - public override int GetHashCode() => typeof(DisableOption).GetHashCode(); - - public override string ToString() => LocalizationHelper.GetString("GpuOptionDisable"); - } - - public abstract class EnableOption : GpuOption - { - public abstract uint Index { get; } - - public abstract GpuDriverInformation GpuInfo { get; } - } - - public class SystemDefaultOption(GpuDriverInformation info) : EnableOption - { - public override uint Index => 0; - - public override GpuDriverInformation GpuInfo { get; } = info; - - public override bool Equals(object? obj) - { - return obj is SystemDefaultOption; - } - - public override int GetHashCode() => typeof(SystemDefaultOption).GetHashCode(); - - public override string ToString() => LocalizationHelper.GetString("GpuOptionSystemDefault") + $" ({GpuInfo.Description})"; - } - - public class SpecificGpuOption : EnableOption - { - private readonly DXGI_ADAPTER_DESC1 _description; - private readonly uint _index; - private readonly string _instancePath; - - public override GpuDriverInformation GpuInfo { get; } - - public bool ShowIndex { get; set; } - - internal SpecificGpuOption(uint index, DXGI_ADAPTER_DESC1 description, string instancePath, GpuDriverInformation info) - { - _index = index; - _description = description; - _instancePath = instancePath; - GpuInfo = info; - } - - public override uint Index => _index; - - public string InstancePath => _instancePath; - - public override bool Equals(object? obj) - { - if (obj is SpecificGpuOption x) - { - return x._description.AdapterLuid.AsLong() == _description.AdapterLuid.AsLong(); - } - - return false; - } - - // ReSharper disable once UsageOfDefaultStructEquality - public override int GetHashCode() => HashCode.Combine(typeof(SpecificGpuOption), _description, _index, _instancePath); - - public override string ToString() => ShowIndex ? _description + $" (GPU {_index})" : _description.Description.ToString(); - } + public override string ToString() => ShowIndex ? _description + $" (GPU {_index})" : _description.Description.ToString(); } } diff --git a/src/MaaWpfGui/Helper/HoverDelayHelper.cs b/src/MaaWpfGui/Helper/HoverDelayHelper.cs index ffd27fdac2..9dc592bf5f 100644 --- a/src/MaaWpfGui/Helper/HoverDelayHelper.cs +++ b/src/MaaWpfGui/Helper/HoverDelayHelper.cs @@ -13,94 +13,92 @@ using System; using System.Windows; -using System.Windows.Media.Animation; using System.Windows.Threading; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +public static class HoverDelayHelper { - public static class HoverDelayHelper + // 是否已完成悬停计时(只读,供外部绑定) + public static readonly DependencyPropertyKey IsHoveredPropertyKey = + DependencyProperty.RegisterAttachedReadOnly( + "IsHovered", + typeof(bool), + typeof(HoverDelayHelper), + new(false, OnPropertyChanged)); + + public static readonly DependencyProperty IsHoveredProperty = IsHoveredPropertyKey.DependencyProperty; + + public static bool GetIsHovered(DependencyObject obj) => (bool)obj.GetValue(IsHoveredProperty); + + private static readonly DependencyProperty _hoverTimerProperty = + DependencyProperty.RegisterAttached( + "_hoverTimer", + typeof(DispatcherTimer), + typeof(HoverDelayHelper), + new(null)); + + public static readonly DependencyProperty DelayProperty = + DependencyProperty.RegisterAttached( + "Delay", + typeof(int), + typeof(HoverDelayHelper), + new(-1, OnPropertyChanged)); + + // Getter/Setter 方法 + public static int GetDelay(DependencyObject obj) { - // 是否已完成悬停计时(只读,供外部绑定) - public static readonly DependencyPropertyKey IsHoveredPropertyKey = - DependencyProperty.RegisterAttachedReadOnly( - "IsHovered", - typeof(bool), - typeof(HoverDelayHelper), - new(false, OnPropertyChanged)); + int value = (int)obj.GetValue(DelayProperty); + return value == -1 ? 0 : value; + } - public static readonly DependencyProperty IsHoveredProperty = IsHoveredPropertyKey.DependencyProperty; + public static void SetDelay(DependencyObject obj, int value) => obj.SetValue(DelayProperty, value); - public static bool GetIsHovered(DependencyObject obj) => (bool)obj.GetValue(IsHoveredProperty); - - private static readonly DependencyProperty _hoverTimerProperty = - DependencyProperty.RegisterAttached( - "_hoverTimer", - typeof(DispatcherTimer), - typeof(HoverDelayHelper), - new(null)); - - public static readonly DependencyProperty DelayProperty = - DependencyProperty.RegisterAttached( - "Delay", - typeof(int), - typeof(HoverDelayHelper), - new(-1, OnPropertyChanged)); - - // Getter/Setter 方法 - public static int GetDelay(DependencyObject obj) + private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not UIElement element) { - int value = (int)obj.GetValue(DelayProperty); - return value == -1 ? 0 : value; + return; } - public static void SetDelay(DependencyObject obj, int value) => obj.SetValue(DelayProperty, value); + element.MouseEnter -= OnMouseEnter; + element.MouseLeave -= OnMouseLeave; + var timer = (DispatcherTimer)element.GetValue(_hoverTimerProperty); + timer?.Stop(); - private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + element.MouseEnter += OnMouseEnter; + element.MouseLeave += OnMouseLeave; + } + + private static void OnMouseEnter(object sender, RoutedEventArgs e) + { + var element = (UIElement)sender; + var delay = GetDelay(element); + var timer = (DispatcherTimer)element.GetValue(_hoverTimerProperty); + + timer?.Stop(); + + timer = new() { Interval = TimeSpan.FromMilliseconds(delay) }; + timer.Tick += (s, args) => { - if (d is not UIElement element) + timer.Stop(); + if (!element.IsMouseOver) { return; } - element.MouseEnter -= OnMouseEnter; - element.MouseLeave -= OnMouseLeave; - var timer = (DispatcherTimer)element.GetValue(_hoverTimerProperty); - timer?.Stop(); + element.SetValue(IsHoveredPropertyKey, true); + }; + element.SetValue(_hoverTimerProperty, timer); + timer.Start(); + } - element.MouseEnter += OnMouseEnter; - element.MouseLeave += OnMouseLeave; - } + private static void OnMouseLeave(object sender, RoutedEventArgs e) + { + var element = (UIElement)sender; + var timer = (DispatcherTimer)element.GetValue(_hoverTimerProperty); + timer?.Stop(); - private static void OnMouseEnter(object sender, RoutedEventArgs e) - { - var element = (UIElement)sender; - var delay = GetDelay(element); - var timer = (DispatcherTimer)element.GetValue(_hoverTimerProperty); - - timer?.Stop(); - - timer = new() { Interval = TimeSpan.FromMilliseconds(delay) }; - timer.Tick += (s, args) => - { - timer.Stop(); - if (!element.IsMouseOver) - { - return; - } - - element.SetValue(IsHoveredPropertyKey, true); - }; - element.SetValue(_hoverTimerProperty, timer); - timer.Start(); - } - - private static void OnMouseLeave(object sender, RoutedEventArgs e) - { - var element = (UIElement)sender; - var timer = (DispatcherTimer)element.GetValue(_hoverTimerProperty); - timer?.Stop(); - - element.SetValue(IsHoveredPropertyKey, false); - } + element.SetValue(IsHoveredPropertyKey, false); } } diff --git a/src/MaaWpfGui/Helper/HttpResponseHelper.cs b/src/MaaWpfGui/Helper/HttpResponseHelper.cs index 498dcb8fe0..52fc3d75d6 100644 --- a/src/MaaWpfGui/Helper/HttpResponseHelper.cs +++ b/src/MaaWpfGui/Helper/HttpResponseHelper.cs @@ -19,79 +19,78 @@ using System.Net.Http; using System.Threading.Tasks; using Serilog; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +public static class HttpResponseHelper { - public static class HttpResponseHelper + private static readonly ILogger _logger = Log.ForContext("SourceContext", "HttpResponseHelper"); + + public static async Task SaveResponseToFileAsync(HttpResponseMessage? response, string saveTo, bool saveAndDeleteTmp = true) { - private static readonly ILogger _logger = Log.ForContext("SourceContext", "HttpResponseHelper"); + saveTo = Path.Combine(PathsHelper.BaseDir, saveTo); - public static async Task SaveResponseToFileAsync(HttpResponseMessage? response, string saveTo, bool saveAndDeleteTmp = true) + var tempFile = saveTo + ".tmp"; + if (File.Exists(tempFile)) { - saveTo = Path.Combine(PathsHelper.BaseDir, saveTo); - - var tempFile = saveTo + ".tmp"; - if (File.Exists(tempFile)) - { - File.Delete(tempFile); - } - - try - { - await using var stream = await GetStreamAsync(response); - await using var fileStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true); - await stream!.CopyToAsync(fileStream).ConfigureAwait(false); - } - catch (Exception e) - { - _logger.Error(e, "Failed to save response to file: "); - return false; - } - - // ReSharper disable once InvertIf - if (saveAndDeleteTmp) - { - File.Copy(tempFile, saveTo, true); - File.Delete(tempFile); - } - - return true; + File.Delete(tempFile); } - // ReSharper disable once MemberCanBePrivate.Global - public static async Task GetStreamAsync(HttpResponseMessage? response) + try { - if (response == null) - { - return null; - } - - try - { - return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - } - catch (Exception e) - { - _logger.Error(e, "Failed to get response as stream: "); - return null; - } + await using var stream = await GetStreamAsync(response); + await using var fileStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true); + await stream!.CopyToAsync(fileStream).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.Error(e, "Failed to save response to file: "); + return false; } - public static async Task GetStringAsync(HttpResponseMessage? response) + // ReSharper disable once InvertIf + if (saveAndDeleteTmp) { - if (response == null) - { - return string.Empty; - } + File.Copy(tempFile, saveTo, true); + File.Delete(tempFile); + } - try - { - return await response.Content.ReadAsStringAsync().ConfigureAwait(false); - } - catch (Exception e) - { - _logger.Error(e, "Failed to get response as string: "); - return string.Empty; - } + return true; + } + + // ReSharper disable once MemberCanBePrivate.Global + public static async Task GetStreamAsync(HttpResponseMessage? response) + { + if (response == null) + { + return null; + } + + try + { + return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + } + catch (Exception e) + { + _logger.Error(e, "Failed to get response as stream: "); + return null; + } + } + + public static async Task GetStringAsync(HttpResponseMessage? response) + { + if (response == null) + { + return string.Empty; + } + + try + { + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } + catch (Exception e) + { + _logger.Error(e, "Failed to get response as string: "); + return string.Empty; } } } diff --git a/src/MaaWpfGui/Helper/Instances.cs b/src/MaaWpfGui/Helper/Instances.cs index 7a2261e759..e13758086c 100644 --- a/src/MaaWpfGui/Helper/Instances.cs +++ b/src/MaaWpfGui/Helper/Instances.cs @@ -25,94 +25,93 @@ using MaaWpfGui.ViewModels.UserControl.TaskQueue; using Stylet; using StyletIoC; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +/// +/// The instance manager. +/// +public static class Instances { - /// - /// The instance manager. - /// - public static class Instances + public static class Data { - public static class Data + public static int MedicineUsedTimes { get; set; } + + public static int ExpiringMedicineUsedTimes { get; set; } + + public static int StoneUsedTimes { get; set; } + + public static bool HasPrintedScreencapWarning { get; set; } + + public static void ClearCache() { - public static int MedicineUsedTimes { get; set; } - - public static int ExpiringMedicineUsedTimes { get; set; } - - public static int StoneUsedTimes { get; set; } - - public static bool HasPrintedScreencapWarning { get; set; } - - public static void ClearCache() - { - FightSettingsUserControlModel.SanityReport = null; - MedicineUsedTimes = 0; - ExpiringMedicineUsedTimes = 0; - StoneUsedTimes = 0; - HasPrintedScreencapWarning = false; - } - } - - public static IWindowManager WindowManager { get; private set; } - - public static TaskQueueViewModel TaskQueueViewModel { get; private set; } - - public static ToolboxViewModel ToolboxViewModel { get; private set; } - - public static SettingsViewModel SettingsViewModel { get; private set; } - - public static CopilotViewModel CopilotViewModel { get; private set; } - - public static VersionUpdateViewModel VersionUpdateViewModel { get; private set; } - - public static AnnouncementViewModel AnnouncementViewModel { get; private set; } - - public static AsstProxy AsstProxy { get; private set; } - - public static StageManager StageManager { get; private set; } - - public static HotKeyManager HotKeyManager { get; private set; } - - public static IMaaHotKeyManager MaaHotKeyManager { get; private set; } - - public static IMaaHotKeyActionHandler MaaHotKeyActionHandler { get; private set; } - - // 别的地方有用到这个吗? - // ReSharper disable once UnusedAutoPropertyAccessor.Global - public static RemoteControlService RemoteControlService { get; private set; } - - public static IMainWindowManager MainWindowManager { get; private set; } - - public static IHttpService HttpService { get; private set; } - - public static IMaaApiService MaaApiService { get; private set; } - - public static void Instantiate(IContainer container) - { - WindowManager = container.Get(); - - // 这两实例化时存在依赖顺序 - HttpService = container.Get(); - MaaApiService = container.Get(); - - VersionUpdateViewModel = container.Get(); - AnnouncementViewModel = container.Get(); - AsstProxy = container.Get(); - TaskQueueViewModel = container.Get(); - ToolboxViewModel = container.Get(); - SettingsViewModel = container.Get(); - CopilotViewModel = container.Get(); - - RemoteControlService = container.Get(); - - StageManager = container.Get(); - HotKeyManager = container.Get(); - MaaHotKeyManager = container.Get(); - MaaHotKeyActionHandler = container.Get(); - } - - public static void InstantiateOnRootViewDisplayed(IContainer container) - { - MainWindowManager = container.Get(); + FightSettingsUserControlModel.SanityReport = null; + MedicineUsedTimes = 0; + ExpiringMedicineUsedTimes = 0; + StoneUsedTimes = 0; + HasPrintedScreencapWarning = false; } } + + public static IWindowManager WindowManager { get; private set; } + + public static TaskQueueViewModel TaskQueueViewModel { get; private set; } + + public static ToolboxViewModel ToolboxViewModel { get; private set; } + + public static SettingsViewModel SettingsViewModel { get; private set; } + + public static CopilotViewModel CopilotViewModel { get; private set; } + + public static VersionUpdateViewModel VersionUpdateViewModel { get; private set; } + + public static AnnouncementViewModel AnnouncementViewModel { get; private set; } + + public static AsstProxy AsstProxy { get; private set; } + + public static StageManager StageManager { get; private set; } + + public static HotKeyManager HotKeyManager { get; private set; } + + public static IMaaHotKeyManager MaaHotKeyManager { get; private set; } + + public static IMaaHotKeyActionHandler MaaHotKeyActionHandler { get; private set; } + + // 别的地方有用到这个吗? + // ReSharper disable once UnusedAutoPropertyAccessor.Global + public static RemoteControlService RemoteControlService { get; private set; } + + public static IMainWindowManager MainWindowManager { get; private set; } + + public static IHttpService HttpService { get; private set; } + + public static IMaaApiService MaaApiService { get; private set; } + + public static void Instantiate(IContainer container) + { + WindowManager = container.Get(); + + // 这两实例化时存在依赖顺序 + HttpService = container.Get(); + MaaApiService = container.Get(); + + VersionUpdateViewModel = container.Get(); + AnnouncementViewModel = container.Get(); + AsstProxy = container.Get(); + TaskQueueViewModel = container.Get(); + ToolboxViewModel = container.Get(); + SettingsViewModel = container.Get(); + CopilotViewModel = container.Get(); + + RemoteControlService = container.Get(); + + StageManager = container.Get(); + HotKeyManager = container.Get(); + MaaHotKeyManager = container.Get(); + MaaHotKeyActionHandler = container.Get(); + } + + public static void InstantiateOnRootViewDisplayed(IContainer container) + { + MainWindowManager = container.Get(); + } } diff --git a/src/MaaWpfGui/Helper/ItemListHelper.cs b/src/MaaWpfGui/Helper/ItemListHelper.cs index 9bd234d9a5..9474428300 100644 --- a/src/MaaWpfGui/Helper/ItemListHelper.cs +++ b/src/MaaWpfGui/Helper/ItemListHelper.cs @@ -21,145 +21,143 @@ using System.Text.Json; using System.Windows.Media; using System.Windows.Media.Imaging; using MaaWpfGui.Constants; -using MaaWpfGui.Main; using MaaWpfGui.Models; using Serilog; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +public static class ItemListHelper { - public static class ItemListHelper + public static Dictionary ArkItems { get; } + + private static readonly ILogger _logger = Log.ForContext("SourceContext", "ItemListHelper"); + + static ItemListHelper() { - public static Dictionary ArkItems { get; } - - private static readonly ILogger _logger = Log.ForContext("SourceContext", "ItemListHelper"); - - static ItemListHelper() + var language = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Localization, LocalizationHelper.DefaultLanguage); + string filename = string.Empty; + switch (language) { - var language = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Localization, LocalizationHelper.DefaultLanguage); - string filename = string.Empty; - switch (language) - { - case "zh-cn": - filename = Path.Combine(PathsHelper.ResourceDir, "item_index.json"); - break; + case "zh-cn": + filename = Path.Combine(PathsHelper.ResourceDir, "item_index.json"); + break; - case "pallas": - break; + case "pallas": + break; - default: - filename = Path.Combine(PathsHelper.ResourceDir, "global", DataHelper.ClientDirectoryMapper[language], "resource", "item_index.json"); - break; - } - - var tempItems = new Dictionary(); - - if (File.Exists(filename) is false) - { - ArkItems = tempItems; - _logger.Warning("Item list file not found: {Filename}", filename); - return; - } - - try - { - _logger.Information("Loading item list from {filename}", filename); - var jsonStr = File.ReadAllText(filename); - tempItems = JsonSerializer.Deserialize>(jsonStr); - } - catch (Exception e) - { - _logger.Error(e, "Failed to load item list from {filename}", filename); - } - - ArkItems = tempItems ?? new Dictionary(); + default: + filename = Path.Combine(PathsHelper.ResourceDir, "global", DataHelper.ClientDirectoryMapper[language], "resource", "item_index.json"); + break; } - /// - /// 获取当前语言下的物品名称 / Get the name of the item in the current language - /// - /// 物品 id / Item id - /// 物品名称 - public static string GetItemName(string itemId) + var tempItems = new Dictionary(); + + if (File.Exists(filename) is false) { - return ArkItems.TryGetValue(itemId, out var item) - ? item.Name - : itemId; + ArkItems = tempItems; + _logger.Warning("Item list file not found: {Filename}", filename); + return; } - private static readonly ConcurrentDictionary _imageCache = new(); - - /// - /// 获取对应物品的图标 / Get the icon of the corresponding item - /// - /// 物品 id / Item id - /// 物品图片 - public static BitmapSource? GetItemImage(string itemId) + try { - if (_imageCache.TryGetValue(itemId, out var cachedImage)) - { - return cachedImage; - } - - var imagePath = Path.Combine(PathsHelper.ResourceDir, $"template/items/{itemId}.png"); - if (!File.Exists(imagePath)) - { - _imageCache.TryAdd(itemId, null); - return null; - } - - try - { - var processedImage = ProcessBlackToTransparent(imagePath); - _imageCache.TryAdd(itemId, processedImage); - return processedImage; - } - catch - { - _imageCache.TryAdd(itemId, null); - return null; - } + _logger.Information("Loading item list from {filename}", filename); + var jsonStr = File.ReadAllText(filename); + tempItems = JsonSerializer.Deserialize>(jsonStr); + } + catch (Exception e) + { + _logger.Error(e, "Failed to load item list from {filename}", filename); } - private static BitmapSource ProcessBlackToTransparent(string imagePath) + ArkItems = tempItems ?? new Dictionary(); + } + + /// + /// 获取当前语言下的物品名称 / Get the name of the item in the current language + /// + /// 物品 id / Item id + /// 物品名称 + public static string GetItemName(string itemId) + { + return ArkItems.TryGetValue(itemId, out var item) + ? item.Name + : itemId; + } + + private static readonly ConcurrentDictionary _imageCache = new(); + + /// + /// 获取对应物品的图标 / Get the icon of the corresponding item + /// + /// 物品 id / Item id + /// 物品图片 + public static BitmapSource? GetItemImage(string itemId) + { + if (_imageCache.TryGetValue(itemId, out var cachedImage)) { - var original = new BitmapImage(); - original.BeginInit(); - original.CacheOption = BitmapCacheOption.OnLoad; - original.UriSource = new(imagePath, UriKind.RelativeOrAbsolute); - original.EndInit(); + return cachedImage; + } - var convertedBitmap = new FormatConvertedBitmap(original, PixelFormats.Bgra32, null, 0); - var writeable = new WriteableBitmap(convertedBitmap); + var imagePath = Path.Combine(PathsHelper.ResourceDir, $"template/items/{itemId}.png"); + if (!File.Exists(imagePath)) + { + _imageCache.TryAdd(itemId, null); + return null; + } - writeable.Lock(); - try - { - unsafe - { - var pixels = (uint*)writeable.BackBuffer; - int pixelCount = writeable.PixelWidth * writeable.PixelHeight; - - for (int i = 0; i < pixelCount; i++) - { - uint pixel = pixels[i]; - uint bgr = pixel & 0x00FFFFFF; - - if (bgr == 0) - { - pixels[i] = 0x00000000; - } - } - } - - writeable.AddDirtyRect(new(0, 0, writeable.PixelWidth, writeable.PixelHeight)); - } - finally - { - writeable.Unlock(); - } - - writeable.Freeze(); - return writeable; + try + { + var processedImage = ProcessBlackToTransparent(imagePath); + _imageCache.TryAdd(itemId, processedImage); + return processedImage; + } + catch + { + _imageCache.TryAdd(itemId, null); + return null; } } + + private static BitmapSource ProcessBlackToTransparent(string imagePath) + { + var original = new BitmapImage(); + original.BeginInit(); + original.CacheOption = BitmapCacheOption.OnLoad; + original.UriSource = new(imagePath, UriKind.RelativeOrAbsolute); + original.EndInit(); + + var convertedBitmap = new FormatConvertedBitmap(original, PixelFormats.Bgra32, null, 0); + var writeable = new WriteableBitmap(convertedBitmap); + + writeable.Lock(); + try + { + unsafe + { + var pixels = (uint*)writeable.BackBuffer; + int pixelCount = writeable.PixelWidth * writeable.PixelHeight; + + for (int i = 0; i < pixelCount; i++) + { + uint pixel = pixels[i]; + uint bgr = pixel & 0x00FFFFFF; + + if (bgr == 0) + { + pixels[i] = 0x00000000; + } + } + } + + writeable.AddDirtyRect(new(0, 0, writeable.PixelWidth, writeable.PixelHeight)); + } + finally + { + writeable.Unlock(); + } + + writeable.Freeze(); + return writeable; + } } diff --git a/src/MaaWpfGui/Helper/JsonDataHelper.cs b/src/MaaWpfGui/Helper/JsonDataHelper.cs index 31f64fb614..9a7c16e1f1 100644 --- a/src/MaaWpfGui/Helper/JsonDataHelper.cs +++ b/src/MaaWpfGui/Helper/JsonDataHelper.cs @@ -18,111 +18,110 @@ using System.IO; using Newtonsoft.Json; using Serilog; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +public static class JsonDataHelper { - public static class JsonDataHelper + private static readonly object _lock = new(); + private static readonly ILogger _logger = Log.ForContext("SourceContext", "JsonDataHelper"); + + /// + /// 从 data/{key}.json 读取对象,如果不存在则返回 defaultValue + /// + /// 要获取的数据类型 + /// 文件名 + /// 数据类型 + /// 目标文件夹 + /// 反序列化数据 + public static T? Get(string key, T? defaultValue = default, string? dataDir = null) { - private static readonly object _lock = new(); - private static readonly ILogger _logger = Log.ForContext("SourceContext", "JsonDataHelper"); + var filePath = Path.Combine(dataDir ?? PathsHelper.DataDir, $"{key}.json"); - /// - /// 从 data/{key}.json 读取对象,如果不存在则返回 defaultValue - /// - /// 要获取的数据类型 - /// 文件名 - /// 数据类型 - /// 目标文件夹 - /// 反序列化数据 - public static T? Get(string key, T? defaultValue = default, string? dataDir = null) + if (!File.Exists(filePath)) { - var filePath = Path.Combine(dataDir ?? PathsHelper.DataDir, $"{key}.json"); - - if (!File.Exists(filePath)) - { - return defaultValue; - } - - try - { - var json = File.ReadAllText(filePath); - if (typeof(T) == typeof(string)) - { - return (T)(object)json; - } - - return JsonConvert.DeserializeObject(json); - } - catch (Exception ex) - { - _logger.Error(ex, $"Failed to load data file for key {key}"); - return defaultValue; - } + return defaultValue; } - /// - /// 将对象写入 data/{key}.json - /// - /// 要写入的数据类型 - /// 文件名 - /// 数据 - /// 目标文件夹 - /// 是否设置成功 - public static bool Set(string key, T value, string? dataDir = null) + try { - var filePath = Path.Combine(dataDir ?? PathsHelper.DataDir, $"{key}.json"); - - lock (_lock) + var json = File.ReadAllText(filePath); + if (typeof(T) == typeof(string)) { - try - { - Directory.CreateDirectory(PathsHelper.DataDir); - var json = JsonConvert.SerializeObject(value, Formatting.Indented); - File.WriteAllText(filePath, json); - return true; - } - catch (Exception ex) - { - _logger.Error(ex, $"Failed to save data file for key {key}"); - return false; - } + return (T)(object)json; } + + return JsonConvert.DeserializeObject(json); } - - /// - /// 删除 data/{key}.json - /// - /// 文件名 - /// 是否成功删除 - public static bool Delete(string key) + catch (Exception ex) { - var filePath = Path.Combine(PathsHelper.DataDir, $"{key}.json"); + _logger.Error(ex, $"Failed to load data file for key {key}"); + return defaultValue; + } + } + /// + /// 将对象写入 data/{key}.json + /// + /// 要写入的数据类型 + /// 文件名 + /// 数据 + /// 目标文件夹 + /// 是否设置成功 + public static bool Set(string key, T value, string? dataDir = null) + { + var filePath = Path.Combine(dataDir ?? PathsHelper.DataDir, $"{key}.json"); + + lock (_lock) + { try { - if (!File.Exists(filePath)) - { - return false; - } - - File.Delete(filePath); + Directory.CreateDirectory(PathsHelper.DataDir); + var json = JsonConvert.SerializeObject(value, Formatting.Indented); + File.WriteAllText(filePath, json); return true; } catch (Exception ex) { - _logger.Error(ex, $"Failed to delete data file for key {key}"); + _logger.Error(ex, $"Failed to save data file for key {key}"); return false; } } + } - /// - /// 判断 data/{key}.json 是否存在 - /// - /// 文件名 - /// 是否存在 - public static bool Exists(string key) + /// + /// 删除 data/{key}.json + /// + /// 文件名 + /// 是否成功删除 + public static bool Delete(string key) + { + var filePath = Path.Combine(PathsHelper.DataDir, $"{key}.json"); + + try { - var filePath = Path.Combine(PathsHelper.DataDir, $"{key}.json"); - return File.Exists(filePath); + if (!File.Exists(filePath)) + { + return false; + } + + File.Delete(filePath); + return true; + } + catch (Exception ex) + { + _logger.Error(ex, $"Failed to delete data file for key {key}"); + return false; } } + + /// + /// 判断 data/{key}.json 是否存在 + /// + /// 文件名 + /// 是否存在 + public static bool Exists(string key) + { + var filePath = Path.Combine(PathsHelper.DataDir, $"{key}.json"); + return File.Exists(filePath); + } } diff --git a/src/MaaWpfGui/Helper/LocalizationHelper.cs b/src/MaaWpfGui/Helper/LocalizationHelper.cs index 124afca8c0..4c1313c462 100644 --- a/src/MaaWpfGui/Helper/LocalizationHelper.cs +++ b/src/MaaWpfGui/Helper/LocalizationHelper.cs @@ -23,256 +23,255 @@ using System.Windows; using System.Windows.Markup; using MaaWpfGui.Constants; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +/// +/// The localization class. +/// +public static class LocalizationHelper { /// - /// The localization class. + /// The supported languages. /// - public static class LocalizationHelper + public static readonly Dictionary SupportedLanguages = new() { - /// - /// The supported languages. - /// - public static readonly Dictionary SupportedLanguages = new() + { "zh-cn", "简体中文" }, + { "zh-tw", "繁體中文" }, + { "en-us", "English" }, + { "ja-jp", "日本語" }, + { "ko-kr", "한국어" }, + { "pallas", "🍻🍻🍻🍻" }, + }; + + /// + /// Gets the default language. + /// + public static string DefaultLanguage + { + get { - { "zh-cn", "简体中文" }, - { "zh-tw", "繁體中文" }, - { "en-us", "English" }, - { "ja-jp", "日本語" }, - { "ko-kr", "한국어" }, - { "pallas", "🍻🍻🍻🍻" }, - }; - - /// - /// Gets the default language. - /// - public static string DefaultLanguage - { - get + var local = CultureInfo.CurrentCulture.Name.ToLower(); + if (SupportedLanguages.ContainsKey(local)) { - var local = CultureInfo.CurrentCulture.Name.ToLower(); - if (SupportedLanguages.ContainsKey(local)) - { - return local; - } - - foreach (var lang in from lang in SupportedLanguages - let key = lang.Key.Contains('-') ? lang.Key.Split('-')[0] : lang.Key - where local.StartsWith(key) || key.StartsWith(local) - select lang) - { - return lang.Key; - } - - return "en-us"; - } - } - - private static readonly string _culture = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Localization, DefaultLanguage); - - private static readonly string _customCulture = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.CustomCulture, string.Empty); - - public static CultureInfo CustomCultureInfo - { - get - { - if (string.IsNullOrEmpty(_customCulture)) - { - return CultureInfo.CurrentCulture; - } - - return CultureInfo.GetCultures(CultureTypes.AllCultures) - .Any(c => c.Name.Equals(_customCulture, StringComparison.OrdinalIgnoreCase)) - ? new CultureInfo(_customCulture) - : CultureInfo.CurrentCulture; - } - } - - /// - /// Loads localizations. - /// - public static void Load() - { - if (_culture == "pallas") - { - var dictionary = new ResourceDictionary - { - Source = new(@"Res\Localizations\zh-cn.xaml", UriKind.Relative), - }; - foreach (var key in dictionary.Keys) - { - dictionary[key] = GetPallasString(); - } - - Application.Current.Resources.MergedDictionaries.Add(dictionary); - return; + return local; } - string[] cultureList = _culture switch + foreach (var lang in from lang in SupportedLanguages + let key = lang.Key.Contains('-') ? lang.Key.Split('-')[0] : lang.Key + where local.StartsWith(key) || key.StartsWith(local) + select lang) { - "zh-cn" => [_culture], - "zh-tw" => ["zh-cn", _culture], - "en-us" => ["zh-cn", _culture], - _ => ["zh-cn", "en-us", _culture], - }; - - foreach (var cur in cultureList) - { - var dictionary = new ResourceDictionary - { - Source = new($@"Res\Localizations\{cur}.xaml", UriKind.Relative), - }; - _preprocessedCultures.Add(cur); - PreprocessDictionary(dictionary, cur); - Application.Current.Resources.MergedDictionaries.Add(dictionary); - if (cur == _culture) - { - break; - } + return lang.Key; } - try - { - Thread.CurrentThread.CurrentCulture = !string.IsNullOrEmpty(_customCulture) - ? CustomCultureInfo - : CultureInfo.GetCultureInfo(_culture); - FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(Thread.CurrentThread.CurrentCulture.Name))); - } - catch - { - /* ignore */ - } - } - - private static readonly HashSet _preprocessedCultures = []; - - private static void PreprocessDictionary(ResourceDictionary dictionary, string culture) - { - foreach (var keyObj in dictionary.Keys) - { - if (keyObj is not string key) - { - continue; - } - - if (dictionary[key] is not string raw || !raw.Contains("{key=")) - { - continue; - } - - dictionary[key] = GetFormattedString(key, culture); - } - } - - /// - /// Gets a localized string. - /// - /// The key of the string. - /// The language of the string - /// The string. - public static string GetString(string key, string? culture = null) - { - if (_culture == "pallas") - { - return GetPallasString(); - } - - if (!string.IsNullOrEmpty(culture)) - { - var dictionary = new ResourceDictionary - { - Source = new($@"Res\Localizations\{culture}.xaml", UriKind.Relative), - }; - - if (_preprocessedCultures.Add(culture)) - { - PreprocessDictionary(dictionary, culture); - } - - if (dictionary.Contains(key)) - { - return Regex.Unescape(dictionary[key]?.ToString() ?? $"{{{{ {key} }}}}"); - } - } - - var dictList = Application.Current.Resources.MergedDictionaries; - for (int i = dictList.Count - 1; i >= 0; --i) - { - var dict = dictList[i]; - if (dict.Contains(key)) - { - return Regex.Unescape(dict[key]?.ToString() ?? $"{{{{ {key} }}}}"); - } - } - - return $"{{{{ {key} }}}}"; - } - - /// - /// Gets a formatted localized string. - /// - /// The key of the string. - /// The language of the string - /// The formatted string. - private static string GetFormattedString(string key, string? culture = null) - { - return ResolveNestedKeys(key, GetString(key, culture), culture, new()); - } - - private static string ResolveNestedKeys(string currentKey, string input, string? culture, Stack visited) - { - if (visited.Contains(currentKey)) - { - throw new InvalidOperationException($"Circular reference: {string.Join(" -> ", visited.Reverse())} -> {currentKey}"); - } - - visited.Push(currentKey); - - var result = Regex.Replace(input, @"\{key=(\w+)\}", match => - { - var innerKey = match.Groups[1].Value; - var innerValue = GetString(innerKey, culture); - return ResolveNestedKeys(innerKey, innerValue, culture, visited); - }); - - visited.Pop(); - return result; - } - - private static readonly string[] _pallasChars = ["💃", "🕺", "🍷", "🍸", "🍺", "🍻", "🍷", "🍸", "🍺", "🍻"]; - private static readonly Random _pallasRand = new(); - - public static string GetPallasString(int low = 3, int high = 6) - { - int len = _pallasRand.Next(low, high); - StringBuilder cheersBuilder = new StringBuilder(len); - for (int i = 0; i < len; i++) - { - cheersBuilder.Append(_pallasChars[_pallasRand.Next(0, _pallasChars.Length)]); - } - - return cheersBuilder.ToString(); - } - - public static string FormatVersion(string? version, DateTime dateTime) - { - dateTime = dateTime.ToLocalTime(); - return CustomCultureInfo.Name.ToLowerInvariant() switch - { - "zh-cn" => $"{version}{dateTime:#MMdd}", - "zh-tw" => $"{version}{dateTime:#MMdd}", - "en-us" => $"{dateTime:dd/MM} {version}", - _ => $"{dateTime.ToString(CustomCultureInfo.DateTimeFormat.ShortDatePattern.Replace("yyyy", string.Empty).Trim('/', '.'))} {version}", - }; - } - - public static string FormatDateTime(DateTime dateTime) - { - return CustomCultureInfo.Name.ToLowerInvariant() switch - { - "en-us" => dateTime.ToString("yyyy/MM/dd"), - _ => dateTime.ToString(CustomCultureInfo.DateTimeFormat.ShortDatePattern), - }; + return "en-us"; } } + + private static readonly string _culture = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Localization, DefaultLanguage); + + private static readonly string _customCulture = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.CustomCulture, string.Empty); + + public static CultureInfo CustomCultureInfo + { + get + { + if (string.IsNullOrEmpty(_customCulture)) + { + return CultureInfo.CurrentCulture; + } + + return CultureInfo.GetCultures(CultureTypes.AllCultures) + .Any(c => c.Name.Equals(_customCulture, StringComparison.OrdinalIgnoreCase)) + ? new CultureInfo(_customCulture) + : CultureInfo.CurrentCulture; + } + } + + /// + /// Loads localizations. + /// + public static void Load() + { + if (_culture == "pallas") + { + var dictionary = new ResourceDictionary + { + Source = new(@"Res\Localizations\zh-cn.xaml", UriKind.Relative), + }; + foreach (var key in dictionary.Keys) + { + dictionary[key] = GetPallasString(); + } + + Application.Current.Resources.MergedDictionaries.Add(dictionary); + return; + } + + string[] cultureList = _culture switch + { + "zh-cn" => [_culture], + "zh-tw" => ["zh-cn", _culture], + "en-us" => ["zh-cn", _culture], + _ => ["zh-cn", "en-us", _culture], + }; + + foreach (var cur in cultureList) + { + var dictionary = new ResourceDictionary + { + Source = new($@"Res\Localizations\{cur}.xaml", UriKind.Relative), + }; + _preprocessedCultures.Add(cur); + PreprocessDictionary(dictionary, cur); + Application.Current.Resources.MergedDictionaries.Add(dictionary); + if (cur == _culture) + { + break; + } + } + + try + { + Thread.CurrentThread.CurrentCulture = !string.IsNullOrEmpty(_customCulture) + ? CustomCultureInfo + : CultureInfo.GetCultureInfo(_culture); + FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(Thread.CurrentThread.CurrentCulture.Name))); + } + catch + { + /* ignore */ + } + } + + private static readonly HashSet _preprocessedCultures = []; + + private static void PreprocessDictionary(ResourceDictionary dictionary, string culture) + { + foreach (var keyObj in dictionary.Keys) + { + if (keyObj is not string key) + { + continue; + } + + if (dictionary[key] is not string raw || !raw.Contains("{key=")) + { + continue; + } + + dictionary[key] = GetFormattedString(key, culture); + } + } + + /// + /// Gets a localized string. + /// + /// The key of the string. + /// The language of the string + /// The string. + public static string GetString(string key, string? culture = null) + { + if (_culture == "pallas") + { + return GetPallasString(); + } + + if (!string.IsNullOrEmpty(culture)) + { + var dictionary = new ResourceDictionary + { + Source = new($@"Res\Localizations\{culture}.xaml", UriKind.Relative), + }; + + if (_preprocessedCultures.Add(culture)) + { + PreprocessDictionary(dictionary, culture); + } + + if (dictionary.Contains(key)) + { + return Regex.Unescape(dictionary[key]?.ToString() ?? $"{{{{ {key} }}}}"); + } + } + + var dictList = Application.Current.Resources.MergedDictionaries; + for (int i = dictList.Count - 1; i >= 0; --i) + { + var dict = dictList[i]; + if (dict.Contains(key)) + { + return Regex.Unescape(dict[key]?.ToString() ?? $"{{{{ {key} }}}}"); + } + } + + return $"{{{{ {key} }}}}"; + } + + /// + /// Gets a formatted localized string. + /// + /// The key of the string. + /// The language of the string + /// The formatted string. + private static string GetFormattedString(string key, string? culture = null) + { + return ResolveNestedKeys(key, GetString(key, culture), culture, new()); + } + + private static string ResolveNestedKeys(string currentKey, string input, string? culture, Stack visited) + { + if (visited.Contains(currentKey)) + { + throw new InvalidOperationException($"Circular reference: {string.Join(" -> ", visited.Reverse())} -> {currentKey}"); + } + + visited.Push(currentKey); + + var result = Regex.Replace(input, @"\{key=(\w+)\}", match => + { + var innerKey = match.Groups[1].Value; + var innerValue = GetString(innerKey, culture); + return ResolveNestedKeys(innerKey, innerValue, culture, visited); + }); + + visited.Pop(); + return result; + } + + private static readonly string[] _pallasChars = ["💃", "🕺", "🍷", "🍸", "🍺", "🍻", "🍷", "🍸", "🍺", "🍻"]; + private static readonly Random _pallasRand = new(); + + public static string GetPallasString(int low = 3, int high = 6) + { + int len = _pallasRand.Next(low, high); + StringBuilder cheersBuilder = new StringBuilder(len); + for (int i = 0; i < len; i++) + { + cheersBuilder.Append(_pallasChars[_pallasRand.Next(0, _pallasChars.Length)]); + } + + return cheersBuilder.ToString(); + } + + public static string FormatVersion(string? version, DateTime dateTime) + { + dateTime = dateTime.ToLocalTime(); + return CustomCultureInfo.Name.ToLowerInvariant() switch + { + "zh-cn" => $"{version}{dateTime:#MMdd}", + "zh-tw" => $"{version}{dateTime:#MMdd}", + "en-us" => $"{dateTime:dd/MM} {version}", + _ => $"{dateTime.ToString(CustomCultureInfo.DateTimeFormat.ShortDatePattern.Replace("yyyy", string.Empty).Trim('/', '.'))} {version}", + }; + } + + public static string FormatDateTime(DateTime dateTime) + { + return CustomCultureInfo.Name.ToLowerInvariant() switch + { + "en-us" => dateTime.ToString("yyyy/MM/dd"), + _ => dateTime.ToString(CustomCultureInfo.DateTimeFormat.ShortDatePattern), + }; + } } diff --git a/src/MaaWpfGui/Helper/MessageBoxHelper.cs b/src/MaaWpfGui/Helper/MessageBoxHelper.cs index 81721d044c..52eba9c8ea 100644 --- a/src/MaaWpfGui/Helper/MessageBoxHelper.cs +++ b/src/MaaWpfGui/Helper/MessageBoxHelper.cs @@ -16,7 +16,6 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security; using System.Windows; @@ -25,332 +24,330 @@ using MaaWpfGui.Constants; using Windows.Win32; using Windows.Win32.UI.Controls; using Windows.Win32.UI.Shell; -using Windows.Win32.UI.WindowsAndMessaging; [assembly: SecurityCritical] [assembly: SecurityTreatAsSafe] -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +/// +/// 消息框弹窗格式管理 +/// +public static class MessageBoxHelper { /// - /// 消息框弹窗格式管理 + /// OK text /// - public static class MessageBoxHelper + // ReSharper disable once InconsistentNaming + public static string OK = "OK"; + + /// + /// Cancel text + /// + public static string Cancel = "Cancel"; + + /// + /// Abort text + /// + public static string Abort = "Abort"; + + /// + /// Retry text + /// + public static string Retry = "Retry"; + + /// + /// Ignore text + /// + public static string Ignore = "Ignore"; + + /// + /// Yes text + /// + public static string Yes = "Yes"; + + /// + /// No text + /// + public static string No = "No"; + + private static void SetImage(MessageBoxImage messageBoxImage, ref string iconKey, ref string iconBrushKey) { - /// - /// OK text - /// - // ReSharper disable once InconsistentNaming - public static string OK = "OK"; + var key = string.Empty; + var brushKey = string.Empty; - /// - /// Cancel text - /// - public static string Cancel = "Cancel"; - - /// - /// Abort text - /// - public static string Abort = "Abort"; - - /// - /// Retry text - /// - public static string Retry = "Retry"; - - /// - /// Ignore text - /// - public static string Ignore = "Ignore"; - - /// - /// Yes text - /// - public static string Yes = "Yes"; - - /// - /// No text - /// - public static string No = "No"; - - private static void SetImage(MessageBoxImage messageBoxImage, ref string iconKey, ref string iconBrushKey) + switch (messageBoxImage) { - var key = string.Empty; - var brushKey = string.Empty; + case MessageBoxImage.Question: + key = ResourceToken.AskGeometry; + brushKey = ResourceToken.AccentBrush; + break; - switch (messageBoxImage) - { - case MessageBoxImage.Question: - key = ResourceToken.AskGeometry; - brushKey = ResourceToken.AccentBrush; - break; + case MessageBoxImage.Error: + key = ResourceToken.ErrorGeometry; + brushKey = ResourceToken.DangerBrush; + break; - case MessageBoxImage.Error: - key = ResourceToken.ErrorGeometry; - brushKey = ResourceToken.DangerBrush; - break; + case MessageBoxImage.Warning: + key = ResourceToken.WarningGeometry; + brushKey = ResourceToken.WarningBrush; + break; - case MessageBoxImage.Warning: - key = ResourceToken.WarningGeometry; - brushKey = ResourceToken.WarningBrush; - break; - - case MessageBoxImage.Information: - key = ResourceToken.InfoGeometry; - brushKey = ResourceToken.InfoBrush; - break; - } - - iconKey = string.IsNullOrEmpty(iconKey) ? key : iconKey; - iconBrushKey = string.IsNullOrEmpty(iconBrushKey) ? brushKey : iconBrushKey; + case MessageBoxImage.Information: + key = ResourceToken.InfoGeometry; + brushKey = ResourceToken.InfoBrush; + break; } - public static MessageBoxResult Show(MessageBoxInfo info) => HandyControl.Controls.MessageBox.Show(info); + iconKey = string.IsNullOrEmpty(iconKey) ? key : iconKey; + iconBrushKey = string.IsNullOrEmpty(iconBrushKey) ? brushKey : iconBrushKey; + } - public static MessageBoxResult Show( - string messageBoxText, - string caption = "", - MessageBoxButton buttons = MessageBoxButton.OK, - MessageBoxImage icon = MessageBoxImage.None, - string iconKey = "", - string iconBrushKey = "", - string ok = "", - string cancel = "", - string yes = "", - string no = "", - bool useNativeMethod = false) => Show(WindowHandle.None, messageBoxText, caption, buttons, icon, iconKey, iconBrushKey, ok, cancel, yes, no, useNativeMethod); + public static MessageBoxResult Show(MessageBoxInfo info) => HandyControl.Controls.MessageBox.Show(info); - public static MessageBoxResult Show( - WindowHandle ownerWindow, - string messageBoxText, - string caption = "", - MessageBoxButton buttons = MessageBoxButton.OK, - MessageBoxImage icon = MessageBoxImage.None, - string iconKey = "", - string iconBrushKey = "", - string ok = "", - string cancel = "", - string yes = "", - string no = "", - bool useNativeMethod = false) + public static MessageBoxResult Show( + string messageBoxText, + string caption = "", + MessageBoxButton buttons = MessageBoxButton.OK, + MessageBoxImage icon = MessageBoxImage.None, + string iconKey = "", + string iconBrushKey = "", + string ok = "", + string cancel = "", + string yes = "", + string no = "", + bool useNativeMethod = false) => Show(WindowHandle.None, messageBoxText, caption, buttons, icon, iconKey, iconBrushKey, ok, cancel, yes, no, useNativeMethod); + + public static MessageBoxResult Show( + WindowHandle ownerWindow, + string messageBoxText, + string caption = "", + MessageBoxButton buttons = MessageBoxButton.OK, + MessageBoxImage icon = MessageBoxImage.None, + string iconKey = "", + string iconBrushKey = "", + string ok = "", + string cancel = "", + string yes = "", + string no = "", + bool useNativeMethod = false) + { + caption = string.IsNullOrEmpty(caption) ? LocalizationHelper.GetString("Tip") : caption; + ok = string.IsNullOrEmpty(ok) ? LocalizationHelper.GetString("Ok") : ok; + cancel = string.IsNullOrEmpty(cancel) ? LocalizationHelper.GetString("ManualRestart") : cancel; + yes = string.IsNullOrEmpty(yes) ? LocalizationHelper.GetString("Yes") : yes; + no = string.IsNullOrEmpty(no) ? LocalizationHelper.GetString("No") : no; + + if (useNativeMethod) { - caption = string.IsNullOrEmpty(caption) ? LocalizationHelper.GetString("Tip") : caption; - ok = string.IsNullOrEmpty(ok) ? LocalizationHelper.GetString("Ok") : ok; - cancel = string.IsNullOrEmpty(cancel) ? LocalizationHelper.GetString("ManualRestart") : cancel; - yes = string.IsNullOrEmpty(yes) ? LocalizationHelper.GetString("Yes") : yes; - no = string.IsNullOrEmpty(no) ? LocalizationHelper.GetString("No") : no; - - if (useNativeMethod) + return ShowNative(ownerWindow, messageBoxText, null, caption, buttons, icon, MessageBoxResult.None, false, ok, cancel, yes, no); + } + else + { + SetImage(icon, ref iconKey, ref iconBrushKey); + var info = new MessageBoxInfo { - return ShowNative(ownerWindow, messageBoxText, null, caption, buttons, icon, MessageBoxResult.None, false, ok, cancel, yes, no); + Message = messageBoxText, + Caption = caption, + Button = buttons, + IconKey = iconKey, + IconBrushKey = iconBrushKey, + ConfirmContent = ok, + CancelContent = cancel, + YesContent = yes, + NoContent = no, + }; + + DateTime startTime = DateTime.Now; + + var result = HandyControl.Controls.MessageBox.Show(info); + + var duration = DateTime.Now - startTime; + if (duration.TotalSeconds <= 1) + { + AchievementTrackerHelper.Instance.Unlock(AchievementIds.QuickCloser); + } + + return result; + } + } + + private unsafe readonly ref struct DisposablePin + where T : unmanaged + { + private readonly GCHandle _handle; + + public DisposablePin(object obj) + { + _handle = GCHandle.Alloc(obj, GCHandleType.Pinned); + } + + public T* AddrOfPinnedObject => (T*)_handle.AddrOfPinnedObject(); + + public void Dispose() + { + _handle.Free(); + } + } + + public static unsafe MessageBoxResult ShowNative( + WindowHandle ownerWindow, + string messageBoxText, + string mainInstruction = "", + string windowTitle = "", + MessageBoxButton buttons = MessageBoxButton.OK, + MessageBoxImage icon = MessageBoxImage.None, + MessageBoxResult defaultButton = MessageBoxResult.None, + bool alwaysAllowCancel = false, + string ok = "", + string cancel = "", + string yes = "", + string no = "") + { + using var contentPin = new DisposablePin(messageBoxText); + using var instructionPin = new DisposablePin(mainInstruction); + using var titlePin = new DisposablePin(windowTitle); + + var config = new TASKDIALOGCONFIG() + { + pszContent = contentPin.AddrOfPinnedObject, + pszMainInstruction = instructionPin.AddrOfPinnedObject, + pszWindowTitle = titlePin.AddrOfPinnedObject, + hwndParent = (Windows.Win32.Foundation.HWND)ownerWindow.Handle, + nDefaultButton = (int)defaultButton, + dwFlags = TASKDIALOG_FLAGS.TDF_POSITION_RELATIVE_TO_WINDOW | TASKDIALOG_FLAGS.TDF_SIZE_TO_CONTENT, + }; + + if (alwaysAllowCancel) + { + config.dwFlags |= TASKDIALOG_FLAGS.TDF_ALLOW_DIALOG_CANCELLATION; + } + + switch (icon) + { + case MessageBoxImage.Information: + // case MessageBoxImage.Asterisk: + config.Anonymous1.pszMainIcon = PInvoke.TD_INFORMATION_ICON; + break; + case MessageBoxImage.Hand: + // case MessageBoxImage.Stop: + // case MessageBoxImage.Error + config.Anonymous1.pszMainIcon = PInvoke.TD_ERROR_ICON; + break; + case MessageBoxImage.Exclamation: + // case MessageBoxImage.Warning: + config.Anonymous1.pszMainIcon = PInvoke.TD_WARNING_ICON; + break; + case MessageBoxImage.Question: + var iconInfo = new SHSTOCKICONINFO { cbSize = (uint)Marshal.SizeOf() }; + PInvoke.SHGetStockIconInfo(SHSTOCKICONID.SIID_HELP, SHGSI_FLAGS.SHGSI_ICON, ref iconInfo).ThrowOnFailure(); + config.Anonymous1.hMainIcon = iconInfo.hIcon; + config.dwFlags |= TASKDIALOG_FLAGS.TDF_USE_HICON_MAIN; + break; + case MessageBoxImage.None: + break; + default: + throw new ArgumentOutOfRangeException(nameof(icon), icon, null); + } + + bool hasOk = false, hasCancel = false, hasYes = false, hasNo = false; + var customButtons = new List(); + var gcHandles = new List(); + switch (buttons) + { + case MessageBoxButton.OK: + hasOk = true; + break; + case MessageBoxButton.OKCancel: + hasOk = true; + hasCancel = true; + break; + case MessageBoxButton.YesNoCancel: + hasYes = true; + hasNo = true; + hasCancel = true; + break; + case MessageBoxButton.YesNo: + hasYes = true; + hasNo = true; + break; + default: + throw new ArgumentOutOfRangeException(nameof(buttons), buttons, null); + } + + if (hasOk) + { + if (string.IsNullOrEmpty(ok)) + { + config.dwCommonButtons |= TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_OK_BUTTON; } else { - SetImage(icon, ref iconKey, ref iconBrushKey); - var info = new MessageBoxInfo - { - Message = messageBoxText, - Caption = caption, - Button = buttons, - IconKey = iconKey, - IconBrushKey = iconBrushKey, - ConfirmContent = ok, - CancelContent = cancel, - YesContent = yes, - NoContent = no, - }; - - DateTime startTime = DateTime.Now; - - var result = HandyControl.Controls.MessageBox.Show(info); - - var duration = DateTime.Now - startTime; - if (duration.TotalSeconds <= 1) - { - AchievementTrackerHelper.Instance.Unlock(AchievementIds.QuickCloser); - } - - return result; - } - } - - private unsafe readonly ref struct DisposablePin - where T : unmanaged - { - private readonly GCHandle _handle; - - public DisposablePin(object obj) - { - _handle = GCHandle.Alloc(obj, GCHandleType.Pinned); - } - - public T* AddrOfPinnedObject => (T*)_handle.AddrOfPinnedObject(); - - public void Dispose() - { - _handle.Free(); - } - } - - public static unsafe MessageBoxResult ShowNative( - WindowHandle ownerWindow, - string messageBoxText, - string mainInstruction = "", - string windowTitle = "", - MessageBoxButton buttons = MessageBoxButton.OK, - MessageBoxImage icon = MessageBoxImage.None, - MessageBoxResult defaultButton = MessageBoxResult.None, - bool alwaysAllowCancel = false, - string ok = "", - string cancel = "", - string yes = "", - string no = "") - { - using var contentPin = new DisposablePin(messageBoxText); - using var instructionPin = new DisposablePin(mainInstruction); - using var titlePin = new DisposablePin(windowTitle); - - var config = new TASKDIALOGCONFIG() - { - pszContent = contentPin.AddrOfPinnedObject, - pszMainInstruction = instructionPin.AddrOfPinnedObject, - pszWindowTitle = titlePin.AddrOfPinnedObject, - hwndParent = (Windows.Win32.Foundation.HWND)ownerWindow.Handle, - nDefaultButton = (int)defaultButton, - dwFlags = TASKDIALOG_FLAGS.TDF_POSITION_RELATIVE_TO_WINDOW | TASKDIALOG_FLAGS.TDF_SIZE_TO_CONTENT, - }; - - if (alwaysAllowCancel) - { - config.dwFlags |= TASKDIALOG_FLAGS.TDF_ALLOW_DIALOG_CANCELLATION; - } - - switch (icon) - { - case MessageBoxImage.Information: - // case MessageBoxImage.Asterisk: - config.Anonymous1.pszMainIcon = PInvoke.TD_INFORMATION_ICON; - break; - case MessageBoxImage.Hand: - // case MessageBoxImage.Stop: - // case MessageBoxImage.Error - config.Anonymous1.pszMainIcon = PInvoke.TD_ERROR_ICON; - break; - case MessageBoxImage.Exclamation: - // case MessageBoxImage.Warning: - config.Anonymous1.pszMainIcon = PInvoke.TD_WARNING_ICON; - break; - case MessageBoxImage.Question: - var iconInfo = new SHSTOCKICONINFO { cbSize = (uint)Marshal.SizeOf() }; - PInvoke.SHGetStockIconInfo(SHSTOCKICONID.SIID_HELP, SHGSI_FLAGS.SHGSI_ICON, ref iconInfo).ThrowOnFailure(); - config.Anonymous1.hMainIcon = iconInfo.hIcon; - config.dwFlags |= TASKDIALOG_FLAGS.TDF_USE_HICON_MAIN; - break; - case MessageBoxImage.None: - break; - default: - throw new ArgumentOutOfRangeException(nameof(icon), icon, null); - } - - bool hasOk = false, hasCancel = false, hasYes = false, hasNo = false; - var customButtons = new List(); - var gcHandles = new List(); - switch (buttons) - { - case MessageBoxButton.OK: - hasOk = true; - break; - case MessageBoxButton.OKCancel: - hasOk = true; - hasCancel = true; - break; - case MessageBoxButton.YesNoCancel: - hasYes = true; - hasNo = true; - hasCancel = true; - break; - case MessageBoxButton.YesNo: - hasYes = true; - hasNo = true; - break; - default: - throw new ArgumentOutOfRangeException(nameof(buttons), buttons, null); - } - - if (hasOk) - { - if (string.IsNullOrEmpty(ok)) - { - config.dwCommonButtons |= TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_OK_BUTTON; - } - else - { - var gch = GCHandle.Alloc(ok, GCHandleType.Pinned); - gcHandles.Add(gch); - customButtons.Add(new TASKDIALOG_BUTTON { nButtonID = (int)MessageBoxResult.OK, pszButtonText = (char*)gch.AddrOfPinnedObject() }); - } - } - - if (hasCancel) - { - if (string.IsNullOrEmpty(cancel)) - { - config.dwCommonButtons |= TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_CANCEL_BUTTON; - } - else - { - var gch = GCHandle.Alloc(cancel, GCHandleType.Pinned); - gcHandles.Add(gch); - customButtons.Add(new TASKDIALOG_BUTTON { nButtonID = (int)MessageBoxResult.Cancel, pszButtonText = (char*)gch.AddrOfPinnedObject() }); - } - } - - if (hasYes) - { - if (string.IsNullOrEmpty(yes)) - { - config.dwCommonButtons |= TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_YES_BUTTON; - } - else - { - var gch = GCHandle.Alloc(yes, GCHandleType.Pinned); - gcHandles.Add(gch); - customButtons.Add(new TASKDIALOG_BUTTON { nButtonID = (int)MessageBoxResult.Yes, pszButtonText = (char*)gch.AddrOfPinnedObject() }); - } - } - - if (hasNo) - { - if (string.IsNullOrEmpty(no)) - { - config.dwCommonButtons |= TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_NO_BUTTON; - } - else - { - var gch = GCHandle.Alloc(no, GCHandleType.Pinned); - gcHandles.Add(gch); - customButtons.Add(new TASKDIALOG_BUTTON { nButtonID = (int)MessageBoxResult.No, pszButtonText = (char*)gch.AddrOfPinnedObject() }); - } - } - - if (customButtons.Count != 0) - { - var array = customButtons.ToArray(); - var gch = GCHandle.Alloc(array, GCHandleType.Pinned); + var gch = GCHandle.Alloc(ok, GCHandleType.Pinned); gcHandles.Add(gch); - config.pButtons = (TASKDIALOG_BUTTON*)gch.AddrOfPinnedObject(); - config.cButtons = (uint)customButtons.Count; + customButtons.Add(new TASKDIALOG_BUTTON { nButtonID = (int)MessageBoxResult.OK, pszButtonText = (char*)gch.AddrOfPinnedObject() }); } - - int button = 0; - PInvoke.TaskDialogIndirect(config, &button, null, null).ThrowOnFailure(); - - foreach (var h in gcHandles) - { - h.Free(); - } - - return (MessageBoxResult)button; } + + if (hasCancel) + { + if (string.IsNullOrEmpty(cancel)) + { + config.dwCommonButtons |= TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_CANCEL_BUTTON; + } + else + { + var gch = GCHandle.Alloc(cancel, GCHandleType.Pinned); + gcHandles.Add(gch); + customButtons.Add(new TASKDIALOG_BUTTON { nButtonID = (int)MessageBoxResult.Cancel, pszButtonText = (char*)gch.AddrOfPinnedObject() }); + } + } + + if (hasYes) + { + if (string.IsNullOrEmpty(yes)) + { + config.dwCommonButtons |= TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_YES_BUTTON; + } + else + { + var gch = GCHandle.Alloc(yes, GCHandleType.Pinned); + gcHandles.Add(gch); + customButtons.Add(new TASKDIALOG_BUTTON { nButtonID = (int)MessageBoxResult.Yes, pszButtonText = (char*)gch.AddrOfPinnedObject() }); + } + } + + if (hasNo) + { + if (string.IsNullOrEmpty(no)) + { + config.dwCommonButtons |= TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_NO_BUTTON; + } + else + { + var gch = GCHandle.Alloc(no, GCHandleType.Pinned); + gcHandles.Add(gch); + customButtons.Add(new TASKDIALOG_BUTTON { nButtonID = (int)MessageBoxResult.No, pszButtonText = (char*)gch.AddrOfPinnedObject() }); + } + } + + if (customButtons.Count != 0) + { + var array = customButtons.ToArray(); + var gch = GCHandle.Alloc(array, GCHandleType.Pinned); + gcHandles.Add(gch); + config.pButtons = (TASKDIALOG_BUTTON*)gch.AddrOfPinnedObject(); + config.cButtons = (uint)customButtons.Count; + } + + int button = 0; + PInvoke.TaskDialogIndirect(config, &button, null, null).ThrowOnFailure(); + + foreach (var h in gcHandles) + { + h.Free(); + } + + return (MessageBoxResult)button; } } diff --git a/src/MaaWpfGui/Helper/MouseWheelHelper.cs b/src/MaaWpfGui/Helper/MouseWheelHelper.cs index 6919a6e4ef..9f8210907a 100644 --- a/src/MaaWpfGui/Helper/MouseWheelHelper.cs +++ b/src/MaaWpfGui/Helper/MouseWheelHelper.cs @@ -15,32 +15,31 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Input; -namespace MaaWpfGui.Helper -{ - public static class MouseWheelHelper - { - public static void RouteMouseWheelToParent(object sender, MouseWheelEventArgs e) - { - if (e.Handled) - { - return; - } +namespace MaaWpfGui.Helper; - e.Handled = true; - var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta) - { - RoutedEvent = UIElement.MouseWheelEvent, - }; - var parent = ((Control)sender).Parent as UIElement; - parent?.RaiseEvent(eventArg); +public static class MouseWheelHelper +{ + public static void RouteMouseWheelToParent(object sender, MouseWheelEventArgs e) + { + if (e.Handled) + { + return; } - public static void DisableComboBoxScrollWhenClosed(object sender, MouseWheelEventArgs e) + e.Handled = true; + var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta) { - if (sender is ComboBox { IsDropDownOpen: false }) - { - e.Handled = true; // 阻止滚动 - } + RoutedEvent = UIElement.MouseWheelEvent, + }; + var parent = ((Control)sender).Parent as UIElement; + parent?.RaiseEvent(eventArg); + } + + public static void DisableComboBoxScrollWhenClosed(object sender, MouseWheelEventArgs e) + { + if (sender is ComboBox { IsDropDownOpen: false }) + { + e.Handled = true; // 阻止滚动 } } } diff --git a/src/MaaWpfGui/Helper/ThemeHelper.cs b/src/MaaWpfGui/Helper/ThemeHelper.cs index ec7cddf36e..4f124532d4 100644 --- a/src/MaaWpfGui/Helper/ThemeHelper.cs +++ b/src/MaaWpfGui/Helper/ThemeHelper.cs @@ -20,140 +20,139 @@ using MaaWpfGui.Constants; using MaaWpfGui.WineCompat; using Microsoft.Win32; -namespace MaaWpfGui.Helper -{ - public static class ThemeHelper - { - #region Swith Theme +namespace MaaWpfGui.Helper; - private static void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e) +public static class ThemeHelper +{ + #region Swith Theme + + private static void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e) + { + ThemeManager.Current.ApplicationTheme = ThemeManager.GetSystemTheme(isSystemTheme: false); + + if (!WineRuntimeInformation.IsRunningUnderWine) + { + ThemeManager.Current.AccentColor = ThemeManager.Current.GetAccentColorFromSystem(); + } + + Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor; + } + + public static void SwitchToLightMode() + { + SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged; + ThemeManager.Current.UsingWindowsAppTheme = false; + ThemeManager.Current.ApplicationTheme = ApplicationTheme.Light; + Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor; + } + + public static void SwitchToDarkMode() + { + SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged; + ThemeManager.Current.UsingWindowsAppTheme = false; + ThemeManager.Current.ApplicationTheme = ApplicationTheme.Dark; + Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor; + } + + public static void SwitchToSyncWithOsMode() + { + if (WineRuntimeInformation.IsRunningUnderWine) { ThemeManager.Current.ApplicationTheme = ThemeManager.GetSystemTheme(isSystemTheme: false); - - if (!WineRuntimeInformation.IsRunningUnderWine) - { - ThemeManager.Current.AccentColor = ThemeManager.Current.GetAccentColorFromSystem(); - } - - Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor; + } + else + { + ThemeManager.Current.UsingWindowsAppTheme = true; + SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged; } - public static void SwitchToLightMode() + Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor; + } + + #endregion Swith Theme + + #region Check UiLogColor + + // In official version, using ResourceHelper.GetSkin + private static readonly Color _lightBackground = ((SolidColorBrush)ThemeResources.Current.GetThemeDictionary("Light")["RegionBrush"]).Color; + + private static readonly Color _darkBackground = ((SolidColorBrush)ThemeResources.Current.GetThemeDictionary("Dark")["RegionBrush"]).Color; + + public static bool SimilarToBackground(Color color) + { + return ColorDistance(color, _lightBackground) == 0 || ColorDistance(color, _darkBackground) == 0; + } + + /// + /// Gets the distance between colors c1 and c2. + /// + /// The color c1. + /// The color c2. + /// The distance from 0 to 35. + public static int ColorDistance(Color c1, Color c2) + { + // https://www.compuphase.com/cmetric.htm + long rmean = (c1.R + c2.R) / 2; + long r = c1.R - c2.R; + long g = c1.G - c2.G; + long b = c1.B - c2.B; + return (int)(((((512 + rmean) * r * r) >> 8) + (4 * g * g) + (((767 - rmean) * b * b) >> 8)) >> 14); + } + + #endregion Check UiLogColor + + #region Convert + + public const string DefaultKey = UiLogColor.Message; + + public static SolidColorBrush DefaultBrush => ResourceHelper.GetResource(DefaultKey); + + public static Color DefaultColor => DefaultBrush.Color; + + public static string DefaultHexString => $"#{DefaultColor.A:X2}{DefaultColor.R:X2}{DefaultColor.G:X2}{DefaultColor.B:X2}"; + + public static string Color2HexString(Color color, bool keepAlpha = false) + { + return keepAlpha + ? $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}" + : $"#FF{color.R:X2}{color.G:X2}{color.B:X2}"; + } + + [UsedImplicitly] + public static string Brush2HexString(SolidColorBrush brush, bool keepAlpha = false) + { + return brush != null + ? Color2HexString(brush.Color, keepAlpha) + : DefaultHexString; + } + + public static Color String2Color(string str) + { + if (string.IsNullOrWhiteSpace(str)) { - SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged; - ThemeManager.Current.UsingWindowsAppTheme = false; - ThemeManager.Current.ApplicationTheme = ApplicationTheme.Light; - Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor; - } - - public static void SwitchToDarkMode() - { - SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged; - ThemeManager.Current.UsingWindowsAppTheme = false; - ThemeManager.Current.ApplicationTheme = ApplicationTheme.Dark; - Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor; - } - - public static void SwitchToSyncWithOsMode() - { - if (WineRuntimeInformation.IsRunningUnderWine) - { - ThemeManager.Current.ApplicationTheme = ThemeManager.GetSystemTheme(isSystemTheme: false); - } - else - { - ThemeManager.Current.UsingWindowsAppTheme = true; - SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged; - } - - Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor; - } - - #endregion Swith Theme - - #region Check UiLogColor - - // In official version, using ResourceHelper.GetSkin - private static readonly Color _lightBackground = ((SolidColorBrush)ThemeResources.Current.GetThemeDictionary("Light")["RegionBrush"]).Color; - - private static readonly Color _darkBackground = ((SolidColorBrush)ThemeResources.Current.GetThemeDictionary("Dark")["RegionBrush"]).Color; - - public static bool SimilarToBackground(Color color) - { - return ColorDistance(color, _lightBackground) == 0 || ColorDistance(color, _darkBackground) == 0; - } - - /// - /// Gets the distance between colors c1 and c2. - /// - /// The color c1. - /// The color c2. - /// The distance from 0 to 35. - public static int ColorDistance(Color c1, Color c2) - { - // https://www.compuphase.com/cmetric.htm - long rmean = (c1.R + c2.R) / 2; - long r = c1.R - c2.R; - long g = c1.G - c2.G; - long b = c1.B - c2.B; - return (int)(((((512 + rmean) * r * r) >> 8) + (4 * g * g) + (((767 - rmean) * b * b) >> 8)) >> 14); - } - - #endregion Check UiLogColor - - #region Convert - - public const string DefaultKey = UiLogColor.Message; - - public static SolidColorBrush DefaultBrush => ResourceHelper.GetResource(DefaultKey); - - public static Color DefaultColor => DefaultBrush.Color; - - public static string DefaultHexString => $"#{DefaultColor.A:X2}{DefaultColor.R:X2}{DefaultColor.G:X2}{DefaultColor.B:X2}"; - - public static string Color2HexString(Color color, bool keepAlpha = false) - { - return keepAlpha - ? $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}" - : $"#FF{color.R:X2}{color.G:X2}{color.B:X2}"; - } - - [UsedImplicitly] - public static string Brush2HexString(SolidColorBrush brush, bool keepAlpha = false) - { - return brush != null - ? Color2HexString(brush.Color, keepAlpha) - : DefaultHexString; - } - - public static Color String2Color(string str) - { - if (string.IsNullOrWhiteSpace(str)) - { - return DefaultColor; - } - - try - { - object obj = ColorConverter.ConvertFromString(str); - if (obj is Color color) - { - return color; - } - } - catch - { - return DefaultColor; - } - return DefaultColor; } - public static SolidColorBrush String2Brush(string str) + try { - return new SolidColorBrush(String2Color(str)); + object obj = ColorConverter.ConvertFromString(str); + if (obj is Color color) + { + return color; + } + } + catch + { + return DefaultColor; } - #endregion Convert + return DefaultColor; } + + public static SolidColorBrush String2Brush(string str) + { + return new SolidColorBrush(String2Color(str)); + } + + #endregion Convert } diff --git a/src/MaaWpfGui/Helper/ToastNotification.cs b/src/MaaWpfGui/Helper/ToastNotification.cs index 8a47519722..298ef145fb 100644 --- a/src/MaaWpfGui/Helper/ToastNotification.cs +++ b/src/MaaWpfGui/Helper/ToastNotification.cs @@ -31,414 +31,413 @@ using Stylet; using Windows.Win32; using Windows.Win32.UI.WindowsAndMessaging; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +/// +/// The toast notification. +/// +public class ToastNotification : IDisposable { - /// - /// The toast notification. - /// - public class ToastNotification : IDisposable + private static readonly INotificationPoster _notificationPoster; + + private static readonly string _openUrlPrefix; + + static ToastNotification() { - private static readonly INotificationPoster _notificationPoster; + _notificationPoster = GetNotificationPoster(); + _notificationPoster.ActionActivated += OnActionActivated; + _openUrlPrefix = $"OpenUrl:{_notificationPoster.GetHashCode()}:"; + } - private static readonly string _openUrlPrefix; + private static readonly ILogger _logger = Log.ForContext(); - static ToastNotification() + /// + /// Checks toast system. + /// + /// The toast system is initialized. + private static INotificationPoster GetNotificationPoster() + { + if (WineRuntimeInformation.IsRunningUnderWine) { - _notificationPoster = GetNotificationPoster(); - _notificationPoster.ActionActivated += OnActionActivated; - _openUrlPrefix = $"OpenUrl:{_notificationPoster.GetHashCode()}:"; - } - - private static readonly ILogger _logger = Log.ForContext(); - - /// - /// Checks toast system. - /// - /// The toast system is initialized. - private static INotificationPoster GetNotificationPoster() - { - if (WineRuntimeInformation.IsRunningUnderWine) + if (NotificationImplLibNotify.IsSupported) { - if (NotificationImplLibNotify.IsSupported) - { - return new NotificationImplLibNotify(); - } - } - else if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 10240)) - { - return new NotificationImplWinRT(); - } - - return new NotificationImplWpf(); - } - - /// - /// 按钮激活后的事件,参数为按钮标签 - /// - public static event EventHandler ActionActivated; - - /// - /// 通知标题 - /// - private readonly string _notificationTitle = string.Empty; - - /// - /// 通知文本内容 - /// - private readonly StringBuilder _contentCollection = new(); - - /// - /// Initializes a new instance of the class. - /// - /// 通知标题 - /// - /// Toast通知,基于 实现,方便管理通知样式。 - /// 建议使用 调用,如果不使用 调用,建议手动调用 方法释放。 - /// 在线程中必须使用 Dispatcher.Invoke 相关方法调用。 - /// - public ToastNotification(string title = null) - { - // 初始化通知标题 - _notificationTitle = title ?? _notificationTitle; - } - - private static void OnActionActivated(object sender, string tag) - { - string decodedTag = WebUtility.UrlDecode(tag); - - if (decodedTag.StartsWith(_openUrlPrefix)) - { - var url = decodedTag[_openUrlPrefix.Length..]; - Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); - } - else - { - ActionActivated?.Invoke(sender, decodedTag); + return new NotificationImplLibNotify(); } } - - /// - /// 添加一行文本内容 - /// - /// 文本内容 - /// 返回本类,可继续调用其它方法 - public ToastNotification AppendContentText(string text = null) + else if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 10240)) { - _contentCollection.AppendLine(text ?? string.Empty); - return this; + return new NotificationImplWinRT(); } - #region 通知提示音 + return new NotificationImplWpf(); + } - #region 通知提示音列表枚举 + /// + /// 按钮激活后的事件,参数为按钮标签 + /// + public static event EventHandler ActionActivated; - /// - /// 通知提示音列表枚举, - /// - public enum NotificationSounds + /// + /// 通知标题 + /// + private readonly string _notificationTitle = string.Empty; + + /// + /// 通知文本内容 + /// + private readonly StringBuilder _contentCollection = new(); + + /// + /// Initializes a new instance of the class. + /// + /// 通知标题 + /// + /// Toast通知,基于 实现,方便管理通知样式。 + /// 建议使用 调用,如果不使用 调用,建议手动调用 方法释放。 + /// 在线程中必须使用 Dispatcher.Invoke 相关方法调用。 + /// + public ToastNotification(string title = null) + { + // 初始化通知标题 + _notificationTitle = title ?? _notificationTitle; + } + + private static void OnActionActivated(object sender, string tag) + { + string decodedTag = WebUtility.UrlDecode(tag); + + if (decodedTag.StartsWith(_openUrlPrefix)) { - /// - /// 默认响声。 - /// - [Description("默认响声")] - Beep, - - /// - /// 感叹号。 - /// - [Description("感叹号")] - Exclamation, - - /// - /// 星号。 - /// - [Description("星号")] - Asterisk, - - /// - /// 问题。 - /// - [Description("问题")] - Question, - - /// - /// 关键性停止。 - /// - [Description("关键性停止")] - Hand, - - /// - /// 通知 (Windows 10 及以上特有,低版本系统直接用也可以)。 - /// - [Description("通知 (Windows 10 及以上特有,低版本系统直接用也可以)")] - Notification, - - /// - /// 无声。 - /// - [Description("无声")] - None, + var url = decodedTag[_openUrlPrefix.Length..]; + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); } + else + { + ActionActivated?.Invoke(sender, decodedTag); + } + } - #endregion 通知提示音列表枚举 + /// + /// 添加一行文本内容 + /// + /// 文本内容 + /// 返回本类,可继续调用其它方法 + public ToastNotification AppendContentText(string text = null) + { + _contentCollection.AppendLine(text ?? string.Empty); + return this; + } + + #region 通知提示音 + + #region 通知提示音列表枚举 + + /// + /// 通知提示音列表枚举, + /// + public enum NotificationSounds + { + /// + /// 默认响声。 + /// + [Description("默认响声")] + Beep, /// - /// 播放通知提示音 - /// 如果要播放音频文件,参考微软文档 类 + /// 感叹号。 /// - /// 提示音类型 - protected void PlayNotificationSound(NotificationSounds sounds = NotificationSounds.None) + [Description("感叹号")] + Exclamation, + + /// + /// 星号。 + /// + [Description("星号")] + Asterisk, + + /// + /// 问题。 + /// + [Description("问题")] + Question, + + /// + /// 关键性停止。 + /// + [Description("关键性停止")] + Hand, + + /// + /// 通知 (Windows 10 及以上特有,低版本系统直接用也可以)。 + /// + [Description("通知 (Windows 10 及以上特有,低版本系统直接用也可以)")] + Notification, + + /// + /// 无声。 + /// + [Description("无声")] + None, + } + + #endregion 通知提示音列表枚举 + + /// + /// 播放通知提示音 + /// 如果要播放音频文件,参考微软文档 类 + /// + /// 提示音类型 + protected void PlayNotificationSound(NotificationSounds sounds = NotificationSounds.None) + { + try { - try + switch (sounds) { - switch (sounds) - { - case NotificationSounds.Exclamation: SystemSounds.Exclamation.Play(); break; - case NotificationSounds.Asterisk: SystemSounds.Asterisk.Play(); break; - case NotificationSounds.Question: SystemSounds.Question.Play(); break; - case NotificationSounds.Beep: SystemSounds.Beep.Play(); break; - case NotificationSounds.Hand: SystemSounds.Hand.Play(); break; + case NotificationSounds.Exclamation: SystemSounds.Exclamation.Play(); break; + case NotificationSounds.Asterisk: SystemSounds.Asterisk.Play(); break; + case NotificationSounds.Question: SystemSounds.Question.Play(); break; + case NotificationSounds.Beep: SystemSounds.Beep.Play(); break; + case NotificationSounds.Hand: SystemSounds.Hand.Play(); break; - case NotificationSounds.Notification: - using (var key = Registry.CurrentUser.OpenSubKey(@"AppEvents\Schemes\Apps\.Default\Notification.Default\.Current")) + case NotificationSounds.Notification: + using (var key = Registry.CurrentUser.OpenSubKey(@"AppEvents\Schemes\Apps\.Default\Notification.Default\.Current")) + { + if (key != null) { - if (key != null) + // 获取 (Default) 项 + var o = key.GetValue(null); + if (o != null) { - // 获取 (Default) 项 - var o = key.GetValue(null); - if (o != null) - { - var theSound = new SoundPlayer((string)o); - theSound.Play(); - } - } - else - { - // 如果不支持就播放其它提示音 - PlayNotificationSound(NotificationSounds.Asterisk); + var theSound = new SoundPlayer((string)o); + theSound.Play(); } } + else + { + // 如果不支持就播放其它提示音 + PlayNotificationSound(NotificationSounds.Asterisk); + } + } - break; + break; - case NotificationSounds.None: - default: break; - } - } - catch (Exception) - { - // Ignore + case NotificationSounds.None: + default: break; } } - - #endregion 通知提示音 - - #region 通知按钮设置 - - #region 通知按钮变量 - - private readonly List _actions = []; - - #endregion 通知按钮变量 - - /// - /// 给通知添加一个按钮 - /// - /// 按钮标题 - /// 事件标签 - /// 返回类本身,可继续调用其它方法 - /// 按钮按下时,触发 事件 - public ToastNotification AddButton(string label, string tag) + catch (Exception) { - _actions.Add(new NotificationAction(label, tag)); - return this; + // Ignore + } + } + + #endregion 通知提示音 + + #region 通知按钮设置 + + #region 通知按钮变量 + + private readonly List _actions = []; + + #endregion 通知按钮变量 + + /// + /// 给通知添加一个按钮 + /// + /// 按钮标题 + /// 事件标签 + /// 返回类本身,可继续调用其它方法 + /// 按钮按下时,触发 事件 + public ToastNotification AddButton(string label, string tag) + { + _actions.Add(new NotificationAction(label, tag)); + return this; + } + + public static string GetActionTagForOpenWeb(string url) + { + if (url.StartsWith("http://") || url.StartsWith("https://")) + { + return _openUrlPrefix + url; } - public static string GetActionTagForOpenWeb(string url) + throw new ArgumentException("URL must start with http:// or https://"); + } + + #endregion 通知按钮设置 + + #region 通知显示 + #region 显示通知方法 + + /// + /// 显示通知 + /// + /// 通知显示时间 (s) + /// 内容显示行数,如果内容太多建议使用 + /// 播放提示音 + /// 通知额外元数据,可能影响通知展示方式 + /// + public void Show(double lifeTime = 10d, uint row = 1, + NotificationSounds sound = NotificationSounds.Notification, params NotificationHint[] hints) + { + Execute.OnUIThread(() => { - if (url.StartsWith("http://") || url.StartsWith("https://")) + // TODO: 整理过时代码 + if (!ConfigFactory.Root.GUI.UseNotify) { - return _openUrlPrefix + url; + Growl.Info(_notificationTitle + _contentCollection); + return; } - throw new ArgumentException("URL must start with http:// or https://"); - } + var content = new NotificationContent { Summary = _notificationTitle, Body = _contentCollection.ToString(), }; - #endregion 通知按钮设置 - - #region 通知显示 - #region 显示通知方法 - - /// - /// 显示通知 - /// - /// 通知显示时间 (s) - /// 内容显示行数,如果内容太多建议使用 - /// 播放提示音 - /// 通知额外元数据,可能影响通知展示方式 - /// - public void Show(double lifeTime = 10d, uint row = 1, - NotificationSounds sound = NotificationSounds.Notification, params NotificationHint[] hints) - { - Execute.OnUIThread(() => + foreach (var action in _actions) { - // TODO: 整理过时代码 - if (!ConfigFactory.Root.GUI.UseNotify) - { - Growl.Info(_notificationTitle + _contentCollection); - return; - } + content.Actions.Add(action); + } - var content = new NotificationContent { Summary = _notificationTitle, Body = _contentCollection.ToString(), }; + content.Hints.Add(NotificationHint.RowCount((int)row)); - foreach (var action in _actions) - { - content.Actions.Add(action); - } + // 调整显示时间,如果存在按钮的情况下显示时间将强制设为最大时间 + lifeTime = lifeTime < 3d ? 3d : lifeTime; - content.Hints.Add(NotificationHint.RowCount((int)row)); + var timeSpan = _actions.Count != 0 + ? TimeSpan.FromSeconds(lifeTime) + : TimeSpan.MaxValue; - // 调整显示时间,如果存在按钮的情况下显示时间将强制设为最大时间 - lifeTime = lifeTime < 3d ? 3d : lifeTime; + content.Hints.Add(NotificationHint.ExpirationTime(timeSpan)); - var timeSpan = _actions.Count != 0 - ? TimeSpan.FromSeconds(lifeTime) - : TimeSpan.MaxValue; + foreach (var hint in hints) + { + content.Hints.Add(hint); + } - content.Hints.Add(NotificationHint.ExpirationTime(timeSpan)); + // 显示通知 + _notificationPoster.ShowNotification(content); - foreach (var hint in hints) - { - content.Hints.Add(hint); - } + if (_notificationPoster is not NotificationImplWinRT) + { + PlayNotificationSound(sound); // 播放通知提示音 + } - // 显示通知 - _notificationPoster.ShowNotification(content); + // 任务栏闪烁 + FlashWindowEx(); + }); + } - if (_notificationPoster is not NotificationImplWinRT) - { - PlayNotificationSound(sound); // 播放通知提示音 - } + /// + /// 显示内容更多的通知 + /// + /// 通知显示时间 (s) + /// 内容显示行数,只用于预览一部分通知,多出内容会放在附加按钮的窗口中 + /// 播放提示音,不设置就没有声音 + /// 通知提示 + public void ShowMore(double lifeTime = 12d, uint row = 2, + NotificationSounds sound = NotificationSounds.None, params NotificationHint[] hints) + { + var moreHints = new NotificationHint[hints.Length + 1]; + hints.CopyTo(moreHints, 0); + moreHints[hints.Length] = NotificationHint.Expandable; + Show(lifeTime: lifeTime, + row: row, + sound: sound, + hints: moreHints); + } - // 任务栏闪烁 - FlashWindowEx(); - }); - } + /// + /// 显示公招特殊标签通知 + /// + /// 内容显示行数,比如第 2 行用来放星星 + public void ShowRecruit(uint row = 1) + { + Show(row: row, + sound: NotificationSounds.Notification, + hints: NotificationHint.RecruitHighRarity); + } - /// - /// 显示内容更多的通知 - /// - /// 通知显示时间 (s) - /// 内容显示行数,只用于预览一部分通知,多出内容会放在附加按钮的窗口中 - /// 播放提示音,不设置就没有声音 - /// 通知提示 - public void ShowMore(double lifeTime = 12d, uint row = 2, - NotificationSounds sound = NotificationSounds.None, params NotificationHint[] hints) - { - var moreHints = new NotificationHint[hints.Length + 1]; - hints.CopyTo(moreHints, 0); - moreHints[hints.Length] = NotificationHint.Expandable; - Show(lifeTime: lifeTime, - row: row, - sound: sound, - hints: moreHints); - } + /// + /// 显示公招小车标签通知 + /// + /// 内容显示行数,比如第 2 行用来放星星 + public void ShowRecruitRobot(uint row = 1) + { + Show(row: row, + sound: NotificationSounds.Notification, + hints: NotificationHint.RecruitRobot); + } - /// - /// 显示公招特殊标签通知 - /// - /// 内容显示行数,比如第 2 行用来放星星 - public void ShowRecruit(uint row = 1) - { - Show(row: row, - sound: NotificationSounds.Notification, - hints: NotificationHint.RecruitHighRarity); - } - - /// - /// 显示公招小车标签通知 - /// - /// 内容显示行数,比如第 2 行用来放星星 - public void ShowRecruitRobot(uint row = 1) - { - Show(row: row, + /// + /// 显示更新版本的通知 + /// + /// 内容行数 + public void ShowUpdateVersion(uint row = 3) + { + ShowMore(row: row, sound: NotificationSounds.Notification, - hints: NotificationHint.RecruitRobot); - } + hints: NotificationHint.NewVersion); + } - /// - /// 显示更新版本的通知 - /// - /// 内容行数 - public void ShowUpdateVersion(uint row = 3) + /// + /// 直接显示通知内容 + /// + /// 显示内容 + public static void ShowDirect(string message) + { + using var toast = new ToastNotification(message); + toast.Show(); + } + + #endregion 显示通知方法 + + #endregion 通知显示 + + #region 任务栏闪烁 + + /// + /// 闪烁窗口任务栏 + /// + /// 窗口句柄 + /// 闪烁类型 + /// 闪烁次数 + /// 是否成功 + internal static bool FlashWindowEx(IntPtr hWnd = 0, FLASHWINFO_FLAGS type = FLASHWINFO_FLAGS.FLASHW_TRAY | FLASHWINFO_FLAGS.FLASHW_TIMERNOFG, uint count = 5) + { + try { - ShowMore(row: row, - sound: NotificationSounds.Notification, - hints: NotificationHint.NewVersion); + var fInfo = default(FLASHWINFO); + fInfo.cbSize = Convert.ToUInt32(Marshal.SizeOf(fInfo)); + fInfo.hwnd = new(hWnd != 0 ? hWnd : new WindowInteropHelper(System.Windows.Application.Current.MainWindow!).Handle); + fInfo.dwFlags = type; + fInfo.uCount = count; + fInfo.dwTimeout = 0; + return PInvoke.FlashWindowEx(in fInfo); } - - /// - /// 直接显示通知内容 - /// - /// 显示内容 - public static void ShowDirect(string message) + catch { - using var toast = new ToastNotification(message); - toast.Show(); + _logger.Warning("FlashWindowEx error"); + return false; } + } - #endregion 显示通知方法 + #endregion 任务栏闪烁 - #endregion 通知显示 + /// + /// 通知使用完后释放已使用的数据 + /// + public void Dispose() + { + _contentCollection.Clear(); + } - #region 任务栏闪烁 - - /// - /// 闪烁窗口任务栏 - /// - /// 窗口句柄 - /// 闪烁类型 - /// 闪烁次数 - /// 是否成功 - internal static bool FlashWindowEx(IntPtr hWnd = 0, FLASHWINFO_FLAGS type = FLASHWINFO_FLAGS.FLASHW_TRAY | FLASHWINFO_FLAGS.FLASHW_TIMERNOFG, uint count = 5) + public static void Cleanup() + { + try { - try - { - var fInfo = default(FLASHWINFO); - fInfo.cbSize = Convert.ToUInt32(Marshal.SizeOf(fInfo)); - fInfo.hwnd = new(hWnd != 0 ? hWnd : new WindowInteropHelper(System.Windows.Application.Current.MainWindow!).Handle); - fInfo.dwFlags = type; - fInfo.uCount = count; - fInfo.dwTimeout = 0; - return PInvoke.FlashWindowEx(in fInfo); - } - catch - { - _logger.Warning("FlashWindowEx error"); - return false; - } + (_notificationPoster as IDisposable)?.Dispose(); } - - #endregion 任务栏闪烁 - - /// - /// 通知使用完后释放已使用的数据 - /// - public void Dispose() + catch (Exception e) { - _contentCollection.Clear(); - } - - public static void Cleanup() - { - try - { - (_notificationPoster as IDisposable)?.Dispose(); - } - catch (Exception e) - { - Log.ForContext().Error(e, "Cleanup error"); - } + Log.ForContext().Error(e, "Cleanup error"); } } } diff --git a/src/MaaWpfGui/Helper/ToolTipHelper.cs b/src/MaaWpfGui/Helper/ToolTipHelper.cs index ddf605ec4c..a4b054a126 100644 --- a/src/MaaWpfGui/Helper/ToolTipHelper.cs +++ b/src/MaaWpfGui/Helper/ToolTipHelper.cs @@ -22,230 +22,229 @@ using System.Windows.Documents; using System.Windows.Media; using MaaWpfGui.Extensions; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +public static class ToolTipHelper { - public static class ToolTipHelper + private static ToolTip CreateTooltip(this object content, PlacementMode? placementMode = null) { - private static ToolTip CreateTooltip(this object content, PlacementMode? placementMode = null) + var toolTip = new ToolTip { - var toolTip = new ToolTip - { - Content = content, - }; + Content = content, + }; - if (placementMode.HasValue) - { - toolTip.Placement = placementMode.Value; - } + if (placementMode.HasValue) + { + toolTip.Placement = placementMode.Value; + } - if (placementMode == PlacementMode.Center) + if (placementMode == PlacementMode.Center) + { + toolTip.Opened += (_, _) => { - toolTip.Opened += (_, _) => + if (toolTip.PlacementTarget is FrameworkElement target) { - if (toolTip.PlacementTarget is FrameworkElement target) + double offset = -(target.ActualHeight / 2) - (toolTip.ActualHeight / 2); + toolTip.VerticalOffset = offset; + } + else + { + toolTip.VerticalOffset = -60; + } + }; + } + + return toolTip; + } + + /// + /// 通过字符串创建 Tooltip。 + /// + /// 字符串 + /// 位置模式 + /// ToolTip 最大宽度 + /// ToolTip + public static ToolTip CreateTooltip(this string text, PlacementMode? placementMode = null, int maxWidth = 750) + { + var tb = new TextBlock + { + Text = text, + TextWrapping = TextWrapping.Wrap, + Margin = new(4), + MaxWidth = maxWidth, + }; + + return tb.CreateTooltip(placementMode); + } + + public static ToolTip CreateTooltip(this IEnumerable inlines, PlacementMode? placementMode = null, int maxWidth = 750) + { + var tb = new TextBlock + { + TextWrapping = TextWrapping.Wrap, + Margin = new(4), + MaxWidth = maxWidth, + }; + + foreach (var inline in inlines) + { + tb.Inlines.Add(CloneInline(inline)); + } + + return tb.CreateTooltip(placementMode); + + Inline CloneInline(Inline inline) + { + switch (inline) + { + case Run run: + var newRun = new Run(run.Text) { - double offset = -(target.ActualHeight / 2) - (toolTip.ActualHeight / 2); - toolTip.VerticalOffset = offset; + FontWeight = run.FontWeight, + FontStyle = run.FontStyle, + }; + + if (run.Tag is string resourceKey) + { + newRun.SetResourceReference(TextElement.ForegroundProperty, resourceKey); + newRun.Tag = resourceKey; } else { - toolTip.VerticalOffset = -60; + newRun.Foreground = run.Foreground; } - }; - } - return toolTip; - } - - /// - /// 通过字符串创建 Tooltip。 - /// - /// 字符串 - /// 位置模式 - /// ToolTip 最大宽度 - /// ToolTip - public static ToolTip CreateTooltip(this string text, PlacementMode? placementMode = null, int maxWidth = 750) - { - var tb = new TextBlock - { - Text = text, - TextWrapping = TextWrapping.Wrap, - Margin = new(4), - MaxWidth = maxWidth, - }; - - return tb.CreateTooltip(placementMode); - } - - public static ToolTip CreateTooltip(this IEnumerable inlines, PlacementMode? placementMode = null, int maxWidth = 750) - { - var tb = new TextBlock - { - TextWrapping = TextWrapping.Wrap, - Margin = new(4), - MaxWidth = maxWidth, - }; - - foreach (var inline in inlines) - { - tb.Inlines.Add(CloneInline(inline)); - } - - return tb.CreateTooltip(placementMode); - - Inline CloneInline(Inline inline) - { - switch (inline) - { - case Run run: - var newRun = new Run(run.Text) - { - FontWeight = run.FontWeight, - FontStyle = run.FontStyle, - }; - - if (run.Tag is string resourceKey) - { - newRun.SetResourceReference(TextElement.ForegroundProperty, resourceKey); - newRun.Tag = resourceKey; - } - else - { - newRun.Foreground = run.Foreground; - } - - return newRun; - case LineBreak: - return new LineBreak(); - default: - return new Run(); - } + return newRun; + case LineBreak: + return new LineBreak(); + default: + return new Run(); } } + } - #region 创建日志 Tooltip 的辅助方法 + #region 创建日志 Tooltip 的辅助方法 - /// - /// 掉落识别物品的 Tooltip 创建方法。 - /// - /// 掉落物列表 - /// ToolTip - public static ToolTip CreateMaterialDropTooltip(this IEnumerable<(string ItemId, string ItemName, int Total, int Add)> drops) + /// + /// 掉落识别物品的 Tooltip 创建方法。 + /// + /// 掉落物列表 + /// ToolTip + public static ToolTip CreateMaterialDropTooltip(this IEnumerable<(string ItemId, string ItemName, int Total, int Add)> drops) + { + var row = new WrapPanel { - var row = new WrapPanel + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Center, + Margin = new(4), + ItemWidth = 64, + MaxWidth = (64 * 5) + (4 * 10), + }; + + foreach (var (itemId, _, total, add) in drops) + { + var image = new Image { - Orientation = Orientation.Horizontal, + Source = ItemListHelper.GetItemImage(itemId), + Width = 32, + Height = 32, + Margin = new(2), HorizontalAlignment = HorizontalAlignment.Center, - Margin = new(4), - ItemWidth = 64, - MaxWidth = (64 * 5) + (4 * 10), }; - foreach (var (itemId, _, total, add) in drops) + var text = new TextBlock { - var image = new Image - { - Source = ItemListHelper.GetItemImage(itemId), - Width = 32, - Height = 32, - Margin = new(2), - HorizontalAlignment = HorizontalAlignment.Center, - }; + Text = total.FormatNumber(false), + FontSize = 12, + FontWeight = FontWeights.Bold, + TextAlignment = TextAlignment.Center, + TextWrapping = TextWrapping.NoWrap, + Margin = new(2, 0, 2, 2), + HorizontalAlignment = HorizontalAlignment.Center, + MaxWidth = 64, + }; - var text = new TextBlock + var itemStack = new StackPanel + { + Orientation = Orientation.Vertical, + HorizontalAlignment = HorizontalAlignment.Center, + Margin = new(4, 0, 4, 0), + }; + itemStack.Children.Add(image); + itemStack.Children.Add(text); + + if (add > 0) + { + var textAdd = new TextBlock { - Text = total.FormatNumber(false), - FontSize = 12, - FontWeight = FontWeights.Bold, + FontSize = 10, TextAlignment = TextAlignment.Center, TextWrapping = TextWrapping.NoWrap, Margin = new(2, 0, 2, 2), HorizontalAlignment = HorizontalAlignment.Center, MaxWidth = 64, }; - - var itemStack = new StackPanel - { - Orientation = Orientation.Vertical, - HorizontalAlignment = HorizontalAlignment.Center, - Margin = new(4, 0, 4, 0), - }; - itemStack.Children.Add(image); - itemStack.Children.Add(text); - - if (add > 0) - { - var textAdd = new TextBlock - { - FontSize = 10, - TextAlignment = TextAlignment.Center, - TextWrapping = TextWrapping.NoWrap, - Margin = new(2, 0, 2, 2), - HorizontalAlignment = HorizontalAlignment.Center, - MaxWidth = 64, - }; - textAdd.Inlines.Add(new Run($"(+{add.FormatNumber(false)})")); - itemStack.Children.Add(textAdd); - } - - row.Children.Add(itemStack); + textAdd.Inlines.Add(new Run($"(+{add.FormatNumber(false)})")); + itemStack.Children.Add(textAdd); } - return row.CreateTooltip(PlacementMode.Center); + row.Children.Add(itemStack); } - /// - /// 截图测速 Tooltip 创建方法。 - /// - /// 测速列表 - /// ToolTip - public static ToolTip? CreateScreencapTooltip(this IEnumerable<(string Method, string Cost)>? methods) - { - if (methods == null) - { - return null; - } - - var panel = new StackPanel - { - Orientation = Orientation.Vertical, - Margin = new(4), - }; - - foreach (var (method, cost) in methods) - { - var grid = new Grid(); - grid.ColumnDefinitions.Add(new() { Width = new(1, GridUnitType.Star) }); - grid.ColumnDefinitions.Add(new() { Width = new(1, GridUnitType.Auto) }); - - var methodText = new TextBlock - { - Text = method, - FontSize = 12, - Margin = new(2, 1, 6, 1), - HorizontalAlignment = HorizontalAlignment.Left, - }; - Grid.SetColumn(methodText, 0); - - var costText = new TextBlock - { - Text = cost + " ms", - FontSize = 12, - Margin = new(2, 1, 2, 1), - HorizontalAlignment = HorizontalAlignment.Right, - }; - Grid.SetColumn(costText, 1); - - grid.Children.Add(methodText); - grid.Children.Add(costText); - panel.Children.Add(grid); - } - - return panel.Children.Count == 0 - ? null - : panel.CreateTooltip(PlacementMode.Center); - } - - #endregion + return row.CreateTooltip(PlacementMode.Center); } + + /// + /// 截图测速 Tooltip 创建方法。 + /// + /// 测速列表 + /// ToolTip + public static ToolTip? CreateScreencapTooltip(this IEnumerable<(string Method, string Cost)>? methods) + { + if (methods == null) + { + return null; + } + + var panel = new StackPanel + { + Orientation = Orientation.Vertical, + Margin = new(4), + }; + + foreach (var (method, cost) in methods) + { + var grid = new Grid(); + grid.ColumnDefinitions.Add(new() { Width = new(1, GridUnitType.Star) }); + grid.ColumnDefinitions.Add(new() { Width = new(1, GridUnitType.Auto) }); + + var methodText = new TextBlock + { + Text = method, + FontSize = 12, + Margin = new(2, 1, 6, 1), + HorizontalAlignment = HorizontalAlignment.Left, + }; + Grid.SetColumn(methodText, 0); + + var costText = new TextBlock + { + Text = cost + " ms", + FontSize = 12, + Margin = new(2, 1, 2, 1), + HorizontalAlignment = HorizontalAlignment.Right, + }; + Grid.SetColumn(costText, 1); + + grid.Children.Add(methodText); + grid.Children.Add(costText); + panel.Children.Add(grid); + } + + return panel.Children.Count == 0 + ? null + : panel.CreateTooltip(PlacementMode.Center); + } + + #endregion } diff --git a/src/MaaWpfGui/Helper/WinAdapter.cs b/src/MaaWpfGui/Helper/WinAdapter.cs index a7d2b11dca..561bb96015 100644 --- a/src/MaaWpfGui/Helper/WinAdapter.cs +++ b/src/MaaWpfGui/Helper/WinAdapter.cs @@ -18,136 +18,135 @@ using System.IO; using System.Linq; using Serilog; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +/// +/// The emulator adapter. +/// +public class WinAdapter { - /// - /// The emulator adapter. - /// - public class WinAdapter + private static readonly ILogger _logger = Log.ForContext(); + + private static readonly Dictionary _emulatorIdDict = new() { - private static readonly ILogger _logger = Log.ForContext(); + { "HD-Player", "BlueStacks" }, + { "dnplayer", "LDPlayer" }, + { "Nox", "Nox" }, + { "MuMuPlayer", "MuMuEmulator12" }, // MuMu 12 + { "MuMuNxDevice", "MuMuEmulator12" }, // MuMu 12 5.0 + { "MEmu", "XYAZ" }, + }; - private static readonly Dictionary _emulatorIdDict = new() + private static readonly Dictionary> _adbRelativePathDict = new() + { { - { "HD-Player", "BlueStacks" }, - { "dnplayer", "LDPlayer" }, - { "Nox", "Nox" }, - { "MuMuPlayer", "MuMuEmulator12" }, // MuMu 12 - { "MuMuNxDevice", "MuMuEmulator12" }, // MuMu 12 5.0 - { "MEmu", "XYAZ" }, - }; - - private static readonly Dictionary> _adbRelativePathDict = new() + "BlueStacks", [ + @".\HD-Adb.exe", + @".\Engine\ProgramFiles\HD-Adb.exe" + ] + }, + { "LDPlayer", [@".\adb.exe"] }, + { "Nox", [@".\nox_adb.exe"] }, { - { - "BlueStacks", [ - @".\HD-Adb.exe", - @".\Engine\ProgramFiles\HD-Adb.exe" - ] - }, - { "LDPlayer", [@".\adb.exe"] }, - { "Nox", [@".\nox_adb.exe"] }, - { - "MuMuEmulator12", [ - @"..\..\..\nx_main\adb.exe", + "MuMuEmulator12", [ + @"..\..\..\nx_main\adb.exe", - @"..\vmonitor\bin\adb_server.exe", - @"..\..\MuMu\emulator\nemu\vmonitor\bin\adb_server.exe", - @".\adb.exe" - ] - }, - { "XYAZ", [@".\adb.exe"] }, - }; + @"..\vmonitor\bin\adb_server.exe", + @"..\..\MuMu\emulator\nemu\vmonitor\bin\adb_server.exe", + @".\adb.exe" + ] + }, + { "XYAZ", [@".\adb.exe"] }, + }; - private readonly Dictionary _adbAbsolutePathDict = new(); + private readonly Dictionary _adbAbsolutePathDict = new(); - /// - /// Refreshes emulator information. - /// - /// The list of emulators. - public List RefreshEmulatorsInfo() + /// + /// Refreshes emulator information. + /// + /// The list of emulators. + public List RefreshEmulatorsInfo() + { + var allProcess = Process.GetProcesses(); + var emulators = new List(); + foreach (var process in allProcess) { - var allProcess = Process.GetProcesses(); - var emulators = new List(); - foreach (var process in allProcess) + if (!_emulatorIdDict.TryGetValue(process.ProcessName, out var emulatorId)) { - if (!_emulatorIdDict.TryGetValue(process.ProcessName, out var emulatorId)) - { - continue; - } - - emulators.Add(emulatorId); - var processPath = process.MainModule?.FileName; - foreach (string adbPath in _adbRelativePathDict[emulatorId] - .Select(path => Path.GetDirectoryName(processPath) + "\\" + path) - .Where(File.Exists)) - { - _adbAbsolutePathDict[emulatorId] = adbPath; - } + continue; } - return emulators; - } - - /// - /// Gets ADB path by emulator name. - /// - /// The name of the emulator. - /// The ADB path of the emulator. - public string GetAdbPathByEmulatorName(string emulatorName) - { - return _adbAbsolutePathDict.GetValueOrDefault(emulatorName); - } - - /// - /// Gets ADB addresses by an ADB path. - /// - /// The ADB path. - /// The list of ADB addresses. - public static List GetAdbAddresses(string adbPath) - { - string output = ExecuteAdbCommand(adbPath, "devices"); - var lines = output.Split('\r', '\n'); - return lines - .Where(line => !line.StartsWith("List of devices attached") && - !string.IsNullOrWhiteSpace(line) && - line.Contains("device")) - .Select(line => line.Split('\t')[0]) - .ToList(); - } - - /// - /// Executes an ADB command. - /// - /// adb path - /// adb command - /// output - public static string ExecuteAdbCommand(string adbPath, string command) - { - try + emulators.Add(emulatorId); + var processPath = process.MainModule?.FileName; + foreach (string adbPath in _adbRelativePathDict[emulatorId] + .Select(path => Path.GetDirectoryName(processPath) + "\\" + path) + .Where(File.Exists)) { - var process = new Process + _adbAbsolutePathDict[emulatorId] = adbPath; + } + } + + return emulators; + } + + /// + /// Gets ADB path by emulator name. + /// + /// The name of the emulator. + /// The ADB path of the emulator. + public string GetAdbPathByEmulatorName(string emulatorName) + { + return _adbAbsolutePathDict.GetValueOrDefault(emulatorName); + } + + /// + /// Gets ADB addresses by an ADB path. + /// + /// The ADB path. + /// The list of ADB addresses. + public static List GetAdbAddresses(string adbPath) + { + string output = ExecuteAdbCommand(adbPath, "devices"); + var lines = output.Split('\r', '\n'); + return lines + .Where(line => !line.StartsWith("List of devices attached") && + !string.IsNullOrWhiteSpace(line) && + line.Contains("device")) + .Select(line => line.Split('\t')[0]) + .ToList(); + } + + /// + /// Executes an ADB command. + /// + /// adb path + /// adb command + /// output + public static string ExecuteAdbCommand(string adbPath, string command) + { + try + { + var process = new Process + { + StartInfo = new() { - StartInfo = new() - { - FileName = adbPath, - Arguments = command, - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - }, - }; + FileName = adbPath, + Arguments = command, + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + }, + }; - process.Start(); - string output = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - return output; - } - catch (Exception e) - { - _logger.Error(e.Message); - return string.Empty; - } + process.Start(); + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + return output; + } + catch (Exception e) + { + _logger.Error(e.Message); + return string.Empty; } } } diff --git a/src/MaaWpfGui/Helper/WindowHandle.cs b/src/MaaWpfGui/Helper/WindowHandle.cs index 9f5f190bd5..f0a988af60 100644 --- a/src/MaaWpfGui/Helper/WindowHandle.cs +++ b/src/MaaWpfGui/Helper/WindowHandle.cs @@ -15,25 +15,24 @@ using System; using System.Windows; using System.Windows.Interop; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +public struct WindowHandle { - public struct WindowHandle + public IntPtr Handle { get; private set; } + + public static WindowHandle None => new WindowHandle() { Handle = IntPtr.Zero }; + + public static implicit operator WindowHandle(IntPtr h) => new WindowHandle() { Handle = h }; + + public static implicit operator WindowHandle(Window w) { - public IntPtr Handle { get; private set; } - - public static WindowHandle None => new WindowHandle() { Handle = IntPtr.Zero }; - - public static implicit operator WindowHandle(IntPtr h) => new WindowHandle() { Handle = h }; - - public static implicit operator WindowHandle(Window w) + if (w == null) { - if (w == null) - { - return None; - } - - var interop = new WindowInteropHelper(w); - return new WindowHandle { Handle = interop.Handle }; + return None; } + + var interop = new WindowInteropHelper(w); + return new WindowHandle { Handle = interop.Handle }; } } diff --git a/src/MaaWpfGui/Helper/WindowManager.cs b/src/MaaWpfGui/Helper/WindowManager.cs index 327f8d61e9..58cf9ac917 100644 --- a/src/MaaWpfGui/Helper/WindowManager.cs +++ b/src/MaaWpfGui/Helper/WindowManager.cs @@ -22,231 +22,230 @@ using Serilog; using Stylet; using Rect = MaaWpfGui.Models.Rect; -namespace MaaWpfGui.Helper +namespace MaaWpfGui.Helper; + +public class WindowManager : Stylet.WindowManager { - public class WindowManager : Stylet.WindowManager + public WindowManager(IViewManager viewManager, Func messageBoxViewModelFactory, IWindowManagerConfig config) +: base(viewManager, messageBoxViewModelFactory, config) { - public WindowManager(IViewManager viewManager, Func messageBoxViewModelFactory, IWindowManagerConfig config) - : base(viewManager, messageBoxViewModelFactory, config) + } + + private static readonly ILogger _logger = Log.ForContext(); + + private readonly bool _loadWindowPlacement = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.LoadWindowPlacement, bool.TrueString)); + private readonly bool _saveWindowPlacement = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.SaveWindowPlacement, bool.TrueString)); + private readonly bool _minimizeDirectly = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.MinimizeDirectly, bool.FalseString)); + private readonly bool _minimizeToTray = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.MinimizeToTray, bool.FalseString)); + + /// + /// Center other windows in MaaWpfGui.RootView + /// + /// 需要居中的窗口 + public static void MoveWindowToRootCenter(Window window) + { + var mainWindow = Application.Current.MainWindow; + if (mainWindow is not { WindowState: WindowState.Normal }) { + return; } - private static readonly ILogger _logger = Log.ForContext(); + // In Stylet, CreateWindow().WindowStartupLocation is CenterScreen or CenterOwner (if w.WSLoc == Manual && w.Left == NaN && w.Top == NaN && ...) + window.WindowStartupLocation = WindowStartupLocation.Manual; + window.Left = mainWindow!.Left + ((mainWindow.Width - window.Width) / 2); + window.Top = mainWindow.Top + ((mainWindow.Height - window.Height) / 2); + } - private readonly bool _loadWindowPlacement = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.LoadWindowPlacement, bool.TrueString)); - private readonly bool _saveWindowPlacement = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.SaveWindowPlacement, bool.TrueString)); - private readonly bool _minimizeDirectly = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.MinimizeDirectly, bool.FalseString)); - private readonly bool _minimizeToTray = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.MinimizeToTray, bool.FalseString)); - - /// - /// Center other windows in MaaWpfGui.RootView - /// - /// 需要居中的窗口 - public static void MoveWindowToRootCenter(Window window) + /// + protected override Window CreateWindow(object viewModel, bool isDialog, IViewAware ownerViewModel) + { + Window window = base.CreateWindow(viewModel, isDialog, ownerViewModel); + if (window is RootView) { - var mainWindow = Application.Current.MainWindow; - if (mainWindow is not { WindowState: WindowState.Normal }) + HandyControl.Controls.Dialog.Register(nameof(RootView), window); + + if (_loadWindowPlacement && GetConfiguration(out WindowPlacement wp)) { - return; + window.SourceInitialized += (s, e) => + { + bool success = SetWindowPlacement(window, ref wp, minimizeDirectly: _minimizeDirectly); + _logger.Information("Whether the window placement was set successfully: {Success}", success); + }; } - // In Stylet, CreateWindow().WindowStartupLocation is CenterScreen or CenterOwner (if w.WSLoc == Manual && w.Left == NaN && w.Top == NaN && ...) - window.WindowStartupLocation = WindowStartupLocation.Manual; - window.Left = mainWindow!.Left + ((mainWindow.Width - window.Width) / 2); - window.Top = mainWindow.Top + ((mainWindow.Height - window.Height) / 2); - } - - /// - protected override Window CreateWindow(object viewModel, bool isDialog, IViewAware ownerViewModel) - { - Window window = base.CreateWindow(viewModel, isDialog, ownerViewModel); - if (window is RootView) + if (_loadWindowPlacement && _saveWindowPlacement) { - HandyControl.Controls.Dialog.Register(nameof(RootView), window); - - if (_loadWindowPlacement && GetConfiguration(out WindowPlacement wp)) + window.Closing += (s, e) => { - window.SourceInitialized += (s, e) => + if (!GetWindowPlacement(window, out WindowPlacement windowPlacement)) { - bool success = SetWindowPlacement(window, ref wp, minimizeDirectly: _minimizeDirectly); - _logger.Information("Whether the window placement was set successfully: {Success}", success); - }; - } + _logger.Error("Failed to get window placement"); + } - if (_loadWindowPlacement && _saveWindowPlacement) - { - window.Closing += (s, e) => + WindowPlacement defaultPlacement = default; + if (Equals(windowPlacement, defaultPlacement)) { - if (!GetWindowPlacement(window, out WindowPlacement windowPlacement)) - { - _logger.Error("Failed to get window placement"); - } + _logger.Information("Window placement is the default value; skipping save."); + return; + } - WindowPlacement defaultPlacement = default; - if (Equals(windowPlacement, defaultPlacement)) - { - _logger.Information("Window placement is the default value; skipping save."); - return; - } - - if (!SetConfiguration(windowPlacement)) - { - _logger.Error("Failed to save window placement"); - } - }; - } - - // ReSharper disable once InvertIf - if (_minimizeDirectly && _minimizeToTray) - { - window.ShowInTaskbar = false; - window.Visibility = Visibility.Hidden; - } + if (!SetConfiguration(windowPlacement)) + { + _logger.Error("Failed to save window placement"); + } + }; } - else if (!isDialog && ownerViewModel == null) + + // ReSharper disable once InvertIf + if (_minimizeDirectly && _minimizeToTray) { - MoveWindowToRootCenter(window); + window.ShowInTaskbar = false; + window.Visibility = Visibility.Hidden; } - - return window; + } + else if (!isDialog && ownerViewModel == null) + { + MoveWindowToRootCenter(window); } - private bool SetConfiguration(WindowPlacement wp) + return window; + } + + private bool SetConfiguration(WindowPlacement wp) + { + try { - try - { - // 请在配置文件中修改该部分配置,暂不支持从GUI设置 - // Please modify this part of configuration in the configuration file. - ConfigurationHelper.SetGlobalValue(ConfigurationKeys.LoadWindowPlacement, _loadWindowPlacement.ToString()); - ConfigurationHelper.SetGlobalValue(ConfigurationKeys.SaveWindowPlacement, _saveWindowPlacement.ToString()); + // 请在配置文件中修改该部分配置,暂不支持从GUI设置 + // Please modify this part of configuration in the configuration file. + ConfigurationHelper.SetGlobalValue(ConfigurationKeys.LoadWindowPlacement, _loadWindowPlacement.ToString()); + ConfigurationHelper.SetGlobalValue(ConfigurationKeys.SaveWindowPlacement, _saveWindowPlacement.ToString()); - return ConfigurationHelper.SetGlobalValue(ConfigurationKeys.WindowPlacement, JsonConvert.SerializeObject(wp)); - } - catch (Exception e) - { - _logger.Error(e, "Failed to serialize json string to {Key}", ConfigurationKeys.WindowPlacement); - } + return ConfigurationHelper.SetGlobalValue(ConfigurationKeys.WindowPlacement, JsonConvert.SerializeObject(wp)); + } + catch (Exception e) + { + _logger.Error(e, "Failed to serialize json string to {Key}", ConfigurationKeys.WindowPlacement); + } + return false; + } + + private static bool GetConfiguration(out WindowPlacement wp) + { + wp = default; + var jsonStr = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.WindowPlacement, string.Empty); + if (string.IsNullOrEmpty(jsonStr)) + { return false; } - private static bool GetConfiguration(out WindowPlacement wp) + try { - wp = default; - var jsonStr = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.WindowPlacement, string.Empty); - if (string.IsNullOrEmpty(jsonStr)) - { - return false; - } + wp = JsonConvert.DeserializeObject(jsonStr) ?? throw new Exception("Failed to parse json string"); + return true; + } + catch (Exception e) + { + _logger.Error(e, "Failed to deserialize json string from {Key}", ConfigurationKeys.WindowPlacement); + } - try - { - wp = JsonConvert.DeserializeObject(jsonStr) ?? throw new Exception("Failed to parse json string"); - return true; - } - catch (Exception e) - { - _logger.Error(e, "Failed to deserialize json string from {Key}", ConfigurationKeys.WindowPlacement); - } + return false; + } + /// + /// 显示窗口并激活 + /// + /// 需要操作的窗口 + /// 是否成功激活 + public static bool ShowWindow(Window window) + { + window.Show(); + window.WindowState = WindowState.Normal; + return window.Activate(); + } + + /// + /// 强制显示窗口并激活 + /// + /// 需要操作的窗口 + /// 是否成功激活 + public static bool ForceShow(Window window) + { + WindowPlacement wp = default; + var result = SetWindowPlacement(window, ref wp, true); + return result; + } + + private static bool SetWindowPlacement(WindowHandle window, ref WindowPlacement wp, bool force = false, bool minimizeDirectly = false) + { + try + { + // Load window placement details for previous application session from application settings + // Note - if window was closed on a monitor that is now disconnected from the computer, + // SetWindowPlacement will place the window onto a visible monitor. + wp.Length = Marshal.SizeOf(typeof(WindowPlacement)); + wp.Flags = 0; + + // wp.ShowCmd = wp.ShowCmd == SwShowminimized ? SwShownormal : wp.ShowCmd; + wp.ShowCmd = !minimizeDirectly || force ? SwShownormal : SwShowminimized; + return SetWindowPlacement(window.Handle, ref wp); + } + catch + { + return false; + } + } + + private static bool GetWindowPlacement(WindowHandle window, out WindowPlacement wp) + { + if (!GetWindowPlacement(window.Handle, out wp)) + { return false; } - /// - /// 显示窗口并激活 - /// - /// 需要操作的窗口 - /// 是否成功激活 - public static bool ShowWindow(Window window) + // get the Aero Snap position of the window + if (wp.ShowCmd != SwShownormal || !GetWindowRect(window.Handle, out Rect rect)) { - window.Show(); - window.WindowState = WindowState.Normal; - return window.Activate(); - } - - /// - /// 强制显示窗口并激活 - /// - /// 需要操作的窗口 - /// 是否成功激活 - public static bool ForceShow(Window window) - { - WindowPlacement wp = default; - var result = SetWindowPlacement(window, ref wp, true); - return result; - } - - private static bool SetWindowPlacement(WindowHandle window, ref WindowPlacement wp, bool force = false, bool minimizeDirectly = false) - { - try - { - // Load window placement details for previous application session from application settings - // Note - if window was closed on a monitor that is now disconnected from the computer, - // SetWindowPlacement will place the window onto a visible monitor. - wp.Length = Marshal.SizeOf(typeof(WindowPlacement)); - wp.Flags = 0; - - // wp.ShowCmd = wp.ShowCmd == SwShowminimized ? SwShownormal : wp.ShowCmd; - wp.ShowCmd = !minimizeDirectly || force ? SwShownormal : SwShowminimized; - return SetWindowPlacement(window.Handle, ref wp); - } - catch - { - return false; - } - } - - private static bool GetWindowPlacement(WindowHandle window, out WindowPlacement wp) - { - if (!GetWindowPlacement(window.Handle, out wp)) - { - return false; - } - - // get the Aero Snap position of the window - if (wp.ShowCmd != SwShownormal || !GetWindowRect(window.Handle, out Rect rect)) - { - return true; - } - - wp.NormalPosition = rect; - - // rect is screen coordinates, wp is workspace coordinates - if (!SetWindowPlacement(window.Handle, ref wp) || !GetWindowPlacement(window.Handle, out WindowPlacement currentWp) || !GetWindowRect(window.Handle, out Rect currentRect)) - { - return true; - } - - var taskBarWidth = currentRect.Left - currentWp.NormalPosition.Left; - var taskBarHeight = currentRect.Top - currentWp.NormalPosition.Top; - if (taskBarWidth == 0 && taskBarHeight == 0) - { - return true; - } - - rect.Left -= taskBarWidth; - rect.Right -= taskBarWidth; - rect.Top -= taskBarHeight; - rect.Bottom -= taskBarHeight; - wp.NormalPosition = rect; - SetWindowPlacement(window.Handle, ref wp); - return true; } - #region Win32 API declarations to set and get window placement - [DllImport("user32.dll")] - private static extern bool SetWindowPlacement(IntPtr hWnd, [In] ref WindowPlacement lpwndpl); + wp.NormalPosition = rect; - [DllImport("user32.dll")] - private static extern bool GetWindowPlacement(IntPtr hWnd, out WindowPlacement lpwndpl); + // rect is screen coordinates, wp is workspace coordinates + if (!SetWindowPlacement(window.Handle, ref wp) || !GetWindowPlacement(window.Handle, out WindowPlacement currentWp) || !GetWindowRect(window.Handle, out Rect currentRect)) + { + return true; + } - [DllImport("user32.dll")] - private static extern bool GetWindowRect(IntPtr hWnd, out Rect lpRect); + var taskBarWidth = currentRect.Left - currentWp.NormalPosition.Left; + var taskBarHeight = currentRect.Top - currentWp.NormalPosition.Top; + if (taskBarWidth == 0 && taskBarHeight == 0) + { + return true; + } - private const int SwShownormal = 1; - private const int SwShowminimized = 2; + rect.Left -= taskBarWidth; + rect.Right -= taskBarWidth; + rect.Top -= taskBarHeight; + rect.Bottom -= taskBarHeight; + wp.NormalPosition = rect; + SetWindowPlacement(window.Handle, ref wp); - #endregion + return true; } + + #region Win32 API declarations to set and get window placement + [DllImport("user32.dll")] + private static extern bool SetWindowPlacement(IntPtr hWnd, [In] ref WindowPlacement lpwndpl); + + [DllImport("user32.dll")] + private static extern bool GetWindowPlacement(IntPtr hWnd, out WindowPlacement lpwndpl); + + [DllImport("user32.dll")] + private static extern bool GetWindowRect(IntPtr hWnd, out Rect lpRect); + + private const int SwShownormal = 1; + private const int SwShowminimized = 2; + + #endregion } diff --git a/src/MaaWpfGui/Main/AsstProxy.cs b/src/MaaWpfGui/Main/AsstProxy.cs index c840c2c6ab..d49277e7cf 100644 --- a/src/MaaWpfGui/Main/AsstProxy.cs +++ b/src/MaaWpfGui/Main/AsstProxy.cs @@ -58,2658 +58,2657 @@ using AsstTaskId = System.Int32; using FightTask = MaaWpfGui.ViewModels.UserControl.TaskQueue.FightSettingsUserControlModel; using ToastNotification = MaaWpfGui.Helper.ToastNotification; -namespace MaaWpfGui.Main +namespace MaaWpfGui.Main; + +/// +/// MaaCore 代理类。 +/// +public class AsstProxy { - /// - /// MaaCore 代理类。 - /// - public class AsstProxy + private readonly RunningState _runningState; + private static readonly ILogger _logger = Log.ForContext(); + + public DateTimeOffset StartTaskTime { get; set; } + + private static unsafe byte[] EncodeNullTerminatedUtf8(string s) { - private readonly RunningState _runningState; - private static readonly ILogger _logger = Log.ForContext(); - - public DateTimeOffset StartTaskTime { get; set; } - - private static unsafe byte[] EncodeNullTerminatedUtf8(string s) + var enc = Encoding.UTF8.GetEncoder(); + fixed (char* c = s) { - var enc = Encoding.UTF8.GetEncoder(); - fixed (char* c = s) + var len = enc.GetByteCount(c, s.Length, true); + var buf = new byte[len + 1]; + fixed (byte* ptr = buf) { - var len = enc.GetByteCount(c, s.Length, true); - var buf = new byte[len + 1]; - fixed (byte* ptr = buf) - { - enc.Convert(c, s.Length, ptr, len, true, out _, out _, out _); - } - - return buf; + enc.Convert(c, s.Length, ptr, len, true, out _, out _, out _); } + + return buf; } + } - private static unsafe bool AsstSetUserDir(string dirname) + private static unsafe bool AsstSetUserDir(string dirname) + { + fixed (byte* ptr = EncodeNullTerminatedUtf8(dirname)) { - fixed (byte* ptr = EncodeNullTerminatedUtf8(dirname)) - { - _logger.Information("AsstSetUserDir dirame: {Dirname}", dirname); - var ret = MaaService.AsstSetUserDir(ptr); - _logger.Information("AsstSetUserDir ret: {Ret}", ret); - return ret; - } + _logger.Information("AsstSetUserDir dirame: {Dirname}", dirname); + var ret = MaaService.AsstSetUserDir(ptr); + _logger.Information("AsstSetUserDir ret: {Ret}", ret); + return ret; } + } - private static unsafe bool AsstLoadResource(string dirname) + private static unsafe bool AsstLoadResource(string dirname) + { + fixed (byte* ptr = EncodeNullTerminatedUtf8(dirname)) { - fixed (byte* ptr = EncodeNullTerminatedUtf8(dirname)) - { - _logger.Information("AsstLoadResource dirname: {Dirname}", dirname); - var ret = MaaService.AsstLoadResource(ptr); - _logger.Information("AsstLoadResource ret: {Ret}", ret); - return ret; - } + _logger.Information("AsstLoadResource dirname: {Dirname}", dirname); + var ret = MaaService.AsstLoadResource(ptr); + _logger.Information("AsstLoadResource ret: {Ret}", ret); + return ret; } + } - private static unsafe bool AsstSetInstanceOption(AsstHandle handle, AsstInstanceOptionKey key, string value) + private static unsafe bool AsstSetInstanceOption(AsstHandle handle, AsstInstanceOptionKey key, string value) + { + fixed (byte* ptr1 = EncodeNullTerminatedUtf8(value)) { - fixed (byte* ptr1 = EncodeNullTerminatedUtf8(value)) - { - return MaaService.AsstSetInstanceOption(handle, key, ptr1); - } + return MaaService.AsstSetInstanceOption(handle, key, ptr1); } + } - private static unsafe bool AsstConnect(AsstHandle handle, string adbPath, string address, string config) + private static unsafe bool AsstConnect(AsstHandle handle, string adbPath, string address, string config) + { + _logger.Information("handle: {Handle}, adbPath: {AdbPath}, address: {Address}, config: {Config}", (long)handle, adbPath, address, config); + + fixed (byte* ptr1 = EncodeNullTerminatedUtf8(adbPath), + ptr2 = EncodeNullTerminatedUtf8(address), + ptr3 = EncodeNullTerminatedUtf8(config)) { - _logger.Information("handle: {Handle}, adbPath: {AdbPath}, address: {Address}, config: {Config}", (long)handle, adbPath, address, config); - - fixed (byte* ptr1 = EncodeNullTerminatedUtf8(adbPath), - ptr2 = EncodeNullTerminatedUtf8(address), - ptr3 = EncodeNullTerminatedUtf8(config)) - { - bool ret = MaaService.AsstConnect(handle, ptr1, ptr2, ptr3); - _logger.Information("handle: {Handle}, adbPath: {AdbPath}, address: {Address}, config: {Config}, return: {Ret}", (long)handle, adbPath, address, config, ret); - return ret; - } + bool ret = MaaService.AsstConnect(handle, ptr1, ptr2, ptr3); + _logger.Information("handle: {Handle}, adbPath: {AdbPath}, address: {Address}, config: {Config}, return: {Ret}", (long)handle, adbPath, address, config, ret); + return ret; } + } - private static unsafe void AsstSetConnectionExtras(string name, string extras) + private static unsafe void AsstSetConnectionExtras(string name, string extras) + { + _logger.Information("name: {Name}, extras: {Extras}", name, extras); + + fixed (byte* ptr1 = EncodeNullTerminatedUtf8(name), + ptr2 = EncodeNullTerminatedUtf8(extras)) { - _logger.Information("name: {Name}, extras: {Extras}", name, extras); - - fixed (byte* ptr1 = EncodeNullTerminatedUtf8(name), - ptr2 = EncodeNullTerminatedUtf8(extras)) - { - MaaService.AsstSetConnectionExtras(ptr1, ptr2); - } + MaaService.AsstSetConnectionExtras(ptr1, ptr2); } + } - private static void AsstSetConnectionExtrasMuMu12(string extras) + private static void AsstSetConnectionExtrasMuMu12(string extras) + { + AsstSetConnectionExtras("MuMuEmulator12", extras); + } + + private static void AsstSetConnectionExtrasLdPlayer(string extras) + { + AsstSetConnectionExtras("LDPlayer", extras); + } + + private static unsafe AsstTaskId AsstAppendTask(AsstHandle handle, string type, string taskParams) + { + fixed (byte* ptr1 = EncodeNullTerminatedUtf8(type), + ptr2 = EncodeNullTerminatedUtf8(taskParams)) { - AsstSetConnectionExtras("MuMuEmulator12", extras); + return MaaService.AsstAppendTask(handle, ptr1, ptr2); } + } - private static void AsstSetConnectionExtrasLdPlayer(string extras) + private static unsafe bool AsstSetTaskParams(AsstHandle handle, AsstTaskId id, string taskParams) + { + fixed (byte* ptr1 = EncodeNullTerminatedUtf8(taskParams)) { - AsstSetConnectionExtras("LDPlayer", extras); + return MaaService.AsstSetTaskParams(handle, id, ptr1); } + } - private static unsafe AsstTaskId AsstAppendTask(AsstHandle handle, string type, string taskParams) + // 现在拆分了 core 和 UI 的日志,所以这个函数暂时没用到 + /* + [DllImport("MaaCore.dll")] + private static extern unsafe void AsstLog(byte* level, byte* message); + + /// + /// 记录日志。 + /// + /// 日志内容。 + public static unsafe void AsstLog(string message) + { + var level = new ReadOnlySpan(new byte[] { (byte)'G', (byte)'U', (byte)'I', 0 }); + fixed (byte* ptr1 = level, ptr2 = EncodeNullTerminatedUtf8(message)) { - fixed (byte* ptr1 = EncodeNullTerminatedUtf8(type), - ptr2 = EncodeNullTerminatedUtf8(taskParams)) - { - return MaaService.AsstAppendTask(handle, ptr1, ptr2); - } + AsstLog(ptr1, ptr2); } + } + */ - private static unsafe bool AsstSetTaskParams(AsstHandle handle, AsstTaskId id, string taskParams) + public static unsafe BitmapImage? AsstGetImage(AsstHandle handle) + { + var buffer = ArrayPool.Shared.Rent(1280 * 720 * 3); + try { - fixed (byte* ptr1 = EncodeNullTerminatedUtf8(taskParams)) - { - return MaaService.AsstSetTaskParams(handle, id, ptr1); - } - } - - // 现在拆分了 core 和 UI 的日志,所以这个函数暂时没用到 - /* - [DllImport("MaaCore.dll")] - private static extern unsafe void AsstLog(byte* level, byte* message); - - /// - /// 记录日志。 - /// - /// 日志内容。 - public static unsafe void AsstLog(string message) - { - var level = new ReadOnlySpan(new byte[] { (byte)'G', (byte)'U', (byte)'I', 0 }); - fixed (byte* ptr1 = level, ptr2 = EncodeNullTerminatedUtf8(message)) - { - AsstLog(ptr1, ptr2); - } - } - */ - - public static unsafe BitmapImage? AsstGetImage(AsstHandle handle) - { - var buffer = ArrayPool.Shared.Rent(1280 * 720 * 3); - try - { - ulong readSize; - fixed (byte* ptr = buffer) - { - readSize = MaaService.AsstGetImage(handle, ptr, (ulong)buffer.Length); - } - - if (readSize == MaaService.AsstGetNullSize()) - { - return null; - } - - // buff is a png data - var image = new BitmapImage(); - image.BeginInit(); - using var stream = new MemoryStream(buffer, 0, (int)readSize, false); - image.StreamSource = stream; - image.CacheOption = BitmapCacheOption.OnLoad; - image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; - image.EndInit(); - image.Freeze(); - return image; - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } - - public BitmapImage? AsstGetImage() - { - return AsstGetImage(_handle); - } - - public BitmapImage? AsstGetFreshImage() - { - MaaService.AsstAsyncScreencap(_handle, true); - return AsstGetImage(_handle); - } - - public static async Task AsstGetImageAsync(AsstHandle handle) - { - return await Task.Run(() => AsstGetImage(handle)); - } - - public async Task AsstGetImageAsync() - { - return await AsstGetImageAsync(_handle); - } - - public async Task AsstGetFreshImageAsync() - { - MaaService.AsstAsyncScreencap(_handle, true); - return await AsstGetImageAsync(_handle); - } - - // 需要外部调用 ArrayPool.Shared.Return(buffer) - public static unsafe byte[]? AsstGetImageBgrData(AsstHandle handle) - { - const int Width = 1280, Height = 720, Channels = 3; - const int TotalSize = Width * Height * Channels; - - var buffer = ArrayPool.Shared.Rent(TotalSize); - ulong readSize; fixed (byte* ptr = buffer) { - readSize = MaaService.AsstGetImageBgr(handle, ptr, TotalSize); + readSize = MaaService.AsstGetImage(handle, ptr, (ulong)buffer.Length); } if (readSize == MaaService.AsstGetNullSize()) { - ArrayPool.Shared.Return(buffer); return null; } - // **不拷贝,直接返回池内存** - // 外层代码用完必须调用 ArrayPool.Shared.Return(buffer) - return buffer; + // buff is a png data + var image = new BitmapImage(); + image.BeginInit(); + using var stream = new MemoryStream(buffer, 0, (int)readSize, false); + image.StreamSource = stream; + image.CacheOption = BitmapCacheOption.OnLoad; + image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; + image.EndInit(); + image.Freeze(); + return image; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public BitmapImage? AsstGetImage() + { + return AsstGetImage(_handle); + } + + public BitmapImage? AsstGetFreshImage() + { + MaaService.AsstAsyncScreencap(_handle, true); + return AsstGetImage(_handle); + } + + public static async Task AsstGetImageAsync(AsstHandle handle) + { + return await Task.Run(() => AsstGetImage(handle)); + } + + public async Task AsstGetImageAsync() + { + return await AsstGetImageAsync(_handle); + } + + public async Task AsstGetFreshImageAsync() + { + MaaService.AsstAsyncScreencap(_handle, true); + return await AsstGetImageAsync(_handle); + } + + // 需要外部调用 ArrayPool.Shared.Return(buffer) + public static unsafe byte[]? AsstGetImageBgrData(AsstHandle handle) + { + const int Width = 1280, Height = 720, Channels = 3; + const int TotalSize = Width * Height * Channels; + + var buffer = ArrayPool.Shared.Rent(TotalSize); + + ulong readSize; + fixed (byte* ptr = buffer) + { + readSize = MaaService.AsstGetImageBgr(handle, ptr, TotalSize); } - // 需要外部调用 ArrayPool.Shared.Return(buffer) - public byte[]? AsstGetImageBgrData() + if (readSize == MaaService.AsstGetNullSize()) { - return AsstGetImageBgrData(_handle); + ArrayPool.Shared.Return(buffer); + return null; } - // 需要外部调用 ArrayPool.Shared.Return(buffer) - public byte[]? AsstGetFreshImageBgrData() + // **不拷贝,直接返回池内存** + // 外层代码用完必须调用 ArrayPool.Shared.Return(buffer) + return buffer; + } + + // 需要外部调用 ArrayPool.Shared.Return(buffer) + public byte[]? AsstGetImageBgrData() + { + return AsstGetImageBgrData(_handle); + } + + // 需要外部调用 ArrayPool.Shared.Return(buffer) + public byte[]? AsstGetFreshImageBgrData() + { + MaaService.AsstAsyncScreencap(_handle, true); + return AsstGetImageBgrData(_handle); + } + + // 需要外部调用 ArrayPool.Shared.Return(buffer) + public static async Task AsstGetImageBgrDataAsync(AsstHandle handle) + { + return await Task.Run(() => AsstGetImageBgrData(handle)); + } + + // 需要外部调用 ArrayPool.Shared.Return(buffer) + public async Task AsstGetImageBgrDataAsync() + { + return await AsstGetImageBgrDataAsync(_handle); + } + + // 需要外部调用 ArrayPool.Shared.Return(buffer) + public async Task AsstGetFreshImageBgrDataAsync() + { + MaaService.AsstAsyncScreencap(_handle, true); + return await AsstGetImageBgrDataAsync(_handle); + } + + public static WriteableBitmap WriteBgrToBitmap(byte[] bgrData, WriteableBitmap? targetBitmap) + { + const int Width = 1280, Height = 720; + const int Stride = Width * 3; + + targetBitmap ??= new(Width, Height, 96, 96, PixelFormats.Bgr24, null); + targetBitmap.Lock(); + targetBitmap.WritePixels( + new(0, 0, Width, Height), + bgrData, + Stride, + 0); + targetBitmap.Unlock(); + return targetBitmap; + } + + private readonly MaaService.CallbackDelegate _callback; + + /// + /// Initializes a new instance of the class. + /// + public AsstProxy() + { + _callback = CallbackFunction; + _runningState = RunningState.Instance; + _tasksStatus.CollectionChanged += (in NotifyCollectionChangedEventArgs> args) => { - MaaService.AsstAsyncScreencap(_handle, true); - return AsstGetImageBgrData(_handle); - } - - // 需要外部调用 ArrayPool.Shared.Return(buffer) - public static async Task AsstGetImageBgrDataAsync(AsstHandle handle) - { - return await Task.Run(() => AsstGetImageBgrData(handle)); - } - - // 需要外部调用 ArrayPool.Shared.Return(buffer) - public async Task AsstGetImageBgrDataAsync() - { - return await AsstGetImageBgrDataAsync(_handle); - } - - // 需要外部调用 ArrayPool.Shared.Return(buffer) - public async Task AsstGetFreshImageBgrDataAsync() - { - MaaService.AsstAsyncScreencap(_handle, true); - return await AsstGetImageBgrDataAsync(_handle); - } - - public static WriteableBitmap WriteBgrToBitmap(byte[] bgrData, WriteableBitmap? targetBitmap) - { - const int Width = 1280, Height = 720; - const int Stride = Width * 3; - - targetBitmap ??= new(Width, Height, 96, 96, PixelFormats.Bgr24, null); - targetBitmap.Lock(); - targetBitmap.WritePixels( - new(0, 0, Width, Height), - bgrData, - Stride, - 0); - targetBitmap.Unlock(); - return targetBitmap; - } - - private readonly MaaService.CallbackDelegate _callback; - - /// - /// Initializes a new instance of the class. - /// - public AsstProxy() - { - _callback = CallbackFunction; - _runningState = RunningState.Instance; - _tasksStatus.CollectionChanged += (in NotifyCollectionChangedEventArgs> args) => + if (args.Action == NotifyCollectionChangedAction.Reset) { - if (args.Action == NotifyCollectionChangedAction.Reset) + TaskSettingVisibilityInfo.Instance.CurrentTask = string.Empty; + } + }; + + AsstSetUserDir(PathsHelper.BaseDir); + } + + /// + /// Finalizes an instance of the class. + /// + ~AsstProxy() + { + if (_handle != AsstHandle.Zero) + { + AsstDestroy(); + } + } + + /// + /// 加载全局资源。新版 core 全惰性加载资源,所以可以无脑调用 + /// + /// 是否成功。 + public bool LoadResource() + { + _logger.Information("Load Resource"); + + string clientType = SettingsViewModel.GameSettings.ClientType; + + string mainRes = PathsHelper.ResourceDir; + string globalRes = Path.Combine(mainRes, "global", clientType, "resource"); + string mainCacheRes = PathsHelper.CacheDir; + string globalCacheRes = Path.Combine(mainCacheRes, "global", clientType, "resource"); + + bool loaded; + if (clientType is "" or "Official" or "Bilibili") + { + // Read resources first, then read cache + CopyTasksJson(mainCacheRes); + loaded = LoadResIfExists(mainRes); + loaded &= LoadResIfExists(mainCacheRes); + } + else + { + // Read resources first, then read cache + CopyTasksJson(mainCacheRes); + CopyTasksJson(globalCacheRes); + loaded = LoadResIfExists(mainRes) && LoadResIfExists(mainCacheRes); + loaded &= LoadResIfExists(globalRes) && LoadResIfExists(globalCacheRes); + } + + return loaded; + + static bool LoadResIfExists(string path) + { + if (!Directory.Exists(path)) + { + _logger.Warning("Resource not found: {Path}", path); + return true; + } + + _logger.Information("Load resource: {Path}", path); + + // AsstLoadResource 需要的是 resource 的上级目录 + var parent = Directory.GetParent(path)?.FullName ?? string.Empty; + if (string.IsNullOrEmpty(parent)) + { + _logger.Warning("Resource path invalid: {Path}", path); + return false; + } + + return AsstLoadResource(parent); + } + + // 新的目录结构为 tasks/tasks.json,api 为了兼容,仍然存在 resource/tasks.json + static void CopyTasksJson(string oldPath) + { + try + { + string tasksJsonPath = Path.Combine(oldPath, @"resource\tasks.json"); + string tasksFolderPath = Path.Combine(oldPath, @"resource\tasks"); + string newTasksJsonPath = Path.Combine(tasksFolderPath, "tasks.json"); + + if (!File.Exists(tasksJsonPath)) { - TaskSettingVisibilityInfo.Instance.CurrentTask = string.Empty; + return; } - }; - AsstSetUserDir(PathsHelper.BaseDir); - } + if (!Directory.Exists(tasksFolderPath)) + { + Directory.CreateDirectory(tasksFolderPath); + _logger.Information("Created directory: {TasksFolderPath}", tasksFolderPath); + } - /// - /// Finalizes an instance of the class. - /// - ~AsstProxy() - { - if (_handle != AsstHandle.Zero) + File.Copy(tasksJsonPath, newTasksJsonPath, true); + _logger.Information("Moved {TasksJsonPath} to {NewTasksJsonPath}", tasksJsonPath, newTasksJsonPath); + } + catch (Exception ex) { - AsstDestroy(); + _logger.Error("Failed to move tasks.json: {ExMessage}", ex.Message); } } + } - /// - /// 加载全局资源。新版 core 全惰性加载资源,所以可以无脑调用 - /// - /// 是否成功。 - public bool LoadResource() + /// + /// 初始化。 + /// + public void Init() + { + if (GpuOption.GetCurrent() is GpuOption.EnableOption x) { - _logger.Information("Load Resource"); + var info = x.GpuInfo; + var description = info?.Description; + var version = info?.DriverVersion; + var date = info?.DriverDate?.ToString("yyyy-MM-dd"); - string clientType = SettingsViewModel.GameSettings.ClientType; - - string mainRes = PathsHelper.ResourceDir; - string globalRes = Path.Combine(mainRes, "global", clientType, "resource"); - string mainCacheRes = PathsHelper.CacheDir; - string globalCacheRes = Path.Combine(mainCacheRes, "global", clientType, "resource"); - - bool loaded; - if (clientType is "" or "Official" or "Bilibili") + if (x.IsDeprecated) { - // Read resources first, then read cache - CopyTasksJson(mainCacheRes); - loaded = LoadResIfExists(mainRes); - loaded &= LoadResIfExists(mainCacheRes); + Instances.TaskQueueViewModel.AddLog(string.Format(LocalizationHelper.GetString("GpuDeprecatedMessage"), description), UiLogColor.Warning); + _logger.Warning("Using deprecated GPU {0} (Driver {1} {2})", description, version, date); } else { - // Read resources first, then read cache - CopyTasksJson(mainCacheRes); - CopyTasksJson(globalCacheRes); - loaded = LoadResIfExists(mainRes) && LoadResIfExists(mainCacheRes); - loaded &= LoadResIfExists(globalRes) && LoadResIfExists(globalCacheRes); + _logger.Information("Using GPU {0} (Driver {1} {2})", description, version, date); } - return loaded; - - static bool LoadResIfExists(string path) - { - if (!Directory.Exists(path)) - { - _logger.Warning("Resource not found: {Path}", path); - return true; - } - - _logger.Information("Load resource: {Path}", path); - - // AsstLoadResource 需要的是 resource 的上级目录 - var parent = Directory.GetParent(path)?.FullName ?? string.Empty; - if (string.IsNullOrEmpty(parent)) - { - _logger.Warning("Resource path invalid: {Path}", path); - return false; - } - - return AsstLoadResource(parent); - } - - // 新的目录结构为 tasks/tasks.json,api 为了兼容,仍然存在 resource/tasks.json - static void CopyTasksJson(string oldPath) - { - try - { - string tasksJsonPath = Path.Combine(oldPath, @"resource\tasks.json"); - string tasksFolderPath = Path.Combine(oldPath, @"resource\tasks"); - string newTasksJsonPath = Path.Combine(tasksFolderPath, "tasks.json"); - - if (!File.Exists(tasksJsonPath)) - { - return; - } - - if (!Directory.Exists(tasksFolderPath)) - { - Directory.CreateDirectory(tasksFolderPath); - _logger.Information("Created directory: {TasksFolderPath}", tasksFolderPath); - } - - File.Copy(tasksJsonPath, newTasksJsonPath, true); - _logger.Information("Moved {TasksJsonPath} to {NewTasksJsonPath}", tasksJsonPath, newTasksJsonPath); - } - catch (Exception ex) - { - _logger.Error("Failed to move tasks.json: {ExMessage}", ex.Message); - } - } + AsstSetStaticOption(AsstStaticOptionKey.GpuOCR, x.Index.ToString()); } - /// - /// 初始化。 - /// - public void Init() - { - if (GpuOption.GetCurrent() is GpuOption.EnableOption x) - { - var info = x.GpuInfo; - var description = info?.Description; - var version = info?.DriverVersion; - var date = info?.DriverDate?.ToString("yyyy-MM-dd"); + bool loaded = LoadResource(); - if (x.IsDeprecated) + _handle = MaaService.AsstCreateEx(_callback, AsstHandle.Zero); + + if (loaded == false || _handle == AsstHandle.Zero) + { + Execute.OnUIThreadAsync( + () => + { + MessageBoxHelper.Show(LocalizationHelper.GetString("ResourceBroken"), LocalizationHelper.GetString("Error"), iconKey: ResourceToken.FatalGeometry, iconBrushKey: ResourceToken.DangerBrush); + Bootstrapper.Shutdown(); + }); + } + + _runningState.SetInit(true); + AsstSetInstanceOption(InstanceOptionKey.TouchMode, SettingsViewModel.ConnectSettings.TouchMode); + AsstSetInstanceOption(InstanceOptionKey.DeploymentWithPause, SettingsViewModel.GameSettings.DeploymentWithPause ? "1" : "0"); + AsstSetInstanceOption(InstanceOptionKey.AdbLiteEnabled, SettingsViewModel.ConnectSettings.AdbLiteEnabled ? "1" : "0"); + + // TODO: 之后把这个 OnUIThread 拆出来 + // ReSharper disable once AsyncVoidLambda + Execute.OnUIThread( + async () => + { + if (SettingsViewModel.StartSettings.RunDirectly) + { + // 如果是直接运行模式,就先让按钮显示为运行 + _runningState.SetIdle(false); + } + + await Task.Run(() => SettingsViewModel.StartSettings.TryToStartEmulator(true)); + + // 一般是点了“停止”按钮了 + if (_runningState.GetStopping()) + { + Instances.TaskQueueViewModel.SetStopped(); + return; + } + + // ReSharper disable once InvertIf + if (SettingsViewModel.StartSettings.RunDirectly) + { + // 重置按钮状态,不影响LinkStart判断 + _runningState.SetIdle(true); + await Instances.TaskQueueViewModel.LinkStart(); + } + }); + } + + /// + /// Determines the length of the specified string (not including the terminating null character). + /// + /// The null-terminated string to be checked. + /// + /// The function returns the length of the string, in characters. + /// If is , the function returns 0. + /// + [DllImport("ucrtbase.dll", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + internal static extern int strlen(AsstHandle ptr); + + private static string? PtrToStringCustom(AsstHandle ptr, Encoding enc) + { + if (ptr == AsstHandle.Zero) + { + return null; + } + + int len = strlen(ptr); + + if (len == 0) + { + return string.Empty; + } + + byte[] bytes = new byte[len]; + Marshal.Copy(ptr, bytes, 0, len); + return enc.GetString(bytes); + } + + private void CallbackFunction(int msg, AsstHandle jsonBuffer, AsstHandle customArg) + { + var jsonStr = PtrToStringCustom(jsonBuffer, Encoding.UTF8); + + // Console.WriteLine(json_str); + var json = (JObject?)JsonConvert.DeserializeObject(jsonStr ?? string.Empty); + MaaService.ProcCallbackMsg dlg = ProcMsg; + Execute.OnUIThread( + () => + { + dlg((AsstMsg)msg, json); + }); + } + + private AsstHandle _handle; + + private void ProcMsg(AsstMsg msg, JObject details) + { + switch (msg) + { + case AsstMsg.InternalError: + break; + + case AsstMsg.InitFailed: + MessageBoxHelper.Show(LocalizationHelper.GetString("InitializationError"), LocalizationHelper.GetString("Error"), iconKey: ResourceToken.FatalGeometry, iconBrushKey: ResourceToken.DangerBrush); + Bootstrapper.Shutdown(); + break; + + case AsstMsg.ConnectionInfo: + ProcConnectInfo(details); + break; + + case AsstMsg.TaskChainStart: + Instances.TaskQueueViewModel.Running = true; + goto case AsstMsg.TaskChainExtraInfo; // fallthrough + case AsstMsg.AllTasksCompleted: + case AsstMsg.AsyncCallInfo: + case AsstMsg.Destroyed: + case AsstMsg.TaskChainError: + case AsstMsg.TaskChainCompleted: + case AsstMsg.TaskChainStopped: + Instances.TaskQueueViewModel.Running = false; + goto case AsstMsg.TaskChainExtraInfo; // fallthrough + case AsstMsg.TaskChainExtraInfo: + ProcTaskChainMsg(msg, details); + break; + + case AsstMsg.SubTaskError: + case AsstMsg.SubTaskStart: + case AsstMsg.SubTaskCompleted: + case AsstMsg.SubTaskExtraInfo: + ProcSubTaskMsg(msg, details); + TaskQueueViewModel.InvokeProcSubTaskMsg(msg, details); + break; + + case AsstMsg.SubTaskStopped: + break; + + case AsstMsg.ReportRequest: + _ = ProcReportRequest(details); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(msg), msg, null); + } + } + + public bool Connected { get; set; } + + private string _connectedAdb = string.Empty; + private string _connectedAddress = string.Empty; + + private void ProcConnectInfo(JObject details) + { + var what = details["what"]?.ToString() ?? string.Empty; + switch (what) + { + case "Connected": + Connected = true; + _connectedAdb = details["details"]!["adb"]!.ToString(); + _connectedAddress = details["details"]!["address"]!.ToString(); + SettingsViewModel.ConnectSettings.ConnectAddress = _connectedAddress; + break; + + case "UnsupportedResolution": + Connected = false; + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ResolutionNotSupported"), UiLogColor.Error); + break; + + case "ResolutionError": + Connected = false; + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ResolutionAcquisitionFailure"), UiLogColor.Error); + break; + + case "Reconnecting": + Instances.TaskQueueViewModel.AddLog($"{LocalizationHelper.GetString("TryToReconnect")} ({Convert.ToUInt32(details!["details"]!["times"]) + 1})", UiLogColor.Error); + break; + + case "Reconnected": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ReconnectSuccess")); + break; + + case "Disconnect": + Connected = false; + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ReconnectFailed"), UiLogColor.Error); + if (_runningState.GetIdle()) { - Instances.TaskQueueViewModel.AddLog(string.Format(LocalizationHelper.GetString("GpuDeprecatedMessage"), description), UiLogColor.Warning); - _logger.Warning("Using deprecated GPU {0} (Driver {1} {2})", description, version, date); + break; + } + + if (!AsstStop()) + { + _logger.Warning("Failed to stop Asst"); + } + + break; + + case "ScreencapFailed": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ScreencapFailed"), UiLogColor.Error); + break; + + case "TouchModeNotAvailable": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("TouchModeNotAvailable"), UiLogColor.Error); + Connected = false; + break; + + case "FastestWayToScreencap": + { + string costString = details["details"]?["cost"]?.ToString() ?? "???"; + string method = details["details"]?["method"]?.ToString() ?? "???"; + SettingsViewModel.ConnectSettings.ScreencapMethod = method; + + List<(string Method, string Cost)>? screencapAlternatives = null; + var alternativesToken = details["details"]?["alternatives"]; + if (alternativesToken is JArray { Count: > 1 } arr) + { + screencapAlternatives = arr.Select(item => + { + string method1 = item?["method"]?.ToString() ?? "???"; + string cost1 = item?["cost"]?.ToString() ?? "???"; + return (method1, cost1); + }).ToList(); + } + + StringBuilder fastestScreencapStringBuilder = new(); + string color = UiLogColor.Trace; + if (int.TryParse(costString, out var timeCost)) + { + switch (timeCost) + { + case > 800: + costString = timeCost.ToString("#,#"); + color = UiLogColor.Warning; + break; + + case > 400: + color = UiLogColor.Warning; + break; + } + } + else + { + color = UiLogColor.Error; + } + + var needToStop = false; + switch (SettingsViewModel.ConnectSettings.ConnectConfig) + { + case "MuMuEmulator12": + if (!SettingsViewModel.ConnectSettings.MuMuEmulator12Extras.Enable) + { + break; + } + + if (method != "MumuExtras") + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("MuMuExtrasNotEnabledMessage"), UiLogColor.Error); + Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("MuMuExtrasNotEnabledMessage"), UiLogColor.Error, showTime: false); + needToStop = true; + } + else if (timeCost < 100) + { + color = UiLogColor.MuMuSpecialScreenshot; + } + + break; + + case "LDPlayer": + if (!SettingsViewModel.ConnectSettings.LdPlayerExtras.Enable) + { + break; + } + + if (method != "LDExtras") + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("LdExtrasNotEnabledMessage"), UiLogColor.Error); + Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("LdExtrasNotEnabledMessage"), UiLogColor.Error, showTime: false); + needToStop = true; + } + else if (timeCost < 100) + { + color = UiLogColor.LdSpecialScreenshot; + } + + break; + } + + fastestScreencapStringBuilder.Insert(0, string.Format(LocalizationHelper.GetString("FastestWayToScreencap"), costString, method)); + var fastestScreencapString = fastestScreencapStringBuilder.ToString(); + SettingsViewModel.ConnectSettings.ScreencapTestCost = fastestScreencapString; + Instances.TaskQueueViewModel.AddLog(fastestScreencapString, color, toolTip: screencapAlternatives.CreateScreencapTooltip()); + Instances.CopilotViewModel.AddLog(fastestScreencapString, color, showTime: false); + + // 截图增强未生效禁止启动 + if (needToStop) + { + Execute.OnUIThreadAsync(async () => + { + Connected = false; + await Instances.TaskQueueViewModel.Stop(); + Instances.TaskQueueViewModel.SetStopped(); + }); + } + } + + break; + + case "ScreencapCost": + var screencapCostMin = details["details"]?["min"]?.ToString() ?? "???"; + var screencapCostAvg = details["details"]?["avg"]?.ToString() ?? "???"; + var screencapCostMax = details["details"]?["max"]?.ToString() ?? "???"; + var currentTime = DateTimeOffset.Now.ToString("HH:mm:ss"); + SettingsViewModel.ConnectSettings.ScreencapCost = string.Format(LocalizationHelper.GetString("ScreencapCost"), screencapCostMin, screencapCostAvg, screencapCostMax, currentTime); + if (!HasPrintedScreencapWarning && int.TryParse(screencapCostAvg, out var screencapCostAvgInt)) + { + static void AddLog(string message, string color) + { + Instances.TaskQueueViewModel.AddLog(message, color); + Instances.CopilotViewModel.AddLog(message, color, showTime: false); + HasPrintedScreencapWarning = true; + } + + switch (screencapCostAvgInt) + { + // 日志提示 + case >= 800: + AddLog(string.Format(LocalizationHelper.GetString("FastestWayToScreencapErrorTip"), screencapCostAvgInt), UiLogColor.Warning); + AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge1); + break; + + case >= 400: + AddLog(string.Format(LocalizationHelper.GetString("FastestWayToScreencapWarningTip"), screencapCostAvgInt), UiLogColor.Warning); + AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge2); + break; + + default: + { + AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge3); + + if (screencapCostAvgInt < 100) + { + AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge4); + } + + if (screencapCostAvgInt < 10) + { + AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge5); + } + + if (screencapCostAvgInt < 5) + { + AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge6); + } + + break; + } + } + } + + break; + } + } + + private DispatcherTimer? _toastNotificationTimer; + + private void OnToastNotificationTimerTick(object? sender, EventArgs e) + { + if (FightTask.SanityReport is not null) + { + var sanityReport = LocalizationHelper.GetString("SanityReport"); + var recoveryTime = FightTask.SanityReport.ReportTime.AddMinutes(FightTask.SanityReport.SanityCurrent < FightTask.SanityReport.SanityMax ? (FightTask.SanityReport.SanityMax - FightTask.SanityReport.SanityCurrent) * 6 : 0); + sanityReport = sanityReport.Replace("{DateTime}", recoveryTime.ToString("yyyy-MM-dd HH:mm")).Replace("{TimeDiff}", (recoveryTime - DateTimeOffset.Now).ToString(@"h\h\ m\m")); + ToastNotification.ShowDirect(sanityReport); + } + + DisposeTimer(); + } + + public void DisposeTimer() + { + if (_toastNotificationTimer is null) + { + return; + } + + _toastNotificationTimer.Stop(); + _toastNotificationTimer.Tick -= OnToastNotificationTimerTick; + _toastNotificationTimer = null; + } + + private void ProcTaskChainMsg(AsstMsg msg, JObject details) + { + string taskChain = details["taskchain"]?.ToString() ?? string.Empty; + AsstTaskId taskId = details["taskid"]?.ToObject() ?? 0; + switch (taskChain) + { + case "CloseDown": + return; + + case "Recruit": + { + if (msg == AsstMsg.TaskChainError) + { + Instances.ToolboxViewModel.RecruitInfo = LocalizationHelper.GetString("IdentifyTheMistakes"); + ToastNotification.ShowDirect(LocalizationHelper.GetString("IdentifyTheMistakes")); + } + + break; + } + } + + bool isCopilotTaskChain = taskChain is "Copilot" or "VideoRecognition"; + + switch (msg) + { + case AsstMsg.TaskChainStopped: + Instances.TaskQueueViewModel.SetStopped(); + TaskStatusUpdate(taskId, TaskStatus.Completed); + _tasksStatus.Clear(); + break; + + case AsstMsg.TaskChainError: + { + // 对剿灭的特殊处理,如果刷完了剿灭还选了剿灭会因为找不到入口报错 + TaskStatusUpdate(taskId, TaskStatus.Completed); + _tasksStatus.TryGetValue(taskId, out var value); + if (value is { Type: TaskType.Fight } && + TaskQueueViewModel.FightTask.Stage == "Annihilation" && + TaskQueueViewModel.FightTask.UseAlternateStage && + TaskQueueViewModel.FightTask.Stages.Any(stage => + Instances.TaskQueueViewModel.IsStageOpen(stage ?? string.Empty) && + stage != "Annihilation")) + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("AnnihilationTaskFailed"), UiLogColor.Warning); + } + else if (value is { Type: TaskType.Copilot } or { Type: TaskType.VideoRec }) + { + Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("CombatError"), UiLogColor.Error); + AchievementTrackerHelper.Instance.Unlock(AchievementIds.CopilotError); + } + else + { + var log = LocalizationHelper.GetString("TaskError") + LocalizationHelper.GetString(taskChain); + Instances.TaskQueueViewModel.AddLog(log, UiLogColor.Error); + ToastNotification.ShowDirect(log); + + if (SettingsViewModel.ExternalNotificationSettings.ExternalNotificationSendWhenError) + { + ExternalNotificationService.Send(log, log); + } + } + + break; + } + + case AsstMsg.TaskChainStart: + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StartTask") + LocalizationHelper.GetString(taskChain)); + TaskStatusUpdate(taskId, TaskStatus.InProgress); + break; + } + + case AsstMsg.TaskChainCompleted: + { + // 判断 _latestTaskId 中是否有元素的值和 details["taskid"] 相等,如果有再判断这个 id 对应的任务是否在 _mainTaskTypes 中 + TaskStatusUpdate(taskId, TaskStatus.Completed); + if (_tasksStatus.TryGetValue(taskId, out var task)) + { + if (_mainTaskTypes.Contains(task.Type)) + { + Instances.TaskQueueViewModel.UpdateMainTasksProgress(); + } + } + + switch (taskChain) + { + case "Infrast": + InfrastSettingsUserControlModel.Instance.IncreaseCustomInfrastPlanIndex(); + InfrastSettingsUserControlModel.Instance.RefreshCustomInfrastPlanIndexByPeriod(); + break; + } + + if (taskChain == "Fight" && FightTask.SanityReport is not null) + { + var sanityLog = "\n" + string.Format(LocalizationHelper.GetString("CurrentSanity"), FightTask.SanityReport.SanityCurrent, FightTask.SanityReport.SanityMax); + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CompleteTask") + LocalizationHelper.GetString(taskChain) + sanityLog); + } + else + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CompleteTask") + LocalizationHelper.GetString(taskChain)); + } + + if (isCopilotTaskChain) + { + AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.UseCopilotGroup); + } + + break; + } + + case AsstMsg.TaskChainExtraInfo: + { + var what = details["what"]?.ToString(); + var why = details["why"]?.ToString(); + + switch (what) + { + case "RoutingRestart": + string msgText = string.Empty; + switch (why) + { + case "TooManyBattlesAhead": + var cost = details["node_cost"]?.ToString() ?? "?"; + msgText = string.Format(LocalizationHelper.GetString("RoutingRestartTooManyBattles"), cost); + break; + } + + Instances.TaskQueueViewModel.AddLog(msgText, UiLogColor.Warning); + break; + } + + break; + } + + case AsstMsg.AllTasksCompleted: + bool isMainTaskQueueAllCompleted = false; + var taskList = details["finished_tasks"]?.ToObject(); + if (taskList?.Length > 0) + { + var latestMainTaskIds = _tasksStatus.Where(i => _mainTaskTypes.Contains(i.Value.Type)).Select(i => i.Key); + isMainTaskQueueAllCompleted = taskList.Any(i => latestMainTaskIds.Contains(i)); + } + + if (_tasksStatus.Any(t => t.Value.Type == TaskType.Copilot)) + { + if (SettingsViewModel.GameSettings.CopilotWithScript) + { + Task.Run(() => SettingsViewModel.GameSettings.RunScript("EndsWithScript", showLog: false)); + if (!string.IsNullOrWhiteSpace(SettingsViewModel.GameSettings.EndsWithScript)) + { + Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("EndsWithScript")); + } + } + } + + bool buyWine = _tasksStatus.Any(t => t.Value.Type == TaskType.Mall) && Instances.SettingsViewModel.DidYouBuyWine(); + _tasksStatus.Clear(); + + Instances.TaskQueueViewModel.ResetAllTemporaryVariable(); + _runningState.SetIdle(true); + + if (isMainTaskQueueAllCompleted) + { + var dateTimeNow = DateTimeOffset.Now; + var diffTaskTime = (dateTimeNow - StartTaskTime).ToString(@"h\h\ m\m\ s\s"); + + var allTaskCompleteTitle = string.Format(LocalizationHelper.GetString("AllTasksComplete"), diffTaskTime); + var allTaskCompleteMessage = LocalizationHelper.GetString("AllTaskCompleteContent"); + var sanityReport = LocalizationHelper.GetString("SanityReport"); + + var configurationPreset = ConfigurationHelper.GetCurrentConfiguration(); + + allTaskCompleteMessage = allTaskCompleteMessage + .Replace("{DateTime}", dateTimeNow.ToString("yyyy-MM-dd HH:mm:ss")) + .Replace("{Preset}", configurationPreset) + .Replace("{TimeDiff}", diffTaskTime); + + var allTaskCompleteLog = string.Format(LocalizationHelper.GetString("AllTasksComplete"), diffTaskTime); + + if (FightTask.SanityReport is not null) + { + var recoveryTime = FightTask.SanityReport.ReportTime.AddMinutes(FightTask.SanityReport.SanityCurrent < FightTask.SanityReport.SanityMax ? (FightTask.SanityReport.SanityMax - FightTask.SanityReport.SanityCurrent) * 6 : 0); + sanityReport = sanityReport.Replace("{DateTime}", recoveryTime.ToString("yyyy-MM-dd HH:mm")).Replace("{TimeDiff}", (recoveryTime - DateTimeOffset.Now).ToString(@"h\h\ m\m")); + + allTaskCompleteLog = allTaskCompleteLog + Environment.NewLine + sanityReport; + Instances.TaskQueueViewModel.AddLog(allTaskCompleteLog); + + if (SettingsViewModel.ExternalNotificationSettings.ExternalNotificationSendWhenComplete) + { + var logs = SettingsViewModel.ExternalNotificationSettings.ExternalNotificationEnableDetails + ? Instances.TaskQueueViewModel.LogItemViewModels.Aggregate(string.Empty, (current, logItem) => current + $"[{logItem.Time}][{logItem.Color}]{logItem.Content}\n") + : string.Empty; + logs += allTaskCompleteMessage; + + ExternalNotificationService.Send(allTaskCompleteTitle, logs + Environment.NewLine + sanityReport); + } + + if (_toastNotificationTimer is not null) + { + DisposeTimer(); + } + + var interval = recoveryTime - DateTimeOffset.Now.AddMinutes(6); + if (interval > TimeSpan.Zero) + { + _toastNotificationTimer = new DispatcherTimer + { + Interval = interval, + }; + _toastNotificationTimer.Tick += OnToastNotificationTimerTick; + _toastNotificationTimer.Start(); + } + } + else + { + Instances.TaskQueueViewModel.AddLog(allTaskCompleteLog); + + if (SettingsViewModel.ExternalNotificationSettings.ExternalNotificationSendWhenComplete) + { + var logs = SettingsViewModel.ExternalNotificationSettings.ExternalNotificationEnableDetails + ? Instances.TaskQueueViewModel.LogItemViewModels.Aggregate(string.Empty, (current, logItem) => current + $"[{logItem.Time}][{logItem.Color}]{logItem.Content}\n") + : string.Empty; + logs += allTaskCompleteMessage; + + ExternalNotificationService.Send(allTaskCompleteTitle, logs); + } + } + + using (var toast = new ToastNotification(allTaskCompleteTitle)) + { + if (FightTask.SanityReport is not null) + { + toast.AppendContentText(sanityReport); + } + + toast.Show(); + } + + if (DateTime.UtcNow.ToYjDate().IsAprilFoolsDay()) + { + if (Application.Current.MainWindow?.DataContext is RootViewModel rvm) + { + rvm.GifVisibility = true; + rvm.ChangeGif(); + } + } + + // Instances.TaskQueueViewModel.CheckAndShutdown(); + _ = Instances.TaskQueueViewModel.CheckAfterCompleted(); + } + else if (isCopilotTaskChain) + { + ToastNotification.ShowDirect(LocalizationHelper.GetString("CompleteTask") + LocalizationHelper.GetString(taskChain)); + } + + if (buyWine) + { + Instances.SettingsViewModel.LastBuyWineTime = DateTime.UtcNow.ToYjDate().ToFormattedString(); + var result = MessageBoxHelper.Show( + LocalizationHelper.GetString("DrunkAndStaggering"), + LocalizationHelper.GetString("Burping"), + iconKey: "DrunkAndStaggeringGeometry", + iconBrushKey: "PallasBrush"); + if (result == MessageBoxResult.OK) + { + Instances.SettingsViewModel.Cheers = true; + Bootstrapper.ShutdownAndRestartWithoutArgs(); + } + } + + break; + + case AsstMsg.InternalError: + break; + + case AsstMsg.InitFailed: + break; + + case AsstMsg.ConnectionInfo: + break; + + case AsstMsg.AsyncCallInfo: + break; + + case AsstMsg.Destroyed: + break; + + case AsstMsg.SubTaskError: + break; + + case AsstMsg.SubTaskStart: + break; + + case AsstMsg.SubTaskCompleted: + break; + + case AsstMsg.SubTaskExtraInfo: + break; + + case AsstMsg.SubTaskStopped: + break; + + default: + throw new ArgumentOutOfRangeException(nameof(msg), msg, null); + } + } + + private static void ProcSubTaskMsg(AsstMsg msg, JObject details) + { + // 下面几行注释暂时没用到,先注释起来... + // string taskChain = details["taskchain"].ToString(); + // string classType = details["class"].ToString(); + switch (msg) + { + case AsstMsg.SubTaskError: + ProcSubTaskError(details); + break; + + case AsstMsg.SubTaskStart: + ProcSubTaskStart(details); + break; + + case AsstMsg.SubTaskCompleted: + ProcSubTaskCompleted(details); + break; + + case AsstMsg.SubTaskExtraInfo: + ProcSubTaskExtraInfo(details); + break; + + case AsstMsg.InternalError: + break; + + case AsstMsg.InitFailed: + break; + + case AsstMsg.ConnectionInfo: + break; + + case AsstMsg.AllTasksCompleted: + break; + + case AsstMsg.TaskChainError: + break; + + case AsstMsg.TaskChainStart: + break; + + case AsstMsg.TaskChainCompleted: + break; + + case AsstMsg.TaskChainExtraInfo: + break; + + case AsstMsg.TaskChainStopped: + break; + + case AsstMsg.SubTaskStopped: + break; + + default: + throw new ArgumentOutOfRangeException(nameof(msg), msg, null); + } + } + + private static void ProcSubTaskError(JObject details) + { + string subTask = details["subtask"]?.ToString() ?? string.Empty; + switch (subTask) + { + case "StartGameTask": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FailedToOpenClient"), UiLogColor.Error); + break; + + case "StopGameTask": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CloseArknightsFailed"), UiLogColor.Error); + break; + + case "AutoRecruitTask": + { + var whyStr = details.TryGetValue("why", out var why) ? why.ToString() : LocalizationHelper.GetString("ErrorOccurred"); + Instances.TaskQueueViewModel.AddLog(whyStr + ", " + LocalizationHelper.GetString("HasReturned"), UiLogColor.Error); + break; + } + + case "RecognizeDrops": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("DropRecognitionError"), UiLogColor.Error); + break; + + case "ReportToPenguinStats": + { + var why = details["why"]!.ToString(); + Instances.TaskQueueViewModel.AddLog(why + ", " + LocalizationHelper.GetString("GiveUpUploadingPenguins"), UiLogColor.Warning); + break; + } + + case "CheckStageValid": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("TheEx"), UiLogColor.Error); + break; + + case "BattleFormationTask": + { + var why = details.TryGetValue("why", out var whyObj) ? whyObj.ToString() : string.Empty; + if (why == "OperatorMissing") + { + var missingOpers = details["details"]?["opers"]?.ToObject>(); + if (missingOpers is not null && missingOpers.Count > 0) + { + var str = new StringBuilder(); + str.AppendLine(); + foreach (var (groupName, opers) in missingOpers) + { + if (opers.Count == 1) + { + str.AppendLine($"{groupName}"); + continue; + } + else + { + var operList = opers.Cast().ToList(); // 确保 opers 是动态类型 + str.AppendLine($"{groupName}=> {string.Join(" / ", operList.Select(i => i.name).ToList())}"); + } + } + + Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("MissingOperators") + str.ToString().TrimEnd(), UiLogColor.Error); + } + else + { + Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("MissingOperators"), UiLogColor.Error); + } + + /* + if (missingOpers is not null && missingOpers.Count >= 2) + { + AchievementTrackerHelper.Instance.Unlock(AchievementIds.Irreplaceable); + }*/ + } + + break; + } + } + } + + private static void ProcSubTaskStart(JObject details) + { + string subTask = details["subtask"]?.ToString() ?? string.Empty; + switch (subTask) + { + case "ProcessTask": + { + string taskName = details!["details"]!["task"]!.ToString(); + int execTimes = (int)details!["details"]!["exec_times"]!; + + switch (taskName) + { + case "StartButton2": + case "AnnihilationConfirm": + StringBuilder missionStartLogBuilder = new(); + if (FightTask.FightReport is null) + { + missionStartLogBuilder.AppendLine(string.Format(LocalizationHelper.GetString("MissionStart.FightTask"), "???", "???")); + } + else + { + var times = FightTask.FightReport.Series == 1 ? $"{FightTask.FightReport.TimesFinished + 1}" : $"{FightTask.FightReport.TimesFinished + 1}~{FightTask.FightReport.TimesFinished + FightTask.FightReport.Series}"; + missionStartLogBuilder.AppendLine(string.Format(LocalizationHelper.GetString("MissionStart.FightTask"), times, FightTask.FightReport.SanityCost)); + } + + if (FightTask.SanityReport is not null) + { + missionStartLogBuilder.AppendFormat(LocalizationHelper.GetString("CurrentSanity"), FightTask.SanityReport.SanityCurrent, FightTask.SanityReport.SanityMax); + } + + if (ExpiringMedicineUsedTimes > 0) + { + missionStartLogBuilder.AppendFormat(LocalizationHelper.GetString("MedicineUsedTimesWithExpiring"), MedicineUsedTimes, ExpiringMedicineUsedTimes); + } + else if (MedicineUsedTimes > 0) + { + missionStartLogBuilder.AppendFormat(LocalizationHelper.GetString("MedicineUsedTimes"), MedicineUsedTimes); + } + + if (StoneUsedTimes > 0) + { + missionStartLogBuilder.AppendFormat(LocalizationHelper.GetString("StoneUsedTimes"), StoneUsedTimes); + } + + Instances.TaskQueueViewModel.AddLog(missionStartLogBuilder.ToString().TrimEnd(), UiLogColor.Info); + break; + + case "StoneConfirm": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StoneUsed") + $" {execTimes} " + LocalizationHelper.GetString("UnitTime"), UiLogColor.Info); + StoneUsedTimes++; + break; + + case "AbandonAction": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ActingCommandError"), UiLogColor.Error); + break; + + case "RecruitRefreshConfirm": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("LabelsRefreshed"), UiLogColor.Info); + break; + + case "RecruitConfirm": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RecruitConfirm"), UiLogColor.Info); + break; + + case "InfrastDormDoubleConfirmButton": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("InfrastDormDoubleConfirmed"), UiLogColor.Error); + break; + + /* 肉鸽相关 */ + case "ExitThenAbandon": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ExplorationAbandoned"), UiLogColor.ExplorationAbandonedIS); + AchievementTrackerHelper.Instance.AddProgress(AchievementIds.RoguelikeRetreat); + break; + + // case "StartAction": + // Instances.TaskQueueViewModel.AddLog("开始战斗"); + // break; + case "MissionCompletedFlag": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FightCompleted"), UiLogColor.SuccessIS); + break; + + case "MissionFailedFlag": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FightFailed"), UiLogColor.Error); + break; + + case "StageTrader": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("Trader"), UiLogColor.TraderIS); + break; + + case "StageSafeHouse": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("SafeHouse"), UiLogColor.SafehouseIS); + break; + + case "StageFilterTruth": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FilterTruth"), UiLogColor.TruthIS); + break; + + // case "StageBoonsEnter": + // Instances.TaskQueueViewModel.AddLog("古堡馈赠"); + // break; + case "StageCombatDps": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CombatDps"), UiLogColor.CombatIS); + break; + + case "StageEmergencyDps": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("EmergencyDps"), UiLogColor.EmergencyIS); + break; + + case "StageDreadfulFoe": + case "StageDreadfulFoe-5": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("DreadfulFoe"), UiLogColor.BossIS); + break; + + case "StageTraderInvestSystemFull": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("UpperLimit"), UiLogColor.Info); + break; + + case "OfflineConfirm": + if (TaskQueueViewModel.FightTask.AutoRestartOnDrop) + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("GameDrop"), UiLogColor.Warning); + } + else + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("GameDropNoRestart"), UiLogColor.Warning); + ToastNotification.ShowDirect(LocalizationHelper.GetString("GameDropNoRestart")); + _ = Instances.TaskQueueViewModel.Stop(); + } + + break; + + case "GamePass": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoguelikeGamePass"), UiLogColor.RareOperator); + AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.RoguelikeGamePassGroup); + break; + + case "BattleStartAll": + Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("MissionStart"), UiLogColor.Info); + break; + + case "StageDrops-Stars-3": + case "StageDrops-Stars-Adverse": + { + Instances.CopilotViewModel.CopilotTaskSuccess(); + Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("CompleteCombat"), UiLogColor.Info); + break; + } + + case "StageTraderSpecialShoppingAfterRefresh": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoguelikeSpecialItemBought"), UiLogColor.RareOperator); + break; + + case "DeepExplorationNotUnlockedComplain": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("DeepExplorationNotUnlockedComplain"), UiLogColor.Warning); + break; + + case "PNS-Resume": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ReclamationPnsModeError"), UiLogColor.Error); + break; + + case "PIS-Commence": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ReclamationPisModeError"), UiLogColor.Error); + break; + } + + break; + } + + case "CombatRecordRecognitionTask": + { + var what = details["what"]?.ToString(); + if (!string.IsNullOrEmpty(what)) + { + Instances.CopilotViewModel.AddLog(what); + } + + break; + } + } + } + + private static void ProcSubTaskCompleted(JObject details) + { + string subTask = details["subtask"]?.ToString() ?? string.Empty; + switch (subTask) + { + case "ProcessTask": + var taskchain = details["taskchain"]?.ToString(); + switch (taskchain) + { + case "Roguelike": + { + var taskName = details!["details"]!["task"]!.ToString(); + int execTimes = (int)details!["details"]!["exec_times"]!; + + if (taskName == "StartExplore") + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("BegunToExplore") + $" {execTimes} " + LocalizationHelper.GetString("UnitTime"), UiLogColor.Info); + } + + break; + } + + case "Mall": + { + var taskName = details["details"]!["task"]!.ToString(); + switch (taskName) + { + case "EndOfActionThenStop": + TaskQueueViewModel.MallTask.LastCreditFightTaskTime = DateTime.UtcNow.ToYjDate().ToFormattedString(); + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CompleteTask") + LocalizationHelper.GetString("CreditFight")); + AchievementTrackerHelper.Instance.AddProgress(AchievementIds.MosquitoLeg); + break; + + case "VisitLimited" or "VisitNextBlack": + TaskQueueViewModel.MallTask.LastCreditVisitFriendsTime = DateTime.UtcNow.ToYjDate().ToFormattedString(); + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CompleteTask") + LocalizationHelper.GetString("Visiting")); + break; + } + + break; + } + } + + break; + } + } + + private static void ProcSubTaskExtraInfo(JObject details) + { + string taskChain = details["taskchain"]?.ToString() ?? string.Empty; + switch (taskChain) + { + case "Recruit": + ProcRecruitCalcMsg(details); + break; + + case "VideoRecognition": + ProcVideoRecMsg(details); + break; + } + + var subTaskDetails = details["details"]; + switch (taskChain) + { + case "Depot": + Instances.ToolboxViewModel.DepotParse((JObject?)subTaskDetails); + break; + + case "OperBox": + Instances.ToolboxViewModel.OperBoxParse((JObject?)subTaskDetails); + break; + } + + string what = details["what"]?.ToString() ?? string.Empty; + switch (what) + { + case "StageDrops": + { + string allDrops = string.Empty; + var statistics = subTaskDetails!["stats"] ?? new JArray(); + var stageInfo = subTaskDetails!["stage"] ?? new JObject(); + int curTimes = (int)(subTaskDetails["cur_times"] ?? -1); + var drops = new List<(string ItemId, string ItemName, int Total, int Add)>(); + + foreach (var item in statistics) + { + var itemId = item["itemId"]?.ToString() ?? string.Empty; + var itemName = item["itemName"]?.ToString() ?? string.Empty; + if (itemName == "furni") + { + itemName = LocalizationHelper.GetString("FurnitureDrop"); + itemId = "3401"; + } + + int totalQuantity = (int)(item["quantity"] ?? -1); + int addQuantity = (int)(item["addQuantity"] ?? -1); + + drops.Add((itemId, itemName, totalQuantity, addQuantity)); + } + + // 先按新增数量降序,再按总数量降序 + drops = [.. drops.OrderByDescending(x => x.Add).ThenByDescending(x => x.Total)]; + + foreach (var (_, itemName, totalQuantity, addQuantity) in drops) + { + allDrops += $"{itemName} : {totalQuantity.FormatNumber(false)}"; + if (addQuantity > 0) + { + allDrops += $" (+{addQuantity.FormatNumber(false)})"; + } + + allDrops += "\n"; + } + + var stageCode = stageInfo["stageCode"]?.ToString(); + allDrops = allDrops.EndsWith('\n') ? allDrops.TrimEnd('\n') : LocalizationHelper.GetString("NoDrop"); + + var dropsForTooltip = drops.Where(x => !string.IsNullOrEmpty(x.ItemId)).ToList(); + + Instances.TaskQueueViewModel.AddLog( + $"{stageCode} {LocalizationHelper.GetString("TotalDrop")}\n" + + $"{allDrops}{(curTimes >= 0 + ? $"\n{LocalizationHelper.GetString("CurTimes")} : {curTimes}" + : string.Empty)}", + toolTip: dropsForTooltip.CreateMaterialDropTooltip()); + + AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.SanitySpenderGroup, curTimes > 0 ? curTimes : 1); + + break; + } + + case "EnterFacility": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ThisFacility") + + LocalizationHelper.GetString($"{subTaskDetails?["facility"]}") + " " + + ((int)(subTaskDetails?["index"] ?? -2) + 1).ToString("D2")); + break; + + case "ProductIncorrect": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ProductIncorrect"), UiLogColor.Error); + break; + + case "ProductUnknown": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ProductUnknown"), UiLogColor.Error); + break; + + case "ProductChanged": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ProductChanged"), UiLogColor.Info); + break; + + case "RecruitTagsDetected": + { + var tags = subTaskDetails!["tags"] ?? new JArray(); + string logContent = tags.Select(tagName => tagName.ToString()) + .Aggregate(string.Empty, (current, tagStr) => current + (tagStr + "\n")); + + logContent = logContent.EndsWith('\n') ? logContent.TrimEnd('\n') : LocalizationHelper.GetString("Error"); + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RecruitingResults") + "\n" + logContent); + + break; + } + + case "RecruitSpecialTag": + { + string special = subTaskDetails!["tag"]!.ToString(); + if (special == "支援机械" && TaskQueueViewModel.RecruitTask.NotChooseLevel1 == false) + { + break; + } + + using var toast = new ToastNotification(LocalizationHelper.GetString("RecruitingTips")); + toast.AppendContentText(special).ShowRecruit(); + + break; + } + + case "RecruitRobotTag": + { + string special = subTaskDetails!["tag"]!.ToString(); + using var toast = new ToastNotification(LocalizationHelper.GetString("RecruitingTips")); + toast.AppendContentText(special).ShowRecruitRobot(); + + break; + } + + case "RecruitResult": + { + int level = (int)subTaskDetails!["level"]!; + var tooltip = Instances.ToolboxViewModel.RecruitResultInlines.CreateTooltip(PlacementMode.Center); + if (level >= 5) + { + using (var toast = new ToastNotification(string.Format(LocalizationHelper.GetString("RecruitmentOfStar"), level))) + { + toast.AppendContentText(new string('★', level)).ShowRecruit(row: 2); + } + + Instances.TaskQueueViewModel.AddLog(level + " ★ Tags", UiLogColor.RareOperator, "Bold", toolTip: tooltip); + } + else + { + Instances.TaskQueueViewModel.AddLog(level + " ★ Tags", UiLogColor.Info, toolTip: tooltip); + } + + if (level == 6) + { + AchievementTrackerHelper.Instance.SetProgress(AchievementIds.RecruitNoSixStarStreak, 1); + } + else + { + AchievementTrackerHelper.Instance.AddProgress(AchievementIds.RecruitNoSixStar); // 累计 + AchievementTrackerHelper.Instance.AddProgress(AchievementIds.RecruitNoSixStarStreak); // 连续 + } + + /* + bool robot = (bool)subTaskDetails["robot"]; + if (robot) + { + using (var toast = new ToastNotification(Localization.GetString("RecruitmentOfBot"))) + { + toast.AppendContentText(new string('★', 1)).ShowRecruitRobot(row: 2); + } + + Instances.TaskQueueViewModel.AddLog(1 + " ★ Tag", LogColor.RobotOperator, "Bold"); + } + */ + + break; + } + + case "RecruitSupportOperator": + { + var name = subTaskDetails!["name"]!.ToString(); + Instances.TaskQueueViewModel.AddLog(string.Format(LocalizationHelper.GetString("RecruitSupportOperator"), name), UiLogColor.Info); + break; + } + + case "RecruitTagsSelected": + { + var selected = subTaskDetails!["tags"] ?? new JArray(); + string selectedLog = selected.Aggregate(string.Empty, (current, tag) => current + (tag + "\n")); + + selectedLog = selectedLog.EndsWith('\n') ? selectedLog.TrimEnd('\n') : LocalizationHelper.GetString("NoDrop"); + + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("Choose") + " Tags:\n" + selectedLog); + + break; + } + + case "RecruitTagsRefreshed": + { + int refreshCount = (int)subTaskDetails!["count"]!; + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("Refreshed") + refreshCount + LocalizationHelper.GetString("UnitTime")); + break; + } + + case "RecruitNoPermit": + { + bool continueRefresh = (bool)subTaskDetails!["continue"]!; + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString(continueRefresh ? "ContinueRefresh" : "NoRecruitmentPermit")); + break; + } + + case "NotEnoughStaff": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("NotEnoughStaff"), UiLogColor.Error); + break; + + case "CreditFullOnlyBuyDiscount": + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CreditFullOnlyBuyDiscount") + subTaskDetails!["credit"], UiLogColor.Message); + break; + } + + /* Roguelike */ + case "StageInfo": + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StartCombat") + subTaskDetails!["name"]); + if (TaskQueueViewModel.RoguelikeTask.RoguelikeDelayAbortUntilCombatComplete) + { + Instances.TaskQueueViewModel.RoguelikeInCombatAndShowWait = true; + } + + break; + } + + case "StageInfoError": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StageInfoError"), UiLogColor.Error); + break; + + case "BattleFormation": + Instances.CopilotViewModel.AddLog( + LocalizationHelper.GetString("BattleFormation") + + "\n[" + + string.Join( + ", ", + (subTaskDetails!["formation"]?.ToObject>() ?? []) + .Select(oper => DataHelper.GetLocalizedCharacterName(oper) ?? oper) + .Where(oper => !string.IsNullOrEmpty(oper))) + "]"); + break; + + case "BattleFormationSelected": + { + var oper_name = DataHelper.GetLocalizedCharacterName(subTaskDetails!["selected"]?.ToString()); + var group_name = subTaskDetails!["group_name"]?.ToString(); + if (group_name is not null && oper_name != group_name) + { + oper_name = $"{group_name} => {oper_name}"; + } + + Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("BattleFormationSelected") + oper_name); + break; + } + + case "BattleFormationOperUnavailable": + { + var oper_name = DataHelper.GetLocalizedCharacterName(subTaskDetails!["oper_name"]?.ToString()); + var requirement_type = subTaskDetails["requirement_type"]?.ToString() == "module" + ? LocalizationHelper.GetString("BattleFormationModuleUnavailable") + : subTaskDetails["requirement_type"]?.ToString() ?? "UnknownRequirementType"; + + Instances.CopilotViewModel.AddLog(string.Format(LocalizationHelper.GetString("BattleFormationOperUnavailable"), oper_name, requirement_type), Instances.CopilotViewModel.IgnoreRequirements ? UiLogColor.Warning : UiLogColor.Error); + break; + } + + case "CopilotAction": + { + var doc = subTaskDetails!["doc"]?.ToString(); + if (!string.IsNullOrEmpty(doc)) + { + var color = subTaskDetails["doc_color"]?.ToString(); + Instances.CopilotViewModel.AddLog(doc, string.IsNullOrEmpty(color) ? UiLogColor.Message : color); + } + + var target = subTaskDetails["target"]?.ToString(); + var actionToken = subTaskDetails?["action"]; + var actionString = actionToken?.ToString() ?? "UnknownAction"; + Instances.CopilotViewModel.AddLog( + string.Format( + LocalizationHelper.GetString("CurrentSteps"), + LocalizationHelper.GetString(actionString), + DataHelper.GetLocalizedCharacterName(target) ?? target)); + + break; + } + + case "CopilotListLoadTaskFileSuccess": + Instances.CopilotViewModel.AddLog($"Parse {subTaskDetails!["file_name"]}[{subTaskDetails["stage_name"]}] Success"); + break; + + case "SSSStage": + Instances.CopilotViewModel.AddLog(string.Format(LocalizationHelper.GetString("CurrentStage"), subTaskDetails!["stage"]), UiLogColor.Info); + break; + + case "SSSSettlement": + Instances.CopilotViewModel.AddLog($"{details["why"]}", UiLogColor.Info); + break; + + case "SSSGamePass": + Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("SSSGamePass"), UiLogColor.RareOperator); + break; + + case "UnsupportedLevel": + Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("UnsupportedLevel") + subTaskDetails!["level"], UiLogColor.Error); + _ = ResourceUpdater.ResourceUpdateAndReloadAsync(); + break; + + case "CustomInfrastRoomGroupsMatch": + // 选用xxx组编组 + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoomGroupsMatch") + subTaskDetails!["group"]); + break; + + case "CustomInfrastRoomGroupsMatchFailed": + // 干员编组匹配失败 + var groups = (JArray?)subTaskDetails!["groups"]; + if (groups != null) + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoomGroupsMatchFailed") + string.Join(", ", groups)); + } + + break; + + case "CustomInfrastRoomOperators": + string nameStr = (subTaskDetails!["names"] ?? new JArray()) + .Aggregate(string.Empty, (current, name) => current + DataHelper.GetLocalizedCharacterName(name.ToString()) + ", "); + + if (nameStr != string.Empty) + { + nameStr = nameStr.Remove(nameStr.Length - 2); + } + + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoomOperators") + nameStr); + break; + + case "InfrastTrainingIdle": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("TrainingIdle")); + break; + + case "InfrastTrainingCompleted": + { + var operatorName = DataHelper.GetLocalizedCharacterName(subTaskDetails!["operator"]?.ToString()) ?? "UnKnown"; + var skillName = subTaskDetails["skill"]?.ToString() ?? "UnKnown"; + Instances.TaskQueueViewModel.AddLog( + $"[{operatorName}] {skillName}\n" + + $"{LocalizationHelper.GetString("TrainingLevel")}: {(int)(subTaskDetails["level"] ?? -1)} {LocalizationHelper.GetString("TrainingCompleted")}", + UiLogColor.Info); + break; + } + + case "InfrastTrainingTimeLeft": + { + var operatorName = DataHelper.GetLocalizedCharacterName(subTaskDetails!["operator"]?.ToString()) ?? "UnKnown"; + var skillName = subTaskDetails["skill"]?.ToString() ?? "UnKnown"; + Instances.TaskQueueViewModel.AddLog( + $"[{operatorName}] {skillName}\n" + + $"{LocalizationHelper.GetString("TrainingLevel")}: {(int)(subTaskDetails["level"] ?? -1)}\n" + + $"{LocalizationHelper.GetString("TrainingTimeLeft")}: {subTaskDetails["time"]}", + UiLogColor.Info); + break; + } + + /* 生息演算 */ + case "ReclamationReport": + Instances.TaskQueueViewModel.AddLog( + LocalizationHelper.GetString("AlgorithmFinish") + "\n" + + LocalizationHelper.GetString("AlgorithmBadge") + ": " + $"{(int)(subTaskDetails!["total_badges"] ?? -1)}(+{(int)(subTaskDetails["badges"] ?? -1)})" + "\n" + + LocalizationHelper.GetString("AlgorithmConstructionPoint") + ": " + $"{(int)(subTaskDetails["total_construction_points"] ?? -1)}(+{(int)(subTaskDetails["construction_points"] ?? -1)})"); + break; + + case "ReclamationProcedureStart": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("MissionStart") + $" {(int)(subTaskDetails!["times"] ?? -1)} " + LocalizationHelper.GetString("UnitTime"), UiLogColor.Info); + break; + + case "ReclamationSmeltGold": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("AlgorithmDoneSmeltGold") + $" {(int)(subTaskDetails!["times"] ?? -1)} " + LocalizationHelper.GetString("UnitTime")); + break; + + case "SanityBeforeStage": + { + FightTask.SanityReport = null; + if (subTaskDetails?.ToObject() is { SanityMax: > 0 } report) + { + FightTask.SanityReport = report; + } + + break; + } + + case "FightTimes": + { + FightTask.FightReport = null; + if ((subTaskDetails?.Children())?.Any() is true) + { + FightTask.FightReport = subTaskDetails.ToObject()!; + if (FightTask.FightReport.TimesFinished > 0) + { + AchievementTrackerHelper.Instance.SetProgress(AchievementIds.OverLimitAgent, FightTask.FightReport.TimesFinished); + } + + if (FightTask.Instance.HasTimesLimited != false && FightTask.FightReport.TimesFinished + FightTask.FightReport.Series > FightTask.Instance.MaxTimes) + { + Instances.TaskQueueViewModel.AddLog(string.Format(LocalizationHelper.GetString("FightTimesUnused"), FightTask.FightReport.TimesFinished, FightTask.FightReport.Series, FightTask.FightReport.TimesFinished + FightTask.FightReport.Series, FightTask.Instance.MaxTimes), UiLogColor.Error); + } + } + + break; + } + + case "UseMedicine": + var medicineReport = (JObject?)subTaskDetails; + if (medicineReport is null || !medicineReport.ContainsKey("is_expiring") || !medicineReport.ContainsKey("count")) + { + break; + } + + var isExpiringMedicine = medicineReport.TryGetValue("is_expiring", out var isExpiringMedicineToken) && (bool)isExpiringMedicineToken; + int medicineCount = medicineReport.TryGetValue("count", out var medicineCountToken) ? (int)medicineCountToken : -1; + + if (medicineCount == -1) + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("MedicineUsed") + " Unknown times", UiLogColor.Error); + break; + } + + string medicineLog; + if (!isExpiringMedicine) + { + MedicineUsedTimes += medicineCount; + medicineLog = LocalizationHelper.GetString("MedicineUsed") + $" {MedicineUsedTimes}(+{medicineCount})"; + AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.SanitySaverGroup, medicineCount); } else { - _logger.Information("Using GPU {0} (Driver {1} {2})", description, version, date); + ExpiringMedicineUsedTimes += medicineCount; + medicineLog = LocalizationHelper.GetString("ExpiringMedicineUsed") + $" {ExpiringMedicineUsedTimes}(+{medicineCount})"; + AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.SanitySaverGroup, medicineCount); + AchievementTrackerHelper.Instance.SetProgress(AchievementIds.SanityExpire, ExpiringMedicineUsedTimes); } - AsstSetStaticOption(AsstStaticOptionKey.GpuOCR, x.Index.ToString()); + Instances.TaskQueueViewModel.AddLog(medicineLog, UiLogColor.Info); + break; + + case "StageQueueUnableToAgent": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StageQueue") + $" {subTaskDetails!["stage_code"]} " + LocalizationHelper.GetString("UnableToAgent"), UiLogColor.Info); + break; + + case "StageQueueMissionCompleted": + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StageQueue") + $" {subTaskDetails!["stage_code"]} - {subTaskDetails["stars"]} ★", UiLogColor.Info); + break; + } + } + + private static void ProcRecruitCalcMsg(JObject details) + { + Instances.ToolboxViewModel.ProcRecruitMsg(details); + } + + private static void ProcVideoRecMsg(JObject details) + { + string what = details["what"]?.ToString() ?? string.Empty; + switch (what) + { + case "Finished": + var filename = details["details"]?["filename"]; + Instances.CopilotViewModel.AddLog("Save to: " + filename, UiLogColor.Info, showTime: false); + + // string p = @"C:\tmp\this path contains spaces, and,commas\target.txt"; + string args = $"/e, /select, \"{filename}\""; + + ProcessStartInfo info = new() + { + FileName = "explorer", + Arguments = args, + }; + Process.Start(info); + break; + } + } + + private static async Task ProcReportRequest(JObject details) + { + string? url = (string?)details["url"]; + if (string.IsNullOrEmpty(url)) + { + _logger.Error("Report request received with empty URL."); + return; + } + + var headersToken = details["headers"]; + Dictionary headers = []; + if (headersToken is JObject headersObj) + { + foreach (var prop in headersObj.Properties()) + { + headers[prop.Name] = prop.Value.ToString(); + } + } + + string? body = (string?)details["body"]; + if (string.IsNullOrEmpty(body)) + { + _logger.Error("Report request received with empty body."); + return; + } + + var content = new StringContent(body, Encoding.UTF8, "application/json"); + + string subTask = details["subtask"]?.ToString() ?? string.Empty; + + bool success = false; + try + { + success = await GameDataReportService.PostWithRetryAsync(url, content, headers, subTask, penguinId => + { + SettingsViewModel.GameSettings.PenguinId = penguinId; + _logger.Information("New PenguinId got: {PenguinId}", penguinId); + }); + } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to report: {Url}", url); + } + + if (!success) + { + Instances.TaskQueueViewModel.AddLog("Failed to report, " + LocalizationHelper.GetString("GiveUpUploadingPenguins"), UiLogColor.Warning); + } + } + + public bool AsstSetInstanceOption(InstanceOptionKey key, string value) + { + return AsstSetInstanceOption(_handle, (AsstInstanceOptionKey)key, value); + } + + public bool AsstSetStaticOption(AsstStaticOptionKey key, string value) + { + return MaaService.AsstSetStaticOption(key, value); + } + + private static readonly bool _forcedReloadResource = File.Exists("DEBUG") || File.Exists("DEBUG.txt"); + + /// + /// 使用 TCP 或 adb devices 命令检查连接。TCP 检测相比 adb devices 更快,但不支持实体机。 + /// + /// adb path,用于实体机检测 + /// 连接地址 + /// 设备是否在线 + private static bool CheckConnection(string adbPath, string? address) + { + if (string.IsNullOrEmpty(address)) + { + return false; + } + + // 实体机可能设备名 -> [host] + if (!address.Contains(':') && !address.Contains('-')) + { + return WinAdapter.GetAdbAddresses(adbPath).Contains(address); + } + + // normal -> [host]:[port] + // LdPlayer -> emulator-[port] + string[] hostAndPort = address.Split([':', '-'], StringSplitOptions.RemoveEmptyEntries); + + if (hostAndPort.Length != 2 || !int.TryParse(hostAndPort[1], out var port)) + { + return false; + } + + string host = hostAndPort[0]; + if (host.StartsWith("emulator")) + { + host = "127.0.0.1"; + port += 1; + } + + using var client = new TcpClient(); + try + { + IAsyncResult result = client.BeginConnect(host, port, null, null); + bool success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(0.5)); + + if (success) + { + client.EndConnect(result); + return true; } - bool loaded = LoadResource(); + client.Close(); + return false; + } + catch + { + return false; + } + } - _handle = MaaService.AsstCreateEx(_callback, AsstHandle.Zero); + /// + /// 连接模拟器。 + /// + /// 具体的连接错误。 + /// 是否成功。 + public bool AsstConnect(ref string error) + { + switch (SettingsViewModel.ConnectSettings.ConnectConfig) + { + case "MuMuEmulator12": + AsstSetConnectionExtrasMuMu12(SettingsViewModel.ConnectSettings.MuMuEmulator12Extras.Config); + break; - if (loaded == false || _handle == AsstHandle.Zero) + case "LDPlayer": + AsstSetConnectionExtrasLdPlayer(SettingsViewModel.ConnectSettings.LdPlayerExtras.Config); + break; + } + + if (SettingsViewModel.ConnectSettings.AutoDetectConnection) + { + if (!AutoDetectConnection(ref error)) + { + return false; + } + } + + if (Connected && _connectedAdb == SettingsViewModel.ConnectSettings.AdbPath && + _connectedAddress == SettingsViewModel.ConnectSettings.ConnectAddress) + { + var actualConnectionStatus = CheckConnection(_connectedAdb, _connectedAddress); + if (!actualConnectionStatus) + { + Connected = false; + _logger.Information("Connection lost to {ConnectedAdb} {ConnectedAddress}", _connectedAdb, _connectedAddress); + error = "Connection lost"; + } + else + { + _logger.Information("Already connected to {ConnectedAdb} {ConnectedAddress}", _connectedAdb, _connectedAddress); + if (!_forcedReloadResource) + { + return true; + } + + _logger.Information("Forced reload resource"); + if (!LoadResource()) + { + error = "Load Resource Failed"; + return false; + } + + ToastNotification.ShowDirect("Auto Reload"); + + return true; + } + } + + bool ret = AsstConnect(_handle, SettingsViewModel.ConnectSettings.AdbPath, SettingsViewModel.ConnectSettings.ConnectAddress, SettingsViewModel.ConnectSettings.ConnectConfig); + + // 尝试默认的备选端口 + if (!ret && SettingsViewModel.ConnectSettings.AutoDetectConnection) + { + if (SettingsViewModel.ConnectSettings.DefaultAddress.TryGetValue(SettingsViewModel.ConnectSettings.ConnectConfig, out var value)) + { + foreach (var address in value + .TakeWhile(_ => !_runningState.GetIdle())) + { + ret = AsstConnect(_handle, SettingsViewModel.ConnectSettings.AdbPath, address, SettingsViewModel.ConnectSettings.ConnectConfig); + if (!ret) + { + continue; + } + + SettingsViewModel.ConnectSettings.ConnectAddress = address; + break; + } + } + else { Execute.OnUIThreadAsync( () => { - MessageBoxHelper.Show(LocalizationHelper.GetString("ResourceBroken"), LocalizationHelper.GetString("Error"), iconKey: ResourceToken.FatalGeometry, iconBrushKey: ResourceToken.DangerBrush); - Bootstrapper.Shutdown(); + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("AutoDetectConnectionNotSupported"), UiLogColor.Error); }); } - - _runningState.SetInit(true); - AsstSetInstanceOption(InstanceOptionKey.TouchMode, SettingsViewModel.ConnectSettings.TouchMode); - AsstSetInstanceOption(InstanceOptionKey.DeploymentWithPause, SettingsViewModel.GameSettings.DeploymentWithPause ? "1" : "0"); - AsstSetInstanceOption(InstanceOptionKey.AdbLiteEnabled, SettingsViewModel.ConnectSettings.AdbLiteEnabled ? "1" : "0"); - - // TODO: 之后把这个 OnUIThread 拆出来 - // ReSharper disable once AsyncVoidLambda - Execute.OnUIThread( - async () => - { - if (SettingsViewModel.StartSettings.RunDirectly) - { - // 如果是直接运行模式,就先让按钮显示为运行 - _runningState.SetIdle(false); - } - - await Task.Run(() => SettingsViewModel.StartSettings.TryToStartEmulator(true)); - - // 一般是点了“停止”按钮了 - if (_runningState.GetStopping()) - { - Instances.TaskQueueViewModel.SetStopped(); - return; - } - - // ReSharper disable once InvertIf - if (SettingsViewModel.StartSettings.RunDirectly) - { - // 重置按钮状态,不影响LinkStart判断 - _runningState.SetIdle(true); - await Instances.TaskQueueViewModel.LinkStart(); - } - }); } - /// - /// Determines the length of the specified string (not including the terminating null character). - /// - /// The null-terminated string to be checked. - /// - /// The function returns the length of the string, in characters. - /// If is , the function returns 0. - /// - [DllImport("ucrtbase.dll", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] - internal static extern int strlen(AsstHandle ptr); - - private static string? PtrToStringCustom(AsstHandle ptr, Encoding enc) + if (!ret) { - if (ptr == AsstHandle.Zero) - { - return null; - } - - int len = strlen(ptr); - - if (len == 0) - { - return string.Empty; - } - - byte[] bytes = new byte[len]; - Marshal.Copy(ptr, bytes, 0, len); - return enc.GetString(bytes); + error = LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("CheckSettings"); + } + else if (SettingsViewModel.ConnectSettings.AutoDetectConnection && !SettingsViewModel.ConnectSettings.AlwaysAutoDetectConnection) + { + SettingsViewModel.ConnectSettings.AutoDetectConnection = false; } - private void CallbackFunction(int msg, AsstHandle jsonBuffer, AsstHandle customArg) + return ret; + } + + private static bool AutoDetectConnection(ref string error) + { + var adbPath = SettingsViewModel.ConnectSettings.AdbPath; + bool adbResult = !string.IsNullOrEmpty(adbPath) && + File.Exists(adbPath) && + Path.GetFileName(adbPath).Contains("adb", StringComparison.InvariantCultureIgnoreCase) && + CheckConnection(adbPath, SettingsViewModel.ConnectSettings.ConnectAddress); + + if (adbResult) { - var jsonStr = PtrToStringCustom(jsonBuffer, Encoding.UTF8); - - // Console.WriteLine(json_str); - var json = (JObject?)JsonConvert.DeserializeObject(jsonStr ?? string.Empty); - MaaService.ProcCallbackMsg dlg = ProcMsg; - Execute.OnUIThread( - () => - { - dlg((AsstMsg)msg, json); - }); - } - - private AsstHandle _handle; - - private void ProcMsg(AsstMsg msg, JObject details) - { - switch (msg) - { - case AsstMsg.InternalError: - break; - - case AsstMsg.InitFailed: - MessageBoxHelper.Show(LocalizationHelper.GetString("InitializationError"), LocalizationHelper.GetString("Error"), iconKey: ResourceToken.FatalGeometry, iconBrushKey: ResourceToken.DangerBrush); - Bootstrapper.Shutdown(); - break; - - case AsstMsg.ConnectionInfo: - ProcConnectInfo(details); - break; - - case AsstMsg.TaskChainStart: - Instances.TaskQueueViewModel.Running = true; - goto case AsstMsg.TaskChainExtraInfo; // fallthrough - case AsstMsg.AllTasksCompleted: - case AsstMsg.AsyncCallInfo: - case AsstMsg.Destroyed: - case AsstMsg.TaskChainError: - case AsstMsg.TaskChainCompleted: - case AsstMsg.TaskChainStopped: - Instances.TaskQueueViewModel.Running = false; - goto case AsstMsg.TaskChainExtraInfo; // fallthrough - case AsstMsg.TaskChainExtraInfo: - ProcTaskChainMsg(msg, details); - break; - - case AsstMsg.SubTaskError: - case AsstMsg.SubTaskStart: - case AsstMsg.SubTaskCompleted: - case AsstMsg.SubTaskExtraInfo: - ProcSubTaskMsg(msg, details); - TaskQueueViewModel.InvokeProcSubTaskMsg(msg, details); - break; - - case AsstMsg.SubTaskStopped: - break; - - case AsstMsg.ReportRequest: - _ = ProcReportRequest(details); - break; - - default: - throw new ArgumentOutOfRangeException(nameof(msg), msg, null); - } - } - - public bool Connected { get; set; } - - private string _connectedAdb = string.Empty; - private string _connectedAddress = string.Empty; - - private void ProcConnectInfo(JObject details) - { - var what = details["what"]?.ToString() ?? string.Empty; - switch (what) - { - case "Connected": - Connected = true; - _connectedAdb = details["details"]!["adb"]!.ToString(); - _connectedAddress = details["details"]!["address"]!.ToString(); - SettingsViewModel.ConnectSettings.ConnectAddress = _connectedAddress; - break; - - case "UnsupportedResolution": - Connected = false; - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ResolutionNotSupported"), UiLogColor.Error); - break; - - case "ResolutionError": - Connected = false; - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ResolutionAcquisitionFailure"), UiLogColor.Error); - break; - - case "Reconnecting": - Instances.TaskQueueViewModel.AddLog($"{LocalizationHelper.GetString("TryToReconnect")} ({Convert.ToUInt32(details!["details"]!["times"]) + 1})", UiLogColor.Error); - break; - - case "Reconnected": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ReconnectSuccess")); - break; - - case "Disconnect": - Connected = false; - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ReconnectFailed"), UiLogColor.Error); - if (_runningState.GetIdle()) - { - break; - } - - if (!AsstStop()) - { - _logger.Warning("Failed to stop Asst"); - } - - break; - - case "ScreencapFailed": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ScreencapFailed"), UiLogColor.Error); - break; - - case "TouchModeNotAvailable": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("TouchModeNotAvailable"), UiLogColor.Error); - Connected = false; - break; - - case "FastestWayToScreencap": - { - string costString = details["details"]?["cost"]?.ToString() ?? "???"; - string method = details["details"]?["method"]?.ToString() ?? "???"; - SettingsViewModel.ConnectSettings.ScreencapMethod = method; - - List<(string Method, string Cost)>? screencapAlternatives = null; - var alternativesToken = details["details"]?["alternatives"]; - if (alternativesToken is JArray { Count: > 1 } arr) - { - screencapAlternatives = arr.Select(item => - { - string method1 = item?["method"]?.ToString() ?? "???"; - string cost1 = item?["cost"]?.ToString() ?? "???"; - return (method1, cost1); - }).ToList(); - } - - StringBuilder fastestScreencapStringBuilder = new(); - string color = UiLogColor.Trace; - if (int.TryParse(costString, out var timeCost)) - { - switch (timeCost) - { - case > 800: - costString = timeCost.ToString("#,#"); - color = UiLogColor.Warning; - break; - - case > 400: - color = UiLogColor.Warning; - break; - } - } - else - { - color = UiLogColor.Error; - } - - var needToStop = false; - switch (SettingsViewModel.ConnectSettings.ConnectConfig) - { - case "MuMuEmulator12": - if (!SettingsViewModel.ConnectSettings.MuMuEmulator12Extras.Enable) - { - break; - } - - if (method != "MumuExtras") - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("MuMuExtrasNotEnabledMessage"), UiLogColor.Error); - Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("MuMuExtrasNotEnabledMessage"), UiLogColor.Error, showTime: false); - needToStop = true; - } - else if (timeCost < 100) - { - color = UiLogColor.MuMuSpecialScreenshot; - } - - break; - - case "LDPlayer": - if (!SettingsViewModel.ConnectSettings.LdPlayerExtras.Enable) - { - break; - } - - if (method != "LDExtras") - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("LdExtrasNotEnabledMessage"), UiLogColor.Error); - Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("LdExtrasNotEnabledMessage"), UiLogColor.Error, showTime: false); - needToStop = true; - } - else if (timeCost < 100) - { - color = UiLogColor.LdSpecialScreenshot; - } - - break; - } - - fastestScreencapStringBuilder.Insert(0, string.Format(LocalizationHelper.GetString("FastestWayToScreencap"), costString, method)); - var fastestScreencapString = fastestScreencapStringBuilder.ToString(); - SettingsViewModel.ConnectSettings.ScreencapTestCost = fastestScreencapString; - Instances.TaskQueueViewModel.AddLog(fastestScreencapString, color, toolTip: screencapAlternatives.CreateScreencapTooltip()); - Instances.CopilotViewModel.AddLog(fastestScreencapString, color, showTime: false); - - // 截图增强未生效禁止启动 - if (needToStop) - { - Execute.OnUIThreadAsync(async () => - { - Connected = false; - await Instances.TaskQueueViewModel.Stop(); - Instances.TaskQueueViewModel.SetStopped(); - }); - } - } - - break; - - case "ScreencapCost": - var screencapCostMin = details["details"]?["min"]?.ToString() ?? "???"; - var screencapCostAvg = details["details"]?["avg"]?.ToString() ?? "???"; - var screencapCostMax = details["details"]?["max"]?.ToString() ?? "???"; - var currentTime = DateTimeOffset.Now.ToString("HH:mm:ss"); - SettingsViewModel.ConnectSettings.ScreencapCost = string.Format(LocalizationHelper.GetString("ScreencapCost"), screencapCostMin, screencapCostAvg, screencapCostMax, currentTime); - if (!HasPrintedScreencapWarning && int.TryParse(screencapCostAvg, out var screencapCostAvgInt)) - { - static void AddLog(string message, string color) - { - Instances.TaskQueueViewModel.AddLog(message, color); - Instances.CopilotViewModel.AddLog(message, color, showTime: false); - HasPrintedScreencapWarning = true; - } - - switch (screencapCostAvgInt) - { - // 日志提示 - case >= 800: - AddLog(string.Format(LocalizationHelper.GetString("FastestWayToScreencapErrorTip"), screencapCostAvgInt), UiLogColor.Warning); - AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge1); - break; - - case >= 400: - AddLog(string.Format(LocalizationHelper.GetString("FastestWayToScreencapWarningTip"), screencapCostAvgInt), UiLogColor.Warning); - AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge2); - break; - - default: - { - AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge3); - - if (screencapCostAvgInt < 100) - { - AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge4); - } - - if (screencapCostAvgInt < 10) - { - AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge5); - } - - if (screencapCostAvgInt < 5) - { - AchievementTrackerHelper.Instance.Unlock(AchievementIds.SnapshotChallenge6); - } - - break; - } - } - } - - break; - } - } - - private DispatcherTimer? _toastNotificationTimer; - - private void OnToastNotificationTimerTick(object? sender, EventArgs e) - { - if (FightTask.SanityReport is not null) - { - var sanityReport = LocalizationHelper.GetString("SanityReport"); - var recoveryTime = FightTask.SanityReport.ReportTime.AddMinutes(FightTask.SanityReport.SanityCurrent < FightTask.SanityReport.SanityMax ? (FightTask.SanityReport.SanityMax - FightTask.SanityReport.SanityCurrent) * 6 : 0); - sanityReport = sanityReport.Replace("{DateTime}", recoveryTime.ToString("yyyy-MM-dd HH:mm")).Replace("{TimeDiff}", (recoveryTime - DateTimeOffset.Now).ToString(@"h\h\ m\m")); - ToastNotification.ShowDirect(sanityReport); - } - - DisposeTimer(); - } - - public void DisposeTimer() - { - if (_toastNotificationTimer is null) - { - return; - } - - _toastNotificationTimer.Stop(); - _toastNotificationTimer.Tick -= OnToastNotificationTimerTick; - _toastNotificationTimer = null; - } - - private void ProcTaskChainMsg(AsstMsg msg, JObject details) - { - string taskChain = details["taskchain"]?.ToString() ?? string.Empty; - AsstTaskId taskId = details["taskid"]?.ToObject() ?? 0; - switch (taskChain) - { - case "CloseDown": - return; - - case "Recruit": - { - if (msg == AsstMsg.TaskChainError) - { - Instances.ToolboxViewModel.RecruitInfo = LocalizationHelper.GetString("IdentifyTheMistakes"); - ToastNotification.ShowDirect(LocalizationHelper.GetString("IdentifyTheMistakes")); - } - - break; - } - } - - bool isCopilotTaskChain = taskChain is "Copilot" or "VideoRecognition"; - - switch (msg) - { - case AsstMsg.TaskChainStopped: - Instances.TaskQueueViewModel.SetStopped(); - TaskStatusUpdate(taskId, TaskStatus.Completed); - _tasksStatus.Clear(); - break; - - case AsstMsg.TaskChainError: - { - // 对剿灭的特殊处理,如果刷完了剿灭还选了剿灭会因为找不到入口报错 - TaskStatusUpdate(taskId, TaskStatus.Completed); - _tasksStatus.TryGetValue(taskId, out var value); - if (value is { Type: TaskType.Fight } && - TaskQueueViewModel.FightTask.Stage == "Annihilation" && - TaskQueueViewModel.FightTask.UseAlternateStage && - TaskQueueViewModel.FightTask.Stages.Any(stage => - Instances.TaskQueueViewModel.IsStageOpen(stage ?? string.Empty) && - stage != "Annihilation")) - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("AnnihilationTaskFailed"), UiLogColor.Warning); - } - else if (value is { Type: TaskType.Copilot } or { Type: TaskType.VideoRec }) - { - Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("CombatError"), UiLogColor.Error); - AchievementTrackerHelper.Instance.Unlock(AchievementIds.CopilotError); - } - else - { - var log = LocalizationHelper.GetString("TaskError") + LocalizationHelper.GetString(taskChain); - Instances.TaskQueueViewModel.AddLog(log, UiLogColor.Error); - ToastNotification.ShowDirect(log); - - if (SettingsViewModel.ExternalNotificationSettings.ExternalNotificationSendWhenError) - { - ExternalNotificationService.Send(log, log); - } - } - - break; - } - - case AsstMsg.TaskChainStart: - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StartTask") + LocalizationHelper.GetString(taskChain)); - TaskStatusUpdate(taskId, TaskStatus.InProgress); - break; - } - - case AsstMsg.TaskChainCompleted: - { - // 判断 _latestTaskId 中是否有元素的值和 details["taskid"] 相等,如果有再判断这个 id 对应的任务是否在 _mainTaskTypes 中 - TaskStatusUpdate(taskId, TaskStatus.Completed); - if (_tasksStatus.TryGetValue(taskId, out var task)) - { - if (_mainTaskTypes.Contains(task.Type)) - { - Instances.TaskQueueViewModel.UpdateMainTasksProgress(); - } - } - - switch (taskChain) - { - case "Infrast": - InfrastSettingsUserControlModel.Instance.IncreaseCustomInfrastPlanIndex(); - InfrastSettingsUserControlModel.Instance.RefreshCustomInfrastPlanIndexByPeriod(); - break; - } - - if (taskChain == "Fight" && FightTask.SanityReport is not null) - { - var sanityLog = "\n" + string.Format(LocalizationHelper.GetString("CurrentSanity"), FightTask.SanityReport.SanityCurrent, FightTask.SanityReport.SanityMax); - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CompleteTask") + LocalizationHelper.GetString(taskChain) + sanityLog); - } - else - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CompleteTask") + LocalizationHelper.GetString(taskChain)); - } - - if (isCopilotTaskChain) - { - AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.UseCopilotGroup); - } - - break; - } - - case AsstMsg.TaskChainExtraInfo: - { - var what = details["what"]?.ToString(); - var why = details["why"]?.ToString(); - - switch (what) - { - case "RoutingRestart": - string msgText = string.Empty; - switch (why) - { - case "TooManyBattlesAhead": - var cost = details["node_cost"]?.ToString() ?? "?"; - msgText = string.Format(LocalizationHelper.GetString("RoutingRestartTooManyBattles"), cost); - break; - } - - Instances.TaskQueueViewModel.AddLog(msgText, UiLogColor.Warning); - break; - } - - break; - } - - case AsstMsg.AllTasksCompleted: - bool isMainTaskQueueAllCompleted = false; - var taskList = details["finished_tasks"]?.ToObject(); - if (taskList?.Length > 0) - { - var latestMainTaskIds = _tasksStatus.Where(i => _mainTaskTypes.Contains(i.Value.Type)).Select(i => i.Key); - isMainTaskQueueAllCompleted = taskList.Any(i => latestMainTaskIds.Contains(i)); - } - - if (_tasksStatus.Any(t => t.Value.Type == TaskType.Copilot)) - { - if (SettingsViewModel.GameSettings.CopilotWithScript) - { - Task.Run(() => SettingsViewModel.GameSettings.RunScript("EndsWithScript", showLog: false)); - if (!string.IsNullOrWhiteSpace(SettingsViewModel.GameSettings.EndsWithScript)) - { - Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("EndsWithScript")); - } - } - } - - bool buyWine = _tasksStatus.Any(t => t.Value.Type == TaskType.Mall) && Instances.SettingsViewModel.DidYouBuyWine(); - _tasksStatus.Clear(); - - Instances.TaskQueueViewModel.ResetAllTemporaryVariable(); - _runningState.SetIdle(true); - - if (isMainTaskQueueAllCompleted) - { - var dateTimeNow = DateTimeOffset.Now; - var diffTaskTime = (dateTimeNow - StartTaskTime).ToString(@"h\h\ m\m\ s\s"); - - var allTaskCompleteTitle = string.Format(LocalizationHelper.GetString("AllTasksComplete"), diffTaskTime); - var allTaskCompleteMessage = LocalizationHelper.GetString("AllTaskCompleteContent"); - var sanityReport = LocalizationHelper.GetString("SanityReport"); - - var configurationPreset = ConfigurationHelper.GetCurrentConfiguration(); - - allTaskCompleteMessage = allTaskCompleteMessage - .Replace("{DateTime}", dateTimeNow.ToString("yyyy-MM-dd HH:mm:ss")) - .Replace("{Preset}", configurationPreset) - .Replace("{TimeDiff}", diffTaskTime); - - var allTaskCompleteLog = string.Format(LocalizationHelper.GetString("AllTasksComplete"), diffTaskTime); - - if (FightTask.SanityReport is not null) - { - var recoveryTime = FightTask.SanityReport.ReportTime.AddMinutes(FightTask.SanityReport.SanityCurrent < FightTask.SanityReport.SanityMax ? (FightTask.SanityReport.SanityMax - FightTask.SanityReport.SanityCurrent) * 6 : 0); - sanityReport = sanityReport.Replace("{DateTime}", recoveryTime.ToString("yyyy-MM-dd HH:mm")).Replace("{TimeDiff}", (recoveryTime - DateTimeOffset.Now).ToString(@"h\h\ m\m")); - - allTaskCompleteLog = allTaskCompleteLog + Environment.NewLine + sanityReport; - Instances.TaskQueueViewModel.AddLog(allTaskCompleteLog); - - if (SettingsViewModel.ExternalNotificationSettings.ExternalNotificationSendWhenComplete) - { - var logs = SettingsViewModel.ExternalNotificationSettings.ExternalNotificationEnableDetails - ? Instances.TaskQueueViewModel.LogItemViewModels.Aggregate(string.Empty, (current, logItem) => current + $"[{logItem.Time}][{logItem.Color}]{logItem.Content}\n") - : string.Empty; - logs += allTaskCompleteMessage; - - ExternalNotificationService.Send(allTaskCompleteTitle, logs + Environment.NewLine + sanityReport); - } - - if (_toastNotificationTimer is not null) - { - DisposeTimer(); - } - - var interval = recoveryTime - DateTimeOffset.Now.AddMinutes(6); - if (interval > TimeSpan.Zero) - { - _toastNotificationTimer = new DispatcherTimer - { - Interval = interval, - }; - _toastNotificationTimer.Tick += OnToastNotificationTimerTick; - _toastNotificationTimer.Start(); - } - } - else - { - Instances.TaskQueueViewModel.AddLog(allTaskCompleteLog); - - if (SettingsViewModel.ExternalNotificationSettings.ExternalNotificationSendWhenComplete) - { - var logs = SettingsViewModel.ExternalNotificationSettings.ExternalNotificationEnableDetails - ? Instances.TaskQueueViewModel.LogItemViewModels.Aggregate(string.Empty, (current, logItem) => current + $"[{logItem.Time}][{logItem.Color}]{logItem.Content}\n") - : string.Empty; - logs += allTaskCompleteMessage; - - ExternalNotificationService.Send(allTaskCompleteTitle, logs); - } - } - - using (var toast = new ToastNotification(allTaskCompleteTitle)) - { - if (FightTask.SanityReport is not null) - { - toast.AppendContentText(sanityReport); - } - - toast.Show(); - } - - if (DateTime.UtcNow.ToYjDate().IsAprilFoolsDay()) - { - if (Application.Current.MainWindow?.DataContext is RootViewModel rvm) - { - rvm.GifVisibility = true; - rvm.ChangeGif(); - } - } - - // Instances.TaskQueueViewModel.CheckAndShutdown(); - _ = Instances.TaskQueueViewModel.CheckAfterCompleted(); - } - else if (isCopilotTaskChain) - { - ToastNotification.ShowDirect(LocalizationHelper.GetString("CompleteTask") + LocalizationHelper.GetString(taskChain)); - } - - if (buyWine) - { - Instances.SettingsViewModel.LastBuyWineTime = DateTime.UtcNow.ToYjDate().ToFormattedString(); - var result = MessageBoxHelper.Show( - LocalizationHelper.GetString("DrunkAndStaggering"), - LocalizationHelper.GetString("Burping"), - iconKey: "DrunkAndStaggeringGeometry", - iconBrushKey: "PallasBrush"); - if (result == MessageBoxResult.OK) - { - Instances.SettingsViewModel.Cheers = true; - Bootstrapper.ShutdownAndRestartWithoutArgs(); - } - } - - break; - - case AsstMsg.InternalError: - break; - - case AsstMsg.InitFailed: - break; - - case AsstMsg.ConnectionInfo: - break; - - case AsstMsg.AsyncCallInfo: - break; - - case AsstMsg.Destroyed: - break; - - case AsstMsg.SubTaskError: - break; - - case AsstMsg.SubTaskStart: - break; - - case AsstMsg.SubTaskCompleted: - break; - - case AsstMsg.SubTaskExtraInfo: - break; - - case AsstMsg.SubTaskStopped: - break; - - default: - throw new ArgumentOutOfRangeException(nameof(msg), msg, null); - } - } - - private static void ProcSubTaskMsg(AsstMsg msg, JObject details) - { - // 下面几行注释暂时没用到,先注释起来... - // string taskChain = details["taskchain"].ToString(); - // string classType = details["class"].ToString(); - switch (msg) - { - case AsstMsg.SubTaskError: - ProcSubTaskError(details); - break; - - case AsstMsg.SubTaskStart: - ProcSubTaskStart(details); - break; - - case AsstMsg.SubTaskCompleted: - ProcSubTaskCompleted(details); - break; - - case AsstMsg.SubTaskExtraInfo: - ProcSubTaskExtraInfo(details); - break; - - case AsstMsg.InternalError: - break; - - case AsstMsg.InitFailed: - break; - - case AsstMsg.ConnectionInfo: - break; - - case AsstMsg.AllTasksCompleted: - break; - - case AsstMsg.TaskChainError: - break; - - case AsstMsg.TaskChainStart: - break; - - case AsstMsg.TaskChainCompleted: - break; - - case AsstMsg.TaskChainExtraInfo: - break; - - case AsstMsg.TaskChainStopped: - break; - - case AsstMsg.SubTaskStopped: - break; - - default: - throw new ArgumentOutOfRangeException(nameof(msg), msg, null); - } - } - - private static void ProcSubTaskError(JObject details) - { - string subTask = details["subtask"]?.ToString() ?? string.Empty; - switch (subTask) - { - case "StartGameTask": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FailedToOpenClient"), UiLogColor.Error); - break; - - case "StopGameTask": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CloseArknightsFailed"), UiLogColor.Error); - break; - - case "AutoRecruitTask": - { - var whyStr = details.TryGetValue("why", out var why) ? why.ToString() : LocalizationHelper.GetString("ErrorOccurred"); - Instances.TaskQueueViewModel.AddLog(whyStr + ", " + LocalizationHelper.GetString("HasReturned"), UiLogColor.Error); - break; - } - - case "RecognizeDrops": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("DropRecognitionError"), UiLogColor.Error); - break; - - case "ReportToPenguinStats": - { - var why = details["why"]!.ToString(); - Instances.TaskQueueViewModel.AddLog(why + ", " + LocalizationHelper.GetString("GiveUpUploadingPenguins"), UiLogColor.Warning); - break; - } - - case "CheckStageValid": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("TheEx"), UiLogColor.Error); - break; - - case "BattleFormationTask": - { - var why = details.TryGetValue("why", out var whyObj) ? whyObj.ToString() : string.Empty; - if (why == "OperatorMissing") - { - var missingOpers = details["details"]?["opers"]?.ToObject>(); - if (missingOpers is not null && missingOpers.Count > 0) - { - var str = new StringBuilder(); - str.AppendLine(); - foreach (var (groupName, opers) in missingOpers) - { - if (opers.Count == 1) - { - str.AppendLine($"{groupName}"); - continue; - } - else - { - var operList = opers.Cast().ToList(); // 确保 opers 是动态类型 - str.AppendLine($"{groupName}=> {string.Join(" / ", operList.Select(i => i.name).ToList())}"); - } - } - - Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("MissingOperators") + str.ToString().TrimEnd(), UiLogColor.Error); - } - else - { - Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("MissingOperators"), UiLogColor.Error); - } - - /* - if (missingOpers is not null && missingOpers.Count >= 2) - { - AchievementTrackerHelper.Instance.Unlock(AchievementIds.Irreplaceable); - }*/ - } - - break; - } - } - } - - private static void ProcSubTaskStart(JObject details) - { - string subTask = details["subtask"]?.ToString() ?? string.Empty; - switch (subTask) - { - case "ProcessTask": - { - string taskName = details!["details"]!["task"]!.ToString(); - int execTimes = (int)details!["details"]!["exec_times"]!; - - switch (taskName) - { - case "StartButton2": - case "AnnihilationConfirm": - StringBuilder missionStartLogBuilder = new(); - if (FightTask.FightReport is null) - { - missionStartLogBuilder.AppendLine(string.Format(LocalizationHelper.GetString("MissionStart.FightTask"), "???", "???")); - } - else - { - var times = FightTask.FightReport.Series == 1 ? $"{FightTask.FightReport.TimesFinished + 1}" : $"{FightTask.FightReport.TimesFinished + 1}~{FightTask.FightReport.TimesFinished + FightTask.FightReport.Series}"; - missionStartLogBuilder.AppendLine(string.Format(LocalizationHelper.GetString("MissionStart.FightTask"), times, FightTask.FightReport.SanityCost)); - } - - if (FightTask.SanityReport is not null) - { - missionStartLogBuilder.AppendFormat(LocalizationHelper.GetString("CurrentSanity"), FightTask.SanityReport.SanityCurrent, FightTask.SanityReport.SanityMax); - } - - if (ExpiringMedicineUsedTimes > 0) - { - missionStartLogBuilder.AppendFormat(LocalizationHelper.GetString("MedicineUsedTimesWithExpiring"), MedicineUsedTimes, ExpiringMedicineUsedTimes); - } - else if (MedicineUsedTimes > 0) - { - missionStartLogBuilder.AppendFormat(LocalizationHelper.GetString("MedicineUsedTimes"), MedicineUsedTimes); - } - - if (StoneUsedTimes > 0) - { - missionStartLogBuilder.AppendFormat(LocalizationHelper.GetString("StoneUsedTimes"), StoneUsedTimes); - } - - Instances.TaskQueueViewModel.AddLog(missionStartLogBuilder.ToString().TrimEnd(), UiLogColor.Info); - break; - - case "StoneConfirm": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StoneUsed") + $" {execTimes} " + LocalizationHelper.GetString("UnitTime"), UiLogColor.Info); - StoneUsedTimes++; - break; - - case "AbandonAction": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ActingCommandError"), UiLogColor.Error); - break; - - case "RecruitRefreshConfirm": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("LabelsRefreshed"), UiLogColor.Info); - break; - - case "RecruitConfirm": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RecruitConfirm"), UiLogColor.Info); - break; - - case "InfrastDormDoubleConfirmButton": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("InfrastDormDoubleConfirmed"), UiLogColor.Error); - break; - - /* 肉鸽相关 */ - case "ExitThenAbandon": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ExplorationAbandoned"), UiLogColor.ExplorationAbandonedIS); - AchievementTrackerHelper.Instance.AddProgress(AchievementIds.RoguelikeRetreat); - break; - - // case "StartAction": - // Instances.TaskQueueViewModel.AddLog("开始战斗"); - // break; - case "MissionCompletedFlag": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FightCompleted"), UiLogColor.SuccessIS); - break; - - case "MissionFailedFlag": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FightFailed"), UiLogColor.Error); - break; - - case "StageTrader": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("Trader"), UiLogColor.TraderIS); - break; - - case "StageSafeHouse": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("SafeHouse"), UiLogColor.SafehouseIS); - break; - - case "StageFilterTruth": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FilterTruth"), UiLogColor.TruthIS); - break; - - // case "StageBoonsEnter": - // Instances.TaskQueueViewModel.AddLog("古堡馈赠"); - // break; - case "StageCombatDps": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CombatDps"), UiLogColor.CombatIS); - break; - - case "StageEmergencyDps": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("EmergencyDps"), UiLogColor.EmergencyIS); - break; - - case "StageDreadfulFoe": - case "StageDreadfulFoe-5": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("DreadfulFoe"), UiLogColor.BossIS); - break; - - case "StageTraderInvestSystemFull": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("UpperLimit"), UiLogColor.Info); - break; - - case "OfflineConfirm": - if (TaskQueueViewModel.FightTask.AutoRestartOnDrop) - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("GameDrop"), UiLogColor.Warning); - } - else - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("GameDropNoRestart"), UiLogColor.Warning); - ToastNotification.ShowDirect(LocalizationHelper.GetString("GameDropNoRestart")); - _ = Instances.TaskQueueViewModel.Stop(); - } - - break; - - case "GamePass": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoguelikeGamePass"), UiLogColor.RareOperator); - AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.RoguelikeGamePassGroup); - break; - - case "BattleStartAll": - Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("MissionStart"), UiLogColor.Info); - break; - - case "StageDrops-Stars-3": - case "StageDrops-Stars-Adverse": - { - Instances.CopilotViewModel.CopilotTaskSuccess(); - Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("CompleteCombat"), UiLogColor.Info); - break; - } - - case "StageTraderSpecialShoppingAfterRefresh": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoguelikeSpecialItemBought"), UiLogColor.RareOperator); - break; - - case "DeepExplorationNotUnlockedComplain": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("DeepExplorationNotUnlockedComplain"), UiLogColor.Warning); - break; - - case "PNS-Resume": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ReclamationPnsModeError"), UiLogColor.Error); - break; - - case "PIS-Commence": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ReclamationPisModeError"), UiLogColor.Error); - break; - } - - break; - } - - case "CombatRecordRecognitionTask": - { - var what = details["what"]?.ToString(); - if (!string.IsNullOrEmpty(what)) - { - Instances.CopilotViewModel.AddLog(what); - } - - break; - } - } - } - - private static void ProcSubTaskCompleted(JObject details) - { - string subTask = details["subtask"]?.ToString() ?? string.Empty; - switch (subTask) - { - case "ProcessTask": - var taskchain = details["taskchain"]?.ToString(); - switch (taskchain) - { - case "Roguelike": - { - var taskName = details!["details"]!["task"]!.ToString(); - int execTimes = (int)details!["details"]!["exec_times"]!; - - if (taskName == "StartExplore") - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("BegunToExplore") + $" {execTimes} " + LocalizationHelper.GetString("UnitTime"), UiLogColor.Info); - } - - break; - } - - case "Mall": - { - var taskName = details["details"]!["task"]!.ToString(); - switch (taskName) - { - case "EndOfActionThenStop": - TaskQueueViewModel.MallTask.LastCreditFightTaskTime = DateTime.UtcNow.ToYjDate().ToFormattedString(); - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CompleteTask") + LocalizationHelper.GetString("CreditFight")); - AchievementTrackerHelper.Instance.AddProgress(AchievementIds.MosquitoLeg); - break; - - case "VisitLimited" or "VisitNextBlack": - TaskQueueViewModel.MallTask.LastCreditVisitFriendsTime = DateTime.UtcNow.ToYjDate().ToFormattedString(); - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CompleteTask") + LocalizationHelper.GetString("Visiting")); - break; - } - - break; - } - } - - break; - } - } - - private static void ProcSubTaskExtraInfo(JObject details) - { - string taskChain = details["taskchain"]?.ToString() ?? string.Empty; - switch (taskChain) - { - case "Recruit": - ProcRecruitCalcMsg(details); - break; - - case "VideoRecognition": - ProcVideoRecMsg(details); - break; - } - - var subTaskDetails = details["details"]; - switch (taskChain) - { - case "Depot": - Instances.ToolboxViewModel.DepotParse((JObject?)subTaskDetails); - break; - - case "OperBox": - Instances.ToolboxViewModel.OperBoxParse((JObject?)subTaskDetails); - break; - } - - string what = details["what"]?.ToString() ?? string.Empty; - switch (what) - { - case "StageDrops": - { - string allDrops = string.Empty; - var statistics = subTaskDetails!["stats"] ?? new JArray(); - var stageInfo = subTaskDetails!["stage"] ?? new JObject(); - int curTimes = (int)(subTaskDetails["cur_times"] ?? -1); - var drops = new List<(string ItemId, string ItemName, int Total, int Add)>(); - - foreach (var item in statistics) - { - var itemId = item["itemId"]?.ToString() ?? string.Empty; - var itemName = item["itemName"]?.ToString() ?? string.Empty; - if (itemName == "furni") - { - itemName = LocalizationHelper.GetString("FurnitureDrop"); - itemId = "3401"; - } - - int totalQuantity = (int)(item["quantity"] ?? -1); - int addQuantity = (int)(item["addQuantity"] ?? -1); - - drops.Add((itemId, itemName, totalQuantity, addQuantity)); - } - - // 先按新增数量降序,再按总数量降序 - drops = [.. drops.OrderByDescending(x => x.Add).ThenByDescending(x => x.Total)]; - - foreach (var (_, itemName, totalQuantity, addQuantity) in drops) - { - allDrops += $"{itemName} : {totalQuantity.FormatNumber(false)}"; - if (addQuantity > 0) - { - allDrops += $" (+{addQuantity.FormatNumber(false)})"; - } - - allDrops += "\n"; - } - - var stageCode = stageInfo["stageCode"]?.ToString(); - allDrops = allDrops.EndsWith('\n') ? allDrops.TrimEnd('\n') : LocalizationHelper.GetString("NoDrop"); - - var dropsForTooltip = drops.Where(x => !string.IsNullOrEmpty(x.ItemId)).ToList(); - - Instances.TaskQueueViewModel.AddLog( - $"{stageCode} {LocalizationHelper.GetString("TotalDrop")}\n" + - $"{allDrops}{(curTimes >= 0 - ? $"\n{LocalizationHelper.GetString("CurTimes")} : {curTimes}" - : string.Empty)}", - toolTip: dropsForTooltip.CreateMaterialDropTooltip()); - - AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.SanitySpenderGroup, curTimes > 0 ? curTimes : 1); - - break; - } - - case "EnterFacility": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ThisFacility") + - LocalizationHelper.GetString($"{subTaskDetails?["facility"]}") + " " + - ((int)(subTaskDetails?["index"] ?? -2) + 1).ToString("D2")); - break; - - case "ProductIncorrect": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ProductIncorrect"), UiLogColor.Error); - break; - - case "ProductUnknown": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ProductUnknown"), UiLogColor.Error); - break; - - case "ProductChanged": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ProductChanged"), UiLogColor.Info); - break; - - case "RecruitTagsDetected": - { - var tags = subTaskDetails!["tags"] ?? new JArray(); - string logContent = tags.Select(tagName => tagName.ToString()) - .Aggregate(string.Empty, (current, tagStr) => current + (tagStr + "\n")); - - logContent = logContent.EndsWith('\n') ? logContent.TrimEnd('\n') : LocalizationHelper.GetString("Error"); - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RecruitingResults") + "\n" + logContent); - - break; - } - - case "RecruitSpecialTag": - { - string special = subTaskDetails!["tag"]!.ToString(); - if (special == "支援机械" && TaskQueueViewModel.RecruitTask.NotChooseLevel1 == false) - { - break; - } - - using var toast = new ToastNotification(LocalizationHelper.GetString("RecruitingTips")); - toast.AppendContentText(special).ShowRecruit(); - - break; - } - - case "RecruitRobotTag": - { - string special = subTaskDetails!["tag"]!.ToString(); - using var toast = new ToastNotification(LocalizationHelper.GetString("RecruitingTips")); - toast.AppendContentText(special).ShowRecruitRobot(); - - break; - } - - case "RecruitResult": - { - int level = (int)subTaskDetails!["level"]!; - var tooltip = Instances.ToolboxViewModel.RecruitResultInlines.CreateTooltip(PlacementMode.Center); - if (level >= 5) - { - using (var toast = new ToastNotification(string.Format(LocalizationHelper.GetString("RecruitmentOfStar"), level))) - { - toast.AppendContentText(new string('★', level)).ShowRecruit(row: 2); - } - - Instances.TaskQueueViewModel.AddLog(level + " ★ Tags", UiLogColor.RareOperator, "Bold", toolTip: tooltip); - } - else - { - Instances.TaskQueueViewModel.AddLog(level + " ★ Tags", UiLogColor.Info, toolTip: tooltip); - } - - if (level == 6) - { - AchievementTrackerHelper.Instance.SetProgress(AchievementIds.RecruitNoSixStarStreak, 1); - } - else - { - AchievementTrackerHelper.Instance.AddProgress(AchievementIds.RecruitNoSixStar); // 累计 - AchievementTrackerHelper.Instance.AddProgress(AchievementIds.RecruitNoSixStarStreak); // 连续 - } - - /* - bool robot = (bool)subTaskDetails["robot"]; - if (robot) - { - using (var toast = new ToastNotification(Localization.GetString("RecruitmentOfBot"))) - { - toast.AppendContentText(new string('★', 1)).ShowRecruitRobot(row: 2); - } - - Instances.TaskQueueViewModel.AddLog(1 + " ★ Tag", LogColor.RobotOperator, "Bold"); - } - */ - - break; - } - - case "RecruitSupportOperator": - { - var name = subTaskDetails!["name"]!.ToString(); - Instances.TaskQueueViewModel.AddLog(string.Format(LocalizationHelper.GetString("RecruitSupportOperator"), name), UiLogColor.Info); - break; - } - - case "RecruitTagsSelected": - { - var selected = subTaskDetails!["tags"] ?? new JArray(); - string selectedLog = selected.Aggregate(string.Empty, (current, tag) => current + (tag + "\n")); - - selectedLog = selectedLog.EndsWith('\n') ? selectedLog.TrimEnd('\n') : LocalizationHelper.GetString("NoDrop"); - - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("Choose") + " Tags:\n" + selectedLog); - - break; - } - - case "RecruitTagsRefreshed": - { - int refreshCount = (int)subTaskDetails!["count"]!; - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("Refreshed") + refreshCount + LocalizationHelper.GetString("UnitTime")); - break; - } - - case "RecruitNoPermit": - { - bool continueRefresh = (bool)subTaskDetails!["continue"]!; - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString(continueRefresh ? "ContinueRefresh" : "NoRecruitmentPermit")); - break; - } - - case "NotEnoughStaff": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("NotEnoughStaff"), UiLogColor.Error); - break; - - case "CreditFullOnlyBuyDiscount": - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CreditFullOnlyBuyDiscount") + subTaskDetails!["credit"], UiLogColor.Message); - break; - } - - /* Roguelike */ - case "StageInfo": - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StartCombat") + subTaskDetails!["name"]); - if (TaskQueueViewModel.RoguelikeTask.RoguelikeDelayAbortUntilCombatComplete) - { - Instances.TaskQueueViewModel.RoguelikeInCombatAndShowWait = true; - } - - break; - } - - case "StageInfoError": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StageInfoError"), UiLogColor.Error); - break; - - case "BattleFormation": - Instances.CopilotViewModel.AddLog( - LocalizationHelper.GetString("BattleFormation") + - "\n[" + - string.Join( - ", ", - (subTaskDetails!["formation"]?.ToObject>() ?? []) - .Select(oper => DataHelper.GetLocalizedCharacterName(oper) ?? oper) - .Where(oper => !string.IsNullOrEmpty(oper))) + "]"); - break; - - case "BattleFormationSelected": - { - var oper_name = DataHelper.GetLocalizedCharacterName(subTaskDetails!["selected"]?.ToString()); - var group_name = subTaskDetails!["group_name"]?.ToString(); - if (group_name is not null && oper_name != group_name) - { - oper_name = $"{group_name} => {oper_name}"; - } - - Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("BattleFormationSelected") + oper_name); - break; - } - - case "BattleFormationOperUnavailable": - { - var oper_name = DataHelper.GetLocalizedCharacterName(subTaskDetails!["oper_name"]?.ToString()); - var requirement_type = subTaskDetails["requirement_type"]?.ToString() == "module" - ? LocalizationHelper.GetString("BattleFormationModuleUnavailable") - : subTaskDetails["requirement_type"]?.ToString() ?? "UnknownRequirementType"; - - Instances.CopilotViewModel.AddLog(string.Format(LocalizationHelper.GetString("BattleFormationOperUnavailable"), oper_name, requirement_type), Instances.CopilotViewModel.IgnoreRequirements ? UiLogColor.Warning : UiLogColor.Error); - break; - } - - case "CopilotAction": - { - var doc = subTaskDetails!["doc"]?.ToString(); - if (!string.IsNullOrEmpty(doc)) - { - var color = subTaskDetails["doc_color"]?.ToString(); - Instances.CopilotViewModel.AddLog(doc, string.IsNullOrEmpty(color) ? UiLogColor.Message : color); - } - - var target = subTaskDetails["target"]?.ToString(); - var actionToken = subTaskDetails?["action"]; - var actionString = actionToken?.ToString() ?? "UnknownAction"; - Instances.CopilotViewModel.AddLog( - string.Format( - LocalizationHelper.GetString("CurrentSteps"), - LocalizationHelper.GetString(actionString), - DataHelper.GetLocalizedCharacterName(target) ?? target)); - - break; - } - - case "CopilotListLoadTaskFileSuccess": - Instances.CopilotViewModel.AddLog($"Parse {subTaskDetails!["file_name"]}[{subTaskDetails["stage_name"]}] Success"); - break; - - case "SSSStage": - Instances.CopilotViewModel.AddLog(string.Format(LocalizationHelper.GetString("CurrentStage"), subTaskDetails!["stage"]), UiLogColor.Info); - break; - - case "SSSSettlement": - Instances.CopilotViewModel.AddLog($"{details["why"]}", UiLogColor.Info); - break; - - case "SSSGamePass": - Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("SSSGamePass"), UiLogColor.RareOperator); - break; - - case "UnsupportedLevel": - Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("UnsupportedLevel") + subTaskDetails!["level"], UiLogColor.Error); - _ = ResourceUpdater.ResourceUpdateAndReloadAsync(); - break; - - case "CustomInfrastRoomGroupsMatch": - // 选用xxx组编组 - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoomGroupsMatch") + subTaskDetails!["group"]); - break; - - case "CustomInfrastRoomGroupsMatchFailed": - // 干员编组匹配失败 - var groups = (JArray?)subTaskDetails!["groups"]; - if (groups != null) - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoomGroupsMatchFailed") + string.Join(", ", groups)); - } - - break; - - case "CustomInfrastRoomOperators": - string nameStr = (subTaskDetails!["names"] ?? new JArray()) - .Aggregate(string.Empty, (current, name) => current + DataHelper.GetLocalizedCharacterName(name.ToString()) + ", "); - - if (nameStr != string.Empty) - { - nameStr = nameStr.Remove(nameStr.Length - 2); - } - - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RoomOperators") + nameStr); - break; - - case "InfrastTrainingIdle": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("TrainingIdle")); - break; - - case "InfrastTrainingCompleted": - { - var operatorName = DataHelper.GetLocalizedCharacterName(subTaskDetails!["operator"]?.ToString()) ?? "UnKnown"; - var skillName = subTaskDetails["skill"]?.ToString() ?? "UnKnown"; - Instances.TaskQueueViewModel.AddLog( - $"[{operatorName}] {skillName}\n" + - $"{LocalizationHelper.GetString("TrainingLevel")}: {(int)(subTaskDetails["level"] ?? -1)} {LocalizationHelper.GetString("TrainingCompleted")}", - UiLogColor.Info); - break; - } - - case "InfrastTrainingTimeLeft": - { - var operatorName = DataHelper.GetLocalizedCharacterName(subTaskDetails!["operator"]?.ToString()) ?? "UnKnown"; - var skillName = subTaskDetails["skill"]?.ToString() ?? "UnKnown"; - Instances.TaskQueueViewModel.AddLog( - $"[{operatorName}] {skillName}\n" + - $"{LocalizationHelper.GetString("TrainingLevel")}: {(int)(subTaskDetails["level"] ?? -1)}\n" + - $"{LocalizationHelper.GetString("TrainingTimeLeft")}: {subTaskDetails["time"]}", - UiLogColor.Info); - break; - } - - /* 生息演算 */ - case "ReclamationReport": - Instances.TaskQueueViewModel.AddLog( - LocalizationHelper.GetString("AlgorithmFinish") + "\n" + - LocalizationHelper.GetString("AlgorithmBadge") + ": " + $"{(int)(subTaskDetails!["total_badges"] ?? -1)}(+{(int)(subTaskDetails["badges"] ?? -1)})" + "\n" + - LocalizationHelper.GetString("AlgorithmConstructionPoint") + ": " + $"{(int)(subTaskDetails["total_construction_points"] ?? -1)}(+{(int)(subTaskDetails["construction_points"] ?? -1)})"); - break; - - case "ReclamationProcedureStart": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("MissionStart") + $" {(int)(subTaskDetails!["times"] ?? -1)} " + LocalizationHelper.GetString("UnitTime"), UiLogColor.Info); - break; - - case "ReclamationSmeltGold": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("AlgorithmDoneSmeltGold") + $" {(int)(subTaskDetails!["times"] ?? -1)} " + LocalizationHelper.GetString("UnitTime")); - break; - - case "SanityBeforeStage": - { - FightTask.SanityReport = null; - if (subTaskDetails?.ToObject() is { SanityMax: > 0 } report) - { - FightTask.SanityReport = report; - } - - break; - } - - case "FightTimes": - { - FightTask.FightReport = null; - if ((subTaskDetails?.Children())?.Any() is true) - { - FightTask.FightReport = subTaskDetails.ToObject()!; - if (FightTask.FightReport.TimesFinished > 0) - { - AchievementTrackerHelper.Instance.SetProgress(AchievementIds.OverLimitAgent, FightTask.FightReport.TimesFinished); - } - - if (FightTask.Instance.HasTimesLimited != false && FightTask.FightReport.TimesFinished + FightTask.FightReport.Series > FightTask.Instance.MaxTimes) - { - Instances.TaskQueueViewModel.AddLog(string.Format(LocalizationHelper.GetString("FightTimesUnused"), FightTask.FightReport.TimesFinished, FightTask.FightReport.Series, FightTask.FightReport.TimesFinished + FightTask.FightReport.Series, FightTask.Instance.MaxTimes), UiLogColor.Error); - } - } - - break; - } - - case "UseMedicine": - var medicineReport = (JObject?)subTaskDetails; - if (medicineReport is null || !medicineReport.ContainsKey("is_expiring") || !medicineReport.ContainsKey("count")) - { - break; - } - - var isExpiringMedicine = medicineReport.TryGetValue("is_expiring", out var isExpiringMedicineToken) && (bool)isExpiringMedicineToken; - int medicineCount = medicineReport.TryGetValue("count", out var medicineCountToken) ? (int)medicineCountToken : -1; - - if (medicineCount == -1) - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("MedicineUsed") + " Unknown times", UiLogColor.Error); - break; - } - - string medicineLog; - if (!isExpiringMedicine) - { - MedicineUsedTimes += medicineCount; - medicineLog = LocalizationHelper.GetString("MedicineUsed") + $" {MedicineUsedTimes}(+{medicineCount})"; - AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.SanitySaverGroup, medicineCount); - } - else - { - ExpiringMedicineUsedTimes += medicineCount; - medicineLog = LocalizationHelper.GetString("ExpiringMedicineUsed") + $" {ExpiringMedicineUsedTimes}(+{medicineCount})"; - AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.SanitySaverGroup, medicineCount); - AchievementTrackerHelper.Instance.SetProgress(AchievementIds.SanityExpire, ExpiringMedicineUsedTimes); - } - - Instances.TaskQueueViewModel.AddLog(medicineLog, UiLogColor.Info); - break; - - case "StageQueueUnableToAgent": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StageQueue") + $" {subTaskDetails!["stage_code"]} " + LocalizationHelper.GetString("UnableToAgent"), UiLogColor.Info); - break; - - case "StageQueueMissionCompleted": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StageQueue") + $" {subTaskDetails!["stage_code"]} - {subTaskDetails["stars"]} ★", UiLogColor.Info); - break; - } - } - - private static void ProcRecruitCalcMsg(JObject details) - { - Instances.ToolboxViewModel.ProcRecruitMsg(details); - } - - private static void ProcVideoRecMsg(JObject details) - { - string what = details["what"]?.ToString() ?? string.Empty; - switch (what) - { - case "Finished": - var filename = details["details"]?["filename"]; - Instances.CopilotViewModel.AddLog("Save to: " + filename, UiLogColor.Info, showTime: false); - - // string p = @"C:\tmp\this path contains spaces, and,commas\target.txt"; - string args = $"/e, /select, \"{filename}\""; - - ProcessStartInfo info = new() - { - FileName = "explorer", - Arguments = args, - }; - Process.Start(info); - break; - } - } - - private static async Task ProcReportRequest(JObject details) - { - string? url = (string?)details["url"]; - if (string.IsNullOrEmpty(url)) - { - _logger.Error("Report request received with empty URL."); - return; - } - - var headersToken = details["headers"]; - Dictionary headers = []; - if (headersToken is JObject headersObj) - { - foreach (var prop in headersObj.Properties()) - { - headers[prop.Name] = prop.Value.ToString(); - } - } - - string? body = (string?)details["body"]; - if (string.IsNullOrEmpty(body)) - { - _logger.Error("Report request received with empty body."); - return; - } - - var content = new StringContent(body, Encoding.UTF8, "application/json"); - - string subTask = details["subtask"]?.ToString() ?? string.Empty; - - bool success = false; - try - { - success = await GameDataReportService.PostWithRetryAsync(url, content, headers, subTask, penguinId => - { - SettingsViewModel.GameSettings.PenguinId = penguinId; - _logger.Information("New PenguinId got: {PenguinId}", penguinId); - }); - } - catch (Exception ex) - { - _logger.Warning(ex, "Failed to report: {Url}", url); - } - - if (!success) - { - Instances.TaskQueueViewModel.AddLog("Failed to report, " + LocalizationHelper.GetString("GiveUpUploadingPenguins"), UiLogColor.Warning); - } - } - - public bool AsstSetInstanceOption(InstanceOptionKey key, string value) - { - return AsstSetInstanceOption(_handle, (AsstInstanceOptionKey)key, value); - } - - public bool AsstSetStaticOption(AsstStaticOptionKey key, string value) - { - return MaaService.AsstSetStaticOption(key, value); - } - - private static readonly bool _forcedReloadResource = File.Exists("DEBUG") || File.Exists("DEBUG.txt"); - - /// - /// 使用 TCP 或 adb devices 命令检查连接。TCP 检测相比 adb devices 更快,但不支持实体机。 - /// - /// adb path,用于实体机检测 - /// 连接地址 - /// 设备是否在线 - private static bool CheckConnection(string adbPath, string? address) - { - if (string.IsNullOrEmpty(address)) - { - return false; - } - - // 实体机可能设备名 -> [host] - if (!address.Contains(':') && !address.Contains('-')) - { - return WinAdapter.GetAdbAddresses(adbPath).Contains(address); - } - - // normal -> [host]:[port] - // LdPlayer -> emulator-[port] - string[] hostAndPort = address.Split([':', '-'], StringSplitOptions.RemoveEmptyEntries); - - if (hostAndPort.Length != 2 || !int.TryParse(hostAndPort[1], out var port)) - { - return false; - } - - string host = hostAndPort[0]; - if (host.StartsWith("emulator")) - { - host = "127.0.0.1"; - port += 1; - } - - using var client = new TcpClient(); - try - { - IAsyncResult result = client.BeginConnect(host, port, null, null); - bool success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(0.5)); - - if (success) - { - client.EndConnect(result); - return true; - } - - client.Close(); - return false; - } - catch - { - return false; - } - } - - /// - /// 连接模拟器。 - /// - /// 具体的连接错误。 - /// 是否成功。 - public bool AsstConnect(ref string error) - { - switch (SettingsViewModel.ConnectSettings.ConnectConfig) - { - case "MuMuEmulator12": - AsstSetConnectionExtrasMuMu12(SettingsViewModel.ConnectSettings.MuMuEmulator12Extras.Config); - break; - - case "LDPlayer": - AsstSetConnectionExtrasLdPlayer(SettingsViewModel.ConnectSettings.LdPlayerExtras.Config); - break; - } - - if (SettingsViewModel.ConnectSettings.AutoDetectConnection) - { - if (!AutoDetectConnection(ref error)) - { - return false; - } - } - - if (Connected && _connectedAdb == SettingsViewModel.ConnectSettings.AdbPath && - _connectedAddress == SettingsViewModel.ConnectSettings.ConnectAddress) - { - var actualConnectionStatus = CheckConnection(_connectedAdb, _connectedAddress); - if (!actualConnectionStatus) - { - Connected = false; - _logger.Information("Connection lost to {ConnectedAdb} {ConnectedAddress}", _connectedAdb, _connectedAddress); - error = "Connection lost"; - } - else - { - _logger.Information("Already connected to {ConnectedAdb} {ConnectedAddress}", _connectedAdb, _connectedAddress); - if (!_forcedReloadResource) - { - return true; - } - - _logger.Information("Forced reload resource"); - if (!LoadResource()) - { - error = "Load Resource Failed"; - return false; - } - - ToastNotification.ShowDirect("Auto Reload"); - - return true; - } - } - - bool ret = AsstConnect(_handle, SettingsViewModel.ConnectSettings.AdbPath, SettingsViewModel.ConnectSettings.ConnectAddress, SettingsViewModel.ConnectSettings.ConnectConfig); - - // 尝试默认的备选端口 - if (!ret && SettingsViewModel.ConnectSettings.AutoDetectConnection) - { - if (SettingsViewModel.ConnectSettings.DefaultAddress.TryGetValue(SettingsViewModel.ConnectSettings.ConnectConfig, out var value)) - { - foreach (var address in value - .TakeWhile(_ => !_runningState.GetIdle())) - { - ret = AsstConnect(_handle, SettingsViewModel.ConnectSettings.AdbPath, address, SettingsViewModel.ConnectSettings.ConnectConfig); - if (!ret) - { - continue; - } - - SettingsViewModel.ConnectSettings.ConnectAddress = address; - break; - } - } - else - { - Execute.OnUIThreadAsync( - () => - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("AutoDetectConnectionNotSupported"), UiLogColor.Error); - }); - } - } - - if (!ret) - { - error = LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("CheckSettings"); - } - else if (SettingsViewModel.ConnectSettings.AutoDetectConnection && !SettingsViewModel.ConnectSettings.AlwaysAutoDetectConnection) - { - SettingsViewModel.ConnectSettings.AutoDetectConnection = false; - } - - return ret; - } - - private static bool AutoDetectConnection(ref string error) - { - var adbPath = SettingsViewModel.ConnectSettings.AdbPath; - bool adbResult = !string.IsNullOrEmpty(adbPath) && - File.Exists(adbPath) && - Path.GetFileName(adbPath).Contains("adb", StringComparison.InvariantCultureIgnoreCase) && - CheckConnection(adbPath, SettingsViewModel.ConnectSettings.ConnectAddress); - - if (adbResult) - { - error = string.Empty; - return true; - } - - // 蓝叠的特殊处理 - { - string bsHvAddress = SettingsViewModel.ConnectSettings.TryToSetBlueStacksHyperVAddress() ?? string.Empty; - bool bsResult = CheckConnection(adbPath, bsHvAddress); - if (bsResult) - { - error = string.Empty; - if (string.IsNullOrEmpty(SettingsViewModel.ConnectSettings.AdbPath) && SettingsViewModel.ConnectSettings.DetectAdbConfig(ref error)) - { - return string.IsNullOrEmpty(error); - } - - SettingsViewModel.ConnectSettings.ConnectAddress = bsHvAddress; - return true; - } - } - - if (SettingsViewModel.ConnectSettings.DetectAdbConfig(ref error)) - { - // https://github.com/MaaAssistantArknights/MaaAssistantArknights/issues/8547 - // DetectAdbConfig 会把 ConnectAddress 变成第一个不是 emulator 开头的地址,可能会存在多开问题 - error = string.Empty; - return true; - } - - return false; - } - - private AsstTaskId AsstAppendTaskWithEncoding(AsstTaskType type, JObject? taskParams = null) - { - taskParams ??= []; - return AsstAppendTask(_handle, type.ToString(), JsonConvert.SerializeObject(taskParams)); - } - - private bool AsstSetTaskParamsWithEncoding(AsstTaskId id, JObject? taskParams = null) - { - if (id == 0) - { - return false; - } - - taskParams ??= []; - return AsstSetTaskParams(_handle, id, JsonConvert.SerializeObject(taskParams)); - } - - public enum TaskType - { - /// 开始唤醒 - StartUp, - - /// 关闭游戏 - CloseDown, - - /// 刷理智 - Fight, - - /// 关卡选择为剿灭时的备选刷理智 - FightAnnihilationAlternate, - - /// 剩余理智 - FightRemainingSanity, - - /// 自动公招 - Recruit, - - /// 基建 - Infrast, - - /// 获取信用点/访问好友/信用商店 - Mall, - - /// 领奖励/邮箱/幸运墙等 - Award, - - /// 自动肉鸽 - Roguelike, - - /// 公招识别 - RecruitCalc, - - /// 自动战斗 - Copilot, - - /// 视频识别(真有人用吗) - VideoRec, - - /// 仓库识别 - Depot, - - /// 干员识别 - OperBox, - - /// 抽卡 - Gacha, - - /// 生息演算 - Reclamation, - - /// 小游戏 - MiniGame, - - /// 自定义任务s - Custom, - } - - private readonly HashSet _mainTaskTypes = - [ - TaskType.StartUp, - TaskType.Fight, - TaskType.FightAnnihilationAlternate, - TaskType.FightRemainingSanity, - TaskType.Recruit, - TaskType.Infrast, - TaskType.Mall, - TaskType.Award, - TaskType.Roguelike, - TaskType.Reclamation, - ]; - - private readonly ObservableDictionary _tasksStatus = []; - - public IReadOnlyDictionary TasksStatus => new Dictionary(_tasksStatus); - - private bool TaskStatusUpdate(AsstTaskId id, TaskStatus status) - { - if (id == 0) - { - return false; - } - - if (_tasksStatus.TryGetValue(id, out var value)) - { - value.Status = status; - if (value.Status == TaskStatus.InProgress) - { - TaskSettingVisibilityInfo.Instance.CurrentTask = value.Type.ToString(); - } - - return true; - } - - _logger.Error("Task ID {TaskId} not found in _tasksStatus", id); - return false; - } - - public bool AsstAppendCloseDown(string clientType) - { - if (!AsstStop()) - { - _logger.Warning("Failed to stop Asst"); - } - - var (type, param) = new AsstCloseDownTask() { ClientType = clientType }.Serialize(); - return AsstAppendTaskWithEncoding(TaskType.CloseDown, type, param); - } - - /// - /// CloseDown 任务。 - /// - /// 客户端版本。 - /// 是否成功。 - public bool AsstStartCloseDown(string clientType) - { - return AsstAppendCloseDown(clientType) && AsstStart(); - } - - public bool AsstBackToHome() - { - return MaaService.AsstBackToHome(_handle); - } - - /// - /// 仓库识别。 - /// - /// 是否成功。 - public bool AsstStartDepot() - { - return AsstAppendTaskWithEncoding(TaskType.Depot, AsstTaskType.Depot) && AsstStart(); - } - - /// - /// 干员识别。 - /// - /// 是否成功。 - public bool AsstStartOperBox() - { - return AsstAppendTaskWithEncoding(TaskType.OperBox, AsstTaskType.OperBox) && AsstStart(); - } - - /// - /// 牛牛抽卡。 - /// - /// 是否为单抽,默认为 true - /// 是否成功。 - public bool AsstStartGacha(bool once = true) - { - var task = new AsstCustomTask() - { - CustomTasks = [once ? "GachaOnce" : "GachaTenTimes"], - }; - var (type, param) = task.Serialize(); - return AsstAppendTaskWithEncoding(TaskType.Gacha, type, param) && AsstStart(); - } - - /// - /// 小游戏。 - /// - /// 任务名(tasks.json 中的 key) - /// 是否成功。 - public bool AsstMiniGame(string taskName) - { - var task = new AsstCustomTask() - { - CustomTasks = [taskName], - }; - var (type, param) = task.Serialize(); - return AsstAppendTaskWithEncoding(TaskType.MiniGame, type, param) && AsstStart(); - } - - /// - /// 视频识别。 - /// - /// 文件路径 - /// 是否成功。 - public bool AsstStartVideoRec(string filename) - { - var taskParams = new JObject - { - ["filename"] = filename, - }; - AsstTaskId id = AsstAppendTaskWithEncoding(AsstTaskType.VideoRecognition, taskParams); - _tasksStatus.Add(id, (TaskType.Copilot, TaskStatus.Idle)); - return id != 0 && AsstStart(); - } - - public bool AsstAppendTaskWithEncoding(TaskType wpfTasktype, AsstBaseTask task) - { - return AsstAppendTaskWithEncoding(wpfTasktype, task.Serialize()); - } - - public bool AsstAppendTaskWithEncoding(TaskType wpfTaskType, (AsstTaskType Type, JObject? TaskParams) task) - { - return AsstAppendTaskWithEncoding(wpfTaskType, task.Type, task.TaskParams); - } - - public bool AsstAppendTaskWithEncoding(TaskType wpfTaskType, AsstTaskType type, JObject? taskParams = null) - { - taskParams ??= []; - AsstTaskId id = AsstAppendTask(_handle, type.ToString(), JsonConvert.SerializeObject(taskParams)); - if (id == 0) - { - return false; - } - - _tasksStatus.Add(id, (wpfTaskType, TaskStatus.Idle)); + error = string.Empty; return true; } - public bool AsstSetTaskParamsEncoded(AsstTaskId id, AsstBaseTask task) + // 蓝叠的特殊处理 { - return AsstSetTaskParamsEncoded(id, task.Serialize().Params); + string bsHvAddress = SettingsViewModel.ConnectSettings.TryToSetBlueStacksHyperVAddress() ?? string.Empty; + bool bsResult = CheckConnection(adbPath, bsHvAddress); + if (bsResult) + { + error = string.Empty; + if (string.IsNullOrEmpty(SettingsViewModel.ConnectSettings.AdbPath) && SettingsViewModel.ConnectSettings.DetectAdbConfig(ref error)) + { + return string.IsNullOrEmpty(error); + } + + SettingsViewModel.ConnectSettings.ConnectAddress = bsHvAddress; + return true; + } } - public bool AsstSetTaskParamsEncoded(AsstTaskId id, JObject? taskParams = null) + if (SettingsViewModel.ConnectSettings.DetectAdbConfig(ref error)) { - if (id == 0) + // https://github.com/MaaAssistantArknights/MaaAssistantArknights/issues/8547 + // DetectAdbConfig 会把 ConnectAddress 变成第一个不是 emulator 开头的地址,可能会存在多开问题 + error = string.Empty; + return true; + } + + return false; + } + + private AsstTaskId AsstAppendTaskWithEncoding(AsstTaskType type, JObject? taskParams = null) + { + taskParams ??= []; + return AsstAppendTask(_handle, type.ToString(), JsonConvert.SerializeObject(taskParams)); + } + + private bool AsstSetTaskParamsWithEncoding(AsstTaskId id, JObject? taskParams = null) + { + if (id == 0) + { + return false; + } + + taskParams ??= []; + return AsstSetTaskParams(_handle, id, JsonConvert.SerializeObject(taskParams)); + } + + public enum TaskType + { + /// 开始唤醒 + StartUp, + + /// 关闭游戏 + CloseDown, + + /// 刷理智 + Fight, + + /// 关卡选择为剿灭时的备选刷理智 + FightAnnihilationAlternate, + + /// 剩余理智 + FightRemainingSanity, + + /// 自动公招 + Recruit, + + /// 基建 + Infrast, + + /// 获取信用点/访问好友/信用商店 + Mall, + + /// 领奖励/邮箱/幸运墙等 + Award, + + /// 自动肉鸽 + Roguelike, + + /// 公招识别 + RecruitCalc, + + /// 自动战斗 + Copilot, + + /// 视频识别(真有人用吗) + VideoRec, + + /// 仓库识别 + Depot, + + /// 干员识别 + OperBox, + + /// 抽卡 + Gacha, + + /// 生息演算 + Reclamation, + + /// 小游戏 + MiniGame, + + /// 自定义任务s + Custom, + } + + private readonly HashSet _mainTaskTypes = + [ + TaskType.StartUp, + TaskType.Fight, + TaskType.FightAnnihilationAlternate, + TaskType.FightRemainingSanity, + TaskType.Recruit, + TaskType.Infrast, + TaskType.Mall, + TaskType.Award, + TaskType.Roguelike, + TaskType.Reclamation, + ]; + + private readonly ObservableDictionary _tasksStatus = []; + + public IReadOnlyDictionary TasksStatus => new Dictionary(_tasksStatus); + + private bool TaskStatusUpdate(AsstTaskId id, TaskStatus status) + { + if (id == 0) + { + return false; + } + + if (_tasksStatus.TryGetValue(id, out var value)) + { + value.Status = status; + if (value.Status == TaskStatus.InProgress) { - return false; + TaskSettingVisibilityInfo.Instance.CurrentTask = value.Type.ToString(); } - taskParams ??= []; - return AsstSetTaskParams(_handle, id, JsonConvert.SerializeObject(taskParams)); + return true; } - /// - /// 启动。 - /// - /// 是否成功。 - public bool AsstStart() + _logger.Error("Task ID {TaskId} not found in _tasksStatus", id); + return false; + } + + public bool AsstAppendCloseDown(string clientType) + { + if (!AsstStop()) { - return MaaService.AsstStart(_handle); + _logger.Warning("Failed to stop Asst"); } - /// - /// 运行中。 - /// - /// 是否正在运行。 - public bool AsstRunning() - { - return MaaService.AsstRunning(_handle); - } - - /// - /// 停止。 - /// - /// 是否成功。 - public bool AsstStop() - { - return MaaService.AsstStop(_handle); - } - - /// - /// 销毁。 - /// - public void AsstDestroy() - { - MaaService.AsstDestroy(_handle); - } + var (type, param) = new AsstCloseDownTask() { ClientType = clientType }.Serialize(); + return AsstAppendTaskWithEncoding(TaskType.CloseDown, type, param); } /// - /// MaaCore 消息。 + /// CloseDown 任务。 /// - [SuppressMessage("ReSharper", "UnusedMember.Global")] - public enum AsstMsg + /// 客户端版本。 + /// 是否成功。 + public bool AsstStartCloseDown(string clientType) { - /* Global Info */ + return AsstAppendCloseDown(clientType) && AsstStart(); + } - /// - /// 内部错误。 - /// - InternalError = 0, - - /// - /// 初始化失败。 - /// - InitFailed, - - /// - /// 连接相关错误。 - /// - ConnectionInfo, - - /// - /// 全部任务完成。 - /// - AllTasksCompleted, - - /// - /// 外部异步调用信息 - /// - AsyncCallInfo, - - /// - /// 实例已销毁 - /// - Destroyed, - - /* TaskChain Info */ - - /// - /// 任务链执行/识别错误。 - /// - TaskChainError = 10000, - - /// - /// 任务链开始。 - /// - TaskChainStart, - - /// - /// 任务链完成。 - /// - TaskChainCompleted, - - /// - /// 任务链额外信息。 - /// - TaskChainExtraInfo, - - /// - /// 任务链手动停止 - /// - TaskChainStopped, - - /* SubTask Info */ - - /// - /// 原子任务执行/识别错误。 - /// - SubTaskError = 20000, - - /// - /// 原子任务开始。 - /// - SubTaskStart, - - /// - /// 原子任务完成。 - /// - SubTaskCompleted, - - /// - /// 原子任务额外信息。 - /// - SubTaskExtraInfo, - - /// - /// 原子任务手动停止 - /// - SubTaskStopped, - - /// - /// 上报请求 - /// - ReportRequest = 30000, + public bool AsstBackToHome() + { + return MaaService.AsstBackToHome(_handle); } /// - /// 任务状态 + /// 仓库识别。 /// - public enum TaskStatus + /// 是否成功。 + public bool AsstStartDepot() { - /// - /// 未开始 - /// - Idle = 0, - - /// - /// 进行中 - /// - InProgress = 1, - - /// - /// 已完成 - /// - Completed = 2, + return AsstAppendTaskWithEncoding(TaskType.Depot, AsstTaskType.Depot) && AsstStart(); } - public enum AsstStaticOptionKey + /// + /// 干员识别。 + /// + /// 是否成功。 + public bool AsstStartOperBox() { - /// - /// 无效 - /// - Invalid, - - /// - /// 用CPU进行OCR - /// - CpuOCR, - - /// - /// 用GPU进行OCR - /// - GpuOCR, + return AsstAppendTaskWithEncoding(TaskType.OperBox, AsstTaskType.OperBox) && AsstStart(); } - public enum InstanceOptionKey + /// + /// 牛牛抽卡。 + /// + /// 是否为单抽,默认为 true + /// 是否成功。 + public bool AsstStartGacha(bool once = true) { - /* Deprecated */ // MinitouchEnabled = 1, + var task = new AsstCustomTask() + { + CustomTasks = [once ? "GachaOnce" : "GachaTenTimes"], + }; + var (type, param) = task.Serialize(); + return AsstAppendTaskWithEncoding(TaskType.Gacha, type, param) && AsstStart(); + } - /// - /// Indicates the touch mode. - /// - TouchMode = 2, + /// + /// 小游戏。 + /// + /// 任务名(tasks.json 中的 key) + /// 是否成功。 + public bool AsstMiniGame(string taskName) + { + var task = new AsstCustomTask() + { + CustomTasks = [taskName], + }; + var (type, param) = task.Serialize(); + return AsstAppendTaskWithEncoding(TaskType.MiniGame, type, param) && AsstStart(); + } - /// - /// Indicates whether the deployment should be paused. - /// - DeploymentWithPause = 3, + /// + /// 视频识别。 + /// + /// 文件路径 + /// 是否成功。 + public bool AsstStartVideoRec(string filename) + { + var taskParams = new JObject + { + ["filename"] = filename, + }; + AsstTaskId id = AsstAppendTaskWithEncoding(AsstTaskType.VideoRecognition, taskParams); + _tasksStatus.Add(id, (TaskType.Copilot, TaskStatus.Idle)); + return id != 0 && AsstStart(); + } - /// - /// Indicates whether AdbLite is used. - /// - AdbLiteEnabled = 4, + public bool AsstAppendTaskWithEncoding(TaskType wpfTasktype, AsstBaseTask task) + { + return AsstAppendTaskWithEncoding(wpfTasktype, task.Serialize()); + } - /// - /// Indicates whether the ADB server process should be killed when the instance is exited. - /// - KillAdbOnExit = 5, + public bool AsstAppendTaskWithEncoding(TaskType wpfTaskType, (AsstTaskType Type, JObject? TaskParams) task) + { + return AsstAppendTaskWithEncoding(wpfTaskType, task.Type, task.TaskParams); + } + + public bool AsstAppendTaskWithEncoding(TaskType wpfTaskType, AsstTaskType type, JObject? taskParams = null) + { + taskParams ??= []; + AsstTaskId id = AsstAppendTask(_handle, type.ToString(), JsonConvert.SerializeObject(taskParams)); + if (id == 0) + { + return false; + } + + _tasksStatus.Add(id, (wpfTaskType, TaskStatus.Idle)); + return true; + } + + public bool AsstSetTaskParamsEncoded(AsstTaskId id, AsstBaseTask task) + { + return AsstSetTaskParamsEncoded(id, task.Serialize().Params); + } + + public bool AsstSetTaskParamsEncoded(AsstTaskId id, JObject? taskParams = null) + { + if (id == 0) + { + return false; + } + + taskParams ??= []; + return AsstSetTaskParams(_handle, id, JsonConvert.SerializeObject(taskParams)); + } + + /// + /// 启动。 + /// + /// 是否成功。 + public bool AsstStart() + { + return MaaService.AsstStart(_handle); + } + + /// + /// 运行中。 + /// + /// 是否正在运行。 + public bool AsstRunning() + { + return MaaService.AsstRunning(_handle); + } + + /// + /// 停止。 + /// + /// 是否成功。 + public bool AsstStop() + { + return MaaService.AsstStop(_handle); + } + + /// + /// 销毁。 + /// + public void AsstDestroy() + { + MaaService.AsstDestroy(_handle); } } + +/// +/// MaaCore 消息。 +/// +[SuppressMessage("ReSharper", "UnusedMember.Global")] +public enum AsstMsg +{ + /* Global Info */ + + /// + /// 内部错误。 + /// + InternalError = 0, + + /// + /// 初始化失败。 + /// + InitFailed, + + /// + /// 连接相关错误。 + /// + ConnectionInfo, + + /// + /// 全部任务完成。 + /// + AllTasksCompleted, + + /// + /// 外部异步调用信息 + /// + AsyncCallInfo, + + /// + /// 实例已销毁 + /// + Destroyed, + + /* TaskChain Info */ + + /// + /// 任务链执行/识别错误。 + /// + TaskChainError = 10000, + + /// + /// 任务链开始。 + /// + TaskChainStart, + + /// + /// 任务链完成。 + /// + TaskChainCompleted, + + /// + /// 任务链额外信息。 + /// + TaskChainExtraInfo, + + /// + /// 任务链手动停止 + /// + TaskChainStopped, + + /* SubTask Info */ + + /// + /// 原子任务执行/识别错误。 + /// + SubTaskError = 20000, + + /// + /// 原子任务开始。 + /// + SubTaskStart, + + /// + /// 原子任务完成。 + /// + SubTaskCompleted, + + /// + /// 原子任务额外信息。 + /// + SubTaskExtraInfo, + + /// + /// 原子任务手动停止 + /// + SubTaskStopped, + + /// + /// 上报请求 + /// + ReportRequest = 30000, +} + +/// +/// 任务状态 +/// +public enum TaskStatus +{ + /// + /// 未开始 + /// + Idle = 0, + + /// + /// 进行中 + /// + InProgress = 1, + + /// + /// 已完成 + /// + Completed = 2, +} + +public enum AsstStaticOptionKey +{ + /// + /// 无效 + /// + Invalid, + + /// + /// 用CPU进行OCR + /// + CpuOCR, + + /// + /// 用GPU进行OCR + /// + GpuOCR, +} + +public enum InstanceOptionKey +{ + /* Deprecated */ // MinitouchEnabled = 1, + + /// + /// Indicates the touch mode. + /// + TouchMode = 2, + + /// + /// Indicates whether the deployment should be paused. + /// + DeploymentWithPause = 3, + + /// + /// Indicates whether AdbLite is used. + /// + AdbLiteEnabled = 4, + + /// + /// Indicates whether the ADB server process should be killed when the instance is exited. + /// + KillAdbOnExit = 5, +} diff --git a/src/MaaWpfGui/Main/Bootstrapper.cs b/src/MaaWpfGui/Main/Bootstrapper.cs index 476a1a8e6c..5bbfb300ba 100644 --- a/src/MaaWpfGui/Main/Bootstrapper.cs +++ b/src/MaaWpfGui/Main/Bootstrapper.cs @@ -50,682 +50,681 @@ using Serilog.Events; using Stylet; using StyletIoC; -namespace MaaWpfGui.Main +namespace MaaWpfGui.Main; + +/// +/// The bootstrapper. +/// +public class Bootstrapper : Bootstrapper { - /// - /// The bootstrapper. - /// - public class Bootstrapper : Bootstrapper + private static ILogger _logger = Logger.None; + + private static Mutex _mutex; + private static bool _hasMutex; + + public static readonly string UiLogFile = Path.Combine(PathsHelper.DebugDir, "gui.log"); + public static readonly string UiLogBakFile = Path.Combine(PathsHelper.DebugDir, "gui.bak.log"); + public static readonly string CoreLogFile = Path.Combine(PathsHelper.DebugDir, "asst.log"); + public static readonly string CoreLogBakFile = Path.Combine(PathsHelper.DebugDir, "asst.bak.log"); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern IntPtr LoadLibrary(string dllName); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern bool FreeLibrary(IntPtr hModule); + + private static List UnknownDllDetected() { - private static ILogger _logger = Logger.None; - - private static Mutex _mutex; - private static bool _hasMutex; - - public static readonly string UiLogFile = Path.Combine(PathsHelper.DebugDir, "gui.log"); - public static readonly string UiLogBakFile = Path.Combine(PathsHelper.DebugDir, "gui.bak.log"); - public static readonly string CoreLogFile = Path.Combine(PathsHelper.DebugDir, "asst.log"); - public static readonly string CoreLogBakFile = Path.Combine(PathsHelper.DebugDir, "asst.bak.log"); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - private static extern IntPtr LoadLibrary(string dllName); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto)] - private static extern bool FreeLibrary(IntPtr hModule); - - private static List UnknownDllDetected() + try { - try + // 属于 MAA 的 DLL 列表 + // 因为经常有人把 MAA 和别的东西解压到一起然后发生 DLL 劫持然后报错,遂检测 + var maaDlls = new HashSet(StringComparer.OrdinalIgnoreCase) { - // 属于 MAA 的 DLL 列表 - // 因为经常有人把 MAA 和别的东西解压到一起然后发生 DLL 劫持然后报错,遂检测 - var maaDlls = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "DirectML.dll", - "fastdeploy_ppocr.dll", - "MaaCore.dll", - "onnxruntime_maa.dll", - "opencv_world4_maa.dll", - }; + "DirectML.dll", + "fastdeploy_ppocr.dll", + "MaaCore.dll", + "onnxruntime_maa.dll", + "opencv_world4_maa.dll", + }; - var currentDirectory = AppDomain.CurrentDomain.BaseDirectory; + var currentDirectory = AppDomain.CurrentDomain.BaseDirectory; - var dllFiles = Directory.GetFiles(currentDirectory, "*.dll"); + var dllFiles = Directory.GetFiles(currentDirectory, "*.dll"); - return [.. dllFiles - .Select(Path.GetFileName) - .Where(fileName => !maaDlls.Contains(fileName) && !fileName.Contains("maa", StringComparison.OrdinalIgnoreCase))]; - } - catch (Exception) + return [.. dllFiles + .Select(Path.GetFileName) + .Where(fileName => !maaDlls.Contains(fileName) && !fileName.Contains("maa", StringComparison.OrdinalIgnoreCase))]; + } + catch (Exception) + { + return []; + } + } + + private static bool IsVCppInstalled() + { + IntPtr handle = IntPtr.Zero; + try + { + handle = LoadLibrary("MaaCore.dll"); + + // 如果句柄非空,说明 DLL 存在且可加载 + return handle != IntPtr.Zero; + } + catch (Exception) + { + return false; + } + finally + { + if (handle != IntPtr.Zero) { - return []; + FreeLibrary(handle); // 释放 DLL 句柄 } } + } - private static bool IsVCppInstalled() + public static bool IsWritable(string path) + { + try { - IntPtr handle = IntPtr.Zero; - try - { - handle = LoadLibrary("MaaCore.dll"); + string testFile = Path.Combine(path, "write_test.tmp"); + File.WriteAllText(testFile, "test"); + File.Delete(testFile); + return true; + } + catch + { + return false; + } + } - // 如果句柄非空,说明 DLL 存在且可加载 - return handle != IntPtr.Zero; - } - catch (Exception) + public static void ParseCrashLog() + { + var crashFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "crash.log"); + if (!File.Exists(crashFile)) + { + return; + } + + try + { + string[] lines = File.ReadAllLines(crashFile, Encoding.UTF8); + + StringBuilder message = new StringBuilder(); + string currentReason = null; + string currentDetail = null; + + foreach (var line in lines) { - return false; - } - finally - { - if (handle != IntPtr.Zero) + if (line.StartsWith("Reason: ")) { - FreeLibrary(handle); // 释放 DLL 句柄 + currentReason = line[7..].Trim(); } - } - } - - public static bool IsWritable(string path) - { - try - { - string testFile = Path.Combine(path, "write_test.tmp"); - File.WriteAllText(testFile, "test"); - File.Delete(testFile); - return true; - } - catch - { - return false; - } - } - - public static void ParseCrashLog() - { - var crashFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "crash.log"); - if (!File.Exists(crashFile)) - { - return; - } - - try - { - string[] lines = File.ReadAllLines(crashFile, Encoding.UTF8); - - StringBuilder message = new StringBuilder(); - string currentReason = null; - string currentDetail = null; - - foreach (var line in lines) + else if (line.StartsWith("Detail: ")) { - if (line.StartsWith("Reason: ")) + currentDetail = line[8..].Trim(); + } + else if (line.StartsWith("===================")) + { + if (!string.IsNullOrEmpty(currentReason)) { - currentReason = line[7..].Trim(); - } - else if (line.StartsWith("Detail: ")) - { - currentDetail = line[8..].Trim(); - } - else if (line.StartsWith("===================")) - { - if (!string.IsNullOrEmpty(currentReason)) + message.AppendLine($"Reason: {currentReason}"); + if (!string.IsNullOrEmpty(currentDetail)) { - message.AppendLine($"Reason: {currentReason}"); - if (!string.IsNullOrEmpty(currentDetail)) - { - message.AppendLine($"Detail: {currentDetail}"); - } - - message.AppendLine(); + message.AppendLine($"Detail: {currentDetail}"); } - currentReason = null; - currentDetail = null; + message.AppendLine(); } - } - if (message.Length > 0) - { - message.AppendLine(LocalizationHelper.GetString("ErrorCrashMessageHeader")); - message.AppendLine(); - message.AppendLine(LocalizationHelper.GetString("ErrorCrashMessageOpenLog")); - message.AppendLine(LocalizationHelper.GetString("ErrorCrashMessageGenerateReport")); - message.AppendLine(); - message.AppendLine(LocalizationHelper.GetString("ErrorCrashMessageHelpTip")); - - MessageBoxHelper.Show( - message.ToString(), - LocalizationHelper.GetString("ErrorCrashDialogTitle"), - MessageBoxButton.OK, - MessageBoxImage.Error); - - try - { - File.Delete(crashFile); - } - catch - { - // ignored - } - } - } - catch - { - // ignored - } - } - - /// - /// 初始化些啥自己加。 - protected override void OnStart() - { - Directory.SetCurrentDirectory(AppContext.BaseDirectory); - if (!Directory.Exists("debug")) - { - Directory.CreateDirectory("debug"); - } - - if (File.Exists(UiLogFile) && new FileInfo(UiLogFile).Length > 4 * 1024 * 1024) - { - if (File.Exists(UiLogBakFile)) - { - File.Delete(UiLogBakFile); - } - - File.Move(UiLogFile, UiLogBakFile); - } - - // Bootstrap serilog - var loggerConfiguration = new LoggerConfiguration() - .WriteTo.Debug(outputTemplate: "[{Timestamp:HH:mm:ss}][{Level:u3}]{ClassName} <{ThreadId}> {Message:lj}{NewLine}{Exception}") - .WriteTo.File( - UiLogFile, - outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}][{Level:u3}]{ClassName} <{ThreadId}> {Message:lj}{NewLine}{Exception}") - .Enrich.With() - .Enrich.FromLogContext() - .Enrich.WithThreadId() - .Enrich.WithThreadName(); - - var uiVersion = Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion.Split('+')[0] ?? "0.0.1"; - uiVersion = uiVersion == "0.0.1" ? "DEBUG VERSION" : uiVersion; - var builtDate = Assembly.GetExecutingAssembly().GetCustomAttribute()?.BuildDateTime ?? DateTime.MinValue; - var maaEnv = Environment.GetEnvironmentVariable("MAA_ENVIRONMENT") == "Debug" - ? "Debug" - : "Production"; - var args = Environment.GetCommandLineArgs(); - var withDebugFile = File.Exists("DEBUG") || File.Exists("DEBUG.txt"); - loggerConfiguration = (maaEnv == "Debug" || withDebugFile) - ? loggerConfiguration.MinimumLevel.Verbose() - : loggerConfiguration.MinimumLevel.Information(); - var workingDirectory = PathsHelper.BaseDir; - var folderName = Path.GetFileName(workingDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); - var isBuildOutputFolder = - string.Equals(folderName, "Release", StringComparison.OrdinalIgnoreCase) || - string.Equals(folderName, "Debug", StringComparison.OrdinalIgnoreCase) || - string.Equals(folderName, "RelWithDebInfo", StringComparison.OrdinalIgnoreCase); - - Log.Logger = loggerConfiguration.CreateLogger(); - _logger = Log.Logger.ForContext(); - _logger.Information("==================================="); - _logger.Information("MaaAssistantArknights GUI started"); - _logger.Information("Version {UiVersion}", uiVersion); - _logger.Information("Built at {BuiltDate:O}", builtDate); - _logger.Information("Maa ENV: {MaaEnv}", maaEnv); - _logger.Information("Command Line: {Join}", string.Join(' ', args)); - _logger.Information("User Dir {BaseDirectory}", workingDirectory); - if (withDebugFile) - { - _logger.Information("Start with DEBUG file"); - } - - if (IsAdministratorWithUac()) - { - _logger.Information("Run as Administrator"); - } - - if (WineRuntimeInformation.IsRunningUnderWine) - { - _logger.Information("Running under Wine {WineVersion} on {HostSystemName}", WineRuntimeInformation.WineVersion, WineRuntimeInformation.HostSystemName); - RenderOptions.ProcessRenderMode = RenderMode.SoftwareOnly; - _logger.Information("MaaWineBridge status: {WineBridgeAvailability}", MaaWineBridge.Availability); - _logger.Information("MaaDesktopIntegration available: {Available}", MaaDesktopIntegration.Available); - } - - _logger.Information("==================================="); - - ConfigurationHelper.Load(); - LocalizationHelper.Load(); - ETagCache.Load(); - - // 检查 MaaCore.dll 是否存在 - if (!File.Exists("MaaCore.dll")) - { - throw new FileNotFoundException("MaaCore.dll not found!"); - } - - // 检查 resource 文件夹是否存在 - if (!Directory.Exists(PathsHelper.ResourceDir)) - { - throw new DirectoryNotFoundException("resource folder not found!"); - } - - // Debug 模式下 DLL 是未打包的 - if (maaEnv != "Debug" && !isBuildOutputFolder) - { - var unknownDlls = UnknownDllDetected(); - if (unknownDlls.Count > 0) - { - MessageBoxHelper.Show( - LocalizationHelper.GetString("UnknownDllDetected") + "\n" + string.Join("\n", unknownDlls), - "MAA", - MessageBoxButton.OK, - MessageBoxImage.Error); - _logger.Fatal("Unknown DLL(s) detected: {UnknownDlls}", string.Join(", ", unknownDlls)); - Shutdown(); - return; + currentReason = null; + currentDetail = null; } } - if (!IsVCppInstalled()) + if (message.Length > 0) { - var ret = MessageBoxHelper.Show( - LocalizationHelper.GetString("VC++NotInstalled"), - "MAA", - MessageBoxButton.OKCancel, - MessageBoxImage.Information, - ok: LocalizationHelper.GetString("Confirm"), - cancel: LocalizationHelper.GetString("Cancel")); - if (ret == MessageBoxResult.OK) - { - var startInfo = new ProcessStartInfo - { - FileName = "DependencySetup_依赖库安装.bat", - WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory, // 设置工作目录 - WindowStyle = ProcessWindowStyle.Normal, // 显示窗口让用户看到进度 - }; + message.AppendLine(LocalizationHelper.GetString("ErrorCrashMessageHeader")); + message.AppendLine(); + message.AppendLine(LocalizationHelper.GetString("ErrorCrashMessageOpenLog")); + message.AppendLine(LocalizationHelper.GetString("ErrorCrashMessageGenerateReport")); + message.AppendLine(); + message.AppendLine(LocalizationHelper.GetString("ErrorCrashMessageHelpTip")); - Process.Start(startInfo); - } + MessageBoxHelper.Show( + message.ToString(), + LocalizationHelper.GetString("ErrorCrashDialogTitle"), + MessageBoxButton.OK, + MessageBoxImage.Error); - Shutdown(); - return; - } - - if (!HandleMultipleInstances()) - { - Shutdown(); - return; - } - - if (!IsWritable(PathsHelper.BaseDir)) - { - Task.Run(() => MessageBoxHelper.Show(LocalizationHelper.GetString("SoftwareLocationWarning"), LocalizationHelper.GetString("Error"), MessageBoxButton.OK, MessageBoxImage.Error)); - } - - Task.Run(ParseCrashLog); - - base.OnStart(); - _hasMutex = true; - - const string ConfigFlag = "--config"; - const string AnotherFlag = "--another"; // 示例,之后如果有其他参数,可以继续添加 - - var parsedArgs = ParseArgs(args, ConfigFlag, AnotherFlag); - - if (parsedArgs.TryGetValue(ConfigFlag, out string configArgs) && Config(configArgs)) - { - // return; - } - } - - public class ClassNameEnricher : ILogEventEnricher - { - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) - { - if (!logEvent.Properties.TryGetValue("SourceContext", out var sourceContextValue)) - { - return; - } - - var sourceContext = sourceContextValue.ToString().Trim('"'); - var className = sourceContext.Split('.').Last(); - className = ("[" + className + "]").PadRight(24); - logEvent.AddOrUpdateProperty(propertyFactory.CreateProperty("ClassName", className)); - } - } - - protected override void OnLaunch() - { - BadModules.CheckAndWarnBadInjectedModules(); - } - - private static bool HandleMultipleInstances() - { - // 设置互斥量的名称 - string mutexName = "MAA_" + PathsHelper.BaseDir.Replace("\\", "_").Replace(":", string.Empty); - _mutex = new Mutex(true, mutexName, out var isOnlyInstance); - - try - { - if (isOnlyInstance || _mutex.WaitOne(500)) - { - return true; - } - - MessageBoxHelper.Show(LocalizationHelper.GetString("MultiInstanceUnderSamePath"), "MAA", MessageBoxButton.OK, MessageBoxImage.Warning); - return false; - } - catch (AbandonedMutexException) - { - // 上一个程序没有正常释放互斥量 - // 即使捕获到这个异常,此时也已经获得了锁 - return true; - } - catch (Exception e) - { - MessageBoxHelper.Show(LocalizationHelper.GetString("MultiInstanceUnderSamePath") + e.Message, "MAA", MessageBoxButton.OK, MessageBoxImage.Warning); - return false; - } - } - - public static bool IsUserAdministrator() => new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); - - public static bool IsUacEnabled() - { - try - { - using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"); - if (key == null) - { - return true; - } - - var value = key.GetValue("EnableLUA"); - if (value is int intValue) - { - return intValue != 0; - } - - return true; - } - catch - { - return true; - } - } - - public static bool IsAdministratorWithUac() => IsUserAdministrator() && IsUacEnabled(); - - /// - protected override void ConfigureIoC(IStyletIoCBuilder builder) - { - builder.Bind().ToSelf().InSingletonScope(); - builder.Bind().ToSelf().InSingletonScope(); - builder.Bind().ToSelf().InSingletonScope(); - builder.Bind().ToSelf().InSingletonScope(); - - builder.Bind().ToSelf().InSingletonScope(); - builder.Bind().ToSelf().InSingletonScope(); - - builder.Bind().ToSelf().InSingletonScope(); - - builder.Bind().To().InSingletonScope(); - builder.Bind().To().InSingletonScope(); - - builder.Bind().To().InSingletonScope(); - - builder.Bind().To().InSingletonScope(); - - builder.Bind().To().InSingletonScope(); - builder.Bind().To().InSingletonScope(); - } - - protected override void Configure() - { - base.Configure(); - Instances.Instantiate(Container); - } - - /// - protected override void DisplayRootView(object rootViewModel) - { - if (Application.Current.IsShuttingDown()) - { - return; - } - - bool wasFirstBoot = Instances.VersionUpdateViewModel.IsFirstBootAfterUpdate; - - Instances.WindowManager.ShowWindow(rootViewModel); - Instances.InstantiateOnRootViewDisplayed(Container); - - // 如果 IsFirstBootAfterUpdate 从 false 变为 true,说明这次启动只是解压更新包,不用执行后续逻辑 - if (!wasFirstBoot && Instances.VersionUpdateViewModel.IsFirstBootAfterUpdate) - { - return; - } - - AchievementTrackerHelper.Events.Startup(); - - var buildTimeInterval = (DateTime.UtcNow - VersionUpdateSettingsUserControlModel.BuildDateTime).TotalDays; - var resourceTimeInterval = (DateTime.UtcNow - SettingsViewModel.VersionUpdateSettings.ResourceDateTime).TotalDays; - var maxTimeInterval = Math.Max(buildTimeInterval, resourceTimeInterval); - if (maxTimeInterval > 90) - { - Instances.TaskQueueViewModel.LogItemViewModels.Add(new(string.Format(LocalizationHelper.GetString("Achievement.Martian.ConditionsTip"), (maxTimeInterval / 30.436875).ToString("F2")), UiLogColor.Error)); - } - } - - /// - /// 退出时执行啥自己加。 - protected override void OnExit(ExitEventArgs e) - { - // MessageBox.Show("O(∩_∩)O 拜拜"); - try - { - Instances.TaskQueueViewModel.ResetAllTemporaryVariable(); - } - catch - { - // ignored - } - - Release(); - - _logger.Information("MaaAssistantArknights GUI exited"); - _logger.Information("{Message}", string.Empty); - Log.CloseAndFlush(); - base.OnExit(e); - - if (!_isRestartingAfterUpdate) - { // 如果是更新后重启,则不删除 .old try - { // 退出时移除.old - Directory.Delete(".old", true); + { + File.Delete(crashFile); + } + catch + { + // ignored + } + } + } + catch + { + // ignored + } + } + + /// + /// 初始化些啥自己加。 + protected override void OnStart() + { + Directory.SetCurrentDirectory(AppContext.BaseDirectory); + if (!Directory.Exists("debug")) + { + Directory.CreateDirectory("debug"); + } + + if (File.Exists(UiLogFile) && new FileInfo(UiLogFile).Length > 4 * 1024 * 1024) + { + if (File.Exists(UiLogBakFile)) + { + File.Delete(UiLogBakFile); + } + + File.Move(UiLogFile, UiLogBakFile); + } + + // Bootstrap serilog + var loggerConfiguration = new LoggerConfiguration() + .WriteTo.Debug(outputTemplate: "[{Timestamp:HH:mm:ss}][{Level:u3}]{ClassName} <{ThreadId}> {Message:lj}{NewLine}{Exception}") + .WriteTo.File( + UiLogFile, + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}][{Level:u3}]{ClassName} <{ThreadId}> {Message:lj}{NewLine}{Exception}") + .Enrich.With() + .Enrich.FromLogContext() + .Enrich.WithThreadId() + .Enrich.WithThreadName(); + + var uiVersion = Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion.Split('+')[0] ?? "0.0.1"; + uiVersion = uiVersion == "0.0.1" ? "DEBUG VERSION" : uiVersion; + var builtDate = Assembly.GetExecutingAssembly().GetCustomAttribute()?.BuildDateTime ?? DateTime.MinValue; + var maaEnv = Environment.GetEnvironmentVariable("MAA_ENVIRONMENT") == "Debug" + ? "Debug" + : "Production"; + var args = Environment.GetCommandLineArgs(); + var withDebugFile = File.Exists("DEBUG") || File.Exists("DEBUG.txt"); + loggerConfiguration = (maaEnv == "Debug" || withDebugFile) + ? loggerConfiguration.MinimumLevel.Verbose() + : loggerConfiguration.MinimumLevel.Information(); + var workingDirectory = PathsHelper.BaseDir; + var folderName = Path.GetFileName(workingDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + var isBuildOutputFolder = + string.Equals(folderName, "Release", StringComparison.OrdinalIgnoreCase) || + string.Equals(folderName, "Debug", StringComparison.OrdinalIgnoreCase) || + string.Equals(folderName, "RelWithDebInfo", StringComparison.OrdinalIgnoreCase); + + Log.Logger = loggerConfiguration.CreateLogger(); + _logger = Log.Logger.ForContext(); + _logger.Information("==================================="); + _logger.Information("MaaAssistantArknights GUI started"); + _logger.Information("Version {UiVersion}", uiVersion); + _logger.Information("Built at {BuiltDate:O}", builtDate); + _logger.Information("Maa ENV: {MaaEnv}", maaEnv); + _logger.Information("Command Line: {Join}", string.Join(' ', args)); + _logger.Information("User Dir {BaseDirectory}", workingDirectory); + if (withDebugFile) + { + _logger.Information("Start with DEBUG file"); + } + + if (IsAdministratorWithUac()) + { + _logger.Information("Run as Administrator"); + } + + if (WineRuntimeInformation.IsRunningUnderWine) + { + _logger.Information("Running under Wine {WineVersion} on {HostSystemName}", WineRuntimeInformation.WineVersion, WineRuntimeInformation.HostSystemName); + RenderOptions.ProcessRenderMode = RenderMode.SoftwareOnly; + _logger.Information("MaaWineBridge status: {WineBridgeAvailability}", MaaWineBridge.Availability); + _logger.Information("MaaDesktopIntegration available: {Available}", MaaDesktopIntegration.Available); + } + + _logger.Information("==================================="); + + ConfigurationHelper.Load(); + LocalizationHelper.Load(); + ETagCache.Load(); + + // 检查 MaaCore.dll 是否存在 + if (!File.Exists("MaaCore.dll")) + { + throw new FileNotFoundException("MaaCore.dll not found!"); + } + + // 检查 resource 文件夹是否存在 + if (!Directory.Exists(PathsHelper.ResourceDir)) + { + throw new DirectoryNotFoundException("resource folder not found!"); + } + + // Debug 模式下 DLL 是未打包的 + if (maaEnv != "Debug" && !isBuildOutputFolder) + { + var unknownDlls = UnknownDllDetected(); + if (unknownDlls.Count > 0) + { + MessageBoxHelper.Show( + LocalizationHelper.GetString("UnknownDllDetected") + "\n" + string.Join("\n", unknownDlls), + "MAA", + MessageBoxButton.OK, + MessageBoxImage.Error); + _logger.Fatal("Unknown DLL(s) detected: {UnknownDlls}", string.Join(", ", unknownDlls)); + Shutdown(); + return; + } + } + + if (!IsVCppInstalled()) + { + var ret = MessageBoxHelper.Show( + LocalizationHelper.GetString("VC++NotInstalled"), + "MAA", + MessageBoxButton.OKCancel, + MessageBoxImage.Information, + ok: LocalizationHelper.GetString("Confirm"), + cancel: LocalizationHelper.GetString("Cancel")); + if (ret == MessageBoxResult.OK) + { + var startInfo = new ProcessStartInfo + { + FileName = "DependencySetup_依赖库安装.bat", + WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory, // 设置工作目录 + WindowStyle = ProcessWindowStyle.Normal, // 显示窗口让用户看到进度 + }; + + Process.Start(startInfo); + } + + Shutdown(); + return; + } + + if (!HandleMultipleInstances()) + { + Shutdown(); + return; + } + + if (!IsWritable(PathsHelper.BaseDir)) + { + Task.Run(() => MessageBoxHelper.Show(LocalizationHelper.GetString("SoftwareLocationWarning"), LocalizationHelper.GetString("Error"), MessageBoxButton.OK, MessageBoxImage.Error)); + } + + Task.Run(ParseCrashLog); + + base.OnStart(); + _hasMutex = true; + + const string ConfigFlag = "--config"; + const string AnotherFlag = "--another"; // 示例,之后如果有其他参数,可以继续添加 + + var parsedArgs = ParseArgs(args, ConfigFlag, AnotherFlag); + + if (parsedArgs.TryGetValue(ConfigFlag, out string configArgs) && Config(configArgs)) + { + // return; + } + } + + public class ClassNameEnricher : ILogEventEnricher + { + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (!logEvent.Properties.TryGetValue("SourceContext", out var sourceContextValue)) + { + return; + } + + var sourceContext = sourceContextValue.ToString().Trim('"'); + var className = sourceContext.Split('.').Last(); + className = ("[" + className + "]").PadRight(24); + logEvent.AddOrUpdateProperty(propertyFactory.CreateProperty("ClassName", className)); + } + } + + protected override void OnLaunch() + { + BadModules.CheckAndWarnBadInjectedModules(); + } + + private static bool HandleMultipleInstances() + { + // 设置互斥量的名称 + string mutexName = "MAA_" + PathsHelper.BaseDir.Replace("\\", "_").Replace(":", string.Empty); + _mutex = new Mutex(true, mutexName, out var isOnlyInstance); + + try + { + if (isOnlyInstance || _mutex.WaitOne(500)) + { + return true; + } + + MessageBoxHelper.Show(LocalizationHelper.GetString("MultiInstanceUnderSamePath"), "MAA", MessageBoxButton.OK, MessageBoxImage.Warning); + return false; + } + catch (AbandonedMutexException) + { + // 上一个程序没有正常释放互斥量 + // 即使捕获到这个异常,此时也已经获得了锁 + return true; + } + catch (Exception e) + { + MessageBoxHelper.Show(LocalizationHelper.GetString("MultiInstanceUnderSamePath") + e.Message, "MAA", MessageBoxButton.OK, MessageBoxImage.Warning); + return false; + } + } + + public static bool IsUserAdministrator() => new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); + + public static bool IsUacEnabled() + { + try + { + using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"); + if (key == null) + { + return true; + } + + var value = key.GetValue("EnableLUA"); + if (value is int intValue) + { + return intValue != 0; + } + + return true; + } + catch + { + return true; + } + } + + public static bool IsAdministratorWithUac() => IsUserAdministrator() && IsUacEnabled(); + + /// + protected override void ConfigureIoC(IStyletIoCBuilder builder) + { + builder.Bind().ToSelf().InSingletonScope(); + builder.Bind().ToSelf().InSingletonScope(); + builder.Bind().ToSelf().InSingletonScope(); + builder.Bind().ToSelf().InSingletonScope(); + + builder.Bind().ToSelf().InSingletonScope(); + builder.Bind().ToSelf().InSingletonScope(); + + builder.Bind().ToSelf().InSingletonScope(); + + builder.Bind().To().InSingletonScope(); + builder.Bind().To().InSingletonScope(); + + builder.Bind().To().InSingletonScope(); + + builder.Bind().To().InSingletonScope(); + + builder.Bind().To().InSingletonScope(); + builder.Bind().To().InSingletonScope(); + } + + protected override void Configure() + { + base.Configure(); + Instances.Instantiate(Container); + } + + /// + protected override void DisplayRootView(object rootViewModel) + { + if (Application.Current.IsShuttingDown()) + { + return; + } + + bool wasFirstBoot = Instances.VersionUpdateViewModel.IsFirstBootAfterUpdate; + + Instances.WindowManager.ShowWindow(rootViewModel); + Instances.InstantiateOnRootViewDisplayed(Container); + + // 如果 IsFirstBootAfterUpdate 从 false 变为 true,说明这次启动只是解压更新包,不用执行后续逻辑 + if (!wasFirstBoot && Instances.VersionUpdateViewModel.IsFirstBootAfterUpdate) + { + return; + } + + AchievementTrackerHelper.Events.Startup(); + + var buildTimeInterval = (DateTime.UtcNow - VersionUpdateSettingsUserControlModel.BuildDateTime).TotalDays; + var resourceTimeInterval = (DateTime.UtcNow - SettingsViewModel.VersionUpdateSettings.ResourceDateTime).TotalDays; + var maxTimeInterval = Math.Max(buildTimeInterval, resourceTimeInterval); + if (maxTimeInterval > 90) + { + Instances.TaskQueueViewModel.LogItemViewModels.Add(new(string.Format(LocalizationHelper.GetString("Achievement.Martian.ConditionsTip"), (maxTimeInterval / 30.436875).ToString("F2")), UiLogColor.Error)); + } + } + + /// + /// 退出时执行啥自己加。 + protected override void OnExit(ExitEventArgs e) + { + // MessageBox.Show("O(∩_∩)O 拜拜"); + try + { + Instances.TaskQueueViewModel.ResetAllTemporaryVariable(); + } + catch + { + // ignored + } + + Release(); + + _logger.Information("MaaAssistantArknights GUI exited"); + _logger.Information("{Message}", string.Empty); + Log.CloseAndFlush(); + base.OnExit(e); + + if (!_isRestartingAfterUpdate) + { // 如果是更新后重启,则不删除 .old + try + { // 退出时移除.old + Directory.Delete(".old", true); + } + catch (Exception) + { // ignored + } + + foreach (var file in new DirectoryInfo(".").GetFiles("*.old")) + { + try + { + file.Delete(); } catch (Exception) - { // ignored - } - - foreach (var file in new DirectoryInfo(".").GetFiles("*.old")) { - try - { - file.Delete(); - } - catch (Exception) - { - // ignored - } + // ignored } } + } - if (!_isRestartingWithoutArgs) + if (!_isRestartingWithoutArgs) + { + return; + } + + if (Environment.ProcessPath is null) + { + return; + } + + ProcessStartInfo startInfo = new ProcessStartInfo { FileName = Environment.ProcessPath, }; + + Process.Start(startInfo); + } + + public static void Release() + { + ETagCache.Save(); + Instances.SettingsViewModel.Sober(); + Instances.MaaHotKeyManager.Release(); + + // 关闭程序时清理操作中心中的通知 + ToastNotification.Cleanup(); + + ConfigurationHelper.Release(); + ConfigFactory.Release(); + + // 释放互斥量 + if (!_hasMutex) + { + return; + } + + _mutex?.ReleaseMutex(); + _mutex?.Dispose(); + } + + private static bool _isRestartingWithoutArgs; + + /// + /// 重启,不带参数 + /// + /// Caller Member Name + public static void ShutdownAndRestartWithoutArgs([CallerMemberName] string caller = "") + { + _isRestartingWithoutArgs = true; + _logger.Information("Shutdown and restart without Args, call by `{Caller}`", caller); + Execute.OnUIThread(Application.Current.Shutdown); + } + + private static bool _isRestartingAfterUpdate; + + public static void RestartAfterUpdate([CallerMemberName] string caller = "") + { + _isRestartingAfterUpdate = true; + ShutdownAndRestartWithoutArgs(); + } + + public static void Shutdown([CallerMemberName] string caller = "") + { + _logger.Information("Shutdown called by `{Caller}`", caller); + Execute.OnUIThread(Application.Current.Shutdown); + } + + private static bool _isWaitingToRestart; + + public static async Task RestartAfterIdleAsync() + { + if (_isWaitingToRestart) + { + return; + } + + _isWaitingToRestart = true; + + await RunningState.Instance.UntilIdleAsync(60000); + ShutdownAndRestartWithoutArgs(); + } + + /// + protected override void OnUnhandledException(DispatcherUnhandledExceptionEventArgs e) + { + LogUnhandledException(e.Exception); + ShowErrorDialog(e.Exception); + e.Handled = true; + } + + private static void LogUnhandledException(Exception exception) + { + if (_logger != Logger.None) + { + _logger.Fatal(exception, "Unhandled exception occurred"); + } + } + + private static void ShowErrorDialog(Exception exception) + { + Application.Current.Dispatcher.Invoke(() => + { + // DragDrop.DoDragSourceMove 会导致崩溃,但不需要退出程序 + // 这是一坨屎,但是没办法,只能这样了 + var isDragDropException = exception is COMException && exception.ToString()!.Contains("DragDrop.DoDragSourceMove"); + + var shouldExit = !isDragDropException; + + var errorView = new ErrorView(exception, shouldExit); + errorView.ShowDialog(); + }); + } + + private static Dictionary ParseArgs(string[] args, params string[] flags) + { + var result = new Dictionary(); + var flagSet = new HashSet(flags); + + for (int i = 0; i < args.Length; ++i) + { + if (flagSet.Contains(args[i]) && i + 1 < args.Length) { - return; - } - - if (Environment.ProcessPath is null) - { - return; - } - - ProcessStartInfo startInfo = new ProcessStartInfo { FileName = Environment.ProcessPath, }; - - Process.Start(startInfo); - } - - public static void Release() - { - ETagCache.Save(); - Instances.SettingsViewModel.Sober(); - Instances.MaaHotKeyManager.Release(); - - // 关闭程序时清理操作中心中的通知 - ToastNotification.Cleanup(); - - ConfigurationHelper.Release(); - ConfigFactory.Release(); - - // 释放互斥量 - if (!_hasMutex) - { - return; - } - - _mutex?.ReleaseMutex(); - _mutex?.Dispose(); - } - - private static bool _isRestartingWithoutArgs; - - /// - /// 重启,不带参数 - /// - /// Caller Member Name - public static void ShutdownAndRestartWithoutArgs([CallerMemberName] string caller = "") - { - _isRestartingWithoutArgs = true; - _logger.Information("Shutdown and restart without Args, call by `{Caller}`", caller); - Execute.OnUIThread(Application.Current.Shutdown); - } - - private static bool _isRestartingAfterUpdate; - - public static void RestartAfterUpdate([CallerMemberName] string caller = "") - { - _isRestartingAfterUpdate = true; - ShutdownAndRestartWithoutArgs(); - } - - public static void Shutdown([CallerMemberName] string caller = "") - { - _logger.Information("Shutdown called by `{Caller}`", caller); - Execute.OnUIThread(Application.Current.Shutdown); - } - - private static bool _isWaitingToRestart; - - public static async Task RestartAfterIdleAsync() - { - if (_isWaitingToRestart) - { - return; - } - - _isWaitingToRestart = true; - - await RunningState.Instance.UntilIdleAsync(60000); - ShutdownAndRestartWithoutArgs(); - } - - /// - protected override void OnUnhandledException(DispatcherUnhandledExceptionEventArgs e) - { - LogUnhandledException(e.Exception); - ShowErrorDialog(e.Exception); - e.Handled = true; - } - - private static void LogUnhandledException(Exception exception) - { - if (_logger != Logger.None) - { - _logger.Fatal(exception, "Unhandled exception occurred"); + result[args[i]] = args[i + 1]; + ++i; } } - private static void ShowErrorDialog(Exception exception) + return result; + } + + /// + /// 检查配置并切换,如果成功切换则重启 + /// + /// 配置名 + /// 切换并重启 + private static bool Config(string desiredConfig) + { + const string ConfigFile = @".\config\gui.json"; + if (!File.Exists(ConfigFile) || string.IsNullOrEmpty(desiredConfig)) { - Application.Current.Dispatcher.Invoke(() => - { - // DragDrop.DoDragSourceMove 会导致崩溃,但不需要退出程序 - // 这是一坨屎,但是没办法,只能这样了 - var isDragDropException = exception is COMException && exception.ToString()!.Contains("DragDrop.DoDragSourceMove"); - - var shouldExit = !isDragDropException; - - var errorView = new ErrorView(exception, shouldExit); - errorView.ShowDialog(); - }); - } - - private static Dictionary ParseArgs(string[] args, params string[] flags) - { - var result = new Dictionary(); - var flagSet = new HashSet(flags); - - for (int i = 0; i < args.Length; ++i) - { - if (flagSet.Contains(args[i]) && i + 1 < args.Length) - { - result[args[i]] = args[i + 1]; - ++i; - } - } - - return result; - } - - /// - /// 检查配置并切换,如果成功切换则重启 - /// - /// 配置名 - /// 切换并重启 - private static bool Config(string desiredConfig) - { - const string ConfigFile = @".\config\gui.json"; - if (!File.Exists(ConfigFile) || string.IsNullOrEmpty(desiredConfig)) - { - return false; - } - - try - { - if (UpdateConfiguration(desiredConfig)) - { - ShutdownAndRestartWithoutArgs(); - return true; - } - } - catch (Exception ex) - { - _logger.Error("Error updating configuration: {DesiredConfig}, ex: {ExMessage}", desiredConfig, ex.Message); - } - return false; } - /// - /// 切换配置 - /// - /// 配置名 - /// 是否成功切换配置 - private static bool UpdateConfiguration(string desiredConfig) + try { - // 配置名可能就包在引号中,需要转义符,如 \"a\" - string currentConfig = ConfigurationHelper.GetCurrentConfiguration(); - return currentConfig != desiredConfig && ConfigurationHelper.SwitchConfiguration(desiredConfig); + if (UpdateConfiguration(desiredConfig)) + { + ShutdownAndRestartWithoutArgs(); + return true; + } } + catch (Exception ex) + { + _logger.Error("Error updating configuration: {DesiredConfig}, ex: {ExMessage}", desiredConfig, ex.Message); + } + + return false; + } + + /// + /// 切换配置 + /// + /// 配置名 + /// 是否成功切换配置 + private static bool UpdateConfiguration(string desiredConfig) + { + // 配置名可能就包在引号中,需要转义符,如 \"a\" + string currentConfig = ConfigurationHelper.GetCurrentConfiguration(); + return currentConfig != desiredConfig && ConfigurationHelper.SwitchConfiguration(desiredConfig); } } diff --git a/src/MaaWpfGui/Models/Achievement.cs b/src/MaaWpfGui/Models/Achievement.cs index 8bf39f3da9..83713d0480 100644 --- a/src/MaaWpfGui/Models/Achievement.cs +++ b/src/MaaWpfGui/Models/Achievement.cs @@ -22,95 +22,94 @@ using MaaWpfGui.Helper; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace MaaWpfGui.Models +namespace MaaWpfGui.Models; + +public class Achievement { - public class Achievement + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Id { get; set; } = string.Empty; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool IsUnlocked { get; set; } = false; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public DateTime? UnlockedTime { get; set; } = null; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int Progress { get; set; } = 0; // 用于累计型成就 + + // 用法示例 + // 记录上次打开时间 + // achievement.CustomData["LastLaunchTime"] = DateTime.UtcNow; + // 读取时 + // if (achievement.CustomData.TryGetValue("LastLaunchTime", out var token)) + // { + // var lastTime = token.ToObject(); + // } + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public Dictionary? CustomData { get; set; } = null; + + [JsonIgnore] + public string Title => IsUnlocked ? LocalizationHelper.GetString($"Achievement.{Id}.Title") : "???"; + + [JsonIgnore] + public string Description => IsUnlocked ? LocalizationHelper.GetString($"Achievement.{Id}.Description") : "???"; + + [JsonIgnore] + public string Conditions => CanShow ? LocalizationHelper.GetString($"Achievement.{Id}.Conditions") : "???"; + + [JsonIgnore] + public DateTime? UnlockedTimeLocal => UnlockedTime?.ToLocalTime(); + + [JsonIgnore] + public bool IsHidden { get; set; } = false; + + [JsonIgnore] + public bool CanShow => !IsHidden || IsUnlocked; + + [JsonIgnore] + public bool IsProgressive => Target != 0; + + [JsonIgnore] + public int Target { get; set; } = 0; // 可选目标值 + + [JsonIgnore] + public AchievementCategory Category { get; set; } // 分组 + + [JsonIgnore] + public bool IsRare { get; set; } = false; + + [JsonIgnore] + public bool IsVisibleInSearch { get; set; } = true; + + [JsonIgnore] + public string Group { get; set; } = string.Empty; + + [JsonIgnore] + public string MedalBrushKey { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Id { get; set; } = string.Empty; - - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public bool IsUnlocked { get; set; } = false; - - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public DateTime? UnlockedTime { get; set; } = null; - - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public int Progress { get; set; } = 0; // 用于累计型成就 - - // 用法示例 - // 记录上次打开时间 - // achievement.CustomData["LastLaunchTime"] = DateTime.UtcNow; - // 读取时 - // if (achievement.CustomData.TryGetValue("LastLaunchTime", out var token)) - // { - // var lastTime = token.ToObject(); - // } - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public Dictionary? CustomData { get; set; } = null; - - [JsonIgnore] - public string Title => IsUnlocked ? LocalizationHelper.GetString($"Achievement.{Id}.Title") : "???"; - - [JsonIgnore] - public string Description => IsUnlocked ? LocalizationHelper.GetString($"Achievement.{Id}.Description") : "???"; - - [JsonIgnore] - public string Conditions => CanShow ? LocalizationHelper.GetString($"Achievement.{Id}.Conditions") : "???"; - - [JsonIgnore] - public DateTime? UnlockedTimeLocal => UnlockedTime?.ToLocalTime(); - - [JsonIgnore] - public bool IsHidden { get; set; } = false; - - [JsonIgnore] - public bool CanShow => !IsHidden || IsUnlocked; - - [JsonIgnore] - public bool IsProgressive => Target != 0; - - [JsonIgnore] - public int Target { get; set; } = 0; // 可选目标值 - - [JsonIgnore] - public AchievementCategory Category { get; set; } // 分组 - - [JsonIgnore] - public bool IsRare { get; set; } = false; - - [JsonIgnore] - public bool IsVisibleInSearch { get; set; } = true; - - [JsonIgnore] - public string Group { get; set; } = string.Empty; - - [JsonIgnore] - public string MedalBrushKey + get { - get + if (!IsUnlocked) { - if (!IsUnlocked) - { - return "LockedMedalBrush"; - } - - if (IsRare) - { - return "AchievementBrush.Rare.LinearGradientBrush"; - } - - if (IsHidden) - { - return "HiddenMedalBrush"; - } - - return $"AchievementBrush.{Category}"; + return "LockedMedalBrush"; } - } - // 新解锁的成就放前面,仅本次关闭前生效 - [JsonIgnore] - public bool IsNewUnlock { get; set; } = false; + if (IsRare) + { + return "AchievementBrush.Rare.LinearGradientBrush"; + } + + if (IsHidden) + { + return "HiddenMedalBrush"; + } + + return $"AchievementBrush.{Category}"; + } } + + // 新解锁的成就放前面,仅本次关闭前生效 + [JsonIgnore] + public bool IsNewUnlock { get; set; } = false; } diff --git a/src/MaaWpfGui/Models/ArkItem.cs b/src/MaaWpfGui/Models/ArkItem.cs index 3b16189468..8364d12cc3 100644 --- a/src/MaaWpfGui/Models/ArkItem.cs +++ b/src/MaaWpfGui/Models/ArkItem.cs @@ -14,27 +14,25 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -namespace MaaWpfGui.Models +namespace MaaWpfGui.Models; + +public class ArkItem { - [SuppressMessage("ReSharper", "UnusedMember.Global")] - public class ArkItem - { - [JsonPropertyName("classifyType")] - public string ClassifyType { get; set; } + [JsonPropertyName("classifyType")] + public string ClassifyType { get; set; } - [JsonPropertyName("description")] - public string Description { get; set; } + [JsonPropertyName("description")] + public string Description { get; set; } - [JsonPropertyName("icon")] - public string Icon { get; set; } + [JsonPropertyName("icon")] + public string Icon { get; set; } - [JsonPropertyName("name")] - public string Name { get; set; } + [JsonPropertyName("name")] + public string Name { get; set; } - [JsonPropertyName("sortId")] - public int SortId { get; set; } + [JsonPropertyName("sortId")] + public int SortId { get; set; } - [JsonPropertyName("usage")] - public string Usage { get; set; } - } + [JsonPropertyName("usage")] + public string Usage { get; set; } } diff --git a/src/MaaWpfGui/Models/ResourceUpdater.cs b/src/MaaWpfGui/Models/ResourceUpdater.cs index 74684f86c2..d34f86ddb6 100644 --- a/src/MaaWpfGui/Models/ResourceUpdater.cs +++ b/src/MaaWpfGui/Models/ResourceUpdater.cs @@ -30,422 +30,421 @@ using Newtonsoft.Json.Linq; using Serilog; using static MaaWpfGui.ViewModels.UI.VersionUpdateViewModel; -namespace MaaWpfGui.Models +namespace MaaWpfGui.Models; + +public static class ResourceUpdater { - public static class ResourceUpdater + private static readonly ILogger _logger = Log.ForContext("SourceContext", "ResourceUpdater"); + + public static async Task UpdateFromGithubAsync() { - private static readonly ILogger _logger = Log.ForContext("SourceContext", "ResourceUpdater"); + ToastNotification.ShowDirect(LocalizationHelper.GetString("GameResourceUpdating")); - public static async Task UpdateFromGithubAsync() + if (!await DownloadFullPackageAsync(MaaUrls.GithubResourceUpdate, "MaaResourceGithub.zip", true).ConfigureAwait(false)) { - ToastNotification.ShowDirect(LocalizationHelper.GetString("GameResourceUpdating")); - - if (!await DownloadFullPackageAsync(MaaUrls.GithubResourceUpdate, "MaaResourceGithub.zip", true).ConfigureAwait(false)) - { - Fail(); - return false; - } - - OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("GameResourceUpdatePreparing")); - - const string GithubZipFile = "MaaResourceGithub.zip"; - const string ExtractFolder = "MaaResourceGithub"; - - // 解压到 MaaResource 文件夹 - try - { - if (Directory.Exists(ExtractFolder)) - { - Directory.Delete(ExtractFolder, true); - } - - ZipFile.ExtractToDirectory(GithubZipFile, ExtractFolder); - } - catch (Exception e) - { - _logger.Error("Failed to extract MaaResourceGithub.zip: " + e.Message); - Fail(); - return false; - } - - // 把 \MaaResource-main 中的 cache 和 resource 文件夹复制到当前目录 - try - { - string basePath = Path.Combine(ExtractFolder, "MaaResource-main"); - foreach (var folder in new[] { "cache", "resource" }) - { - DirectoryMerge( - Path.Combine(basePath, folder), - Path.Combine(PathsHelper.BaseDir, folder)); - } - } - catch (Exception e) - { - _logger.Error("Failed to copy folders: " + e.Message); - Fail(); - return false; - } - - // 删除 MaaResource 文件夹 和 MaaResource.zip - try - { - Directory.Delete(ExtractFolder, true); - File.Delete(GithubZipFile); - } - catch (Exception e) - { - _logger.Error("Failed to delete MaaResource files: " + e.Message); - } - - SettingsViewModel.VersionUpdateSettings.NewResourceFoundInfo = string.Empty; - OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("GameResourceUpdated")); - return true; - - static void Fail() - { - string msg = LocalizationHelper.GetString("GameResourceFailed"); - ToastNotification.ShowDirect(msg); - OutputDownloadProgress(downloading: false, output: msg); - } + Fail(); + return false; } - /// - /// 从 MirrorChyan 检查更新 - /// - /// 返回一个 枚举值,指示更新检查的结果。 - /// - /// :已是最新版本。 - /// :有新版本。 - /// :有新版本,但未填写 cdk - /// :网络错误。 - /// :其他错误。 - /// - public static async Task<(CheckUpdateRetT Ret, string? UpdateUrl, string? ReleaseNote)> CheckFromMirrorChyanAsync() + OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("GameResourceUpdatePreparing")); + + const string GithubZipFile = "MaaResourceGithub.zip"; + const string ExtractFolder = "MaaResourceGithub"; + + // 解压到 MaaResource 文件夹 + try { - // https://mirrorc.top/api/resources/MaaResource/latest?current_version=<当前版本日期,从 version.json 里拿时间戳>&cdk=&sp_id=<唯一识别码> - // 响应格式为 {"code":0,"msg":"success","data":{"version_name":"2025-01-22 14:28:32.839","version_number":9,"url":"<增量更新网址>"}} - const string BaseUrl = MaaUrls.MirrorChyanResourceUpdate; - var currentVersionDateTime = VersionUpdateSettingsUserControlModel - .GetResourceVersionByClientType(SettingsViewModel.GameSettings.ClientType) - .DateTime; - var currentVersion = currentVersionDateTime.ToString("yyyy-MM-dd+HH:mm:ss.fff"); - var cdk = SettingsViewModel.VersionUpdateSettings.MirrorChyanCdk.Trim(); - var spid = HardwareInfoUtility.GetMachineGuid().StableHash(); - - var url = $"{BaseUrl}?current_version={currentVersion}&cdk={cdk}&user_agent=MaaWpfGui&sp_id={spid}"; - - HttpResponseMessage? response = null; - try + if (Directory.Exists(ExtractFolder)) { - response = await Instances.HttpService.GetAsync(new(url), uriPartial: UriPartial.Path); - } - catch (Exception e) - { - _logger.Error(e, "Failed to send GET request to {Uri}", new Uri(url).GetLeftPart(UriPartial.Path)); - _logger.Information("current_version: {CurrentVersion}, cdk: {Mask}", currentVersion, cdk.Mask()); + Directory.Delete(ExtractFolder, true); } - if (response is null) + ZipFile.ExtractToDirectory(GithubZipFile, ExtractFolder); + } + catch (Exception e) + { + _logger.Error("Failed to extract MaaResourceGithub.zip: " + e.Message); + Fail(); + return false; + } + + // 把 \MaaResource-main 中的 cache 和 resource 文件夹复制到当前目录 + try + { + string basePath = Path.Combine(ExtractFolder, "MaaResource-main"); + foreach (var folder in new[] { "cache", "resource" }) { - _logger.Error("mirrorc failed"); - ToastNotification.ShowDirect(LocalizationHelper.GetString("GameResourceFailed")); - SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = true; - return (CheckUpdateRetT.NetworkError, null, null); + DirectoryMerge( + Path.Combine(basePath, folder), + Path.Combine(PathsHelper.BaseDir, folder)); + } + } + catch (Exception e) + { + _logger.Error("Failed to copy folders: " + e.Message); + Fail(); + return false; + } + + // 删除 MaaResource 文件夹 和 MaaResource.zip + try + { + Directory.Delete(ExtractFolder, true); + File.Delete(GithubZipFile); + } + catch (Exception e) + { + _logger.Error("Failed to delete MaaResource files: " + e.Message); + } + + SettingsViewModel.VersionUpdateSettings.NewResourceFoundInfo = string.Empty; + OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("GameResourceUpdated")); + return true; + + static void Fail() + { + string msg = LocalizationHelper.GetString("GameResourceFailed"); + ToastNotification.ShowDirect(msg); + OutputDownloadProgress(downloading: false, output: msg); + } + } + + /// + /// 从 MirrorChyan 检查更新 + /// + /// 返回一个 枚举值,指示更新检查的结果。 + /// + /// :已是最新版本。 + /// :有新版本。 + /// :有新版本,但未填写 cdk + /// :网络错误。 + /// :其他错误。 + /// + public static async Task<(CheckUpdateRetT Ret, string? UpdateUrl, string? ReleaseNote)> CheckFromMirrorChyanAsync() + { + // https://mirrorc.top/api/resources/MaaResource/latest?current_version=<当前版本日期,从 version.json 里拿时间戳>&cdk=&sp_id=<唯一识别码> + // 响应格式为 {"code":0,"msg":"success","data":{"version_name":"2025-01-22 14:28:32.839","version_number":9,"url":"<增量更新网址>"}} + const string BaseUrl = MaaUrls.MirrorChyanResourceUpdate; + var currentVersionDateTime = VersionUpdateSettingsUserControlModel + .GetResourceVersionByClientType(SettingsViewModel.GameSettings.ClientType) + .DateTime; + var currentVersion = currentVersionDateTime.ToString("yyyy-MM-dd+HH:mm:ss.fff"); + var cdk = SettingsViewModel.VersionUpdateSettings.MirrorChyanCdk.Trim(); + var spid = HardwareInfoUtility.GetMachineGuid().StableHash(); + + var url = $"{BaseUrl}?current_version={currentVersion}&cdk={cdk}&user_agent=MaaWpfGui&sp_id={spid}"; + + HttpResponseMessage? response = null; + try + { + response = await Instances.HttpService.GetAsync(new(url), uriPartial: UriPartial.Path); + } + catch (Exception e) + { + _logger.Error(e, "Failed to send GET request to {Uri}", new Uri(url).GetLeftPart(UriPartial.Path)); + _logger.Information("current_version: {CurrentVersion}, cdk: {Mask}", currentVersion, cdk.Mask()); + } + + if (response is null) + { + _logger.Error("mirrorc failed"); + ToastNotification.ShowDirect(LocalizationHelper.GetString("GameResourceFailed")); + SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = true; + return (CheckUpdateRetT.NetworkError, null, null); + } + + var jsonStr = await response.Content.ReadAsStringAsync(); + _logger.Information("{jsonStr}", jsonStr); + JObject? data = null; + try + { + data = (JObject?)JsonConvert.DeserializeObject(jsonStr); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to deserialize json."); + } + + if (data is null) + { + ToastNotification.ShowDirect(LocalizationHelper.GetString("GameResourceFailed")); + SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = true; + return (CheckUpdateRetT.UnknownError, null, null); + } + + var mirrorChyanCdkExpired = data["data"]?["cdk_expired_time"]?.ToObject(); + + if (mirrorChyanCdkExpired.HasValue) + { + SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkExpiredTime = mirrorChyanCdkExpired.Value; + SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = false; + } + else + { + SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = true; + } + + var errorCode = data["code"]?.ToObject() ?? MirrorChyanErrorCode.Undivided; + if (errorCode != MirrorChyanErrorCode.Success) + { + switch (errorCode) + { + case MirrorChyanErrorCode.KeyExpired: + ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkExpired")); + + SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = false; + + // 有人会第一次就填过期的 cdk 吗 + if (SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkExpiredTime == 0) + { + SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkExpiredTime = 1; + } + + // 如果上次查出来的时间比现在的还新,说明换了 cdk,重置过期时间 + if (!SettingsViewModel.VersionUpdateSettings.IsMirrorChyanCdkExpired) + { + SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkExpiredTime = mirrorChyanCdkExpired ?? 1; + } + + break; + case MirrorChyanErrorCode.KeyInvalid: + ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkInvalid")); + AchievementTrackerHelper.Instance.Unlock(AchievementIds.MirrorChyanCdkError); + break; + case MirrorChyanErrorCode.ResourceQuotaExhausted: + ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkQuotaExhausted")); + break; + case MirrorChyanErrorCode.KeyMismatched: + ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkMismatched")); + break; + case MirrorChyanErrorCode.KeyBlocked: + ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkBlocked")); + break; + case MirrorChyanErrorCode.InvalidParams: + case MirrorChyanErrorCode.ResourceNotFound: + case MirrorChyanErrorCode.InvalidOs: + case MirrorChyanErrorCode.InvalidArch: + case MirrorChyanErrorCode.InvalidChannel: + case MirrorChyanErrorCode.Undivided: + ToastNotification.ShowDirect(data["msg"]?.ToString() ?? LocalizationHelper.GetString("GameResourceFailed")); + break; } - var jsonStr = await response.Content.ReadAsStringAsync(); - _logger.Information("{jsonStr}", jsonStr); - JObject? data = null; - try - { - data = (JObject?)JsonConvert.DeserializeObject(jsonStr); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to deserialize json."); - } - - if (data is null) - { - ToastNotification.ShowDirect(LocalizationHelper.GetString("GameResourceFailed")); - SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = true; - return (CheckUpdateRetT.UnknownError, null, null); - } - - var mirrorChyanCdkExpired = data["data"]?["cdk_expired_time"]?.ToObject(); - - if (mirrorChyanCdkExpired.HasValue) - { - SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkExpiredTime = mirrorChyanCdkExpired.Value; - SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = false; - } - else - { - SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = true; - } - - var errorCode = data["code"]?.ToObject() ?? MirrorChyanErrorCode.Undivided; - if (errorCode != MirrorChyanErrorCode.Success) - { - switch (errorCode) - { - case MirrorChyanErrorCode.KeyExpired: - ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkExpired")); - - SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = false; - - // 有人会第一次就填过期的 cdk 吗 - if (SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkExpiredTime == 0) - { - SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkExpiredTime = 1; - } - - // 如果上次查出来的时间比现在的还新,说明换了 cdk,重置过期时间 - if (!SettingsViewModel.VersionUpdateSettings.IsMirrorChyanCdkExpired) - { - SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkExpiredTime = mirrorChyanCdkExpired ?? 1; - } - - break; - case MirrorChyanErrorCode.KeyInvalid: - ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkInvalid")); - AchievementTrackerHelper.Instance.Unlock(AchievementIds.MirrorChyanCdkError); - break; - case MirrorChyanErrorCode.ResourceQuotaExhausted: - ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkQuotaExhausted")); - break; - case MirrorChyanErrorCode.KeyMismatched: - ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkMismatched")); - break; - case MirrorChyanErrorCode.KeyBlocked: - ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkBlocked")); - break; - case MirrorChyanErrorCode.InvalidParams: - case MirrorChyanErrorCode.ResourceNotFound: - case MirrorChyanErrorCode.InvalidOs: - case MirrorChyanErrorCode.InvalidArch: - case MirrorChyanErrorCode.InvalidChannel: - case MirrorChyanErrorCode.Undivided: - ToastNotification.ShowDirect(data["msg"]?.ToString() ?? LocalizationHelper.GetString("GameResourceFailed")); - break; - } - - return (CheckUpdateRetT.UnknownError, null, null); - } - - if (!DateTime.TryParse(data["data"]?["version_name"]?.ToString(), out var versionTime)) - { - ToastNotification.ShowDirect(LocalizationHelper.GetString("GameResourceFailed")); - return (CheckUpdateRetT.UnknownError, null, null); - } - - if (DateTime.Compare(currentVersionDateTime, versionTime) >= 0) - { - return (CheckUpdateRetT.AlreadyLatest, null, null); - } - - // 到这里已经确定有新版本了 - var releaseNote = data["data"]?["release_note"]?.ToString(); - _logger.Information("New version found: {DateTime:yyyy-MM-dd+HH:mm:ss.fff}, {ReleaseNote}", versionTime, releaseNote); - - releaseNote = LocalizationHelper.FormatVersion(releaseNote, versionTime); - - SettingsViewModel.VersionUpdateSettings.NewResourceFoundInfo = string.Format(LocalizationHelper.GetString("MirrorChyanResourceUpdateShortTip"), releaseNote); - - if (string.IsNullOrEmpty(cdk)) - { - return (CheckUpdateRetT.NoMirrorChyanCdk, null, releaseNote); - } - - var uri = data["data"]?["url"]?.ToString(); - if (!string.IsNullOrEmpty(uri)) - { - return (CheckUpdateRetT.OK, uri, releaseNote); - } + return (CheckUpdateRetT.UnknownError, null, null); + } + if (!DateTime.TryParse(data["data"]?["version_name"]?.ToString(), out var versionTime)) + { ToastNotification.ShowDirect(LocalizationHelper.GetString("GameResourceFailed")); return (CheckUpdateRetT.UnknownError, null, null); } - public static async Task DownloadFromMirrorChyanAsync(string? url, string? releaseNote) + if (DateTime.Compare(currentVersionDateTime, versionTime) >= 0) { - if (string.IsNullOrEmpty(url)) - { - return false; - } + return (CheckUpdateRetT.AlreadyLatest, null, null); + } - ToastNotification.ShowDirect(string.Format( - LocalizationHelper.GetString("GameResourceUpdatingMirrorChyan"), releaseNote)); + // 到这里已经确定有新版本了 + var releaseNote = data["data"]?["release_note"]?.ToString(); + _logger.Information("New version found: {DateTime:yyyy-MM-dd+HH:mm:ss.fff}, {ReleaseNote}", versionTime, releaseNote); - const string MirrorchyanZipFile = "MaaResourceMirrorchyan.zip"; - const string ExtractFolder = "MaaResourceMirrorchyan"; + releaseNote = LocalizationHelper.FormatVersion(releaseNote, versionTime); - OutputDownloadProgress(string.Empty, globalSource: false); - if (!await DownloadFullPackageAsync(url, MirrorchyanZipFile, false).ConfigureAwait(false)) - { - Fail(); - return false; - } + SettingsViewModel.VersionUpdateSettings.NewResourceFoundInfo = string.Format(LocalizationHelper.GetString("MirrorChyanResourceUpdateShortTip"), releaseNote); - OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("GameResourceUpdatePreparing")); + if (string.IsNullOrEmpty(cdk)) + { + return (CheckUpdateRetT.NoMirrorChyanCdk, null, releaseNote); + } - try - { - if (Directory.Exists(ExtractFolder)) - { - Directory.Delete(ExtractFolder, true); - } + var uri = data["data"]?["url"]?.ToString(); + if (!string.IsNullOrEmpty(uri)) + { + return (CheckUpdateRetT.OK, uri, releaseNote); + } - ZipFile.ExtractToDirectory(MirrorchyanZipFile, ExtractFolder); - } - catch (Exception e) - { - _logger.Error("Failed to extract MaaResourceMirrorchyan.zip: " + e.Message); - Fail(); - return false; - } + ToastNotification.ShowDirect(LocalizationHelper.GetString("GameResourceFailed")); + return (CheckUpdateRetT.UnknownError, null, null); + } - try - { - DirectoryMerge(ExtractFolder, PathsHelper.BaseDir); - } - catch (Exception e) - { - _logger.Error("Failed to copy folders: " + e.Message); - Fail(); - return false; - } + public static async Task DownloadFromMirrorChyanAsync(string? url, string? releaseNote) + { + if (string.IsNullOrEmpty(url)) + { + return false; + } - try + ToastNotification.ShowDirect(string.Format( + LocalizationHelper.GetString("GameResourceUpdatingMirrorChyan"), releaseNote)); + + const string MirrorchyanZipFile = "MaaResourceMirrorchyan.zip"; + const string ExtractFolder = "MaaResourceMirrorchyan"; + + OutputDownloadProgress(string.Empty, globalSource: false); + if (!await DownloadFullPackageAsync(url, MirrorchyanZipFile, false).ConfigureAwait(false)) + { + Fail(); + return false; + } + + OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("GameResourceUpdatePreparing")); + + try + { + if (Directory.Exists(ExtractFolder)) { Directory.Delete(ExtractFolder, true); - File.Delete(MirrorchyanZipFile); - } - catch (Exception e) - { - _logger.Error("Cleanup failed: " + e.Message); } - SettingsViewModel.VersionUpdateSettings.NewResourceFoundInfo = string.Empty; - AchievementTrackerHelper.Instance.Unlock(AchievementIds.MirrorChyanFirstUse); - OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("GameResourceUpdated")); - - return true; - - static void Fail() - { - string msg = LocalizationHelper.GetString("GameResourceFailed"); - ToastNotification.ShowDirect(msg); - OutputDownloadProgress(downloading: false, output: msg); - } + ZipFile.ExtractToDirectory(MirrorchyanZipFile, ExtractFolder); + } + catch (Exception e) + { + _logger.Error("Failed to extract MaaResourceMirrorchyan.zip: " + e.Message); + Fail(); + return false; } - /// - /// 检查并下载资源更新。 - /// - /// 返回一个 枚举值,指示更新检查和下载的结果。 - /// - /// :已是最新版本。 - /// :有新版本。(海外源不会自动下载) - /// :有新版本,但未填写 cdk - /// :下载成功。 - /// :网络错误。 - /// :其他错误。 - /// - public static async Task CheckAndDownloadResourceUpdate() + try { - try + DirectoryMerge(ExtractFolder, PathsHelper.BaseDir); + } + catch (Exception e) + { + _logger.Error("Failed to copy folders: " + e.Message); + Fail(); + return false; + } + + try + { + Directory.Delete(ExtractFolder, true); + File.Delete(MirrorchyanZipFile); + } + catch (Exception e) + { + _logger.Error("Cleanup failed: " + e.Message); + } + + SettingsViewModel.VersionUpdateSettings.NewResourceFoundInfo = string.Empty; + AchievementTrackerHelper.Instance.Unlock(AchievementIds.MirrorChyanFirstUse); + OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("GameResourceUpdated")); + + return true; + + static void Fail() + { + string msg = LocalizationHelper.GetString("GameResourceFailed"); + ToastNotification.ShowDirect(msg); + OutputDownloadProgress(downloading: false, output: msg); + } + } + + /// + /// 检查并下载资源更新。 + /// + /// 返回一个 枚举值,指示更新检查和下载的结果。 + /// + /// :已是最新版本。 + /// :有新版本。(海外源不会自动下载) + /// :有新版本,但未填写 cdk + /// :下载成功。 + /// :网络错误。 + /// :其他错误。 + /// + public static async Task CheckAndDownloadResourceUpdate() + { + try + { + SettingsViewModel.VersionUpdateSettings.IsCheckingForUpdates = true; + + var (ret, uri, releaseNote) = await CheckFromMirrorChyanAsync(); + if (ret == CheckUpdateRetT.NoMirrorChyanCdk) { - SettingsViewModel.VersionUpdateSettings.IsCheckingForUpdates = true; - - var (ret, uri, releaseNote) = await CheckFromMirrorChyanAsync(); - if (ret == CheckUpdateRetT.NoMirrorChyanCdk) - { - ToastNotification.ShowDirect(string.Format(LocalizationHelper.GetString("MirrorChyanResourceUpdateTip"), releaseNote)); - } - - if (ret != CheckUpdateRetT.OK) - { - return ret; - } - - if (SettingsViewModel.VersionUpdateSettings.UpdateSource == "MirrorChyan" && - await DownloadFromMirrorChyanAsync(uri, releaseNote)) - { - return CheckUpdateRetT.OnlyGameResourceUpdated; - } + ToastNotification.ShowDirect(string.Format(LocalizationHelper.GetString("MirrorChyanResourceUpdateTip"), releaseNote)); + } + if (ret != CheckUpdateRetT.OK) + { return ret; } - finally + + if (SettingsViewModel.VersionUpdateSettings.UpdateSource == "MirrorChyan" && + await DownloadFromMirrorChyanAsync(uri, releaseNote)) { - SettingsViewModel.VersionUpdateSettings.IsCheckingForUpdates = false; + return CheckUpdateRetT.OnlyGameResourceUpdated; } + + return ret; + } + finally + { + SettingsViewModel.VersionUpdateSettings.IsCheckingForUpdates = false; + } + } + + public static async Task ResourceUpdateAndReloadAsync() + { + if (SettingsViewModel.VersionUpdateSettings.IsCheckingForUpdates) + { + return; } - public static async Task ResourceUpdateAndReloadAsync() + var ret = await CheckAndDownloadResourceUpdate(); + if (ret == CheckUpdateRetT.OnlyGameResourceUpdated) { - if (SettingsViewModel.VersionUpdateSettings.IsCheckingForUpdates) - { - return; - } + ResourceReload(); + } + } - var ret = await CheckAndDownloadResourceUpdate(); - if (ret == CheckUpdateRetT.OnlyGameResourceUpdated) - { - ResourceReload(); - } + public static void ResourceReload() + { + Instances.AsstProxy.LoadResource(); + DataHelper.Reload(); + SettingsViewModel.VersionUpdateSettings.ResourceInfoUpdate(); + ToastNotification.ShowDirect(LocalizationHelper.GetString("GameResourceUpdated")); + } + + private static async Task DownloadFullPackageAsync(string url, string saveTo, bool globalSource) + { + try + { + return await Instances.HttpService.DownloadFileAsync(new(url), saveTo, "application/zip"); + } + catch (Exception e) + { + _logger.Error(e, "Failed to send GET request to {Uri}", url); + OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("GameResourceFailed"), globalSource: globalSource); + return false; + } + } + + private static void DirectoryMerge(string sourceDirName, string destDirName) + { + DirectoryInfo dir = new DirectoryInfo(sourceDirName); + DirectoryInfo[] dirs = dir.GetDirectories(); + + if (!dir.Exists) + { + throw new DirectoryNotFoundException("Source directory does not exist or could not be found: " + sourceDirName); } - public static void ResourceReload() + if (!Directory.Exists(destDirName)) { - Instances.AsstProxy.LoadResource(); - DataHelper.Reload(); - SettingsViewModel.VersionUpdateSettings.ResourceInfoUpdate(); - ToastNotification.ShowDirect(LocalizationHelper.GetString("GameResourceUpdated")); + Directory.CreateDirectory(destDirName); } - private static async Task DownloadFullPackageAsync(string url, string saveTo, bool globalSource) + FileInfo[] files = dir.GetFiles(); + foreach (FileInfo file in files) { - try - { - return await Instances.HttpService.DownloadFileAsync(new(url), saveTo, "application/zip"); - } - catch (Exception e) - { - _logger.Error(e, "Failed to send GET request to {Uri}", url); - OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("GameResourceFailed"), globalSource: globalSource); - return false; - } + string tempPath = Path.Combine(destDirName, file.Name); + file.CopyTo(tempPath, true); // 覆盖现有文件 } - private static void DirectoryMerge(string sourceDirName, string destDirName) + foreach (DirectoryInfo subDir in dirs) { - DirectoryInfo dir = new DirectoryInfo(sourceDirName); - DirectoryInfo[] dirs = dir.GetDirectories(); - - if (!dir.Exists) - { - throw new DirectoryNotFoundException("Source directory does not exist or could not be found: " + sourceDirName); - } - - if (!Directory.Exists(destDirName)) - { - Directory.CreateDirectory(destDirName); - } - - FileInfo[] files = dir.GetFiles(); - foreach (FileInfo file in files) - { - string tempPath = Path.Combine(destDirName, file.Name); - file.CopyTo(tempPath, true); // 覆盖现有文件 - } - - foreach (DirectoryInfo subDir in dirs) - { - string tempPath = Path.Combine(destDirName, subDir.Name); - DirectoryMerge(subDir.FullName, tempPath); - } + string tempPath = Path.Combine(destDirName, subDir.Name); + DirectoryMerge(subDir.FullName, tempPath); } } } diff --git a/src/MaaWpfGui/Models/StageActivityInfo.cs b/src/MaaWpfGui/Models/StageActivityInfo.cs index 38ff3642b9..85fafc3aff 100644 --- a/src/MaaWpfGui/Models/StageActivityInfo.cs +++ b/src/MaaWpfGui/Models/StageActivityInfo.cs @@ -14,55 +14,54 @@ using System; using MaaWpfGui.Services; -namespace MaaWpfGui.Models +namespace MaaWpfGui.Models; + +/// +/// Stage activity info +/// +public class StageActivityInfo { /// - /// Stage activity info + /// Gets or sets the activity tip /// - public class StageActivityInfo - { - /// - /// Gets or sets the activity tip - /// - public string Tip { get; set; } + public string Tip { get; set; } - /// - /// Gets or sets the stage name - /// - public string StageName { get; set; } + /// + /// Gets or sets the stage name + /// + public string StageName { get; set; } - /// - /// Gets or sets the activity UTC expire time - /// - public DateTime UtcExpireTime { get; set; } + /// + /// Gets or sets the activity UTC expire time + /// + public DateTime UtcExpireTime { get; set; } - /// - /// Gets or sets the activity UTC start time - /// - public DateTime UtcStartTime { get; set; } + /// + /// Gets or sets the activity UTC start time + /// + public DateTime UtcStartTime { get; set; } - /// - /// Gets or sets a value indicating whether the activity is a resource collection activity - /// - /// - /// Sets to true indicates a resource collection activity, - /// when the activity expires, will continue to check stage open days. - /// - public bool IsResourceCollection { get; set; } = false; + /// + /// Gets or sets a value indicating whether the activity is a resource collection activity + /// + /// + /// Sets to true indicates a resource collection activity, + /// when the activity expires, will continue to check stage open days. + /// + public bool IsResourceCollection { get; set; } = false; - /// - /// Gets a value indicating whether the activity is open or not - /// - public bool BeingOpen => !NotOpenYet && !IsExpired; + /// + /// Gets a value indicating whether the activity is open or not + /// + public bool BeingOpen => !NotOpenYet && !IsExpired; - /// - /// Gets a value indicating whether the activity is expired - /// - public bool IsExpired => DateTime.UtcNow >= UtcExpireTime; + /// + /// Gets a value indicating whether the activity is expired + /// + public bool IsExpired => DateTime.UtcNow >= UtcExpireTime; - /// - /// Gets a value indicating whether the activity is expired - /// - public bool NotOpenYet => DateTime.UtcNow <= UtcStartTime; - } + /// + /// Gets a value indicating whether the activity is expired + /// + public bool NotOpenYet => DateTime.UtcNow <= UtcStartTime; } diff --git a/src/MaaWpfGui/Models/StageInfo.cs b/src/MaaWpfGui/Models/StageInfo.cs index ff1e049609..24fb7511ba 100644 --- a/src/MaaWpfGui/Models/StageInfo.cs +++ b/src/MaaWpfGui/Models/StageInfo.cs @@ -17,121 +17,120 @@ using System.Linq; using MaaWpfGui.Helper; using MaaWpfGui.Utilities.ValueType; -namespace MaaWpfGui.Models +namespace MaaWpfGui.Models; + +/// +/// Stage info +/// +public class StageInfo : CombinedData { /// - /// Stage info + /// Gets or sets the stage tip /// - public class StageInfo : CombinedData + public string Tip { get; set; } + + /// + /// Gets or sets the stage open days + /// + public IEnumerable OpenDays { get; set; } + + /// + /// Gets or sets the stage associated activity + /// + public StageActivityInfo Activity { get; set; } + + /// + /// Gets or sets a value indicating whether the stage is hidden + /// + public bool IsHidden { get; set; } + + /// + /// Gets or sets the stage drop + /// + public string Drop { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public StageInfo() { - /// - /// Gets or sets the stage tip - /// - public string Tip { get; set; } + } - /// - /// Gets or sets the stage open days - /// - public IEnumerable OpenDays { get; set; } + /// + /// Initializes a new instance of the class with tip and open days. + /// + /// Stage name + /// Localization key of tip + /// Open days of week + /// Associated activity + public StageInfo(string name, string tipKey, IEnumerable openDays, StageActivityInfo activity) + { + Value = name; + Display = LocalizationHelper.GetString(name); + OpenDays = openDays; + Activity = activity; - /// - /// Gets or sets the stage associated activity - /// - public StageActivityInfo Activity { get; set; } - - /// - /// Gets or sets a value indicating whether the stage is hidden - /// - public bool IsHidden { get; set; } - - /// - /// Gets or sets the stage drop - /// - public string Drop { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public StageInfo() + if (!string.IsNullOrEmpty(tipKey)) { + Tip = LocalizationHelper.GetString(tipKey); } + } - /// - /// Initializes a new instance of the class with tip and open days. - /// - /// Stage name - /// Localization key of tip - /// Open days of week - /// Associated activity - public StageInfo(string name, string tipKey, IEnumerable openDays, StageActivityInfo activity) + /// + /// Determine whether the stage associated activity is closed + /// + /// Whether activity is closed + public bool IsActivityClosed() + { + return Activity is { BeingOpen: false, IsResourceCollection: false }; + } + + /// + /// Determine whether the stage is open + /// + /// Current day of week + /// Whether stage is open + public bool IsStageOpen(DayOfWeek dayOfWeek) + { + if (Activity != null) { - Value = name; - Display = LocalizationHelper.GetString(name); - OpenDays = openDays; - Activity = activity; - - if (!string.IsNullOrEmpty(tipKey)) - { - Tip = LocalizationHelper.GetString(tipKey); - } - } - - /// - /// Determine whether the stage associated activity is closed - /// - /// Whether activity is closed - public bool IsActivityClosed() - { - return Activity is { BeingOpen: false, IsResourceCollection: false }; - } - - /// - /// Determine whether the stage is open - /// - /// Current day of week - /// Whether stage is open - public bool IsStageOpen(DayOfWeek dayOfWeek) - { - if (Activity != null) - { - if (Activity.BeingOpen) - { - return true; - } - - // expired activity - if (!Activity.IsResourceCollection) - { - return false; - } - - // expired resource activity, check open days - } - - // resource stage - if (OpenDays != null && OpenDays.Any()) - { - return OpenDays.Contains(dayOfWeek); - } - - // regular stage, always open - return true; - } - - /// - /// Determine whether the stage is open or will open - /// - /// Whether stage is open - public bool IsStageOpenOrWillOpen() - { - // 只有活动会过期且不开放 - if (Activity == null) + if (Activity.BeingOpen) { return true; } - return !Activity.IsExpired || - Activity.IsResourceCollection; + // expired activity + if (!Activity.IsResourceCollection) + { + return false; + } + + // expired resource activity, check open days } + + // resource stage + if (OpenDays != null && OpenDays.Any()) + { + return OpenDays.Contains(dayOfWeek); + } + + // regular stage, always open + return true; + } + + /// + /// Determine whether the stage is open or will open + /// + /// Whether stage is open + public bool IsStageOpenOrWillOpen() + { + // 只有活动会过期且不开放 + if (Activity == null) + { + return true; + } + + return !Activity.IsExpired || + Activity.IsResourceCollection; } } diff --git a/src/MaaWpfGui/Models/TaskSettingVisibilityInfo.cs b/src/MaaWpfGui/Models/TaskSettingVisibilityInfo.cs index 4ea26b1038..98efce6601 100644 --- a/src/MaaWpfGui/Models/TaskSettingVisibilityInfo.cs +++ b/src/MaaWpfGui/Models/TaskSettingVisibilityInfo.cs @@ -17,225 +17,224 @@ using MaaWpfGui.Helper; using MaaWpfGui.ViewModels.UI; using Stylet; -namespace MaaWpfGui.Models +namespace MaaWpfGui.Models; + +/// +/// The task setting enable info. +/// +public class TaskSettingVisibilityInfo : PropertyChangedBase { - /// - /// The task setting enable info. - /// - public class TaskSettingVisibilityInfo : PropertyChangedBase + public const string DefaultVisibleTaskSetting = "Combat"; + + private bool _startUp; + private bool _recruit; + private bool _infrast; + private bool _fight; + private bool _mall; + private bool _award; + private bool _roguelike; + private bool _reclamation; + private bool _postAction; + private bool _custom; + + public bool WakeUp { get => _startUp; set => SetAndNotify(ref _startUp, value); } + + public bool Recruiting { get => _recruit; set => SetAndNotify(ref _recruit, value); } + + public bool Base { get => _infrast; set => SetAndNotify(ref _infrast, value); } + + public bool Combat { get => _fight; set => SetAndNotify(ref _fight, value); } + + public bool Mall { get => _mall; set => SetAndNotify(ref _mall, value); } + + public bool Mission { get => _award; set => SetAndNotify(ref _award, value); } + + public bool AutoRoguelike { get => _roguelike; set => SetAndNotify(ref _roguelike, value); } + + public bool Reclamation { get => _reclamation; set => SetAndNotify(ref _reclamation, value); } + + public bool AfterAction { get => _postAction; set => SetAndNotify(ref _postAction, value); } + + public bool Custom { get => _custom; set => SetAndNotify(ref _custom, value); } + + public static TaskSettingVisibilityInfo Instance { get; } = new(); + + // 长草任务当前选中 + public int CurrentIndex { get; set; } + + public void Set(string taskName, bool enable) { - public const string DefaultVisibleTaskSetting = "Combat"; - - private bool _startUp; - private bool _recruit; - private bool _infrast; - private bool _fight; - private bool _mall; - private bool _award; - private bool _roguelike; - private bool _reclamation; - private bool _postAction; - private bool _custom; - - public bool WakeUp { get => _startUp; set => SetAndNotify(ref _startUp, value); } - - public bool Recruiting { get => _recruit; set => SetAndNotify(ref _recruit, value); } - - public bool Base { get => _infrast; set => SetAndNotify(ref _infrast, value); } - - public bool Combat { get => _fight; set => SetAndNotify(ref _fight, value); } - - public bool Mall { get => _mall; set => SetAndNotify(ref _mall, value); } - - public bool Mission { get => _award; set => SetAndNotify(ref _award, value); } - - public bool AutoRoguelike { get => _roguelike; set => SetAndNotify(ref _roguelike, value); } - - public bool Reclamation { get => _reclamation; set => SetAndNotify(ref _reclamation, value); } - - public bool AfterAction { get => _postAction; set => SetAndNotify(ref _postAction, value); } - - public bool Custom { get => _custom; set => SetAndNotify(ref _custom, value); } - - public static TaskSettingVisibilityInfo Instance { get; } = new(); - - // 长草任务当前选中 - public int CurrentIndex { get; set; } - - public void Set(string taskName, bool enable) + bool ret = false; + if (Guide && enable) { + _currentEnableSetting = taskName; + enable = false; + } + + switch (taskName) + { + case "WakeUp": + WakeUp = enable; + SetRunning("StartUp"); + break; + case "Recruiting": + Recruiting = enable; + SetRunning("Recruit"); + break; + case "Base": + Base = enable; + SetRunning("Infrast"); + break; + case "Combat": + Combat = enable; + SetRunning("Fight"); + break; + case "Mall": + Mall = enable; + SetRunning("Mall"); + break; + case "Mission": + Mission = enable; + SetRunning("Award"); + break; + case "AutoRoguelike": + AutoRoguelike = enable; + SetRunning("Roguelike"); + break; + case "Reclamation": + Reclamation = enable; + SetRunning("Reclamation"); + break; + case "AfterAction": + AfterAction = enable; + break; + case "Custom": + Custom = enable; + SetRunning("Custom"); + break; + } + + EnableAdvancedSettings = false; + if (Mission || WakeUp || AfterAction) + { + AdvancedSettingsVisibility = false; + } + else + { + AdvancedSettingsVisibility = true; + } + + // 如果切换到的不是当前运行任务 + if (enable && !ret) + { + IsCurrentTaskRunning = false; + } + + void SetRunning(in string task) + { + if (enable && CurrentTask.StartsWith(task)) + { + IsCurrentTaskRunning = true; + ret = true; + } + } + } + + private string _currentTask = string.Empty; + + // 重构前的临时过渡 + public string CurrentTask + { + get => _currentTask; + set + { + _currentTask = value; + if (string.IsNullOrEmpty(value)) + { + IsCurrentTaskRunning = false; + return; + } + bool ret = false; - if (Guide && enable) - { - _currentEnableSetting = taskName; - enable = false; - } + CheckTask("StartUp", _startUp); + CheckTask("Recruit", _recruit); + CheckTask("Infrast", _infrast); + CheckTask("Fight", _fight); + CheckTask("Mall", _mall); + CheckTask("Award", _award); + CheckTask("Roguelike", _roguelike); + CheckTask("Reclamation", _reclamation); + CheckTask("Custom", _custom); - switch (taskName) - { - case "WakeUp": - WakeUp = enable; - SetRunning("StartUp"); - break; - case "Recruiting": - Recruiting = enable; - SetRunning("Recruit"); - break; - case "Base": - Base = enable; - SetRunning("Infrast"); - break; - case "Combat": - Combat = enable; - SetRunning("Fight"); - break; - case "Mall": - Mall = enable; - SetRunning("Mall"); - break; - case "Mission": - Mission = enable; - SetRunning("Award"); - break; - case "AutoRoguelike": - AutoRoguelike = enable; - SetRunning("Roguelike"); - break; - case "Reclamation": - Reclamation = enable; - SetRunning("Reclamation"); - break; - case "AfterAction": - AfterAction = enable; - break; - case "Custom": - Custom = enable; - SetRunning("Custom"); - break; - } - - EnableAdvancedSettings = false; - if (Mission || WakeUp || AfterAction) - { - AdvancedSettingsVisibility = false; - } - else - { - AdvancedSettingsVisibility = true; - } - - // 如果切换到的不是当前运行任务 - if (enable && !ret) + // 如果没有匹配上任何任务 + if (!ret) { IsCurrentTaskRunning = false; } - void SetRunning(in string task) + void CheckTask(string taskName, bool taskIsShown) { - if (enable && CurrentTask.StartsWith(task)) + if (taskIsShown && value.StartsWith(taskName)) { IsCurrentTaskRunning = true; ret = true; } } } - - private string _currentTask = string.Empty; - - // 重构前的临时过渡 - public string CurrentTask - { - get => _currentTask; - set - { - _currentTask = value; - if (string.IsNullOrEmpty(value)) - { - IsCurrentTaskRunning = false; - return; - } - - bool ret = false; - CheckTask("StartUp", _startUp); - CheckTask("Recruit", _recruit); - CheckTask("Infrast", _infrast); - CheckTask("Fight", _fight); - CheckTask("Mall", _mall); - CheckTask("Award", _award); - CheckTask("Roguelike", _roguelike); - CheckTask("Reclamation", _reclamation); - CheckTask("Custom", _custom); - - // 如果没有匹配上任何任务 - if (!ret) - { - IsCurrentTaskRunning = false; - } - - void CheckTask(string taskName, bool taskIsShown) - { - if (taskIsShown && value.StartsWith(taskName)) - { - IsCurrentTaskRunning = true; - ret = true; - } - } - } - } - - private bool _isCurrentTaskRunning; - - /// Gets or sets a value indicating whether 当前选中的任务是否正在运行 - public bool IsCurrentTaskRunning - { - get => _isCurrentTaskRunning; - set => SetAndNotify(ref _isCurrentTaskRunning, value); - } - - private bool _enableAdvancedSettings; - - public bool EnableAdvancedSettings - { - get => _enableAdvancedSettings; - set => SetAndNotify(ref _enableAdvancedSettings, value); - } - - private bool _advancedSettingsVisibility; - - public bool AdvancedSettingsVisibility - { - get => _advancedSettingsVisibility; - set => SetAndNotify(ref _advancedSettingsVisibility, value); - } - - private string _currentEnableSetting; - - private bool _guide = Convert.ToInt32(ConfigurationHelper.GetValue(ConfigurationKeys.GuideStepIndex, "0")) < SettingsViewModel.GuideMaxStep; - - public bool Guide - { - get => _guide; - set - { - SetAndNotify(ref _guide, value); - Set(_currentEnableSetting, !value); - } - } - - #region 双入口设置可见性 - - private bool _customInfrastPlanShowInFightSettings = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.CustomInfrastPlanShowInFightSettings, bool.FalseString)); - - public bool CustomInfrastPlanShowInFightSettings - { - get => _customInfrastPlanShowInFightSettings; - set - { - SetAndNotify(ref _customInfrastPlanShowInFightSettings, value); - ConfigurationHelper.SetValue(ConfigurationKeys.CustomInfrastPlanShowInFightSettings, value.ToString()); - } - } - - #endregion } + + private bool _isCurrentTaskRunning; + + /// Gets or sets a value indicating whether 当前选中的任务是否正在运行 + public bool IsCurrentTaskRunning + { + get => _isCurrentTaskRunning; + set => SetAndNotify(ref _isCurrentTaskRunning, value); + } + + private bool _enableAdvancedSettings; + + public bool EnableAdvancedSettings + { + get => _enableAdvancedSettings; + set => SetAndNotify(ref _enableAdvancedSettings, value); + } + + private bool _advancedSettingsVisibility; + + public bool AdvancedSettingsVisibility + { + get => _advancedSettingsVisibility; + set => SetAndNotify(ref _advancedSettingsVisibility, value); + } + + private string _currentEnableSetting; + + private bool _guide = Convert.ToInt32(ConfigurationHelper.GetValue(ConfigurationKeys.GuideStepIndex, "0")) < SettingsViewModel.GuideMaxStep; + + public bool Guide + { + get => _guide; + set + { + SetAndNotify(ref _guide, value); + Set(_currentEnableSetting, !value); + } + } + + #region 双入口设置可见性 + + private bool _customInfrastPlanShowInFightSettings = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.CustomInfrastPlanShowInFightSettings, bool.FalseString)); + + public bool CustomInfrastPlanShowInFightSettings + { + get => _customInfrastPlanShowInFightSettings; + set + { + SetAndNotify(ref _customInfrastPlanShowInFightSettings, value); + ConfigurationHelper.SetValue(ConfigurationKeys.CustomInfrastPlanShowInFightSettings, value.ToString()); + } + } + + #endregion } diff --git a/src/MaaWpfGui/Models/WindowPlacement.cs b/src/MaaWpfGui/Models/WindowPlacement.cs index 9d056e7226..4c18e52e80 100644 --- a/src/MaaWpfGui/Models/WindowPlacement.cs +++ b/src/MaaWpfGui/Models/WindowPlacement.cs @@ -14,49 +14,48 @@ using System; using System.Runtime.InteropServices; -namespace MaaWpfGui.Models +namespace MaaWpfGui.Models; + +[Serializable] +[StructLayout(LayoutKind.Sequential)] +public struct WindowPlacement { - [Serializable] - [StructLayout(LayoutKind.Sequential)] - public struct WindowPlacement + public int Length; + public int Flags; + public int ShowCmd; + public Point MinPosition; + public Point MaxPosition; + public Rect NormalPosition; +} + +[Serializable] +[StructLayout(LayoutKind.Sequential)] +public struct Rect +{ + public int Left; + public int Top; + public int Right; + public int Bottom; + + public Rect(int left, int top, int right, int bottom) { - public int Length; - public int Flags; - public int ShowCmd; - public Point MinPosition; - public Point MaxPosition; - public Rect NormalPosition; - } - - [Serializable] - [StructLayout(LayoutKind.Sequential)] - public struct Rect - { - public int Left; - public int Top; - public int Right; - public int Bottom; - - public Rect(int left, int top, int right, int bottom) - { - Left = left; - Top = top; - Right = right; - Bottom = bottom; - } - } - - [Serializable] - [StructLayout(LayoutKind.Sequential)] - public struct Point - { - public int X; - public int Y; - - public Point(int x, int y) - { - X = x; - Y = y; - } + Left = left; + Top = top; + Right = right; + Bottom = bottom; + } +} + +[Serializable] +[StructLayout(LayoutKind.Sequential)] +public struct Point +{ + public int X; + public int Y; + + public Point(int x, int y) + { + X = x; + Y = y; } } diff --git a/src/MaaWpfGui/Properties/BuildDateTimeAttribute.cs b/src/MaaWpfGui/Properties/BuildDateTimeAttribute.cs index 26d5551436..7322eb2ad5 100644 --- a/src/MaaWpfGui/Properties/BuildDateTimeAttribute.cs +++ b/src/MaaWpfGui/Properties/BuildDateTimeAttribute.cs @@ -13,11 +13,10 @@ using System; -namespace MaaWpfGui.Properties +namespace MaaWpfGui.Properties; + +[AttributeUsage(AttributeTargets.Assembly)] +public class BuildDateTimeAttribute(string date) : Attribute { - [AttributeUsage(AttributeTargets.Assembly)] - public class BuildDateTimeAttribute(string date) : Attribute - { - public DateTime BuildDateTime { get; } = DateTime.ParseExact(date, "O", null); - } + public DateTime BuildDateTime { get; } = DateTime.ParseExact(date, "O", null); } diff --git a/src/MaaWpfGui/Res/Styles/ComboBox.xaml.cs b/src/MaaWpfGui/Res/Styles/ComboBox.xaml.cs index 68420f8f1e..fd68a63d11 100644 --- a/src/MaaWpfGui/Res/Styles/ComboBox.xaml.cs +++ b/src/MaaWpfGui/Res/Styles/ComboBox.xaml.cs @@ -17,40 +17,39 @@ using System.Windows.Controls; using System.Windows.Media; using JetBrains.Annotations; -namespace MaaWpfGui.Res.Styles +namespace MaaWpfGui.Res.Styles; + +/// +/// ComboBox 的光标颜色不会跟随主题变化 +/// +[UsedImplicitly] +public partial class ComboBox { - /// - /// ComboBox 的光标颜色不会跟随主题变化 - /// - [UsedImplicitly] - public partial class ComboBox + private void ComboBox_Loaded_SetCaretBrush(object sender, RoutedEventArgs e) { - private void ComboBox_Loaded_SetCaretBrush(object sender, RoutedEventArgs e) + if (sender is not System.Windows.Controls.ComboBox cb) { - if (sender is not System.Windows.Controls.ComboBox cb) + return; + } + + SetCaret(); + + // 订阅 IsEditable 变化 + var dpd = DependencyPropertyDescriptor.FromProperty(System.Windows.Controls.ComboBox.IsEditableProperty, typeof(System.Windows.Controls.ComboBox)); + dpd.AddValueChanged(cb, (_, _) => SetCaret()); + return; + + void SetCaret() + { + if (!cb.IsEditable) { return; } - SetCaret(); - - // 订阅 IsEditable 变化 - var dpd = DependencyPropertyDescriptor.FromProperty(System.Windows.Controls.ComboBox.IsEditableProperty, typeof(System.Windows.Controls.ComboBox)); - dpd.AddValueChanged(cb, (_, _) => SetCaret()); - return; - - void SetCaret() + cb.ApplyTemplate(); + if (cb.Template.FindName("PART_EditableTextBox", cb) is TextBox tb) { - if (!cb.IsEditable) - { - return; - } - - cb.ApplyTemplate(); - if (cb.Template.FindName("PART_EditableTextBox", cb) is TextBox tb) - { - tb.CaretBrush = (Brush)Application.Current.Resources["PrimaryTextBrush"]; - } + tb.CaretBrush = (Brush)Application.Current.Resources["PrimaryTextBrush"]; } } } diff --git a/src/MaaWpfGui/Services/HotKeys/IMaaHotKeyActionHandler.cs b/src/MaaWpfGui/Services/HotKeys/IMaaHotKeyActionHandler.cs index 7b151a0473..86de4e3c6a 100644 --- a/src/MaaWpfGui/Services/HotKeys/IMaaHotKeyActionHandler.cs +++ b/src/MaaWpfGui/Services/HotKeys/IMaaHotKeyActionHandler.cs @@ -11,10 +11,9 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Services.HotKeys +namespace MaaWpfGui.Services.HotKeys; + +public interface IMaaHotKeyActionHandler { - public interface IMaaHotKeyActionHandler - { - void HandleKeyPressed(MaaHotKeyAction action); - } + void HandleKeyPressed(MaaHotKeyAction action); } diff --git a/src/MaaWpfGui/Services/HotKeys/IMaaHotKeyManager.cs b/src/MaaWpfGui/Services/HotKeys/IMaaHotKeyManager.cs index 2e2a839a84..75410666b3 100644 --- a/src/MaaWpfGui/Services/HotKeys/IMaaHotKeyManager.cs +++ b/src/MaaWpfGui/Services/HotKeys/IMaaHotKeyManager.cs @@ -11,16 +11,15 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Services.HotKeys +namespace MaaWpfGui.Services.HotKeys; + +public interface IMaaHotKeyManager { - public interface IMaaHotKeyManager - { - bool TryRegister(MaaHotKeyAction action, MaaHotKey hotKey); + bool TryRegister(MaaHotKeyAction action, MaaHotKey hotKey); - void UnRegister(MaaHotKeyAction action); + void UnRegister(MaaHotKeyAction action); - MaaHotKey GetOrNull(MaaHotKeyAction action); + MaaHotKey GetOrNull(MaaHotKeyAction action); - void Release(); - } + void Release(); } diff --git a/src/MaaWpfGui/Services/HotKeys/MaaHotKey.cs b/src/MaaWpfGui/Services/HotKeys/MaaHotKey.cs index 76be5ca45e..56de9eb3cc 100644 --- a/src/MaaWpfGui/Services/HotKeys/MaaHotKey.cs +++ b/src/MaaWpfGui/Services/HotKeys/MaaHotKey.cs @@ -15,46 +15,45 @@ using System.Text; using System.Windows.Input; using GlobalHotKey; -namespace MaaWpfGui.Services.HotKeys +namespace MaaWpfGui.Services.HotKeys; + +public class MaaHotKey : HotKey { - public class MaaHotKey : HotKey + public MaaHotKey() { - public MaaHotKey() + } + + public MaaHotKey(Key key, ModifierKeys modifiers) + : base(key, modifiers) + { + } + + public override string ToString() + { + var str = new StringBuilder(); + + if (Modifiers.HasFlag(ModifierKeys.Control)) { + str.Append("Ctrl + "); } - public MaaHotKey(Key key, ModifierKeys modifiers) - : base(key, modifiers) + if (Modifiers.HasFlag(ModifierKeys.Shift)) { + str.Append("Shift + "); } - public override string ToString() + if (Modifiers.HasFlag(ModifierKeys.Alt)) { - var str = new StringBuilder(); - - if (Modifiers.HasFlag(ModifierKeys.Control)) - { - str.Append("Ctrl + "); - } - - if (Modifiers.HasFlag(ModifierKeys.Shift)) - { - str.Append("Shift + "); - } - - if (Modifiers.HasFlag(ModifierKeys.Alt)) - { - str.Append("Alt + "); - } - - if (Modifiers.HasFlag(ModifierKeys.Windows)) - { - str.Append("Win + "); - } - - str.Append(Key); - - return str.ToString(); + str.Append("Alt + "); } + + if (Modifiers.HasFlag(ModifierKeys.Windows)) + { + str.Append("Win + "); + } + + str.Append(Key); + + return str.ToString(); } } diff --git a/src/MaaWpfGui/Services/HotKeys/MaaHotKeyAction.cs b/src/MaaWpfGui/Services/HotKeys/MaaHotKeyAction.cs index 0f4258b149..b7b008086d 100644 --- a/src/MaaWpfGui/Services/HotKeys/MaaHotKeyAction.cs +++ b/src/MaaWpfGui/Services/HotKeys/MaaHotKeyAction.cs @@ -13,19 +13,18 @@ using System; -namespace MaaWpfGui.Services.HotKeys -{ - [Flags] - public enum MaaHotKeyAction - { - /// - /// Shows the GUI. - /// - ShowGui, +namespace MaaWpfGui.Services.HotKeys; - /// - /// Starts the link. - /// - LinkStart, - } +[Flags] +public enum MaaHotKeyAction +{ + /// + /// Shows the GUI. + /// + ShowGui, + + /// + /// Starts the link. + /// + LinkStart, } diff --git a/src/MaaWpfGui/Services/HotKeys/MaaHotKeyActionHandler.cs b/src/MaaWpfGui/Services/HotKeys/MaaHotKeyActionHandler.cs index 1c6b051ab4..9116ec451a 100644 --- a/src/MaaWpfGui/Services/HotKeys/MaaHotKeyActionHandler.cs +++ b/src/MaaWpfGui/Services/HotKeys/MaaHotKeyActionHandler.cs @@ -16,70 +16,69 @@ using System.Windows; using MaaWpfGui.Helper; using MaaWpfGui.States; -namespace MaaWpfGui.Services.HotKeys +namespace MaaWpfGui.Services.HotKeys; + +public class MaaHotKeyActionHandler : IMaaHotKeyActionHandler { - public class MaaHotKeyActionHandler : IMaaHotKeyActionHandler + private readonly RunningState _runningState; + + /// + /// Initializes a new instance of the class. + /// + public MaaHotKeyActionHandler() { - private readonly RunningState _runningState; + _runningState = RunningState.Instance; + } - /// - /// Initializes a new instance of the class. - /// - public MaaHotKeyActionHandler() + /// + public void HandleKeyPressed(MaaHotKeyAction action) + { + switch (action) { - _runningState = RunningState.Instance; + case MaaHotKeyAction.ShowGui: + HandleShowGui(); + break; + + case MaaHotKeyAction.LinkStart: + HandleLinkStart(); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(action), action, null); + } + } + + protected virtual void HandleShowGui() => Instances.MainWindowManager.SwitchWindowState(); + + protected virtual void HandleLinkStart() + { + if (_runningState.GetStopping()) + { + return; } - /// - public void HandleKeyPressed(MaaHotKeyAction action) + if (_runningState.GetIdle()) { - switch (action) - { - case MaaHotKeyAction.ShowGui: - HandleShowGui(); - break; + _ = Instances.TaskQueueViewModel.LinkStart(); - case MaaHotKeyAction.LinkStart: - HandleLinkStart(); - break; - - default: - throw new ArgumentOutOfRangeException(nameof(action), action, null); - } - } - - protected virtual void HandleShowGui() => Instances.MainWindowManager.SwitchWindowState(); - - protected virtual void HandleLinkStart() - { - if (_runningState.GetStopping()) + if (Instances.MainWindowManager.GetWindowState() != WindowState.Minimized) { return; } - if (_runningState.GetIdle()) + ToastNotification.ShowDirect(LocalizationHelper.GetString("BackgroundLinkStarted")); + } + else + { + _ = Instances.TaskQueueViewModel.Stop(); + + if (Application.Current.MainWindow == null || + Application.Current.MainWindow.WindowState != WindowState.Minimized) { - _ = Instances.TaskQueueViewModel.LinkStart(); - - if (Instances.MainWindowManager.GetWindowState() != WindowState.Minimized) - { - return; - } - - ToastNotification.ShowDirect(LocalizationHelper.GetString("BackgroundLinkStarted")); + return; } - else - { - _ = Instances.TaskQueueViewModel.Stop(); - if (Application.Current.MainWindow == null || - Application.Current.MainWindow.WindowState != WindowState.Minimized) - { - return; - } - - ToastNotification.ShowDirect(LocalizationHelper.GetString("BackgroundLinkStopped")); - } + ToastNotification.ShowDirect(LocalizationHelper.GetString("BackgroundLinkStopped")); } } } diff --git a/src/MaaWpfGui/Services/HotKeys/MaaHotKeyManager.cs b/src/MaaWpfGui/Services/HotKeys/MaaHotKeyManager.cs index e2b53354fe..8c723892c3 100644 --- a/src/MaaWpfGui/Services/HotKeys/MaaHotKeyManager.cs +++ b/src/MaaWpfGui/Services/HotKeys/MaaHotKeyManager.cs @@ -18,128 +18,127 @@ using GlobalHotKey; using MaaWpfGui.Helper; using Newtonsoft.Json; -namespace MaaWpfGui.Services.HotKeys +namespace MaaWpfGui.Services.HotKeys; + +public class MaaHotKeyManager : IMaaHotKeyManager { - public class MaaHotKeyManager : IMaaHotKeyManager + private readonly Dictionary _actionHotKeyMapping = new(); + + private const string HotKeyConfigName = "HotKeys"; + + public MaaHotKeyManager() { - private readonly Dictionary _actionHotKeyMapping = new(); + Instances.HotKeyManager.KeyPressed += HotKeyManagerPressed; - private const string HotKeyConfigName = "HotKeys"; - - public MaaHotKeyManager() + foreach (var kvPair in GetPersistentHotKeys()) { - Instances.HotKeyManager.KeyPressed += HotKeyManagerPressed; - - foreach (var kvPair in GetPersistentHotKeys()) - { - TryRegister(kvPair.Key, kvPair.Value); - } - } - - public bool TryRegister(MaaHotKeyAction action, MaaHotKey hotKey) - { - InternalUnRegister(action); - - var hotKeyOwner = _actionHotKeyMapping.FirstOrDefault(x => x.Value != null && x.Value.Equals(hotKey)); - - if (hotKeyOwner.Value != null) - { - return false; - } - - try - { - Instances.HotKeyManager.Register(hotKey); - _actionHotKeyMapping[action] = hotKey; - } - catch - { - return false; - } - - try - { - PersistHotKeys(); - } - catch - { - // ignored - } - - return true; - } - - public void UnRegister(MaaHotKeyAction action) - { - InternalUnRegister(action); - - try - { - PersistHotKeys(); - } - catch - { - // ignored - } - } - - public void Release() - { - foreach (var kvPair in _actionHotKeyMapping) - { - InternalUnRegister(kvPair.Key); - } - } - - private void InternalUnRegister(MaaHotKeyAction action) - { - if (!_actionHotKeyMapping.TryGetValue(action, out var value) || value == null) - { - return; - } - - Instances.HotKeyManager.Unregister(value); - _actionHotKeyMapping[action] = null; - } - - public MaaHotKey GetOrNull(MaaHotKeyAction action) - { - return _actionHotKeyMapping.GetValueOrDefault(action); - } - - private void HotKeyManagerPressed(object sender, KeyPressedEventArgs e) - { - var action = _actionHotKeyMapping.Where(x => x.Value.Equals(e.HotKey)).Select(x => x.Key).FirstOrDefault(); - Instances.MaaHotKeyActionHandler.HandleKeyPressed(action); - } - - private static Dictionary GetPersistentHotKeys() - { - var hotKeysString = ConfigurationHelper.GetGlobalValue(HotKeyConfigName, null); - - return hotKeysString is null - ? CreateInitialHotKeys() - : JsonConvert.DeserializeObject>(hotKeysString); - } - - private static Dictionary CreateInitialHotKeys() - { - var hotKeys = new Dictionary - { - { - MaaHotKeyAction.ShowGui, new MaaHotKey(Key.M, ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt) - }, - { - MaaHotKeyAction.LinkStart, new MaaHotKey(Key.L, ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt) - }, - }; - - return hotKeys; - } - - private void PersistHotKeys() - { - ConfigurationHelper.SetGlobalValue(HotKeyConfigName, JsonConvert.SerializeObject(_actionHotKeyMapping)); + TryRegister(kvPair.Key, kvPair.Value); } } + + public bool TryRegister(MaaHotKeyAction action, MaaHotKey hotKey) + { + InternalUnRegister(action); + + var hotKeyOwner = _actionHotKeyMapping.FirstOrDefault(x => x.Value != null && x.Value.Equals(hotKey)); + + if (hotKeyOwner.Value != null) + { + return false; + } + + try + { + Instances.HotKeyManager.Register(hotKey); + _actionHotKeyMapping[action] = hotKey; + } + catch + { + return false; + } + + try + { + PersistHotKeys(); + } + catch + { + // ignored + } + + return true; + } + + public void UnRegister(MaaHotKeyAction action) + { + InternalUnRegister(action); + + try + { + PersistHotKeys(); + } + catch + { + // ignored + } + } + + public void Release() + { + foreach (var kvPair in _actionHotKeyMapping) + { + InternalUnRegister(kvPair.Key); + } + } + + private void InternalUnRegister(MaaHotKeyAction action) + { + if (!_actionHotKeyMapping.TryGetValue(action, out var value) || value == null) + { + return; + } + + Instances.HotKeyManager.Unregister(value); + _actionHotKeyMapping[action] = null; + } + + public MaaHotKey GetOrNull(MaaHotKeyAction action) + { + return _actionHotKeyMapping.GetValueOrDefault(action); + } + + private void HotKeyManagerPressed(object sender, KeyPressedEventArgs e) + { + var action = _actionHotKeyMapping.Where(x => x.Value.Equals(e.HotKey)).Select(x => x.Key).FirstOrDefault(); + Instances.MaaHotKeyActionHandler.HandleKeyPressed(action); + } + + private static Dictionary GetPersistentHotKeys() + { + var hotKeysString = ConfigurationHelper.GetGlobalValue(HotKeyConfigName, null); + + return hotKeysString is null + ? CreateInitialHotKeys() + : JsonConvert.DeserializeObject>(hotKeysString); + } + + private static Dictionary CreateInitialHotKeys() + { + var hotKeys = new Dictionary + { + { + MaaHotKeyAction.ShowGui, new MaaHotKey(Key.M, ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt) + }, + { + MaaHotKeyAction.LinkStart, new MaaHotKey(Key.L, ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt) + }, + }; + + return hotKeys; + } + + private void PersistHotKeys() + { + ConfigurationHelper.SetGlobalValue(HotKeyConfigName, JsonConvert.SerializeObject(_actionHotKeyMapping)); + } } diff --git a/src/MaaWpfGui/Services/MaaService.cs b/src/MaaWpfGui/Services/MaaService.cs index 33e7d44f65..e6b2cb1154 100644 --- a/src/MaaWpfGui/Services/MaaService.cs +++ b/src/MaaWpfGui/Services/MaaService.cs @@ -19,152 +19,151 @@ using AsstHandle = System.IntPtr; using AsstInstanceOptionKey = System.Int32; using AsstTaskId = System.Int32; -namespace MaaWpfGui.Services +namespace MaaWpfGui.Services; + +public static class MaaService { - public static class MaaService - { - public delegate void CallbackDelegate(int msg, IntPtr jsonBuffer, IntPtr customArg); + public delegate void CallbackDelegate(int msg, IntPtr jsonBuffer, IntPtr customArg); - public delegate void ProcCallbackMsg(AsstMsg msg, JObject details); + public delegate void ProcCallbackMsg(AsstMsg msg, JObject details); - [DllImport("MaaCore.dll")] - public static extern AsstHandle AsstCreateEx(CallbackDelegate callback, IntPtr customArg); + [DllImport("MaaCore.dll")] + public static extern AsstHandle AsstCreateEx(CallbackDelegate callback, IntPtr customArg); - [DllImport("MaaCore.dll")] - public static extern void AsstDestroy(AsstHandle handle); + [DllImport("MaaCore.dll")] + public static extern void AsstDestroy(AsstHandle handle); - [DllImport("MaaCore.dll")] - public static extern unsafe bool AsstSetInstanceOption(AsstHandle handle, AsstInstanceOptionKey key, byte* value); + [DllImport("MaaCore.dll")] + public static extern unsafe bool AsstSetInstanceOption(AsstHandle handle, AsstInstanceOptionKey key, byte* value); - [DllImport("MaaCore.dll")] - public static extern bool AsstSetStaticOption(AsstStaticOptionKey key, [MarshalAs(UnmanagedType.LPUTF8Str)]string value); + [DllImport("MaaCore.dll")] + public static extern bool AsstSetStaticOption(AsstStaticOptionKey key, [MarshalAs(UnmanagedType.LPUTF8Str)]string value); - [DllImport("MaaCore.dll")] - public static extern unsafe bool AsstSetUserDir(byte* dirname); + [DllImport("MaaCore.dll")] + public static extern unsafe bool AsstSetUserDir(byte* dirname); - [DllImport("MaaCore.dll")] - public static extern unsafe bool AsstLoadResource(byte* dirname); + [DllImport("MaaCore.dll")] + public static extern unsafe bool AsstLoadResource(byte* dirname); - [DllImport("MaaCore.dll")] - public static extern unsafe bool AsstConnect(AsstHandle handle, byte* adbPath, byte* address, byte* config); + [DllImport("MaaCore.dll")] + public static extern unsafe bool AsstConnect(AsstHandle handle, byte* adbPath, byte* address, byte* config); - [DllImport("MaaCore.dll")] - public static extern unsafe AsstTaskId AsstAppendTask(AsstHandle handle, byte* type, byte* taskParams); + [DllImport("MaaCore.dll")] + public static extern unsafe AsstTaskId AsstAppendTask(AsstHandle handle, byte* type, byte* taskParams); - [DllImport("MaaCore.dll")] - public static extern unsafe bool AsstSetTaskParams(AsstHandle handle, AsstTaskId id, byte* taskParams); + [DllImport("MaaCore.dll")] + public static extern unsafe bool AsstSetTaskParams(AsstHandle handle, AsstTaskId id, byte* taskParams); - [DllImport("MaaCore.dll")] - public static extern bool AsstStart(AsstHandle handle); + [DllImport("MaaCore.dll")] + public static extern bool AsstStart(AsstHandle handle); - [DllImport("MaaCore.dll")] - public static extern bool AsstRunning(AsstHandle handle); + [DllImport("MaaCore.dll")] + public static extern bool AsstRunning(AsstHandle handle); - [DllImport("MaaCore.dll")] - public static extern bool AsstStop(AsstHandle handle); + [DllImport("MaaCore.dll")] + public static extern bool AsstStop(AsstHandle handle); - [DllImport("MaaCore.dll")] - public static extern unsafe Int32 AsstAsyncScreencap(AsstHandle handle, bool block); + [DllImport("MaaCore.dll")] + public static extern unsafe Int32 AsstAsyncScreencap(AsstHandle handle, bool block); - [DllImport("MaaCore.dll")] - public static extern unsafe ulong AsstGetImage(AsstHandle handle, byte* buff, ulong buffSize); + [DllImport("MaaCore.dll")] + public static extern unsafe ulong AsstGetImage(AsstHandle handle, byte* buff, ulong buffSize); - [DllImport("MaaCore.dll")] - public static extern unsafe ulong AsstGetImageBgr(AsstHandle handle, byte* buff, ulong buffSize); + [DllImport("MaaCore.dll")] + public static extern unsafe ulong AsstGetImageBgr(AsstHandle handle, byte* buff, ulong buffSize); - [DllImport("MaaCore.dll")] - public static extern ulong AsstGetNullSize(); + [DllImport("MaaCore.dll")] + public static extern ulong AsstGetNullSize(); - [DllImport("MaaCore.dll")] - public static extern IntPtr AsstGetVersion(); + [DllImport("MaaCore.dll")] + public static extern IntPtr AsstGetVersion(); - [DllImport("MaaCore.dll")] - public static extern bool AsstBackToHome(AsstHandle handle); + [DllImport("MaaCore.dll")] + public static extern bool AsstBackToHome(AsstHandle handle); - [DllImport("MaaCore.dll")] - public static extern unsafe void AsstSetConnectionExtras(byte* name, byte* extras); - } - - public enum AsstTaskType : byte - { - /// - /// 开始唤醒。 - /// - StartUp = 0, - - /// - /// 关闭明日方舟 - /// - CloseDown, - - /// - /// 刷理智 - /// - Fight, - - /// - /// 领取奖励 - /// - Award, - - /// - /// 信用商店 - /// - Mall, - - /// - /// 基建 - /// - Infrast, - - /// - /// 招募 - /// - Recruit, - - /// - /// 肉鸽 - /// - Roguelike, - - /// - /// 自动战斗 - /// - Copilot, - - /// - /// 自动战斗-保全ver - /// - SSSCopilot, - - /// - /// 单步任务(目前仅支持战斗) - /// - SingleStep, - - /// - /// 视频识别 - /// - VideoRecognition, - - /// - /// 仓库识别 - /// - Depot, - - /// - /// 干员识别 - /// - OperBox, - - /// - /// 生息演算 - /// - Reclamation, - - /// - /// 自定义任务 - /// - Custom, - } + [DllImport("MaaCore.dll")] + public static extern unsafe void AsstSetConnectionExtras(byte* name, byte* extras); +} + +public enum AsstTaskType : byte +{ + /// + /// 开始唤醒。 + /// + StartUp = 0, + + /// + /// 关闭明日方舟 + /// + CloseDown, + + /// + /// 刷理智 + /// + Fight, + + /// + /// 领取奖励 + /// + Award, + + /// + /// 信用商店 + /// + Mall, + + /// + /// 基建 + /// + Infrast, + + /// + /// 招募 + /// + Recruit, + + /// + /// 肉鸽 + /// + Roguelike, + + /// + /// 自动战斗 + /// + Copilot, + + /// + /// 自动战斗-保全ver + /// + SSSCopilot, + + /// + /// 单步任务(目前仅支持战斗) + /// + SingleStep, + + /// + /// 视频识别 + /// + VideoRecognition, + + /// + /// 仓库识别 + /// + Depot, + + /// + /// 干员识别 + /// + OperBox, + + /// + /// 生息演算 + /// + Reclamation, + + /// + /// 自定义任务 + /// + Custom, } diff --git a/src/MaaWpfGui/Services/Managers/IMainWindowManager.cs b/src/MaaWpfGui/Services/Managers/IMainWindowManager.cs index 2d9dd5c3ce..3b65d0f79d 100644 --- a/src/MaaWpfGui/Services/Managers/IMainWindowManager.cs +++ b/src/MaaWpfGui/Services/Managers/IMainWindowManager.cs @@ -14,57 +14,56 @@ using System; using System.Windows; -namespace MaaWpfGui.Services.Managers +namespace MaaWpfGui.Services.Managers; + +/// +/// Manager of the MAA main window +/// +public interface IMainWindowManager { /// - /// Manager of the MAA main window + /// Show the main window /// - public interface IMainWindowManager - { - /// - /// Show the main window - /// - void Show(); + void Show(); - /// - /// Force show the main window - /// - void ForceShow(); + /// + /// Force show the main window + /// + void ForceShow(); - /// - /// Collapse the main window - /// - void Collapse(); + /// + /// Collapse the main window + /// + void Collapse(); - /// - /// Show the main window if it collapsed and vice versa. - /// - void SwitchWindowState(); + /// + /// Show the main window if it collapsed and vice versa. + /// + void SwitchWindowState(); - /// - /// Get the current window state of the main window - /// - /// WindowState - WindowState GetWindowState(); + /// + /// Get the current window state of the main window + /// + /// WindowState + WindowState GetWindowState(); - /// - /// Sets whether to minimize to tray. - /// - /// Whether to minimize to taskBar. - void SetMinimizeToTray(bool shouldMinimizeToTray); + /// + /// Sets whether to minimize to tray. + /// + /// Whether to minimize to taskBar. + void SetMinimizeToTray(bool shouldMinimizeToTray); - /// - /// Sets whether to use the tray icon. - /// - /// Whether to use the tray icon. - void SetUseTrayIcon(bool useTrayIcon); + /// + /// Sets whether to use the tray icon. + /// + /// Whether to use the tray icon. + void SetUseTrayIcon(bool useTrayIcon); - /// - /// Get the main window if it is visible. - /// - /// The if it is visible, or null. - Window GetWindowIfVisible(); + /// + /// Get the main window if it is visible. + /// + /// The if it is visible, or null. + Window GetWindowIfVisible(); - event EventHandler WindowRestored; - } + event EventHandler WindowRestored; } diff --git a/src/MaaWpfGui/Services/Managers/MainWindowManager.cs b/src/MaaWpfGui/Services/Managers/MainWindowManager.cs index 722480e51e..87b91bcbf4 100644 --- a/src/MaaWpfGui/Services/Managers/MainWindowManager.cs +++ b/src/MaaWpfGui/Services/Managers/MainWindowManager.cs @@ -17,169 +17,168 @@ using MaaWpfGui.Constants; using MaaWpfGui.Helper; using MaaWpfGui.Views.UI; -namespace MaaWpfGui.Services.Managers +namespace MaaWpfGui.Services.Managers; + +/// +public sealed class MainWindowManager : IMainWindowManager { - /// - public sealed class MainWindowManager : IMainWindowManager + /// + /// Gets the main window object + /// + private static Window MainWindow => Application.Current.MainWindow; + + /// + /// Gets or sets a value indicating whether minimize to tray. + /// + private bool ShouldMinimizeToTray { get; set; } + + /// + /// Gets or sets a value indicating whether to use tray icon. + /// + private bool ShouldUseTrayIcon { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public MainWindowManager() { - /// - /// Gets the main window object - /// - private static Window MainWindow => Application.Current.MainWindow; + MainWindow.StateChanged += MainWindowStateChanged; - /// - /// Gets or sets a value indicating whether minimize to tray. - /// - private bool ShouldMinimizeToTray { get; set; } + bool minimizeToTray = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.MinimizeToTray, bool.FalseString)); + SetMinimizeToTray(minimizeToTray); - /// - /// Gets or sets a value indicating whether to use tray icon. - /// - private bool ShouldUseTrayIcon { get; set; } + bool useTrayIcon = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.UseTray, bool.TrueString)); + SetUseTrayIcon(useTrayIcon); - /// - /// Initializes a new instance of the class. - /// - public MainWindowManager() + _previousState = GetWindowState(); + if (_previousState == WindowState.Minimized) { - MainWindow.StateChanged += MainWindowStateChanged; - - bool minimizeToTray = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.MinimizeToTray, bool.FalseString)); - SetMinimizeToTray(minimizeToTray); - - bool useTrayIcon = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.UseTray, bool.TrueString)); - SetUseTrayIcon(useTrayIcon); - - _previousState = GetWindowState(); - if (_previousState == WindowState.Minimized) - { - _minimizedSince = DateTime.Now; - } + _minimizedSince = DateTime.Now; } - - /// - public void Show() - { - WindowManager.ShowWindow(MainWindow); - } - - /// - public void ForceShow() - { - WindowManager.ForceShow(MainWindow); - } - - /// - public void Collapse() - { - MainWindow.WindowState = WindowState.Minimized; - } - - /// - public void SwitchWindowState() - { - if (MainWindow.WindowState == WindowState.Minimized) - { - Show(); - } - else - { - Collapse(); - } - } - - /// - public WindowState GetWindowState() => MainWindow.WindowState; - - /// - public void SetMinimizeToTray(bool shouldMinimizeToTray) - { - ShouldMinimizeToTray = shouldMinimizeToTray; - } - - /// - public void SetUseTrayIcon(bool useTrayIcon) - { - ShouldUseTrayIcon = useTrayIcon; - ((RootView)MainWindow).NotifyIcon.notifyIcon.Visibility = useTrayIcon ? Visibility.Visible : Visibility.Collapsed; - } - - private WindowState? _previousState = null; - private DateTime? _minimizedSince = null; - - /// - /// Handle the main window's state changed event - /// - /// The object that triggered the event. - /// The event arguments. - private void MainWindowStateChanged(object sender, EventArgs e) - { - var currentState = MainWindow.WindowState; - - if (ShouldMinimizeToTray && ShouldUseTrayIcon) - { - ChangeVisibility(currentState != WindowState.Minimized); - } - - // 检测进入 Minimized - if (_previousState != WindowState.Minimized && currentState == WindowState.Minimized) - { - _minimizedSince = DateTime.Now; - } - - // 触发事件:从 Minimized 恢复 - if (_previousState == WindowState.Minimized && currentState != WindowState.Minimized) - { - if (_minimizedSince != null) - { - var duration = DateTime.Now - _minimizedSince.Value; - if (duration.TotalHours >= 1) - { - AchievementTrackerHelper.Instance.Unlock(AchievementIds.AfkWatcher); - } - } - - WindowRestored?.Invoke(this, EventArgs.Empty); - } - - _previousState = currentState; - } - - /// - /// Change visibility of the main window - /// - /// A boolean indicating whether the main window should be visible or hidden. - private static void ChangeVisibility(bool visible) - { - if (visible) - { - MainWindow.ShowInTaskbar = true; - MainWindow.Visibility = Visibility.Visible; - } - else - { - MainWindow.ShowInTaskbar = false; - MainWindow.Visibility = Visibility.Hidden; - } - - ((RootView)MainWindow).NotifyIcon.hideTrayMenu.Visibility = visible ? Visibility.Visible : Visibility.Collapsed; - } - - public Window GetWindowIfVisible() - { - if (MainWindow == null || - MainWindow.WindowState == WindowState.Minimized || - MainWindow.Visibility != Visibility.Visible) - { - return null; - } - - return MainWindow; - } - - /// - /// 在窗口从最小化恢复时触发 - /// - public event EventHandler WindowRestored; } + + /// + public void Show() + { + WindowManager.ShowWindow(MainWindow); + } + + /// + public void ForceShow() + { + WindowManager.ForceShow(MainWindow); + } + + /// + public void Collapse() + { + MainWindow.WindowState = WindowState.Minimized; + } + + /// + public void SwitchWindowState() + { + if (MainWindow.WindowState == WindowState.Minimized) + { + Show(); + } + else + { + Collapse(); + } + } + + /// + public WindowState GetWindowState() => MainWindow.WindowState; + + /// + public void SetMinimizeToTray(bool shouldMinimizeToTray) + { + ShouldMinimizeToTray = shouldMinimizeToTray; + } + + /// + public void SetUseTrayIcon(bool useTrayIcon) + { + ShouldUseTrayIcon = useTrayIcon; + ((RootView)MainWindow).NotifyIcon.notifyIcon.Visibility = useTrayIcon ? Visibility.Visible : Visibility.Collapsed; + } + + private WindowState? _previousState = null; + private DateTime? _minimizedSince = null; + + /// + /// Handle the main window's state changed event + /// + /// The object that triggered the event. + /// The event arguments. + private void MainWindowStateChanged(object sender, EventArgs e) + { + var currentState = MainWindow.WindowState; + + if (ShouldMinimizeToTray && ShouldUseTrayIcon) + { + ChangeVisibility(currentState != WindowState.Minimized); + } + + // 检测进入 Minimized + if (_previousState != WindowState.Minimized && currentState == WindowState.Minimized) + { + _minimizedSince = DateTime.Now; + } + + // 触发事件:从 Minimized 恢复 + if (_previousState == WindowState.Minimized && currentState != WindowState.Minimized) + { + if (_minimizedSince != null) + { + var duration = DateTime.Now - _minimizedSince.Value; + if (duration.TotalHours >= 1) + { + AchievementTrackerHelper.Instance.Unlock(AchievementIds.AfkWatcher); + } + } + + WindowRestored?.Invoke(this, EventArgs.Empty); + } + + _previousState = currentState; + } + + /// + /// Change visibility of the main window + /// + /// A boolean indicating whether the main window should be visible or hidden. + private static void ChangeVisibility(bool visible) + { + if (visible) + { + MainWindow.ShowInTaskbar = true; + MainWindow.Visibility = Visibility.Visible; + } + else + { + MainWindow.ShowInTaskbar = false; + MainWindow.Visibility = Visibility.Hidden; + } + + ((RootView)MainWindow).NotifyIcon.hideTrayMenu.Visibility = visible ? Visibility.Visible : Visibility.Collapsed; + } + + public Window GetWindowIfVisible() + { + if (MainWindow == null || + MainWindow.WindowState == WindowState.Minimized || + MainWindow.Visibility != Visibility.Visible) + { + return null; + } + + return MainWindow; + } + + /// + /// 在窗口从最小化恢复时触发 + /// + public event EventHandler WindowRestored; } diff --git a/src/MaaWpfGui/Services/Notification/BarkNotificationProvider.cs b/src/MaaWpfGui/Services/Notification/BarkNotificationProvider.cs index ddc89a260c..66b3f9d703 100644 --- a/src/MaaWpfGui/Services/Notification/BarkNotificationProvider.cs +++ b/src/MaaWpfGui/Services/Notification/BarkNotificationProvider.cs @@ -16,82 +16,80 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; -using MaaWpfGui.Helper; using MaaWpfGui.Services.Web; using MaaWpfGui.ViewModels.UI; using Serilog; -namespace MaaWpfGui.Services.Notification +namespace MaaWpfGui.Services.Notification; + +public class BarkNotificationProvider(IHttpService httpService) : IExternalNotificationProvider { - public class BarkNotificationProvider(IHttpService httpService) : IExternalNotificationProvider + private readonly ILogger _logger = Log.ForContext(); + + public async Task SendAsync(string title, string content) { - private readonly ILogger _logger = Log.ForContext(); - - public async Task SendAsync(string title, string content) + var sendKey = SettingsViewModel.ExternalNotificationSettings.BarkSendKey; + if (string.IsNullOrWhiteSpace(sendKey)) { - var sendKey = SettingsViewModel.ExternalNotificationSettings.BarkSendKey; - if (string.IsNullOrWhiteSpace(sendKey)) - { - _logger.Warning("Failed to send Bark notification, Bark send key is empty"); - return false; - } - - var apiBase = SettingsViewModel.ExternalNotificationSettings.BarkServer; - if (string.IsNullOrWhiteSpace(apiBase)) - { - _logger.Warning("Failed to send Bark notification, Bark server address is empty"); - return false; - } - - var response = await httpService.PostAsJsonAsync( - new Uri(new Uri(apiBase), "/push"), - new BarkPostContent { Title = title, Content = content, SendKey = sendKey }); - if (response == null) - { - _logger.Warning("Failed to send Bark notification, response is null"); - return false; - } - - var data = JsonSerializer.Deserialize(response); - if (data?.Code == 200) - { - return true; - } - - _logger.Warning("Failed to send Bark notification: {Code} {Message}", data?.Code, data?.Message); + _logger.Warning("Failed to send Bark notification, Bark send key is empty"); return false; } - private class BarkPostContent + var apiBase = SettingsViewModel.ExternalNotificationSettings.BarkServer; + if (string.IsNullOrWhiteSpace(apiBase)) { - // ReSharper disable UnusedAutoPropertyAccessor.Local - // ReSharper disable UnusedMember.Local - [JsonPropertyName("device_key")] - public string? SendKey { get; set; } - - [JsonPropertyName("title")] - public string? Title { get; set; } - - [JsonPropertyName("body")] - public string? Content { get; set; } - - [JsonPropertyName("icon")] - public static string Icon { get => "https://cdn.jsdelivr.net/gh/MaaAssistantArknights/design@main/logo/maa-logo_256x256.png"; } - - // ReSharper restore UnusedAutoPropertyAccessor.Local - // ReSharper restore UnusedMember.Local + _logger.Warning("Failed to send Bark notification, Bark server address is empty"); + return false; } - private class BarkResponse + var response = await httpService.PostAsJsonAsync( + new Uri(new Uri(apiBase), "/push"), + new BarkPostContent { Title = title, Content = content, SendKey = sendKey }); + if (response == null) { - [JsonPropertyName("code")] - public int? Code { get; init; } - - [JsonPropertyName("message")] - public string? Message { get; init; } - - [JsonPropertyName("timestamp")] - public long? Timestamp { get; init; } + _logger.Warning("Failed to send Bark notification, response is null"); + return false; } + + var data = JsonSerializer.Deserialize(response); + if (data?.Code == 200) + { + return true; + } + + _logger.Warning("Failed to send Bark notification: {Code} {Message}", data?.Code, data?.Message); + return false; + } + + private class BarkPostContent + { + // ReSharper disable UnusedAutoPropertyAccessor.Local + // ReSharper disable UnusedMember.Local + [JsonPropertyName("device_key")] + public string? SendKey { get; set; } + + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonPropertyName("body")] + public string? Content { get; set; } + + [JsonPropertyName("icon")] + public static string Icon { get => "https://cdn.jsdelivr.net/gh/MaaAssistantArknights/design@main/logo/maa-logo_256x256.png"; } + + // ReSharper restore UnusedAutoPropertyAccessor.Local + // ReSharper restore UnusedMember.Local + } + + private class BarkResponse + { + [JsonPropertyName("code")] + public int? Code { get; init; } + + [JsonPropertyName("message")] + public string? Message { get; init; } + + [JsonPropertyName("timestamp")] + public long? Timestamp { get; init; } } } diff --git a/src/MaaWpfGui/Services/Notification/DummyNotificationProvider.cs b/src/MaaWpfGui/Services/Notification/DummyNotificationProvider.cs index b8699e56ab..6a764c0635 100644 --- a/src/MaaWpfGui/Services/Notification/DummyNotificationProvider.cs +++ b/src/MaaWpfGui/Services/Notification/DummyNotificationProvider.cs @@ -13,13 +13,12 @@ using System.Threading.Tasks; -namespace MaaWpfGui.Services.Notification +namespace MaaWpfGui.Services.Notification; + +public class DummyNotificationProvider : IExternalNotificationProvider { - public class DummyNotificationProvider : IExternalNotificationProvider + public Task SendAsync(string title, string content) { - public Task SendAsync(string title, string content) - { - return Task.FromResult(true); - } + return Task.FromResult(true); } } diff --git a/src/MaaWpfGui/Services/Notification/ExternalNotificationService.cs b/src/MaaWpfGui/Services/Notification/ExternalNotificationService.cs index bc2853a763..35c848d801 100644 --- a/src/MaaWpfGui/Services/Notification/ExternalNotificationService.cs +++ b/src/MaaWpfGui/Services/Notification/ExternalNotificationService.cs @@ -18,65 +18,64 @@ using MaaWpfGui.Helper; using MaaWpfGui.ViewModels.UI; using Serilog; -namespace MaaWpfGui.Services.Notification +namespace MaaWpfGui.Services.Notification; + +public static class ExternalNotificationService { - public static class ExternalNotificationService + private static readonly List _taskContainers = new List(); + + private static readonly ILogger _logger = Log.Logger; + + private static async Task SendAsync(string title, string content, bool isTest = false) { - private static readonly List _taskContainers = new List(); + var enabledProviders = SettingsViewModel.ExternalNotificationSettings.EnabledExternalNotificationProviderList; - private static readonly ILogger _logger = Log.Logger; - - private static async Task SendAsync(string title, string content, bool isTest = false) + foreach (var enabledProvider in enabledProviders) { - var enabledProviders = SettingsViewModel.ExternalNotificationSettings.EnabledExternalNotificationProviderList; - - foreach (var enabledProvider in enabledProviders) + IExternalNotificationProvider provider = enabledProvider switch { - IExternalNotificationProvider provider = enabledProvider switch - { - "ServerChan" => new ServerChanNotificationProvider(Instances.HttpService), - "Telegram" => new TelegramNotificationProvider(Instances.HttpService), - "Discord" => new DiscordNotificationProvider(Instances.HttpService), - "Discord Webhook" => new DiscordWebhookNotificationProvider(Instances.HttpService), - "Custom Webhook" => new CustomWebhookNotificationProvider(Instances.HttpService), - "SMTP" => new SmtpNotificationProvider(), - "Bark" => new BarkNotificationProvider(Instances.HttpService), - "Qmsg" => new QmsgNotificationProvider(Instances.HttpService), - _ => new DummyNotificationProvider(), - }; + "ServerChan" => new ServerChanNotificationProvider(Instances.HttpService), + "Telegram" => new TelegramNotificationProvider(Instances.HttpService), + "Discord" => new DiscordNotificationProvider(Instances.HttpService), + "Discord Webhook" => new DiscordWebhookNotificationProvider(Instances.HttpService), + "Custom Webhook" => new CustomWebhookNotificationProvider(Instances.HttpService), + "SMTP" => new SmtpNotificationProvider(), + "Bark" => new BarkNotificationProvider(Instances.HttpService), + "Qmsg" => new QmsgNotificationProvider(Instances.HttpService), + _ => new DummyNotificationProvider(), + }; - var result = false; - try - { - result = await provider.SendAsync(title, content); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to send External Notifications"); - } - - if (isTest is false && result) - { - return; - } - - ToastNotification.ShowDirect( - enabledProvider + " " + - LocalizationHelper.GetString(result ? "ExternalNotificationSendSuccess" : "ExternalNotificationSendFail")); + var result = false; + try + { + result = await provider.SendAsync(title, content); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to send External Notifications"); } - } - /// - /// Send notification - /// - /// The title of the notification - /// The content of the notification - /// Indicate if it is a test or not. - public static void Send(string title, string content, bool isTest = false) - { - var task = SendAsync("[MAA] " + title, content, isTest); - _taskContainers.RemoveAll(x => x.Status != TaskStatus.Running); - _taskContainers.Add(task); + if (isTest is false && result) + { + return; + } + + ToastNotification.ShowDirect( + enabledProvider + " " + + LocalizationHelper.GetString(result ? "ExternalNotificationSendSuccess" : "ExternalNotificationSendFail")); } } + + /// + /// Send notification + /// + /// The title of the notification + /// The content of the notification + /// Indicate if it is a test or not. + public static void Send(string title, string content, bool isTest = false) + { + var task = SendAsync("[MAA] " + title, content, isTest); + _taskContainers.RemoveAll(x => x.Status != TaskStatus.Running); + _taskContainers.Add(task); + } } diff --git a/src/MaaWpfGui/Services/Notification/IExternalNotificationProvider.cs b/src/MaaWpfGui/Services/Notification/IExternalNotificationProvider.cs index 72f85c49b3..648e2a0e2a 100644 --- a/src/MaaWpfGui/Services/Notification/IExternalNotificationProvider.cs +++ b/src/MaaWpfGui/Services/Notification/IExternalNotificationProvider.cs @@ -13,16 +13,15 @@ using System.Threading.Tasks; -namespace MaaWpfGui.Services.Notification +namespace MaaWpfGui.Services.Notification; + +public interface IExternalNotificationProvider { - public interface IExternalNotificationProvider - { - /// - /// Send notification - /// - /// The title of the notification - /// The content of the notification - /// True for success, False for fail - public Task SendAsync(string title, string content); - } + /// + /// Send notification + /// + /// The title of the notification + /// The content of the notification + /// True for success, False for fail + public Task SendAsync(string title, string content); } diff --git a/src/MaaWpfGui/Services/Notification/QmsgNotificationProvider.cs b/src/MaaWpfGui/Services/Notification/QmsgNotificationProvider.cs index 091fb08875..26b5995ba9 100644 --- a/src/MaaWpfGui/Services/Notification/QmsgNotificationProvider.cs +++ b/src/MaaWpfGui/Services/Notification/QmsgNotificationProvider.cs @@ -22,72 +22,71 @@ using MaaWpfGui.ViewModels.UI; using Newtonsoft.Json; using Serilog; -namespace MaaWpfGui.Services.Notification +namespace MaaWpfGui.Services.Notification; + +public class QmsgNotificationProvider(IHttpService httpService) : IExternalNotificationProvider { - public class QmsgNotificationProvider(IHttpService httpService) : IExternalNotificationProvider + private readonly ILogger _logger = Log.ForContext(); + + public async Task SendAsync(string title, string content) { - private readonly ILogger _logger = Log.ForContext(); + var server = SettingsViewModel.ExternalNotificationSettings.QmsgServer; + var key = SettingsViewModel.ExternalNotificationSettings.QmsgKey; + var receiveUser = SettingsViewModel.ExternalNotificationSettings.QmsgUser; + var sendBot = SettingsViewModel.ExternalNotificationSettings.QmsgBot; - public async Task SendAsync(string title, string content) + var uri = $"{server}/jsend/{key}"; + + var response = await httpService.PostAsJsonAsync( + new Uri(uri), + new QmsgContent { Msg = content, Qq = receiveUser, Bot = sendBot, }); + + if (string.IsNullOrEmpty(response)) { - var server = SettingsViewModel.ExternalNotificationSettings.QmsgServer; - var key = SettingsViewModel.ExternalNotificationSettings.QmsgKey; - var receiveUser = SettingsViewModel.ExternalNotificationSettings.QmsgUser; - var sendBot = SettingsViewModel.ExternalNotificationSettings.QmsgBot; - - var uri = $"{server}/jsend/{key}"; - - var response = await httpService.PostAsJsonAsync( - new Uri(uri), - new QmsgContent { Msg = content, Qq = receiveUser, Bot = sendBot, }); - - if (string.IsNullOrEmpty(response)) - { - _logger.Warning("Failed to send Qmsg notification"); - return false; - } - - var responseRoot = JsonDocument.Parse(response).RootElement; - var hasCodeProperty = responseRoot.TryGetProperty("success", out var codeElement); - if (hasCodeProperty is false) - { - _logger.Warning("Failed to send Qmsg notification, unknown response, {Response}", response); - return false; - } - - var success = codeElement.GetBoolean(); - switch (success) - { - case false: - _logger.Warning("Failed to send Qmsg notification, unknown response {Response}", response); - return false; - case true: - return true; - } + _logger.Warning("Failed to send Qmsg notification"); + return false; } - private class QmsgContent + var responseRoot = JsonDocument.Parse(response).RootElement; + var hasCodeProperty = responseRoot.TryGetProperty("success", out var codeElement); + if (hasCodeProperty is false) { - // 消息内容 - // ReSharper disable UnusedAutoPropertyAccessor.Local - [JsonPropertyName("msg")] - public string Msg { get; set; } + _logger.Warning("Failed to send Qmsg notification, unknown response, {Response}", response); + return false; + } - [JsonPropertyName("qq")] - public string Qq { get; set; } + var success = codeElement.GetBoolean(); + switch (success) + { + case false: + _logger.Warning("Failed to send Qmsg notification, unknown response {Response}", response); + return false; + case true: + return true; + } + } - [JsonPropertyName("bot")] - public string Bot { get; set; } + private class QmsgContent + { + // 消息内容 + // ReSharper disable UnusedAutoPropertyAccessor.Local + [JsonPropertyName("msg")] + public string Msg { get; set; } - /// - /// 转换为Dictionary - /// - public Dictionary ToDictionary() - { - var objstr = JsonConvert.SerializeObject(this); - var map = JsonConvert.DeserializeObject>(objstr); - return map; - } + [JsonPropertyName("qq")] + public string Qq { get; set; } + + [JsonPropertyName("bot")] + public string Bot { get; set; } + + /// + /// 转换为Dictionary + /// + public Dictionary ToDictionary() + { + var objstr = JsonConvert.SerializeObject(this); + var map = JsonConvert.DeserializeObject>(objstr); + return map; } } } diff --git a/src/MaaWpfGui/Services/Notification/ServerChanNotificationProvider.cs b/src/MaaWpfGui/Services/Notification/ServerChanNotificationProvider.cs index 2dbd1f8d39..ce25db0f55 100644 --- a/src/MaaWpfGui/Services/Notification/ServerChanNotificationProvider.cs +++ b/src/MaaWpfGui/Services/Notification/ServerChanNotificationProvider.cs @@ -17,77 +17,75 @@ using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; -using MaaWpfGui.Helper; using MaaWpfGui.Services.Web; using MaaWpfGui.ViewModels.UI; using Serilog; -namespace MaaWpfGui.Services.Notification +namespace MaaWpfGui.Services.Notification; + +public class ServerChanNotificationProvider(IHttpService httpService) : IExternalNotificationProvider { - public class ServerChanNotificationProvider(IHttpService httpService) : IExternalNotificationProvider + private readonly ILogger _logger = Log.ForContext(); + + public async Task SendAsync(string title, string content) { - private readonly ILogger _logger = Log.ForContext(); + // 去掉 title 中的换行符 + title = title.Replace("\n", string.Empty); - public async Task SendAsync(string title, string content) + // 确保 title 的长度不超过 32 个字符 + if (title.Length > 32) { - // 去掉 title 中的换行符 - title = title.Replace("\n", string.Empty); - - // 确保 title 的长度不超过 32 个字符 - if (title.Length > 32) - { - title = title[..32]; // 截取前 32 个字符 - } - - var sendKey = SettingsViewModel.ExternalNotificationSettings.ServerChanSendKey; - - try - { - var url = ConstructUrl(sendKey); - var postData = $"text={Uri.EscapeDataString(title)}&desp={Uri.EscapeDataString(content)}"; - - var response = await httpService.PostAsync(new(url), new StringContent(postData, Encoding.UTF8, "application/x-www-form-urlencoded")); - var responseContent = await response.Content.ReadAsStringAsync(); - - var responseRoot = JsonDocument.Parse(responseContent).RootElement; - if (responseRoot.TryGetProperty("code", out var codeElement) && codeElement.TryGetInt32(out var code)) - { - if (code == 0) - { - return true; - } - - _logger.Warning("Failed to send ServerChan notification, code: {Value}", code); - } - else - { - _logger.Warning("Failed to send ServerChan notification, unknown response: {ResponseContent}", responseContent); - } - } - catch (Exception ex) - { - _logger.Error(ex, "Exception occurred while sending ServerChan notification."); - } - - return false; + title = title[..32]; // 截取前 32 个字符 } - private static string ConstructUrl(string sendKey) + var sendKey = SettingsViewModel.ExternalNotificationSettings.ServerChanSendKey; + + try { - if (!sendKey.StartsWith("sctp")) - { - return $"https://sctapi.ftqq.com/{sendKey}.send"; - } + var url = ConstructUrl(sendKey); + var postData = $"text={Uri.EscapeDataString(title)}&desp={Uri.EscapeDataString(content)}"; - // Server酱3 - var match = Regex.Match(sendKey, @"^sctp(\d+)t"); - if (!match.Success) - { - throw new ArgumentException("Invalid key format for sctp."); - } + var response = await httpService.PostAsync(new(url), new StringContent(postData, Encoding.UTF8, "application/x-www-form-urlencoded")); + var responseContent = await response.Content.ReadAsStringAsync(); - var num = match.Groups[1].Value; - return $"https://{num}.push.ft07.com/send/{sendKey}.send"; + var responseRoot = JsonDocument.Parse(responseContent).RootElement; + if (responseRoot.TryGetProperty("code", out var codeElement) && codeElement.TryGetInt32(out var code)) + { + if (code == 0) + { + return true; + } + + _logger.Warning("Failed to send ServerChan notification, code: {Value}", code); + } + else + { + _logger.Warning("Failed to send ServerChan notification, unknown response: {ResponseContent}", responseContent); + } } + catch (Exception ex) + { + _logger.Error(ex, "Exception occurred while sending ServerChan notification."); + } + + return false; + } + + private static string ConstructUrl(string sendKey) + { + if (!sendKey.StartsWith("sctp")) + { + return $"https://sctapi.ftqq.com/{sendKey}.send"; + } + + // Server酱3 + var match = Regex.Match(sendKey, @"^sctp(\d+)t"); + if (!match.Success) + { + throw new ArgumentException("Invalid key format for sctp."); + } + + var num = match.Groups[1].Value; + return $"https://{num}.push.ft07.com/send/{sendKey}.send"; } } diff --git a/src/MaaWpfGui/Services/Notification/SmtpNotificationProvider.cs b/src/MaaWpfGui/Services/Notification/SmtpNotificationProvider.cs index e35d4f0c84..8e952e3462 100644 --- a/src/MaaWpfGui/Services/Notification/SmtpNotificationProvider.cs +++ b/src/MaaWpfGui/Services/Notification/SmtpNotificationProvider.cs @@ -26,178 +26,177 @@ using MaaWpfGui.ViewModels.UI; using Microsoft.Extensions.Options; using Serilog; -namespace MaaWpfGui.Services.Notification +namespace MaaWpfGui.Services.Notification; + +/// +public partial class SmtpNotificationProvider : IExternalNotificationProvider { - /// - public partial class SmtpNotificationProvider : IExternalNotificationProvider + private readonly ILogger _logger = Log.ForContext(); + + [GeneratedRegex(@"\[(.*?)\]\[(.*?)\]([\s\S]*?)(?=\n\[|$)")] + private static partial Regex ContentRegex(); + + private static string ProcessContent(string content) { - private readonly ILogger _logger = Log.ForContext(); - - [GeneratedRegex(@"\[(.*?)\]\[(.*?)\]([\s\S]*?)(?=\n\[|$)")] - private static partial Regex ContentRegex(); - - private static string ProcessContent(string content) + var matches = ContentRegex().Matches(content); + if (matches.Count == 0) { - var matches = ContentRegex().Matches(content); - if (matches.Count == 0) - { - return content; - } - - var resultContent = new StringBuilder(content); - - string timeRgbColor = GetRgbColor(UiLogColor.Trace); - if (timeRgbColor == null) - { - return content; - } - - foreach (Match match in matches) - { - string time = match.Groups[1].Value; - string colorCode = match.Groups[2].Value; - string contentText = match.Groups[3].Value; - - string rgbColor = GetRgbColor(colorCode); - if (rgbColor == null) - { - continue; - } - - string replacement = $"{time} {contentText}"; - resultContent.Replace(match.Value, replacement); - } - - return resultContent.ToString(); - - static string GetRgbColor(string resourceKey) - { - return Application.Current.Resources[resourceKey] is SolidColorBrush brush - ? $"rgb({brush.Color.R}, {brush.Color.G}, {brush.Color.B})" - : null; - } + return content; } - public async Task SendAsync(string title, string content) + var resultContent = new StringBuilder(content); + + string timeRgbColor = GetRgbColor(UiLogColor.Trace); + if (timeRgbColor == null) { - content = ProcessContent(content); + return content; + } - var smtpServer = SettingsViewModel.ExternalNotificationSettings.SmtpServer; - var smtpPortValid = int.TryParse(SettingsViewModel.ExternalNotificationSettings.SmtpPort, out var smtpPort); - var smtpUser = SettingsViewModel.ExternalNotificationSettings.SmtpUser; - var smtpPassword = SettingsViewModel.ExternalNotificationSettings.SmtpPassword; - var smtpUseSsl = SettingsViewModel.ExternalNotificationSettings.SmtpUseSsl; - var smtpRequiresAuthentication = SettingsViewModel.ExternalNotificationSettings.SmtpRequireAuthentication; + foreach (Match match in matches) + { + string time = match.Groups[1].Value; + string colorCode = match.Groups[2].Value; + string contentText = match.Groups[3].Value; - if (string.IsNullOrEmpty(smtpServer) || smtpPortValid is false) + string rgbColor = GetRgbColor(colorCode); + if (rgbColor == null) { - _logger.Error("Failed to send Email notification, invalid SMTP configuration"); - return false; + continue; } - Email.DefaultSender = new MailKitSender(new SmtpClientOptions - { - Server = smtpServer, - Port = smtpPort, - User = smtpUser, - Password = smtpPassword, - RequiresAuthentication = smtpRequiresAuthentication, - UseSsl = smtpUseSsl, - UsePickupDirectory = false, - }); + string replacement = $"{time} {contentText}"; + resultContent.Replace(match.Value, replacement); + } - Email.DefaultRenderer = new LiquidRenderer(new OptionsWrapper(new LiquidRendererOptions())); + return resultContent.ToString(); - var emailFrom = SettingsViewModel.ExternalNotificationSettings.SmtpFrom; - var emailTo = SettingsViewModel.ExternalNotificationSettings.SmtpTo; + static string GetRgbColor(string resourceKey) + { + return Application.Current.Resources[resourceKey] is SolidColorBrush brush + ? $"rgb({brush.Color.R}, {brush.Color.G}, {brush.Color.B})" + : null; + } + } - title = title.Replace("\r", string.Empty).Replace("\n", string.Empty); - content = content.Replace("\r", string.Empty).Replace("\n", "
"); + public async Task SendAsync(string title, string content) + { + content = ProcessContent(content); - var email = Email - .From(emailFrom) - .To(emailTo) - .Subject($"{title}") - .Body(_emailTemplate.Replace("{title}", title).Replace("{content}", content), true); + var smtpServer = SettingsViewModel.ExternalNotificationSettings.SmtpServer; + var smtpPortValid = int.TryParse(SettingsViewModel.ExternalNotificationSettings.SmtpPort, out var smtpPort); + var smtpUser = SettingsViewModel.ExternalNotificationSettings.SmtpUser; + var smtpPassword = SettingsViewModel.ExternalNotificationSettings.SmtpPassword; + var smtpUseSsl = SettingsViewModel.ExternalNotificationSettings.SmtpUseSsl; + var smtpRequiresAuthentication = SettingsViewModel.ExternalNotificationSettings.SmtpRequireAuthentication; - var sendResult = await email.SendAsync(); - - if (sendResult.Successful) - { - _logger.Information("Successfully sent Email notification to {EmailTo}", emailTo); - return true; - } - - _logger.Warning("Failed to send Email notification to {EmailTo}, {SendResultErrorMessages}", emailTo, sendResult.ErrorMessages); + if (string.IsNullOrEmpty(smtpServer) || smtpPortValid is false) + { + _logger.Error("Failed to send Email notification, invalid SMTP configuration"); return false; } - private static readonly string _emailTemplate = - $$""" - - - -

Maa Assistant Arknights

- -
-

{{LocalizationHelper.GetString("ExternalNotificationEmailTemplateHello")}}

-
- -
- -
-

{title}

-

{content}

-
- -
- - - - """; + Email.DefaultSender = new MailKitSender(new SmtpClientOptions + { + Server = smtpServer, + Port = smtpPort, + User = smtpUser, + Password = smtpPassword, + RequiresAuthentication = smtpRequiresAuthentication, + UseSsl = smtpUseSsl, + UsePickupDirectory = false, + }); + + Email.DefaultRenderer = new LiquidRenderer(new OptionsWrapper(new LiquidRendererOptions())); + + var emailFrom = SettingsViewModel.ExternalNotificationSettings.SmtpFrom; + var emailTo = SettingsViewModel.ExternalNotificationSettings.SmtpTo; + + title = title.Replace("\r", string.Empty).Replace("\n", string.Empty); + content = content.Replace("\r", string.Empty).Replace("\n", "
"); + + var email = Email + .From(emailFrom) + .To(emailTo) + .Subject($"{title}") + .Body(_emailTemplate.Replace("{title}", title).Replace("{content}", content), true); + + var sendResult = await email.SendAsync(); + + if (sendResult.Successful) + { + _logger.Information("Successfully sent Email notification to {EmailTo}", emailTo); + return true; + } + + _logger.Warning("Failed to send Email notification to {EmailTo}, {SendResultErrorMessages}", emailTo, sendResult.ErrorMessages); + return false; } + + private static readonly string _emailTemplate = + $$""" + + + +

Maa Assistant Arknights

+ +
+

{{LocalizationHelper.GetString("ExternalNotificationEmailTemplateHello")}}

+
+ +
+ +
+

{title}

+

{content}

+
+ +
+ + + + """; } diff --git a/src/MaaWpfGui/Services/RemoteControl/RemoteControlService.cs b/src/MaaWpfGui/Services/RemoteControl/RemoteControlService.cs index b555dbbf20..29eef17a08 100644 --- a/src/MaaWpfGui/Services/RemoteControl/RemoteControlService.cs +++ b/src/MaaWpfGui/Services/RemoteControl/RemoteControlService.cs @@ -34,358 +34,301 @@ using Stylet; using Windows.ApplicationModel; using static MaaWpfGui.Main.AsstProxy; -namespace MaaWpfGui.Services.RemoteControl +namespace MaaWpfGui.Services.RemoteControl; + +/// +/// The view model of remote control. +/// +// 通过 container.Get(); 实例化或获取实例 +// ReSharper disable once ClassNeverInstantiated.Global +public class RemoteControlService { - /// - /// The view model of remote control. - /// - // 通过 container.Get(); 实例化或获取实例 - // ReSharper disable once ClassNeverInstantiated.Global - public class RemoteControlService + private Task _pollJobTask = Task.CompletedTask; + private readonly List _enqueueTaskIds = []; + private readonly ConcurrentQueue _sequentialTaskQueue = new(); + private readonly ConcurrentQueue _instantTaskQueue = new(); + private Task _executeSequentialJobTask = Task.CompletedTask; + private Task _executeInstantJobTask = Task.CompletedTask; + private readonly RunningState _runningState; + private bool _inited = false; + + private string _currentSequentialTaskId = string.Empty; + + private static RemoteControlUserControlModel RemoteSettings => SettingsViewModel.RemoteControlSettings; + + public RemoteControlService() { - private Task _pollJobTask = Task.CompletedTask; - private readonly List _enqueueTaskIds = []; - private readonly ConcurrentQueue _sequentialTaskQueue = new(); - private readonly ConcurrentQueue _instantTaskQueue = new(); - private Task _executeSequentialJobTask = Task.CompletedTask; - private Task _executeInstantJobTask = Task.CompletedTask; - private readonly RunningState _runningState; - private bool _inited = false; + InitializePollJobTask(); + _runningState = RunningState.Instance; + } - private string _currentSequentialTaskId = string.Empty; - - private static RemoteControlUserControlModel RemoteSettings => SettingsViewModel.RemoteControlSettings; - - public RemoteControlService() + public void InitializePollJobTask() + { + if (_inited) { - InitializePollJobTask(); - _runningState = RunningState.Instance; + return; } - public void InitializePollJobTask() + if (!IsEndpointValid(RemoteSettings.RemoteControlGetTaskEndpointUri)) { - if (_inited) - { - return; - } + return; + } - if (!IsEndpointValid(RemoteSettings.RemoteControlGetTaskEndpointUri)) - { - return; - } + _inited = true; - _inited = true; - - _pollJobTask = _pollJobTask.ContinueWith(async _ => + _pollJobTask = _pollJobTask.ContinueWith(async _ => + { + while (true) { - while (true) + await Task.Delay(RemoteSettings.RemoteControlPollIntervalMs); + try { - await Task.Delay(RemoteSettings.RemoteControlPollIntervalMs); - try + if (!IsEndpointValid(RemoteSettings.RemoteControlGetTaskEndpointUri)) { - if (!IsEndpointValid(RemoteSettings.RemoteControlGetTaskEndpointUri)) - { - Log.Logger.Information("RemoteControlGetTaskEndpointUri is not valid, return"); - _inited = false; - return; - } + Log.Logger.Information("RemoteControlGetTaskEndpointUri is not valid, return"); + _inited = false; + return; + } - await PollJobTaskLoop(); - } - catch (Exception ex) - { - Log.Logger.Error(ex, "RemoteControl service raises unknown error."); - } + await PollJobTaskLoop(); } - - // ReSharper disable once FunctionNeverReturns - }); - - _executeSequentialJobTask = _executeSequentialJobTask.ContinueWith(async _ => - { - while (true) + catch (Exception ex) { - await Task.Delay(RemoteSettings.RemoteControlPollIntervalMs); - try - { - if (!IsEndpointValid(RemoteSettings.RemoteControlGetTaskEndpointUri)) - { - Log.Logger.Information("RemoteControlGetTaskEndpointUri is not valid, return"); - return; - } - - await ExecuteSequentialJobLoop(); - } - catch (Exception ex) - { - Log.Logger.Error(ex, "RemoteControl service raises unknown error."); - } - } - - // ReSharper disable once FunctionNeverReturns - }); - - _executeInstantJobTask = _executeInstantJobTask.ContinueWith(async _ => - { - while (true) - { - await Task.Delay(RemoteSettings.RemoteControlPollIntervalMs); - try - { - if (!IsEndpointValid(RemoteSettings.RemoteControlGetTaskEndpointUri)) - { - Log.Logger.Information("RemoteControlGetTaskEndpointUri is not valid, return"); - return; - } - - await ExecuteInstantJobLoop(); - } - catch (Exception ex) - { - Log.Logger.Error(ex, "RemoteControl service raises unknown error."); - } - } - - // ReSharper disable once FunctionNeverReturns - }); - } - - #region Private Method Invoker - - private static T GetPrivateFieldValue(object instance, string fieldName) - { - if (instance == null) - { - throw new ArgumentNullException(nameof(instance)); - } - - if (string.IsNullOrEmpty(fieldName)) - { - throw new ArgumentNullException(nameof(fieldName)); - } - - Type type = instance.GetType(); - FieldInfo fieldInfo = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); - - if (fieldInfo == null) - { - throw new ArgumentException($"Field '{fieldName}' not found in type '{type.FullName}'."); - } - - if (!typeof(T).IsAssignableFrom(fieldInfo.FieldType)) - { - throw new ArgumentException($"Field '{fieldName}' is not of type {typeof(T)}."); - } - - return (T)fieldInfo.GetValue(instance); - } - - private static void InvokeInstanceMethod(object instance, string methodName) - { - if (instance == null) - { - throw new ArgumentNullException(nameof(instance)); - } - - if (string.IsNullOrEmpty(methodName)) - { - throw new ArgumentNullException(nameof(methodName)); - } - - Type type = instance.GetType(); - MethodInfo methodInfo = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); - - if (methodInfo == null) - { - throw new ArgumentException($"Method '{methodName}' not found in type '{type.FullName}'."); - } - - methodInfo.Invoke(instance, null); - } - - private static T InvokeInstanceFunction(object instance, string methodName) - { - if (instance == null) - { - throw new ArgumentNullException(nameof(instance)); - } - - if (string.IsNullOrEmpty(methodName)) - { - throw new ArgumentNullException(nameof(methodName)); - } - - Type type = instance.GetType(); - MethodInfo methodInfo = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); - - if (methodInfo == null) - { - throw new ArgumentException($"Method '{methodName}' not found in type '{type.FullName}'."); - } - - if (!typeof(T).IsAssignableFrom(methodInfo.ReturnType)) - { - throw new ArgumentException($"Method '{methodName}' is not {typeof(T)}."); - } - - return (T)methodInfo.Invoke(instance, null); - } - - private static async Task InvokeInstanceAsyncFunction(object instance, string methodName) - { - if (instance == null) - { - throw new ArgumentNullException(nameof(instance)); - } - - if (string.IsNullOrEmpty(methodName)) - { - throw new ArgumentNullException(nameof(methodName)); - } - - Type type = instance.GetType(); - MethodInfo methodInfo = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); - - if (methodInfo == null) - { - throw new ArgumentException($"Method '{methodName}' not found in type '{type.FullName}'."); - } - - // 检查方法是否是异步方法 (返回Task或Task) - if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType)) - { - throw new ArgumentException($"Method '{methodName}' is not asynchronous."); - } - - return await (Task)methodInfo.Invoke(instance, null); - } - - private static TResult InvokeStaticFunction(Type staticType, string methodName) - { - if (staticType == null) - { - throw new ArgumentNullException(nameof(staticType)); - } - - if (string.IsNullOrEmpty(methodName)) - { - throw new ArgumentNullException(nameof(methodName)); - } - - MethodInfo methodInfo = staticType.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static); - - if (methodInfo == null) - { - throw new ArgumentException($"Method '{methodName}' not found in type '{staticType.FullName}'."); - } - - return (TResult)methodInfo.Invoke(null, null); - } - - #endregion - - #region JobLoops - - private async Task PollJobTaskLoop() - { - var endpoint = RemoteSettings.RemoteControlGetTaskEndpointUri; - - if (!IsEndpointValid(endpoint)) - { - return; - } - - var uid = RemoteSettings.RemoteControlUserIdentity; - var did = RemoteSettings.RemoteControlDeviceIdentity; - var response = await Instances.HttpService.PostAsJsonAsync(new Uri(endpoint), new { user = uid, device = did }); - if (response == null) - { - Log.Logger.Error("RemoteControlService endpoint failed."); - return; - } - - var jsonObject = JsonConvert.DeserializeObject(response); - - // A list of task - if (jsonObject?.GetValue("tasks") is JArray tasks) - { - foreach (var task in tasks.OfType()) - { - var type = task.GetValue("type")?.Value(); - if (string.IsNullOrWhiteSpace(type)) - { - continue; - } - - // It is a valid task - var id = task.GetValue("id")?.Value(); - if (_enqueueTaskIds.Contains(id)) - { - continue; - } - - _enqueueTaskIds.Add(id); - - switch (type) - { - case "LinkStart": - case "LinkStart-Base": - case "LinkStart-WakeUp": - case "LinkStart-Combat": - case "LinkStart-Recruiting": - case "LinkStart-Mall": - case "LinkStart-Mission": - case "LinkStart-AutoRoguelike": - case "LinkStart-Reclamation": - case "Toolbox-GachaOnce": - case "Toolbox-GachaTenTimes": - case "CaptureImage": - case "Settings-ConnectAddress": - case "Settings-Stage1": - _sequentialTaskQueue.Enqueue(task); - break; - case "CaptureImageNow": - case "HeartBeat": - case "StopTask": - _instantTaskQueue.Enqueue(task); - break; - } + Log.Logger.Error(ex, "RemoteControl service raises unknown error."); } } + + // ReSharper disable once FunctionNeverReturns + }); + + _executeSequentialJobTask = _executeSequentialJobTask.ContinueWith(async _ => + { + while (true) + { + await Task.Delay(RemoteSettings.RemoteControlPollIntervalMs); + try + { + if (!IsEndpointValid(RemoteSettings.RemoteControlGetTaskEndpointUri)) + { + Log.Logger.Information("RemoteControlGetTaskEndpointUri is not valid, return"); + return; + } + + await ExecuteSequentialJobLoop(); + } + catch (Exception ex) + { + Log.Logger.Error(ex, "RemoteControl service raises unknown error."); + } + } + + // ReSharper disable once FunctionNeverReturns + }); + + _executeInstantJobTask = _executeInstantJobTask.ContinueWith(async _ => + { + while (true) + { + await Task.Delay(RemoteSettings.RemoteControlPollIntervalMs); + try + { + if (!IsEndpointValid(RemoteSettings.RemoteControlGetTaskEndpointUri)) + { + Log.Logger.Information("RemoteControlGetTaskEndpointUri is not valid, return"); + return; + } + + await ExecuteInstantJobLoop(); + } + catch (Exception ex) + { + Log.Logger.Error(ex, "RemoteControl service raises unknown error."); + } + } + + // ReSharper disable once FunctionNeverReturns + }); + } + + #region Private Method Invoker + + private static T GetPrivateFieldValue(object instance, string fieldName) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); } - private async Task ExecuteSequentialJobLoop() + if (string.IsNullOrEmpty(fieldName)) { - if (_sequentialTaskQueue.TryDequeue(out var task)) + throw new ArgumentNullException(nameof(fieldName)); + } + + Type type = instance.GetType(); + FieldInfo fieldInfo = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + + if (fieldInfo == null) + { + throw new ArgumentException($"Field '{fieldName}' not found in type '{type.FullName}'."); + } + + if (!typeof(T).IsAssignableFrom(fieldInfo.FieldType)) + { + throw new ArgumentException($"Field '{fieldName}' is not of type {typeof(T)}."); + } + + return (T)fieldInfo.GetValue(instance); + } + + private static void InvokeInstanceMethod(object instance, string methodName) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + if (string.IsNullOrEmpty(methodName)) + { + throw new ArgumentNullException(nameof(methodName)); + } + + Type type = instance.GetType(); + MethodInfo methodInfo = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); + + if (methodInfo == null) + { + throw new ArgumentException($"Method '{methodName}' not found in type '{type.FullName}'."); + } + + methodInfo.Invoke(instance, null); + } + + private static T InvokeInstanceFunction(object instance, string methodName) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + if (string.IsNullOrEmpty(methodName)) + { + throw new ArgumentNullException(nameof(methodName)); + } + + Type type = instance.GetType(); + MethodInfo methodInfo = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); + + if (methodInfo == null) + { + throw new ArgumentException($"Method '{methodName}' not found in type '{type.FullName}'."); + } + + if (!typeof(T).IsAssignableFrom(methodInfo.ReturnType)) + { + throw new ArgumentException($"Method '{methodName}' is not {typeof(T)}."); + } + + return (T)methodInfo.Invoke(instance, null); + } + + private static async Task InvokeInstanceAsyncFunction(object instance, string methodName) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + if (string.IsNullOrEmpty(methodName)) + { + throw new ArgumentNullException(nameof(methodName)); + } + + Type type = instance.GetType(); + MethodInfo methodInfo = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); + + if (methodInfo == null) + { + throw new ArgumentException($"Method '{methodName}' not found in type '{type.FullName}'."); + } + + // 检查方法是否是异步方法 (返回Task或Task) + if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType)) + { + throw new ArgumentException($"Method '{methodName}' is not asynchronous."); + } + + return await (Task)methodInfo.Invoke(instance, null); + } + + private static TResult InvokeStaticFunction(Type staticType, string methodName) + { + if (staticType == null) + { + throw new ArgumentNullException(nameof(staticType)); + } + + if (string.IsNullOrEmpty(methodName)) + { + throw new ArgumentNullException(nameof(methodName)); + } + + MethodInfo methodInfo = staticType.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static); + + if (methodInfo == null) + { + throw new ArgumentException($"Method '{methodName}' not found in type '{staticType.FullName}'."); + } + + return (TResult)methodInfo.Invoke(null, null); + } + + #endregion + + #region JobLoops + + private async Task PollJobTaskLoop() + { + var endpoint = RemoteSettings.RemoteControlGetTaskEndpointUri; + + if (!IsEndpointValid(endpoint)) + { + return; + } + + var uid = RemoteSettings.RemoteControlUserIdentity; + var did = RemoteSettings.RemoteControlDeviceIdentity; + var response = await Instances.HttpService.PostAsJsonAsync(new Uri(endpoint), new { user = uid, device = did }); + if (response == null) + { + Log.Logger.Error("RemoteControlService endpoint failed."); + return; + } + + var jsonObject = JsonConvert.DeserializeObject(response); + + // A list of task + if (jsonObject?.GetValue("tasks") is JArray tasks) + { + foreach (var task in tasks.OfType()) { var type = task.GetValue("type")?.Value(); - var id = task.GetValue("id")?.Value(); - var data = task.GetValue("params")?.Value(); - var payload = string.Empty; - var status = "SUCCESS"; + if (string.IsNullOrWhiteSpace(type)) + { + continue; + } - _currentSequentialTaskId = id; + // It is a valid task + var id = task.GetValue("id")?.Value(); + if (_enqueueTaskIds.Contains(id)) + { + continue; + } + + _enqueueTaskIds.Add(id); switch (type) { case "LinkStart": - { - // 一键长草特殊任务 - await _runningState.UntilIdleAsync(); - var startLogStr = string.Format(LocalizationHelper.GetString("RemoteControlReceivedTask"), type, id); - - Instances.TaskQueueViewModel.AddLog(startLogStr); - await Execute.OnUIThreadAsync(() => - { - _ = Instances.TaskQueueViewModel.LinkStart(); - }); - await _runningState.UntilIdleAsync(); - - var stopLogStr = string.Format(LocalizationHelper.GetString("RemoteControlCompletedTask"), type, id); - Instances.TaskQueueViewModel.AddLog(stopLogStr); - break; - } - case "LinkStart-Base": case "LinkStart-WakeUp": case "LinkStart-Combat": @@ -394,424 +337,480 @@ namespace MaaWpfGui.Services.RemoteControl case "LinkStart-Mission": case "LinkStart-AutoRoguelike": case "LinkStart-Reclamation": - { - await LinkStart([type.Split('-')[1]]); - break; - } - case "Toolbox-GachaOnce": - { - await _runningState.UntilIdleAsync(); - await Instances.ToolboxViewModel.GachaOnce(); - await _runningState.UntilIdleAsync(); - - break; - } - case "Toolbox-GachaTenTimes": - { - await _runningState.UntilIdleAsync(); - await Instances.ToolboxViewModel.GachaTenTimes(); - await _runningState.UntilIdleAsync(); - - break; - } - case "CaptureImage": - { - string errMsg = string.Empty; - bool connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); - if (connected) - { - var image = await Instances.AsstProxy.AsstGetImageAsync(); - if (image == null) - { - status = "FAILED"; - break; - } - - byte[] bytes; - using (MemoryStream stream = new MemoryStream()) - { - PngBitmapEncoder encoder = new PngBitmapEncoder(); - encoder.Frames.Add(BitmapFrame.Create(image)); - encoder.Save(stream); - bytes = stream.ToArray(); - } - - payload = Convert.ToBase64String(bytes); - break; - } - - status = "FAILED"; - break; - } - case "Settings-ConnectAddress": - // ConfigurationHelper.SetValue(type.Split('-')[1], data); - await Execute.OnUIThreadAsync(() => - { - SettingsViewModel.ConnectSettings.ConnectAddress = data; - }); - break; case "Settings-Stage1": - await Execute.OnUIThreadAsync(() => - { - TaskQueueViewModel.FightTask.Stage1 = data; - }); + _sequentialTaskQueue.Enqueue(task); break; - - // ReSharper disable once RedundantEmptySwitchSection - default: - // 未知的Type统一直接发给MAACore - // No! 未知的任务一概不处理 + case "CaptureImageNow": + case "HeartBeat": + case "StopTask": + _instantTaskQueue.Enqueue(task); break; } - - var endpoint = RemoteSettings.RemoteControlReportStatusUri; - if (IsEndpointValid(endpoint)) - { - var uid = RemoteSettings.RemoteControlUserIdentity; - var did = RemoteSettings.RemoteControlDeviceIdentity; - var response = await Instances.HttpService.PostAsJsonAsync(new Uri(endpoint), new - { - user = uid, - device = did, - status, - task = id, - payload, - }); - if (response == null) - { - Log.Logger.Error("RemoteControlService report task failed."); - } - } - - _currentSequentialTaskId = string.Empty; } } + } - private async Task ExecuteInstantJobLoop() + private async Task ExecuteSequentialJobLoop() + { + if (_sequentialTaskQueue.TryDequeue(out var task)) { - if (_instantTaskQueue.TryDequeue(out var task)) + var type = task.GetValue("type")?.Value(); + var id = task.GetValue("id")?.Value(); + var data = task.GetValue("params")?.Value(); + var payload = string.Empty; + var status = "SUCCESS"; + + _currentSequentialTaskId = id; + + switch (type) { - var type = task.GetValue("type")?.Value(); - var id = task.GetValue("id")?.Value(); + case "LinkStart": + { + // 一键长草特殊任务 + await _runningState.UntilIdleAsync(); + var startLogStr = string.Format(LocalizationHelper.GetString("RemoteControlReceivedTask"), type, id); - // 当前没有任务需要读取params,未来需要时可以取消注释 - // var data = task.GetValue("params")?.Value(); - var payload = string.Empty; - var status = "SUCCESS"; - - switch (type) - { - case "HeartBeat": + Instances.TaskQueueViewModel.AddLog(startLogStr); + await Execute.OnUIThreadAsync(() => { - payload = _currentSequentialTaskId; - break; - } + _ = Instances.TaskQueueViewModel.LinkStart(); + }); + await _runningState.UntilIdleAsync(); - case "StopTask": + var stopLogStr = string.Format(LocalizationHelper.GetString("RemoteControlCompletedTask"), type, id); + Instances.TaskQueueViewModel.AddLog(stopLogStr); + break; + } + + case "LinkStart-Base": + case "LinkStart-WakeUp": + case "LinkStart-Combat": + case "LinkStart-Recruiting": + case "LinkStart-Mall": + case "LinkStart-Mission": + case "LinkStart-AutoRoguelike": + case "LinkStart-Reclamation": + { + await LinkStart([type.Split('-')[1]]); + break; + } + + case "Toolbox-GachaOnce": + { + await _runningState.UntilIdleAsync(); + await Instances.ToolboxViewModel.GachaOnce(); + await _runningState.UntilIdleAsync(); + + break; + } + + case "Toolbox-GachaTenTimes": + { + await _runningState.UntilIdleAsync(); + await Instances.ToolboxViewModel.GachaTenTimes(); + await _runningState.UntilIdleAsync(); + + break; + } + + case "CaptureImage": + { + string errMsg = string.Empty; + bool connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + if (connected) { - await Task.Run(() => + var image = await Instances.AsstProxy.AsstGetImageAsync(); + if (image == null) { - if (!Instances.AsstProxy.AsstStop()) - { - // 无法确定当前的界面,找不到借用的UI位置,因此只能Log - Log.Logger.Error("Failed to stop Asst."); - } - }); - - // 无需等待,甩出任务即可返回,远端应该用心跳来确认界面卡死和取消是否成功。 - break; - } - - case "CaptureImageNow": - { - string errMsg = string.Empty; - bool connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); - if (connected) - { - var image = Instances.AsstProxy.AsstGetImage(); - if (image == null) - { - status = "FAILED"; - break; - } - - byte[] bytes; - using (MemoryStream stream = new MemoryStream()) - { - PngBitmapEncoder encoder = new PngBitmapEncoder(); - encoder.Frames.Add(BitmapFrame.Create(image)); - encoder.Save(stream); - bytes = stream.ToArray(); - } - - payload = Convert.ToBase64String(bytes); + status = "FAILED"; break; } - status = "FAILED"; + byte[] bytes; + using (MemoryStream stream = new MemoryStream()) + { + PngBitmapEncoder encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(image)); + encoder.Save(stream); + bytes = stream.ToArray(); + } + + payload = Convert.ToBase64String(bytes); break; } - // ReSharper disable once RedundantEmptySwitchSection - default: - // 未知的Type统一直接发给MAACore - // No! 未知的任务一概不处理 + status = "FAILED"; break; - } - - var endpoint = RemoteSettings.RemoteControlReportStatusUri; - if (IsEndpointValid(endpoint)) - { - var uid = RemoteSettings.RemoteControlUserIdentity; - var did = RemoteSettings.RemoteControlDeviceIdentity; - var response = await Instances.HttpService.PostAsJsonAsync(new Uri(endpoint), new - { - user = uid, - device = did, - status, - task = id, - payload, - }); - if (response == null) - { - Log.Logger.Error("RemoteControlService report task failed."); } + + case "Settings-ConnectAddress": + // ConfigurationHelper.SetValue(type.Split('-')[1], data); + await Execute.OnUIThreadAsync(() => + { + SettingsViewModel.ConnectSettings.ConnectAddress = data; + }); + break; + case "Settings-Stage1": + await Execute.OnUIThreadAsync(() => + { + TaskQueueViewModel.FightTask.Stage1 = data; + }); + break; + + // ReSharper disable once RedundantEmptySwitchSection + default: + // 未知的Type统一直接发给MAACore + // No! 未知的任务一概不处理 + break; + } + + var endpoint = RemoteSettings.RemoteControlReportStatusUri; + if (IsEndpointValid(endpoint)) + { + var uid = RemoteSettings.RemoteControlUserIdentity; + var did = RemoteSettings.RemoteControlDeviceIdentity; + var response = await Instances.HttpService.PostAsJsonAsync(new Uri(endpoint), new + { + user = uid, + device = did, + status, + task = id, + payload, + }); + if (response == null) + { + Log.Logger.Error("RemoteControlService report task failed."); + } + } + + _currentSequentialTaskId = string.Empty; + } + } + + private async Task ExecuteInstantJobLoop() + { + if (_instantTaskQueue.TryDequeue(out var task)) + { + var type = task.GetValue("type")?.Value(); + var id = task.GetValue("id")?.Value(); + + // 当前没有任务需要读取params,未来需要时可以取消注释 + // var data = task.GetValue("params")?.Value(); + var payload = string.Empty; + var status = "SUCCESS"; + + switch (type) + { + case "HeartBeat": + { + payload = _currentSequentialTaskId; + break; + } + + case "StopTask": + { + await Task.Run(() => + { + if (!Instances.AsstProxy.AsstStop()) + { + // 无法确定当前的界面,找不到借用的UI位置,因此只能Log + Log.Logger.Error("Failed to stop Asst."); + } + }); + + // 无需等待,甩出任务即可返回,远端应该用心跳来确认界面卡死和取消是否成功。 + break; + } + + case "CaptureImageNow": + { + string errMsg = string.Empty; + bool connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + if (connected) + { + var image = Instances.AsstProxy.AsstGetImage(); + if (image == null) + { + status = "FAILED"; + break; + } + + byte[] bytes; + using (MemoryStream stream = new MemoryStream()) + { + PngBitmapEncoder encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(image)); + encoder.Save(stream); + bytes = stream.ToArray(); + } + + payload = Convert.ToBase64String(bytes); + break; + } + + status = "FAILED"; + break; + } + + // ReSharper disable once RedundantEmptySwitchSection + default: + // 未知的Type统一直接发给MAACore + // No! 未知的任务一概不处理 + break; + } + + var endpoint = RemoteSettings.RemoteControlReportStatusUri; + if (IsEndpointValid(endpoint)) + { + var uid = RemoteSettings.RemoteControlUserIdentity; + var did = RemoteSettings.RemoteControlDeviceIdentity; + var response = await Instances.HttpService.PostAsJsonAsync(new Uri(endpoint), new + { + user = uid, + device = did, + status, + task = id, + payload, + }); + if (response == null) + { + Log.Logger.Error("RemoteControlService report task failed."); } } } + } - #endregion + #endregion - /// - /// 根据"一键长草"功能进行修改的方法。 - /// - /// - /// 注意以下特点: - /// - 可以在非UI线程执行。 - /// - 不调用StartScript。 - /// - 不使用Model里的列表。 - /// - 在结尾添加对RunningStatus的等待。 - /// 若"一键长草"功能在未来有更新,此方法也应进行相应的同步更新,但需确保上述特点保持不变。 - /// - /// 指定的任务列表。 - /// 异步任务,无返回结果。 - // 这个是不是可以直接做到 TaskQueueViewModel 里面去? - private async Task LinkStart(IEnumerable originalNames) + /// + /// 根据"一键长草"功能进行修改的方法。 + /// + /// + /// 注意以下特点: + /// - 可以在非UI线程执行。 + /// - 不调用StartScript。 + /// - 不使用Model里的列表。 + /// - 在结尾添加对RunningStatus的等待。 + /// 若"一键长草"功能在未来有更新,此方法也应进行相应的同步更新,但需确保上述特点保持不变。 + /// + /// 指定的任务列表。 + /// 异步任务,无返回结果。 + // 这个是不是可以直接做到 TaskQueueViewModel 里面去? + private async Task LinkStart(IEnumerable originalNames) + { + await _runningState.UntilIdleAsync(); + + _runningState.SetIdle(false); + + await Execute.OnUIThreadAsync(async () => { - await _runningState.UntilIdleAsync(); + // 虽然更改时已经保存过了,不过保险起见还是在点击开始之后再保存一次(任务及基建列表) + Instances.TaskQueueViewModel.TaskItemSelectionChanged(); + TaskQueueViewModel.InfrastTask.InfrastOrderSelectionChanged(); - _runningState.SetIdle(false); + InvokeInstanceMethod(Instances.TaskQueueViewModel, "ClearLog"); - await Execute.OnUIThreadAsync(async () => + /*await Task.Run(() => Instances.SettingsViewModel.RunScript("StartsWithScript"));*/ + + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ConnectingToEmulator")); + + // 一般是点了“停止”按钮了 + if (_runningState.GetStopping()) { - // 虽然更改时已经保存过了,不过保险起见还是在点击开始之后再保存一次(任务及基建列表) - Instances.TaskQueueViewModel.TaskItemSelectionChanged(); - TaskQueueViewModel.InfrastTask.InfrastOrderSelectionChanged(); + Instances.TaskQueueViewModel.SetStopped(); + return; + } - InvokeInstanceMethod(Instances.TaskQueueViewModel, "ClearLog"); - - /*await Task.Run(() => Instances.SettingsViewModel.RunScript("StartsWithScript"));*/ - - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ConnectingToEmulator")); - - // 一般是点了“停止”按钮了 - if (_runningState.GetStopping()) - { - Instances.TaskQueueViewModel.SetStopped(); - return; - } - - if (!await InvokeInstanceAsyncFunction(Instances.TaskQueueViewModel, "ConnectToEmulator")) - { - return; - } - - // 一般是点了“停止”按钮了 - if (_runningState.GetStopping()) - { - Instances.TaskQueueViewModel.SetStopped(); - return; - } - - bool taskRet = true; - - // 直接遍历TaskItemViewModels里面的内容,是排序后的 - int count = 0; - foreach (var item in originalNames) - { - ++count; - switch (item) - { - case "Base": - taskRet &= Instances.TaskQueueViewModel.AppendInfrast(); - break; - - case "WakeUp": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.StartUp, StartUpSettingsUserControlModel.Instance.Serialize()); - break; - - case "Combat": - taskRet &= Instances.TaskQueueViewModel.AppendFight(); - break; - - case "Recruiting": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Recruit, RecruitSettingsUserControlModel.Instance.Serialize()); - break; - - case "Mall": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Mall, MallSettingsUserControlModel.Instance.Serialize()); - break; - - case "Mission": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Award, AwardSettingsUserControlModel.Instance.Serialize()); - break; - - case "AutoRoguelike": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Roguelike, RoguelikeSettingsUserControlModel.Instance.Serialize()); - break; - - case "Reclamation": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Reclamation, ReclamationSettingsUserControlModel.Instance.Serialize()); - break; - - default: - --count; - - // Instances.TaskQueueViewModel._logger.Error("Unknown task: " + item); - break; - } - - if (taskRet) - { - continue; - } - - Instances.TaskQueueViewModel.AddLog(item + "Error", UiLogColor.Error); - taskRet = true; - --count; - } - - if (count == 0) - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("UnselectedTask")); - _runningState.SetIdle(true); - Instances.TaskQueueViewModel.SetStopped(); - return; - } - - taskRet &= Instances.AsstProxy.AsstStart(); - - if (taskRet) - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("Running")); - } - else - { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("UnknownErrorOccurs")); - await Instances.TaskQueueViewModel.Stop(); - Instances.TaskQueueViewModel.SetStopped(); - } - }); - - await _runningState.UntilIdleAsync(); - } - - public static async Task ConnectionTest() - { - var endpoint = RemoteSettings.RemoteControlGetTaskEndpointUri; - - if (!IsEndpointValid(endpoint, alarm: true)) + if (!await InvokeInstanceAsyncFunction(Instances.TaskQueueViewModel, "ConnectToEmulator")) { return; } - var uid = RemoteSettings.RemoteControlUserIdentity; - var did = RemoteSettings.RemoteControlDeviceIdentity; - - try + // 一般是点了“停止”按钮了 + if (_runningState.GetStopping()) { - var body = System.Text.Json.JsonSerializer.Serialize(new { user = uid, device = did }); - var message = new HttpRequestMessage(HttpMethod.Post, endpoint); - message.Headers.Accept.ParseAdd("application/json"); - message.Content = new StringContent(body, Encoding.UTF8, "application/json"); + Instances.TaskQueueViewModel.SetStopped(); + return; + } - var client = GetPrivateFieldValue(Instances.HttpService, "_client"); - var response = await client.SendAsync(message); + bool taskRet = true; - if (response != null) + // 直接遍历TaskItemViewModels里面的内容,是排序后的 + int count = 0; + foreach (var item in originalNames) + { + ++count; + switch (item) { - if (!response.IsSuccessStatusCode) - { - var errorMsg = string.Format(LocalizationHelper.GetString("RemoteControlConnectionTestFail"), response.StatusCode); - ToastNotification.ShowDirect(errorMsg); - return; - } - } - else - { - // 一般来说不会走到这里,因为null response一定会报错 - var errorMsg = string.Format(LocalizationHelper.GetString("RemoteControlConnectionTestFail"), "Unknown"); - ToastNotification.ShowDirect(errorMsg); - return; + case "Base": + taskRet &= Instances.TaskQueueViewModel.AppendInfrast(); + break; + + case "WakeUp": + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.StartUp, StartUpSettingsUserControlModel.Instance.Serialize()); + break; + + case "Combat": + taskRet &= Instances.TaskQueueViewModel.AppendFight(); + break; + + case "Recruiting": + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Recruit, RecruitSettingsUserControlModel.Instance.Serialize()); + break; + + case "Mall": + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Mall, MallSettingsUserControlModel.Instance.Serialize()); + break; + + case "Mission": + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Award, AwardSettingsUserControlModel.Instance.Serialize()); + break; + + case "AutoRoguelike": + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Roguelike, RoguelikeSettingsUserControlModel.Instance.Serialize()); + break; + + case "Reclamation": + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Reclamation, ReclamationSettingsUserControlModel.Instance.Serialize()); + break; + + default: + --count; + + // Instances.TaskQueueViewModel._logger.Error("Unknown task: " + item); + break; } - ToastNotification.ShowDirect( - LocalizationHelper.GetString("RemoteControlConnectionTestSuccess")); - } - catch (Exception e) - { - var error = e.Message; - if (e.InnerException != null) + if (taskRet) { - error = e.InnerException.Message; + continue; } - var errorMsg = string.Format(LocalizationHelper.GetString("RemoteControlConnectionTestFail"), error); - ToastNotification.ShowDirect(errorMsg); - } - } - - public static void RegenerateDeviceIdentity() - { - RemoteSettings.RemoteControlDeviceIdentity = Guid.NewGuid().ToString("N"); - } - - public static bool IsEndpointValid(string endpoint, bool alarm = false) - { - if (string.IsNullOrWhiteSpace(endpoint)) - { - ShowToast("RemoteControlConnectionTestFailEmpty", alarm); - return false; + Instances.TaskQueueViewModel.AddLog(item + "Error", UiLogColor.Error); + taskRet = true; + --count; } - string lowerEndpoint = endpoint.ToLower(); - - if (lowerEndpoint.StartsWith("https://")) + if (count == 0) { - return true; + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("UnselectedTask")); + _runningState.SetIdle(true); + Instances.TaskQueueViewModel.SetStopped(); + return; } - else if (lowerEndpoint.StartsWith("http://")) + + taskRet &= Instances.AsstProxy.AsstStart(); + + if (taskRet) { - ShowToast("RemoteControlConnectionTestWarningHttpUnsafe", alarm); - return true; + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("Running")); } else { - ShowToast("RemoteControlConnectionTestFailNotHttpOrHttps", alarm); - return false; + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("UnknownErrorOccurs")); + await Instances.TaskQueueViewModel.Stop(); + Instances.TaskQueueViewModel.SetStopped(); } + }); + + await _runningState.UntilIdleAsync(); + } + + public static async Task ConnectionTest() + { + var endpoint = RemoteSettings.RemoteControlGetTaskEndpointUri; + + if (!IsEndpointValid(endpoint, alarm: true)) + { + return; } - public static void ShowToast(string message, bool alarm) + var uid = RemoteSettings.RemoteControlUserIdentity; + var did = RemoteSettings.RemoteControlDeviceIdentity; + + try { - if (alarm) + var body = System.Text.Json.JsonSerializer.Serialize(new { user = uid, device = did }); + var message = new HttpRequestMessage(HttpMethod.Post, endpoint); + message.Headers.Accept.ParseAdd("application/json"); + message.Content = new StringContent(body, Encoding.UTF8, "application/json"); + + var client = GetPrivateFieldValue(Instances.HttpService, "_client"); + var response = await client.SendAsync(message); + + if (response != null) { - ToastNotification.ShowDirect(LocalizationHelper.GetString(message)); + if (!response.IsSuccessStatusCode) + { + var errorMsg = string.Format(LocalizationHelper.GetString("RemoteControlConnectionTestFail"), response.StatusCode); + ToastNotification.ShowDirect(errorMsg); + return; + } } + else + { + // 一般来说不会走到这里,因为null response一定会报错 + var errorMsg = string.Format(LocalizationHelper.GetString("RemoteControlConnectionTestFail"), "Unknown"); + ToastNotification.ShowDirect(errorMsg); + return; + } + + ToastNotification.ShowDirect( + LocalizationHelper.GetString("RemoteControlConnectionTestSuccess")); + } + catch (Exception e) + { + var error = e.Message; + if (e.InnerException != null) + { + error = e.InnerException.Message; + } + + var errorMsg = string.Format(LocalizationHelper.GetString("RemoteControlConnectionTestFail"), error); + ToastNotification.ShowDirect(errorMsg); + } + } + + public static void RegenerateDeviceIdentity() + { + RemoteSettings.RemoteControlDeviceIdentity = Guid.NewGuid().ToString("N"); + } + + public static bool IsEndpointValid(string endpoint, bool alarm = false) + { + if (string.IsNullOrWhiteSpace(endpoint)) + { + ShowToast("RemoteControlConnectionTestFailEmpty", alarm); + return false; + } + + string lowerEndpoint = endpoint.ToLower(); + + if (lowerEndpoint.StartsWith("https://")) + { + return true; + } + else if (lowerEndpoint.StartsWith("http://")) + { + ShowToast("RemoteControlConnectionTestWarningHttpUnsafe", alarm); + return true; + } + else + { + ShowToast("RemoteControlConnectionTestFailNotHttpOrHttps", alarm); + return false; + } + } + + public static void ShowToast(string message, bool alarm) + { + if (alarm) + { + ToastNotification.ShowDirect(LocalizationHelper.GetString(message)); } } } diff --git a/src/MaaWpfGui/Services/StageManager.cs b/src/MaaWpfGui/Services/StageManager.cs index 7230fb90b6..f7ab25d5e7 100644 --- a/src/MaaWpfGui/Services/StageManager.cs +++ b/src/MaaWpfGui/Services/StageManager.cs @@ -26,429 +26,427 @@ using MaaWpfGui.Models; using MaaWpfGui.Utilities.ValueType; using MaaWpfGui.ViewModels.UI; using MaaWpfGui.ViewModels.UserControl.Settings; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Semver; using Serilog; using Stylet; -namespace MaaWpfGui.Services +namespace MaaWpfGui.Services; + +/// +/// Stage manager +/// +public class StageManager { + private const string StageApi = "gui/StageActivity.json"; + private const string TasksApi = "resource/tasks.json"; + + private static readonly ILogger _logger = Log.ForContext(); + + // data + private Dictionary _stages = []; + /// - /// Stage manager + /// Initializes a new instance of the class. /// - public class StageManager + public StageManager() { - private const string StageApi = "gui/StageActivity.json"; - private const string TasksApi = "resource/tasks.json"; + UpdateStageLocal(); + } - private static readonly ILogger _logger = Log.ForContext(); + public void UpdateStageLocal() + { + MergePermanentAndActivityStages(LoadLocalStages()); + } - // data - private Dictionary _stages = []; + public async Task UpdateStageWeb() + { + // 清理旧的缓存文件 + string cacheAllFileDownloadComplete = PathsHelper.CacheDir + "allFileDownloadComplete.json"; + string lastUpdateTime = PathsHelper.CacheDir + "LastUpdateTime.json"; + string stageAndTasksUpdateTime = PathsHelper.CacheDir + "stageAndTasksUpdateTime.json"; + var filesToClean = new[] { cacheAllFileDownloadComplete, lastUpdateTime, stageAndTasksUpdateTime }; - /// - /// Initializes a new instance of the class. - /// - public StageManager() - { - UpdateStageLocal(); - } - - public void UpdateStageLocal() - { - MergePermanentAndActivityStages(LoadLocalStages()); - } - - public async Task UpdateStageWeb() - { - // 清理旧的缓存文件 - string cacheAllFileDownloadComplete = PathsHelper.CacheDir + "allFileDownloadComplete.json"; - string lastUpdateTime = PathsHelper.CacheDir + "LastUpdateTime.json"; - string stageAndTasksUpdateTime = PathsHelper.CacheDir + "stageAndTasksUpdateTime.json"; - var filesToClean = new[] { cacheAllFileDownloadComplete, lastUpdateTime, stageAndTasksUpdateTime }; - - foreach (var file in filesToClean) - { - try - { - if (File.Exists(file)) - { - File.Delete(file); - } - } - catch - { - continue; - } - } - - var webStages = await LoadWebStages(); - if (webStages is null) - { - return; - } - - MergePermanentAndActivityStages(webStages); - - _ = Execute.OnUIThreadAsync(() => - { - var growlInfo = new GrowlInfo - { - IsCustom = true, - Message = LocalizationHelper.GetString("ApiUpdateSuccess"), - IconKey = "HangoverGeometry", - IconBrushKey = "PallasBrush", - }; - Growl.Info(growlInfo); - }); - } - - private static string GetClientType() - { - var clientType = SettingsViewModel.GameSettings.ClientType; - - // 官服和B服使用同样的资源 - if (clientType is "Bilibili" or "") - { - clientType = "Official"; - } - - return clientType; - } - - private static JObject LoadLocalStages() - { - JObject activity = Instances.MaaApiService.LoadApiCache(StageApi); - return activity; - } - - /* - private static async Task CheckWebUpdate() - { - // Check if we need to update from the web - const string StageAndTasksUpdateTime = "stageAndTasksUpdateTime.json"; - const string AllFileDownloadCompleteFile = "allFileDownloadComplete.json"; - JObject localLastUpdatedJson = Instances.MaaApiService.LoadApiCache(StageAndTasksUpdateTime); - JObject allFileDownloadCompleteJson = Instances.MaaApiService.LoadApiCache(AllFileDownloadCompleteFile); - JObject webLastUpdatedJson = await Instances.MaaApiService.RequestMaaApiWithCache(StageAndTasksUpdateTime).ConfigureAwait(false); - - if (localLastUpdatedJson?["timestamp"] == null || webLastUpdatedJson?["timestamp"] == null) - { - return true; - } - - long localTimestamp = localLastUpdatedJson["timestamp"]!.ToObject(); - long webTimestamp = webLastUpdatedJson["timestamp"]!.ToObject(); - bool allFileDownloadComplete = allFileDownloadCompleteJson?["allFileDownloadComplete"]?.ToObject() ?? false; - return webTimestamp > localTimestamp || !allFileDownloadComplete; - } - */ - - private static async Task LoadWebStages() - { - var clientType = GetClientType(); - - var activityTask = Instances.MaaApiService.RequestMaaApiWithCache(StageApi, false); - var tasksTask = Instances.MaaApiService.RequestMaaApiWithCache(TasksApi, false); - - await Task.WhenAll(activityTask, tasksTask); - - var activityJson = await activityTask; - var tasksJson = await tasksTask; - JObject? globalTasksJson = null; - if (clientType != "Official" && tasksJson != null) - { - var tasksPath = "resource/global/" + clientType + '/' + TasksApi; - - // Download the client specific resources only when the Official ones are successfully downloaded so that the client specific resource version is the actual version - // TODO: There may be an issue when the CN resource is loaded from cache (e.g. network down) while global resource is downloaded (e.g. network up again) - // var tasksJsonClient = fromWeb ? WebService.RequestMaaApiWithCache(tasksPath) : WebService.RequestMaaApiWithCache(tasksPath); - globalTasksJson = await Instances.MaaApiService.RequestMaaApiWithCache(tasksPath, false); - } - - if (activityJson is null || tasksJson is null) - { - return null; - } - - if (clientType != "Official" && globalTasksJson is null) - { - return null; - } - - await Task.Run(() => - { - Instances.AsstProxy.LoadResource(); - }); - return activityJson; - } - - private void MergePermanentAndActivityStages(JObject? activity) - { - var tempStage = InitializeDefaultStages(); - - var clientType = GetClientType(); - - bool isDebugVersion = Instances.VersionUpdateViewModel.IsDebugVersion(); - bool curVerParsed = TryParseVersion(VersionUpdateSettingsUserControlModel.CoreVersion, out var curVersionObj); - - // bool curResourceVerParsed = SemVersion.TryParse( - // tasksJsonClient?["ResourceVersion"]?.ToString() ?? tasksJson?["ResourceVersion"]?.ToString() ?? string.Empty, - // SemVersionStyles.AllowLowerV, out var curResourceVersionObj); - var resourceCollection = InitializeResourceCollection(activity?[clientType]?["resourceCollection"]); - - if (activity?[clientType] != null && (isDebugVersion || (curVerParsed && curVersionObj != null))) - { - ParseActivityStages(activity[clientType], tempStage, curVerParsed, curVersionObj, isDebugVersion); - } - - AddPermanentStages(tempStage, resourceCollection); - - _stages = tempStage; - } - - private static Dictionary InitializeDefaultStages() - { - // 这里会被 “剩余理智” 复用,第一个必须是 string.Empty 的 - return new() - { - // 「当前/上次」关卡导航 - { string.Empty, new() { Display = LocalizationHelper.GetString("DefaultStage"), Value = string.Empty } }, - - // 周一和周日的关卡提示 - { "Pormpt1", new() { Tip = LocalizationHelper.GetString("Pormpt1"), OpenDays = [DayOfWeek.Monday], IsHidden = true } }, - { "Pormpt2", new() { Tip = LocalizationHelper.GetString("Pormpt2"), OpenDays = [DayOfWeek.Sunday], IsHidden = true } }, - }; - } - - private static bool TryParseVersion(string? version, out SemVersion? versionObj) - { - return SemVersion.TryParse(version, SemVersionStyles.AllowLowerV, out versionObj); - } - - private static DateTime ParseDateTime(JToken? token, string key) - { - return DateTime.ParseExact(token?[key]?.ToString() ?? string.Empty, "yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture) - .AddHours(-(token?["TimeZone"]?.ToObject() ?? 0)); - } - - private static StageActivityInfo InitializeResourceCollection(JToken? resourceCollectionData) - { - if (resourceCollectionData == null) - { - return new() { IsResourceCollection = true }; - } - - // 资源全开放活动 - return new() - { - IsResourceCollection = true, - Tip = resourceCollectionData["Tip"]?.ToString(), - UtcStartTime = ParseDateTime(resourceCollectionData, "UtcStartTime"), - UtcExpireTime = ParseDateTime(resourceCollectionData, "UtcExpireTime"), - }; - } - - private static void ParseActivityStages(JToken? clientData, Dictionary tempStage, bool curVerParsed, SemVersion? curVersionObj, bool isDebugVersion) + foreach (var file in filesToClean) { try { - foreach (var stageObj in clientData?["sideStoryStage"] ?? Enumerable.Empty()) + if (File.Exists(file)) { - if (!TryParseVersion(stageObj["MinimumRequired"]?.ToString(), out var minRequiredObj)) - { - continue; - } - - bool unsupportedStages = !isDebugVersion && curVerParsed && curVersionObj!.CompareSortOrderTo(minRequiredObj) < 0; - - var stageInfo = CreateStageInfo(stageObj, unsupportedStages, minRequiredObj); - tempStage.TryAdd(stageInfo.Display, stageInfo); + File.Delete(file); } } - catch (Exception e) + catch { - _logger.Error(e, "Failed to parse Cache Stage resources"); + continue; } } - private static StageInfo CreateStageInfo(JToken? stageObj, bool unsupportedStages, SemVersion? minRequiredObj) + var webStages = await LoadWebStages(); + if (webStages is null) { - return new() + return; + } + + MergePermanentAndActivityStages(webStages); + + _ = Execute.OnUIThreadAsync(() => + { + var growlInfo = new GrowlInfo { - Display = unsupportedStages - ? LocalizationHelper.GetString("UnsupportedStages") - : stageObj?["Display"]?.ToString() ?? string.Empty, - Value = unsupportedStages - ? LocalizationHelper.GetString("UnsupportedStages") - : stageObj?["Value"]?.ToString() ?? string.Empty, - Drop = unsupportedStages - ? LocalizationHelper.GetString("LowVersion") + '\n' + LocalizationHelper.GetString("MinimumRequirements") + minRequiredObj - : stageObj?["Drop"]?.ToString(), - Activity = new() - { - Tip = stageObj?["Activity"]?["Tip"]?.ToString(), - StageName = stageObj?["Activity"]?["StageName"]?.ToString(), - UtcStartTime = ParseDateTime(stageObj?["Activity"], "UtcStartTime"), - UtcExpireTime = ParseDateTime(stageObj?["Activity"], "UtcExpireTime"), - }, + IsCustom = true, + Message = LocalizationHelper.GetString("ApiUpdateSuccess"), + IconKey = "HangoverGeometry", + IconBrushKey = "PallasBrush", }; + Growl.Info(growlInfo); + }); + } + + private static string GetClientType() + { + var clientType = SettingsViewModel.GameSettings.ClientType; + + // 官服和B服使用同样的资源 + if (clientType is "Bilibili" or "") + { + clientType = "Official"; } - private static void AddPermanentStages(Dictionary tempStage, StageActivityInfo resourceCollection) + return clientType; + } + + private static JObject LoadLocalStages() + { + JObject activity = Instances.MaaApiService.LoadApiCache(StageApi); + return activity; + } + + /* + private static async Task CheckWebUpdate() + { + // Check if we need to update from the web + const string StageAndTasksUpdateTime = "stageAndTasksUpdateTime.json"; + const string AllFileDownloadCompleteFile = "allFileDownloadComplete.json"; + JObject localLastUpdatedJson = Instances.MaaApiService.LoadApiCache(StageAndTasksUpdateTime); + JObject allFileDownloadCompleteJson = Instances.MaaApiService.LoadApiCache(AllFileDownloadCompleteFile); + JObject webLastUpdatedJson = await Instances.MaaApiService.RequestMaaApiWithCache(StageAndTasksUpdateTime).ConfigureAwait(false); + + if (localLastUpdatedJson?["timestamp"] == null || webLastUpdatedJson?["timestamp"] == null) { - var permanentStages = new Dictionary + return true; + } + + long localTimestamp = localLastUpdatedJson["timestamp"]!.ToObject(); + long webTimestamp = webLastUpdatedJson["timestamp"]!.ToObject(); + bool allFileDownloadComplete = allFileDownloadCompleteJson?["allFileDownloadComplete"]?.ToObject() ?? false; + return webTimestamp > localTimestamp || !allFileDownloadComplete; + } + */ + + private static async Task LoadWebStages() + { + var clientType = GetClientType(); + + var activityTask = Instances.MaaApiService.RequestMaaApiWithCache(StageApi, false); + var tasksTask = Instances.MaaApiService.RequestMaaApiWithCache(TasksApi, false); + + await Task.WhenAll(activityTask, tasksTask); + + var activityJson = await activityTask; + var tasksJson = await tasksTask; + JObject? globalTasksJson = null; + if (clientType != "Official" && tasksJson != null) + { + var tasksPath = "resource/global/" + clientType + '/' + TasksApi; + + // Download the client specific resources only when the Official ones are successfully downloaded so that the client specific resource version is the actual version + // TODO: There may be an issue when the CN resource is loaded from cache (e.g. network down) while global resource is downloaded (e.g. network up again) + // var tasksJsonClient = fromWeb ? WebService.RequestMaaApiWithCache(tasksPath) : WebService.RequestMaaApiWithCache(tasksPath); + globalTasksJson = await Instances.MaaApiService.RequestMaaApiWithCache(tasksPath, false); + } + + if (activityJson is null || tasksJson is null) + { + return null; + } + + if (clientType != "Official" && globalTasksJson is null) + { + return null; + } + + await Task.Run(() => + { + Instances.AsstProxy.LoadResource(); + }); + return activityJson; + } + + private void MergePermanentAndActivityStages(JObject? activity) + { + var tempStage = InitializeDefaultStages(); + + var clientType = GetClientType(); + + bool isDebugVersion = Instances.VersionUpdateViewModel.IsDebugVersion(); + bool curVerParsed = TryParseVersion(VersionUpdateSettingsUserControlModel.CoreVersion, out var curVersionObj); + + // bool curResourceVerParsed = SemVersion.TryParse( + // tasksJsonClient?["ResourceVersion"]?.ToString() ?? tasksJson?["ResourceVersion"]?.ToString() ?? string.Empty, + // SemVersionStyles.AllowLowerV, out var curResourceVersionObj); + var resourceCollection = InitializeResourceCollection(activity?[clientType]?["resourceCollection"]); + + if (activity?[clientType] != null && (isDebugVersion || (curVerParsed && curVersionObj != null))) + { + ParseActivityStages(activity[clientType], tempStage, curVerParsed, curVersionObj, isDebugVersion); + } + + AddPermanentStages(tempStage, resourceCollection); + + _stages = tempStage; + } + + private static Dictionary InitializeDefaultStages() + { + // 这里会被 “剩余理智” 复用,第一个必须是 string.Empty 的 + return new() + { + // 「当前/上次」关卡导航 + { string.Empty, new() { Display = LocalizationHelper.GetString("DefaultStage"), Value = string.Empty } }, + + // 周一和周日的关卡提示 + { "Pormpt1", new() { Tip = LocalizationHelper.GetString("Pormpt1"), OpenDays = [DayOfWeek.Monday], IsHidden = true } }, + { "Pormpt2", new() { Tip = LocalizationHelper.GetString("Pormpt2"), OpenDays = [DayOfWeek.Sunday], IsHidden = true } }, + }; + } + + private static bool TryParseVersion(string? version, out SemVersion? versionObj) + { + return SemVersion.TryParse(version, SemVersionStyles.AllowLowerV, out versionObj); + } + + private static DateTime ParseDateTime(JToken? token, string key) + { + return DateTime.ParseExact(token?[key]?.ToString() ?? string.Empty, "yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture) + .AddHours(-(token?["TimeZone"]?.ToObject() ?? 0)); + } + + private static StageActivityInfo InitializeResourceCollection(JToken? resourceCollectionData) + { + if (resourceCollectionData == null) + { + return new() { IsResourceCollection = true }; + } + + // 资源全开放活动 + return new() + { + IsResourceCollection = true, + Tip = resourceCollectionData["Tip"]?.ToString(), + UtcStartTime = ParseDateTime(resourceCollectionData, "UtcStartTime"), + UtcExpireTime = ParseDateTime(resourceCollectionData, "UtcExpireTime"), + }; + } + + private static void ParseActivityStages(JToken? clientData, Dictionary tempStage, bool curVerParsed, SemVersion? curVersionObj, bool isDebugVersion) + { + try + { + foreach (var stageObj in clientData?["sideStoryStage"] ?? Enumerable.Empty()) { - // 主线关卡 - { "1-7", new() { Display = "1-7", Value = "1-7" } }, - { "R8-11", new() { Display = "R8-11", Value = "R8-11" } }, - { "12-17-HARD", new() { Display = "12-17-HARD", Value = "12-17-HARD" } }, + if (!TryParseVersion(stageObj["MinimumRequired"]?.ToString(), out var minRequiredObj)) + { + continue; + } - // 资源本 - { "CE-6", new("CE-6", "CETip", [DayOfWeek.Tuesday, DayOfWeek.Thursday, DayOfWeek.Saturday, DayOfWeek.Sunday], resourceCollection) }, - { "AP-5", new("AP-5", "APTip", [DayOfWeek.Monday, DayOfWeek.Thursday, DayOfWeek.Saturday, DayOfWeek.Sunday], resourceCollection) }, - { "CA-5", new("CA-5", "CATip", [DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Friday, DayOfWeek.Sunday], resourceCollection) }, - { "LS-6", new("LS-6", "LSTip", [], resourceCollection) }, - { "SK-5", new("SK-5", "SKTip", [DayOfWeek.Monday, DayOfWeek.Wednesday, DayOfWeek.Friday, DayOfWeek.Saturday], resourceCollection) }, + bool unsupportedStages = !isDebugVersion && curVerParsed && curVersionObj!.CompareSortOrderTo(minRequiredObj) < 0; - // 剿灭模式 - { "Annihilation", new() { Display = LocalizationHelper.GetString("AnnihilationMode"), Value = "Annihilation" } }, - - // 芯片本 - { "PR-A-1", new("PR-A-1", "PR-ATip", [DayOfWeek.Monday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Sunday], resourceCollection) }, - { "PR-A-2", new("PR-A-2", string.Empty, [DayOfWeek.Monday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Sunday], resourceCollection) }, - { "PR-B-1", new("PR-B-1", "PR-BTip", [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Friday, DayOfWeek.Saturday], resourceCollection) }, - { "PR-B-2", new("PR-B-2", string.Empty, [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Friday, DayOfWeek.Saturday], resourceCollection) }, - { "PR-C-1", new("PR-C-1", "PR-CTip", [DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Saturday, DayOfWeek.Sunday], resourceCollection) }, - { "PR-C-2", new("PR-C-2", string.Empty, [DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Saturday, DayOfWeek.Sunday], resourceCollection) }, - { "PR-D-1", new("PR-D-1", "PR-DTip", [DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Saturday, DayOfWeek.Sunday], resourceCollection) }, - { "PR-D-2", new("PR-D-2", string.Empty, [DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Saturday, DayOfWeek.Sunday], resourceCollection) }, - }; - - foreach (var kvp in permanentStages) - { - tempStage.TryAdd(kvp.Key, kvp.Value); + var stageInfo = CreateStageInfo(stageObj, unsupportedStages, minRequiredObj); + tempStage.TryAdd(stageInfo.Display, stageInfo); } } - - /// - /// Gets stage by name - /// - /// Stage name - /// Stage info - public StageInfo GetStageInfo(string stage) + catch (Exception e) { - _stages.TryGetValue(stage, out var stageInfo); - stageInfo ??= new() { Display = stage, Value = stage }; - return stageInfo; - } - - public bool IsStageInStageList(string stage) - { - return _stages.ContainsKey(stage); - } - - public void AddUnOpenStage(string stage) - { - _stages.Add(stage, new() - { - Display = stage, - Value = stage, - Activity = new() - { - UtcStartTime = DateTime.MinValue, - UtcExpireTime = DateTime.MinValue, - }, - }); - } - - /// - /// Determine whether stage is open - /// - /// Stage name - /// Current day of week - /// Whether stage is open - public bool IsStageOpen(string? stage, DayOfWeek dayOfWeek) - { - return stage is not null && GetStageInfo(stage).IsStageOpen(dayOfWeek); - } - - /// - /// Gets open stage tips at specified day of week - /// - /// Day of week - /// Open stages - public string GetStageTips(DayOfWeek dayOfWeek) - { - var lines = new List(); - var shownSideStories = new HashSet(); - bool resourceTipShown = false; - DateTime now = DateTime.UtcNow; - - foreach (var stage in _stages.Values.Where(s => s.IsStageOpen(dayOfWeek))) - { - var activity = stage.Activity; - - // Resource collection tip - only show one - if (!resourceTipShown && activity is { IsResourceCollection: true, BeingOpen: true }) - { - // 插入到第一行 - lines.Insert(0, $"「{activity.Tip}」 {LocalizationHelper.GetString("DaysLeftOpen")}{GetDaysLeftText(activity.UtcExpireTime, now)}"); - resourceTipShown = true; - } - - // Side story tips - if (!string.IsNullOrEmpty(activity?.StageName) && shownSideStories.Add(activity.StageName)) - { - lines.Add($"「{activity.StageName}」 {LocalizationHelper.GetString("DaysLeftOpen")}{GetDaysLeftText(activity.UtcExpireTime, now)}"); - } - - // Side story Drop item tips - if (!string.IsNullOrEmpty(stage.Drop)) - { - lines.Add($"{stage.Value}: {ItemListHelper.GetItemName(stage.Drop)}"); - } - - // Normal stage tips - if (!string.IsNullOrEmpty(stage.Tip)) - { - lines.Add(stage.Tip); - } - } - - return lines.Count > 0 ? string.Join(Environment.NewLine, lines) : string.Empty; - } - - /// - /// Gets the days left text - /// - /// 活动结束时间 - /// 当前时间 - /// 活动剩余日期 - private static string GetDaysLeftText(DateTime expireTime, DateTime now) - { - int daysLeft = (expireTime - now).Days; - return daysLeft > 0 ? daysLeft.ToString() : LocalizationHelper.GetString("LessThanOneDay"); - } - - /// - /// Gets open stage list at specified day of week - /// - /// Day of week - /// Open stage list - public IEnumerable GetStageList(DayOfWeek dayOfWeek) - { - return _stages.Values.Where(stage => !stage.IsHidden && stage.IsStageOpen(dayOfWeek)); - } - - /// - /// Gets all open or will open stage list - /// - /// Open or will open stage list - public IEnumerable GetStageList() - { - return _stages.Values.Where(stage => !stage.IsHidden && stage.IsStageOpenOrWillOpen()); + _logger.Error(e, "Failed to parse Cache Stage resources"); } } + + private static StageInfo CreateStageInfo(JToken? stageObj, bool unsupportedStages, SemVersion? minRequiredObj) + { + return new() + { + Display = unsupportedStages + ? LocalizationHelper.GetString("UnsupportedStages") + : stageObj?["Display"]?.ToString() ?? string.Empty, + Value = unsupportedStages + ? LocalizationHelper.GetString("UnsupportedStages") + : stageObj?["Value"]?.ToString() ?? string.Empty, + Drop = unsupportedStages + ? LocalizationHelper.GetString("LowVersion") + '\n' + LocalizationHelper.GetString("MinimumRequirements") + minRequiredObj + : stageObj?["Drop"]?.ToString(), + Activity = new() + { + Tip = stageObj?["Activity"]?["Tip"]?.ToString(), + StageName = stageObj?["Activity"]?["StageName"]?.ToString(), + UtcStartTime = ParseDateTime(stageObj?["Activity"], "UtcStartTime"), + UtcExpireTime = ParseDateTime(stageObj?["Activity"], "UtcExpireTime"), + }, + }; + } + + private static void AddPermanentStages(Dictionary tempStage, StageActivityInfo resourceCollection) + { + var permanentStages = new Dictionary + { + // 主线关卡 + { "1-7", new() { Display = "1-7", Value = "1-7" } }, + { "R8-11", new() { Display = "R8-11", Value = "R8-11" } }, + { "12-17-HARD", new() { Display = "12-17-HARD", Value = "12-17-HARD" } }, + + // 资源本 + { "CE-6", new("CE-6", "CETip", [DayOfWeek.Tuesday, DayOfWeek.Thursday, DayOfWeek.Saturday, DayOfWeek.Sunday], resourceCollection) }, + { "AP-5", new("AP-5", "APTip", [DayOfWeek.Monday, DayOfWeek.Thursday, DayOfWeek.Saturday, DayOfWeek.Sunday], resourceCollection) }, + { "CA-5", new("CA-5", "CATip", [DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Friday, DayOfWeek.Sunday], resourceCollection) }, + { "LS-6", new("LS-6", "LSTip", [], resourceCollection) }, + { "SK-5", new("SK-5", "SKTip", [DayOfWeek.Monday, DayOfWeek.Wednesday, DayOfWeek.Friday, DayOfWeek.Saturday], resourceCollection) }, + + // 剿灭模式 + { "Annihilation", new() { Display = LocalizationHelper.GetString("AnnihilationMode"), Value = "Annihilation" } }, + + // 芯片本 + { "PR-A-1", new("PR-A-1", "PR-ATip", [DayOfWeek.Monday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Sunday], resourceCollection) }, + { "PR-A-2", new("PR-A-2", string.Empty, [DayOfWeek.Monday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Sunday], resourceCollection) }, + { "PR-B-1", new("PR-B-1", "PR-BTip", [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Friday, DayOfWeek.Saturday], resourceCollection) }, + { "PR-B-2", new("PR-B-2", string.Empty, [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Friday, DayOfWeek.Saturday], resourceCollection) }, + { "PR-C-1", new("PR-C-1", "PR-CTip", [DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Saturday, DayOfWeek.Sunday], resourceCollection) }, + { "PR-C-2", new("PR-C-2", string.Empty, [DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Saturday, DayOfWeek.Sunday], resourceCollection) }, + { "PR-D-1", new("PR-D-1", "PR-DTip", [DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Saturday, DayOfWeek.Sunday], resourceCollection) }, + { "PR-D-2", new("PR-D-2", string.Empty, [DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Saturday, DayOfWeek.Sunday], resourceCollection) }, + }; + + foreach (var kvp in permanentStages) + { + tempStage.TryAdd(kvp.Key, kvp.Value); + } + } + + /// + /// Gets stage by name + /// + /// Stage name + /// Stage info + public StageInfo GetStageInfo(string stage) + { + _stages.TryGetValue(stage, out var stageInfo); + stageInfo ??= new() { Display = stage, Value = stage }; + return stageInfo; + } + + public bool IsStageInStageList(string stage) + { + return _stages.ContainsKey(stage); + } + + public void AddUnOpenStage(string stage) + { + _stages.Add(stage, new() + { + Display = stage, + Value = stage, + Activity = new() + { + UtcStartTime = DateTime.MinValue, + UtcExpireTime = DateTime.MinValue, + }, + }); + } + + /// + /// Determine whether stage is open + /// + /// Stage name + /// Current day of week + /// Whether stage is open + public bool IsStageOpen(string? stage, DayOfWeek dayOfWeek) + { + return stage is not null && GetStageInfo(stage).IsStageOpen(dayOfWeek); + } + + /// + /// Gets open stage tips at specified day of week + /// + /// Day of week + /// Open stages + public string GetStageTips(DayOfWeek dayOfWeek) + { + var lines = new List(); + var shownSideStories = new HashSet(); + bool resourceTipShown = false; + DateTime now = DateTime.UtcNow; + + foreach (var stage in _stages.Values.Where(s => s.IsStageOpen(dayOfWeek))) + { + var activity = stage.Activity; + + // Resource collection tip - only show one + if (!resourceTipShown && activity is { IsResourceCollection: true, BeingOpen: true }) + { + // 插入到第一行 + lines.Insert(0, $"「{activity.Tip}」 {LocalizationHelper.GetString("DaysLeftOpen")}{GetDaysLeftText(activity.UtcExpireTime, now)}"); + resourceTipShown = true; + } + + // Side story tips + if (!string.IsNullOrEmpty(activity?.StageName) && shownSideStories.Add(activity.StageName)) + { + lines.Add($"「{activity.StageName}」 {LocalizationHelper.GetString("DaysLeftOpen")}{GetDaysLeftText(activity.UtcExpireTime, now)}"); + } + + // Side story Drop item tips + if (!string.IsNullOrEmpty(stage.Drop)) + { + lines.Add($"{stage.Value}: {ItemListHelper.GetItemName(stage.Drop)}"); + } + + // Normal stage tips + if (!string.IsNullOrEmpty(stage.Tip)) + { + lines.Add(stage.Tip); + } + } + + return lines.Count > 0 ? string.Join(Environment.NewLine, lines) : string.Empty; + } + + /// + /// Gets the days left text + /// + /// 活动结束时间 + /// 当前时间 + /// 活动剩余日期 + private static string GetDaysLeftText(DateTime expireTime, DateTime now) + { + int daysLeft = (expireTime - now).Days; + return daysLeft > 0 ? daysLeft.ToString() : LocalizationHelper.GetString("LessThanOneDay"); + } + + /// + /// Gets open stage list at specified day of week + /// + /// Day of week + /// Open stage list + public IEnumerable GetStageList(DayOfWeek dayOfWeek) + { + return _stages.Values.Where(stage => !stage.IsHidden && stage.IsStageOpen(dayOfWeek)); + } + + /// + /// Gets all open or will open stage list + /// + /// Open or will open stage list + public IEnumerable GetStageList() + { + return _stages.Values.Where(stage => !stage.IsHidden && stage.IsStageOpenOrWillOpen()); + } } diff --git a/src/MaaWpfGui/Services/Web/GameDataReportService.cs b/src/MaaWpfGui/Services/Web/GameDataReportService.cs index 90de62b25d..f767b382b8 100644 --- a/src/MaaWpfGui/Services/Web/GameDataReportService.cs +++ b/src/MaaWpfGui/Services/Web/GameDataReportService.cs @@ -16,105 +16,102 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Text; using System.Threading.Tasks; using MaaWpfGui.Constants; using MaaWpfGui.Helper; -using Newtonsoft.Json.Linq; using Serilog; -namespace MaaWpfGui.Services.Web +namespace MaaWpfGui.Services.Web; + +public class GameDataReportService { - public class GameDataReportService + private static readonly ILogger _logger = Log.ForContext(); + private const int MaxRetryPerDomain = 3; + private const int InitialBackoffMs = 3000; + + public static async Task PostWithRetryAsync(string url, HttpContent content, Dictionary headers, string subtask, Action? callback = null) { - private static readonly ILogger _logger = Log.ForContext(); - private const int MaxRetryPerDomain = 3; - private const int InitialBackoffMs = 3000; + _logger.Information("Start PostWithRetryAsync, url: {Url}", url); + _logger.Information("Request headers: {@Headers}", headers); + var body = await content.ReadAsStringAsync(); + _logger.Information("Request body: {Body}", body); - public static async Task PostWithRetryAsync(string url, HttpContent content, Dictionary headers, string subtask, Action? callback = null) + var originalResponse = await TryPostAsync(url, content, headers); + if (originalResponse != null && (int)originalResponse.StatusCode == 200) { - _logger.Information("Start PostWithRetryAsync, url: {Url}", url); - _logger.Information("Request headers: {@Headers}", headers); - var body = await content.ReadAsStringAsync(); - _logger.Information("Request body: {Body}", body); + await ProcessResponseIdAsync(originalResponse, callback); + return true; + } - var originalResponse = await TryPostAsync(url, content, headers); - if (originalResponse != null && (int)originalResponse.StatusCode == 200) + _logger.Warning("Initial request failed, status code: {StatusCode}", originalResponse?.StatusCode); + + if (subtask != "ReportToPenguinStats" || !url.Contains(MaaUrls.PenguinIoDomain)) + { + // 一图流的上报失败不提示 + return true; + } + + foreach (var backupDomain in MaaUrls.PenguinBackupDomains) + { + var newUrl = url.Replace(MaaUrls.PenguinIoDomain, backupDomain); + _logger.Information("Trying backup domain {BackupDomain}, url: {NewUrl}", backupDomain, newUrl); + var resp = await TryPostAsync(newUrl, content, headers); + if (resp != null && (int)resp.StatusCode == 200) { - await ProcessResponseIdAsync(originalResponse, callback); + await ProcessResponseIdAsync(resp, callback); return true; } - _logger.Warning("Initial request failed, status code: {StatusCode}", originalResponse?.StatusCode); - - if (subtask != "ReportToPenguinStats" || !url.Contains(MaaUrls.PenguinIoDomain)) - { - // 一图流的上报失败不提示 - return true; - } - - foreach (var backupDomain in MaaUrls.PenguinBackupDomains) - { - var newUrl = url.Replace(MaaUrls.PenguinIoDomain, backupDomain); - _logger.Information("Trying backup domain {BackupDomain}, url: {NewUrl}", backupDomain, newUrl); - var resp = await TryPostAsync(newUrl, content, headers); - if (resp != null && (int)resp.StatusCode == 200) - { - await ProcessResponseIdAsync(resp, callback); - return true; - } - - _logger.Warning("Request failed on backup domain {BackupDomain}, status code: {StatusCode}", backupDomain, resp?.StatusCode); - } - - _logger.Error("All retries failed for URL: {Url}", url); - return false; + _logger.Warning("Request failed on backup domain {BackupDomain}, status code: {StatusCode}", backupDomain, resp?.StatusCode); } - private static async Task TryPostAsync(string targetUrl, HttpContent content, Dictionary headers) - { - try - { - int currentBackoff = InitialBackoffMs; - for (int attempt = 1; attempt <= MaxRetryPerDomain; attempt++) - { - var response = await Instances.HttpService.PostAsync(new(targetUrl), content, headers); + _logger.Error("All retries failed for URL: {Url}", url); + return false; + } - switch ((int)response.StatusCode) - { - case 200: - return response; - case >= 500 and < 600: - await Task.Delay(currentBackoff); - currentBackoff = (int)(currentBackoff * 1.5); - continue; - default: - return response; - } + private static async Task TryPostAsync(string targetUrl, HttpContent content, Dictionary headers) + { + try + { + int currentBackoff = InitialBackoffMs; + for (int attempt = 1; attempt <= MaxRetryPerDomain; attempt++) + { + var response = await Instances.HttpService.PostAsync(new(targetUrl), content, headers); + + switch ((int)response.StatusCode) + { + case 200: + return response; + case >= 500 and < 600: + await Task.Delay(currentBackoff); + currentBackoff = (int)(currentBackoff * 1.5); + continue; + default: + return response; } } - catch (Exception ex) - { - _logger.Error(ex, "Error during HTTP POST request to {TargetUrl}", targetUrl); - } - - return null; + } + catch (Exception ex) + { + _logger.Error(ex, "Error during HTTP POST request to {TargetUrl}", targetUrl); } - private static Task ProcessResponseIdAsync(HttpResponseMessage response, Action? callback) + return null; + } + + private static Task ProcessResponseIdAsync(HttpResponseMessage response, Action? callback) + { + if (callback is null || !response.Headers.TryGetValues("x-penguin-set-penguinid", out var values)) { - if (callback is null || !response.Headers.TryGetValues("x-penguin-set-penguinid", out var values)) - { - return Task.CompletedTask; - } - - var penguinId = values.FirstOrDefault(); - if (!string.IsNullOrEmpty(penguinId)) - { - callback.Invoke(penguinId); - } - return Task.CompletedTask; } + + var penguinId = values.FirstOrDefault(); + if (!string.IsNullOrEmpty(penguinId)) + { + callback.Invoke(penguinId); + } + + return Task.CompletedTask; } } diff --git a/src/MaaWpfGui/Services/Web/HttpService.cs b/src/MaaWpfGui/Services/Web/HttpService.cs index b8919e1368..1eddd2ebc2 100755 --- a/src/MaaWpfGui/Services/Web/HttpService.cs +++ b/src/MaaWpfGui/Services/Web/HttpService.cs @@ -29,130 +29,65 @@ using MaaWpfGui.Helper; using MaaWpfGui.ViewModels.UI; using Serilog; -namespace MaaWpfGui.Services.Web +namespace MaaWpfGui.Services.Web; + +public class HttpService : IHttpService { - public class HttpService : IHttpService + private readonly string UserAgent; + + private static string Proxy { - private readonly string UserAgent; - - private static string Proxy + get { - get + var proxy = SettingsViewModel.VersionUpdateSettings.Proxy; + if (string.IsNullOrEmpty(proxy)) { - var proxy = SettingsViewModel.VersionUpdateSettings.Proxy; - if (string.IsNullOrEmpty(proxy)) - { - return string.Empty; - } - - return proxy.Contains("://") ? proxy : SettingsViewModel.VersionUpdateSettings.ProxyType + $"://{proxy}"; + return string.Empty; } + + return proxy.Contains("://") ? proxy : SettingsViewModel.VersionUpdateSettings.ProxyType + $"://{proxy}"; } + } - private readonly ILogger _logger = Log.ForContext(); + private readonly ILogger _logger = Log.ForContext(); - private HttpClient _client; + private HttpClient _client; - public HttpService() + public HttpService() + { + string uiVersion = Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion.Split('+')[0] ?? "0.0.1"; + UserAgent = $"MaaWpfGui/{uiVersion}"; + + ConfigurationHelper.ConfigurationUpdateEvent += (key, old, value) => { - string uiVersion = Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion.Split('+')[0] ?? "0.0.1"; - UserAgent = $"MaaWpfGui/{uiVersion}"; - - ConfigurationHelper.ConfigurationUpdateEvent += (key, old, value) => + if (key != ConfigurationKeys.UpdateProxy) { - if (key != ConfigurationKeys.UpdateProxy) - { - return; - } + return; + } - if (old == value) - { - return; - } + if (old == value) + { + return; + } - if (GetProxy() == null) - { - _logger.Warning("Proxy is not a valid URI, and HttpClient is not null, keep using the original HttpClient"); - return; - } - - _client = BuildHttpClient(); - }; + if (GetProxy() == null) + { + _logger.Warning("Proxy is not a valid URI, and HttpClient is not null, keep using the original HttpClient"); + return; + } _client = BuildHttpClient(); - } + }; - public async Task HeadAsync(Uri uri, Dictionary? extraHeader = null, UriPartial uriPartial = UriPartial.Query) + _client = BuildHttpClient(); + } + + public async Task HeadAsync(Uri uri, Dictionary? extraHeader = null, UriPartial uriPartial = UriPartial.Query) + { + try { - try - { - var request = new HttpRequestMessage { RequestUri = uri, Method = HttpMethod.Head, Version = HttpVersion.Version20, }; + var request = new HttpRequestMessage { RequestUri = uri, Method = HttpMethod.Head, Version = HttpVersion.Version20, }; - if (extraHeader != null) - { - foreach (var kvp in extraHeader) - { - request.Headers.Add(kvp.Key, kvp.Value); - } - } - - request.Headers.ConnectionClose = true; - - var stopwatch = Stopwatch.StartNew(); - var response = await _client.SendAsync(request).ConfigureAwait(false); - stopwatch.Stop(); - response.Log(uriPartial, stopwatch.Elapsed.TotalMilliseconds); - - return response.IsSuccessStatusCode is false ? -1.0 : stopwatch.Elapsed.TotalMilliseconds; - } - catch (Exception e) - { - _logger.Error(e, "Failed to send GET request to {Uri}", uri); - return -1.0; - } - } - - public async Task GetStringAsync(Uri uri, Dictionary? extraHeader = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead) - { - try - { - var response = await GetAsync(uri, extraHeader, httpCompletionOption); - if (response.StatusCode != HttpStatusCode.OK) - { - return null; - } - - return await response.Content.ReadAsStringAsync(); - } - catch (Exception e) - { - _logger.Error(e, "Failed to send GET request to {Uri}", uri); - return null; - } - } - - public async Task GetStreamAsync(Uri uri, Dictionary? extraHeader = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead) - { - try - { - var response = await GetAsync(uri, extraHeader, httpCompletionOption); - if (response.StatusCode != HttpStatusCode.OK) - { - return null; - } - - return await response.Content.ReadAsStreamAsync(); - } - catch (Exception e) - { - _logger.Error(e, "Failed to send GET request to {Uri}", uri); - return null; - } - } - - public async Task GetAsync(Uri uri, Dictionary? extraHeader = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseHeadersRead, UriPartial uriPartial = UriPartial.Query) - { - var request = new HttpRequestMessage { RequestUri = uri, Method = HttpMethod.Get, Version = HttpVersion.Version20, }; if (extraHeader != null) { foreach (var kvp in extraHeader) @@ -161,167 +96,231 @@ namespace MaaWpfGui.Services.Web } } + request.Headers.ConnectionClose = true; + var stopwatch = Stopwatch.StartNew(); - var response = await _client.SendAsync(request, httpCompletionOption); + var response = await _client.SendAsync(request).ConfigureAwait(false); stopwatch.Stop(); response.Log(uriPartial, stopwatch.Elapsed.TotalMilliseconds); - return response; + + return response.IsSuccessStatusCode is false ? -1.0 : stopwatch.Elapsed.TotalMilliseconds; } - - public async Task PostAsJsonAsync(Uri uri, T content, Dictionary? extraHeader = null) + catch (Exception e) { - try - { - var response = await PostAsync(uri, new StringContent(JsonSerializer.Serialize(content), Encoding.UTF8, "application/json"), extraHeader); - return await response.Content.ReadAsStringAsync(); - } - catch (Exception e) - { - _logger.Error(e, "Failed to send POST request to {Uri}", uri); - return null; - } - } - - public async Task PostAsFormUrlEncodedAsync(Uri uri, Dictionary content, Dictionary? extraHeader = null) - { - try - { - var response = await PostAsync(uri, new FormUrlEncodedContent(content), extraHeader); - return await response.Content.ReadAsStringAsync(); - } - catch (Exception e) - { - _logger.Error(e, "Failed to send POST request to {Uri}", uri); - return null; - } - } - - public async Task PostAsync(Uri uri, HttpContent content, Dictionary? extraHeader = null, UriPartial uriPartial = UriPartial.Query) - { - var message = new HttpRequestMessage(HttpMethod.Post, uri) { Version = HttpVersion.Version20 }; - if (extraHeader is not null) - { - foreach (var header in extraHeader) - { - message.Headers.Add(header.Key, header.Value); - } - } - - message.Headers.Accept.ParseAdd("application/json"); - message.Content = content; - var stopwatch = Stopwatch.StartNew(); - var response = await _client.SendAsync(message); - stopwatch.Stop(); - response.Log(uriPartial, stopwatch.Elapsed.TotalMilliseconds); - return response; - } - - public async Task DownloadFileAsync(Uri uri, string fileName, string? contentType = "application/octet-stream") - { - string fileDir = PathsHelper.BaseDir; - string fileNameWithTemp = fileName + ".temp"; - string fullFilePath = Path.Combine(fileDir, fileName); - string fullFilePathWithTemp = Path.Combine(fileDir, fileNameWithTemp); - _logger.Information("Start to download file from {Uri} and save to {TempPath}", uri, fullFilePathWithTemp); - - HttpResponseMessage response; - try - { - response = await GetAsync(uri, extraHeader: new Dictionary { { "Accept", contentType ?? "application/octet-stream" } }, httpCompletionOption: HttpCompletionOption.ResponseHeadersRead); - if (response.StatusCode != HttpStatusCode.OK) - { - return false; - } - } - catch (Exception e) - { - _logger.Error(e, "Failed to send GET request to {Uri}", uri); - return false; - } - - var success = true; - try - { - var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - await using (var tempFileStream = new FileStream(fullFilePathWithTemp, FileMode.Create, FileAccess.Write)) - { - // 记录初始化 - long value = 0; - int valueInOneSecond = 0; - long fileMaximum = response.Content.Headers.ContentLength ?? 1; - DateTime beforeDt = DateTime.Now; - - // Dangerous action - VersionUpdateViewModel.OutputDownloadProgress(); - - byte[] buffer = new byte[81920]; - int byteLen = await stream.ReadAsync(buffer, 0, buffer.Length); - - while (byteLen > 0) - { - valueInOneSecond += byteLen; - double ts = DateTime.Now.Subtract(beforeDt).TotalSeconds; - if (ts > 1) - { - beforeDt = DateTime.Now; - value += valueInOneSecond; - - // Dangerous action - VersionUpdateViewModel.OutputDownloadProgress(value, fileMaximum, valueInOneSecond, ts); - valueInOneSecond = 0; - } - - // 输入输出 - tempFileStream.Write(buffer, 0, byteLen); - byteLen = await stream.ReadAsync(buffer, 0, buffer.Length); - } - } - - File.Copy(fullFilePathWithTemp, fullFilePath, true); - } - catch (Exception e) - { - _logger.Error(e, "Failed to copy file stream {TempFile}", fullFilePathWithTemp); - success = false; - } - finally - { - if (File.Exists(fullFilePathWithTemp)) - { - _logger.Information("Remove download temp file {TempFile}", fullFilePathWithTemp); - File.Delete(fullFilePathWithTemp); - } - } - - return success; - } - - private static WebProxy? GetProxy() - { - var proxyIsUri = Uri.TryCreate(Proxy, UriKind.RelativeOrAbsolute, out var uri); - return (proxyIsUri && (!string.IsNullOrEmpty(Proxy))) is false ? null : new WebProxy(uri); - } - - private HttpClient BuildHttpClient() - { - var handler = new HttpClientHandler - { - AutomaticDecompression = DecompressionMethods.All, - AllowAutoRedirect = true, - }; - - var proxy = GetProxy(); - if (proxy != null) - { - _logger.Information("Rebuild HttpClient with proxy {Proxy}", Proxy); - handler.Proxy = proxy; - handler.UseProxy = true; - } - - HttpClient client = new HttpClient(handler); - client.DefaultRequestHeaders.Add("User-Agent", UserAgent); - client.Timeout = TimeSpan.FromSeconds(15); - return client; + _logger.Error(e, "Failed to send GET request to {Uri}", uri); + return -1.0; } } + + public async Task GetStringAsync(Uri uri, Dictionary? extraHeader = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead) + { + try + { + var response = await GetAsync(uri, extraHeader, httpCompletionOption); + if (response.StatusCode != HttpStatusCode.OK) + { + return null; + } + + return await response.Content.ReadAsStringAsync(); + } + catch (Exception e) + { + _logger.Error(e, "Failed to send GET request to {Uri}", uri); + return null; + } + } + + public async Task GetStreamAsync(Uri uri, Dictionary? extraHeader = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead) + { + try + { + var response = await GetAsync(uri, extraHeader, httpCompletionOption); + if (response.StatusCode != HttpStatusCode.OK) + { + return null; + } + + return await response.Content.ReadAsStreamAsync(); + } + catch (Exception e) + { + _logger.Error(e, "Failed to send GET request to {Uri}", uri); + return null; + } + } + + public async Task GetAsync(Uri uri, Dictionary? extraHeader = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseHeadersRead, UriPartial uriPartial = UriPartial.Query) + { + var request = new HttpRequestMessage { RequestUri = uri, Method = HttpMethod.Get, Version = HttpVersion.Version20, }; + if (extraHeader != null) + { + foreach (var kvp in extraHeader) + { + request.Headers.Add(kvp.Key, kvp.Value); + } + } + + var stopwatch = Stopwatch.StartNew(); + var response = await _client.SendAsync(request, httpCompletionOption); + stopwatch.Stop(); + response.Log(uriPartial, stopwatch.Elapsed.TotalMilliseconds); + return response; + } + + public async Task PostAsJsonAsync(Uri uri, T content, Dictionary? extraHeader = null) + { + try + { + var response = await PostAsync(uri, new StringContent(JsonSerializer.Serialize(content), Encoding.UTF8, "application/json"), extraHeader); + return await response.Content.ReadAsStringAsync(); + } + catch (Exception e) + { + _logger.Error(e, "Failed to send POST request to {Uri}", uri); + return null; + } + } + + public async Task PostAsFormUrlEncodedAsync(Uri uri, Dictionary content, Dictionary? extraHeader = null) + { + try + { + var response = await PostAsync(uri, new FormUrlEncodedContent(content), extraHeader); + return await response.Content.ReadAsStringAsync(); + } + catch (Exception e) + { + _logger.Error(e, "Failed to send POST request to {Uri}", uri); + return null; + } + } + + public async Task PostAsync(Uri uri, HttpContent content, Dictionary? extraHeader = null, UriPartial uriPartial = UriPartial.Query) + { + var message = new HttpRequestMessage(HttpMethod.Post, uri) { Version = HttpVersion.Version20 }; + if (extraHeader is not null) + { + foreach (var header in extraHeader) + { + message.Headers.Add(header.Key, header.Value); + } + } + + message.Headers.Accept.ParseAdd("application/json"); + message.Content = content; + var stopwatch = Stopwatch.StartNew(); + var response = await _client.SendAsync(message); + stopwatch.Stop(); + response.Log(uriPartial, stopwatch.Elapsed.TotalMilliseconds); + return response; + } + + public async Task DownloadFileAsync(Uri uri, string fileName, string? contentType = "application/octet-stream") + { + string fileDir = PathsHelper.BaseDir; + string fileNameWithTemp = fileName + ".temp"; + string fullFilePath = Path.Combine(fileDir, fileName); + string fullFilePathWithTemp = Path.Combine(fileDir, fileNameWithTemp); + _logger.Information("Start to download file from {Uri} and save to {TempPath}", uri, fullFilePathWithTemp); + + HttpResponseMessage response; + try + { + response = await GetAsync(uri, extraHeader: new Dictionary { { "Accept", contentType ?? "application/octet-stream" } }, httpCompletionOption: HttpCompletionOption.ResponseHeadersRead); + if (response.StatusCode != HttpStatusCode.OK) + { + return false; + } + } + catch (Exception e) + { + _logger.Error(e, "Failed to send GET request to {Uri}", uri); + return false; + } + + var success = true; + try + { + var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using (var tempFileStream = new FileStream(fullFilePathWithTemp, FileMode.Create, FileAccess.Write)) + { + // 记录初始化 + long value = 0; + int valueInOneSecond = 0; + long fileMaximum = response.Content.Headers.ContentLength ?? 1; + DateTime beforeDt = DateTime.Now; + + // Dangerous action + VersionUpdateViewModel.OutputDownloadProgress(); + + byte[] buffer = new byte[81920]; + int byteLen = await stream.ReadAsync(buffer, 0, buffer.Length); + + while (byteLen > 0) + { + valueInOneSecond += byteLen; + double ts = DateTime.Now.Subtract(beforeDt).TotalSeconds; + if (ts > 1) + { + beforeDt = DateTime.Now; + value += valueInOneSecond; + + // Dangerous action + VersionUpdateViewModel.OutputDownloadProgress(value, fileMaximum, valueInOneSecond, ts); + valueInOneSecond = 0; + } + + // 输入输出 + tempFileStream.Write(buffer, 0, byteLen); + byteLen = await stream.ReadAsync(buffer, 0, buffer.Length); + } + } + + File.Copy(fullFilePathWithTemp, fullFilePath, true); + } + catch (Exception e) + { + _logger.Error(e, "Failed to copy file stream {TempFile}", fullFilePathWithTemp); + success = false; + } + finally + { + if (File.Exists(fullFilePathWithTemp)) + { + _logger.Information("Remove download temp file {TempFile}", fullFilePathWithTemp); + File.Delete(fullFilePathWithTemp); + } + } + + return success; + } + + private static WebProxy? GetProxy() + { + var proxyIsUri = Uri.TryCreate(Proxy, UriKind.RelativeOrAbsolute, out var uri); + return (proxyIsUri && (!string.IsNullOrEmpty(Proxy))) is false ? null : new WebProxy(uri); + } + + private HttpClient BuildHttpClient() + { + var handler = new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.All, + AllowAutoRedirect = true, + }; + + var proxy = GetProxy(); + if (proxy != null) + { + _logger.Information("Rebuild HttpClient with proxy {Proxy}", Proxy); + handler.Proxy = proxy; + handler.UseProxy = true; + } + + HttpClient client = new HttpClient(handler); + client.DefaultRequestHeaders.Add("User-Agent", UserAgent); + client.Timeout = TimeSpan.FromSeconds(15); + return client; + } } diff --git a/src/MaaWpfGui/Services/Web/IHttpService.cs b/src/MaaWpfGui/Services/Web/IHttpService.cs index e4775ae3e3..a666d6e485 100644 --- a/src/MaaWpfGui/Services/Web/IHttpService.cs +++ b/src/MaaWpfGui/Services/Web/IHttpService.cs @@ -20,84 +20,83 @@ using System.Net.Http; using System.Threading.Tasks; using JetBrains.Annotations; -namespace MaaWpfGui.Services.Web +namespace MaaWpfGui.Services.Web; + +public interface IHttpService { - public interface IHttpService - { - /// - /// Test url available and return legacy - /// - /// Target Uri - /// Extra HTTP Request Headers - /// Which parts of uri to log - /// Legacy in ms, -1 when response code not equal 200 - Task HeadAsync(Uri uri, Dictionary? extraHeader = null, UriPartial uriPartial = UriPartial.Query); + /// + /// Test url available and return legacy + /// + /// Target Uri + /// Extra HTTP Request Headers + /// Which parts of uri to log + /// Legacy in ms, -1 when response code not equal 200 + Task HeadAsync(Uri uri, Dictionary? extraHeader = null, UriPartial uriPartial = UriPartial.Query); - /// - /// Send HTTP GET request and get a string response - /// - /// Target Uri - /// Extra HTTP Request Headers - /// The HTTP completion option - /// Response string, null when failed - Task GetStringAsync(Uri uri, Dictionary? extraHeader = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead); + /// + /// Send HTTP GET request and get a string response + /// + /// Target Uri + /// Extra HTTP Request Headers + /// The HTTP completion option + /// Response string, null when failed + Task GetStringAsync(Uri uri, Dictionary? extraHeader = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead); - /// - /// Send HTTP GET request and get a body stream response - /// - /// Target Uri - /// Extra HTTP Request Headers - /// The HTTP completion option - /// Response stream, null when failed - [UsedImplicitly] - Task GetStreamAsync(Uri uri, Dictionary? extraHeader = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead); + /// + /// Send HTTP GET request and get a body stream response + /// + /// Target Uri + /// Extra HTTP Request Headers + /// The HTTP completion option + /// Response stream, null when failed + [UsedImplicitly] + Task GetStreamAsync(Uri uri, Dictionary? extraHeader = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead); - /// - /// Send HTTP GET request and get the original - /// - /// Target Uri - /// Extra HTTP Request Headers - /// The HTTP completion option - /// Which parts of uri to log - /// object - Task GetAsync(Uri uri, Dictionary? extraHeader = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseHeadersRead, UriPartial uriPartial = UriPartial.Query); + /// + /// Send HTTP GET request and get the original + /// + /// Target Uri + /// Extra HTTP Request Headers + /// The HTTP completion option + /// Which parts of uri to log + /// object + Task GetAsync(Uri uri, Dictionary? extraHeader = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseHeadersRead, UriPartial uriPartial = UriPartial.Query); - /// - /// Send HTTP POST request and a string response - /// - /// Target Uri - /// The POST body content, will be serialized by - /// Extra HTTP Request Headers - /// The type of the POST body content - /// Response string, null when failed - Task PostAsJsonAsync(Uri uri, T content, Dictionary? extraHeader = null); + /// + /// Send HTTP POST request and a string response + /// + /// Target Uri + /// The POST body content, will be serialized by + /// Extra HTTP Request Headers + /// The type of the POST body content + /// Response string, null when failed + Task PostAsJsonAsync(Uri uri, T content, Dictionary? extraHeader = null); - /// - /// Send HTTP POST request and a string response - /// - /// Target Uri - /// The POST body content - /// Extra HTTP Request Headers - /// Response string, null when failed - Task PostAsFormUrlEncodedAsync(Uri uri, Dictionary content, Dictionary? extraHeader = null); + /// + /// Send HTTP POST request and a string response + /// + /// Target Uri + /// The POST body content + /// Extra HTTP Request Headers + /// Response string, null when failed + Task PostAsFormUrlEncodedAsync(Uri uri, Dictionary content, Dictionary? extraHeader = null); - /// - /// Send HTTP POST request with raw HttpContent and get the response - /// - /// Target Uri - /// The POST body content - /// Extra HTTP Request Headers - /// Which parts of uri to log - /// HttpResponseMessage, null when failed - Task PostAsync(Uri uri, HttpContent content, Dictionary? extraHeader = null, UriPartial uriPartial = UriPartial.Query); + /// + /// Send HTTP POST request with raw HttpContent and get the response + /// + /// Target Uri + /// The POST body content + /// Extra HTTP Request Headers + /// Which parts of uri to log + /// HttpResponseMessage, null when failed + Task PostAsync(Uri uri, HttpContent content, Dictionary? extraHeader = null, UriPartial uriPartial = UriPartial.Query); - /// - /// Download a file from the Web - /// - /// The URI of the file - /// On disk filename - /// File content type - /// True if success, False if failed - Task DownloadFileAsync(Uri uri, string fileName, string? contentType = null); - } + /// + /// Download a file from the Web + /// + /// The URI of the file + /// On disk filename + /// File content type + /// True if success, False if failed + Task DownloadFileAsync(Uri uri, string fileName, string? contentType = null); } diff --git a/src/MaaWpfGui/Services/Web/IMaaApiService.cs b/src/MaaWpfGui/Services/Web/IMaaApiService.cs index 2233e74a2c..b43fb6e050 100644 --- a/src/MaaWpfGui/Services/Web/IMaaApiService.cs +++ b/src/MaaWpfGui/Services/Web/IMaaApiService.cs @@ -14,12 +14,11 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; -namespace MaaWpfGui.Services.Web -{ - public interface IMaaApiService - { - Task RequestMaaApiWithCache(string api, bool allowFallbackToCache = true); +namespace MaaWpfGui.Services.Web; - JObject LoadApiCache(string api); - } +public interface IMaaApiService +{ + Task RequestMaaApiWithCache(string api, bool allowFallbackToCache = true); + + JObject LoadApiCache(string api); } diff --git a/src/MaaWpfGui/Services/Web/MaaApiService.cs b/src/MaaWpfGui/Services/Web/MaaApiService.cs index 633dc4b30a..64ad1b6cf2 100644 --- a/src/MaaWpfGui/Services/Web/MaaApiService.cs +++ b/src/MaaWpfGui/Services/Web/MaaApiService.cs @@ -20,92 +20,91 @@ using MaaWpfGui.Helper; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace MaaWpfGui.Services.Web +namespace MaaWpfGui.Services.Web; + +public class MaaApiService : IMaaApiService { - public class MaaApiService : IMaaApiService + private static readonly string CacheDir = PathsHelper.BaseDir + "/cache/"; + + public async Task RequestMaaApiWithCache(string api, bool allowFallbackToCache = true) { - private static readonly string CacheDir = PathsHelper.BaseDir + "/cache/"; + return await RequestWithFallback(api, MaaUrls.MaaApi, MaaUrls.MaaApi2, allowFallbackToCache); + } - public async Task RequestMaaApiWithCache(string api, bool allowFallbackToCache = true) + private async Task RequestWithFallback(string api, string primaryBaseUrl, string? fallbackBaseUrl = null, bool allowFallbackToCache = true) + { + var json = await TryRequest(api, primaryBaseUrl, allowFallbackToCache); + if (json != null || string.IsNullOrEmpty(fallbackBaseUrl)) { - return await RequestWithFallback(api, MaaUrls.MaaApi, MaaUrls.MaaApi2, allowFallbackToCache); + return json; } - private async Task RequestWithFallback(string api, string primaryBaseUrl, string? fallbackBaseUrl = null, bool allowFallbackToCache = true) - { - var json = await TryRequest(api, primaryBaseUrl, allowFallbackToCache); - if (json != null || string.IsNullOrEmpty(fallbackBaseUrl)) - { - return json; - } + return await TryRequest(api, fallbackBaseUrl, allowFallbackToCache); + } - return await TryRequest(api, fallbackBaseUrl, allowFallbackToCache); + private async Task TryRequest(string api, string baseUrl, bool allowFallbackToCache = true) + { + var url = baseUrl + api; + var cache = CacheDir + api; + + var response = await ETagCache.FetchResponseWithEtag(url, !File.Exists(cache)); + if (response == null) + { + return allowFallbackToCache ? LoadApiCache(api) : null; } - private async Task TryRequest(string api, string baseUrl, bool allowFallbackToCache = true) + if (response.StatusCode == System.Net.HttpStatusCode.NotModified) { - var url = baseUrl + api; - var cache = CacheDir + api; - - var response = await ETagCache.FetchResponseWithEtag(url, !File.Exists(cache)); - if (response == null) - { - return allowFallbackToCache ? LoadApiCache(api) : null; - } - - if (response.StatusCode == System.Net.HttpStatusCode.NotModified) - { - return LoadApiCache(api); - } - - if (response.StatusCode != System.Net.HttpStatusCode.OK) - { - return allowFallbackToCache ? LoadApiCache(api) : null; - } - - var body = await HttpResponseHelper.GetStringAsync(response); - if (string.IsNullOrEmpty(body)) - { - return LoadApiCache(api); - } - - try - { - var json = (JObject?)JsonConvert.DeserializeObject(body); - string? directoryPath = Path.GetDirectoryName(cache); - - if (!Directory.Exists(directoryPath)) - { - Directory.CreateDirectory(directoryPath!); - } - - await File.WriteAllTextAsync(cache, body); - ETagCache.Set(response, url); - - return json; - } - catch - { - return null; - } + return LoadApiCache(api); } - public JObject? LoadApiCache(string api) + if (response.StatusCode != System.Net.HttpStatusCode.OK) { - var cache = CacheDir + api; - if (!File.Exists(cache)) + return allowFallbackToCache ? LoadApiCache(api) : null; + } + + var body = await HttpResponseHelper.GetStringAsync(response); + if (string.IsNullOrEmpty(body)) + { + return LoadApiCache(api); + } + + try + { + var json = (JObject?)JsonConvert.DeserializeObject(body); + string? directoryPath = Path.GetDirectoryName(cache); + + if (!Directory.Exists(directoryPath)) { - return null; + Directory.CreateDirectory(directoryPath!); } - try - { - return (JObject?)JsonConvert.DeserializeObject(File.ReadAllText(cache)); - } - catch - { - return null; - } + await File.WriteAllTextAsync(cache, body); + ETagCache.Set(response, url); + + return json; + } + catch + { + return null; + } + } + + public JObject? LoadApiCache(string api) + { + var cache = CacheDir + api; + if (!File.Exists(cache)) + { + return null; + } + + try + { + return (JObject?)JsonConvert.DeserializeObject(File.ReadAllText(cache)); + } + catch + { + return null; } } } diff --git a/src/MaaWpfGui/States/RunningState.cs b/src/MaaWpfGui/States/RunningState.cs index b866c72530..fa85c94e37 100644 --- a/src/MaaWpfGui/States/RunningState.cs +++ b/src/MaaWpfGui/States/RunningState.cs @@ -21,245 +21,244 @@ using MaaWpfGui.Utilities; using MaaWpfGui.ViewModels.UI; using Serilog; -namespace MaaWpfGui.States +namespace MaaWpfGui.States; + +public class RunningState { - public class RunningState + public class RunningStateChangedEventArgs(bool idle, bool inited, bool stopping) : EventArgs { - public class RunningStateChangedEventArgs(bool idle, bool inited, bool stopping) : EventArgs + public bool Idle { get; } = idle; + + public bool Inited { get; } = inited; + + public bool Stopping { get; } = stopping; + } + + private static RunningState? _instance; + private static readonly ILogger _logger = Log.Logger.ForContext(); + + private RunningState() + { + if (ReminderIntervalMinutes < 1) { - public bool Idle { get; } = idle; - - public bool Inited { get; } = inited; - - public bool Stopping { get; } = stopping; + ReminderIntervalMinutes = 1; } - private static RunningState? _instance; - private static readonly ILogger _logger = Log.Logger.ForContext(); + _timeoutReminderTimer.Interval = ReminderIntervalMinutes * 60 * 1000; + _timeoutReminderTimer.Elapsed += TimeoutReminderTimer_Elapsed; + } - private RunningState() + public static RunningState Instance + { + get { - if (ReminderIntervalMinutes < 1) - { - ReminderIntervalMinutes = 1; - } - - _timeoutReminderTimer.Interval = ReminderIntervalMinutes * 60 * 1000; - _timeoutReminderTimer.Elapsed += TimeoutReminderTimer_Elapsed; + _instance ??= new(); + return _instance; } + } - public static RunningState Instance + // 超时相关字段 + private readonly System.Timers.Timer _timeoutReminderTimer = new(); + private DateTime? _taskStartTime; + + public int TaskTimeoutMinutes { get; set; } = SettingsViewModel.GameSettings.TaskTimeoutMinutes; + + private int _reminderIntervalMinutes = SettingsViewModel.GameSettings.ReminderIntervalMinutes; + + public int ReminderIntervalMinutes + { + get => _reminderIntervalMinutes; + set { - get - { - _instance ??= new(); - return _instance; - } - } - - // 超时相关字段 - private readonly System.Timers.Timer _timeoutReminderTimer = new(); - private DateTime? _taskStartTime; - - public int TaskTimeoutMinutes { get; set; } = SettingsViewModel.GameSettings.TaskTimeoutMinutes; - - private int _reminderIntervalMinutes = SettingsViewModel.GameSettings.ReminderIntervalMinutes; - - public int ReminderIntervalMinutes - { - get => _reminderIntervalMinutes; - set - { - if (value < 1) - { - return; - } - - _reminderIntervalMinutes = value; - TimeoutReminderTimer_Elapsed(null, null); - _timeoutReminderTimer.Interval = value * 60 * 1000; - } - } - - // 超时事件 - public event EventHandler? TimeoutOccurred; - - public void StartTimeoutTimer() - { - _taskStartTime = DateTime.Now; - _timeoutReminderTimer.Start(); - } - - public void StopTimeoutTimer() - { - _timeoutReminderTimer.Stop(); - _taskStartTime = null; - } - - public void ResetTimeout() - { - _taskStartTime = DateTime.Now; - } - - // 超时计时器回调 - private void TimeoutReminderTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs? e) - { - if (!_taskStartTime.HasValue || _idle) + if (value < 1) { return; } - var elapsedMinutes = (DateTime.Now - _taskStartTime.Value).TotalMinutes; + _reminderIntervalMinutes = value; + TimeoutReminderTimer_Elapsed(null, null); + _timeoutReminderTimer.Interval = value * 60 * 1000; + } + } - if (elapsedMinutes > 3 * 60) - { - AchievementTrackerHelper.Instance.Unlock(AchievementIds.ProxyOnline3Hours); - } + // 超时事件 + public event EventHandler? TimeoutOccurred; - // 如果任务运行时间未超过超时时间,则直接返回 - if (elapsedMinutes <= TaskTimeoutMinutes) + public void StartTimeoutTimer() + { + _taskStartTime = DateTime.Now; + _timeoutReminderTimer.Start(); + } + + public void StopTimeoutTimer() + { + _timeoutReminderTimer.Stop(); + _taskStartTime = null; + } + + public void ResetTimeout() + { + _taskStartTime = DateTime.Now; + } + + // 超时计时器回调 + private void TimeoutReminderTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs? e) + { + if (!_taskStartTime.HasValue || _idle) + { + return; + } + + var elapsedMinutes = (DateTime.Now - _taskStartTime.Value).TotalMinutes; + + if (elapsedMinutes > 3 * 60) + { + AchievementTrackerHelper.Instance.Unlock(AchievementIds.ProxyOnline3Hours); + } + + // 如果任务运行时间未超过超时时间,则直接返回 + if (elapsedMinutes <= TaskTimeoutMinutes) + { + return; + } + + // 每隔 ReminderIntervalMinutes 提示一次 + var message = string.Format( + LocalizationHelper.GetString("TaskTimeoutWarning"), + TaskTimeoutMinutes, + Math.Round(elapsedMinutes)); + + AchievementTrackerHelper.Instance.Unlock(AchievementIds.LongTaskTimeout); + + TimeoutOccurred?.Invoke(this, message); + } + + private bool _idle = true; + + public bool Idle + { + get => _idle; + set + { + if (_idle == value) { return; } - // 每隔 ReminderIntervalMinutes 提示一次 - var message = string.Format( - LocalizationHelper.GetString("TaskTimeoutWarning"), - TaskTimeoutMinutes, - Math.Round(elapsedMinutes)); + _idle = value; - AchievementTrackerHelper.Instance.Unlock(AchievementIds.LongTaskTimeout); - - TimeoutOccurred?.Invoke(this, message); - } - - private bool _idle = true; - - public bool Idle - { - get => _idle; - set + if (value) { - if (_idle == value) - { - return; - } + StopTimeoutTimer(); + SleepManagement.AllowSleep(); + } + else + { + StartTimeoutTimer(); + SleepManagement.BlockSleep(); + } - _idle = value; + RaiseStateChanged(); + } + } - if (value) - { - StopTimeoutTimer(); - SleepManagement.AllowSleep(); - } - else - { - StartTimeoutTimer(); - SleepManagement.BlockSleep(); - } + public bool GetIdle() => Idle; + public void SetIdle(bool idle, [CallerMemberName] string caller = "") + { + _logger.Information("Idle: {Old} to {New} (called from {Caller})", Idle, idle, caller); + Idle = idle; + } + + private bool _inited; + + public bool Inited + { + get => _inited; + set + { + if (_inited != value) + { + _inited = value; RaiseStateChanged(); } } + } - public bool GetIdle() => Idle; + public bool GetInit() => Inited; - public void SetIdle(bool idle, [CallerMemberName] string caller = "") + public void SetInit(bool init, [CallerMemberName] string caller = "") + { + _logger.Information("Init: {Old} to {New} (called from {Caller})", Inited, init, caller); + Inited = init; + } + + private bool _stopping; + + public bool Stopping + { + get => _stopping; + set { - _logger.Information("Idle: {Old} to {New} (called from {Caller})", Idle, idle, caller); - Idle = idle; - } - - private bool _inited; - - public bool Inited - { - get => _inited; - set + if (_stopping != value) { - if (_inited != value) - { - _inited = value; - RaiseStateChanged(); - } + _stopping = value; + RaiseStateChanged(); } } + } - public bool GetInit() => Inited; + public bool GetStopping() => Stopping; - public void SetInit(bool init, [CallerMemberName] string caller = "") + public void SetStopping(bool stopping, [CallerMemberName] string caller = "") + { + _logger.Information("Stopping: {Old} to {New} (called from {Caller})", Stopping, stopping, caller); + Stopping = stopping; + } + + public event EventHandler? StateChanged; + + private void RaiseStateChanged() + { + StateChanged?.Invoke(this, new(_idle, _inited, _stopping)); + } + + /// + /// 等待状态变为闲置 + /// + /// 查询间隔(ms) + /// 确认间隔(ms) + /// 确认次数 + /// Task + public async Task UntilIdleAsync(int time = 1000, int confirmInterval = 1000, int confirmTimes = 3) + { + while (true) { - _logger.Information("Init: {Old} to {New} (called from {Caller})", Inited, init, caller); - Inited = init; - } - - private bool _stopping; - - public bool Stopping - { - get => _stopping; - set + while (!GetIdle()) { - if (_stopping != value) + await Task.Delay(time); + } + + int confirmed = 0; + while (confirmed < confirmTimes) + { + await Task.Delay(confirmInterval); + + if (GetIdle()) { - _stopping = value; - RaiseStateChanged(); + confirmed++; + } + else + { + _logger.Information("Idle state changed during confirmation, resetting confirmation count."); + break; } } - } - public bool GetStopping() => Stopping; - - public void SetStopping(bool stopping, [CallerMemberName] string caller = "") - { - _logger.Information("Stopping: {Old} to {New} (called from {Caller})", Stopping, stopping, caller); - Stopping = stopping; - } - - public event EventHandler? StateChanged; - - private void RaiseStateChanged() - { - StateChanged?.Invoke(this, new(_idle, _inited, _stopping)); - } - - /// - /// 等待状态变为闲置 - /// - /// 查询间隔(ms) - /// 确认间隔(ms) - /// 确认次数 - /// Task - public async Task UntilIdleAsync(int time = 1000, int confirmInterval = 1000, int confirmTimes = 3) - { - while (true) + if (confirmed >= confirmTimes) { - while (!GetIdle()) - { - await Task.Delay(time); - } - - int confirmed = 0; - while (confirmed < confirmTimes) - { - await Task.Delay(confirmInterval); - - if (GetIdle()) - { - confirmed++; - } - else - { - _logger.Information("Idle state changed during confirmation, resetting confirmation count."); - break; - } - } - - if (confirmed >= confirmTimes) - { - _logger.Information("Idle state confirmed after {ConfirmTimes} checks.", confirmTimes); - return; - } + _logger.Information("Idle state confirmed after {ConfirmTimes} checks.", confirmTimes); + return; } } } diff --git a/src/MaaWpfGui/Styles/Controls/NoAutomationDataGrid.cs b/src/MaaWpfGui/Styles/Controls/NoAutomationDataGrid.cs index d953cd7f13..34df951f80 100644 --- a/src/MaaWpfGui/Styles/Controls/NoAutomationDataGrid.cs +++ b/src/MaaWpfGui/Styles/Controls/NoAutomationDataGrid.cs @@ -16,57 +16,56 @@ using System.Windows.Automation.Peers; using System.Windows.Controls; using System.Windows.Input; -namespace MaaWpfGui.Styles.Controls +namespace MaaWpfGui.Styles.Controls; + +public class NoAutomationDataGrid : DataGrid { - public class NoAutomationDataGrid : DataGrid + protected override AutomationPeer OnCreateAutomationPeer() { - protected override AutomationPeer OnCreateAutomationPeer() - { - return null; - } + return null; + } - protected override void OnPreviewKeyDown(KeyEventArgs e) - { - base.OnPreviewKeyDown(e); + protected override void OnPreviewKeyDown(KeyEventArgs e) + { + base.OnPreviewKeyDown(e); - // 因为干掉了 AutomationPeer,Tab 键无法正常切换焦点,这里手动实现 Tab 键切换行 - if (e.Key == Key.Tab) + // 因为干掉了 AutomationPeer,Tab 键无法正常切换焦点,这里手动实现 Tab 键切换行 + if (e.Key == Key.Tab) + { + e.Handled = true; + + var items = ItemsSource?.Cast().ToList(); + if (items == null || items.Count == 0 || SelectedItem == null) { - e.Handled = true; - - var items = ItemsSource?.Cast().ToList(); - if (items == null || items.Count == 0 || SelectedItem == null) - { - return; - } - - int currentIndex = items.IndexOf(SelectedItem); - int nextIndex; - - if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift)) - { - // Shift + Tab 往前 - nextIndex = currentIndex - 1; - if (nextIndex < 0) - { - nextIndex = items.Count - 1; - } - } - else - { - // 普通 Tab 往后 - nextIndex = currentIndex + 1; - if (nextIndex >= items.Count) - { - nextIndex = 0; - } - } - - var nextItem = items[nextIndex]; - - SelectedItem = nextItem; - ScrollIntoView(nextItem); + return; } + + int currentIndex = items.IndexOf(SelectedItem); + int nextIndex; + + if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift)) + { + // Shift + Tab 往前 + nextIndex = currentIndex - 1; + if (nextIndex < 0) + { + nextIndex = items.Count - 1; + } + } + else + { + // 普通 Tab 往后 + nextIndex = currentIndex + 1; + if (nextIndex >= items.Count) + { + nextIndex = 0; + } + } + + var nextItem = items[nextIndex]; + + SelectedItem = nextItem; + ScrollIntoView(nextItem); } } } diff --git a/src/MaaWpfGui/Styles/Controls/TextBlock.cs b/src/MaaWpfGui/Styles/Controls/TextBlock.cs index 64c0abb066..0cfdbdc594 100644 --- a/src/MaaWpfGui/Styles/Controls/TextBlock.cs +++ b/src/MaaWpfGui/Styles/Controls/TextBlock.cs @@ -17,91 +17,90 @@ using System.Windows.Documents; using System.Windows.Media; using MaaWpfGui.Helper; -namespace MaaWpfGui.Styles.Controls +namespace MaaWpfGui.Styles.Controls; + +public class TextBlock : System.Windows.Controls.TextBlock { - public class TextBlock : System.Windows.Controls.TextBlock + static TextBlock() { - static TextBlock() + DefaultStyleKeyProperty.OverrideMetadata(typeof(TextBlock), new FrameworkPropertyMetadata(typeof(TextBlock))); + } + + public static readonly DependencyProperty CustomForegroundProperty = DependencyProperty.Register(nameof(CustomForeground), typeof(Brush), typeof(TextBlock), new PropertyMetadata(ThemeHelper.DefaultBrush)); + + public Brush CustomForeground + { + get { return (Brush)GetValue(CustomForegroundProperty); } + set { SetValue(CustomForegroundProperty, value); } + } + + public static readonly DependencyProperty ForegroundKeyProperty = DependencyProperty.Register(nameof(ForegroundKey), typeof(string), typeof(TextBlock), new PropertyMetadata(ThemeHelper.DefaultKey, OnForegroundKeyChanged)); + + private static void OnForegroundKeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var element = (TextBlock)d; + if (e.NewValue != null) { - DefaultStyleKeyProperty.OverrideMetadata(typeof(TextBlock), new FrameworkPropertyMetadata(typeof(TextBlock))); + element.ForegroundKey = (string)e.NewValue; + } + } + + public string ForegroundKey + { + get + { + return (string)GetValue(ForegroundKeyProperty); } - public static readonly DependencyProperty CustomForegroundProperty = DependencyProperty.Register(nameof(CustomForeground), typeof(Brush), typeof(TextBlock), new PropertyMetadata(ThemeHelper.DefaultBrush)); - - public Brush CustomForeground + set { - get { return (Brush)GetValue(CustomForegroundProperty); } - set { SetValue(CustomForegroundProperty, value); } - } - - public static readonly DependencyProperty ForegroundKeyProperty = DependencyProperty.Register(nameof(ForegroundKey), typeof(string), typeof(TextBlock), new PropertyMetadata(ThemeHelper.DefaultKey, OnForegroundKeyChanged)); - - private static void OnForegroundKeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - var element = (TextBlock)d; - if (e.NewValue != null) - { - element.ForegroundKey = (string)e.NewValue; - } - } - - public string ForegroundKey - { - get - { - return (string)GetValue(ForegroundKeyProperty); - } - - set - { - SetValue(ForegroundKeyProperty, value); - if (Application.Current.Resources.Contains(value)) - { - SetResourceReference(ForegroundProperty, value); - return; - } - - var brush = ThemeHelper.String2Brush(value); - if (ThemeHelper.SimilarToBackground(brush.Color)) - { - SetResourceReference(ForegroundProperty, ThemeHelper.DefaultKey); - return; - } - - SetValue(ForegroundProperty, brush); - } - } - - public static readonly DependencyProperty BindableInlinesProperty = - DependencyProperty.RegisterAttached( - "BindableInlines", - typeof(IEnumerable), - typeof(TextBlock), - new PropertyMetadata(null, OnBindableInlinesChanged)); - - public static void SetBindableInlines(DependencyObject element, IEnumerable value) - => element.SetValue(BindableInlinesProperty, value); - - public static IEnumerable GetBindableInlines(DependencyObject element) - => (IEnumerable)element.GetValue(BindableInlinesProperty); - - private static void OnBindableInlinesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is not TextBlock tb) + SetValue(ForegroundKeyProperty, value); + if (Application.Current.Resources.Contains(value)) { + SetResourceReference(ForegroundProperty, value); return; } - tb.Inlines.Clear(); - if (e.NewValue is not IEnumerable inlines) + var brush = ThemeHelper.String2Brush(value); + if (ThemeHelper.SimilarToBackground(brush.Color)) { + SetResourceReference(ForegroundProperty, ThemeHelper.DefaultKey); return; } - foreach (var inline in inlines) - { - tb.Inlines.Add(inline); - } + SetValue(ForegroundProperty, brush); + } + } + + public static readonly DependencyProperty BindableInlinesProperty = + DependencyProperty.RegisterAttached( + "BindableInlines", + typeof(IEnumerable), + typeof(TextBlock), + new PropertyMetadata(null, OnBindableInlinesChanged)); + + public static void SetBindableInlines(DependencyObject element, IEnumerable value) + => element.SetValue(BindableInlinesProperty, value); + + public static IEnumerable GetBindableInlines(DependencyObject element) + => (IEnumerable)element.GetValue(BindableInlinesProperty); + + private static void OnBindableInlinesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not TextBlock tb) + { + return; + } + + tb.Inlines.Clear(); + if (e.NewValue is not IEnumerable inlines) + { + return; + } + + foreach (var inline in inlines) + { + tb.Inlines.Add(inline); } } } diff --git a/src/MaaWpfGui/Styles/Properties/AutoScroll.cs b/src/MaaWpfGui/Styles/Properties/AutoScroll.cs index 50992f3c83..545a995991 100644 --- a/src/MaaWpfGui/Styles/Properties/AutoScroll.cs +++ b/src/MaaWpfGui/Styles/Properties/AutoScroll.cs @@ -17,92 +17,91 @@ using System.Windows; using System.Windows.Controls; using JetBrains.Annotations; -namespace MaaWpfGui.Styles.Properties +namespace MaaWpfGui.Styles.Properties; + +/// +/// The auto scroll property. +/// +public static class AutoScroll { + private static bool _autoScroll; + + /// + /// Gets auto scroll property. + /// + /// The instance. + /// The property value. + [UsedImplicitly] + public static bool GetAutoScroll(DependencyObject obj) + { + return (bool)obj.GetValue(AutoScrollProperty); + } + + /// + /// Sets auto scroll property. + /// + /// The instance. + /// The new property value. + public static void SetAutoScroll(DependencyObject obj, bool value) + { + obj.SetValue(AutoScrollProperty, value); + } + /// /// The auto scroll property. /// - public static class AutoScroll + public static readonly DependencyProperty AutoScrollProperty = + DependencyProperty.RegisterAttached("AutoScroll", typeof(bool), typeof(AutoScroll), new PropertyMetadata(false, AutoScrollPropertyChanged)); + + private static void AutoScrollPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - private static bool _autoScroll; - - /// - /// Gets auto scroll property. - /// - /// The instance. - /// The property value. - [UsedImplicitly] - public static bool GetAutoScroll(DependencyObject obj) + if (d is ScrollViewer scrollViewer) { - return (bool)obj.GetValue(AutoScrollProperty); - } - - /// - /// Sets auto scroll property. - /// - /// The instance. - /// The new property value. - public static void SetAutoScroll(DependencyObject obj, bool value) - { - obj.SetValue(AutoScrollProperty, value); - } - - /// - /// The auto scroll property. - /// - public static readonly DependencyProperty AutoScrollProperty = - DependencyProperty.RegisterAttached("AutoScroll", typeof(bool), typeof(AutoScroll), new PropertyMetadata(false, AutoScrollPropertyChanged)); - - private static void AutoScrollPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is ScrollViewer scrollViewer) + bool alwaysScrollToEnd = (e.NewValue != null) && (bool)e.NewValue; + if (alwaysScrollToEnd) { - bool alwaysScrollToEnd = (e.NewValue != null) && (bool)e.NewValue; - if (alwaysScrollToEnd) - { - scrollViewer.ScrollToEnd(); - scrollViewer.ScrollChanged += ScrollChanged; - } - else - { - scrollViewer.ScrollChanged -= ScrollChanged; - } - } - else if (d is ListBox listBox) - { - INotifyCollectionChanged view = listBox.Items; - view.CollectionChanged += (sender, arg) => - { - switch (arg.Action) - { - case NotifyCollectionChangedAction.Add: - listBox.ScrollIntoView(listBox.Items[arg.NewStartingIndex]); - break; - } - }; + scrollViewer.ScrollToEnd(); + scrollViewer.ScrollChanged += ScrollChanged; } else { - throw new InvalidOperationException("The attached AlwaysScrollToEnd property can only be applied to ScrollViewer instances."); + scrollViewer.ScrollChanged -= ScrollChanged; } } - - private static void ScrollChanged(object sender, ScrollChangedEventArgs e) + else if (d is ListBox listBox) { - if (sender is not ScrollViewer scroll) + INotifyCollectionChanged view = listBox.Items; + view.CollectionChanged += (sender, arg) => { - throw new InvalidOperationException("The attached AlwaysScrollToEnd property can only be applied to ScrollViewer instances."); - } + switch (arg.Action) + { + case NotifyCollectionChangedAction.Add: + listBox.ScrollIntoView(listBox.Items[arg.NewStartingIndex]); + break; + } + }; + } + else + { + throw new InvalidOperationException("The attached AlwaysScrollToEnd property can only be applied to ScrollViewer instances."); + } + } - if (e.ExtentHeightChange == 0) - { - _autoScroll = Math.Abs(scroll.VerticalOffset - scroll.ScrollableHeight) < 1e-6; - } + private static void ScrollChanged(object sender, ScrollChangedEventArgs e) + { + if (sender is not ScrollViewer scroll) + { + throw new InvalidOperationException("The attached AlwaysScrollToEnd property can only be applied to ScrollViewer instances."); + } - if (_autoScroll && e.ExtentHeightChange != 0) - { - scroll.ScrollToVerticalOffset(scroll.ExtentHeight); - } + if (e.ExtentHeightChange == 0) + { + _autoScroll = Math.Abs(scroll.VerticalOffset - scroll.ScrollableHeight) < 1e-6; + } + + if (_autoScroll && e.ExtentHeightChange != 0) + { + scroll.ScrollToVerticalOffset(scroll.ExtentHeight); } } } diff --git a/src/MaaWpfGui/Styles/Properties/ScrollViewerBinding.cs b/src/MaaWpfGui/Styles/Properties/ScrollViewerBinding.cs index 7d2828381e..d759ee4345 100644 --- a/src/MaaWpfGui/Styles/Properties/ScrollViewerBinding.cs +++ b/src/MaaWpfGui/Styles/Properties/ScrollViewerBinding.cs @@ -20,357 +20,356 @@ using System.Windows.Media; using JetBrains.Annotations; using ScrollViewer = System.Windows.Controls.ScrollViewer; -namespace MaaWpfGui.Styles.Properties +namespace MaaWpfGui.Styles.Properties; + +/// +/// The scroll viewer properties. +/// +public static class ScrollViewerBinding { + #region VerticalOffset attached property + /// - /// The scroll viewer properties. + /// VerticalOffset attached property. /// - public static class ScrollViewerBinding + public static readonly DependencyProperty VerticalOffsetProperty = + DependencyProperty.RegisterAttached("VerticalOffset", typeof(double), + typeof(ScrollViewerBinding), new FrameworkPropertyMetadata(double.NaN, + FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + OnVerticalOffsetPropertyChanged)); + + /// + /// Just a flag that the binding has been applied. + /// + private static readonly DependencyProperty _verticalOffsetBindingProperty = + DependencyProperty.RegisterAttached("_verticalOffsetBinding", typeof(bool?), typeof(ScrollViewerBinding)); + + /// + /// Gets vertical offset property. + /// + /// The instance. + /// The property value. + [UsedImplicitly] + public static double GetVerticalOffset(DependencyObject depObj) { - #region VerticalOffset attached property - - /// - /// VerticalOffset attached property. - /// - public static readonly DependencyProperty VerticalOffsetProperty = - DependencyProperty.RegisterAttached("VerticalOffset", typeof(double), - typeof(ScrollViewerBinding), new FrameworkPropertyMetadata(double.NaN, - FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, - OnVerticalOffsetPropertyChanged)); - - /// - /// Just a flag that the binding has been applied. - /// - private static readonly DependencyProperty _verticalOffsetBindingProperty = - DependencyProperty.RegisterAttached("_verticalOffsetBinding", typeof(bool?), typeof(ScrollViewerBinding)); - - /// - /// Gets vertical offset property. - /// - /// The instance. - /// The property value. - [UsedImplicitly] - public static double GetVerticalOffset(DependencyObject depObj) + if (!(depObj is ScrollViewer)) { - if (!(depObj is ScrollViewer)) - { - return 0; - } - - return (double)depObj.GetValue(VerticalOffsetProperty); + return 0; } - /// - /// Sets vertical offset property. - /// - /// The instance. - /// The new property value. - public static void SetVerticalOffset(DependencyObject depObj, double value) - { - if (!(depObj is ScrollViewer)) - { - return; - } - - depObj.SetValue(VerticalOffsetProperty, value); - } - - private static void OnVerticalOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (!(d is ScrollViewer scrollViewer)) - { - return; - } - - BindVerticalOffset(scrollViewer); - scrollViewer.ScrollToVerticalOffset((double)e.NewValue); - } - - private static void BindVerticalOffset(ScrollViewer scrollViewer) - { - if (scrollViewer.GetValue(_verticalOffsetBindingProperty) != null) - { - return; - } - - scrollViewer.SetValue(_verticalOffsetBindingProperty, true); - scrollViewer.ScrollChanged += (s, se) => - { - if (se.VerticalChange == 0) - { - return; - } - - SetVerticalOffset(scrollViewer, se.VerticalOffset); - }; - } - - #endregion VerticalOffset attached property - - #region ViewportHeight attached property - - /// - /// The viewport height property. - /// - public static readonly DependencyProperty ViewportHeightProperty = - DependencyProperty.RegisterAttached("ViewportHeight", typeof(double), - typeof(ScrollViewerBinding), new FrameworkPropertyMetadata(double.NaN, - FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, - OnViewportHeightPropertyChanged)); - - private static readonly DependencyProperty _viewportHeightBindingProperty = - DependencyProperty.RegisterAttached("_viewportHeightBinding", typeof(bool?), typeof(ScrollViewerBinding)); - - /// - /// Gets viewport height property. - /// - /// The instance. - /// The property value. - [UsedImplicitly] - public static double GetViewportHeight(DependencyObject depObj) - { - if (!(depObj is ScrollViewer scrollViewer)) - { - return double.NaN; - } - - return scrollViewer.ViewportHeight; - } - - /// - /// Sets viewport height property. - /// - /// The instance. - /// The new property value. - public static void SetViewportHeight(DependencyObject depObj, double value) - { - if (!(depObj is ScrollViewer)) - { - return; - } - - depObj.SetValue(ViewportHeightProperty, value); - } - - private static void OnViewportHeightPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (!(d is ScrollViewer scrollViewer)) - { - return; - } - - BindViewportHeight(scrollViewer); - } - - private static void BindViewportHeight(ScrollViewer scrollViewer) - { - if (scrollViewer.GetValue(_viewportHeightBindingProperty) != null) - { - return; - } - - scrollViewer.SetValue(_viewportHeightBindingProperty, true); - - scrollViewer.Loaded += (s, se) => - { - SetViewportHeight(scrollViewer, scrollViewer.ViewportHeight); - }; - - scrollViewer.ScrollChanged += (s, se) => - { - SetViewportHeight(scrollViewer, se.ViewportHeight); - }; - } - - #endregion ViewportHeight attached property - - #region ExtentHeight attached property - - /// - /// The extent height property. - /// - public static readonly DependencyProperty ExtentHeightProperty = - DependencyProperty.RegisterAttached("ExtentHeight", typeof(double), - typeof(ScrollViewerBinding), new FrameworkPropertyMetadata(double.NaN, - FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, - OnExtentHeightPropertyChanged)); - - private static readonly DependencyProperty _extentHeightBindingProperty = - DependencyProperty.RegisterAttached("_extentHeightBinding", typeof(bool?), typeof(ScrollViewerBinding)); - - /// - /// Gets extent height property. - /// - /// The instance. - /// The property value. - [UsedImplicitly] - public static double GetExtentHeight(DependencyObject depObj) - { - if (!(depObj is ScrollViewer scrollViewer)) - { - return double.NaN; - } - - return scrollViewer.ExtentHeight; - } - - /// - /// Sets extent height property. - /// - /// The instance. - /// The new property value. - public static void SetExtentHeight(DependencyObject depObj, double value) - { - if (!(depObj is ScrollViewer)) - { - return; - } - - depObj.SetValue(ExtentHeightProperty, value); - } - - private static void OnExtentHeightPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (!(d is ScrollViewer scrollViewer)) - { - return; - } - - BindExtentHeight(scrollViewer); - } - - private static void BindExtentHeight(ScrollViewer scrollViewer) - { - if (scrollViewer.GetValue(_extentHeightBindingProperty) != null) - { - return; - } - - scrollViewer.SetValue(_extentHeightBindingProperty, true); - - scrollViewer.Loaded += (s, se) => - { - SetExtentHeight(scrollViewer, scrollViewer.ExtentHeight); - }; - } - - #endregion ExtentHeight attached property - - #region DividerVerticalOffsetList attached property - - /// - /// DividerIndex attached property. - /// - public static readonly DependencyProperty DividerVerticalOffsetListProperty = - DependencyProperty.RegisterAttached("DividerVerticalOffsetList", - typeof(List), - typeof(ScrollViewerBinding), - new FrameworkPropertyMetadata(new List(), - FrameworkPropertyMetadataOptions.AffectsRender | - FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, - OnDividerVerticalOffsetListPropertyChanged)); - - /// - /// Just a flag that the binding has been applied. - /// - private static readonly DependencyProperty _dividerVerticalOffsetListBindingProperty = - DependencyProperty.RegisterAttached("_dividerVerticalOffsetListBinding", typeof(bool?), typeof(ScrollViewerBinding)); - - /// - /// Gets divider vertical offset property. - /// - /// The instance. - /// The property value. - [UsedImplicitly] - public static List GetDividerVerticalOffsetList(DependencyObject depObj) - { - return (List)depObj.GetValue(DividerVerticalOffsetListProperty); - } - - /// - /// Sets divider vertical offset property. - /// - /// The instance. - /// The new property value. - public static void SetDividerVerticalOffsetList(DependencyObject depObj, List value) - { - if (depObj is not ScrollViewer) - { - return; - } - - depObj.SetValue(DividerVerticalOffsetListProperty, value); - } - - private static void OnDividerVerticalOffsetListPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is not ScrollViewer scrollViewer) - { - return; - } - - BindDividerVerticalOffsetList(scrollViewer); - } - - private static void BindDividerVerticalOffsetList(ScrollViewer scrollViewer) - { - if (scrollViewer.GetValue(_dividerVerticalOffsetListBindingProperty) != null) - { - return; - } - - scrollViewer.SetValue(_dividerVerticalOffsetListBindingProperty, true); - - if (scrollViewer.Content is FrameworkElement content) - { - content.SizeChanged += (s, e) => - { - RefreshDividerOffsets(scrollViewer); - }; - } - } - - public static void RefreshDividerOffsets(ScrollViewer scrollViewer) - { - if (scrollViewer.Content is not Grid rootGrid) - { - return; - } - - var point = new Point(10, scrollViewer.VerticalOffset); - - var dividerOffsetList = ( - from child in rootGrid.Children.OfType() - orderby Grid.GetRow(child) - let divider = FindFirstDivider(child) - where divider != null - let pos = divider.TransformToVisual(scrollViewer).Transform(point) - select pos.Y).ToList(); - - SetDividerVerticalOffsetList(scrollViewer, dividerOffsetList); - } - - private static FrameworkElement FindFirstDivider(DependencyObject root) - { - int count = VisualTreeHelper.GetChildrenCount(root); - for (int i = 0; i < count; i++) - { - var child = VisualTreeHelper.GetChild(root, i); - if (child is HandyControl.Controls.Divider divider) - { - return divider; - } - - var found = FindFirstDivider(child); - if (found != null) - { - return found; - } - } - - return null; - } - - #endregion DividerVerticalOffsetList attached property + return (double)depObj.GetValue(VerticalOffsetProperty); } + + /// + /// Sets vertical offset property. + /// + /// The instance. + /// The new property value. + public static void SetVerticalOffset(DependencyObject depObj, double value) + { + if (!(depObj is ScrollViewer)) + { + return; + } + + depObj.SetValue(VerticalOffsetProperty, value); + } + + private static void OnVerticalOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (!(d is ScrollViewer scrollViewer)) + { + return; + } + + BindVerticalOffset(scrollViewer); + scrollViewer.ScrollToVerticalOffset((double)e.NewValue); + } + + private static void BindVerticalOffset(ScrollViewer scrollViewer) + { + if (scrollViewer.GetValue(_verticalOffsetBindingProperty) != null) + { + return; + } + + scrollViewer.SetValue(_verticalOffsetBindingProperty, true); + scrollViewer.ScrollChanged += (s, se) => + { + if (se.VerticalChange == 0) + { + return; + } + + SetVerticalOffset(scrollViewer, se.VerticalOffset); + }; + } + + #endregion VerticalOffset attached property + + #region ViewportHeight attached property + + /// + /// The viewport height property. + /// + public static readonly DependencyProperty ViewportHeightProperty = + DependencyProperty.RegisterAttached("ViewportHeight", typeof(double), + typeof(ScrollViewerBinding), new FrameworkPropertyMetadata(double.NaN, + FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + OnViewportHeightPropertyChanged)); + + private static readonly DependencyProperty _viewportHeightBindingProperty = + DependencyProperty.RegisterAttached("_viewportHeightBinding", typeof(bool?), typeof(ScrollViewerBinding)); + + /// + /// Gets viewport height property. + /// + /// The instance. + /// The property value. + [UsedImplicitly] + public static double GetViewportHeight(DependencyObject depObj) + { + if (!(depObj is ScrollViewer scrollViewer)) + { + return double.NaN; + } + + return scrollViewer.ViewportHeight; + } + + /// + /// Sets viewport height property. + /// + /// The instance. + /// The new property value. + public static void SetViewportHeight(DependencyObject depObj, double value) + { + if (!(depObj is ScrollViewer)) + { + return; + } + + depObj.SetValue(ViewportHeightProperty, value); + } + + private static void OnViewportHeightPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (!(d is ScrollViewer scrollViewer)) + { + return; + } + + BindViewportHeight(scrollViewer); + } + + private static void BindViewportHeight(ScrollViewer scrollViewer) + { + if (scrollViewer.GetValue(_viewportHeightBindingProperty) != null) + { + return; + } + + scrollViewer.SetValue(_viewportHeightBindingProperty, true); + + scrollViewer.Loaded += (s, se) => + { + SetViewportHeight(scrollViewer, scrollViewer.ViewportHeight); + }; + + scrollViewer.ScrollChanged += (s, se) => + { + SetViewportHeight(scrollViewer, se.ViewportHeight); + }; + } + + #endregion ViewportHeight attached property + + #region ExtentHeight attached property + + /// + /// The extent height property. + /// + public static readonly DependencyProperty ExtentHeightProperty = + DependencyProperty.RegisterAttached("ExtentHeight", typeof(double), + typeof(ScrollViewerBinding), new FrameworkPropertyMetadata(double.NaN, + FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + OnExtentHeightPropertyChanged)); + + private static readonly DependencyProperty _extentHeightBindingProperty = + DependencyProperty.RegisterAttached("_extentHeightBinding", typeof(bool?), typeof(ScrollViewerBinding)); + + /// + /// Gets extent height property. + /// + /// The instance. + /// The property value. + [UsedImplicitly] + public static double GetExtentHeight(DependencyObject depObj) + { + if (!(depObj is ScrollViewer scrollViewer)) + { + return double.NaN; + } + + return scrollViewer.ExtentHeight; + } + + /// + /// Sets extent height property. + /// + /// The instance. + /// The new property value. + public static void SetExtentHeight(DependencyObject depObj, double value) + { + if (!(depObj is ScrollViewer)) + { + return; + } + + depObj.SetValue(ExtentHeightProperty, value); + } + + private static void OnExtentHeightPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (!(d is ScrollViewer scrollViewer)) + { + return; + } + + BindExtentHeight(scrollViewer); + } + + private static void BindExtentHeight(ScrollViewer scrollViewer) + { + if (scrollViewer.GetValue(_extentHeightBindingProperty) != null) + { + return; + } + + scrollViewer.SetValue(_extentHeightBindingProperty, true); + + scrollViewer.Loaded += (s, se) => + { + SetExtentHeight(scrollViewer, scrollViewer.ExtentHeight); + }; + } + + #endregion ExtentHeight attached property + + #region DividerVerticalOffsetList attached property + + /// + /// DividerIndex attached property. + /// + public static readonly DependencyProperty DividerVerticalOffsetListProperty = + DependencyProperty.RegisterAttached("DividerVerticalOffsetList", + typeof(List), + typeof(ScrollViewerBinding), + new FrameworkPropertyMetadata(new List(), + FrameworkPropertyMetadataOptions.AffectsRender | + FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + OnDividerVerticalOffsetListPropertyChanged)); + + /// + /// Just a flag that the binding has been applied. + /// + private static readonly DependencyProperty _dividerVerticalOffsetListBindingProperty = + DependencyProperty.RegisterAttached("_dividerVerticalOffsetListBinding", typeof(bool?), typeof(ScrollViewerBinding)); + + /// + /// Gets divider vertical offset property. + /// + /// The instance. + /// The property value. + [UsedImplicitly] + public static List GetDividerVerticalOffsetList(DependencyObject depObj) + { + return (List)depObj.GetValue(DividerVerticalOffsetListProperty); + } + + /// + /// Sets divider vertical offset property. + /// + /// The instance. + /// The new property value. + public static void SetDividerVerticalOffsetList(DependencyObject depObj, List value) + { + if (depObj is not ScrollViewer) + { + return; + } + + depObj.SetValue(DividerVerticalOffsetListProperty, value); + } + + private static void OnDividerVerticalOffsetListPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not ScrollViewer scrollViewer) + { + return; + } + + BindDividerVerticalOffsetList(scrollViewer); + } + + private static void BindDividerVerticalOffsetList(ScrollViewer scrollViewer) + { + if (scrollViewer.GetValue(_dividerVerticalOffsetListBindingProperty) != null) + { + return; + } + + scrollViewer.SetValue(_dividerVerticalOffsetListBindingProperty, true); + + if (scrollViewer.Content is FrameworkElement content) + { + content.SizeChanged += (s, e) => + { + RefreshDividerOffsets(scrollViewer); + }; + } + } + + public static void RefreshDividerOffsets(ScrollViewer scrollViewer) + { + if (scrollViewer.Content is not Grid rootGrid) + { + return; + } + + var point = new Point(10, scrollViewer.VerticalOffset); + + var dividerOffsetList = ( + from child in rootGrid.Children.OfType() + orderby Grid.GetRow(child) + let divider = FindFirstDivider(child) + where divider != null + let pos = divider.TransformToVisual(scrollViewer).Transform(point) + select pos.Y).ToList(); + + SetDividerVerticalOffsetList(scrollViewer, dividerOffsetList); + } + + private static FrameworkElement FindFirstDivider(DependencyObject root) + { + int count = VisualTreeHelper.GetChildrenCount(root); + for (int i = 0; i < count; i++) + { + var child = VisualTreeHelper.GetChild(root, i); + if (child is HandyControl.Controls.Divider divider) + { + return divider; + } + + var found = FindFirstDivider(child); + if (found != null) + { + return found; + } + } + + return null; + } + + #endregion DividerVerticalOffsetList attached property } diff --git a/src/MaaWpfGui/Utilities/AutoStart.cs b/src/MaaWpfGui/Utilities/AutoStart.cs index 5d0169166d..3364b0f79b 100644 --- a/src/MaaWpfGui/Utilities/AutoStart.cs +++ b/src/MaaWpfGui/Utilities/AutoStart.cs @@ -19,123 +19,122 @@ using MaaWpfGui.Main; using Microsoft.Win32; using Serilog; -namespace MaaWpfGui.Utilities +namespace MaaWpfGui.Utilities; + +/// +/// The model of auto-starting settings. +/// +public static class AutoStart { - /// - /// The model of auto-starting settings. - /// - public static class AutoStart + private static readonly ILogger _logger = Log.ForContext("SourceContext", "AutoStart"); + + private static readonly string _fileValue = Environment.ProcessPath; + private static readonly string _uniqueIdentifier = GetHashCode(_fileValue); + + private static readonly string _startupFolderPath = Environment.GetFolderPath(Environment.SpecialFolder.Startup); + private static readonly string _registryKeyName = $"MAA_{_uniqueIdentifier}"; + private static readonly string _startupShortcutPath = Path.Combine(_startupFolderPath, _registryKeyName + ".lnk"); + + private const string CurrentUserRunKey = @"Software\Microsoft\Windows\CurrentVersion\Run"; + + private static string GetHashCode(string input) { - private static readonly ILogger _logger = Log.ForContext("SourceContext", "AutoStart"); + int hash1 = (5381 << 16) + 5381; + int hash2 = hash1; - private static readonly string _fileValue = Environment.ProcessPath; - private static readonly string _uniqueIdentifier = GetHashCode(_fileValue); - - private static readonly string _startupFolderPath = Environment.GetFolderPath(Environment.SpecialFolder.Startup); - private static readonly string _registryKeyName = $"MAA_{_uniqueIdentifier}"; - private static readonly string _startupShortcutPath = Path.Combine(_startupFolderPath, _registryKeyName + ".lnk"); - - private const string CurrentUserRunKey = @"Software\Microsoft\Windows\CurrentVersion\Run"; - - private static string GetHashCode(string input) + for (int i = 0; i < input.Length; i += 2) { - int hash1 = (5381 << 16) + 5381; - int hash2 = hash1; - - for (int i = 0; i < input.Length; i += 2) + hash1 = ((hash1 << 5) + hash1) ^ input[i]; + if (i == input.Length - 1) { - hash1 = ((hash1 << 5) + hash1) ^ input[i]; - if (i == input.Length - 1) - { - break; - } - - hash2 = ((hash2 << 5) + hash2) ^ input[i + 1]; + break; } - return (hash1 + (hash2 * 1566083941)).ToString("X"); + hash2 = ((hash2 << 5) + hash2) ^ input[i + 1]; } - /// - /// Checks whether this program starts up with OS. - /// - /// The value. - public static bool CheckStart() - { - if (Bootstrapper.IsAdministratorWithUac()) - { - SetStart(false, out _); - return false; - } + return (hash1 + (hash2 * 1566083941)).ToString("X"); + } - try - { - using var key = Registry.CurrentUser.OpenSubKey(CurrentUserRunKey, false); - return key?.GetValue(_registryKeyName) != null; - } - catch (Exception e) - { - _logger.Error("Failed to check startup: " + e.Message); - return false; - } + /// + /// Checks whether this program starts up with OS. + /// + /// The value. + public static bool CheckStart() + { + if (Bootstrapper.IsAdministratorWithUac()) + { + SetStart(false, out _); + return false; } - /// - /// Sets whether this program starts up with OS. - /// - /// The new value. - /// Outputs the error message in case of failure. - /// Whether the operation is successful. - public static bool SetStart(bool set, out string error) + try { - error = string.Empty; + using var key = Registry.CurrentUser.OpenSubKey(CurrentUserRunKey, false); + return key?.GetValue(_registryKeyName) != null; + } + catch (Exception e) + { + _logger.Error("Failed to check startup: " + e.Message); + return false; + } + } - if (set && Bootstrapper.IsAdministratorWithUac()) + /// + /// Sets whether this program starts up with OS. + /// + /// The new value. + /// Outputs the error message in case of failure. + /// Whether the operation is successful. + public static bool SetStart(bool set, out string error) + { + error = string.Empty; + + if (set && Bootstrapper.IsAdministratorWithUac()) + { + error = LocalizationHelper.GetString("LaunchOnSystemStartupAdminPrompt"); + return false; + } + + try + { + using var key = Registry.CurrentUser.OpenSubKey(CurrentUserRunKey, true); + if (key == null) { - error = LocalizationHelper.GetString("LaunchOnSystemStartupAdminPrompt"); - return false; - } - - try - { - using var key = Registry.CurrentUser.OpenSubKey(CurrentUserRunKey, true); - if (key == null) - { - error = "Failed to open registry key."; - _logger.Error("{ErrorMessage}", error); - return false; - } - - if (set) - { - key.SetValue(_registryKeyName, "\"" + _fileValue + "\""); - _logger.Information("Set [{RegistryKeyName}, \"{FileValue}\"] into \"{CurrentUserRunKey}\"", _registryKeyName, _fileValue, CurrentUserRunKey); - } - else - { - key.DeleteValue(_registryKeyName); - } - - return set == CheckStart(); - } - catch (UnauthorizedAccessException uae) - { - error = "Unauthorized access: " + uae.Message; + error = "Failed to open registry key."; _logger.Error("{ErrorMessage}", error); return false; } - catch (SecurityException se) + + if (set) { - error = "Security error: " + se.Message; - _logger.Error("{ErrorMessage}", error); - return false; + key.SetValue(_registryKeyName, "\"" + _fileValue + "\""); + _logger.Information("Set [{RegistryKeyName}, \"{FileValue}\"] into \"{CurrentUserRunKey}\"", _registryKeyName, _fileValue, CurrentUserRunKey); } - catch (Exception e) + else { - error = "Failed to set startup: " + e.Message; - _logger.Error("{ErrorMessage}", error); - return false; + key.DeleteValue(_registryKeyName); } + + return set == CheckStart(); + } + catch (UnauthorizedAccessException uae) + { + error = "Unauthorized access: " + uae.Message; + _logger.Error("{ErrorMessage}", error); + return false; + } + catch (SecurityException se) + { + error = "Security error: " + se.Message; + _logger.Error("{ErrorMessage}", error); + return false; + } + catch (Exception e) + { + error = "Failed to set startup: " + e.Message; + _logger.Error("{ErrorMessage}", error); + return false; } } } diff --git a/src/MaaWpfGui/Utilities/BadModules.cs b/src/MaaWpfGui/Utilities/BadModules.cs index 3004bd9392..25ead26f17 100644 --- a/src/MaaWpfGui/Utilities/BadModules.cs +++ b/src/MaaWpfGui/Utilities/BadModules.cs @@ -16,145 +16,143 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; using MaaWpfGui.Configuration.Factory; using MaaWpfGui.Helper; using Windows.Win32; -namespace MaaWpfGui.Utilities +namespace MaaWpfGui.Utilities; + +internal class BadModules { - internal class BadModules + private static readonly string[] _names = ["NahimicOSD.dll", "AudioDevProps2.dll"]; + + public static unsafe string[] GetBadInjectedModules() { - private static readonly string[] _names = ["NahimicOSD.dll", "AudioDevProps2.dll"]; - - public static unsafe string[] GetBadInjectedModules() + var result = new List(); + char[]? buffer = null; + foreach (var name in _names) { - var result = new List(); - char[]? buffer = null; - foreach (var name in _names) + var hmod = PInvoke.GetModuleHandle(name); + if (hmod.IsInvalid) { - var hmod = PInvoke.GetModuleHandle(name); - if (hmod.IsInvalid) - { - continue; - } - - buffer ??= new char[65536]; - fixed (char* ptr = buffer) - { - if (PInvoke.GetModuleFileName(new(hmod.DangerousGetHandle()), ptr, 65536) > 0) - { - result.Add(new string(ptr)); - } - } + continue; } - return result.ToArray(); - } - - private class WpfWin32Window(System.Windows.Window w) : IWin32Window, System.Windows.Interop.IWin32Window - { - public IntPtr Handle => _helper.Handle; - - private readonly System.Windows.Interop.WindowInteropHelper _helper = new(w); - } - - public static void CheckAndWarnBadInjectedModules() - { - if (System.Windows.Application.Current.MainWindow is null) + buffer ??= new char[65536]; + fixed (char* ptr = buffer) { - return; + if (PInvoke.GetModuleFileName(new(hmod.DangerousGetHandle()), ptr, 65536) > 0) + { + result.Add(new string(ptr)); + } } + } - var allBadModules = GetBadInjectedModules(); - var prevFound = ConfigFactory.Root.GUI.FoundBadModules.Split(";", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - var suppressed = ConfigFactory.Root.GUI.SuppressedBadModules.Split(";", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - var newFoundBadModules = allBadModules.Where(x => !prevFound.Contains(x, StringComparer.InvariantCultureIgnoreCase)).ToArray(); - var notSuppressedBadModules = allBadModules.Where(x => !suppressed.Contains(x, StringComparer.InvariantCultureIgnoreCase)).ToArray(); - ConfigFactory.Root.GUI.FoundBadModules = string.Join(";", [.. prevFound, .. newFoundBadModules]); + return result.ToArray(); + } - if (notSuppressedBadModules.Length <= 0) + private class WpfWin32Window(System.Windows.Window w) : IWin32Window, System.Windows.Interop.IWin32Window + { + public IntPtr Handle => _helper.Handle; + + private readonly System.Windows.Interop.WindowInteropHelper _helper = new(w); + } + + public static void CheckAndWarnBadInjectedModules() + { + if (System.Windows.Application.Current.MainWindow is null) + { + return; + } + + var allBadModules = GetBadInjectedModules(); + var prevFound = ConfigFactory.Root.GUI.FoundBadModules.Split(";", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var suppressed = ConfigFactory.Root.GUI.SuppressedBadModules.Split(";", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var newFoundBadModules = allBadModules.Where(x => !prevFound.Contains(x, StringComparer.InvariantCultureIgnoreCase)).ToArray(); + var notSuppressedBadModules = allBadModules.Where(x => !suppressed.Contains(x, StringComparer.InvariantCultureIgnoreCase)).ToArray(); + ConfigFactory.Root.GUI.FoundBadModules = string.Join(";", [.. prevFound, .. newFoundBadModules]); + + if (notSuppressedBadModules.Length <= 0) + { + return; + } + + var sb = new StringBuilder(); + sb.AppendLine(LocalizationHelper.GetString("BadModules.Warning.Prolog")); + sb.AppendLine(); + foreach (var module in allBadModules) + { + sb.AppendLine(BreakLongPath(module, 70)); + } + + sb.AppendLine(); + sb.Append(LocalizationHelper.GetString("BadModules.Warning.Epilog")); + + var page = new TaskDialogPage + { + Caption = "MAA", + Heading = LocalizationHelper.GetString("BadModules.Warning.Heading"), + Text = sb.ToString(), + Icon = TaskDialogIcon.Warning, + Buttons = { TaskDialogButton.OK }, + SizeToContent = true, + }; + + if (newFoundBadModules.Length == 0) + { + // only show the "Do not show again" checkbox on the second time + page.Verification = new() { - return; + Text = LocalizationHelper.GetString("BadModules.Warning.DoNotShowAgain"), + Checked = false, + }; + } + + TaskDialog.ShowDialog(new WpfWin32Window(System.Windows.Application.Current.MainWindow), page); + + if (page.Verification?.Checked ?? false) + { + ConfigFactory.Root.GUI.SuppressedBadModules = string.Join(";", allBadModules); + } + + return; + + string BreakLongPath(string path, int maxLen) + { + if (path.Length <= maxLen) + { + return path; } var sb = new StringBuilder(); - sb.AppendLine(LocalizationHelper.GetString("BadModules.Warning.Prolog")); - sb.AppendLine(); - foreach (var module in allBadModules) - { - sb.AppendLine(BreakLongPath(module, 70)); - } + int start = 0; - sb.AppendLine(); - sb.Append(LocalizationHelper.GetString("BadModules.Warning.Epilog")); - - var page = new TaskDialogPage + while (start < path.Length) { - Caption = "MAA", - Heading = LocalizationHelper.GetString("BadModules.Warning.Heading"), - Text = sb.ToString(), - Icon = TaskDialogIcon.Warning, - Buttons = { TaskDialogButton.OK }, - SizeToContent = true, - }; - - if (newFoundBadModules.Length == 0) - { - // only show the "Do not show again" checkbox on the second time - page.Verification = new() + if (start + maxLen >= path.Length) { - Text = LocalizationHelper.GetString("BadModules.Warning.DoNotShowAgain"), - Checked = false, - }; - } - - TaskDialog.ShowDialog(new WpfWin32Window(System.Windows.Application.Current.MainWindow), page); - - if (page.Verification?.Checked ?? false) - { - ConfigFactory.Root.GUI.SuppressedBadModules = string.Join(";", allBadModules); - } - - return; - - string BreakLongPath(string path, int maxLen) - { - if (path.Length <= maxLen) - { - return path; + sb.AppendLine(path[start..]); + break; } - var sb = new StringBuilder(); - int start = 0; + int breakPos = path.LastIndexOf('\\', start + maxLen); - while (start < path.Length) + if (breakPos <= start) { - if (start + maxLen >= path.Length) - { - sb.AppendLine(path[start..]); - break; - } - - int breakPos = path.LastIndexOf('\\', start + maxLen); - - if (breakPos <= start) - { - breakPos = Math.Min(start + maxLen, path.Length); - sb.AppendLine(path.Substring(start, breakPos - start)); - start = breakPos; - } - else - { - int length = breakPos - start + 1; - sb.AppendLine(path.Substring(start, length)); - start = breakPos + 1; - } + breakPos = Math.Min(start + maxLen, path.Length); + sb.AppendLine(path.Substring(start, breakPos - start)); + start = breakPos; + } + else + { + int length = breakPos - start + 1; + sb.AppendLine(path.Substring(start, length)); + start = breakPos + 1; } - - return sb.ToString(); } + + return sb.ToString(); } } } diff --git a/src/MaaWpfGui/Utilities/HardwareInfoUtility.cs b/src/MaaWpfGui/Utilities/HardwareInfoUtility.cs index 188f5248a4..b8769bf1ff 100644 --- a/src/MaaWpfGui/Utilities/HardwareInfoUtility.cs +++ b/src/MaaWpfGui/Utilities/HardwareInfoUtility.cs @@ -12,43 +12,39 @@ // #nullable enable -using System; using System.Management; -using System.Security.Cryptography; -using System.Text; using Microsoft.Win32; -namespace MaaWpfGui.Utilities +namespace MaaWpfGui.Utilities; + +public static class HardwareInfoUtility { - public static class HardwareInfoUtility + public static string GetMachineGuid() { - public static string GetMachineGuid() + const string Key = @"SOFTWARE\Microsoft\Cryptography"; + using RegistryKey? rk = Registry.LocalMachine.OpenSubKey(Key); + return rk?.GetValue("MachineGuid")?.ToString() ?? string.Empty; + } + + private static string GetCpuInfo() + { + using var mc = new ManagementClass("Win32_Processor"); + foreach (var mo in mc.GetInstances()) { - const string Key = @"SOFTWARE\Microsoft\Cryptography"; - using RegistryKey? rk = Registry.LocalMachine.OpenSubKey(Key); - return rk?.GetValue("MachineGuid")?.ToString() ?? string.Empty; + return mo["ProcessorId"]?.ToString() ?? string.Empty; } - private static string GetCpuInfo() - { - using var mc = new ManagementClass("Win32_Processor"); - foreach (var mo in mc.GetInstances()) - { - return mo["ProcessorId"]?.ToString() ?? string.Empty; - } + return string.Empty; + } - return string.Empty; + private static string GetDiskSerialNumber() + { + using var mc = new ManagementClass("Win32_DiskDrive"); + foreach (var mo in mc.GetInstances()) + { + return mo["SerialNumber"]?.ToString()?.Trim() ?? string.Empty; } - private static string GetDiskSerialNumber() - { - using var mc = new ManagementClass("Win32_DiskDrive"); - foreach (var mo in mc.GetInstances()) - { - return mo["SerialNumber"]?.ToString()?.Trim() ?? string.Empty; - } - - return string.Empty; - } + return string.Empty; } } diff --git a/src/MaaWpfGui/Utilities/PowerManagement.cs b/src/MaaWpfGui/Utilities/PowerManagement.cs index 43db87aac6..68fe08e67a 100644 --- a/src/MaaWpfGui/Utilities/PowerManagement.cs +++ b/src/MaaWpfGui/Utilities/PowerManagement.cs @@ -16,69 +16,68 @@ using System.Diagnostics; using Serilog; using Windows.Win32; -namespace MaaWpfGui.Utilities +namespace MaaWpfGui.Utilities; + +public class PowerManagement { - public class PowerManagement + private static readonly ILogger _logger = Log.ForContext(); + + /// + /// 关机,使用 shutdown.exe + /// + /// 等待关机时间 + public static void Shutdown(int delaySeconds = 70) { - private static readonly ILogger _logger = Log.ForContext(); - - /// - /// 关机,使用 shutdown.exe - /// - /// 等待关机时间 - public static void Shutdown(int delaySeconds = 70) + try { - try - { - _logger.Information("Scheduling shutdown in {DelaySeconds} seconds.", delaySeconds); - Process.Start("shutdown.exe", $"-s -t {delaySeconds}"); - } - catch (Exception ex) - { - _logger.Error("Shutdown failed: {ExMessage}", ex.Message); - } + _logger.Information("Scheduling shutdown in {DelaySeconds} seconds.", delaySeconds); + Process.Start("shutdown.exe", $"-s -t {delaySeconds}"); } - - /// - /// 取消正在进行的 shutdown.exe - /// - public static void AbortShutdown() + catch (Exception ex) { - try - { - _logger.Information("Aborting shutdown."); - Process.Start("shutdown.exe", "-a"); - } - catch (Exception ex) - { - _logger.Error("Abort shutdown failed: {ExMessage}", ex.Message); - } + _logger.Error("Shutdown failed: {ExMessage}", ex.Message); } + } - public static bool Hibernate() + /// + /// 取消正在进行的 shutdown.exe + /// + public static void AbortShutdown() + { + try { - try - { - return PInvoke.SetSuspendState(true, true, true); - } - catch (Exception ex) - { - _logger.Error("Hibernate error: {ExMessage}", ex.Message); - return false; - } + _logger.Information("Aborting shutdown."); + Process.Start("shutdown.exe", "-a"); } - - public static bool Sleep() + catch (Exception ex) { - try - { - return PInvoke.SetSuspendState(false, true, true); - } - catch (Exception ex) - { - _logger.Error("Sleep error: {ExMessage}", ex.Message); - return false; - } + _logger.Error("Abort shutdown failed: {ExMessage}", ex.Message); + } + } + + public static bool Hibernate() + { + try + { + return PInvoke.SetSuspendState(true, true, true); + } + catch (Exception ex) + { + _logger.Error("Hibernate error: {ExMessage}", ex.Message); + return false; + } + } + + public static bool Sleep() + { + try + { + return PInvoke.SetSuspendState(false, true, true); + } + catch (Exception ex) + { + _logger.Error("Sleep error: {ExMessage}", ex.Message); + return false; } } } diff --git a/src/MaaWpfGui/Utilities/ShellLink.cs b/src/MaaWpfGui/Utilities/ShellLink.cs index cea4135eea..0599e6f800 100644 --- a/src/MaaWpfGui/Utilities/ShellLink.cs +++ b/src/MaaWpfGui/Utilities/ShellLink.cs @@ -14,53 +14,52 @@ using System; using System.Runtime.InteropServices; -namespace MaaWpfGui.Utilities +namespace MaaWpfGui.Utilities; + +[ComImport] +[Guid("00021401-0000-0000-C000-000000000046")] +internal class ShellLink { - [ComImport] - [Guid("00021401-0000-0000-C000-000000000046")] - internal class ShellLink - { - } - - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("000214F9-0000-0000-C000-000000000046")] - internal unsafe interface IShellLink - { - void GetPath(char* pszFile, int cchMaxPath, IntPtr pfd, int fFlags); - - void GetIDList(out IntPtr ppidl); - - void SetIDList(IntPtr pidl); - - void GetDescription(char* pszName, int cchMaxName); - - void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName); - - void GetWorkingDirectory(char* pszDir, int cchMaxPath); - - void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir); - - void GetArguments(char* pszArgs, int cchMaxPath); - - void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs); - - void GetHotkey(out short pwHotkey); - - void SetHotkey(short wHotkey); - - void GetShowCmd(out int piShowCmd); - - void SetShowCmd(int iShowCmd); - - void GetIconLocation(char* pszIconPath, int cchIconPath, out int piIcon); - - void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon); - - void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved); - - void Resolve(IntPtr hwnd, int fFlags); - - void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile); - } +} + +[ComImport] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +[Guid("000214F9-0000-0000-C000-000000000046")] +internal unsafe interface IShellLink +{ + void GetPath(char* pszFile, int cchMaxPath, IntPtr pfd, int fFlags); + + void GetIDList(out IntPtr ppidl); + + void SetIDList(IntPtr pidl); + + void GetDescription(char* pszName, int cchMaxName); + + void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName); + + void GetWorkingDirectory(char* pszDir, int cchMaxPath); + + void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir); + + void GetArguments(char* pszArgs, int cchMaxPath); + + void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs); + + void GetHotkey(out short pwHotkey); + + void SetHotkey(short wHotkey); + + void GetShowCmd(out int piShowCmd); + + void SetShowCmd(int iShowCmd); + + void GetIconLocation(char* pszIconPath, int cchIconPath, out int piIcon); + + void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon); + + void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved); + + void Resolve(IntPtr hwnd, int fFlags); + + void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile); } diff --git a/src/MaaWpfGui/Utilities/SleepManagement.cs b/src/MaaWpfGui/Utilities/SleepManagement.cs index 4a5cc28452..dfbdfe802f 100644 --- a/src/MaaWpfGui/Utilities/SleepManagement.cs +++ b/src/MaaWpfGui/Utilities/SleepManagement.cs @@ -19,71 +19,70 @@ using MaaWpfGui.Constants; using MaaWpfGui.Helper; using Serilog; -namespace MaaWpfGui.Utilities +namespace MaaWpfGui.Utilities; + +public static class SleepManagement { - public static class SleepManagement + [DllImport("kernel32.dll")] + private static extern ExecutionState SetThreadExecutionState(ExecutionState esFlags); + + private static readonly ILogger _logger = Log.ForContext("SourceContext", "SleepManagement"); + + private static bool _allowBlockSleep = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.BlockSleep, bool.FalseString)); + private static bool _blockSleepWithScreenOn = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.BlockSleepWithScreenOn, bool.TrueString)); + private static bool _isBlockingSleep = false; + + public static void SetBlockSleep(bool allowBlockSleep) { - [DllImport("kernel32.dll")] - private static extern ExecutionState SetThreadExecutionState(ExecutionState esFlags); + _allowBlockSleep = allowBlockSleep; + } - private static readonly ILogger _logger = Log.ForContext("SourceContext", "SleepManagement"); + public static void SetBlockSleepWithScreenOn(bool blockSleepWithScreenOn) + { + _blockSleepWithScreenOn = blockSleepWithScreenOn; + } - private static bool _allowBlockSleep = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.BlockSleep, bool.FalseString)); - private static bool _blockSleepWithScreenOn = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.BlockSleepWithScreenOn, bool.TrueString)); - private static bool _isBlockingSleep = false; + [Flags] + private enum ExecutionState : uint + { + SystemRequired = 0x01, + DisplayRequired = 0x02, + Continuous = 0x80000000, + } - public static void SetBlockSleep(bool allowBlockSleep) + public static void AllowSleep() + { + if (!_isBlockingSleep) { - _allowBlockSleep = allowBlockSleep; + return; } - public static void SetBlockSleepWithScreenOn(bool blockSleepWithScreenOn) + _isBlockingSleep = false; + + _logger.Information("Allowing system to sleep"); + SetThreadExecutionState(ExecutionState.Continuous); + } + + public static void BlockSleep(bool? allowBlockSleep = null, bool? blockSleepWithScreenOn = null) + { + if (!(allowBlockSleep ?? _allowBlockSleep)) { - _blockSleepWithScreenOn = blockSleepWithScreenOn; + return; } - [Flags] - private enum ExecutionState : uint - { - SystemRequired = 0x01, - DisplayRequired = 0x02, - Continuous = 0x80000000, - } + _isBlockingSleep = true; - public static void AllowSleep() - { - if (!_isBlockingSleep) - { - return; - } + bool keepDisplayOn = blockSleepWithScreenOn ?? _blockSleepWithScreenOn; + _logger.Information("Blocking system from sleeping"); + ExecutionState state = ExecutionState.Continuous | ExecutionState.SystemRequired | + (keepDisplayOn ? ExecutionState.DisplayRequired : 0); + SetThreadExecutionState(state); + } - _isBlockingSleep = false; - - _logger.Information("Allowing system to sleep"); - SetThreadExecutionState(ExecutionState.Continuous); - } - - public static void BlockSleep(bool? allowBlockSleep = null, bool? blockSleepWithScreenOn = null) - { - if (!(allowBlockSleep ?? _allowBlockSleep)) - { - return; - } - - _isBlockingSleep = true; - - bool keepDisplayOn = blockSleepWithScreenOn ?? _blockSleepWithScreenOn; - _logger.Information("Blocking system from sleeping"); - ExecutionState state = ExecutionState.Continuous | ExecutionState.SystemRequired | - (keepDisplayOn ? ExecutionState.DisplayRequired : 0); - SetThreadExecutionState(state); - } - - public static void ResetIdle(bool keepDisplayOn = true) - { - ExecutionState state = ExecutionState.SystemRequired | - (keepDisplayOn ? ExecutionState.DisplayRequired : 0); - SetThreadExecutionState(state); - } + public static void ResetIdle(bool keepDisplayOn = true) + { + ExecutionState state = ExecutionState.SystemRequired | + (keepDisplayOn ? ExecutionState.DisplayRequired : 0); + SetThreadExecutionState(state); } } diff --git a/src/MaaWpfGui/Utilities/ValueType/CombinedData.cs b/src/MaaWpfGui/Utilities/ValueType/CombinedData.cs index 4ecc5cb6cc..8948f5c429 100644 --- a/src/MaaWpfGui/Utilities/ValueType/CombinedData.cs +++ b/src/MaaWpfGui/Utilities/ValueType/CombinedData.cs @@ -11,14 +11,13 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Utilities.ValueType +namespace MaaWpfGui.Utilities.ValueType; + +/// +/// The with as the value type. +/// +public class CombinedData : GenericCombinedData { - /// - /// The with as the value type. - /// - public class CombinedData : GenericCombinedData - { - /// - public override string ToString() => Display; - } + /// + public override string ToString() => Display; } diff --git a/src/MaaWpfGui/Utilities/ValueType/GenericCombinedData.cs b/src/MaaWpfGui/Utilities/ValueType/GenericCombinedData.cs index 5359120106..1afc3038e2 100644 --- a/src/MaaWpfGui/Utilities/ValueType/GenericCombinedData.cs +++ b/src/MaaWpfGui/Utilities/ValueType/GenericCombinedData.cs @@ -11,37 +11,35 @@ // but WITHOUT ANY WARRANTY // -using System.ComponentModel; using Stylet; -namespace MaaWpfGui.Utilities.ValueType +namespace MaaWpfGui.Utilities.ValueType; + +/// +/// Generic combined data class. +/// +/// The type of value. +public class GenericCombinedData : PropertyChangedBase { + private string _name = string.Empty; + /// - /// Generic combined data class. + /// Gets or sets the name displayed. /// - /// The type of value. - public class GenericCombinedData : PropertyChangedBase + public string Display { - private string _name = string.Empty; + get => _name; + set => SetAndNotify(ref _name, value); + } - /// - /// Gets or sets the name displayed. - /// - public string Display - { - get => _name; - set => SetAndNotify(ref _name, value); - } + private TValueType _value; - private TValueType _value; - - /// - /// Gets or sets the value. - /// - public TValueType Value - { - get => _value; - set => SetAndNotify(ref _value, value); - } + /// + /// Gets or sets the value. + /// + public TValueType Value + { + get => _value; + set => SetAndNotify(ref _value, value); } } diff --git a/src/MaaWpfGui/ViewModels/CopilotItemViewModel.cs b/src/MaaWpfGui/ViewModels/CopilotItemViewModel.cs index e0d2f39390..fbe64c9116 100644 --- a/src/MaaWpfGui/ViewModels/CopilotItemViewModel.cs +++ b/src/MaaWpfGui/ViewModels/CopilotItemViewModel.cs @@ -15,84 +15,83 @@ using MaaWpfGui.Helper; using Newtonsoft.Json; using Stylet; -namespace MaaWpfGui.ViewModels +namespace MaaWpfGui.ViewModels; + +[JsonObject(MemberSerialization.OptIn)] +public class CopilotItemViewModel : PropertyChangedBase { - [JsonObject(MemberSerialization.OptIn)] - public class CopilotItemViewModel : PropertyChangedBase + /// + /// Initializes a new instance of the class. + /// + /// The name + /// The original Name of file + /// 是否为突袭关 + /// 作业站对应 id,本地作业应为默认值 0 + /// isChecked + public CopilotItemViewModel(string name, string filePath, bool isRaid = false, int copilotId = 0, bool isChecked = true) { - /// - /// Initializes a new instance of the class. - /// - /// The name - /// The original Name of file - /// 是否为突袭关 - /// 作业站对应 id,本地作业应为默认值 0 - /// isChecked - public CopilotItemViewModel(string name, string filePath, bool isRaid = false, int copilotId = 0, bool isChecked = true) + Name = name; + FilePath = filePath; + _isRaid = isRaid; + CopilotId = copilotId; + _isChecked = isChecked; + } + + /// + /// Gets the name. + /// + [JsonProperty("name")] + public string Name { get; } + + /// + /// Gets the original_name. + /// + [JsonProperty("file_path")] + public string FilePath { get; } + + /// + /// Gets or sets 作业站对应 id,本地作业应为默认值 0 + /// + [JsonProperty("copilot_id")] + public int CopilotId { get; set; } + + [JsonProperty("is_raid")] + private bool _isRaid; + + /// + /// Gets or sets a value indicating whether 突袭关 + /// + public bool IsRaid + { + get => _isRaid; + set { - Name = name; - FilePath = filePath; - _isRaid = isRaid; - CopilotId = copilotId; - _isChecked = isChecked; - } - - /// - /// Gets the name. - /// - [JsonProperty("name")] - public string Name { get; } - - /// - /// Gets the original_name. - /// - [JsonProperty("file_path")] - public string FilePath { get; } - - /// - /// Gets or sets 作业站对应 id,本地作业应为默认值 0 - /// - [JsonProperty("copilot_id")] - public int CopilotId { get; set; } - - [JsonProperty("is_raid")] - private bool _isRaid; - - /// - /// Gets or sets a value indicating whether 突袭关 - /// - public bool IsRaid - { - get => _isRaid; - set - { - SetAndNotify(ref _isRaid, value); - Instances.CopilotViewModel.SaveCopilotTask(); - } - } - - [JsonProperty("is_checked")] - private bool _isChecked; - - /// - /// Gets or sets a value indicating whether the key is checked. - /// - public bool IsChecked - { - get => _isChecked; - set - { - SetAndNotify(ref _isChecked, value); - Instances.CopilotViewModel.SaveCopilotTask(); - } - } - - private int _index; - - public int Index - { - get => _index; - set => SetAndNotify(ref _index, value); + SetAndNotify(ref _isRaid, value); + Instances.CopilotViewModel.SaveCopilotTask(); } } + + [JsonProperty("is_checked")] + private bool _isChecked; + + /// + /// Gets or sets a value indicating whether the key is checked. + /// + public bool IsChecked + { + get => _isChecked; + set + { + SetAndNotify(ref _isChecked, value); + Instances.CopilotViewModel.SaveCopilotTask(); + } + } + + private int _index; + + public int Index + { + get => _index; + set => SetAndNotify(ref _index, value); + } } diff --git a/src/MaaWpfGui/ViewModels/DragItemViewModel.cs b/src/MaaWpfGui/ViewModels/DragItemViewModel.cs index 756df48ce2..d4b9402143 100644 --- a/src/MaaWpfGui/ViewModels/DragItemViewModel.cs +++ b/src/MaaWpfGui/ViewModels/DragItemViewModel.cs @@ -17,141 +17,140 @@ using MaaWpfGui.Helper; using MaaWpfGui.Models; using Stylet; -namespace MaaWpfGui.ViewModels +namespace MaaWpfGui.ViewModels; + +/// +/// The view model of drag item. +/// +public class DragItemViewModel : PropertyChangedBase { + private readonly string _storageKey; + /// - /// The view model of drag item. + /// Initializes a new instance of the class. /// - public class DragItemViewModel : PropertyChangedBase + /// The name. + /// The storage key. + /// The default checked status. + public DragItemViewModel(string name, string storageKey, bool defaultCheck = true) { - private readonly string _storageKey; + Name = name; + OriginalName = name; + _storageKey = storageKey; + IsChecked = Convert.ToBoolean(ConfigurationHelper.GetCheckedStorage(storageKey, name, defaultCheck.ToString())); + } - /// - /// Initializes a new instance of the class. - /// - /// The name. - /// The storage key. - /// The default checked status. - public DragItemViewModel(string name, string storageKey, bool defaultCheck = true) + /// + /// Initializes a new instance of the class. + /// + /// The name (viewed name). + /// The original name (may not be the same as viewed name). + /// The storage key. + /// The default checked status. + public DragItemViewModel(string name, string originalName, string storageKey, bool defaultCheck = true) + { + Name = name; + OriginalName = originalName; + _storageKey = storageKey; + IsChecked = Convert.ToBoolean(ConfigurationHelper.GetCheckedStorage(storageKey, originalName, defaultCheck.ToString())); + } + + private string _originalName; + + /// + /// Gets or sets the original_name. + /// + public string OriginalName + { + get => _originalName; + set => SetAndNotify(ref _originalName, value); + } + + private string _name; + + /// + /// Gets or sets the name. + /// + public string Name + { + get => _name; + set => SetAndNotify(ref _name, value); + } + + private bool? _isCheckedWithNull; + + /// + /// Gets or sets a value indicating whether the key is checked with null. + /// + public bool? IsCheckedWithNull + { + get => _isCheckedWithNull; + set { - Name = name; - OriginalName = name; - _storageKey = storageKey; - IsChecked = Convert.ToBoolean(ConfigurationHelper.GetCheckedStorage(storageKey, name, defaultCheck.ToString())); + SetAndNotify(ref _isCheckedWithNull, value); + value ??= false; + ConfigurationHelper.SetCheckedStorage(_storageKey, OriginalName, value.ToString()); } + } - /// - /// Initializes a new instance of the class. - /// - /// The name (viewed name). - /// The original name (may not be the same as viewed name). - /// The storage key. - /// The default checked status. - public DragItemViewModel(string name, string originalName, string storageKey, bool defaultCheck = true) + /// + /// Gets or sets a value indicating whether the key is checked. + /// + public bool IsChecked + { + get => IsCheckedWithNull != false; + set => IsCheckedWithNull = value; + } + + // 换成图标的话要这个,暂时没用 + private string _iconPath; + + /// + /// Gets or sets the icon path. + /// + [UsedImplicitly] + public string IconPath + { + get => _iconPath; + set => SetAndNotify(ref _iconPath, value); + } + + private string _token; + + /// + /// Gets or sets the token. + /// + [UsedImplicitly] + public string Token + { + get => _token; + set => SetAndNotify(ref _token, value); + } + + private string _runStatus; + + /// + /// Gets or sets the running status. + /// + [UsedImplicitly] + public string RunStatus + { + get => _runStatus; + set => SetAndNotify(ref _runStatus, value); + } + + private bool _enableSetting; + + /// + /// Gets or sets a value indicating whether the setting enabled. + /// + public bool EnableSetting + { + get => _enableSetting; + set { - Name = name; - OriginalName = originalName; - _storageKey = storageKey; - IsChecked = Convert.ToBoolean(ConfigurationHelper.GetCheckedStorage(storageKey, originalName, defaultCheck.ToString())); - } - - private string _originalName; - - /// - /// Gets or sets the original_name. - /// - public string OriginalName - { - get => _originalName; - set => SetAndNotify(ref _originalName, value); - } - - private string _name; - - /// - /// Gets or sets the name. - /// - public string Name - { - get => _name; - set => SetAndNotify(ref _name, value); - } - - private bool? _isCheckedWithNull; - - /// - /// Gets or sets a value indicating whether the key is checked with null. - /// - public bool? IsCheckedWithNull - { - get => _isCheckedWithNull; - set - { - SetAndNotify(ref _isCheckedWithNull, value); - value ??= false; - ConfigurationHelper.SetCheckedStorage(_storageKey, OriginalName, value.ToString()); - } - } - - /// - /// Gets or sets a value indicating whether the key is checked. - /// - public bool IsChecked - { - get => IsCheckedWithNull != false; - set => IsCheckedWithNull = value; - } - - // 换成图标的话要这个,暂时没用 - private string _iconPath; - - /// - /// Gets or sets the icon path. - /// - [UsedImplicitly] - public string IconPath - { - get => _iconPath; - set => SetAndNotify(ref _iconPath, value); - } - - private string _token; - - /// - /// Gets or sets the token. - /// - [UsedImplicitly] - public string Token - { - get => _token; - set => SetAndNotify(ref _token, value); - } - - private string _runStatus; - - /// - /// Gets or sets the running status. - /// - [UsedImplicitly] - public string RunStatus - { - get => _runStatus; - set => SetAndNotify(ref _runStatus, value); - } - - private bool _enableSetting; - - /// - /// Gets or sets a value indicating whether the setting enabled. - /// - public bool EnableSetting - { - get => _enableSetting; - set - { - SetAndNotify(ref _enableSetting, value); - TaskSettingVisibilityInfo.Instance.Set(OriginalName, value); - } + SetAndNotify(ref _enableSetting, value); + TaskSettingVisibilityInfo.Instance.Set(OriginalName, value); } } } diff --git a/src/MaaWpfGui/ViewModels/LogItemViewModel.cs b/src/MaaWpfGui/ViewModels/LogItemViewModel.cs index f0484803c0..28ef23396a 100644 --- a/src/MaaWpfGui/ViewModels/LogItemViewModel.cs +++ b/src/MaaWpfGui/ViewModels/LogItemViewModel.cs @@ -13,114 +13,106 @@ #nullable enable using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows; using System.Windows.Controls; -using System.Windows.Controls.Primitives; -using System.Windows.Documents; -using System.Windows.Media; using MaaWpfGui.Constants; -using MaaWpfGui.Helper; using MaaWpfGui.Utilities; using MaaWpfGui.ViewModels.UI; using Stylet; -namespace MaaWpfGui.ViewModels +namespace MaaWpfGui.ViewModels; + +/// +/// The view model of log item. +/// +public class LogItemViewModel : PropertyChangedBase { /// - /// The view model of log item. + /// Initializes a new instance of the class. /// - public class LogItemViewModel : PropertyChangedBase + /// The content. + /// The font color. + /// The font weight. + /// The Date format string + /// The showtime bool. + /// The toolTip + public LogItemViewModel(string content, string color = UiLogColor.Message, string weight = "Regular", string dateFormat = "", bool showTime = true, ToolTip? toolTip = null) { - /// - /// Initializes a new instance of the class. - /// - /// The content. - /// The font color. - /// The font weight. - /// The Date format string - /// The showtime bool. - /// The toolTip - public LogItemViewModel(string content, string color = UiLogColor.Message, string weight = "Regular", string dateFormat = "", bool showTime = true, ToolTip? toolTip = null) + if (string.IsNullOrEmpty(dateFormat)) { - if (string.IsNullOrEmpty(dateFormat)) - { - dateFormat = SettingsViewModel.GuiSettings.LogItemDateFormatString; - } - - _time = DateTime.Now.ToString(dateFormat); - _content = content; - _color = color; - _weight = weight; - _showTime = showTime; - _toolTip = toolTip; + dateFormat = SettingsViewModel.GuiSettings.LogItemDateFormatString; } - private string _time; + _time = DateTime.Now.ToString(dateFormat); + _content = content; + _color = color; + _weight = weight; + _showTime = showTime; + _toolTip = toolTip; + } - /// - /// Gets or sets the time. - /// - public string Time - { - get => _time; - set => SetAndNotify(ref _time, value); - } + private string _time; - private bool _showTime; + /// + /// Gets or sets the time. + /// + public string Time + { + get => _time; + set => SetAndNotify(ref _time, value); + } - public bool ShowTime - { - get => _showTime; - set => SetAndNotify(ref _showTime, value); - } + private bool _showTime; - private string _content; + public bool ShowTime + { + get => _showTime; + set => SetAndNotify(ref _showTime, value); + } - /// - /// Gets or sets the content. - /// - public string Content - { - get => _content; - set => SetAndNotify(ref _content, value); - } + private string _content; - private string _color; + /// + /// Gets or sets the content. + /// + public string Content + { + get => _content; + set => SetAndNotify(ref _content, value); + } - /// - /// Gets or sets the font color. - /// - public string Color - { - get => _color; - set => SetAndNotify(ref _color, value); - } + private string _color; - private string _weight; + /// + /// Gets or sets the font color. + /// + public string Color + { + get => _color; + set => SetAndNotify(ref _color, value); + } - /// - /// Gets or sets the font weight. - /// - public string Weight - { - get => _weight; - set => SetAndNotify(ref _weight, value); - } + private string _weight; - [PropertyDependsOn(nameof(ToolTip))] - public bool ShowToolTip => _toolTip is { Content: not null }; + /// + /// Gets or sets the font weight. + /// + public string Weight + { + get => _weight; + set => SetAndNotify(ref _weight, value); + } - private ToolTip? _toolTip; + [PropertyDependsOn(nameof(ToolTip))] + public bool ShowToolTip => _toolTip is { Content: not null }; - /// - /// Gets or sets the toolTip. - /// - public ToolTip? ToolTip - { - get => _toolTip; - set => SetAndNotify(ref _toolTip, value); - } + private ToolTip? _toolTip; + + /// + /// Gets or sets the toolTip. + /// + public ToolTip? ToolTip + { + get => _toolTip; + set => SetAndNotify(ref _toolTip, value); } } diff --git a/src/MaaWpfGui/ViewModels/SettingItemViewModel.cs b/src/MaaWpfGui/ViewModels/SettingItemViewModel.cs index 35231015e5..1af3af15eb 100644 --- a/src/MaaWpfGui/ViewModels/SettingItemViewModel.cs +++ b/src/MaaWpfGui/ViewModels/SettingItemViewModel.cs @@ -13,35 +13,34 @@ using Stylet; -namespace MaaWpfGui.ViewModels +namespace MaaWpfGui.ViewModels; + +/// +/// The view model of setting item. +/// +public class SettingItemViewModel(string key, string display, int value) : PropertyChangedBase { - /// - /// The view model of setting item. - /// - public class SettingItemViewModel(string key, string display, int value) : PropertyChangedBase + private string _key = key; + + public string Key { - private string _key = key; + get => _key; + set => SetAndNotify(ref _key, value); + } - public string Key - { - get => _key; - set => SetAndNotify(ref _key, value); - } + private string _display = display; - private string _display = display; + public string Display + { + get => _display; + set => SetAndNotify(ref _display, value); + } - public string Display - { - get => _display; - set => SetAndNotify(ref _display, value); - } + private int _value = value; - private int _value = value; - - public int Value - { - get => _value; - set => SetAndNotify(ref _value, value); - } + public int Value + { + get => _value; + set => SetAndNotify(ref _value, value); } } diff --git a/src/MaaWpfGui/ViewModels/UI/AnnouncementViewModel.cs b/src/MaaWpfGui/ViewModels/UI/AnnouncementViewModel.cs index 598b4bf1f0..02bd9d3601 100644 --- a/src/MaaWpfGui/ViewModels/UI/AnnouncementViewModel.cs +++ b/src/MaaWpfGui/ViewModels/UI/AnnouncementViewModel.cs @@ -25,255 +25,254 @@ using MaaWpfGui.Helper; using Serilog; using Stylet; -namespace MaaWpfGui.ViewModels.UI +namespace MaaWpfGui.ViewModels.UI; + +/// +/// The view model of version update. +/// +// 通过 container.Get(); 实例化或获取实例 +// ReSharper disable once ClassNeverInstantiated.Global +public class AnnouncementViewModel : Screen { - /// - /// The view model of version update. - /// - // 通过 container.Get(); 实例化或获取实例 - // ReSharper disable once ClassNeverInstantiated.Global - public class AnnouncementViewModel : Screen + private static readonly ILogger _logger = Log.ForContext(); + + private static readonly object _lock = new(); + + public string ImageSource { get; set; } + + public class AnnouncementSection { - private static readonly ILogger _logger = Log.ForContext(); + public string Title { get; set; } - private static readonly object _lock = new(); + public string Content { get; set; } + } - public string ImageSource { get; set; } + public AnnouncementViewModel() + { + UpdateImageSource(); - public class AnnouncementSection + UpdateScrollStateCommand = new RelayCommand(scrollViewer => { - public string Title { get; set; } - - public string Content { get; set; } - } - - public AnnouncementViewModel() - { - UpdateImageSource(); - - UpdateScrollStateCommand = new RelayCommand(scrollViewer => - { - if (scrollViewer == null) - { - return; - } - - // 计算是否滚动到底部 - IsScrolledToBottom |= scrollViewer.VerticalOffset >= scrollViewer.ScrollableHeight - 10; - }); - - ScrollToTopCommand = new RelayCommand(scrollViewer => - { - if (scrollViewer == null) - { - return; - } - - scrollViewer.ScrollToTop(); - }); - AnnouncementSections = [.. ParseAnnouncementInfo(AnnouncementInfo)]; - } - - private void UpdateImageSource() - { - ImageSource = SettingsViewModel.GuiSettings.Language switch - { - "zh-cn" or "zh-tw" => "/Res/Img/NoSkland.jpg", - _ => "/Res/Img/NoSkLandEn.jpg", - }; - } - - private static ObservableCollection ParseAnnouncementInfo(string markdown) - { - var sections = markdown.Split(["### "], StringSplitOptions.RemoveEmptyEntries) - .Select(section => - { - var lines = section.Split('\n'); - return new AnnouncementSection - { - Title = lines.FirstOrDefault(), - Content = "### " + string.Join("\n", lines).Trim([' ', '\n', '-']), - }; - }).ToList(); - - sections.Insert(0, new() - { - Title = "ALL~ the Announcements", - Content = markdown, - }); - - return [.. sections]; - } - - public ICommand UpdateScrollStateCommand { get; } - - public ICommand ScrollToTopCommand { get; } - - private bool _isScrolledToBottom; - - public bool IsScrolledToBottom - { - get => _isScrolledToBottom; - set => SetAndNotify(ref _isScrolledToBottom, value); - } - - private ObservableCollection _announcementSections; - - public ObservableCollection AnnouncementSections - { - get => _announcementSections; - set - { - SetAndNotify(ref _announcementSections, value); - SelectedAnnouncementSection = AnnouncementSections.FirstOrDefault(); - } - } - - private AnnouncementSection _selectedAnnouncementSection; - - public AnnouncementSection SelectedAnnouncementSection - { - get => _selectedAnnouncementSection; - set => SetAndNotify(ref _selectedAnnouncementSection, value); - } - - private static readonly string _announcementInFile = SettingsViewModel.GuiSettings.Language switch - { - "zh-cn" or "zh-tw" => Path.Combine(PathsHelper.CacheDir, "announcement.md"), - _ => Path.Combine(PathsHelper.CacheDir, "announcement_en.md"), - }; - - private static string AnnouncementInFile - { - get - { - if (!File.Exists(_announcementInFile)) - { - return null; - } - - try - { - lock (_lock) - { - return File.ReadAllText(_announcementInFile); - } - } - catch (Exception e) - { - _logger.Error(e, "Failed to read announcement from file"); - } - - return null; - } - - set - { - try - { - lock (_lock) - { - File.WriteAllText(_announcementInFile, value); - } - } - catch (Exception e) - { - _logger.Error(e, "Failed to write announcement to file"); - } - } - } - - private string _announcementInfo = AnnouncementInFile ?? string.Empty; - - /// - /// Gets the announcement info. - /// - // ReSharper disable once MemberCanBePrivate.Global - public string AnnouncementInfo - { - get => _announcementInfo; - private set - { - SetAndNotify(ref _announcementInfo, value); - AnnouncementInFile = value; - AnnouncementSections = [.. ParseAnnouncementInfo(AnnouncementInfo)]; - } - } - - private bool _doNotRemindThisAnnouncementAgain = ConfigFactory.Root.AnnouncementInfo.DoNotShowAgain; - - public bool DoNotRemindThisAnnouncementAgain - { - get => _doNotRemindThisAnnouncementAgain; - set - { - SetAndNotify(ref _doNotRemindThisAnnouncementAgain, value); - ConfigFactory.Root.AnnouncementInfo.DoNotShowAgain = value; - } - } - - private bool _doNotShowAnnouncement = ConfigFactory.Root.AnnouncementInfo.DoNotShow; - - /// - /// Gets or sets a value indicating whether to show the update. - /// - public bool DoNotShowAnnouncement - { - get => _doNotShowAnnouncement; - set - { - SetAndNotify(ref _doNotShowAnnouncement, value); - ConfigFactory.Root.AnnouncementInfo.DoNotShow = value; - } - } - - /// - /// 检查更新 - /// - /// A representing the asynchronous operation. - public async Task CheckAndDownloadAnnouncement() - { - string path = "announcements/wpf"; - if (SettingsViewModel.GuiSettings.Language is not ("zh-cn" or "zh-tw")) - { - path += "_en"; - } - - string url = MaaUrls.MaaApi + path + ".md"; - - using var response = await ETagCache.FetchResponseWithEtag(url, string.IsNullOrEmpty(AnnouncementInfo)); - - if (response == null || - response.StatusCode == System.Net.HttpStatusCode.NotModified || - response.StatusCode != System.Net.HttpStatusCode.OK) + if (scrollViewer == null) { return; } - var body = await HttpResponseHelper.GetStringAsync(response); - if (!string.IsNullOrEmpty(body) && AnnouncementInfo != body) + // 计算是否滚动到底部 + IsScrolledToBottom |= scrollViewer.VerticalOffset >= scrollViewer.ScrollableHeight - 10; + }); + + ScrollToTopCommand = new RelayCommand(scrollViewer => + { + if (scrollViewer == null) { - const string Template = - "----------- OLD -----------\n" + - "{AnnouncementInfo}\n" + - "---------------------------\n\n" + - "=========== NEW ===========\n" + - "{Body}\n" + - "==========================="; - _logger.Information(Template, - AnnouncementInfo, - body); - AnnouncementInfo = body; - DoNotRemindThisAnnouncementAgain = false; + return; } - ETagCache.Set(response, url); - ETagCache.Save(); - } + scrollViewer.ScrollToTop(); + }); + AnnouncementSections = [.. ParseAnnouncementInfo(AnnouncementInfo)]; + } - public void Close() + private void UpdateImageSource() + { + ImageSource = SettingsViewModel.GuiSettings.Language switch { - RequestClose(); + "zh-cn" or "zh-tw" => "/Res/Img/NoSkland.jpg", + _ => "/Res/Img/NoSkLandEn.jpg", + }; + } + + private static ObservableCollection ParseAnnouncementInfo(string markdown) + { + var sections = markdown.Split(["### "], StringSplitOptions.RemoveEmptyEntries) + .Select(section => + { + var lines = section.Split('\n'); + return new AnnouncementSection + { + Title = lines.FirstOrDefault(), + Content = "### " + string.Join("\n", lines).Trim([' ', '\n', '-']), + }; + }).ToList(); + + sections.Insert(0, new() + { + Title = "ALL~ the Announcements", + Content = markdown, + }); + + return [.. sections]; + } + + public ICommand UpdateScrollStateCommand { get; } + + public ICommand ScrollToTopCommand { get; } + + private bool _isScrolledToBottom; + + public bool IsScrolledToBottom + { + get => _isScrolledToBottom; + set => SetAndNotify(ref _isScrolledToBottom, value); + } + + private ObservableCollection _announcementSections; + + public ObservableCollection AnnouncementSections + { + get => _announcementSections; + set + { + SetAndNotify(ref _announcementSections, value); + SelectedAnnouncementSection = AnnouncementSections.FirstOrDefault(); } } + + private AnnouncementSection _selectedAnnouncementSection; + + public AnnouncementSection SelectedAnnouncementSection + { + get => _selectedAnnouncementSection; + set => SetAndNotify(ref _selectedAnnouncementSection, value); + } + + private static readonly string _announcementInFile = SettingsViewModel.GuiSettings.Language switch + { + "zh-cn" or "zh-tw" => Path.Combine(PathsHelper.CacheDir, "announcement.md"), + _ => Path.Combine(PathsHelper.CacheDir, "announcement_en.md"), + }; + + private static string AnnouncementInFile + { + get + { + if (!File.Exists(_announcementInFile)) + { + return null; + } + + try + { + lock (_lock) + { + return File.ReadAllText(_announcementInFile); + } + } + catch (Exception e) + { + _logger.Error(e, "Failed to read announcement from file"); + } + + return null; + } + + set + { + try + { + lock (_lock) + { + File.WriteAllText(_announcementInFile, value); + } + } + catch (Exception e) + { + _logger.Error(e, "Failed to write announcement to file"); + } + } + } + + private string _announcementInfo = AnnouncementInFile ?? string.Empty; + + /// + /// Gets the announcement info. + /// + // ReSharper disable once MemberCanBePrivate.Global + public string AnnouncementInfo + { + get => _announcementInfo; + private set + { + SetAndNotify(ref _announcementInfo, value); + AnnouncementInFile = value; + AnnouncementSections = [.. ParseAnnouncementInfo(AnnouncementInfo)]; + } + } + + private bool _doNotRemindThisAnnouncementAgain = ConfigFactory.Root.AnnouncementInfo.DoNotShowAgain; + + public bool DoNotRemindThisAnnouncementAgain + { + get => _doNotRemindThisAnnouncementAgain; + set + { + SetAndNotify(ref _doNotRemindThisAnnouncementAgain, value); + ConfigFactory.Root.AnnouncementInfo.DoNotShowAgain = value; + } + } + + private bool _doNotShowAnnouncement = ConfigFactory.Root.AnnouncementInfo.DoNotShow; + + /// + /// Gets or sets a value indicating whether to show the update. + /// + public bool DoNotShowAnnouncement + { + get => _doNotShowAnnouncement; + set + { + SetAndNotify(ref _doNotShowAnnouncement, value); + ConfigFactory.Root.AnnouncementInfo.DoNotShow = value; + } + } + + /// + /// 检查更新 + /// + /// A representing the asynchronous operation. + public async Task CheckAndDownloadAnnouncement() + { + string path = "announcements/wpf"; + if (SettingsViewModel.GuiSettings.Language is not ("zh-cn" or "zh-tw")) + { + path += "_en"; + } + + string url = MaaUrls.MaaApi + path + ".md"; + + using var response = await ETagCache.FetchResponseWithEtag(url, string.IsNullOrEmpty(AnnouncementInfo)); + + if (response == null || + response.StatusCode == System.Net.HttpStatusCode.NotModified || + response.StatusCode != System.Net.HttpStatusCode.OK) + { + return; + } + + var body = await HttpResponseHelper.GetStringAsync(response); + if (!string.IsNullOrEmpty(body) && AnnouncementInfo != body) + { + const string Template = + "----------- OLD -----------\n" + + "{AnnouncementInfo}\n" + + "---------------------------\n\n" + + "=========== NEW ===========\n" + + "{Body}\n" + + "==========================="; + _logger.Information(Template, + AnnouncementInfo, + body); + AnnouncementInfo = body; + DoNotRemindThisAnnouncementAgain = false; + } + + ETagCache.Set(response, url); + ETagCache.Save(); + } + + public void Close() + { + RequestClose(); + } } diff --git a/src/MaaWpfGui/ViewModels/UI/CopilotViewModel.cs b/src/MaaWpfGui/ViewModels/UI/CopilotViewModel.cs index e426d99153..1561c433c0 100644 --- a/src/MaaWpfGui/ViewModels/UI/CopilotViewModel.cs +++ b/src/MaaWpfGui/ViewModels/UI/CopilotViewModel.cs @@ -44,1516 +44,1515 @@ using static MaaWpfGui.Models.AsstTasks.AsstCopilotTask; using DataFormats = System.Windows.Forms.DataFormats; using Task = System.Threading.Tasks.Task; -namespace MaaWpfGui.ViewModels.UI +namespace MaaWpfGui.ViewModels.UI; + +/// +/// The view model of copilot. +/// +// 通过 container.Get(); 实例化或获取实例 +// ReSharper disable once ClassNeverInstantiated.Global +public partial class CopilotViewModel : Screen { + private readonly RunningState _runningState; + private static readonly ILogger _logger = Log.ForContext(); + private static readonly SemaphoreSlim _semaphore = new(1, 1); + private readonly List _copilotIdList = []; // 用于保存作业列表中的作业的Id,对于同一个作业,只有都执行成功才点赞 + private readonly List _recentlyRatedCopilotId = []; // TODO: 可能考虑加个持久化 + private AsstTaskType _taskType = AsstTaskType.Copilot; + /// - /// The view model of copilot. + /// 缓存的已解析作业,非即时添加的作业会使用该缓存 /// - // 通过 container.Get(); 实例化或获取实例 - // ReSharper disable once ClassNeverInstantiated.Global - public partial class CopilotViewModel : Screen + private CopilotBase? _copilotCache; + private const string CopilotIdPrefix = "maa://"; + private static readonly string TempCopilotFile = Path.Combine(CacheDir, "_temp_copilot.json"); + private static readonly string[] _supportExt = [".json", ".mp4", ".m4s", ".mkv", ".flv", ".avi"]; + private static readonly string CopilotJsonDir = Path.Combine(ConfigDir, "copilot"); + private const string StageNameRegex = @"(?:[a-z]{0,3})(?:\d{0,2})-(?:(?:A|B|C|D|EX|S|TR|MO)-?)?(?:\d{1,2})"; + private const string InvalidStageNameChars = @"[:',\.\(\)\|\[\]\?,。【】{};:]"; // 无效字符 + + [GeneratedRegex(InvalidStageNameChars)] + private static partial Regex InvalidStageNameRegex(); + + /// + /// Gets the view models of log items. + /// + public ObservableCollection LogItemViewModels { get; } = []; + + /// + /// Gets or private sets the view models of Copilot items. + /// + public ObservableCollection CopilotItemViewModels { get; } = []; + + /// + /// Initializes a new instance of the class. + /// + public CopilotViewModel() { - private readonly RunningState _runningState; - private static readonly ILogger _logger = Log.ForContext(); - private static readonly SemaphoreSlim _semaphore = new(1, 1); - private readonly List _copilotIdList = []; // 用于保存作业列表中的作业的Id,对于同一个作业,只有都执行成功才点赞 - private readonly List _recentlyRatedCopilotId = []; // TODO: 可能考虑加个持久化 - private AsstTaskType _taskType = AsstTaskType.Copilot; - - /// - /// 缓存的已解析作业,非即时添加的作业会使用该缓存 - /// - private CopilotBase? _copilotCache; - private const string CopilotIdPrefix = "maa://"; - private static readonly string TempCopilotFile = Path.Combine(CacheDir, "_temp_copilot.json"); - private static readonly string[] _supportExt = [".json", ".mp4", ".m4s", ".mkv", ".flv", ".avi"]; - private static readonly string CopilotJsonDir = Path.Combine(ConfigDir, "copilot"); - private const string StageNameRegex = @"(?:[a-z]{0,3})(?:\d{0,2})-(?:(?:A|B|C|D|EX|S|TR|MO)-?)?(?:\d{1,2})"; - private const string InvalidStageNameChars = @"[:',\.\(\)\|\[\]\?,。【】{};:]"; // 无效字符 - - [GeneratedRegex(InvalidStageNameChars)] - private static partial Regex InvalidStageNameRegex(); - - /// - /// Gets the view models of log items. - /// - public ObservableCollection LogItemViewModels { get; } = []; - - /// - /// Gets or private sets the view models of Copilot items. - /// - public ObservableCollection CopilotItemViewModels { get; } = []; - - /// - /// Initializes a new instance of the class. - /// - public CopilotViewModel() + DisplayName = LocalizationHelper.GetString("Copilot"); + AddLog(LocalizationHelper.GetString("CopilotTip"), showTime: false); + _runningState = RunningState.Instance; + _runningState.StateChanged += (_, e) => { - DisplayName = LocalizationHelper.GetString("Copilot"); - AddLog(LocalizationHelper.GetString("CopilotTip"), showTime: false); - _runningState = RunningState.Instance; - _runningState.StateChanged += (_, e) => - { - Idle = e.Idle; - Inited = e.Inited; - Stopping = e.Stopping; - }; + Idle = e.Idle; + Inited = e.Inited; + Stopping = e.Stopping; + }; - var copilotTaskList = ConfigurationHelper.GetValue(ConfigurationKeys.CopilotTaskList, string.Empty); - if (string.IsNullOrEmpty(copilotTaskList)) - { - return; - } - - var list = JsonConvert.DeserializeObject>(copilotTaskList) ?? []; - for (int i = 0; i < list.Count; i++) - { - list[i].Index = i; - CopilotItemViewModels.Add(list[i]); - } - - SaveCopilotTask(); - } - - #region UI绑定及操作 - - #region Log - - /// - /// Adds log. - /// - /// The content. - /// The font color. - /// The font weight. - /// Whether show time. - public void AddLog(string content, string color = UiLogColor.Trace, string weight = "Regular", bool showTime = true) + var copilotTaskList = ConfigurationHelper.GetValue(ConfigurationKeys.CopilotTaskList, string.Empty); + if (string.IsNullOrEmpty(copilotTaskList)) { - Execute.OnUIThread(() => - { - LogItemViewModels.Add(new LogItemViewModel(content, color, weight, "HH':'mm':'ss", showTime: showTime)); - if (showTime) - { - _logger.Information(content); - } - }); - - // LogItemViewModels.Insert(0, new LogItemViewModel(time + content, color, weight)); - } - - /// - /// Clears log. - /// - private void ClearLog() - { - Execute.OnUIThread(() => - { - LogItemViewModels.Clear(); - AddLog(LocalizationHelper.GetString("CopilotTip"), showTime: false); - }); - } - - #endregion Log - - #region 属性 - - private bool _idle; - - /// - /// Gets a value indicating whether it is idle. - /// - public bool Idle - { - get => _idle; - private set => SetAndNotify(ref _idle, value); - } - - private bool _inited; - - public bool Inited - { - get => _inited; - set => SetAndNotify(ref _inited, value); - } - - private bool _stopping; - - public bool Stopping - { - get => _stopping; - set => SetAndNotify(ref _stopping, value); - } - - private bool _startEnabled = true; - - /// - /// Gets or sets a value indicating whether the start button is enabled. - /// - public bool StartEnabled - { - get => _startEnabled; - set => SetAndNotify(ref _startEnabled, value); - } - - private int _activeTabIndex = 0; - - /// - /// Gets or sets 作业类型,0:主线/故事集/SS 1:保全派驻 2:悖论模拟 3:其他活动 - /// - public int ActiveTabIndex - { - get => _activeTabIndex; - set - { - if (!SetAndNotify(ref _activeTabIndex, value)) - { - return; - } - - Form = false; - UseCopilotList = value switch - { - 1 => false, - _ => UseCopilotList, - }; - } - } - - private string _filename = string.Empty; - - /// - /// Gets or sets the filename. - /// - public string Filename - { - get => _filename; - set - { - if (!File.Exists(value)) - { - var resourceFile = Path.Combine(ResourceDir, "copilot", Path.GetFileName(value)); - if (File.Exists(resourceFile)) - { - value = resourceFile; - } - } - - SetAndNotify(ref _filename, value); - ClearLog(); - if (string.IsNullOrWhiteSpace(value)) - { - CopilotUrl = CopilotUiUrl; - } - else - { - _ = UpdateFilename(value); - } - } - } - - private bool _form; - - /// - /// Gets or sets a value indicating whether to use auto-formation. - /// - public bool Form - { - get => _form; - set => SetAndNotify(ref _form, value); - } - - private bool _addTrust; - - /// - /// Gets or sets a value indicating whether to use auto-formation. - /// - public bool AddTrust - { - get => _addTrust; - set => SetAndNotify(ref _addTrust, value); - } - - private bool _ignoreRequirements; - - /// - /// Gets or sets a value indicating whether to use auto-formation. - /// - public bool IgnoreRequirements - { - get => _ignoreRequirements; - set => SetAndNotify(ref _ignoreRequirements, value); - } - - private bool _useSanityPotion; - - public bool UseSanityPotion - { - get => _useSanityPotion; - set => SetAndNotify(ref _useSanityPotion, value); - } - - private bool _addUserAdditional = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.CopilotAddUserAdditional, bool.FalseString)); - - /// - /// Gets or sets a value indicating whether to use auto-formation. - /// - public bool AddUserAdditional - { - get => _addUserAdditional; - set - { - SetAndNotify(ref _addUserAdditional, value); - ConfigurationHelper.SetValue(ConfigurationKeys.CopilotAddUserAdditional, value.ToString()); - } - } - - private string _userAdditional = ConfigurationHelper.GetValue(ConfigurationKeys.CopilotUserAdditional, string.Empty).Replace(",", ",").Replace(";", ";").Trim(); - - /// - /// Gets or sets a value indicating whether to use auto-formation. - /// - public string UserAdditional - { - get => _userAdditional; - set - { - value = value.Replace(",", ",").Replace(";", ";").Trim(); - SetAndNotify(ref _userAdditional, value); - ConfigurationHelper.SetValue(ConfigurationKeys.CopilotUserAdditional, value); - } - } - - private bool _useFormation; - - public bool UseFormation - { - get => _useFormation; - set => SetAndNotify(ref _useFormation, value); - } - - public List> FormationSelectList { get; } = - [ - new() { Display = "1", Value = 1 }, - new() { Display = "2", Value = 2 }, - new() { Display = "3", Value = 3 }, - new() { Display = "4", Value = 4 }, - ]; - - private int _formationIndex = ConfigurationHelper.GetValue(ConfigurationKeys.CopilotSelectFormation, 1); - - public int SelectFormation - { - get => _formationIndex; - set - { - SetAndNotify(ref _formationIndex, value); - ConfigurationHelper.SetValue(ConfigurationKeys.CopilotSelectFormation, value.ToString()); - } - } - - private bool _useCopilotList; - - /// - /// Gets or sets a value indicating whether 自动编队. - /// - public bool UseCopilotList - { - get => _useCopilotList; - set - { - if (value) - { - _taskType = AsstTaskType.Copilot; - Form = true; - } - - SetAndNotify(ref _useCopilotList, value); - } - } - - /// - /// Gets or sets a value indicating whether to use auto-formation. - /// - private string? _copilotTaskName = string.Empty; - - public string? CopilotTaskName - { - get => _copilotTaskName; - set - { - value = InvalidStageNameRegex().Replace(value ?? string.Empty, string.Empty).Trim(); - SetAndNotify(ref _copilotTaskName, value); - } - } - - public bool Loop { get; set; } - - private int _loopTimes = ConfigurationHelper.GetValue(ConfigurationKeys.CopilotLoopTimes, 1); - - public int LoopTimes - { - get => _loopTimes; - set - { - SetAndNotify(ref _loopTimes, value); - ConfigurationHelper.SetValue(ConfigurationKeys.CopilotLoopTimes, value.ToString()); - } - } - - private string _urlText = LocalizationHelper.GetString("PrtsPlus"); - - /// - /// Gets or private sets the UrlText. - /// - public string UrlText - { - get => _urlText; - private set => SetAndNotify(ref _urlText, value); - } - - private const string CopilotUiUrl = MaaUrls.PrtsPlus; - - private string _copilotUrl = CopilotUiUrl; - - /// - /// Gets or private sets the copilot URL. - /// - public string CopilotUrl - { - get => _copilotUrl; - private set - { - UrlText = value == CopilotUiUrl ? LocalizationHelper.GetString("PrtsPlus") : LocalizationHelper.GetString("VideoLink"); - SetAndNotify(ref _copilotUrl, value); - } - } - - private const string MapUiUrl = MaaUrls.MapPrts; - - private string _mapUrl = MapUiUrl; - - public string MapUrl - { - get => _mapUrl; - private set => SetAndNotify(ref _mapUrl, value); - } - - private bool _couldLikeWebJson; - - public bool CouldLikeWebJson - { - get => _couldLikeWebJson; - set => SetAndNotify(ref _couldLikeWebJson, value); - } - - #endregion 属性 - - #region 方法 - - /// - /// Selects file. - /// UI 绑定的方法 - /// - [UsedImplicitly] - public void SelectFile() - { - var dialog = new OpenFileDialog - { - Filter = "JSON|*.json|Video|*.mp4;*.m4s;*.mkv;*.flv;*.avi", - }; - - if (dialog.ShowDialog() == true) - { - Filename = dialog.FileName; - } - } - - /// - /// Paste clipboard contents. - /// UI 绑定的方法 - /// - [UsedImplicitly] - public void PasteClipboard() - { - if (Clipboard.ContainsText()) - { - Filename = Clipboard.GetText().Trim(); - } - else if (Clipboard.ContainsFileDropList()) - { - DropFile(Clipboard.GetFileDropList()[0]); - } - } - - /// - /// Paste clipboard contents. - /// UI 绑定的方法 - /// - /// Task - [UsedImplicitly] - public async Task PasteClipboardCopilotSet() - { - if (ActiveTabIndex is 1 or 3) - { - return; - } - - StartEnabled = false; - UseCopilotList = true; - ClearLog(); - if (Clipboard.ContainsText()) - { - await GetCopilotSetAsync(Clipboard.GetText().Trim()); - } - - StartEnabled = true; - } - - /// - /// 批量导入作业 - /// UI 绑定的方法 - /// - /// Task - [UsedImplicitly] - public async Task ImportFiles() - { - var dialog = new OpenFileDialog - { - Filter = "JSON|*.json", - Multiselect = true, - }; - - if (dialog.ShowDialog() != true) - { - return; - } - - CopilotId = 0; - _copilotCache = null; - foreach (var file in dialog.FileNames) - { - var fileInfo = new FileInfo(file); - if (!fileInfo.Exists) - { - AddLog(LocalizationHelper.GetString("CopilotNoFound") + file, showTime: false); - return; - } - - try - { - using var reader = new StreamReader(File.OpenRead(file)); - var str = await reader.ReadToEndAsync(); - var payload = JsonConvert.DeserializeObject(str, new CopilotContentConverter()); - if (payload is CopilotModel copilot) - { - var difficulty = copilot.Difficulty; - if (difficulty == CopilotModel.DifficultyFlags.None) - { - difficulty = CopilotModel.DifficultyFlags.Normal; - } - - await AddCopilotTaskToList(copilot, difficulty); - } - else if (payload is SSSCopilotModel) - { - AddLog(LocalizationHelper.GetString("CopilotSSSNotSupport"), UiLogColor.Error, showTime: false); - } - } - catch - { - AddLog(LocalizationHelper.GetString("CopilotFileReadError"), UiLogColor.Error, showTime: false); - return; - } - } - } - - // UI 绑定的方法 - [UsedImplicitly] - public async Task AddCopilotTask() - { - await AddCopilotTaskToList(CopilotTaskName, false); - } - - // UI 绑定的方法 - [UsedImplicitly] - public async Task AddCopilotTask_Adverse() - { - await AddCopilotTaskToList(CopilotTaskName, true); - } - - // UI 绑定的方法 - [UsedImplicitly] - public void SelectCopilotTask(object? sender, MouseButtonEventArgs? e = null) - { - if (e?.Source is FrameworkElement element && element.Tag is int index) - { - Filename = CopilotItemViewModels[index].FilePath; // 假设原方法接受int参数 - if (e.ChangedButton == MouseButton.Right) - { - UseCopilotList = false; - } - } - } - - // UI 绑定的方法 - [UsedImplicitly] - public void DeleteCopilotTask(int index) - { - CopilotItemViewModels.RemoveAt(index); - CopilotItemIndexChanged(); - } - - // UI 绑定的方法 - [UsedImplicitly] - public void CleanUnableCopilotTask() - { - foreach (var item in CopilotItemViewModels.Where(model => !model.IsChecked).ToList()) - { - CopilotItemViewModels.Remove(item); - } - - CopilotItemIndexChanged(); - } - - // UI 绑定的方法 - [UsedImplicitly] - public void ClearCopilotTask() - { - CopilotItemViewModels.Clear(); - SaveCopilotTask(); - - try - { - Directory.Delete(CopilotJsonDir, true); - } - catch - { - // ignored - } - } - - // UI 绑定的方法 - [UsedImplicitly] - public async Task LikeWebJson() - { - CouldLikeWebJson = false; - if (await RateCopilot(CopilotId) == PrtsStatus.Success) - { - AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.CopilotLikeGroup); - } - } - - // UI 绑定的方法 - [UsedImplicitly] - public void DislikeWebJson() - { - CouldLikeWebJson = false; - _ = RateCopilot(CopilotId, false); - } - - #endregion 方法 - - #endregion UI绑定及操作 - - private async Task UpdateFilename(string filename) - { - StartEnabled = false; - await UpdateFileDoc(filename); - StartEnabled = true; - } - - private async Task UpdateFileDoc(string filename) - { - ClearLog(); - CopilotUrl = CopilotUiUrl; - MapUrl = MapUiUrl; - IsDataFromWeb = false; - CopilotId = 0; - _copilotCache = null; - - int copilotId = 0; - bool writeToCache = false; - object? payload; - - if (File.Exists(filename)) - { - var fileSize = new FileInfo(filename).Length; - bool isJsonFile = filename.ToLower().EndsWith(".json") || fileSize < 4 * 1024 * 1024; - if (!isJsonFile) - { - _taskType = AsstTaskType.VideoRecognition; - return; - } - - try - { - using var reader = new StreamReader(File.OpenRead(filename)); - var str = await reader.ReadToEndAsync(); - payload = JsonConvert.DeserializeObject(str, new CopilotContentConverter()); - } - catch (Exception e) - { - AddLog(LocalizationHelper.GetString("CopilotFileReadError") + $"\n{e.Message}", UiLogColor.Error, showTime: false); - return; - } - } - else if (filename.StartsWith(CopilotIdPrefix, StringComparison.OrdinalIgnoreCase) || int.TryParse(filename, out _)) - { - (copilotId, payload) = await GetCopilotAsync(filename); - if (payload is not null) - { - IsDataFromWeb = true; - writeToCache = true; - CopilotId = copilotId; - } - } - else - { - payload = null; - } - - switch (payload) - { - case CopilotModel copilot: - await ParseCopilotAsync(copilot, writeToCache, UseCopilotList, copilotId); - return; - case SSSCopilotModel sss: - await ParseSSSCopilot(sss, writeToCache); - return; - default: - AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); - return; - } - } - - /// - /// 为自动战斗列表匹配名字 - /// - /// 用于匹配的名字 - /// 关卡名 or string.Empty - private static string? FindStageName(params string[] names) - { - names = names.Where(str => !string.IsNullOrEmpty(str)).ToArray(); - if (names.Length == 0) - { - return string.Empty; - } - - // 一旦有由小写字母、数字、'-'组成的name则视为关卡名直接使用 - var directName = names.FirstOrDefault(name => Regex.IsMatch(name.ToLower(), @"^[0-9a-z\-]+$")); - if (!string.IsNullOrEmpty(directName)) - { - return directName; - } - - var regex = new Regex(StageNameRegex, RegexOptions.Compiled | RegexOptions.IgnoreCase); - return names.Select(str => regex.Match(str)).FirstOrDefault(result => result.Success)?.Value ?? string.Empty; - } - - #region 作业解析 - - private async Task<(int CopilotId, CopilotBase? Payload)> GetCopilotAsync(string copilotCodeString) - { - if (copilotCodeString.StartsWith(CopilotIdPrefix, StringComparison.OrdinalIgnoreCase)) - { - copilotCodeString = copilotCodeString[CopilotIdPrefix.Length..]; - } - - if (int.TryParse(copilotCodeString, out var copilotCode)) - { - return await GetCopilotAsync(copilotCode); - } - - AddLog(LocalizationHelper.GetString("CopilotNoFound") + $":{copilotCodeString}", UiLogColor.Error, showTime: false); - return (0, null); - } - - private async Task<(int CopilotId, CopilotBase? Payload)> GetCopilotAsync(int copilotId) - { - var (status, copilotset) = await RequestCopilotAsync(copilotId); - if (status == PrtsStatus.NetworkError) - { - return (0, null); - } - else if (status == PrtsStatus.Success && copilotset is PrtsCopilotModel { StatusCode: 200 }) - { - if (copilotset.Data?.Content is CopilotModel { } copilot) - { - return (copilotId, copilot); // await ParseCopilotAsync(copilot, true, copilotList, copilotId)); - } - else if (copilotset.Data?.Content is SSSCopilotModel { } sss) - { - return (copilotId, sss); // await ParseSSSCopilot(sss, true)); - } - - AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); - return (0, null); - } - - AddLog(LocalizationHelper.GetString("CopilotNoFound") + $":{copilotId}", UiLogColor.Error, showTime: false); - return (0, null); - } - - private async Task ParseCopilotAsync(CopilotModel copilot, bool writeToCache, bool copilotList, int copilotId) - { - if (string.IsNullOrEmpty(copilot.StageName)) - { - AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); - return false; - } - - _taskType = AsstTaskType.Copilot; - _copilotCache = copilot; - if (copilot.Documentation?.Details is not null) - { - CopilotUrl = CopilotUiUrl; - var linkParser = new Regex(@"(?:av\d+|bv[a-z0-9]{10})(?:\/\?p=\d+)?", RegexOptions.Compiled | RegexOptions.IgnoreCase); - var match = linkParser.Match(copilot.Documentation.Details); - if (match.Success) - { - CopilotUrl = MaaUrls.BilibiliVideo + match.Value; // 视频链接 - } - } - - foreach (var (output, color) in copilot.Output()) - { - AddLog(output, color ?? UiLogColor.Message, showTime: false); - } - - MapUrl = MapUiUrl.Replace("areas", "map/" + copilot.StageName); - var navigateName = DataHelper.FindMap(copilot.StageName)?.Code; - if (navigateName is null) - { - // 不支持的关卡 - AddLog(LocalizationHelper.GetString("UnsupportedStages") + $" {copilot.StageName}", UiLogColor.Error, showTime: false); - navigateName = FindStageName(copilot.Documentation?.Title ?? string.Empty); - _ = Task.Run(ResourceUpdater.ResourceUpdateAndReloadAsync); - AchievementTrackerHelper.Instance.Unlock(AchievementIds.MapOutdated); - } - - CopilotTaskName = navigateName; - if (!writeToCache) - {// 现在是暂时将所有本地作业不添加到列表 - } - else if (copilotList) - { - switch (copilot.Difficulty) - { - case CopilotModel.DifficultyFlags.None: - await AddCopilotTaskToList(copilot, CopilotModel.DifficultyFlags.Normal, navigateName, copilotId); - break; - default: - await AddCopilotTaskToList(copilot, copilot.Difficulty, navigateName, copilotId); - break; - } - } - else - { - try - { - await File.WriteAllTextAsync(TempCopilotFile, JsonConvert.SerializeObject(copilot, Formatting.Indented)); - } - catch - { - _logger.Error("Could not save copilot task to file: " + TempCopilotFile); - return false; - } - } - - return true; - } - - private async Task ParseSSSCopilot(SSSCopilotModel copilot, bool writeToCache) - { - if (string.IsNullOrEmpty(copilot.StageName) || copilot.Type != new SSSCopilotModel().Type) - { - return false; - } - - _taskType = AsstTaskType.SSSCopilot; - _copilotCache = copilot; - MapUrl = MapUiUrl.Replace("areas", "map/" + copilot.StageName); - if (copilot.Documentation?.Details is not null) - { - CopilotUrl = CopilotUiUrl; - var linkParser = new Regex(@"(?:av\d+|bv[a-z0-9]{10})(?:\/\?p=\d+)?", RegexOptions.Compiled | RegexOptions.IgnoreCase); - var match = linkParser.Match(copilot.Documentation.Details); - if (match.Success) - { - CopilotUrl = MaaUrls.BilibiliVideo + match.Value; // 视频链接 - } - } - - foreach (var (output, color) in copilot.Output()) - { - AddLog(output, color ?? UiLogColor.Message, showTime: false); - } - - // 不支持的关卡 - var stages = copilot.Stages?.Select(copilot => DataHelper.FindMap(copilot.StageName)); - if (stages?.Any(i => i is null) is null or true) - { - AddLog(LocalizationHelper.GetString("UnsupportedStages") + $" {copilot.StageName}", UiLogColor.Error, showTime: false); - _ = Task.Run(ResourceUpdater.ResourceUpdateAndReloadAsync); - AchievementTrackerHelper.Instance.Unlock(AchievementIds.MapOutdated); - } - - if (writeToCache) - { - try - { - await File.WriteAllTextAsync(TempCopilotFile, JsonConvert.SerializeObject(copilot, Formatting.Indented)); - } - catch - { - _logger.Error("Could not save copilot task to file: " + TempCopilotFile); - return false; - } - } - - return true; - } - - #endregion 作业解析 - - #region 作业集解析 - - private async Task GetCopilotSetAsync(string copilotCodeString) - { - if (copilotCodeString.StartsWith(CopilotIdPrefix, StringComparison.OrdinalIgnoreCase)) - { - copilotCodeString = copilotCodeString[CopilotIdPrefix.Length..]; - } - - if (int.TryParse(copilotCodeString, out var copilotCode)) - { - await GetCopilotSetAsync(copilotCode); - return; - } - - AddLog(LocalizationHelper.GetString("CopilotNoFound") + $" {copilotCodeString}", UiLogColor.Error, showTime: false); - } - - private async Task GetCopilotSetAsync(int copilotCode) - { - var (status, copilotset) = await RequestCopilotSetAsync(copilotCode); - if (status == PrtsStatus.NetworkError) - { - return; - } - else if (status == PrtsStatus.Success && copilotset is PrtsCopilotSetModel { StatusCode: 200 }) - { - await ParseCopilotSetAsync(copilotset.Data); - return; - } - - AddLog(LocalizationHelper.GetString("CopilotNoFound") + $" {copilotCode}", UiLogColor.Error, showTime: false); return; } - private async Task ParseCopilotSetAsync(PrtsCopilotSetModel.CopilotSetData? copilotSet) + var list = JsonConvert.DeserializeObject>(copilotTaskList) ?? []; + for (int i = 0; i < list.Count; i++) { - CopilotId = 0; - _copilotCache = null; - ClearLog(); - if (copilotSet?.CopilotIds is null) + list[i].Index = i; + CopilotItemViewModels.Add(list[i]); + } + + SaveCopilotTask(); + } + + #region UI绑定及操作 + + #region Log + + /// + /// Adds log. + /// + /// The content. + /// The font color. + /// The font weight. + /// Whether show time. + public void AddLog(string content, string color = UiLogColor.Trace, string weight = "Regular", bool showTime = true) + { + Execute.OnUIThread(() => + { + LogItemViewModels.Add(new LogItemViewModel(content, color, weight, "HH':'mm':'ss", showTime: showTime)); + if (showTime) { - AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); - return; + _logger.Information(content); } - else if (copilotSet.CopilotIds.Count == 0) + }); + + // LogItemViewModels.Insert(0, new LogItemViewModel(time + content, color, weight)); + } + + /// + /// Clears log. + /// + private void ClearLog() + { + Execute.OnUIThread(() => + { + LogItemViewModels.Clear(); + AddLog(LocalizationHelper.GetString("CopilotTip"), showTime: false); + }); + } + + #endregion Log + + #region 属性 + + private bool _idle; + + /// + /// Gets a value indicating whether it is idle. + /// + public bool Idle + { + get => _idle; + private set => SetAndNotify(ref _idle, value); + } + + private bool _inited; + + public bool Inited + { + get => _inited; + set => SetAndNotify(ref _inited, value); + } + + private bool _stopping; + + public bool Stopping + { + get => _stopping; + set => SetAndNotify(ref _stopping, value); + } + + private bool _startEnabled = true; + + /// + /// Gets or sets a value indicating whether the start button is enabled. + /// + public bool StartEnabled + { + get => _startEnabled; + set => SetAndNotify(ref _startEnabled, value); + } + + private int _activeTabIndex = 0; + + /// + /// Gets or sets 作业类型,0:主线/故事集/SS 1:保全派驻 2:悖论模拟 3:其他活动 + /// + public int ActiveTabIndex + { + get => _activeTabIndex; + set + { + if (!SetAndNotify(ref _activeTabIndex, value)) { - Log(copilotSet.Name, copilotSet.Description); return; } - var list = copilotSet.CopilotIds.Select(async (copilotId) => await GetCopilotAsync(copilotId)).ToList(); - foreach (var task in list) + Form = false; + UseCopilotList = value switch { - var (copilotId, payload) = await task; + 1 => false, + _ => UseCopilotList, + }; + } + } + + private string _filename = string.Empty; + + /// + /// Gets or sets the filename. + /// + public string Filename + { + get => _filename; + set + { + if (!File.Exists(value)) + { + var resourceFile = Path.Combine(ResourceDir, "copilot", Path.GetFileName(value)); + if (File.Exists(resourceFile)) + { + value = resourceFile; + } + } + + SetAndNotify(ref _filename, value); + ClearLog(); + if (string.IsNullOrWhiteSpace(value)) + { + CopilotUrl = CopilotUiUrl; + } + else + { + _ = UpdateFilename(value); + } + } + } + + private bool _form; + + /// + /// Gets or sets a value indicating whether to use auto-formation. + /// + public bool Form + { + get => _form; + set => SetAndNotify(ref _form, value); + } + + private bool _addTrust; + + /// + /// Gets or sets a value indicating whether to use auto-formation. + /// + public bool AddTrust + { + get => _addTrust; + set => SetAndNotify(ref _addTrust, value); + } + + private bool _ignoreRequirements; + + /// + /// Gets or sets a value indicating whether to use auto-formation. + /// + public bool IgnoreRequirements + { + get => _ignoreRequirements; + set => SetAndNotify(ref _ignoreRequirements, value); + } + + private bool _useSanityPotion; + + public bool UseSanityPotion + { + get => _useSanityPotion; + set => SetAndNotify(ref _useSanityPotion, value); + } + + private bool _addUserAdditional = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.CopilotAddUserAdditional, bool.FalseString)); + + /// + /// Gets or sets a value indicating whether to use auto-formation. + /// + public bool AddUserAdditional + { + get => _addUserAdditional; + set + { + SetAndNotify(ref _addUserAdditional, value); + ConfigurationHelper.SetValue(ConfigurationKeys.CopilotAddUserAdditional, value.ToString()); + } + } + + private string _userAdditional = ConfigurationHelper.GetValue(ConfigurationKeys.CopilotUserAdditional, string.Empty).Replace(",", ",").Replace(";", ";").Trim(); + + /// + /// Gets or sets a value indicating whether to use auto-formation. + /// + public string UserAdditional + { + get => _userAdditional; + set + { + value = value.Replace(",", ",").Replace(";", ";").Trim(); + SetAndNotify(ref _userAdditional, value); + ConfigurationHelper.SetValue(ConfigurationKeys.CopilotUserAdditional, value); + } + } + + private bool _useFormation; + + public bool UseFormation + { + get => _useFormation; + set => SetAndNotify(ref _useFormation, value); + } + + public List> FormationSelectList { get; } = + [ + new() { Display = "1", Value = 1 }, + new() { Display = "2", Value = 2 }, + new() { Display = "3", Value = 3 }, + new() { Display = "4", Value = 4 }, + ]; + + private int _formationIndex = ConfigurationHelper.GetValue(ConfigurationKeys.CopilotSelectFormation, 1); + + public int SelectFormation + { + get => _formationIndex; + set + { + SetAndNotify(ref _formationIndex, value); + ConfigurationHelper.SetValue(ConfigurationKeys.CopilotSelectFormation, value.ToString()); + } + } + + private bool _useCopilotList; + + /// + /// Gets or sets a value indicating whether 自动编队. + /// + public bool UseCopilotList + { + get => _useCopilotList; + set + { + if (value) + { + _taskType = AsstTaskType.Copilot; + Form = true; + } + + SetAndNotify(ref _useCopilotList, value); + } + } + + /// + /// Gets or sets a value indicating whether to use auto-formation. + /// + private string? _copilotTaskName = string.Empty; + + public string? CopilotTaskName + { + get => _copilotTaskName; + set + { + value = InvalidStageNameRegex().Replace(value ?? string.Empty, string.Empty).Trim(); + SetAndNotify(ref _copilotTaskName, value); + } + } + + public bool Loop { get; set; } + + private int _loopTimes = ConfigurationHelper.GetValue(ConfigurationKeys.CopilotLoopTimes, 1); + + public int LoopTimes + { + get => _loopTimes; + set + { + SetAndNotify(ref _loopTimes, value); + ConfigurationHelper.SetValue(ConfigurationKeys.CopilotLoopTimes, value.ToString()); + } + } + + private string _urlText = LocalizationHelper.GetString("PrtsPlus"); + + /// + /// Gets or private sets the UrlText. + /// + public string UrlText + { + get => _urlText; + private set => SetAndNotify(ref _urlText, value); + } + + private const string CopilotUiUrl = MaaUrls.PrtsPlus; + + private string _copilotUrl = CopilotUiUrl; + + /// + /// Gets or private sets the copilot URL. + /// + public string CopilotUrl + { + get => _copilotUrl; + private set + { + UrlText = value == CopilotUiUrl ? LocalizationHelper.GetString("PrtsPlus") : LocalizationHelper.GetString("VideoLink"); + SetAndNotify(ref _copilotUrl, value); + } + } + + private const string MapUiUrl = MaaUrls.MapPrts; + + private string _mapUrl = MapUiUrl; + + public string MapUrl + { + get => _mapUrl; + private set => SetAndNotify(ref _mapUrl, value); + } + + private bool _couldLikeWebJson; + + public bool CouldLikeWebJson + { + get => _couldLikeWebJson; + set => SetAndNotify(ref _couldLikeWebJson, value); + } + + #endregion 属性 + + #region 方法 + + /// + /// Selects file. + /// UI 绑定的方法 + /// + [UsedImplicitly] + public void SelectFile() + { + var dialog = new OpenFileDialog + { + Filter = "JSON|*.json|Video|*.mp4;*.m4s;*.mkv;*.flv;*.avi", + }; + + if (dialog.ShowDialog() == true) + { + Filename = dialog.FileName; + } + } + + /// + /// Paste clipboard contents. + /// UI 绑定的方法 + /// + [UsedImplicitly] + public void PasteClipboard() + { + if (Clipboard.ContainsText()) + { + Filename = Clipboard.GetText().Trim(); + } + else if (Clipboard.ContainsFileDropList()) + { + DropFile(Clipboard.GetFileDropList()[0]); + } + } + + /// + /// Paste clipboard contents. + /// UI 绑定的方法 + /// + /// Task + [UsedImplicitly] + public async Task PasteClipboardCopilotSet() + { + if (ActiveTabIndex is 1 or 3) + { + return; + } + + StartEnabled = false; + UseCopilotList = true; + ClearLog(); + if (Clipboard.ContainsText()) + { + await GetCopilotSetAsync(Clipboard.GetText().Trim()); + } + + StartEnabled = true; + } + + /// + /// 批量导入作业 + /// UI 绑定的方法 + /// + /// Task + [UsedImplicitly] + public async Task ImportFiles() + { + var dialog = new OpenFileDialog + { + Filter = "JSON|*.json", + Multiselect = true, + }; + + if (dialog.ShowDialog() != true) + { + return; + } + + CopilotId = 0; + _copilotCache = null; + foreach (var file in dialog.FileNames) + { + var fileInfo = new FileInfo(file); + if (!fileInfo.Exists) + { + AddLog(LocalizationHelper.GetString("CopilotNoFound") + file, showTime: false); + return; + } + + try + { + using var reader = new StreamReader(File.OpenRead(file)); + var str = await reader.ReadToEndAsync(); + var payload = JsonConvert.DeserializeObject(str, new CopilotContentConverter()); if (payload is CopilotModel copilot) { - await ParseCopilotAsync(copilot, true, true, copilotId); + var difficulty = copilot.Difficulty; + if (difficulty == CopilotModel.DifficultyFlags.None) + { + difficulty = CopilotModel.DifficultyFlags.Normal; + } + + await AddCopilotTaskToList(copilot, difficulty); } - } - - ClearLog(); - Log(copilotSet.Name, copilotSet.Description); - return; - - void Log(string? name, string? description) - { - if (!string.IsNullOrWhiteSpace(name)) - { - AddLog(name, UiLogColor.Message, showTime: false); - } - - if (!string.IsNullOrWhiteSpace(description)) - { - AddLog(description, UiLogColor.Message, showTime: false); - } - } - } - - #endregion 作业集解析 - - /// - /// Drops file. - /// - /// The sender. - /// The event arguments. - /// TODO: 不知道为啥现在拖放不用了,之后瞅瞅 - [UsedImplicitly] - public void DropFile(object sender, DragEventArgs e) - { - if (!e.Data.GetDataPresent(DataFormats.FileDrop)) - { - return; - } - - var filename = ((Array?)e.Data.GetData(DataFormats.FileDrop))?.GetValue(0)?.ToString(); - DropFile(filename); - } - - private void DropFile(string? filename) - { - if (string.IsNullOrEmpty(filename)) - { - return; - } - - var filenameLower = filename.ToLower(); - bool support = _supportExt.Any(ext => filenameLower.EndsWith(ext)); - - if (support) - { - Filename = filename; - } - else - { - Filename = string.Empty; - ClearLog(); - AddLog(LocalizationHelper.GetString("NotCopilotJson"), UiLogColor.Error, showTime: false); - } - } - - /// - /// On comboBox drop down opened. - /// - /// The sender. - /// The event arguments. - [UsedImplicitly] - public void OnDropDownOpened(object sender, EventArgs e) - { - if (sender is not ComboBox comboBox) - { - return; - } - - try - { - var files = Directory.GetFiles(Path.Combine(ResourceDir, "copilot"), "*.json"); - comboBox.ItemsSource = files.Select(Path.GetFileName).ToList(); - } - catch (Exception exception) - { - comboBox.ItemsSource = null; - AddLog(exception.Message, UiLogColor.Error, showTime: false); - } - } - - private async Task AddCopilotTaskToList(string? stageName, bool isRaid) - { - if (string.IsNullOrEmpty(stageName) || InvalidStageNameRegex().IsMatch(stageName)) - { - AddLog(LocalizationHelper.GetString("CopilotInvalidStageNameForNavigation"), UiLogColor.Error, showTime: false); - return; - } - - try - { - if (_copilotCache is CopilotModel { } copilot) - { - await AddCopilotTaskToList(copilot, !isRaid ? CopilotModel.DifficultyFlags.Normal : CopilotModel.DifficultyFlags.Raid, stageName, CopilotId); - } - else + else if (payload is SSSCopilotModel) { AddLog(LocalizationHelper.GetString("CopilotSSSNotSupport"), UiLogColor.Error, showTime: false); } } - catch (Exception ex) + catch { - AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); - _logger.Error(ex, "Exception caught"); + AddLog(LocalizationHelper.GetString("CopilotFileReadError"), UiLogColor.Error, showTime: false); + return; } } + } - /// - /// 将作业添加到列表 - /// - /// 作业 - /// 难度等级 - /// 关卡 code,用于导航 - /// 作业站 id - /// 是否添加了作业 - private async Task AddCopilotTaskToList(CopilotModel copilot, CopilotModel.DifficultyFlags flags, string? navigateName = null, int copilotId = 0) + // UI 绑定的方法 + [UsedImplicitly] + public async Task AddCopilotTask() + { + await AddCopilotTaskToList(CopilotTaskName, false); + } + + // UI 绑定的方法 + [UsedImplicitly] + public async Task AddCopilotTask_Adverse() + { + await AddCopilotTaskToList(CopilotTaskName, true); + } + + // UI 绑定的方法 + [UsedImplicitly] + public void SelectCopilotTask(object? sender, MouseButtonEventArgs? e = null) + { + if (e?.Source is FrameworkElement element && element.Tag is int index) { - if (string.IsNullOrEmpty(copilot.StageName)) + Filename = CopilotItemViewModels[index].FilePath; // 假设原方法接受int参数 + if (e.ChangedButton == MouseButton.Right) { - _logger.Error("Could not add copilot task with empty stage"); - return false; + UseCopilotList = false; } + } + } - if (!Path.Exists(CopilotJsonDir)) - { - try - { - Directory.CreateDirectory(CopilotJsonDir); - } - catch - { - return false; - } - } + // UI 绑定的方法 + [UsedImplicitly] + public void DeleteCopilotTask(int index) + { + CopilotItemViewModels.RemoveAt(index); + CopilotItemIndexChanged(); + } - var mapInfo = DataHelper.FindMap(copilot.StageName); - var stageCode = mapInfo?.Code; - var stageId = mapInfo?.StageId; - if (mapInfo is null) - { - AddLog(string.Format(LocalizationHelper.GetString("CopilotStageNameNotFound"), stageCode), UiLogColor.Error, showTime: false); - return false; - } + // UI 绑定的方法 + [UsedImplicitly] + public void CleanUnableCopilotTask() + { + foreach (var item in CopilotItemViewModels.Where(model => !model.IsChecked).ToList()) + { + CopilotItemViewModels.Remove(item); + } - navigateName = string.IsNullOrEmpty(navigateName) ? stageCode : navigateName; - if (stageCode != navigateName) - { - stageCode = navigateName; - AddLog(LocalizationHelper.GetString("CopilotStageNameNotEqualWithNavigateName"), UiLogColor.Warning, showTime: false); - } + CopilotItemIndexChanged(); + } - if (stageId is null || stageCode is null || navigateName is null) - { - return false; - } + // UI 绑定的方法 + [UsedImplicitly] + public void ClearCopilotTask() + { + CopilotItemViewModels.Clear(); + SaveCopilotTask(); - var fileName = !string.IsNullOrEmpty(stageCode) ? stageCode : DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); - var cachePath = $"{CopilotJsonDir}/{fileName}.json"; - await _semaphore.WaitAsync(); - if (File.Exists(cachePath) && CopilotItemViewModels.Any(i => i.FilePath == cachePath)) + try + { + Directory.Delete(CopilotJsonDir, true); + } + catch + { + // ignored + } + } + + // UI 绑定的方法 + [UsedImplicitly] + public async Task LikeWebJson() + { + CouldLikeWebJson = false; + if (await RateCopilot(CopilotId) == PrtsStatus.Success) + { + AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.CopilotLikeGroup); + } + } + + // UI 绑定的方法 + [UsedImplicitly] + public void DislikeWebJson() + { + CouldLikeWebJson = false; + _ = RateCopilot(CopilotId, false); + } + + #endregion 方法 + + #endregion UI绑定及操作 + + private async Task UpdateFilename(string filename) + { + StartEnabled = false; + await UpdateFileDoc(filename); + StartEnabled = true; + } + + private async Task UpdateFileDoc(string filename) + { + ClearLog(); + CopilotUrl = CopilotUiUrl; + MapUrl = MapUiUrl; + IsDataFromWeb = false; + CopilotId = 0; + _copilotCache = null; + + int copilotId = 0; + bool writeToCache = false; + object? payload; + + if (File.Exists(filename)) + { + var fileSize = new FileInfo(filename).Length; + bool isJsonFile = filename.ToLower().EndsWith(".json") || fileSize < 4 * 1024 * 1024; + if (!isJsonFile) { - cachePath = $"{CopilotJsonDir}/{fileName}_{DateTimeOffset.Now.ToUnixTimeMilliseconds()}.json"; - if (CopilotItemViewModels.Any(i => i.FilePath == cachePath)) - { - _logger.Error("Could not add copilot task with duplicate stage name: {StageName}", copilot.StageName); - _semaphore.Release(); - return false; - } + _taskType = AsstTaskType.VideoRecognition; + return; } try { - await File.WriteAllTextAsync(cachePath, JsonConvert.SerializeObject(copilot, Formatting.Indented)); + using var reader = new StreamReader(File.OpenRead(filename)); + var str = await reader.ReadToEndAsync(); + payload = JsonConvert.DeserializeObject(str, new CopilotContentConverter()); + } + catch (Exception e) + { + AddLog(LocalizationHelper.GetString("CopilotFileReadError") + $"\n{e.Message}", UiLogColor.Error, showTime: false); + return; + } + } + else if (filename.StartsWith(CopilotIdPrefix, StringComparison.OrdinalIgnoreCase) || int.TryParse(filename, out _)) + { + (copilotId, payload) = await GetCopilotAsync(filename); + if (payload is not null) + { + IsDataFromWeb = true; + writeToCache = true; + CopilotId = copilotId; + } + } + else + { + payload = null; + } + + switch (payload) + { + case CopilotModel copilot: + await ParseCopilotAsync(copilot, writeToCache, UseCopilotList, copilotId); + return; + case SSSCopilotModel sss: + await ParseSSSCopilot(sss, writeToCache); + return; + default: + AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); + return; + } + } + + /// + /// 为自动战斗列表匹配名字 + /// + /// 用于匹配的名字 + /// 关卡名 or string.Empty + private static string? FindStageName(params string[] names) + { + names = names.Where(str => !string.IsNullOrEmpty(str)).ToArray(); + if (names.Length == 0) + { + return string.Empty; + } + + // 一旦有由小写字母、数字、'-'组成的name则视为关卡名直接使用 + var directName = names.FirstOrDefault(name => Regex.IsMatch(name.ToLower(), @"^[0-9a-z\-]+$")); + if (!string.IsNullOrEmpty(directName)) + { + return directName; + } + + var regex = new Regex(StageNameRegex, RegexOptions.Compiled | RegexOptions.IgnoreCase); + return names.Select(str => regex.Match(str)).FirstOrDefault(result => result.Success)?.Value ?? string.Empty; + } + + #region 作业解析 + + private async Task<(int CopilotId, CopilotBase? Payload)> GetCopilotAsync(string copilotCodeString) + { + if (copilotCodeString.StartsWith(CopilotIdPrefix, StringComparison.OrdinalIgnoreCase)) + { + copilotCodeString = copilotCodeString[CopilotIdPrefix.Length..]; + } + + if (int.TryParse(copilotCodeString, out var copilotCode)) + { + return await GetCopilotAsync(copilotCode); + } + + AddLog(LocalizationHelper.GetString("CopilotNoFound") + $":{copilotCodeString}", UiLogColor.Error, showTime: false); + return (0, null); + } + + private async Task<(int CopilotId, CopilotBase? Payload)> GetCopilotAsync(int copilotId) + { + var (status, copilotset) = await RequestCopilotAsync(copilotId); + if (status == PrtsStatus.NetworkError) + { + return (0, null); + } + else if (status == PrtsStatus.Success && copilotset is PrtsCopilotModel { StatusCode: 200 }) + { + if (copilotset.Data?.Content is CopilotModel { } copilot) + { + return (copilotId, copilot); // await ParseCopilotAsync(copilot, true, copilotList, copilotId)); + } + else if (copilotset.Data?.Content is SSSCopilotModel { } sss) + { + return (copilotId, sss); // await ParseSSSCopilot(sss, true)); + } + + AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); + return (0, null); + } + + AddLog(LocalizationHelper.GetString("CopilotNoFound") + $":{copilotId}", UiLogColor.Error, showTime: false); + return (0, null); + } + + private async Task ParseCopilotAsync(CopilotModel copilot, bool writeToCache, bool copilotList, int copilotId) + { + if (string.IsNullOrEmpty(copilot.StageName)) + { + AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); + return false; + } + + _taskType = AsstTaskType.Copilot; + _copilotCache = copilot; + if (copilot.Documentation?.Details is not null) + { + CopilotUrl = CopilotUiUrl; + var linkParser = new Regex(@"(?:av\d+|bv[a-z0-9]{10})(?:\/\?p=\d+)?", RegexOptions.Compiled | RegexOptions.IgnoreCase); + var match = linkParser.Match(copilot.Documentation.Details); + if (match.Success) + { + CopilotUrl = MaaUrls.BilibiliVideo + match.Value; // 视频链接 + } + } + + foreach (var (output, color) in copilot.Output()) + { + AddLog(output, color ?? UiLogColor.Message, showTime: false); + } + + MapUrl = MapUiUrl.Replace("areas", "map/" + copilot.StageName); + var navigateName = DataHelper.FindMap(copilot.StageName)?.Code; + if (navigateName is null) + { + // 不支持的关卡 + AddLog(LocalizationHelper.GetString("UnsupportedStages") + $" {copilot.StageName}", UiLogColor.Error, showTime: false); + navigateName = FindStageName(copilot.Documentation?.Title ?? string.Empty); + _ = Task.Run(ResourceUpdater.ResourceUpdateAndReloadAsync); + AchievementTrackerHelper.Instance.Unlock(AchievementIds.MapOutdated); + } + + CopilotTaskName = navigateName; + if (!writeToCache) + {// 现在是暂时将所有本地作业不添加到列表 + } + else if (copilotList) + { + switch (copilot.Difficulty) + { + case CopilotModel.DifficultyFlags.None: + await AddCopilotTaskToList(copilot, CopilotModel.DifficultyFlags.Normal, navigateName, copilotId); + break; + default: + await AddCopilotTaskToList(copilot, copilot.Difficulty, navigateName, copilotId); + break; + } + } + else + { + try + { + await File.WriteAllTextAsync(TempCopilotFile, JsonConvert.SerializeObject(copilot, Formatting.Indented)); } catch { - AddLog(LocalizationHelper.GetString("CopilotCouldNotSaveFile") + cachePath, UiLogColor.Error, showTime: false); + _logger.Error("Could not save copilot task to file: " + TempCopilotFile); + return false; + } + } + + return true; + } + + private async Task ParseSSSCopilot(SSSCopilotModel copilot, bool writeToCache) + { + if (string.IsNullOrEmpty(copilot.StageName) || copilot.Type != new SSSCopilotModel().Type) + { + return false; + } + + _taskType = AsstTaskType.SSSCopilot; + _copilotCache = copilot; + MapUrl = MapUiUrl.Replace("areas", "map/" + copilot.StageName); + if (copilot.Documentation?.Details is not null) + { + CopilotUrl = CopilotUiUrl; + var linkParser = new Regex(@"(?:av\d+|bv[a-z0-9]{10})(?:\/\?p=\d+)?", RegexOptions.Compiled | RegexOptions.IgnoreCase); + var match = linkParser.Match(copilot.Documentation.Details); + if (match.Success) + { + CopilotUrl = MaaUrls.BilibiliVideo + match.Value; // 视频链接 + } + } + + foreach (var (output, color) in copilot.Output()) + { + AddLog(output, color ?? UiLogColor.Message, showTime: false); + } + + // 不支持的关卡 + var stages = copilot.Stages?.Select(copilot => DataHelper.FindMap(copilot.StageName)); + if (stages?.Any(i => i is null) is null or true) + { + AddLog(LocalizationHelper.GetString("UnsupportedStages") + $" {copilot.StageName}", UiLogColor.Error, showTime: false); + _ = Task.Run(ResourceUpdater.ResourceUpdateAndReloadAsync); + AchievementTrackerHelper.Instance.Unlock(AchievementIds.MapOutdated); + } + + if (writeToCache) + { + try + { + await File.WriteAllTextAsync(TempCopilotFile, JsonConvert.SerializeObject(copilot, Formatting.Indented)); + } + catch + { + _logger.Error("Could not save copilot task to file: " + TempCopilotFile); + return false; + } + } + + return true; + } + + #endregion 作业解析 + + #region 作业集解析 + + private async Task GetCopilotSetAsync(string copilotCodeString) + { + if (copilotCodeString.StartsWith(CopilotIdPrefix, StringComparison.OrdinalIgnoreCase)) + { + copilotCodeString = copilotCodeString[CopilotIdPrefix.Length..]; + } + + if (int.TryParse(copilotCodeString, out var copilotCode)) + { + await GetCopilotSetAsync(copilotCode); + return; + } + + AddLog(LocalizationHelper.GetString("CopilotNoFound") + $" {copilotCodeString}", UiLogColor.Error, showTime: false); + } + + private async Task GetCopilotSetAsync(int copilotCode) + { + var (status, copilotset) = await RequestCopilotSetAsync(copilotCode); + if (status == PrtsStatus.NetworkError) + { + return; + } + else if (status == PrtsStatus.Success && copilotset is PrtsCopilotSetModel { StatusCode: 200 }) + { + await ParseCopilotSetAsync(copilotset.Data); + return; + } + + AddLog(LocalizationHelper.GetString("CopilotNoFound") + $" {copilotCode}", UiLogColor.Error, showTime: false); + return; + } + + private async Task ParseCopilotSetAsync(PrtsCopilotSetModel.CopilotSetData? copilotSet) + { + CopilotId = 0; + _copilotCache = null; + ClearLog(); + if (copilotSet?.CopilotIds is null) + { + AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); + return; + } + else if (copilotSet.CopilotIds.Count == 0) + { + Log(copilotSet.Name, copilotSet.Description); + return; + } + + var list = copilotSet.CopilotIds.Select(async (copilotId) => await GetCopilotAsync(copilotId)).ToList(); + foreach (var task in list) + { + var (copilotId, payload) = await task; + if (payload is CopilotModel copilot) + { + await ParseCopilotAsync(copilot, true, true, copilotId); + } + } + + ClearLog(); + Log(copilotSet.Name, copilotSet.Description); + return; + + void Log(string? name, string? description) + { + if (!string.IsNullOrWhiteSpace(name)) + { + AddLog(name, UiLogColor.Message, showTime: false); + } + + if (!string.IsNullOrWhiteSpace(description)) + { + AddLog(description, UiLogColor.Message, showTime: false); + } + } + } + + #endregion 作业集解析 + + /// + /// Drops file. + /// + /// The sender. + /// The event arguments. + /// TODO: 不知道为啥现在拖放不用了,之后瞅瞅 + [UsedImplicitly] + public void DropFile(object sender, DragEventArgs e) + { + if (!e.Data.GetDataPresent(DataFormats.FileDrop)) + { + return; + } + + var filename = ((Array?)e.Data.GetData(DataFormats.FileDrop))?.GetValue(0)?.ToString(); + DropFile(filename); + } + + private void DropFile(string? filename) + { + if (string.IsNullOrEmpty(filename)) + { + return; + } + + var filenameLower = filename.ToLower(); + bool support = _supportExt.Any(ext => filenameLower.EndsWith(ext)); + + if (support) + { + Filename = filename; + } + else + { + Filename = string.Empty; + ClearLog(); + AddLog(LocalizationHelper.GetString("NotCopilotJson"), UiLogColor.Error, showTime: false); + } + } + + /// + /// On comboBox drop down opened. + /// + /// The sender. + /// The event arguments. + [UsedImplicitly] + public void OnDropDownOpened(object sender, EventArgs e) + { + if (sender is not ComboBox comboBox) + { + return; + } + + try + { + var files = Directory.GetFiles(Path.Combine(ResourceDir, "copilot"), "*.json"); + comboBox.ItemsSource = files.Select(Path.GetFileName).ToList(); + } + catch (Exception exception) + { + comboBox.ItemsSource = null; + AddLog(exception.Message, UiLogColor.Error, showTime: false); + } + } + + private async Task AddCopilotTaskToList(string? stageName, bool isRaid) + { + if (string.IsNullOrEmpty(stageName) || InvalidStageNameRegex().IsMatch(stageName)) + { + AddLog(LocalizationHelper.GetString("CopilotInvalidStageNameForNavigation"), UiLogColor.Error, showTime: false); + return; + } + + try + { + if (_copilotCache is CopilotModel { } copilot) + { + await AddCopilotTaskToList(copilot, !isRaid ? CopilotModel.DifficultyFlags.Normal : CopilotModel.DifficultyFlags.Raid, stageName, CopilotId); + } + else + { + AddLog(LocalizationHelper.GetString("CopilotSSSNotSupport"), UiLogColor.Error, showTime: false); + } + } + catch (Exception ex) + { + AddLog(LocalizationHelper.GetString("CopilotJsonError"), UiLogColor.Error, showTime: false); + _logger.Error(ex, "Exception caught"); + } + } + + /// + /// 将作业添加到列表 + /// + /// 作业 + /// 难度等级 + /// 关卡 code,用于导航 + /// 作业站 id + /// 是否添加了作业 + private async Task AddCopilotTaskToList(CopilotModel copilot, CopilotModel.DifficultyFlags flags, string? navigateName = null, int copilotId = 0) + { + if (string.IsNullOrEmpty(copilot.StageName)) + { + _logger.Error("Could not add copilot task with empty stage"); + return false; + } + + if (!Path.Exists(CopilotJsonDir)) + { + try + { + Directory.CreateDirectory(CopilotJsonDir); + } + catch + { + return false; + } + } + + var mapInfo = DataHelper.FindMap(copilot.StageName); + var stageCode = mapInfo?.Code; + var stageId = mapInfo?.StageId; + if (mapInfo is null) + { + AddLog(string.Format(LocalizationHelper.GetString("CopilotStageNameNotFound"), stageCode), UiLogColor.Error, showTime: false); + return false; + } + + navigateName = string.IsNullOrEmpty(navigateName) ? stageCode : navigateName; + if (stageCode != navigateName) + { + stageCode = navigateName; + AddLog(LocalizationHelper.GetString("CopilotStageNameNotEqualWithNavigateName"), UiLogColor.Warning, showTime: false); + } + + if (stageId is null || stageCode is null || navigateName is null) + { + return false; + } + + var fileName = !string.IsNullOrEmpty(stageCode) ? stageCode : DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + var cachePath = $"{CopilotJsonDir}/{fileName}.json"; + await _semaphore.WaitAsync(); + if (File.Exists(cachePath) && CopilotItemViewModels.Any(i => i.FilePath == cachePath)) + { + cachePath = $"{CopilotJsonDir}/{fileName}_{DateTimeOffset.Now.ToUnixTimeMilliseconds()}.json"; + if (CopilotItemViewModels.Any(i => i.FilePath == cachePath)) + { + _logger.Error("Could not add copilot task with duplicate stage name: {StageName}", copilot.StageName); _semaphore.Release(); return false; } + } - if (ActiveTabIndex == 2) + try + { + await File.WriteAllTextAsync(cachePath, JsonConvert.SerializeObject(copilot, Formatting.Indented)); + } + catch + { + AddLog(LocalizationHelper.GetString("CopilotCouldNotSaveFile") + cachePath, UiLogColor.Error, showTime: false); + _semaphore.Release(); + return false; + } + + if (ActiveTabIndex == 2) + { + string? name = null; + if (stageId?.Length > 6) { - string? name = null; - if (stageId?.Length > 6) - { - var codeName = stageId[4..^2]; - var characterInfo = DataHelper.GetCharacterByCodeName(codeName); - name = DataHelper.GetLocalizedCharacterName(characterInfo); - } + var codeName = stageId[4..^2]; + var characterInfo = DataHelper.GetCharacterByCodeName(codeName); + name = DataHelper.GetLocalizedCharacterName(characterInfo); + } - name ??= stageCode; + name ??= stageCode; - var item = new CopilotItemViewModel(name, cachePath, false, copilotId) { Index = CopilotItemViewModels.Count, }; + var item = new CopilotItemViewModel(name, cachePath, false, copilotId) { Index = CopilotItemViewModels.Count, }; + CopilotItemViewModels.Add(item); + } + else + { + if (flags.HasFlag(CopilotModel.DifficultyFlags.Normal)) + { + var item = new CopilotItemViewModel(stageCode, cachePath, false, copilotId) { Index = CopilotItemViewModels.Count, }; CopilotItemViewModels.Add(item); } - else + + if (flags.HasFlag(CopilotModel.DifficultyFlags.Raid)) { - if (flags.HasFlag(CopilotModel.DifficultyFlags.Normal)) + var item = new CopilotItemViewModel(stageCode, cachePath, true, copilotId) { Index = CopilotItemViewModels.Count, }; + CopilotItemViewModels.Add(item); + } + } + + _semaphore.Release(); + SaveCopilotTask(); + return true; + } + + public void SaveCopilotTask() + { + var task = JsonConvert.SerializeObject(CopilotItemViewModels, new JsonSerializerSettings() { DefaultValueHandling = DefaultValueHandling.Ignore }); + ConfigurationHelper.SetValue(ConfigurationKeys.CopilotTaskList, task); + } + + /// + /// 战斗列表的当前战斗任务成功 + /// + public void CopilotTaskSuccess() + { + Execute.OnUIThread(() => + { + foreach (var model in CopilotItemViewModels) + { + if (!model.IsChecked) { - var item = new CopilotItemViewModel(stageCode, cachePath, false, copilotId) { Index = CopilotItemViewModels.Count, }; - CopilotItemViewModels.Add(item); + continue; } - if (flags.HasFlag(CopilotModel.DifficultyFlags.Raid)) + model.IsChecked = false; + + if (model.CopilotId > 0 && _copilotIdList.Remove(model.CopilotId) && _copilotIdList.IndexOf(model.CopilotId) == -1) { - var item = new CopilotItemViewModel(stageCode, cachePath, true, copilotId) { Index = CopilotItemViewModels.Count, }; - CopilotItemViewModels.Add(item); + _ = RateCopilot(model.CopilotId); } + + break; } - _semaphore.Release(); SaveCopilotTask(); - return true; + }); + } + + /// + /// 更新任务顺序 + /// UI 绑定的方法 + /// + [UsedImplicitly] + public void CopilotItemIndexChanged() + { + Execute.OnUIThread(() => + { + for (int i = 0; i < CopilotItemViewModels.Count; i++) + { + CopilotItemViewModels[i].Index = i; + } + + SaveCopilotTask(); + }); + } + + /// + /// Starts copilot. + /// UI 绑定的方法 + /// + /// Task + [UsedImplicitly] + public async Task Start() + { + /* + if (_form) + { + AddLog(Localization.GetString("AutoSquadTip"), LogColor.Message); + }*/ + _runningState.SetIdle(false); + + if (_taskType == AsstTaskType.VideoRecognition) + { + _ = StartVideoTask(); + return; } - public void SaveCopilotTask() + if (_taskType == AsstTaskType.Copilot && UseCopilotList && !await VerifyCopilotListTask()) { - var task = JsonConvert.SerializeObject(CopilotItemViewModels, new JsonSerializerSettings() { DefaultValueHandling = DefaultValueHandling.Ignore }); - ConfigurationHelper.SetValue(ConfigurationKeys.CopilotTaskList, task); + // 校验作业合法性 + _runningState.SetIdle(true); + return; + } + else if (_taskType == AsstTaskType.Copilot && !UseCopilotList && _copilotCache is null) + { + AddLog(LocalizationHelper.GetString("CopilotEmptyError"), UiLogColor.Error, showTime: false); + _runningState.SetIdle(true); + return; } - /// - /// 战斗列表的当前战斗任务成功 - /// - public void CopilotTaskSuccess() + if (SettingsViewModel.GameSettings.CopilotWithScript) { - Execute.OnUIThread(() => + await Task.Run(() => SettingsViewModel.GameSettings.RunScript("StartsWithScript", showLog: false)); + if (!string.IsNullOrWhiteSpace(SettingsViewModel.GameSettings.StartsWithScript)) { - foreach (var model in CopilotItemViewModels) - { - if (!model.IsChecked) - { - continue; - } - - model.IsChecked = false; - - if (model.CopilotId > 0 && _copilotIdList.Remove(model.CopilotId) && _copilotIdList.IndexOf(model.CopilotId) == -1) - { - _ = RateCopilot(model.CopilotId); - } - - break; - } - - SaveCopilotTask(); - }); + AddLog(LocalizationHelper.GetString("StartsWithScript")); + } } - /// - /// 更新任务顺序 - /// UI 绑定的方法 - /// - [UsedImplicitly] - public void CopilotItemIndexChanged() - { - Execute.OnUIThread(() => - { - for (int i = 0; i < CopilotItemViewModels.Count; i++) - { - CopilotItemViewModels[i].Index = i; - } + AddLog(LocalizationHelper.GetString("ConnectingToEmulator")); - SaveCopilotTask(); - }); + string errMsg = string.Empty; + bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + if (!caught) + { + AddLog(errMsg, UiLogColor.Error); + Stop(); + return; } - /// - /// Starts copilot. - /// UI 绑定的方法 - /// - /// Task - [UsedImplicitly] - public async Task Start() + Regex regex = new Regex(@"(?<=;)(?[^,;]+)(?:, *(?\d))?(?=;)", RegexOptions.Compiled); + + var userAdditional = regex.Matches(";" + UserAdditional + ";").ToList().Select(match => new UserAdditional { - /* - if (_form) - { - AddLog(Localization.GetString("AutoSquadTip"), LogColor.Message); - }*/ - _runningState.SetIdle(false); + Name = match.Groups[1].Value.Trim(), + Skill = string.IsNullOrEmpty(match.Groups[2].Value) ? 0 : int.Parse(match.Groups[2].Value), + }); - if (_taskType == AsstTaskType.VideoRecognition) + bool ret = true; + try + { + if (UseCopilotList) { - _ = StartVideoTask(); - return; - } + _copilotIdList.Clear(); - if (_taskType == AsstTaskType.Copilot && UseCopilotList && !await VerifyCopilotListTask()) - { - // 校验作业合法性 - _runningState.SetIdle(true); - return; - } - else if (_taskType == AsstTaskType.Copilot && !UseCopilotList && _copilotCache is null) - { - AddLog(LocalizationHelper.GetString("CopilotEmptyError"), UiLogColor.Error, showTime: false); - _runningState.SetIdle(true); - return; - } - - if (SettingsViewModel.GameSettings.CopilotWithScript) - { - await Task.Run(() => SettingsViewModel.GameSettings.RunScript("StartsWithScript", showLog: false)); - if (!string.IsNullOrWhiteSpace(SettingsViewModel.GameSettings.StartsWithScript)) + var t = CopilotItemViewModels.Where(i => i.IsChecked).Select(i => { - AddLog(LocalizationHelper.GetString("StartsWithScript")); - } - } + _copilotIdList.Add(i.CopilotId); + return new MultiTask { FileName = i.FilePath, IsRaid = i.IsRaid, StageName = i.Name, IsParadox = ActiveTabIndex == 2, }; + }); - AddLog(LocalizationHelper.GetString("ConnectingToEmulator")); - - string errMsg = string.Empty; - bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); - if (!caught) - { - AddLog(errMsg, UiLogColor.Error); - Stop(); - return; - } - - Regex regex = new Regex(@"(?<=;)(?[^,;]+)(?:, *(?\d))?(?=;)", RegexOptions.Compiled); - - var userAdditional = regex.Matches(";" + UserAdditional + ";").ToList().Select(match => new UserAdditional - { - Name = match.Groups[1].Value.Trim(), - Skill = string.IsNullOrEmpty(match.Groups[2].Value) ? 0 : int.Parse(match.Groups[2].Value), - }); - - bool ret = true; - try - { - if (UseCopilotList) + var task = new AsstCopilotTask() { - _copilotIdList.Clear(); + MultiTasks = [.. t], + Formation = _form, + AddTrust = _addTrust, + IgnoreRequirements = _ignoreRequirements, + UserAdditionals = AddUserAdditional ? userAdditional.ToList() : [], + UseSanityPotion = _useSanityPotion, + }; - var t = CopilotItemViewModels.Where(i => i.IsChecked).Select(i => - { - _copilotIdList.Add(i.CopilotId); - return new MultiTask { FileName = i.FilePath, IsRaid = i.IsRaid, StageName = i.Name, IsParadox = ActiveTabIndex == 2, }; - }); - - var task = new AsstCopilotTask() - { - MultiTasks = [.. t], - Formation = _form, - AddTrust = _addTrust, - IgnoreRequirements = _ignoreRequirements, - UserAdditionals = AddUserAdditional ? userAdditional.ToList() : [], - UseSanityPotion = _useSanityPotion, - }; - - ret = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Copilot, task); - ret = ret && Instances.AsstProxy.AsstStart(); - } - else - { - if (IsDataFromWeb) - { - try - { - await File.WriteAllTextAsync(TempCopilotFile, JsonConvert.SerializeObject(_copilotCache, Formatting.Indented)); - } - catch - { - AddLog(LocalizationHelper.GetString("CopilotCouldNotSaveFile") + TempCopilotFile, UiLogColor.Error); - Stop(); - return; - } - } - - var task = new AsstCopilotTask() - { - FileName = IsDataFromWeb ? TempCopilotFile : Filename, - Formation = _form, - AddTrust = _addTrust, - IgnoreRequirements = _ignoreRequirements, - UserAdditionals = AddUserAdditional ? userAdditional.ToList() : [], - LoopTimes = Loop ? LoopTimes : 1, - UseSanityPotion = _useSanityPotion, - FormationIndex = UseFormation ? _formationIndex : 0, - }; - ret = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Copilot, _taskType, task.Serialize().Params); - ret = ret && Instances.AsstProxy.AsstStart(); - } - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to start copilot task"); - AddLog(LocalizationHelper.GetString("CopilotStartError") + ex.Message, UiLogColor.Error, showTime: false); - ret = false; - } - - if (ret) - { - AddLog(LocalizationHelper.GetString("Running")); + ret = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Copilot, task); + ret = ret && Instances.AsstProxy.AsstStart(); } else { - if (!Instances.AsstProxy.AsstStop()) + if (IsDataFromWeb) { - _logger.Warning("Failed to stop Asst"); + try + { + await File.WriteAllTextAsync(TempCopilotFile, JsonConvert.SerializeObject(_copilotCache, Formatting.Indented)); + } + catch + { + AddLog(LocalizationHelper.GetString("CopilotCouldNotSaveFile") + TempCopilotFile, UiLogColor.Error); + Stop(); + return; + } } - _runningState.SetIdle(true); - AddLog(LocalizationHelper.GetString("CopilotFileReadError"), UiLogColor.Error, showTime: false); - } - } - - private bool StartVideoTask() - { - return Instances.AsstProxy.AsstStartVideoRec(Filename); - } - - /// - /// Stops copilot. - /// UI 绑定的方法 - /// - public void Stop() - { - if (SettingsViewModel.GameSettings.CopilotWithScript && SettingsViewModel.GameSettings.ManualStopWithScript) - { - Task.Run(() => SettingsViewModel.GameSettings.RunScript("EndsWithScript", showLog: false)); - if (!string.IsNullOrWhiteSpace(SettingsViewModel.GameSettings.EndsWithScript)) + var task = new AsstCopilotTask() { - Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("EndsWithScript")); - } + FileName = IsDataFromWeb ? TempCopilotFile : Filename, + Formation = _form, + AddTrust = _addTrust, + IgnoreRequirements = _ignoreRequirements, + UserAdditionals = AddUserAdditional ? userAdditional.ToList() : [], + LoopTimes = Loop ? LoopTimes : 1, + UseSanityPotion = _useSanityPotion, + FormationIndex = UseFormation ? _formationIndex : 0, + }; + ret = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Copilot, _taskType, task.Serialize().Params); + ret = ret && Instances.AsstProxy.AsstStart(); } + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to start copilot task"); + AddLog(LocalizationHelper.GetString("CopilotStartError") + ex.Message, UiLogColor.Error, showTime: false); + ret = false; + } + if (ret) + { + AddLog(LocalizationHelper.GetString("Running")); + } + else + { if (!Instances.AsstProxy.AsstStop()) { _logger.Warning("Failed to stop Asst"); } _runningState.SetIdle(true); - } - - private bool _isDataFromWeb; - - private bool IsDataFromWeb - { - get => _isDataFromWeb; - set => SetAndNotify(ref _isDataFromWeb, value); - } - - private int _copilotId; - - private int CopilotId - { - get => _copilotId; - set - { - SetAndNotify(ref _copilotId, value); - CouldLikeWebJson = value > 0; - } - } - - private async Task RateCopilot(int copilotId, bool isLike = true) - { - if (copilotId <= 0 || _recentlyRatedCopilotId.Contains(copilotId)) - { - return PrtsStatus.NotFound; - } - - var result = await RateWebJsonAsync(copilotId, isLike ? "Like" : "Dislike"); - switch (result) - { - case PrtsStatus.Success: - _recentlyRatedCopilotId.Add(copilotId); - AddLog(LocalizationHelper.GetString("ThanksForLikeWebJson"), UiLogColor.Info, showTime: false); - break; - case PrtsStatus.NetworkError: - AddLog(LocalizationHelper.GetString("FailedToLikeWebJson"), UiLogColor.Error, showTime: false); - break; - } - - return result; - } - - private async Task VerifyCopilotListTask() - { - var copilotItemViewModels = CopilotItemViewModels.Where(i => i.IsChecked).ToArray(); - switch (copilotItemViewModels.Length) - { - case 0: - AddLog(LocalizationHelper.GetString("Copilot.StartWithEmptyList"), UiLogColor.Error, showTime: false); - return false; - case 1: - AddLog(LocalizationHelper.GetString("CopilotSingleTaskWarning"), UiLogColor.Warning, showTime: false); - break; // 降级为警告, 有用户炸就派uuu - } - - if (copilotItemViewModels.Any(i => string.IsNullOrEmpty(i.Name?.Trim()))) - { - AddLog(LocalizationHelper.GetString("CopilotTasksWithEmptyName"), UiLogColor.Error, showTime: false); - return false; - } - - var stageNames = copilotItemViewModels.Select(i => i.FilePath).ToHashSet().Select(async path => - { - if (!File.Exists(path)) - { - AddLog(LocalizationHelper.GetString("CopilotNoFound") + path, UiLogColor.Error, showTime: false); - return null; - } - - try - { - var str = await File.ReadAllTextAsync(path); - return JsonConvert.DeserializeObject(str)?.StageName; - } - catch (Exception ex) - { - _logger.Error(ex, "could not read & parse copilot file: {Path}", path); - return null; - } - }); - foreach (var stageName in stageNames) - { - var name = await stageName; - if (!string.IsNullOrEmpty(name) && DataHelper.FindMap(name) is not null) - { - continue; - } - - AddLog(LocalizationHelper.GetString("UnsupportedStages") + $" {name}", UiLogColor.Error, showTime: false); - _ = Task.Run(ResourceUpdater.ResourceUpdateAndReloadAsync); - AchievementTrackerHelper.Instance.Unlock(AchievementIds.MapOutdated); - return false; - } - - return true; - } - - /// - /// 点击后移除界面中元素焦点 - /// - /// 点击事件发送者 - /// 点击事件 - [UsedImplicitly] - public void MouseDown(object sender, MouseButtonEventArgs e) - { - if (sender is not UIElement element) - { - return; - } - - DependencyObject scope = FocusManager.GetFocusScope(element); - FocusManager.SetFocusedElement(scope, element); - Keyboard.ClearFocus(); - } - - /// - /// 回车键点击后移除界面中元素焦点 - /// - /// 点击事件发送者 - /// 点击事件 - [UsedImplicitly] - public void KeyDown(object sender, KeyEventArgs e) - { - if (e.Key != Key.Enter) - { - return; - } - - if (sender is not UIElement element) - { - return; - } - - DependencyObject scope = FocusManager.GetFocusScope(element); - FocusManager.SetFocusedElement(scope, element); - Keyboard.ClearFocus(); + AddLog(LocalizationHelper.GetString("CopilotFileReadError"), UiLogColor.Error, showTime: false); } } + + private bool StartVideoTask() + { + return Instances.AsstProxy.AsstStartVideoRec(Filename); + } + + /// + /// Stops copilot. + /// UI 绑定的方法 + /// + public void Stop() + { + if (SettingsViewModel.GameSettings.CopilotWithScript && SettingsViewModel.GameSettings.ManualStopWithScript) + { + Task.Run(() => SettingsViewModel.GameSettings.RunScript("EndsWithScript", showLog: false)); + if (!string.IsNullOrWhiteSpace(SettingsViewModel.GameSettings.EndsWithScript)) + { + Instances.CopilotViewModel.AddLog(LocalizationHelper.GetString("EndsWithScript")); + } + } + + if (!Instances.AsstProxy.AsstStop()) + { + _logger.Warning("Failed to stop Asst"); + } + + _runningState.SetIdle(true); + } + + private bool _isDataFromWeb; + + private bool IsDataFromWeb + { + get => _isDataFromWeb; + set => SetAndNotify(ref _isDataFromWeb, value); + } + + private int _copilotId; + + private int CopilotId + { + get => _copilotId; + set + { + SetAndNotify(ref _copilotId, value); + CouldLikeWebJson = value > 0; + } + } + + private async Task RateCopilot(int copilotId, bool isLike = true) + { + if (copilotId <= 0 || _recentlyRatedCopilotId.Contains(copilotId)) + { + return PrtsStatus.NotFound; + } + + var result = await RateWebJsonAsync(copilotId, isLike ? "Like" : "Dislike"); + switch (result) + { + case PrtsStatus.Success: + _recentlyRatedCopilotId.Add(copilotId); + AddLog(LocalizationHelper.GetString("ThanksForLikeWebJson"), UiLogColor.Info, showTime: false); + break; + case PrtsStatus.NetworkError: + AddLog(LocalizationHelper.GetString("FailedToLikeWebJson"), UiLogColor.Error, showTime: false); + break; + } + + return result; + } + + private async Task VerifyCopilotListTask() + { + var copilotItemViewModels = CopilotItemViewModels.Where(i => i.IsChecked).ToArray(); + switch (copilotItemViewModels.Length) + { + case 0: + AddLog(LocalizationHelper.GetString("Copilot.StartWithEmptyList"), UiLogColor.Error, showTime: false); + return false; + case 1: + AddLog(LocalizationHelper.GetString("CopilotSingleTaskWarning"), UiLogColor.Warning, showTime: false); + break; // 降级为警告, 有用户炸就派uuu + } + + if (copilotItemViewModels.Any(i => string.IsNullOrEmpty(i.Name?.Trim()))) + { + AddLog(LocalizationHelper.GetString("CopilotTasksWithEmptyName"), UiLogColor.Error, showTime: false); + return false; + } + + var stageNames = copilotItemViewModels.Select(i => i.FilePath).ToHashSet().Select(async path => + { + if (!File.Exists(path)) + { + AddLog(LocalizationHelper.GetString("CopilotNoFound") + path, UiLogColor.Error, showTime: false); + return null; + } + + try + { + var str = await File.ReadAllTextAsync(path); + return JsonConvert.DeserializeObject(str)?.StageName; + } + catch (Exception ex) + { + _logger.Error(ex, "could not read & parse copilot file: {Path}", path); + return null; + } + }); + foreach (var stageName in stageNames) + { + var name = await stageName; + if (!string.IsNullOrEmpty(name) && DataHelper.FindMap(name) is not null) + { + continue; + } + + AddLog(LocalizationHelper.GetString("UnsupportedStages") + $" {name}", UiLogColor.Error, showTime: false); + _ = Task.Run(ResourceUpdater.ResourceUpdateAndReloadAsync); + AchievementTrackerHelper.Instance.Unlock(AchievementIds.MapOutdated); + return false; + } + + return true; + } + + /// + /// 点击后移除界面中元素焦点 + /// + /// 点击事件发送者 + /// 点击事件 + [UsedImplicitly] + public void MouseDown(object sender, MouseButtonEventArgs e) + { + if (sender is not UIElement element) + { + return; + } + + DependencyObject scope = FocusManager.GetFocusScope(element); + FocusManager.SetFocusedElement(scope, element); + Keyboard.ClearFocus(); + } + + /// + /// 回车键点击后移除界面中元素焦点 + /// + /// 点击事件发送者 + /// 点击事件 + [UsedImplicitly] + public void KeyDown(object sender, KeyEventArgs e) + { + if (e.Key != Key.Enter) + { + return; + } + + if (sender is not UIElement element) + { + return; + } + + DependencyObject scope = FocusManager.GetFocusScope(element); + FocusManager.SetFocusedElement(scope, element); + Keyboard.ClearFocus(); + } } diff --git a/src/MaaWpfGui/ViewModels/UI/RootViewModel.cs b/src/MaaWpfGui/ViewModels/UI/RootViewModel.cs index a3f492d08e..bc9c81f8ad 100644 --- a/src/MaaWpfGui/ViewModels/UI/RootViewModel.cs +++ b/src/MaaWpfGui/ViewModels/UI/RootViewModel.cs @@ -31,287 +31,286 @@ using MaaWpfGui.ViewModels.UserControl.Settings; using Microsoft.WindowsAPICodePack.Taskbar; using Stylet; -namespace MaaWpfGui.ViewModels.UI +namespace MaaWpfGui.ViewModels.UI; + +/// +/// The root view model. +/// +public class RootViewModel : Conductor.Collection.OneActive { - /// - /// The root view model. - /// - public class RootViewModel : Conductor.Collection.OneActive + /// + protected override void OnViewLoaded() { - /// - protected override void OnViewLoaded() + // 更新直接重启 + if (Instances.VersionUpdateViewModel.CheckAndUpdateNow()) { - // 更新直接重启 - if (Instances.VersionUpdateViewModel.CheckAndUpdateNow()) - { - Bootstrapper.RestartAfterUpdate(); - return; - } - - InitViewModels(); - _ = InitProxy(); - if (SettingsViewModel.VersionUpdateSettings.VersionType == VersionUpdateSettingsUserControlModel.UpdateVersionType.Nightly && - !SettingsViewModel.VersionUpdateSettings.HasAcknowledgedNightlyWarning) - { - MessageBoxHelper.Show(LocalizationHelper.GetString("NightlyWarning")); - } - - Task.Run(async () => - { - await Instances.AnnouncementViewModel.CheckAndDownloadAnnouncement(); - if (Instances.AnnouncementViewModel.DoNotRemindThisAnnouncementAgain) - { - return; - } - - if (Instances.AnnouncementViewModel.DoNotShowAnnouncement) - { - return; - } - - if (Instances.AnnouncementViewModel.AnnouncementInfo != string.Empty) - { - _ = Execute.OnUIThreadAsync(() => Instances.WindowManager.ShowWindow(Instances.AnnouncementViewModel)); - } - }); - - _ = Instances.VersionUpdateViewModel.ShowUpdateOrDownload(); + Bootstrapper.RestartAfterUpdate(); + return; } - private static async Task InitProxy() + InitViewModels(); + _ = InitProxy(); + if (SettingsViewModel.VersionUpdateSettings.VersionType == VersionUpdateSettingsUserControlModel.UpdateVersionType.Nightly && + !SettingsViewModel.VersionUpdateSettings.HasAcknowledgedNightlyWarning) { - try - { - await Task.Run(Instances.AsstProxy.Init); - } - catch - { - // ignored - } + MessageBoxHelper.Show(LocalizationHelper.GetString("NightlyWarning")); } - private void InitViewModels() + Task.Run(async () => { - Items.Add(Instances.TaskQueueViewModel); - Items.Add(Instances.CopilotViewModel); - Items.Add(Instances.ToolboxViewModel); - Items.Add(Instances.SettingsViewModel); - - Instances.SettingsViewModel.UpdateWindowTitle(); // 在标题栏上显示模拟器和IP端口 必须在 Items.Add(settings)之后执行。 - ActiveItem = Instances.TaskQueueViewModel; - } - - private string _windowTitle = "MAA"; - - /// - /// Gets or sets the window title. - /// - public string WindowTitle - { - get => _windowTitle; - set => SetAndNotify(ref _windowTitle, value); - } - - private string _windowVersionUpdateInfo = string.Empty; - - /// - /// Gets or sets the version update info. - /// - public string WindowVersionUpdateInfo - { - get => _windowVersionUpdateInfo; - set => SetAndNotify(ref _windowVersionUpdateInfo, value); - } - - private string _windowResourceUpdateInfo = string.Empty; - - /// - /// Gets or sets the resource update info. - /// - public string WindowResourceUpdateInfo - { - get => _windowResourceUpdateInfo; - set => SetAndNotify(ref _windowResourceUpdateInfo, value); - } - - private (int Current, int Max)? _taskProgress; - - /// - /// Gets or sets the TaskProgress. - /// 0.0 to 1.0. - /// 置 0 以隐藏进度条. - /// - public (int Current, int Max)? TaskProgress - { - get => _taskProgress; - set - { - SetAndNotify(ref _taskProgress, value); - - Execute.OnUIThreadAsync(() => - { - if (Application.Current.MainWindow == null || !Application.Current.MainWindow.IsVisible) - { - return; - } - - try - { - if (value is null) - { - TaskbarManager.Instance.SetProgressValue(0, 0, Application.Current.MainWindow); - } - else - { - TaskbarManager.Instance.SetProgressValue(value.Value.Current, value.Value.Max, Application.Current.MainWindow); - } - } - catch (Exception e) - { - // 不知道会不会有异常,先捕获一下 - Logger.Warning("TaskbarManager Exception: " + e.Message); - } - }); - } - } - - private bool _windowTitleScrollable = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.WindowTitleScrollable, bool.FalseString)); - - /// - /// Gets or sets a value indicating whether to scroll the window title. - /// - public bool WindowTitleScrollable - { - get => _windowTitleScrollable; - set => SetAndNotify(ref _windowTitleScrollable, value); - } - - private bool _showCloseButton = !Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.HideCloseButton, bool.FalseString)); - - /// - /// Gets or sets a value indicating whether to show close button. - /// - public bool ShowCloseButton - { - get => _showCloseButton; - set => SetAndNotify(ref _showCloseButton, value); - } - - private bool _isWindowTopMost; - - public bool IsWindowTopMost - { - get => _isWindowTopMost; - set - { - if (_isWindowTopMost == value) - { - return; - } - - SetAndNotify(ref _isWindowTopMost, value); - } - } - - // UI 绑定的方法 - [UsedImplicitly] - public void ToggleTopMostCommand() - { - IsWindowTopMost = !IsWindowTopMost; - } - - /// - protected override void OnClose() - { - Bootstrapper.Shutdown(); - } - - private static readonly string[] _gitList = - [ - "/Res/Img/EasterEgg/1.gif", - "/Res/Img/EasterEgg/2.gif", - ]; - - private static int _gifIndex = -1; - - private static string _gifPath = string.Empty; - - public string GifPath - { - get => _gifPath; - set => SetAndNotify(ref _gifPath, value); - } - - private bool _gifVisibility = true; - - public bool GifVisibility - { - get => _gifVisibility; - set => SetAndNotify(ref _gifVisibility, value); - } - - public void ChangeGif() - { - if (++_gifIndex >= _gitList.Length) - { - _gifIndex = 0; - } - - GifPath = _gitList[_gifIndex]; - } - - private static bool _isDragging = false; - private static Point _offset; - - // UI 绑定的方法 - [UsedImplicitly] - public void DraggableElementMouseLeftButtonDown(object sender, MouseButtonEventArgs e) - { - if (sender is not HandyControl.Controls.GifImage childElement) + await Instances.AnnouncementViewModel.CheckAndDownloadAnnouncement(); + if (Instances.AnnouncementViewModel.DoNotRemindThisAnnouncementAgain) { return; } - _isDragging = true; - _offset = e.GetPosition(childElement); - childElement.CaptureMouse(); - } - - // UI 绑定的方法 - [UsedImplicitly] - public void DraggableElementMouseLeftButtonUp(object sender, MouseButtonEventArgs e) - { - if (sender is not HandyControl.Controls.GifImage childElement) + if (Instances.AnnouncementViewModel.DoNotShowAnnouncement) { return; } - _isDragging = false; - childElement.ReleaseMouseCapture(); - } - - // UI 绑定的方法 - [UsedImplicitly] - public void DraggableElementMouseMove(object sender, MouseEventArgs e) - { - if (!_isDragging || sender is not HandyControl.Controls.GifImage { Parent: Grid parentElement } childElement) + if (Instances.AnnouncementViewModel.AnnouncementInfo != string.Empty) { - return; + _ = Execute.OnUIThreadAsync(() => Instances.WindowManager.ShowWindow(Instances.AnnouncementViewModel)); } + }); - Point currentPosition = e.GetPosition(parentElement); + _ = Instances.VersionUpdateViewModel.ShowUpdateOrDownload(); + } - // 计算偏移量 - double newX = currentPosition.X - _offset.X; - double newY = currentPosition.Y - _offset.Y; - - // 确保元素在父元素范围内 - newX = Math.Max(10, Math.Min(newX, parentElement.ActualWidth - childElement.ActualWidth - 10)); - newY = Math.Max(10, Math.Min(newY, parentElement.ActualHeight - childElement.ActualHeight - 10)); - - childElement.HorizontalAlignment = HorizontalAlignment.Left; - childElement.VerticalAlignment = VerticalAlignment.Top; - childElement.Margin = new(newX, newY, 10, 10); + private static async Task InitProxy() + { + try + { + await Task.Run(Instances.AsstProxy.Init); + } + catch + { + // ignored } } + + private void InitViewModels() + { + Items.Add(Instances.TaskQueueViewModel); + Items.Add(Instances.CopilotViewModel); + Items.Add(Instances.ToolboxViewModel); + Items.Add(Instances.SettingsViewModel); + + Instances.SettingsViewModel.UpdateWindowTitle(); // 在标题栏上显示模拟器和IP端口 必须在 Items.Add(settings)之后执行。 + ActiveItem = Instances.TaskQueueViewModel; + } + + private string _windowTitle = "MAA"; + + /// + /// Gets or sets the window title. + /// + public string WindowTitle + { + get => _windowTitle; + set => SetAndNotify(ref _windowTitle, value); + } + + private string _windowVersionUpdateInfo = string.Empty; + + /// + /// Gets or sets the version update info. + /// + public string WindowVersionUpdateInfo + { + get => _windowVersionUpdateInfo; + set => SetAndNotify(ref _windowVersionUpdateInfo, value); + } + + private string _windowResourceUpdateInfo = string.Empty; + + /// + /// Gets or sets the resource update info. + /// + public string WindowResourceUpdateInfo + { + get => _windowResourceUpdateInfo; + set => SetAndNotify(ref _windowResourceUpdateInfo, value); + } + + private (int Current, int Max)? _taskProgress; + + /// + /// Gets or sets the TaskProgress. + /// 0.0 to 1.0. + /// 置 0 以隐藏进度条. + /// + public (int Current, int Max)? TaskProgress + { + get => _taskProgress; + set + { + SetAndNotify(ref _taskProgress, value); + + Execute.OnUIThreadAsync(() => + { + if (Application.Current.MainWindow == null || !Application.Current.MainWindow.IsVisible) + { + return; + } + + try + { + if (value is null) + { + TaskbarManager.Instance.SetProgressValue(0, 0, Application.Current.MainWindow); + } + else + { + TaskbarManager.Instance.SetProgressValue(value.Value.Current, value.Value.Max, Application.Current.MainWindow); + } + } + catch (Exception e) + { + // 不知道会不会有异常,先捕获一下 + Logger.Warning("TaskbarManager Exception: " + e.Message); + } + }); + } + } + + private bool _windowTitleScrollable = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.WindowTitleScrollable, bool.FalseString)); + + /// + /// Gets or sets a value indicating whether to scroll the window title. + /// + public bool WindowTitleScrollable + { + get => _windowTitleScrollable; + set => SetAndNotify(ref _windowTitleScrollable, value); + } + + private bool _showCloseButton = !Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.HideCloseButton, bool.FalseString)); + + /// + /// Gets or sets a value indicating whether to show close button. + /// + public bool ShowCloseButton + { + get => _showCloseButton; + set => SetAndNotify(ref _showCloseButton, value); + } + + private bool _isWindowTopMost; + + public bool IsWindowTopMost + { + get => _isWindowTopMost; + set + { + if (_isWindowTopMost == value) + { + return; + } + + SetAndNotify(ref _isWindowTopMost, value); + } + } + + // UI 绑定的方法 + [UsedImplicitly] + public void ToggleTopMostCommand() + { + IsWindowTopMost = !IsWindowTopMost; + } + + /// + protected override void OnClose() + { + Bootstrapper.Shutdown(); + } + + private static readonly string[] _gitList = + [ + "/Res/Img/EasterEgg/1.gif", + "/Res/Img/EasterEgg/2.gif", + ]; + + private static int _gifIndex = -1; + + private static string _gifPath = string.Empty; + + public string GifPath + { + get => _gifPath; + set => SetAndNotify(ref _gifPath, value); + } + + private bool _gifVisibility = true; + + public bool GifVisibility + { + get => _gifVisibility; + set => SetAndNotify(ref _gifVisibility, value); + } + + public void ChangeGif() + { + if (++_gifIndex >= _gitList.Length) + { + _gifIndex = 0; + } + + GifPath = _gitList[_gifIndex]; + } + + private static bool _isDragging = false; + private static Point _offset; + + // UI 绑定的方法 + [UsedImplicitly] + public void DraggableElementMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (sender is not HandyControl.Controls.GifImage childElement) + { + return; + } + + _isDragging = true; + _offset = e.GetPosition(childElement); + childElement.CaptureMouse(); + } + + // UI 绑定的方法 + [UsedImplicitly] + public void DraggableElementMouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (sender is not HandyControl.Controls.GifImage childElement) + { + return; + } + + _isDragging = false; + childElement.ReleaseMouseCapture(); + } + + // UI 绑定的方法 + [UsedImplicitly] + public void DraggableElementMouseMove(object sender, MouseEventArgs e) + { + if (!_isDragging || sender is not HandyControl.Controls.GifImage { Parent: Grid parentElement } childElement) + { + return; + } + + Point currentPosition = e.GetPosition(parentElement); + + // 计算偏移量 + double newX = currentPosition.X - _offset.X; + double newY = currentPosition.Y - _offset.Y; + + // 确保元素在父元素范围内 + newX = Math.Max(10, Math.Min(newX, parentElement.ActualWidth - childElement.ActualWidth - 10)); + newY = Math.Max(10, Math.Min(newY, parentElement.ActualHeight - childElement.ActualHeight - 10)); + + childElement.HorizontalAlignment = HorizontalAlignment.Left; + childElement.VerticalAlignment = VerticalAlignment.Top; + childElement.Margin = new(newX, newY, 10, 10); + } } diff --git a/src/MaaWpfGui/ViewModels/UI/SettingsViewModel.cs b/src/MaaWpfGui/ViewModels/UI/SettingsViewModel.cs index d82d030cf6..4560d8d3e1 100644 --- a/src/MaaWpfGui/ViewModels/UI/SettingsViewModel.cs +++ b/src/MaaWpfGui/ViewModels/UI/SettingsViewModel.cs @@ -17,10 +17,8 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; -using System.ComponentModel; using System.Globalization; using System.Linq; -using System.Printing; using System.Threading.Tasks; using System.Windows; using HandyControl.Controls; @@ -35,932 +33,930 @@ using MaaWpfGui.Services.HotKeys; using MaaWpfGui.States; using MaaWpfGui.Utilities.ValueType; using MaaWpfGui.ViewModels.UserControl.Settings; -using MaaWpfGui.Views.UI; using Newtonsoft.Json; using Serilog; using Stylet; using ComboBox = System.Windows.Controls.ComboBox; using Timer = System.Timers.Timer; -namespace MaaWpfGui.ViewModels.UI +namespace MaaWpfGui.ViewModels.UI; + +/// +/// The view model of settings. +/// +public class SettingsViewModel : Screen { + private readonly RunningState _runningState; + + private static readonly ILogger _logger = Log.ForContext(); + /// - /// The view model of settings. + /// Gets the visibility of task setting views. /// - public class SettingsViewModel : Screen + public TaskSettingVisibilityInfo TaskSettingVisibilities { get; } = TaskSettingVisibilityInfo.Instance; + + #region 设置界面 Model + + /// + /// Gets 游戏设置 model + /// + public static GameSettingsUserControlModel GameSettings { get; } = GameSettingsUserControlModel.Instance; + + /// + /// Gets 连接设置 model + /// + public static ConnectSettingsUserControlModel ConnectSettings { get; } = ConnectSettingsUserControlModel.Instance; + + /// + /// Gets 启动设置 model + /// + public static StartSettingsUserControlModel StartSettings { get; } = StartSettingsUserControlModel.Instance; + + /// + /// Gets 界面设置 model + /// + public static GuiSettingsUserControlModel GuiSettings { get; } = GuiSettingsUserControlModel.Instance; + + /// + /// Gets 背景设置 model + /// + public static BackgroundSettingsUserControlModel BackgroundSettings { get; } = BackgroundSettingsUserControlModel.Instance; + + /// + /// Gets 定时设置 model + /// + public static TimerSettingsUserControlModel TimerSettings { get; } = TimerSettingsUserControlModel.Instance; + + /// + /// Gets 远程控制 model + /// + public static RemoteControlUserControlModel RemoteControlSettings { get; } = RemoteControlUserControlModel.Instance; + + /// + /// Gets 软件更新 model + /// + public static VersionUpdateSettingsUserControlModel VersionUpdateSettings { get; } = VersionUpdateSettingsUserControlModel.Instance; + + /// + /// Gets 外部通知 model + /// + public static ExternalNotificationSettingsUserControlModel ExternalNotificationSettings { get; } = ExternalNotificationSettingsUserControlModel.Instance; + + /// + /// Gets 性能设置 model + /// + public static PerformanceUserControlModel PerformanceSettings { get; } = PerformanceUserControlModel.Instance; + + /// + /// Gets 问题反馈 model + /// + public static IssueReportUserControlModel IssueReportSettings { get; } = IssueReportUserControlModel.Instance; + + /// + /// Gets 成就 model + /// + public static AchievementSettingsUserControlModel AchievementSettings { get; } = AchievementSettingsUserControlModel.Instance; + + #endregion 设置界面 Model + + /// + /// Initializes a new instance of the class. + /// + public SettingsViewModel() { - private readonly RunningState _runningState; + DisplayName = LocalizationHelper.GetString("Settings"); - private static readonly ILogger _logger = Log.ForContext(); + Init(); - /// - /// Gets the visibility of task setting views. - /// - public TaskSettingVisibilityInfo TaskSettingVisibilities { get; } = TaskSettingVisibilityInfo.Instance; + HangoverEnd(); - #region 设置界面 Model - - /// - /// Gets 游戏设置 model - /// - public static GameSettingsUserControlModel GameSettings { get; } = GameSettingsUserControlModel.Instance; - - /// - /// Gets 连接设置 model - /// - public static ConnectSettingsUserControlModel ConnectSettings { get; } = ConnectSettingsUserControlModel.Instance; - - /// - /// Gets 启动设置 model - /// - public static StartSettingsUserControlModel StartSettings { get; } = StartSettingsUserControlModel.Instance; - - /// - /// Gets 界面设置 model - /// - public static GuiSettingsUserControlModel GuiSettings { get; } = GuiSettingsUserControlModel.Instance; - - /// - /// Gets 背景设置 model - /// - public static BackgroundSettingsUserControlModel BackgroundSettings { get; } = BackgroundSettingsUserControlModel.Instance; - - /// - /// Gets 定时设置 model - /// - public static TimerSettingsUserControlModel TimerSettings { get; } = TimerSettingsUserControlModel.Instance; - - /// - /// Gets 远程控制 model - /// - public static RemoteControlUserControlModel RemoteControlSettings { get; } = RemoteControlUserControlModel.Instance; - - /// - /// Gets 软件更新 model - /// - public static VersionUpdateSettingsUserControlModel VersionUpdateSettings { get; } = VersionUpdateSettingsUserControlModel.Instance; - - /// - /// Gets 外部通知 model - /// - public static ExternalNotificationSettingsUserControlModel ExternalNotificationSettings { get; } = ExternalNotificationSettingsUserControlModel.Instance; - - /// - /// Gets 性能设置 model - /// - public static PerformanceUserControlModel PerformanceSettings { get; } = PerformanceUserControlModel.Instance; - - /// - /// Gets 问题反馈 model - /// - public static IssueReportUserControlModel IssueReportSettings { get; } = IssueReportUserControlModel.Instance; - - /// - /// Gets 成就 model - /// - public static AchievementSettingsUserControlModel AchievementSettings { get; } = AchievementSettingsUserControlModel.Instance; - - #endregion 设置界面 Model - - /// - /// Initializes a new instance of the class. - /// - public SettingsViewModel() + _runningState = RunningState.Instance; + _runningState.StateChanged += (_, e) => { - DisplayName = LocalizationHelper.GetString("Settings"); + Idle = e.Idle; - Init(); + // Inited = e.Inited; + // Stopping = e.Stopping; + }; + } - HangoverEnd(); + #region Init - _runningState = RunningState.Instance; - _runningState.StateChanged += (_, e) => + private bool _idle; + + /// + /// Gets or sets a value indicating whether it is idle. + /// + public bool Idle + { + get => _idle; + set => SetAndNotify(ref _idle, value); + } + + private void Init() + { + InitSettings(); + TaskQueueViewModel.InfrastTask.InitInfrast(); + TaskQueueViewModel.RoguelikeTask.InitRoguelike(); + InitConfiguration(); + InitUiSettings(); + InitConnectConfig(); + InitVersionUpdate(); + } + + public SettingItemViewModel GetSettingItemByKey(string key) + { + return Settings.First(s => s.Key == key); + } + + public SettingItemViewModel SwitchConfigurationSetting => GetSettingItemByKey("SwitchConfiguration"); + + public SettingItemViewModel ScheduleSettingsSetting => GetSettingItemByKey("ScheduleSettings"); + + public SettingItemViewModel PerformanceSettingsSetting => GetSettingItemByKey("PerformanceSettings"); + + public SettingItemViewModel GameSettingsSetting => GetSettingItemByKey("GameSettings"); + + public SettingItemViewModel ConnectionSettingsSetting => GetSettingItemByKey("ConnectionSettings"); + + public SettingItemViewModel StartupSettingsSetting => GetSettingItemByKey("StartupSettings"); + + public SettingItemViewModel RemoteControlSettingsSetting => GetSettingItemByKey("RemoteControlSettings"); + + public SettingItemViewModel UiSettingsSetting => GetSettingItemByKey("UiSettings"); + + public SettingItemViewModel BackgroundSettingsSetting => GetSettingItemByKey("BackgroundSettings"); + + public SettingItemViewModel ExternalNotificationSettingsSetting => GetSettingItemByKey("ExternalNotificationSettings"); + + public SettingItemViewModel HotKeySettingsSetting => GetSettingItemByKey("HotKeySettings"); + + public SettingItemViewModel AchievementSettingsSetting => GetSettingItemByKey("AchievementSettings"); + + public SettingItemViewModel UpdateSettingsSetting => GetSettingItemByKey("UpdateSettings"); + + public SettingItemViewModel IssueReportSetting => GetSettingItemByKey("IssueReport"); + + public SettingItemViewModel AboutUsSetting => GetSettingItemByKey("AboutUs"); + + private void InitSettings() + { + List keyList = + [ + "SwitchConfiguration", + "ScheduleSettings", + "PerformanceSettings", + "GameSettings", + "ConnectionSettings", + "StartupSettings", + "RemoteControlSettings", + "UiSettings", + "BackgroundSettings", + "ExternalNotificationSettings", + "HotKeySettings", + "AchievementSettings", + "UpdateSettings", + "IssueReport", + "AboutUs", + ]; + + var tempOrderList = new List(new SettingItemViewModel[keyList.Count]); + var nonOrderList = new List(); + + foreach (var key in keyList) + { + int order = ConfigurationHelper.GetSettingOrder(key, -1); + + var item = new SettingItemViewModel(key, LocalizationHelper.GetString(key), -1); + + if (order < 0 || order >= tempOrderList.Count || tempOrderList[order] != null) { - Idle = e.Idle; - - // Inited = e.Inited; - // Stopping = e.Stopping; - }; - } - - #region Init - - private bool _idle; - - /// - /// Gets or sets a value indicating whether it is idle. - /// - public bool Idle - { - get => _idle; - set => SetAndNotify(ref _idle, value); - } - - private void Init() - { - InitSettings(); - TaskQueueViewModel.InfrastTask.InitInfrast(); - TaskQueueViewModel.RoguelikeTask.InitRoguelike(); - InitConfiguration(); - InitUiSettings(); - InitConnectConfig(); - InitVersionUpdate(); - } - - public SettingItemViewModel GetSettingItemByKey(string key) - { - return Settings.First(s => s.Key == key); - } - - public SettingItemViewModel SwitchConfigurationSetting => GetSettingItemByKey("SwitchConfiguration"); - - public SettingItemViewModel ScheduleSettingsSetting => GetSettingItemByKey("ScheduleSettings"); - - public SettingItemViewModel PerformanceSettingsSetting => GetSettingItemByKey("PerformanceSettings"); - - public SettingItemViewModel GameSettingsSetting => GetSettingItemByKey("GameSettings"); - - public SettingItemViewModel ConnectionSettingsSetting => GetSettingItemByKey("ConnectionSettings"); - - public SettingItemViewModel StartupSettingsSetting => GetSettingItemByKey("StartupSettings"); - - public SettingItemViewModel RemoteControlSettingsSetting => GetSettingItemByKey("RemoteControlSettings"); - - public SettingItemViewModel UiSettingsSetting => GetSettingItemByKey("UiSettings"); - - public SettingItemViewModel BackgroundSettingsSetting => GetSettingItemByKey("BackgroundSettings"); - - public SettingItemViewModel ExternalNotificationSettingsSetting => GetSettingItemByKey("ExternalNotificationSettings"); - - public SettingItemViewModel HotKeySettingsSetting => GetSettingItemByKey("HotKeySettings"); - - public SettingItemViewModel AchievementSettingsSetting => GetSettingItemByKey("AchievementSettings"); - - public SettingItemViewModel UpdateSettingsSetting => GetSettingItemByKey("UpdateSettings"); - - public SettingItemViewModel IssueReportSetting => GetSettingItemByKey("IssueReport"); - - public SettingItemViewModel AboutUsSetting => GetSettingItemByKey("AboutUs"); - - private void InitSettings() - { - List keyList = - [ - "SwitchConfiguration", - "ScheduleSettings", - "PerformanceSettings", - "GameSettings", - "ConnectionSettings", - "StartupSettings", - "RemoteControlSettings", - "UiSettings", - "BackgroundSettings", - "ExternalNotificationSettings", - "HotKeySettings", - "AchievementSettings", - "UpdateSettings", - "IssueReport", - "AboutUs", - ]; - - var tempOrderList = new List(new SettingItemViewModel[keyList.Count]); - var nonOrderList = new List(); - - foreach (var key in keyList) + nonOrderList.Add(item); + } + else { - int order = ConfigurationHelper.GetSettingOrder(key, -1); + item.Value = order; + tempOrderList[order] = item; + } + } - var item = new SettingItemViewModel(key, LocalizationHelper.GetString(key), -1); - - if (order < 0 || order >= tempOrderList.Count || tempOrderList[order] != null) - { - nonOrderList.Add(item); - } - else - { - item.Value = order; - tempOrderList[order] = item; - } + int fillIndex = 0; + foreach (var item in nonOrderList.OfType()) + { + while (fillIndex < tempOrderList.Count && tempOrderList[fillIndex] != null) + { + fillIndex++; } - int fillIndex = 0; - foreach (var item in nonOrderList.OfType()) + if (fillIndex < tempOrderList.Count) { - while (fillIndex < tempOrderList.Count && tempOrderList[fillIndex] != null) + item.Value = fillIndex; + tempOrderList[fillIndex] = item; + ConfigurationHelper.SetSettingOrder(item.Key, fillIndex); + } + } + + Settings = [.. tempOrderList.OfType()]; + + Settings.CollectionChanged += Settings_CollectionChanged; + } + + private void Settings_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs? e) + { + Execute.OnUIThread(() => + { + for (int i = 0; i < Settings.Count; i++) + { + var item = Settings[i]; + if (item.Value == i) { - fillIndex++; + continue; } - if (fillIndex < tempOrderList.Count) - { - item.Value = fillIndex; - tempOrderList[fillIndex] = item; - ConfigurationHelper.SetSettingOrder(item.Key, fillIndex); - } + item.Value = i; + ConfigurationHelper.SetSettingOrder(item.Key, i); } - Settings = [.. tempOrderList.OfType()]; + OnSettingItemValueChanged(); + }); + } - Settings.CollectionChanged += Settings_CollectionChanged; + private void OnSettingItemValueChanged() + { + Application.Current.Dispatcher.InvokeAsync(() => + { + RefreshDividerOffsetsRequested?.Invoke(this, EventArgs.Empty); + }, System.Windows.Threading.DispatcherPriority.Loaded); + } + + public event EventHandler? RefreshDividerOffsetsRequested; + + private ObservableCollection _settings = []; + + public ObservableCollection Settings + { + get => _settings; + set => SetAndNotify(ref _settings, value); + } + + private void InitConfiguration() + { + var configurations = new ObservableCollection(); + foreach (var conf in ConfigurationHelper.GetConfigurationList()) + { + configurations.Add(new CombinedData { Display = conf, Value = conf }); } - private void Settings_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs? e) + ConfigurationList = configurations; + } + + private void InitUiSettings() + { + var languageList = (from pair in LocalizationHelper.SupportedLanguages + where pair.Key != PallasLangKey || Cheers + select new CombinedData { Display = pair.Value, Value = pair.Key }) + .ToList(); + + GuiSettings.LanguageList = languageList; + GuiSettings.SwitchDarkMode(); + } + + private void InitConnectConfig() + { + var addressListJson = ConfigurationHelper.GetValue(ConfigurationKeys.AddressHistory, string.Empty); + if (!string.IsNullOrEmpty(addressListJson)) { - Execute.OnUIThread(() => - { - for (int i = 0; i < Settings.Count; i++) - { - var item = Settings[i]; - if (item.Value == i) - { - continue; - } - - item.Value = i; - ConfigurationHelper.SetSettingOrder(item.Key, i); - } - - OnSettingItemValueChanged(); - }); + ConnectSettings.ConnectAddressHistory = JsonConvert.DeserializeObject>(addressListJson) ?? []; } + } - private void OnSettingItemValueChanged() + private void InitVersionUpdate() + { + if (VersionUpdateSettings is { VersionType: VersionUpdateSettingsUserControlModel.UpdateVersionType.Nightly, AllowNightlyUpdates: false }) { - Application.Current.Dispatcher.InvokeAsync(() => - { - RefreshDividerOffsetsRequested?.Invoke(this, EventArgs.Empty); - }, System.Windows.Threading.DispatcherPriority.Loaded); + VersionUpdateSettings.VersionType = VersionUpdateSettingsUserControlModel.UpdateVersionType.Beta; } + } - public event EventHandler? RefreshDividerOffsetsRequested; + #endregion Init - private ObservableCollection _settings = []; + #region EasterEggs - public ObservableCollection Settings + /// + /// The Pallas language key. + /// + public const string PallasLangKey = "pallas"; + + private bool _cheers = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Cheers, bool.FalseString)); + + /// + /// Gets or sets a value indicating whether to cheer. + /// + public bool Cheers + { + get => _cheers; + set { - get => _settings; - set => SetAndNotify(ref _settings, value); - } - - private void InitConfiguration() - { - var configurations = new ObservableCollection(); - foreach (var conf in ConfigurationHelper.GetConfigurationList()) - { - configurations.Add(new CombinedData { Display = conf, Value = conf }); - } - - ConfigurationList = configurations; - } - - private void InitUiSettings() - { - var languageList = (from pair in LocalizationHelper.SupportedLanguages - where pair.Key != PallasLangKey || Cheers - select new CombinedData { Display = pair.Value, Value = pair.Key }) - .ToList(); - - GuiSettings.LanguageList = languageList; - GuiSettings.SwitchDarkMode(); - } - - private void InitConnectConfig() - { - var addressListJson = ConfigurationHelper.GetValue(ConfigurationKeys.AddressHistory, string.Empty); - if (!string.IsNullOrEmpty(addressListJson)) - { - ConnectSettings.ConnectAddressHistory = JsonConvert.DeserializeObject>(addressListJson) ?? []; - } - } - - private void InitVersionUpdate() - { - if (VersionUpdateSettings is { VersionType: VersionUpdateSettingsUserControlModel.UpdateVersionType.Nightly, AllowNightlyUpdates: false }) - { - VersionUpdateSettings.VersionType = VersionUpdateSettingsUserControlModel.UpdateVersionType.Beta; - } - } - - #endregion Init - - #region EasterEggs - - /// - /// The Pallas language key. - /// - public const string PallasLangKey = "pallas"; - - private bool _cheers = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Cheers, bool.FalseString)); - - /// - /// Gets or sets a value indicating whether to cheer. - /// - public bool Cheers - { - get => _cheers; - set - { - if (_cheers == value) - { - return; - } - - SetAndNotify(ref _cheers, value); - ConfigurationHelper.SetGlobalValue(ConfigurationKeys.Cheers, value.ToString()); - if (_cheers) - { - ConfigurationHelper.SetGlobalValue(ConfigurationKeys.Localization, PallasLangKey); - } - } - } - - private bool _hangover = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Hangover, bool.FalseString)); - - /// - /// Gets or sets a value indicating whether to hangover. - /// - public bool Hangover - { - get => _hangover; - set - { - SetAndNotify(ref _hangover, value); - ConfigurationHelper.SetGlobalValue(ConfigurationKeys.Hangover, value.ToString()); - } - } - - private string _lastBuyWineTime = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.LastBuyWineTime, DateTime.UtcNow.ToYjDate().AddDays(-1).ToFormattedString()); - - public string LastBuyWineTime - { - get => _lastBuyWineTime; - set - { - SetAndNotify(ref _lastBuyWineTime, value); - ConfigurationHelper.SetGlobalValue(ConfigurationKeys.LastBuyWineTime, value); - } - } - - public void HangoverEnd() - { - if (!Hangover) + if (_cheers == value) { return; } - Hangover = false; - MessageBoxHelper.Show( - LocalizationHelper.GetString("Hangover"), - LocalizationHelper.GetString("Burping"), - iconKey: "HangoverGeometry", - iconBrushKey: "PallasBrush"); + SetAndNotify(ref _cheers, value); + ConfigurationHelper.SetGlobalValue(ConfigurationKeys.Cheers, value.ToString()); + if (_cheers) + { + ConfigurationHelper.SetGlobalValue(ConfigurationKeys.Localization, PallasLangKey); + } + } + } + + private bool _hangover = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Hangover, bool.FalseString)); + + /// + /// Gets or sets a value indicating whether to hangover. + /// + public bool Hangover + { + get => _hangover; + set + { + SetAndNotify(ref _hangover, value); + ConfigurationHelper.SetGlobalValue(ConfigurationKeys.Hangover, value.ToString()); + } + } + + private string _lastBuyWineTime = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.LastBuyWineTime, DateTime.UtcNow.ToYjDate().AddDays(-1).ToFormattedString()); + + public string LastBuyWineTime + { + get => _lastBuyWineTime; + set + { + SetAndNotify(ref _lastBuyWineTime, value); + ConfigurationHelper.SetGlobalValue(ConfigurationKeys.LastBuyWineTime, value); + } + } + + public void HangoverEnd() + { + if (!Hangover) + { + return; + } + + Hangover = false; + MessageBoxHelper.Show( + LocalizationHelper.GetString("Hangover"), + LocalizationHelper.GetString("Burping"), + iconKey: "HangoverGeometry", + iconBrushKey: "PallasBrush"); + Bootstrapper.ShutdownAndRestartWithoutArgs(); + } + + public void Sober() + { + if (!Cheers || GuiSettings.Language != PallasLangKey) + { + return; + } + + ConfigurationHelper.SetGlobalValue(ConfigurationKeys.Localization, SoberLanguage); + Hangover = true; + Cheers = false; + } + + private string _soberLanguage = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.SoberLanguage, LocalizationHelper.DefaultLanguage); + + public string SoberLanguage + { + get => _soberLanguage; + set + { + SetAndNotify(ref _soberLanguage, value); + ConfigurationHelper.SetGlobalValue(ConfigurationKeys.SoberLanguage, value); + } + } + + /// + /// Did you buy wine? + /// + /// The answer. + public bool DidYouBuyWine() + { + var now = DateTime.UtcNow.ToYjDate(); + if (now == DateTime.ParseExact(LastBuyWineTime.Replace('-', '/'), "yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture)) + { + return false; + } + + // if (now.IsAprilFoolsDay()) + // { + // return true; + // } + string[] wineList = ["酒", "liquor", "drink", "wine", "beer", "술", "🍷", "🍸", "🍺", "🍻", "🥃", "🍶"]; + return wineList.Any(TaskQueueViewModel.MallTask.CreditFirstList.Contains); + } + + #endregion EasterEggs + + #region HotKey + + /// + /// Gets or sets the hotkey: ShowGui. + /// + public static MaaHotKey? HotKeyShowGui + { + get => Instances.MaaHotKeyManager.GetOrNull(MaaHotKeyAction.ShowGui); + set => SetHotKey(MaaHotKeyAction.ShowGui, value); + } + + /// + /// Gets or sets the hotkey: LinkStart. + /// + public static MaaHotKey? HotKeyLinkStart + { + get => Instances.MaaHotKeyManager.GetOrNull(MaaHotKeyAction.LinkStart); + set => SetHotKey(MaaHotKeyAction.LinkStart, value); + } + + private static void SetHotKey(MaaHotKeyAction action, MaaHotKey? value) + { + if (value != null) + { + Instances.MaaHotKeyManager.TryRegister(action, value); + } + else + { + Instances.MaaHotKeyManager.UnRegister(action); + } + } + + #endregion HotKey + + #region 配置 + + public ObservableCollection ConfigurationList { get; set; } = []; + + private string? _currentConfiguration = ConfigurationHelper.GetCurrentConfiguration(); + + public string? CurrentConfiguration + { + get => _currentConfiguration; + set + { + SetAndNotify(ref _currentConfiguration, value); + ConfigurationHelper.SwitchConfiguration(value); + Bootstrapper.ShutdownAndRestartWithoutArgs(); } + } - public void Sober() + private string _newConfigurationName = string.Empty; + + public string NewConfigurationName + { + get => _newConfigurationName; + set => SetAndNotify(ref _newConfigurationName, value); + } + + // UI 绑定的方法 + [UsedImplicitly] + public void AddConfiguration() + { + if (string.IsNullOrEmpty(NewConfigurationName)) { - if (!Cheers || GuiSettings.Language != PallasLangKey) + NewConfigurationName = DateTime.Now.ToString("yy/MM/dd HH:mm:ss"); + } + + if (ConfigurationHelper.AddConfiguration(NewConfigurationName, CurrentConfiguration)) + { + ConfigurationList.Add(new CombinedData { Display = NewConfigurationName, Value = NewConfigurationName }); + + var growlInfo = new GrowlInfo + { + IsCustom = true, + Message = string.Format(LocalizationHelper.GetString("AddConfigSuccess"), NewConfigurationName), + IconKey = "HangoverGeometry", + IconBrushKey = "PallasBrush", + }; + Growl.Info(growlInfo); + } + else + { + var growlInfo = new GrowlInfo + { + IsCustom = true, + Message = string.Format(LocalizationHelper.GetString("ConfigExists"), NewConfigurationName), + IconKey = "HangoverGeometry", + IconBrushKey = "PallasBrush", + }; + Growl.Info(growlInfo); + } + } + + // UI 绑定的方法 + [UsedImplicitly] + public void DeleteConfiguration(CombinedData delete) + { + if (ConfigurationHelper.DeleteConfiguration(delete.Display)) + { + ConfigurationList.Remove(delete); + } + } + + #endregion 配置 + + #region SettingsGuide + + public static int GuideMaxStep => 6; + + private int _guideStepIndex = Convert.ToInt32(ConfigurationHelper.GetValue(ConfigurationKeys.GuideStepIndex, "0")); + + public int GuideStepIndex + { + get => _guideStepIndex; + set + { + SetAndNotify(ref _guideStepIndex, value); + ConfigurationHelper.SetValue(ConfigurationKeys.GuideStepIndex, value.ToString()); + } + } + + private string _guideTransitionMode = "Bottom2Top"; + + public string GuideTransitionMode + { + get => _guideTransitionMode; + set => SetAndNotify(ref _guideTransitionMode, value); + } + + // UI 绑定的方法 + [UsedImplicitly] + public void NextGuide(StepBar stepBar) + { + GuideTransitionMode = "Bottom2Top"; + stepBar.Next(); + } + + // UI 绑定的方法 + [UsedImplicitly] + public void PrevGuide(StepBar stepBar) + { + GuideTransitionMode = "Top2Bottom"; + stepBar.Prev(); + } + + // UI 绑定的方法 + [UsedImplicitly] + public void DoneGuide() + { + TaskSettingVisibilities.Guide = false; + GuideStepIndex = GuideMaxStep; + } + + #endregion SettingsGuide + + #region 设置页面列表和滚动视图联动绑定 + + private enum NotifyType + { + None, + SelectedIndex, + ScrollOffset, + } + + private NotifyType _notifySource = NotifyType.None; + + private Timer? _resetNotifyTimer; + + private void ResetNotifySource() + { + if (_resetNotifyTimer != null) + { + _resetNotifyTimer.Stop(); + _resetNotifyTimer.Close(); + } + + _resetNotifyTimer = new Timer(20); + _resetNotifyTimer.Elapsed += (_, _) => + { + _notifySource = NotifyType.None; + }; + _resetNotifyTimer.AutoReset = false; + _resetNotifyTimer.Enabled = true; + _resetNotifyTimer.Start(); + } + + /// + /// Gets or sets the height of scroll viewport. + /// + public double ScrollViewportHeight { get; set; } + + /// + /// Gets or sets the extent height of scroll. + /// + public double ScrollExtentHeight { get; set; } + + private List _dividerVerticalOffsetList = []; + + /// + /// Gets or sets the list of divider vertical offset. + /// + public List DividerVerticalOffsetList + { + get => _dividerVerticalOffsetList; + set + { + if (_dividerVerticalOffsetList == value) { return; } - ConfigurationHelper.SetGlobalValue(ConfigurationKeys.Localization, SoberLanguage); - Hangover = true; - Cheers = false; + _dividerVerticalOffsetList = value; + SetAndNotify(ref _dividerVerticalOffsetList, value); } + } - private string _soberLanguage = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.SoberLanguage, LocalizationHelper.DefaultLanguage); + private int _selectedIndex; - public string SoberLanguage + /// + /// Gets or sets the index selected. + /// + public int SelectedIndex + { + get => _selectedIndex; + set { - get => _soberLanguage; - set + if (_selectedIndex == value) { - SetAndNotify(ref _soberLanguage, value); - ConfigurationHelper.SetGlobalValue(ConfigurationKeys.SoberLanguage, value); - } - } - - /// - /// Did you buy wine? - /// - /// The answer. - public bool DidYouBuyWine() - { - var now = DateTime.UtcNow.ToYjDate(); - if (now == DateTime.ParseExact(LastBuyWineTime.Replace('-', '/'), "yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture)) - { - return false; + return; } - // if (now.IsAprilFoolsDay()) - // { - // return true; - // } - string[] wineList = ["酒", "liquor", "drink", "wine", "beer", "술", "🍷", "🍸", "🍺", "🍻", "🥃", "🍶"]; - return wineList.Any(TaskQueueViewModel.MallTask.CreditFirstList.Contains); - } - - #endregion EasterEggs - - #region HotKey - - /// - /// Gets or sets the hotkey: ShowGui. - /// - public static MaaHotKey? HotKeyShowGui - { - get => Instances.MaaHotKeyManager.GetOrNull(MaaHotKeyAction.ShowGui); - set => SetHotKey(MaaHotKeyAction.ShowGui, value); - } - - /// - /// Gets or sets the hotkey: LinkStart. - /// - public static MaaHotKey? HotKeyLinkStart - { - get => Instances.MaaHotKeyManager.GetOrNull(MaaHotKeyAction.LinkStart); - set => SetHotKey(MaaHotKeyAction.LinkStart, value); - } - - private static void SetHotKey(MaaHotKeyAction action, MaaHotKey? value) - { - if (value != null) + if (value < 0 || value > DividerVerticalOffsetList.Count) { - Instances.MaaHotKeyManager.TryRegister(action, value); - } - else - { - Instances.MaaHotKeyManager.UnRegister(action); - } - } - - #endregion HotKey - - #region 配置 - - public ObservableCollection ConfigurationList { get; set; } = []; - - private string? _currentConfiguration = ConfigurationHelper.GetCurrentConfiguration(); - - public string? CurrentConfiguration - { - get => _currentConfiguration; - set - { - SetAndNotify(ref _currentConfiguration, value); - ConfigurationHelper.SwitchConfiguration(value); - - Bootstrapper.ShutdownAndRestartWithoutArgs(); - } - } - - private string _newConfigurationName = string.Empty; - - public string NewConfigurationName - { - get => _newConfigurationName; - set => SetAndNotify(ref _newConfigurationName, value); - } - - // UI 绑定的方法 - [UsedImplicitly] - public void AddConfiguration() - { - if (string.IsNullOrEmpty(NewConfigurationName)) - { - NewConfigurationName = DateTime.Now.ToString("yy/MM/dd HH:mm:ss"); + return; } - if (ConfigurationHelper.AddConfiguration(NewConfigurationName, CurrentConfiguration)) + switch (_notifySource) { - ConfigurationList.Add(new CombinedData { Display = NewConfigurationName, Value = NewConfigurationName }); + case NotifyType.None: + _notifySource = NotifyType.SelectedIndex; + SetAndNotify(ref _selectedIndex, value); - var growlInfo = new GrowlInfo - { - IsCustom = true, - Message = string.Format(LocalizationHelper.GetString("AddConfigSuccess"), NewConfigurationName), - IconKey = "HangoverGeometry", - IconBrushKey = "PallasBrush", - }; - Growl.Info(growlInfo); - } - else - { - var growlInfo = new GrowlInfo - { - IsCustom = true, - Message = string.Format(LocalizationHelper.GetString("ConfigExists"), NewConfigurationName), - IconKey = "HangoverGeometry", - IconBrushKey = "PallasBrush", - }; - Growl.Info(growlInfo); + ScrollOffset = DividerVerticalOffsetList[value]; + + ResetNotifySource(); + break; + + case NotifyType.ScrollOffset: + SetAndNotify(ref _selectedIndex, value); + break; + + case NotifyType.SelectedIndex: + break; + + default: + throw new ArgumentOutOfRangeException(); } } + } - // UI 绑定的方法 - [UsedImplicitly] - public void DeleteConfiguration(CombinedData delete) + private double _scrollOffset; + + /// + /// Gets or sets the scroll offset. + /// + public double ScrollOffset + { + get => _scrollOffset; + set { - if (ConfigurationHelper.DeleteConfiguration(delete.Display)) + if (!AllowScrollOffsetChange) { - ConfigurationList.Remove(delete); - } - } - - #endregion 配置 - - #region SettingsGuide - - public static int GuideMaxStep => 6; - - private int _guideStepIndex = Convert.ToInt32(ConfigurationHelper.GetValue(ConfigurationKeys.GuideStepIndex, "0")); - - public int GuideStepIndex - { - get => _guideStepIndex; - set - { - SetAndNotify(ref _guideStepIndex, value); - ConfigurationHelper.SetValue(ConfigurationKeys.GuideStepIndex, value.ToString()); - } - } - - private string _guideTransitionMode = "Bottom2Top"; - - public string GuideTransitionMode - { - get => _guideTransitionMode; - set => SetAndNotify(ref _guideTransitionMode, value); - } - - // UI 绑定的方法 - [UsedImplicitly] - public void NextGuide(StepBar stepBar) - { - GuideTransitionMode = "Bottom2Top"; - stepBar.Next(); - } - - // UI 绑定的方法 - [UsedImplicitly] - public void PrevGuide(StepBar stepBar) - { - GuideTransitionMode = "Top2Bottom"; - stepBar.Prev(); - } - - // UI 绑定的方法 - [UsedImplicitly] - public void DoneGuide() - { - TaskSettingVisibilities.Guide = false; - GuideStepIndex = GuideMaxStep; - } - - #endregion SettingsGuide - - #region 设置页面列表和滚动视图联动绑定 - - private enum NotifyType - { - None, - SelectedIndex, - ScrollOffset, - } - - private NotifyType _notifySource = NotifyType.None; - - private Timer? _resetNotifyTimer; - - private void ResetNotifySource() - { - if (_resetNotifyTimer != null) - { - _resetNotifyTimer.Stop(); - _resetNotifyTimer.Close(); + return; } - _resetNotifyTimer = new Timer(20); - _resetNotifyTimer.Elapsed += (_, _) => + switch (_notifySource) { - _notifySource = NotifyType.None; - }; - _resetNotifyTimer.AutoReset = false; - _resetNotifyTimer.Enabled = true; - _resetNotifyTimer.Start(); - } + case NotifyType.None: + _notifySource = NotifyType.ScrollOffset; + SetAndNotify(ref _scrollOffset, value); - /// - /// Gets or sets the height of scroll viewport. - /// - public double ScrollViewportHeight { get; set; } - - /// - /// Gets or sets the extent height of scroll. - /// - public double ScrollExtentHeight { get; set; } - - private List _dividerVerticalOffsetList = []; - - /// - /// Gets or sets the list of divider vertical offset. - /// - public List DividerVerticalOffsetList - { - get => _dividerVerticalOffsetList; - set - { - if (_dividerVerticalOffsetList == value) - { - return; - } - - _dividerVerticalOffsetList = value; - SetAndNotify(ref _dividerVerticalOffsetList, value); - } - } - - private int _selectedIndex; - - /// - /// Gets or sets the index selected. - /// - public int SelectedIndex - { - get => _selectedIndex; - set - { - if (_selectedIndex == value) - { - return; - } - - if (value < 0 || value > DividerVerticalOffsetList.Count) - { - return; - } - - switch (_notifySource) - { - case NotifyType.None: - _notifySource = NotifyType.SelectedIndex; - SetAndNotify(ref _selectedIndex, value); - - ScrollOffset = DividerVerticalOffsetList[value]; - - ResetNotifySource(); - break; - - case NotifyType.ScrollOffset: - SetAndNotify(ref _selectedIndex, value); - break; - - case NotifyType.SelectedIndex: - break; - - default: - throw new ArgumentOutOfRangeException(); - } - } - } - - private double _scrollOffset; - - /// - /// Gets or sets the scroll offset. - /// - public double ScrollOffset - { - get => _scrollOffset; - set - { - if (!AllowScrollOffsetChange) - { - return; - } - - switch (_notifySource) - { - case NotifyType.None: - _notifySource = NotifyType.ScrollOffset; - SetAndNotify(ref _scrollOffset, value); - - // 设置 ListBox SelectedIndex 为当前 ScrollOffset 索引 - if (DividerVerticalOffsetList.Count > 0) + // 设置 ListBox SelectedIndex 为当前 ScrollOffset 索引 + if (DividerVerticalOffsetList.Count > 0) + { + // 滚动条滚动到底部,返回最后一个 Divider 索引 + if (value + ScrollViewportHeight >= ScrollExtentHeight) { - // 滚动条滚动到底部,返回最后一个 Divider 索引 - if (value + ScrollViewportHeight >= ScrollExtentHeight) - { - SelectedIndex = DividerVerticalOffsetList.Count - 1; - ResetNotifySource(); - break; - } - - // 根据出当前 ScrollOffset 选出最后一个在可视范围的 Divider 索引 - var dividerSelect = DividerVerticalOffsetList.Select((n, i) => ( - dividerAppeared: value >= n, - index: i)); - - var index = dividerSelect.LastOrDefault(n => n.dividerAppeared).index; - SelectedIndex = index; + SelectedIndex = DividerVerticalOffsetList.Count - 1; + ResetNotifySource(); + break; } - ResetNotifySource(); - break; + // 根据出当前 ScrollOffset 选出最后一个在可视范围的 Divider 索引 + var dividerSelect = DividerVerticalOffsetList.Select((n, i) => ( + dividerAppeared: value >= n, + index: i)); - case NotifyType.SelectedIndex: - SetAndNotify(ref _scrollOffset, value); - break; - - case NotifyType.ScrollOffset: - break; - - default: - throw new ArgumentOutOfRangeException(); - } - } - } - - public bool AllowScrollOffsetChange { get; set; } = true; - - #endregion 设置页面列表和滚动视图联动绑定 - - /// - /// Requires the user to restart to apply settings. - /// - /// Whether to include the YostarEN resolution tip. - public static void AskRestartToApplySettings(bool isYostarEN = false) - { - var resolutionTip = isYostarEN ? "\n" + LocalizationHelper.GetString("SwitchResolutionTip") : string.Empty; - - var result = MessageBoxHelper.Show( - LocalizationHelper.GetString("PromptRestartForSettingsChange") + resolutionTip, - LocalizationHelper.GetString("Tip"), - MessageBoxButton.OKCancel, - MessageBoxImage.Question); - - if (result == MessageBoxResult.OK) - { - Bootstrapper.ShutdownAndRestartWithoutArgs(); - } - } - - /// - /// Make comboBox searchable - /// UI 绑定的方法 - /// - /// Event sender - /// Event args - [UsedImplicitly] - public static void MakeComboBoxSearchable(object sender, EventArgs e) - { - (sender as ComboBox)?.MakeComboBoxSearchable(); - } - - private bool _isCheckingAnnouncement = false; - - public bool IsCheckingAnnouncement - { - get => _isCheckingAnnouncement; - set - { - SetAndNotify(ref _isCheckingAnnouncement, value); - } - } - - // UI 绑定的方法 - [UsedImplicitly] - public async Task CheckAndDownloadAnnouncement() - { - if (IsCheckingAnnouncement) - { - return; - } - - IsCheckingAnnouncement = true; - - try - { - if (Instances.AnnouncementViewModel.View is System.Windows.Window window) - { - if (window.WindowState == WindowState.Minimized) - { - window.WindowState = WindowState.Normal; + var index = dividerSelect.LastOrDefault(n => n.dividerAppeared).index; + SelectedIndex = index; } - window.Activate(); - } - else - { - Instances.WindowManager.ShowWindow(Instances.AnnouncementViewModel); - } + ResetNotifySource(); + break; - await Instances.AnnouncementViewModel.CheckAndDownloadAnnouncement(); - } - finally - { - IsCheckingAnnouncement = false; + case NotifyType.SelectedIndex: + SetAndNotify(ref _scrollOffset, value); + break; + + case NotifyType.ScrollOffset: + break; + + default: + throw new ArgumentOutOfRangeException(); } } - - /// - /// 标题栏显示模拟器名称和IP端口。 - /// - public void UpdateWindowTitle() - { - var rvm = (RootViewModel)this.Parent; - - var newVersionFoundInfo = VersionUpdateSettings.NewVersionFoundInfo; - var uiVersion = VersionUpdateSettingsUserControlModel.UiVersion; - var startupUpdateCheck = VersionUpdateSettings.StartupUpdateCheck; - var isDebug = Instances.VersionUpdateViewModel.IsDebugVersion(); - - if (newVersionFoundInfo != uiVersion && !isDebug && !string.IsNullOrEmpty(newVersionFoundInfo) && startupUpdateCheck) - { - rvm.WindowVersionUpdateInfo = $"{newVersionFoundInfo}".Trim(); - } - - rvm.WindowResourceUpdateInfo = VersionUpdateSettings.NewResourceFoundInfo; - - string prefix = ConfigurationHelper.GetValue(ConfigurationKeys.WindowTitlePrefix, string.Empty); - if (!string.IsNullOrEmpty(prefix)) - { - prefix += " - "; - } - - List windowTitleSelectShowList = GuiSettings.WindowTitleSelectShowList - .Cast>().Select(pair => pair.Key) - .ToList(); - - string currentConfiguration = string.Empty; - string connectConfigName = string.Empty; - string connectAddress = string.Empty; - string clientName = string.Empty; - - foreach (var select in windowTitleSelectShowList) - { - switch (select) - { - case "1": // 配置名 - currentConfiguration = $" ({CurrentConfiguration})"; - break; - - case "2": // 连接模式 - foreach (var data in ConnectSettings.ConnectConfigList.Where(data => data.Value == ConnectSettings.ConnectConfig)) - { - connectConfigName = $" - {data.Display}"; - } - - break; - - case "3": // 端口地址 - connectAddress = $" ({ConnectSettings.ConnectAddress})"; - break; - - case "4": // 客户端类型 - clientName = $" - {ClientName}"; - break; - } - } - - string resourceVersionDisplay = !string.IsNullOrEmpty(VersionUpdateSettings.ResourceVersion) - ? $" - {LocalizationHelper.FormatVersion(VersionUpdateSettings.ResourceVersion, VersionUpdateSettings.ResourceDateTime)}" - : string.Empty; - string uiVersionDisplay = LocalizationHelper.FormatVersion(uiVersion, VersionUpdateSettingsUserControlModel.BuildDateTime); - rvm.WindowTitle = $"{prefix}MAA{currentConfiguration} - {uiVersionDisplay}{resourceVersionDisplay}{connectConfigName}{connectAddress}{clientName}"; - } - - /// - /// Gets the client type. - /// - private string ClientName - { - get - { - foreach (var item in GameSettings.ClientTypeList.Where(item => item.Value == GameSettings.ClientType)) - { - return item.Display; - } - - return "Unknown Client"; - } - } - - private static readonly Dictionary _serverMapping = new() - { - { string.Empty, "CN" }, - { "Official", "CN" }, - { "Bilibili", "CN" }, - { "YoStarEN", "US" }, - { "YoStarJP", "JP" }, - { "YoStarKR", "KR" }, - { "txwy", "ZH_TW" }, - }; - - /// - /// Gets the server type. - /// - public string ServerType => _serverMapping[GameSettings.ClientType]; } + + public bool AllowScrollOffsetChange { get; set; } = true; + + #endregion 设置页面列表和滚动视图联动绑定 + + /// + /// Requires the user to restart to apply settings. + /// + /// Whether to include the YostarEN resolution tip. + public static void AskRestartToApplySettings(bool isYostarEN = false) + { + var resolutionTip = isYostarEN ? "\n" + LocalizationHelper.GetString("SwitchResolutionTip") : string.Empty; + + var result = MessageBoxHelper.Show( + LocalizationHelper.GetString("PromptRestartForSettingsChange") + resolutionTip, + LocalizationHelper.GetString("Tip"), + MessageBoxButton.OKCancel, + MessageBoxImage.Question); + + if (result == MessageBoxResult.OK) + { + Bootstrapper.ShutdownAndRestartWithoutArgs(); + } + } + + /// + /// Make comboBox searchable + /// UI 绑定的方法 + /// + /// Event sender + /// Event args + [UsedImplicitly] + public static void MakeComboBoxSearchable(object sender, EventArgs e) + { + (sender as ComboBox)?.MakeComboBoxSearchable(); + } + + private bool _isCheckingAnnouncement = false; + + public bool IsCheckingAnnouncement + { + get => _isCheckingAnnouncement; + set + { + SetAndNotify(ref _isCheckingAnnouncement, value); + } + } + + // UI 绑定的方法 + [UsedImplicitly] + public async Task CheckAndDownloadAnnouncement() + { + if (IsCheckingAnnouncement) + { + return; + } + + IsCheckingAnnouncement = true; + + try + { + if (Instances.AnnouncementViewModel.View is System.Windows.Window window) + { + if (window.WindowState == WindowState.Minimized) + { + window.WindowState = WindowState.Normal; + } + + window.Activate(); + } + else + { + Instances.WindowManager.ShowWindow(Instances.AnnouncementViewModel); + } + + await Instances.AnnouncementViewModel.CheckAndDownloadAnnouncement(); + } + finally + { + IsCheckingAnnouncement = false; + } + } + + /// + /// 标题栏显示模拟器名称和IP端口。 + /// + public void UpdateWindowTitle() + { + var rvm = (RootViewModel)this.Parent; + + var newVersionFoundInfo = VersionUpdateSettings.NewVersionFoundInfo; + var uiVersion = VersionUpdateSettingsUserControlModel.UiVersion; + var startupUpdateCheck = VersionUpdateSettings.StartupUpdateCheck; + var isDebug = Instances.VersionUpdateViewModel.IsDebugVersion(); + + if (newVersionFoundInfo != uiVersion && !isDebug && !string.IsNullOrEmpty(newVersionFoundInfo) && startupUpdateCheck) + { + rvm.WindowVersionUpdateInfo = $"{newVersionFoundInfo}".Trim(); + } + + rvm.WindowResourceUpdateInfo = VersionUpdateSettings.NewResourceFoundInfo; + + string prefix = ConfigurationHelper.GetValue(ConfigurationKeys.WindowTitlePrefix, string.Empty); + if (!string.IsNullOrEmpty(prefix)) + { + prefix += " - "; + } + + List windowTitleSelectShowList = GuiSettings.WindowTitleSelectShowList + .Cast>().Select(pair => pair.Key) + .ToList(); + + string currentConfiguration = string.Empty; + string connectConfigName = string.Empty; + string connectAddress = string.Empty; + string clientName = string.Empty; + + foreach (var select in windowTitleSelectShowList) + { + switch (select) + { + case "1": // 配置名 + currentConfiguration = $" ({CurrentConfiguration})"; + break; + + case "2": // 连接模式 + foreach (var data in ConnectSettings.ConnectConfigList.Where(data => data.Value == ConnectSettings.ConnectConfig)) + { + connectConfigName = $" - {data.Display}"; + } + + break; + + case "3": // 端口地址 + connectAddress = $" ({ConnectSettings.ConnectAddress})"; + break; + + case "4": // 客户端类型 + clientName = $" - {ClientName}"; + break; + } + } + + string resourceVersionDisplay = !string.IsNullOrEmpty(VersionUpdateSettings.ResourceVersion) + ? $" - {LocalizationHelper.FormatVersion(VersionUpdateSettings.ResourceVersion, VersionUpdateSettings.ResourceDateTime)}" + : string.Empty; + string uiVersionDisplay = LocalizationHelper.FormatVersion(uiVersion, VersionUpdateSettingsUserControlModel.BuildDateTime); + rvm.WindowTitle = $"{prefix}MAA{currentConfiguration} - {uiVersionDisplay}{resourceVersionDisplay}{connectConfigName}{connectAddress}{clientName}"; + } + + /// + /// Gets the client type. + /// + private string ClientName + { + get + { + foreach (var item in GameSettings.ClientTypeList.Where(item => item.Value == GameSettings.ClientType)) + { + return item.Display; + } + + return "Unknown Client"; + } + } + + private static readonly Dictionary _serverMapping = new() + { + { string.Empty, "CN" }, + { "Official", "CN" }, + { "Bilibili", "CN" }, + { "YoStarEN", "US" }, + { "YoStarJP", "JP" }, + { "YoStarKR", "KR" }, + { "txwy", "ZH_TW" }, + }; + + /// + /// Gets the server type. + /// + public string ServerType => _serverMapping[GameSettings.ClientType]; } diff --git a/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs b/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs index 637f33fc02..7eaa264915 100644 --- a/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs +++ b/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs @@ -44,1564 +44,1518 @@ using Application = System.Windows.Application; using Screen = Stylet.Screen; using Task = System.Threading.Tasks.Task; -namespace MaaWpfGui.ViewModels.UI +namespace MaaWpfGui.ViewModels.UI; + +/// +/// The view model of task queue. +/// +// 通过 container.Get(); 实例化或获取实例 +// ReSharper disable once ClassNeverInstantiated.Global +public class TaskQueueViewModel : Screen { + private readonly RunningState _runningState; + + private static readonly ILogger _logger = Log.ForContext(); + /// - /// The view model of task queue. + /// Gets or private sets the view models of task items. /// - // 通过 container.Get(); 实例化或获取实例 - // ReSharper disable once ClassNeverInstantiated.Global - public class TaskQueueViewModel : Screen + public ObservableCollection TaskItemViewModels { get; private set; } = []; + + /// + /// Gets the visibility of task setting views. + /// + public static TaskSettingVisibilityInfo TaskSettingVisibilities => TaskSettingVisibilityInfo.Instance; + + public static SettingsViewModel TaskSettingDataContext => Instances.SettingsViewModel; + + /// + /// Gets the after action setting. + /// + public PostActionSetting PostActionSetting { get; } = PostActionSetting.Instance; + + #region 长草任务Model + + /// + /// Gets 连接任务Model + /// + public static StartUpSettingsUserControlModel StartUpTask => StartUpSettingsUserControlModel.Instance; + + /// + /// Gets 战斗任务Model + /// + public static FightSettingsUserControlModel FightTask => FightSettingsUserControlModel.Instance; + + /// + /// Gets 招募任务Model + /// + public static RecruitSettingsUserControlModel RecruitTask => RecruitSettingsUserControlModel.Instance; + + /// + /// Gets 信用及购物任务Model + /// + public static MallSettingsUserControlModel MallTask => MallSettingsUserControlModel.Instance; + + /// + /// Gets 基建任务Model + /// + public static InfrastSettingsUserControlModel InfrastTask => InfrastSettingsUserControlModel.Instance; + + /// + /// Gets 领取奖励任务 + /// + public static AwardSettingsUserControlModel AwardTask => AwardSettingsUserControlModel.Instance; + + /// + /// Gets 肉鸽任务Model + /// + public static RoguelikeSettingsUserControlModel RoguelikeTask => RoguelikeSettingsUserControlModel.Instance; + + /// + /// Gets 生稀盐酸任务Model + /// + public static ReclamationSettingsUserControlModel ReclamationTask => ReclamationSettingsUserControlModel.Instance; + + /// + /// Gets 生稀盐酸任务Model + /// + public static CustomSettingsUserControlModel CustomTask => CustomSettingsUserControlModel.Instance; + + #endregion 长草任务Model + + private static readonly IEnumerable _taskViewModelTypes = InitTaskViewModelList(); + + /// + /// 实时更新任务顺序 + /// + /// ignored object + /// ignored NotifyCollectionChangedEventArgs + public void TaskItemSelectionChanged(object? sender = null, NotifyCollectionChangedEventArgs? e = null) { - private readonly RunningState _runningState; - - private static readonly ILogger _logger = Log.ForContext(); - - /// - /// Gets or private sets the view models of task items. - /// - public ObservableCollection TaskItemViewModels { get; private set; } = []; - - /// - /// Gets the visibility of task setting views. - /// - public static TaskSettingVisibilityInfo TaskSettingVisibilities => TaskSettingVisibilityInfo.Instance; - - public static SettingsViewModel TaskSettingDataContext => Instances.SettingsViewModel; - - /// - /// Gets the after action setting. - /// - public PostActionSetting PostActionSetting { get; } = PostActionSetting.Instance; - - #region 长草任务Model - - /// - /// Gets 连接任务Model - /// - public static StartUpSettingsUserControlModel StartUpTask => StartUpSettingsUserControlModel.Instance; - - /// - /// Gets 战斗任务Model - /// - public static FightSettingsUserControlModel FightTask => FightSettingsUserControlModel.Instance; - - /// - /// Gets 招募任务Model - /// - public static RecruitSettingsUserControlModel RecruitTask => RecruitSettingsUserControlModel.Instance; - - /// - /// Gets 信用及购物任务Model - /// - public static MallSettingsUserControlModel MallTask => MallSettingsUserControlModel.Instance; - - /// - /// Gets 基建任务Model - /// - public static InfrastSettingsUserControlModel InfrastTask => InfrastSettingsUserControlModel.Instance; - - /// - /// Gets 领取奖励任务 - /// - public static AwardSettingsUserControlModel AwardTask => AwardSettingsUserControlModel.Instance; - - /// - /// Gets 肉鸽任务Model - /// - public static RoguelikeSettingsUserControlModel RoguelikeTask => RoguelikeSettingsUserControlModel.Instance; - - /// - /// Gets 生稀盐酸任务Model - /// - public static ReclamationSettingsUserControlModel ReclamationTask => ReclamationSettingsUserControlModel.Instance; - - /// - /// Gets 生稀盐酸任务Model - /// - public static CustomSettingsUserControlModel CustomTask => CustomSettingsUserControlModel.Instance; - - #endregion 长草任务Model - - private static readonly IEnumerable _taskViewModelTypes = InitTaskViewModelList(); - - /// - /// 实时更新任务顺序 - /// - /// ignored object - /// ignored NotifyCollectionChangedEventArgs - public void TaskItemSelectionChanged(object? sender = null, NotifyCollectionChangedEventArgs? e = null) - { - _ = (sender, e); - Execute.OnUIThread(() => - { - int index = 0; - foreach (var item in TaskItemViewModels) - { - ConfigurationHelper.SetTaskOrder(item.OriginalName, index.ToString()); - ++index; - } - }); - } - - /// - /// Gets or private sets the view models of log items. - /// - public ObservableCollection LogItemViewModels { get; private set; } = []; - - #region ActionAfterTasks - - private bool _enableAfterActionSetting; - - /// - /// Gets or sets a value indicating whether to show after task queue actions - /// - public bool EnableAfterActionSetting - { - get => _enableAfterActionSetting; - set - { - SetAndNotify(ref _enableAfterActionSetting, value); - TaskSettingVisibilityInfo.Instance.Set("AfterAction", value); - } - } - - /// - /// Checks after completion. - /// - /// Task - public async Task CheckAfterCompleted() - { - await Task.Run(() => SettingsViewModel.GameSettings.RunScript("EndsWithScript")); - var actions = PostActionSetting; - _logger.Information("Post actions: " + actions.ActionDescription); - - if (actions.BackToAndroidHome) - { - Instances.AsstProxy.AsstBackToHome(); - await Task.Delay(1000); - } - - if (actions.ExitArknights) - { - var clientType = SettingsViewModel.GameSettings.ClientType; - if (!Instances.AsstProxy.AsstStartCloseDown(clientType)) - { - AddLog(LocalizationHelper.GetString("CloseArknightsFailed"), UiLogColor.Error); - } - - await Task.Delay(1000); - } - - if (actions.ExitEmulator) - { - DoKillEmulator(); - await Task.Delay(1000); - } - - if (actions.ExitSelf && !(actions.Hibernate || actions.Shutdown || actions.Sleep)) - { - Bootstrapper.Shutdown(); - } - - if (actions.Hibernate) - { - if (actions.IfNoOtherMaa && HasOtherMaa()) - { - Bootstrapper.Shutdown(); - } - else - { - await DoHibernate(); - } - } - - if (actions.Shutdown) - { - if (actions.IfNoOtherMaa && HasOtherMaa()) - { - Bootstrapper.Shutdown(); - } - else - { - await DoShutDown(); - } - } - - if (actions.Sleep) - { - if (actions.IfNoOtherMaa && HasOtherMaa()) - { - Bootstrapper.Shutdown(); - } - else - { - await DoSleep(); - } - } - - if (actions.ExitSelf) - { - Bootstrapper.Shutdown(); - } - - actions.LoadPostActions(); - return; - - bool HasOtherMaa() - { - var processesCount = Process.GetProcessesByName("MAA").Length; - _logger.Information("MAA processes count: {ProcessesCount}", processesCount); - return processesCount > 1; - } - - void DoKillEmulator() - { - if (!EmulatorHelper.KillEmulatorModeSwitcher()) - { - AddLog(LocalizationHelper.GetString("ExitEmulatorFailed"), UiLogColor.Error); - } - } - - async Task DoHibernate() - { - actions.LoadPostActions(); - - // 休眠提示 - AddLog(LocalizationHelper.GetString("HibernatePrompt"), UiLogColor.Error); - await Task.Delay(10000); - PowerManagement.Hibernate(); - } - - async Task DoShutDown() - { - PowerManagement.Shutdown(); - - await Execute.OnUIThreadAsync(() => Instances.MainWindowManager?.Show()); - if (await TimerCanceledAsync( - LocalizationHelper.GetString("Shutdown"), - LocalizationHelper.GetString("AboutToShutdown"), - LocalizationHelper.GetString("Cancel"), - 60)) - { - PowerManagement.AbortShutdown(); - return; - } - - _logger.Information("Shutdown not canceled, proceeding to exit application."); - Bootstrapper.Shutdown(); - } - - async Task DoSleep() - { - actions.LoadPostActions(); - - // 休眠提示 - AddLog(LocalizationHelper.GetString("SleepPrompt"), UiLogColor.Error); - await Task.Delay(10000); - PowerManagement.Sleep(); - } - } - - #endregion - - /// - /// Initializes a new instance of the class. - /// - public TaskQueueViewModel() - { - _runningState = RunningState.Instance; - _runningState.StateChanged += (_, e) => - { - Idle = e.Idle; - Inited = e.Inited; - Stopping = e.Stopping; - - Instances.SettingsViewModel.Idle = e.Idle; - if (!e.Idle) - { - Instances.Data.ClearCache(); - } - }; - _runningState.TimeoutOccurred += RunningState_TimeOut; - } - - private void RunningState_TimeOut(object? sender, string message) - { - Execute.OnUIThread(() => - { - AddLog(message, UiLogColor.Warning); - ToastNotification.ShowDirect(message); - if (!SettingsViewModel.ExternalNotificationSettings.ExternalNotificationSendWhenTimeout) - { - return; - } - - var lastLogs = LogItemViewModels - .TakeLast(5) - .Aggregate(string.Empty, (current, logItem) => current + $"[{logItem.Time}][{logItem.Color}]{logItem.Content}\n"); - ExternalNotificationService.Send(message, lastLogs); - }); - } - - protected override void OnInitialActivate() - { - base.OnInitialActivate(); - - DisplayName = LocalizationHelper.GetString("Farming"); - LogItemViewModels = []; - InitializeItems(); - InitTimer(); - - _ = UpdateDatePromptAndStagesWeb(); - } - - /* - public void ShowButton() - { - Visible = Visibility.Visible; - Hibernate = true; - } - - private Visibility _visible = Visibility.Collapsed; - - public Visibility Visible - { - get => _visible; - set => SetAndNotify(ref _visible, value); - } - */ - - public bool Running { get; set; } - - public bool Closing { get; set; } - - private readonly System.Timers.Timer _timer = new(); - - public bool ConfirmExit() - { - if (Closing) - { - return false; - } - - Closing = true; - if (Application.Current.IsShuttingDown()) - { - // allow close if application is shutting down - return true; - } - - if (!Running) - { - // no need to confirm if no running task - return true; - } - - var result = MessageBoxHelper.Show( - LocalizationHelper.GetString("ConfirmExitText"), - LocalizationHelper.GetString("ConfirmExitTitle"), - MessageBoxButton.YesNo, - MessageBoxImage.Question); - Closing = false; - return result == MessageBoxResult.Yes; - } - - public override Task CanCloseAsync() - { - return Task.FromResult(this.ConfirmExit()); - } - - private void InitTimer() - { - _timer.Interval = 30 * 1000; - _timer.Elapsed += Timer1_Elapsed; - _timer.Start(); - } - - private DateTime _lastTimerElapsed = DateTime.MinValue; - - private async void Timer1_Elapsed(object? sender, EventArgs e) - { - try - { - // 提前记录时间,避免等待超过定时时间 - DateTime currentTime = DateTime.Now; - currentTime = new(currentTime.Year, currentTime.Month, currentTime.Day, currentTime.Hour, currentTime.Minute, 0); - - if (currentTime <= _lastTimerElapsed) - { - return; - } - - _lastTimerElapsed = currentTime; - - VersionUpdateSettingsUserControlModel.Instance.RefreshMirrorChyanCdkRemaining(); - HandleDatePromptUpdate(); - HandleCheckForUpdates(); - - InfrastTask.RefreshCustomInfrastPlanIndexByPeriod(); - - await HandleTimerLogic(currentTime); - } - catch - { - // ignored - } - } - - private static int CalculateRandomDelay() - { - Random random = new Random(); - int delayTime = random.Next(0, 60 * 60 * 1000); - return delayTime; - } - - private bool _isUpdatingDatePrompt; - - private void HandleDatePromptUpdate() - { - if (!NeedToUpdateDatePrompt() || _isUpdatingDatePrompt) - { - return; - } - - _isUpdatingDatePrompt = true; - UpdateDatePromptAndStagesLocally(); - - var delayTime = CalculateRandomDelay(); - _ = Task.Run(async () => - { - await Task.Delay(delayTime); - await _runningState.UntilIdleAsync(60000); - await UpdateDatePromptAndStagesWeb(); - _isUpdatingDatePrompt = false; - }); - } - - private bool _isCheckingForUpdates; - - private void HandleCheckForUpdates() - { - if (!SettingsViewModel.VersionUpdateSettings.UpdateAutoCheck) - { - return; - } - - if (!NeedToCheckForUpdates() || _isCheckingForUpdates) - { - return; - } - - _isCheckingForUpdates = true; - var delayTime = CalculateRandomDelay(); - _ = Task.Run(async () => - { - _logger.Information("waiting for update check: {DelayTime}", delayTime); - await Task.Delay(delayTime); - await Instances.VersionUpdateViewModel.VersionUpdateAndAskToRestartAsync(); - await ResourceUpdater.ResourceUpdateAndReloadAsync(); - - _isCheckingForUpdates = false; - }); - } - - private static (bool _timeToStart, bool _timeToChangeConfig, int _configIndex) CheckTimers(DateTime currentTime) - { - bool timeToStart = false; - bool timeToChangeConfig = false; - int configIndex = 0; - - for (int i = 0; i < 8; ++i) - { - if (SettingsViewModel.TimerSettings.TimerModels.Timers[i].IsOn == false) - { - continue; - } - - DateTime startTime = new DateTime(currentTime.Year, - currentTime.Month, - currentTime.Day, - SettingsViewModel.TimerSettings.TimerModels.Timers[i].Hour, - SettingsViewModel.TimerSettings.TimerModels.Timers[i].Min, - 0); - DateTime restartDateTime = startTime.AddMinutes(-2); - - // 确保0点的定时会在当日的23:58重启 - if (restartDateTime.Day != startTime.Day) - { - restartDateTime = restartDateTime.AddDays(1); - } - - if (currentTime == restartDateTime && - Instances.SettingsViewModel.CurrentConfiguration != SettingsViewModel.TimerSettings.TimerModels.Timers[i].TimerConfig) - { - timeToChangeConfig = true; - configIndex = i; - break; - } - - // ReSharper disable once InvertIf - if (currentTime == startTime) - { - timeToStart = true; - configIndex = i; - break; - } - } - - return (timeToStart, timeToChangeConfig, configIndex); - } - - private async Task HandleTimerLogic(DateTime currentTime) - { - if (!_runningState.GetIdle() && !SettingsViewModel.TimerSettings.ForceScheduledStart) - { - return; - } - - var (timeToStart, timeToChangeConfig, configIndex) = CheckTimers(currentTime); - - if (timeToChangeConfig) - { - _logger.Information("Scheduled configuration change: Timer Index: {ConfigIndex}", configIndex); - HandleConfigChange(configIndex); - return; - } - - if (timeToStart) - { - _logger.Information("Scheduled start: Timer Index: {ConfigIndex}", configIndex); - await HandleScheduledStart(configIndex); - - SettingsViewModel.TimerSettings.TimerModels.Timers[configIndex].IsOn ??= false; - } - } - - private void HandleConfigChange(int configIndex) - { - if (SettingsViewModel.TimerSettings.CustomConfig && - (_runningState.GetIdle() || SettingsViewModel.TimerSettings.ForceScheduledStart)) - { - Instances.SettingsViewModel.CurrentConfiguration = SettingsViewModel.TimerSettings.TimerModels.Timers[configIndex].TimerConfig; - } - } - - private async Task HandleScheduledStart(int configIndex) - { - if (SettingsViewModel.TimerSettings.ForceScheduledStart) - { - if (SettingsViewModel.TimerSettings.CustomConfig && - Instances.SettingsViewModel.CurrentConfiguration != SettingsViewModel.TimerSettings.TimerModels.Timers[configIndex].TimerConfig) - { - _logger.Warning( - "Scheduled start skipped: Custom configuration is enabled, but the current configuration does not match the scheduled timer configuration (Timer Index: {ConfigIndex}). Current Configuration: {CurrentConfiguration}, Scheduled Configuration: {TimerConfig}", - configIndex, - Instances.SettingsViewModel.CurrentConfiguration, - SettingsViewModel.TimerSettings.TimerModels.Timers[configIndex].TimerConfig); - return; - } - - if (SettingsViewModel.TimerSettings.ShowWindowBeforeForceScheduledStart) - { - await Execute.OnUIThreadAsync(() => Instances.MainWindowManager?.Show()); - } - - if (await TimerCanceledAsync( - LocalizationHelper.GetString("ForceScheduledStart"), - LocalizationHelper.GetString("ForceScheduledStartTip"), - LocalizationHelper.GetString("Cancel"))) - { - return; - } - - if (!_runningState.GetIdle()) - { - _logger.Information("Not idle, Stop and CloseDown"); - await Stop(); - SetStopped(); - } - - var mode = SettingsViewModel.GameSettings.ClientType; - if (!Instances.AsstProxy.AsstAppendCloseDown(mode)) - { - AddLog(LocalizationHelper.GetString("CloseArknightsFailed"), UiLogColor.Error); - } - - ResetAllTemporaryVariable(); - InfrastTask.RefreshCustomInfrastPlanIndexByPeriod(); - } - - await LinkStart(); - - AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.ScheduleMasterGroup); - } - - private static async Task TimerCanceledAsync(string content = "", string tipContent = "", string buttonContent = "", int seconds = 10) - { - if (Application.Current.Dispatcher.CheckAccess()) - { - return await ShowDialogAsync(); - } - - return await await Application.Current.Dispatcher.InvokeAsync(ShowDialogAsync); - - async Task ShowDialogAsync() - { - var canceled = false; - var delay = TimeSpan.FromSeconds(seconds); - var dialogUserControl = new Views.UserControl.TextDialogWithTimerUserControl( - content, - tipContent, - buttonContent, - delay.TotalMilliseconds); - var dialog = HandyControl.Controls.Dialog.Show(dialogUserControl, nameof(Views.UI.RootView)); - var tcs = new TaskCompletionSource(); - dialogUserControl.Click += (_, _) => - { - canceled = true; - dialog.Close(); - tcs.TrySetResult(true); - }; - _logger.Information("Timer wait time: {Seconds}", seconds); - await Task.WhenAny(Task.Delay(delay), tcs.Task); - dialog.Close(); - _logger.Information("Timer canceled: {Canceled}", canceled); - return canceled; - } - } - - /// - /// Initializes items. - /// - private void InitializeItems() - { - List taskList = - [ - "WakeUp", - "Recruiting", - "Base", - "Combat", - "Mall", - "Mission", - "AutoRoguelike", - "Reclamation" - ]; - - if (Instances.VersionUpdateViewModel.IsDebugVersion() || File.Exists("DEBUG") || File.Exists("DEBUG.txt")) - { - taskList.Add("Custom"); - } - - var tempOrderList = new List(new DragItemViewModel[taskList.Count]); - var nonOrderList = new List(); - for (int i = 0; i != taskList.Count; ++i) - { - var task = taskList[i]; - bool parsed = int.TryParse(ConfigurationHelper.GetTaskOrder(task, "-1"), out var order); - - DragItemViewModel vm = new DragItemViewModel( - LocalizationHelper.GetString(task), - task, - "TaskQueue.", - task is not ("AutoRoguelike" or "Reclamation" or "Custom")); - - if (task == TaskSettingVisibilityInfo.DefaultVisibleTaskSetting) - { - vm.EnableSetting = true; - } - - if (!parsed || order < 0 || order >= tempOrderList.Count || tempOrderList[order] != null) - { - nonOrderList.Add(vm); - } - else - { - tempOrderList[order] = vm; - } - } - - foreach (var newVm in nonOrderList) - { - if (newVm == null) - { - continue; - } - - int i = 0; - while (tempOrderList[i] != null) - { - ++i; - } - - tempOrderList[i] = newVm; - ConfigurationHelper.SetTaskOrder(newVm.OriginalName, i.ToString()); - } - - TaskItemViewModels = [.. tempOrderList.OfType()]; - TaskItemViewModels.CollectionChanged += TaskItemSelectionChanged; - - FightTask.InitDrops(); - NeedToUpdateDatePrompt(); - UpdateDatePromptAndStagesLocally(); - InfrastTask.RefreshCustomInfrastPlan(); - - if (DateTime.UtcNow.ToYjDate().IsAprilFoolsDay()) - { - AddLog(LocalizationHelper.GetString("BuyWineOnAprilFoolsDay"), UiLogColor.Info); - } - } - - public DayOfWeek CurDayOfWeek { get; private set; } - - /// - /// Determine whether the specified stage is open - /// - /// stage name - /// Whether the specified stage is open - public bool IsStageOpen(string name) => Instances.StageManager.IsStageOpen(name, CurDayOfWeek); - - /// - /// Returns the valid stage if it is open, otherwise returns an empty string. - /// - /// The stage to check. - /// The valid stage or an empty string. - public string GetValidStage(string stage) => IsStageOpen(stage) ? stage : string.Empty; - - /// - /// 更新日期提示和关卡列表 - /// - public void UpdateDatePromptAndStagesLocally() - { - UpdateDatePrompt(); - FightTask.UpdateStageList(); - } - - /// - /// 访问 api 获取更新后更新日期提示和关卡列表 - /// - /// 可等待 - public async Task UpdateDatePromptAndStagesWeb() - { - await Instances.StageManager.UpdateStageWeb(); - UpdateDatePromptAndStagesLocally(); - } - - private DateOnly _lastPromptDate; - - private bool NeedToUpdateDatePrompt() - { - var now = DateTime.UtcNow.ToYjDateTime(); - - CurDayOfWeek = now.DayOfWeek; - - // yj历的 4/16 点 - var today = DateOnly.FromDateTime(now); - bool isCriticalTime = now is { Minute: 0, Hour: 0 or 12 }; - bool isNewDate = today != _lastPromptDate; - - if (!isCriticalTime && !isNewDate) - { - return false; - } - - _lastPromptDate = today; - return true; - } - - private static bool NeedToCheckForUpdates() - { - var now = DateTime.UtcNow.ToYjDateTime(); - - // yj历的 4/22 点 - return now is { Minute: 0, Hour: 0 or 18 }; - } - - /// - /// Updates date prompt. - /// - // FIXME: 被注入对象只能在private函数内使用,只有Model显示之后才会被注入。如果Model还没有触发OnInitialActivate时调用函数会NullPointerException - // 这个函数被列为public可见,意味着他注入对象前被调用 - public void UpdateDatePrompt() - { - var builder = new StringBuilder(LocalizationHelper.GetString("TodaysStageTip") + "\n"); - - // Closed activity stages - foreach (var stage in FightTask.Stages) - { - if (stage == null || Instances.StageManager.GetStageInfo(stage).IsActivityClosed() != true) - { - continue; - } - - builder.Append(stage).Append(": ").AppendLine(LocalizationHelper.GetString("ClosedStage")); - } - - // Open stages today - var openStages = Instances.StageManager.GetStageTips(CurDayOfWeek); - if (!string.IsNullOrEmpty(openStages)) - { - builder.Append(openStages); - } - - var prompt = builder.ToString(); - if (StagesOfToday == prompt) - { - return; - } - - StagesOfToday = prompt; - } - - private string _stagesOfToday = string.Empty; - - /// - /// Gets or private sets the stages of today. - /// - public string StagesOfToday - { - get => _stagesOfToday; - private set => SetAndNotify(ref _stagesOfToday, value); - } - - /// - /// Adds log. - /// - /// The content. - /// The font color. - /// The font weight. - /// The toolTip - public void AddLog(string content, string color = UiLogColor.Trace, string weight = "Regular", ToolTip? toolTip = null) - { - Execute.OnUIThread(() => - { - var log = new LogItemViewModel(content, color, weight, toolTip: toolTip); - LogItemViewModels.Add(log); - _logger.Information("{Content}", content); - }); - } - - /// - /// Clears log. - /// - private void ClearLog() - { - Execute.OnUIThread(() => - { - LogItemViewModels.Clear(); - _logger.Information("Main windows log clear."); - _logger.Information("{Empty}", string.Empty); - }); - } - - /// - /// Selects all. - /// UI 绑定的方法 - /// - [UsedImplicitly] - public void SelectedAll() + _ = (sender, e); + Execute.OnUIThread(() => { + int index = 0; foreach (var item in TaskItemViewModels) { - switch (item.OriginalName) - { - case "AutoRoguelike": - case "Reclamation": - case "Custom": - continue; - } - - item.IsChecked = true; + ConfigurationHelper.SetTaskOrder(item.OriginalName, index.ToString()); + ++index; } + }); + } + + /// + /// Gets or private sets the view models of log items. + /// + public ObservableCollection LogItemViewModels { get; private set; } = []; + + #region ActionAfterTasks + + private bool _enableAfterActionSetting; + + /// + /// Gets or sets a value indicating whether to show after task queue actions + /// + public bool EnableAfterActionSetting + { + get => _enableAfterActionSetting; + set + { + SetAndNotify(ref _enableAfterActionSetting, value); + TaskSettingVisibilityInfo.Instance.Set("AfterAction", value); + } + } + + /// + /// Checks after completion. + /// + /// Task + public async Task CheckAfterCompleted() + { + await Task.Run(() => SettingsViewModel.GameSettings.RunScript("EndsWithScript")); + var actions = PostActionSetting; + _logger.Information("Post actions: " + actions.ActionDescription); + + if (actions.BackToAndroidHome) + { + Instances.AsstProxy.AsstBackToHome(); + await Task.Delay(1000); } - private bool _inverseMode = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.MainFunctionInverseMode, bool.FalseString)); - - /// - /// Gets or sets a value indicating whether to use inverse mode. - /// - public bool InverseMode + if (actions.ExitArknights) { - get => _inverseMode; - set + var clientType = SettingsViewModel.GameSettings.ClientType; + if (!Instances.AsstProxy.AsstStartCloseDown(clientType)) { - SetAndNotify(ref _inverseMode, value); - InverseShowText = value ? LocalizationHelper.GetString("Inverse") : LocalizationHelper.GetString("Clear"); - InverseMenuText = value ? LocalizationHelper.GetString("Clear") : LocalizationHelper.GetString("Inverse"); - ConfigurationHelper.SetValue(ConfigurationKeys.MainFunctionInverseMode, value.ToString()); + AddLog(LocalizationHelper.GetString("CloseArknightsFailed"), UiLogColor.Error); } + + await Task.Delay(1000); } - /// - /// The width of "Select All" when both. - /// - public const int SelectedAllWidthWhenBoth = 80; - - private int _selectedAllWidth = - ConfigurationHelper.GetGlobalValue(ConfigurationKeys.InverseClearMode, "Clear") == "ClearInverse" ? SelectedAllWidthWhenBoth : 85; - - /// - /// Gets or sets the width of "Select All". - /// - public int SelectedAllWidth + if (actions.ExitEmulator) { - get => _selectedAllWidth; - set => SetAndNotify(ref _selectedAllWidth, value); + DoKillEmulator(); + await Task.Delay(1000); } - private bool _showInverse = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.InverseClearMode, "Clear") == "ClearInverse"; - - /// - /// Gets or sets a value indicating whether "Select inversely" is visible. - /// - public bool ShowInverse + if (actions.ExitSelf && !(actions.Hibernate || actions.Shutdown || actions.Sleep)) { - get => _showInverse; - set => SetAndNotify(ref _showInverse, value); + Bootstrapper.Shutdown(); } - private string _inverseShowText = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.MainFunctionInverseMode, bool.FalseString)) - ? LocalizationHelper.GetString("Inverse") - : LocalizationHelper.GetString("Clear"); - - /// - /// Gets or private Sets the text to be displayed for "Select inversely". - /// - public string InverseShowText + if (actions.Hibernate) { - get => _inverseShowText; - private set => SetAndNotify(ref _inverseShowText, value); - } - - private string _inverseMenuText = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.MainFunctionInverseMode, bool.FalseString)) - ? LocalizationHelper.GetString("Clear") - : LocalizationHelper.GetString("Inverse"); - - /// - /// Gets or private sets the text of inversion menu. - /// - public string InverseMenuText - { - get => _inverseMenuText; - private set => SetAndNotify(ref _inverseMenuText, value); - } - - /// - /// Changes inversion mode. - /// UI 绑定的方法 - /// - [UsedImplicitly] - public void ChangeInverseMode() - { - InverseMode = !InverseMode; - } - - /// - /// Selects inversely. - /// UI 绑定的方法 - /// - [UsedImplicitly] - public void InverseSelected() - { - if (InverseMode) + if (actions.IfNoOtherMaa && HasOtherMaa()) { - foreach (var item in TaskItemViewModels) - { - switch (item.OriginalName) - { - case "AutoRoguelike": - case "Reclamation": - case "Custom": - item.IsChecked = false; - continue; - } - - item.IsChecked = !item.IsChecked; - } + Bootstrapper.Shutdown(); } else { - foreach (var item in TaskItemViewModels) - { - item.IsChecked = false; - } + await DoHibernate(); } } - /// - /// Reset unsaved task selection. - /// - public void ResetTaskSelection() + if (actions.Shutdown) { - foreach (var item in TaskItemViewModels) + if (actions.IfNoOtherMaa && HasOtherMaa()) { - if (item.IsCheckedWithNull == null) - { - item.IsChecked = GuiSettingsUserControlModel.Instance.MainTasksInvertNullFunction; - } + Bootstrapper.Shutdown(); + } + else + { + await DoShutDown(); } } - /// - /// 还原所有临时变量(右键半选) - /// - public void ResetAllTemporaryVariable() + if (actions.Sleep) { - FightTask.ResetFightVariables(); - RecruitTask.ResetRecruitVariables(); - ResetTaskSelection(); + if (actions.IfNoOtherMaa && HasOtherMaa()) + { + Bootstrapper.Shutdown(); + } + else + { + await DoSleep(); + } } - private async Task ConnectToEmulator() + if (actions.ExitSelf) { - string errMsg = string.Empty; - bool connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + Bootstrapper.Shutdown(); + } - // 尝试启动模拟器 - if (!connected && SettingsViewModel.ConnectSettings.RetryOnDisconnected) + actions.LoadPostActions(); + return; + + bool HasOtherMaa() + { + var processesCount = Process.GetProcessesByName("MAA").Length; + _logger.Information("MAA processes count: {ProcessesCount}", processesCount); + return processesCount > 1; + } + + void DoKillEmulator() + { + if (!EmulatorHelper.KillEmulatorModeSwitcher()) { - AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("TryToStartEmulator")); + AddLog(LocalizationHelper.GetString("ExitEmulatorFailed"), UiLogColor.Error); + } + } - await Task.Run(() => SettingsViewModel.StartSettings.TryToStartEmulator()); + async Task DoHibernate() + { + actions.LoadPostActions(); - if (_runningState.GetStopping()) - { - SetStopped(); - return false; - } + // 休眠提示 + AddLog(LocalizationHelper.GetString("HibernatePrompt"), UiLogColor.Error); + await Task.Delay(10000); + PowerManagement.Hibernate(); + } - connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + async Task DoShutDown() + { + PowerManagement.Shutdown(); + + await Execute.OnUIThreadAsync(() => Instances.MainWindowManager?.Show()); + if (await TimerCanceledAsync( + LocalizationHelper.GetString("Shutdown"), + LocalizationHelper.GetString("AboutToShutdown"), + LocalizationHelper.GetString("Cancel"), + 60)) + { + PowerManagement.AbortShutdown(); + return; } - // 尝试断开连接, 然后重新连接 - if (!connected) + _logger.Information("Shutdown not canceled, proceeding to exit application."); + Bootstrapper.Shutdown(); + } + + async Task DoSleep() + { + actions.LoadPostActions(); + + // 休眠提示 + AddLog(LocalizationHelper.GetString("SleepPrompt"), UiLogColor.Error); + await Task.Delay(10000); + PowerManagement.Sleep(); + } + } + + #endregion + + /// + /// Initializes a new instance of the class. + /// + public TaskQueueViewModel() + { + _runningState = RunningState.Instance; + _runningState.StateChanged += (_, e) => + { + Idle = e.Idle; + Inited = e.Inited; + Stopping = e.Stopping; + + Instances.SettingsViewModel.Idle = e.Idle; + if (!e.Idle) { - AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("TryToReconnectByAdb")); - await Task.Run(() => SettingsViewModel.StartSettings.ReconnectByAdb()); + Instances.Data.ClearCache(); + } + }; + _runningState.TimeoutOccurred += RunningState_TimeOut; + } - if (_runningState.GetStopping()) - { - SetStopped(); - return false; - } - - Instances.AsstProxy.Connected = false; - connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + private void RunningState_TimeOut(object? sender, string message) + { + Execute.OnUIThread(() => + { + AddLog(message, UiLogColor.Warning); + ToastNotification.ShowDirect(message); + if (!SettingsViewModel.ExternalNotificationSettings.ExternalNotificationSendWhenTimeout) + { + return; } - // 尝试重启 ADB - if (!connected && SettingsViewModel.ConnectSettings.AllowAdbRestart) - { - AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("RestartAdb")); + var lastLogs = LogItemViewModels + .TakeLast(5) + .Aggregate(string.Empty, (current, logItem) => current + $"[{logItem.Time}][{logItem.Color}]{logItem.Content}\n"); + ExternalNotificationService.Send(message, lastLogs); + }); + } - await Task.Run(() => SettingsViewModel.StartSettings.RestartAdb()); + protected override void OnInitialActivate() + { + base.OnInitialActivate(); - if (_runningState.GetStopping()) - { - SetStopped(); - return false; - } + DisplayName = LocalizationHelper.GetString("Farming"); + LogItemViewModels = []; + InitializeItems(); + InitTimer(); - connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); - } + _ = UpdateDatePromptAndStagesWeb(); + } - // 尝试杀掉 ADB 进程 - if (!connected && SettingsViewModel.ConnectSettings.AllowAdbHardRestart) - { - AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("HardRestartAdb")); + /* + public void ShowButton() + { + Visible = Visibility.Visible; + Hibernate = true; + } - await Task.Run(() => SettingsViewModel.StartSettings.HardRestartAdb()); + private Visibility _visible = Visibility.Collapsed; - if (_runningState.GetStopping()) - { - SetStopped(); - return false; - } + public Visibility Visible + { + get => _visible; + set => SetAndNotify(ref _visible, value); + } + */ - connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); - } + public bool Running { get; set; } - if (connected) - { - return true; - } + public bool Closing { get; set; } - AddLog(errMsg, UiLogColor.Error); - _runningState.SetIdle(true); - SetStopped(); + private readonly System.Timers.Timer _timer = new(); + + public bool ConfirmExit() + { + if (Closing) + { return false; } - public int MainTasksCompletedCount { get; set; } - - public int MainTasksSelectedCount => TaskItemViewModels.Count(x => x.IsChecked); - - /// - /// updates the main tasks progress. - /// - /// 已完成任务数,留空则代表 +1 - public void UpdateMainTasksProgress(int? completedCount = null) + Closing = true; + if (Application.Current.IsShuttingDown()) { - var rvm = (RootViewModel)this.Parent; - if (MainTasksSelectedCount == 0) + // allow close if application is shutting down + return true; + } + + if (!Running) + { + // no need to confirm if no running task + return true; + } + + var result = MessageBoxHelper.Show( + LocalizationHelper.GetString("ConfirmExitText"), + LocalizationHelper.GetString("ConfirmExitTitle"), + MessageBoxButton.YesNo, + MessageBoxImage.Question); + Closing = false; + return result == MessageBoxResult.Yes; + } + + public override Task CanCloseAsync() + { + return Task.FromResult(this.ConfirmExit()); + } + + private void InitTimer() + { + _timer.Interval = 30 * 1000; + _timer.Elapsed += Timer1_Elapsed; + _timer.Start(); + } + + private DateTime _lastTimerElapsed = DateTime.MinValue; + + private async void Timer1_Elapsed(object? sender, EventArgs e) + { + try + { + // 提前记录时间,避免等待超过定时时间 + DateTime currentTime = DateTime.Now; + currentTime = new(currentTime.Year, currentTime.Month, currentTime.Day, currentTime.Hour, currentTime.Minute, 0); + + if (currentTime <= _lastTimerElapsed) { - rvm.TaskProgress = null; return; } - MainTasksCompletedCount = completedCount ?? ++MainTasksCompletedCount; + _lastTimerElapsed = currentTime; - if (MainTasksCompletedCount >= MainTasksSelectedCount) + VersionUpdateSettingsUserControlModel.Instance.RefreshMirrorChyanCdkRemaining(); + HandleDatePromptUpdate(); + HandleCheckForUpdates(); + + InfrastTask.RefreshCustomInfrastPlanIndexByPeriod(); + + await HandleTimerLogic(currentTime); + } + catch + { + // ignored + } + } + + private static int CalculateRandomDelay() + { + Random random = new Random(); + int delayTime = random.Next(0, 60 * 60 * 1000); + return delayTime; + } + + private bool _isUpdatingDatePrompt; + + private void HandleDatePromptUpdate() + { + if (!NeedToUpdateDatePrompt() || _isUpdatingDatePrompt) + { + return; + } + + _isUpdatingDatePrompt = true; + UpdateDatePromptAndStagesLocally(); + + var delayTime = CalculateRandomDelay(); + _ = Task.Run(async () => + { + await Task.Delay(delayTime); + await _runningState.UntilIdleAsync(60000); + await UpdateDatePromptAndStagesWeb(); + _isUpdatingDatePrompt = false; + }); + } + + private bool _isCheckingForUpdates; + + private void HandleCheckForUpdates() + { + if (!SettingsViewModel.VersionUpdateSettings.UpdateAutoCheck) + { + return; + } + + if (!NeedToCheckForUpdates() || _isCheckingForUpdates) + { + return; + } + + _isCheckingForUpdates = true; + var delayTime = CalculateRandomDelay(); + _ = Task.Run(async () => + { + _logger.Information("waiting for update check: {DelayTime}", delayTime); + await Task.Delay(delayTime); + await Instances.VersionUpdateViewModel.VersionUpdateAndAskToRestartAsync(); + await ResourceUpdater.ResourceUpdateAndReloadAsync(); + + _isCheckingForUpdates = false; + }); + } + + private static (bool _timeToStart, bool _timeToChangeConfig, int _configIndex) CheckTimers(DateTime currentTime) + { + bool timeToStart = false; + bool timeToChangeConfig = false; + int configIndex = 0; + + for (int i = 0; i < 8; ++i) + { + if (SettingsViewModel.TimerSettings.TimerModels.Timers[i].IsOn == false) { - rvm.TaskProgress = null; + continue; } - else + + DateTime startTime = new DateTime(currentTime.Year, + currentTime.Month, + currentTime.Day, + SettingsViewModel.TimerSettings.TimerModels.Timers[i].Hour, + SettingsViewModel.TimerSettings.TimerModels.Timers[i].Min, + 0); + DateTime restartDateTime = startTime.AddMinutes(-2); + + // 确保0点的定时会在当日的23:58重启 + if (restartDateTime.Day != startTime.Day) { - rvm.TaskProgress = (MainTasksCompletedCount, MainTasksSelectedCount); + restartDateTime = restartDateTime.AddDays(1); + } + + if (currentTime == restartDateTime && + Instances.SettingsViewModel.CurrentConfiguration != SettingsViewModel.TimerSettings.TimerModels.Timers[i].TimerConfig) + { + timeToChangeConfig = true; + configIndex = i; + break; + } + + // ReSharper disable once InvertIf + if (currentTime == startTime) + { + timeToStart = true; + configIndex = i; + break; } } - private DateTime? _taskStartTime; + return (timeToStart, timeToChangeConfig, configIndex); + } - /// - /// Starts. - /// - /// Task - public async Task LinkStart() + private async Task HandleTimerLogic(DateTime currentTime) + { + if (!_runningState.GetIdle() && !SettingsViewModel.TimerSettings.ForceScheduledStart) { + return; + } + + var (timeToStart, timeToChangeConfig, configIndex) = CheckTimers(currentTime); + + if (timeToChangeConfig) + { + _logger.Information("Scheduled configuration change: Timer Index: {ConfigIndex}", configIndex); + HandleConfigChange(configIndex); + return; + } + + if (timeToStart) + { + _logger.Information("Scheduled start: Timer Index: {ConfigIndex}", configIndex); + await HandleScheduledStart(configIndex); + + SettingsViewModel.TimerSettings.TimerModels.Timers[configIndex].IsOn ??= false; + } + } + + private void HandleConfigChange(int configIndex) + { + if (SettingsViewModel.TimerSettings.CustomConfig && + (_runningState.GetIdle() || SettingsViewModel.TimerSettings.ForceScheduledStart)) + { + Instances.SettingsViewModel.CurrentConfiguration = SettingsViewModel.TimerSettings.TimerModels.Timers[configIndex].TimerConfig; + } + } + + private async Task HandleScheduledStart(int configIndex) + { + if (SettingsViewModel.TimerSettings.ForceScheduledStart) + { + if (SettingsViewModel.TimerSettings.CustomConfig && + Instances.SettingsViewModel.CurrentConfiguration != SettingsViewModel.TimerSettings.TimerModels.Timers[configIndex].TimerConfig) + { + _logger.Warning( + "Scheduled start skipped: Custom configuration is enabled, but the current configuration does not match the scheduled timer configuration (Timer Index: {ConfigIndex}). Current Configuration: {CurrentConfiguration}, Scheduled Configuration: {TimerConfig}", + configIndex, + Instances.SettingsViewModel.CurrentConfiguration, + SettingsViewModel.TimerSettings.TimerModels.Timers[configIndex].TimerConfig); + return; + } + + if (SettingsViewModel.TimerSettings.ShowWindowBeforeForceScheduledStart) + { + await Execute.OnUIThreadAsync(() => Instances.MainWindowManager?.Show()); + } + + if (await TimerCanceledAsync( + LocalizationHelper.GetString("ForceScheduledStart"), + LocalizationHelper.GetString("ForceScheduledStartTip"), + LocalizationHelper.GetString("Cancel"))) + { + return; + } + if (!_runningState.GetIdle()) { - _logger.Information("Not idle, return."); - return; - } - - _taskStartTime = DateTime.Now; - - ClearLog(); - - var buildDateTimeLong = VersionUpdateSettingsUserControlModel.BuildDateTimeCurrentCultureString; - var resourceDateTimeLong = SettingsViewModel.VersionUpdateSettings.ResourceDateTimeCurrentCultureString; - AddLog($"Build Time:\n{buildDateTimeLong}\nResource Time:\n{resourceDateTimeLong}"); - - var buildTimeInterval = (DateTime.UtcNow - VersionUpdateSettingsUserControlModel.BuildDateTime).TotalDays; - var resourceTimeInterval = (DateTime.UtcNow - SettingsViewModel.VersionUpdateSettings.ResourceDateTime).TotalDays; - var maxTimeInterval = Math.Max(buildTimeInterval, resourceTimeInterval); - if (maxTimeInterval > 90) - { - AddLog( - string.Format( - LocalizationHelper.GetString("Achievement.Martian.ConditionsTip"), - Math.Round(maxTimeInterval / 30, 1)), - UiLogColor.Error); - } - - var uiVersion = VersionUpdateSettingsUserControlModel.UiVersion; - var coreVersion = VersionUpdateSettingsUserControlModel.CoreVersion; - if (!Instances.VersionUpdateViewModel.IsDebugVersion() && uiVersion != coreVersion) - { - AddLog(string.Format(LocalizationHelper.GetString("VersionMismatch"), uiVersion, coreVersion), UiLogColor.Error); - return; - } - - MainTasksCompletedCount = 0; - - // 所有提前 return 都要放在 _runningState.SetIdle(false) 之前,否则会导致无法再次点击开始 - _runningState.SetIdle(false); - - // 虽然更改时已经保存过了,不过保险起见在点击开始之后再次保存任务和基建列表 - TaskItemSelectionChanged(); - InfrastTask.InfrastOrderSelectionChanged(); - - await Task.Run(() => SettingsViewModel.GameSettings.RunScript("StartsWithScript")); - - AddLog(LocalizationHelper.GetString("ConnectingToEmulator")); - - /* - // 现在的主流模拟器都已经更新过自带的 adb 了,不再需要替换 - if (!Instances.SettingsViewModel.AdbReplaced && !Instances.SettingsViewModel.IsAdbTouchMode()) - { - AddLog(LocalizationHelper.GetString("AdbReplacementTips"), UiLogColor.Info); - } - */ - - // 一般是点了“停止”按钮了 - if (_runningState.GetStopping()) - { + _logger.Information("Not idle, Stop and CloseDown"); + await Stop(); SetStopped(); - return; } - if (!await ConnectToEmulator()) + var mode = SettingsViewModel.GameSettings.ClientType; + if (!Instances.AsstProxy.AsstAppendCloseDown(mode)) { - return; + AddLog(LocalizationHelper.GetString("CloseArknightsFailed"), UiLogColor.Error); } - // 一般是点了“停止”按钮了 - if (_runningState.GetStopping()) + ResetAllTemporaryVariable(); + InfrastTask.RefreshCustomInfrastPlanIndexByPeriod(); + } + + await LinkStart(); + + AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.ScheduleMasterGroup); + } + + private static async Task TimerCanceledAsync(string content = "", string tipContent = "", string buttonContent = "", int seconds = 10) + { + if (Application.Current.Dispatcher.CheckAccess()) + { + return await ShowDialogAsync(); + } + + return await await Application.Current.Dispatcher.InvokeAsync(ShowDialogAsync); + + async Task ShowDialogAsync() + { + var canceled = false; + var delay = TimeSpan.FromSeconds(seconds); + var dialogUserControl = new Views.UserControl.TextDialogWithTimerUserControl( + content, + tipContent, + buttonContent, + delay.TotalMilliseconds); + var dialog = HandyControl.Controls.Dialog.Show(dialogUserControl, nameof(Views.UI.RootView)); + var tcs = new TaskCompletionSource(); + dialogUserControl.Click += (_, _) => { - SetStopped(); - return; + canceled = true; + dialog.Close(); + tcs.TrySetResult(true); + }; + _logger.Information("Timer wait time: {Seconds}", seconds); + await Task.WhenAny(Task.Delay(delay), tcs.Task); + dialog.Close(); + _logger.Information("Timer canceled: {Canceled}", canceled); + return canceled; + } + } + + /// + /// Initializes items. + /// + private void InitializeItems() + { + List taskList = + [ + "WakeUp", + "Recruiting", + "Base", + "Combat", + "Mall", + "Mission", + "AutoRoguelike", + "Reclamation" + ]; + + if (Instances.VersionUpdateViewModel.IsDebugVersion() || File.Exists("DEBUG") || File.Exists("DEBUG.txt")) + { + taskList.Add("Custom"); + } + + var tempOrderList = new List(new DragItemViewModel[taskList.Count]); + var nonOrderList = new List(); + for (int i = 0; i != taskList.Count; ++i) + { + var task = taskList[i]; + bool parsed = int.TryParse(ConfigurationHelper.GetTaskOrder(task, "-1"), out var order); + + DragItemViewModel vm = new DragItemViewModel( + LocalizationHelper.GetString(task), + task, + "TaskQueue.", + task is not ("AutoRoguelike" or "Reclamation" or "Custom")); + + if (task == TaskSettingVisibilityInfo.DefaultVisibleTaskSetting) + { + vm.EnableSetting = true; } - bool taskRet = true; + if (!parsed || order < 0 || order >= tempOrderList.Count || tempOrderList[order] != null) + { + nonOrderList.Add(vm); + } + else + { + tempOrderList[order] = vm; + } + } - // 直接遍历TaskItemViewModels里面的内容,是排序后的 - int count = 0; + foreach (var newVm in nonOrderList) + { + if (newVm == null) + { + continue; + } + + int i = 0; + while (tempOrderList[i] != null) + { + ++i; + } + + tempOrderList[i] = newVm; + ConfigurationHelper.SetTaskOrder(newVm.OriginalName, i.ToString()); + } + + TaskItemViewModels = [.. tempOrderList.OfType()]; + TaskItemViewModels.CollectionChanged += TaskItemSelectionChanged; + + FightTask.InitDrops(); + NeedToUpdateDatePrompt(); + UpdateDatePromptAndStagesLocally(); + InfrastTask.RefreshCustomInfrastPlan(); + + if (DateTime.UtcNow.ToYjDate().IsAprilFoolsDay()) + { + AddLog(LocalizationHelper.GetString("BuyWineOnAprilFoolsDay"), UiLogColor.Info); + } + } + + public DayOfWeek CurDayOfWeek { get; private set; } + + /// + /// Determine whether the specified stage is open + /// + /// stage name + /// Whether the specified stage is open + public bool IsStageOpen(string name) => Instances.StageManager.IsStageOpen(name, CurDayOfWeek); + + /// + /// Returns the valid stage if it is open, otherwise returns an empty string. + /// + /// The stage to check. + /// The valid stage or an empty string. + public string GetValidStage(string stage) => IsStageOpen(stage) ? stage : string.Empty; + + /// + /// 更新日期提示和关卡列表 + /// + public void UpdateDatePromptAndStagesLocally() + { + UpdateDatePrompt(); + FightTask.UpdateStageList(); + } + + /// + /// 访问 api 获取更新后更新日期提示和关卡列表 + /// + /// 可等待 + public async Task UpdateDatePromptAndStagesWeb() + { + await Instances.StageManager.UpdateStageWeb(); + UpdateDatePromptAndStagesLocally(); + } + + private DateOnly _lastPromptDate; + + private bool NeedToUpdateDatePrompt() + { + var now = DateTime.UtcNow.ToYjDateTime(); + + CurDayOfWeek = now.DayOfWeek; + + // yj历的 4/16 点 + var today = DateOnly.FromDateTime(now); + bool isCriticalTime = now is { Minute: 0, Hour: 0 or 12 }; + bool isNewDate = today != _lastPromptDate; + + if (!isCriticalTime && !isNewDate) + { + return false; + } + + _lastPromptDate = today; + return true; + } + + private static bool NeedToCheckForUpdates() + { + var now = DateTime.UtcNow.ToYjDateTime(); + + // yj历的 4/22 点 + return now is { Minute: 0, Hour: 0 or 18 }; + } + + /// + /// Updates date prompt. + /// + // FIXME: 被注入对象只能在private函数内使用,只有Model显示之后才会被注入。如果Model还没有触发OnInitialActivate时调用函数会NullPointerException + // 这个函数被列为public可见,意味着他注入对象前被调用 + public void UpdateDatePrompt() + { + var builder = new StringBuilder(LocalizationHelper.GetString("TodaysStageTip") + "\n"); + + // Closed activity stages + foreach (var stage in FightTask.Stages) + { + if (stage == null || Instances.StageManager.GetStageInfo(stage).IsActivityClosed() != true) + { + continue; + } + + builder.Append(stage).Append(": ").AppendLine(LocalizationHelper.GetString("ClosedStage")); + } + + // Open stages today + var openStages = Instances.StageManager.GetStageTips(CurDayOfWeek); + if (!string.IsNullOrEmpty(openStages)) + { + builder.Append(openStages); + } + + var prompt = builder.ToString(); + if (StagesOfToday == prompt) + { + return; + } + + StagesOfToday = prompt; + } + + private string _stagesOfToday = string.Empty; + + /// + /// Gets or private sets the stages of today. + /// + public string StagesOfToday + { + get => _stagesOfToday; + private set => SetAndNotify(ref _stagesOfToday, value); + } + + /// + /// Adds log. + /// + /// The content. + /// The font color. + /// The font weight. + /// The toolTip + public void AddLog(string content, string color = UiLogColor.Trace, string weight = "Regular", ToolTip? toolTip = null) + { + Execute.OnUIThread(() => + { + var log = new LogItemViewModel(content, color, weight, toolTip: toolTip); + LogItemViewModels.Add(log); + _logger.Information("{Content}", content); + }); + } + + /// + /// Clears log. + /// + private void ClearLog() + { + Execute.OnUIThread(() => + { + LogItemViewModels.Clear(); + _logger.Information("Main windows log clear."); + _logger.Information("{Empty}", string.Empty); + }); + } + + /// + /// Selects all. + /// UI 绑定的方法 + /// + [UsedImplicitly] + public void SelectedAll() + { + foreach (var item in TaskItemViewModels) + { + switch (item.OriginalName) + { + case "AutoRoguelike": + case "Reclamation": + case "Custom": + continue; + } + + item.IsChecked = true; + } + } + + private bool _inverseMode = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.MainFunctionInverseMode, bool.FalseString)); + + /// + /// Gets or sets a value indicating whether to use inverse mode. + /// + public bool InverseMode + { + get => _inverseMode; + set + { + SetAndNotify(ref _inverseMode, value); + InverseShowText = value ? LocalizationHelper.GetString("Inverse") : LocalizationHelper.GetString("Clear"); + InverseMenuText = value ? LocalizationHelper.GetString("Clear") : LocalizationHelper.GetString("Inverse"); + ConfigurationHelper.SetValue(ConfigurationKeys.MainFunctionInverseMode, value.ToString()); + } + } + + /// + /// The width of "Select All" when both. + /// + public const int SelectedAllWidthWhenBoth = 80; + + private int _selectedAllWidth = + ConfigurationHelper.GetGlobalValue(ConfigurationKeys.InverseClearMode, "Clear") == "ClearInverse" ? SelectedAllWidthWhenBoth : 85; + + /// + /// Gets or sets the width of "Select All". + /// + public int SelectedAllWidth + { + get => _selectedAllWidth; + set => SetAndNotify(ref _selectedAllWidth, value); + } + + private bool _showInverse = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.InverseClearMode, "Clear") == "ClearInverse"; + + /// + /// Gets or sets a value indicating whether "Select inversely" is visible. + /// + public bool ShowInverse + { + get => _showInverse; + set => SetAndNotify(ref _showInverse, value); + } + + private string _inverseShowText = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.MainFunctionInverseMode, bool.FalseString)) + ? LocalizationHelper.GetString("Inverse") + : LocalizationHelper.GetString("Clear"); + + /// + /// Gets or private Sets the text to be displayed for "Select inversely". + /// + public string InverseShowText + { + get => _inverseShowText; + private set => SetAndNotify(ref _inverseShowText, value); + } + + private string _inverseMenuText = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.MainFunctionInverseMode, bool.FalseString)) + ? LocalizationHelper.GetString("Clear") + : LocalizationHelper.GetString("Inverse"); + + /// + /// Gets or private sets the text of inversion menu. + /// + public string InverseMenuText + { + get => _inverseMenuText; + private set => SetAndNotify(ref _inverseMenuText, value); + } + + /// + /// Changes inversion mode. + /// UI 绑定的方法 + /// + [UsedImplicitly] + public void ChangeInverseMode() + { + InverseMode = !InverseMode; + } + + /// + /// Selects inversely. + /// UI 绑定的方法 + /// + [UsedImplicitly] + public void InverseSelected() + { + if (InverseMode) + { foreach (var item in TaskItemViewModels) { - if (item.IsChecked == false || (GuiSettingsUserControlModel.Instance.MainTasksInvertNullFunction && item.IsCheckedWithNull == null)) - { - continue; - } - - ++count; switch (item.OriginalName) { - case "Base": - taskRet &= AppendInfrast(); - break; - - case "WakeUp": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.StartUp, StartUpTask.Serialize()); - break; - - case "Combat": - taskRet &= AppendFight(); - break; - - case "Recruiting": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Recruit, RecruitTask.Serialize()); - break; - - case "Mall": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Mall, MallTask.Serialize()); - break; - - case "Mission": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Award, AwardTask.Serialize()); - break; - case "AutoRoguelike": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Roguelike, RoguelikeTask.Serialize()); - break; - case "Reclamation": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Reclamation, ReclamationTask.Serialize()); - break; - case "Custom": - { - var tasks = CustomTask.SerializeMultiTasks(); - foreach (var (type, param) in tasks) - { - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Custom, type, param); - } - - break; - } - - default: - --count; - _logger.Error("Unknown task: " + item.OriginalName); - break; + item.IsChecked = false; + continue; } - if (taskRet) - { - continue; - } - - AddLog($"{LocalizationHelper.GetString(item.OriginalName)} task append error", UiLogColor.Error); - taskRet = true; - --count; - } - - if (count == 0) - { - AddLog(LocalizationHelper.GetString("UnselectedTask")); - _runningState.SetIdle(true); - Instances.AsstProxy.AsstStop(); - SetStopped(); - return; - } - - taskRet &= Instances.AsstProxy.AsstStart(); - - if (taskRet) - { - AddLog(LocalizationHelper.GetString("Running")); - Instances.AsstProxy.StartTaskTime = DateTimeOffset.Now; - } - else - { - AddLog(LocalizationHelper.GetString("UnknownErrorOccurs")); - await Stop(); - SetStopped(); - } - - AchievementTrackerHelper.Instance.MissionStartCountAdd(); - AchievementTrackerHelper.Instance.UseDailyAdd(); - } - - public void ManualStop() - { - if (Stopping || _runningState.GetIdle()) - { - _logger.Information("Already stopping or idle, return."); - return; - } - - _ = Stop(); - AchievementTrackerHelper.Instance.Unlock(AchievementIds.TacticalRetreat); - - if (_taskStartTime is null) - { - return; - } - - var duration = DateTime.Now - _taskStartTime.Value; - if (duration.TotalSeconds < 5) - { - AchievementTrackerHelper.Instance.Unlock(AchievementIds.TaskStartCancel); + item.IsChecked = !item.IsChecked; } } - - /// - /// 通常要和 一起使用,除非能保证回调消息能收到 `AsstMsg.TaskChainStopped` - /// This is usually done with Unless you are guaranteed to receive the callback message `AsstMsg.TaskChainStopped` - /// - /// Timeout millisecond - /// A - /// 尝试等待 core 成功停止运行,默认超时时间一分钟 - /// Try to wait for the core to stop running, the default timeout is one minute - /// - public async Task Stop(int timeout = 60 * 1000) + else { - _runningState.SetStopping(true); - AddLog(LocalizationHelper.GetString("Stopping")); - await Task.Run(() => + foreach (var item in TaskItemViewModels) { - if (!Instances.AsstProxy.AsstStop()) - { - _logger.Warning("Failed to stop Asst"); - } - }); - - int count = 0; - while (Instances.AsstProxy.AsstRunning() && count <= timeout / 100) - { - await Task.Delay(100); - count++; - } - - return !Instances.AsstProxy.AsstRunning(); - } - - // UI 绑定的方法 - [UsedImplicitly] - public async Task WaitAndStop() - { - Waiting = true; - AddLog(LocalizationHelper.GetString("Waiting")); - if (RoguelikeTask.RoguelikeDelayAbortUntilCombatComplete) - { - await WaitUntilRoguelikeCombatComplete(); - - if (Instances.AsstProxy.AsstRunning() && !_runningState.GetStopping()) - { - await Stop(); - } + item.IsChecked = false; } } + } - /// - /// 等待肉鸽战斗结束,10分钟强制退出 - /// - private async Task WaitUntilRoguelikeCombatComplete() + /// + /// Reset unsaved task selection. + /// + public void ResetTaskSelection() + { + foreach (var item in TaskItemViewModels) { - int time = 0; - while (RoguelikeTask.RoguelikeDelayAbortUntilCombatComplete && RoguelikeInCombatAndShowWait && time < 600 && !Stopping) + if (item.IsCheckedWithNull == null) { - await Task.Delay(1000); - ++time; + item.IsChecked = GuiSettingsUserControlModel.Instance.MainTasksInvertNullFunction; } } + } - private bool _roguelikeInCombatAndShowWait; + /// + /// 还原所有临时变量(右键半选) + /// + public void ResetAllTemporaryVariable() + { + FightTask.ResetFightVariables(); + RecruitTask.ResetRecruitVariables(); + ResetTaskSelection(); + } - public bool RoguelikeInCombatAndShowWait + private async Task ConnectToEmulator() + { + string errMsg = string.Empty; + bool connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + + // 尝试启动模拟器 + if (!connected && SettingsViewModel.ConnectSettings.RetryOnDisconnected) { - get => _roguelikeInCombatAndShowWait; - set => SetAndNotify(ref _roguelikeInCombatAndShowWait, value); - } + AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("TryToStartEmulator")); - public void SetStopped() - { - SleepManagement.AllowSleep(); - if (SettingsViewModel.GameSettings.ManualStopWithScript) - { - Task.Run(() => SettingsViewModel.GameSettings.RunScript("EndsWithScript")); - } + await Task.Run(() => SettingsViewModel.StartSettings.TryToStartEmulator()); - if (!_runningState.GetIdle() || _runningState.GetStopping()) - { - AddLog(LocalizationHelper.GetString("Stopped")); - } - - Waiting = false; - _runningState.SetStopping(false); - _runningState.SetIdle(true); - } - - public async Task QuickSwitchAccount() - { - if (!_runningState.GetIdle()) - { - return; - } - - _runningState.SetIdle(false); - - // 虽然更改时已经保存过了,不过保险起见在点击开始之后再次保存任务和基建列表 - TaskItemSelectionChanged(); - InfrastTask.InfrastOrderSelectionChanged(); - - ClearLog(); - - await Task.Run(() => SettingsViewModel.GameSettings.RunScript("StartsWithScript")); - - AddLog(LocalizationHelper.GetString("ConnectingToEmulator")); - - /* - // 现在的主流模拟器都已经更新过自带的 adb 了,不再需要替换 - if (!Instances.SettingsViewModel.AdbReplaced && !Instances.SettingsViewModel.IsAdbTouchMode()) - { - AddLog(LocalizationHelper.GetString("AdbReplacementTips"), UiLogColor.Info); - } - */ - - // 一般是点了“停止”按钮了 if (_runningState.GetStopping()) { SetStopped(); - return; - } - - if (!await ConnectToEmulator()) - { - return; - } - - // 一般是点了“停止”按钮了 - if (_runningState.GetStopping()) - { - SetStopped(); - return; - } - - bool taskRet = true; - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.StartUp, StartUpTask.Serialize()); - taskRet &= Instances.AsstProxy.AsstStart(); - - if (taskRet) - { - AddLog(LocalizationHelper.GetString("Running")); - } - else - { - AddLog(LocalizationHelper.GetString("UnknownErrorOccurs")); - await Stop(); - SetStopped(); - } - } - - public bool AppendFight() - { - string curStage = FightTask.Stage; - - var (type, mainParam) = FightTask.Serialize(); - bool mainFightRet = Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Fight, type, mainParam); - if (!mainFightRet) - { - AddLog(LocalizationHelper.GetString("UnsupportedStages") + ": " + curStage, UiLogColor.Error); return false; } - if ((curStage == "Annihilation") && FightTask.UseAlternateStage) + connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + } + + // 尝试断开连接, 然后重新连接 + if (!connected) + { + AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("TryToReconnectByAdb")); + await Task.Run(() => SettingsViewModel.StartSettings.ReconnectByAdb()); + + if (_runningState.GetStopping()) { - foreach (var stage in FightTask.Stages) - { - if (stage is null || !IsStageOpen(stage) || (stage == curStage)) - { - continue; - } + SetStopped(); + return false; + } - AddLog(LocalizationHelper.GetString("AnnihilationTaskTip"), UiLogColor.Info); - var task = mainParam.ToObject(); - if (task != null) - { - task.Stage = stage; - task.Stone = 0; - task.MaxTimes = int.MaxValue; - task.Drops = []; - mainFightRet = Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.FightAnnihilationAlternate, type, task.Serialize().Params); - } + Instances.AsstProxy.Connected = false; + connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + } + // 尝试重启 ADB + if (!connected && SettingsViewModel.ConnectSettings.AllowAdbRestart) + { + AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("RestartAdb")); + + await Task.Run(() => SettingsViewModel.StartSettings.RestartAdb()); + + if (_runningState.GetStopping()) + { + SetStopped(); + return false; + } + + connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + } + + // 尝试杀掉 ADB 进程 + if (!connected && SettingsViewModel.ConnectSettings.AllowAdbHardRestart) + { + AddLog(LocalizationHelper.GetString("ConnectFailed") + "\n" + LocalizationHelper.GetString("HardRestartAdb")); + + await Task.Run(() => SettingsViewModel.StartSettings.HardRestartAdb()); + + if (_runningState.GetStopping()) + { + SetStopped(); + return false; + } + + connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + } + + if (connected) + { + return true; + } + + AddLog(errMsg, UiLogColor.Error); + _runningState.SetIdle(true); + SetStopped(); + return false; + } + + public int MainTasksCompletedCount { get; set; } + + public int MainTasksSelectedCount => TaskItemViewModels.Count(x => x.IsChecked); + + /// + /// updates the main tasks progress. + /// + /// 已完成任务数,留空则代表 +1 + public void UpdateMainTasksProgress(int? completedCount = null) + { + var rvm = (RootViewModel)this.Parent; + if (MainTasksSelectedCount == 0) + { + rvm.TaskProgress = null; + return; + } + + MainTasksCompletedCount = completedCount ?? ++MainTasksCompletedCount; + + if (MainTasksCompletedCount >= MainTasksSelectedCount) + { + rvm.TaskProgress = null; + } + else + { + rvm.TaskProgress = (MainTasksCompletedCount, MainTasksSelectedCount); + } + } + + private DateTime? _taskStartTime; + + /// + /// Starts. + /// + /// Task + public async Task LinkStart() + { + if (!_runningState.GetIdle()) + { + _logger.Information("Not idle, return."); + return; + } + + _taskStartTime = DateTime.Now; + + ClearLog(); + + var buildDateTimeLong = VersionUpdateSettingsUserControlModel.BuildDateTimeCurrentCultureString; + var resourceDateTimeLong = SettingsViewModel.VersionUpdateSettings.ResourceDateTimeCurrentCultureString; + AddLog($"Build Time:\n{buildDateTimeLong}\nResource Time:\n{resourceDateTimeLong}"); + + var buildTimeInterval = (DateTime.UtcNow - VersionUpdateSettingsUserControlModel.BuildDateTime).TotalDays; + var resourceTimeInterval = (DateTime.UtcNow - SettingsViewModel.VersionUpdateSettings.ResourceDateTime).TotalDays; + var maxTimeInterval = Math.Max(buildTimeInterval, resourceTimeInterval); + if (maxTimeInterval > 90) + { + AddLog( + string.Format( + LocalizationHelper.GetString("Achievement.Martian.ConditionsTip"), + Math.Round(maxTimeInterval / 30, 1)), + UiLogColor.Error); + } + + var uiVersion = VersionUpdateSettingsUserControlModel.UiVersion; + var coreVersion = VersionUpdateSettingsUserControlModel.CoreVersion; + if (!Instances.VersionUpdateViewModel.IsDebugVersion() && uiVersion != coreVersion) + { + AddLog(string.Format(LocalizationHelper.GetString("VersionMismatch"), uiVersion, coreVersion), UiLogColor.Error); + return; + } + + MainTasksCompletedCount = 0; + + // 所有提前 return 都要放在 _runningState.SetIdle(false) 之前,否则会导致无法再次点击开始 + _runningState.SetIdle(false); + + // 虽然更改时已经保存过了,不过保险起见在点击开始之后再次保存任务和基建列表 + TaskItemSelectionChanged(); + InfrastTask.InfrastOrderSelectionChanged(); + + await Task.Run(() => SettingsViewModel.GameSettings.RunScript("StartsWithScript")); + + AddLog(LocalizationHelper.GetString("ConnectingToEmulator")); + + /* + // 现在的主流模拟器都已经更新过自带的 adb 了,不再需要替换 + if (!Instances.SettingsViewModel.AdbReplaced && !Instances.SettingsViewModel.IsAdbTouchMode()) + { + AddLog(LocalizationHelper.GetString("AdbReplacementTips"), UiLogColor.Info); + } + */ + + // 一般是点了“停止”按钮了 + if (_runningState.GetStopping()) + { + SetStopped(); + return; + } + + if (!await ConnectToEmulator()) + { + return; + } + + // 一般是点了“停止”按钮了 + if (_runningState.GetStopping()) + { + SetStopped(); + return; + } + + bool taskRet = true; + + // 直接遍历TaskItemViewModels里面的内容,是排序后的 + int count = 0; + foreach (var item in TaskItemViewModels) + { + if (item.IsChecked == false || (GuiSettingsUserControlModel.Instance.MainTasksInvertNullFunction && item.IsCheckedWithNull == null)) + { + continue; + } + + ++count; + switch (item.OriginalName) + { + case "Base": + taskRet &= AppendInfrast(); + break; + + case "WakeUp": + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.StartUp, StartUpTask.Serialize()); + break; + + case "Combat": + taskRet &= AppendFight(); + break; + + case "Recruiting": + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Recruit, RecruitTask.Serialize()); + break; + + case "Mall": + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Mall, MallTask.Serialize()); + break; + + case "Mission": + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Award, AwardTask.Serialize()); + break; + + case "AutoRoguelike": + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Roguelike, RoguelikeTask.Serialize()); + break; + + case "Reclamation": + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Reclamation, ReclamationTask.Serialize()); + break; + + case "Custom": + { + var tasks = CustomTask.SerializeMultiTasks(); + foreach (var (type, param) in tasks) + { + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Custom, type, param); + } + + break; + } + + default: + --count; + _logger.Error("Unknown task: " + item.OriginalName); break; - } } - if (mainFightRet && FightTask.UseRemainingSanityStage && !string.IsNullOrEmpty(FightTask.RemainingSanityStage)) + if (taskRet) { - var task = new AsstFightTask() + continue; + } + + AddLog($"{LocalizationHelper.GetString(item.OriginalName)} task append error", UiLogColor.Error); + taskRet = true; + --count; + } + + if (count == 0) + { + AddLog(LocalizationHelper.GetString("UnselectedTask")); + _runningState.SetIdle(true); + Instances.AsstProxy.AsstStop(); + SetStopped(); + return; + } + + taskRet &= Instances.AsstProxy.AsstStart(); + + if (taskRet) + { + AddLog(LocalizationHelper.GetString("Running")); + Instances.AsstProxy.StartTaskTime = DateTimeOffset.Now; + } + else + { + AddLog(LocalizationHelper.GetString("UnknownErrorOccurs")); + await Stop(); + SetStopped(); + } + + AchievementTrackerHelper.Instance.MissionStartCountAdd(); + AchievementTrackerHelper.Instance.UseDailyAdd(); + } + + public void ManualStop() + { + if (Stopping || _runningState.GetIdle()) + { + _logger.Information("Already stopping or idle, return."); + return; + } + + _ = Stop(); + AchievementTrackerHelper.Instance.Unlock(AchievementIds.TacticalRetreat); + + if (_taskStartTime is null) + { + return; + } + + var duration = DateTime.Now - _taskStartTime.Value; + if (duration.TotalSeconds < 5) + { + AchievementTrackerHelper.Instance.Unlock(AchievementIds.TaskStartCancel); + } + } + + /// + /// 通常要和 一起使用,除非能保证回调消息能收到 `AsstMsg.TaskChainStopped` + /// This is usually done with Unless you are guaranteed to receive the callback message `AsstMsg.TaskChainStopped` + /// + /// Timeout millisecond + /// A + /// 尝试等待 core 成功停止运行,默认超时时间一分钟 + /// Try to wait for the core to stop running, the default timeout is one minute + /// + public async Task Stop(int timeout = 60 * 1000) + { + _runningState.SetStopping(true); + AddLog(LocalizationHelper.GetString("Stopping")); + await Task.Run(() => + { + if (!Instances.AsstProxy.AsstStop()) + { + _logger.Warning("Failed to stop Asst"); + } + }); + + int count = 0; + while (Instances.AsstProxy.AsstRunning() && count <= timeout / 100) + { + await Task.Delay(100); + count++; + } + + return !Instances.AsstProxy.AsstRunning(); + } + + // UI 绑定的方法 + [UsedImplicitly] + public async Task WaitAndStop() + { + Waiting = true; + AddLog(LocalizationHelper.GetString("Waiting")); + if (RoguelikeTask.RoguelikeDelayAbortUntilCombatComplete) + { + await WaitUntilRoguelikeCombatComplete(); + + if (Instances.AsstProxy.AsstRunning() && !_runningState.GetStopping()) + { + await Stop(); + } + } + } + + /// + /// 等待肉鸽战斗结束,10分钟强制退出 + /// + private async Task WaitUntilRoguelikeCombatComplete() + { + int time = 0; + while (RoguelikeTask.RoguelikeDelayAbortUntilCombatComplete && RoguelikeInCombatAndShowWait && time < 600 && !Stopping) + { + await Task.Delay(1000); + ++time; + } + } + + private bool _roguelikeInCombatAndShowWait; + + public bool RoguelikeInCombatAndShowWait + { + get => _roguelikeInCombatAndShowWait; + set => SetAndNotify(ref _roguelikeInCombatAndShowWait, value); + } + + public void SetStopped() + { + SleepManagement.AllowSleep(); + if (SettingsViewModel.GameSettings.ManualStopWithScript) + { + Task.Run(() => SettingsViewModel.GameSettings.RunScript("EndsWithScript")); + } + + if (!_runningState.GetIdle() || _runningState.GetStopping()) + { + AddLog(LocalizationHelper.GetString("Stopped")); + } + + Waiting = false; + _runningState.SetStopping(false); + _runningState.SetIdle(true); + } + + public async Task QuickSwitchAccount() + { + if (!_runningState.GetIdle()) + { + return; + } + + _runningState.SetIdle(false); + + // 虽然更改时已经保存过了,不过保险起见在点击开始之后再次保存任务和基建列表 + TaskItemSelectionChanged(); + InfrastTask.InfrastOrderSelectionChanged(); + + ClearLog(); + + await Task.Run(() => SettingsViewModel.GameSettings.RunScript("StartsWithScript")); + + AddLog(LocalizationHelper.GetString("ConnectingToEmulator")); + + /* + // 现在的主流模拟器都已经更新过自带的 adb 了,不再需要替换 + if (!Instances.SettingsViewModel.AdbReplaced && !Instances.SettingsViewModel.IsAdbTouchMode()) + { + AddLog(LocalizationHelper.GetString("AdbReplacementTips"), UiLogColor.Info); + } + */ + + // 一般是点了“停止”按钮了 + if (_runningState.GetStopping()) + { + SetStopped(); + return; + } + + if (!await ConnectToEmulator()) + { + return; + } + + // 一般是点了“停止”按钮了 + if (_runningState.GetStopping()) + { + SetStopped(); + return; + } + + bool taskRet = true; + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.StartUp, StartUpTask.Serialize()); + taskRet &= Instances.AsstProxy.AsstStart(); + + if (taskRet) + { + AddLog(LocalizationHelper.GetString("Running")); + } + else + { + AddLog(LocalizationHelper.GetString("UnknownErrorOccurs")); + await Stop(); + SetStopped(); + } + } + + public bool AppendFight() + { + string curStage = FightTask.Stage; + + var (type, mainParam) = FightTask.Serialize(); + bool mainFightRet = Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Fight, type, mainParam); + if (!mainFightRet) + { + AddLog(LocalizationHelper.GetString("UnsupportedStages") + ": " + curStage, UiLogColor.Error); + return false; + } + + if ((curStage == "Annihilation") && FightTask.UseAlternateStage) + { + foreach (var stage in FightTask.Stages) + { + if (stage is null || !IsStageOpen(stage) || (stage == curStage)) { - Stage = FightTask.RemainingSanityStage, - MaxTimes = int.MaxValue, - Series = 0, - IsDrGrandet = FightTask.IsDrGrandet, - ReportToPenguin = SettingsViewModel.GameSettings.EnablePenguin, - ReportToYituliu = SettingsViewModel.GameSettings.EnableYituliu, - PenguinId = SettingsViewModel.GameSettings.PenguinId, - YituliuId = SettingsViewModel.GameSettings.PenguinId, - ServerType = Instances.SettingsViewModel.ServerType, - ClientType = SettingsViewModel.GameSettings.ClientType, - }; - return Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.FightRemainingSanity, type, task.Serialize().Params); - } + continue; + } - return mainFightRet; + AddLog(LocalizationHelper.GetString("AnnihilationTaskTip"), UiLogColor.Info); + var task = mainParam.ToObject(); + if (task != null) + { + task.Stage = stage; + task.Stone = 0; + task.MaxTimes = int.MaxValue; + task.Drops = []; + mainFightRet = Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.FightAnnihilationAlternate, type, task.Serialize().Params); + } + + break; + } } - public bool EnableSetFightParams { get; set; } = true; - - /// - /// Sets parameters. - /// - public void SetFightParams() + if (mainFightRet && FightTask.UseRemainingSanityStage && !string.IsNullOrEmpty(FightTask.RemainingSanityStage)) { - var type = TaskType.Fight; - var id = Instances.AsstProxy.TasksStatus.FirstOrDefault(t => t.Value.Type == type).Key; - if (!EnableSetFightParams || id == 0) - { - return; - } - - var taskParams = FightTask.Serialize().Params; - Instances.AsstProxy.AsstSetTaskParamsEncoded(id, taskParams); - } - - public static void SetFightRemainingSanityParams() - { - var type = TaskType.FightRemainingSanity; - var id = Instances.AsstProxy.TasksStatus.FirstOrDefault(t => t.Value.Type == type).Key; - if (id == 0) - { - return; - } - var task = new AsstFightTask() { - Stage = FightTask.RemainingSanityStage ?? string.Empty, + Stage = FightTask.RemainingSanityStage, MaxTimes = int.MaxValue, Series = 0, IsDrGrandet = FightTask.IsDrGrandet, @@ -1612,155 +1566,200 @@ namespace MaaWpfGui.ViewModels.UI ServerType = Instances.SettingsViewModel.ServerType, ClientType = SettingsViewModel.GameSettings.ClientType, }; - - var taskParams = task.Serialize().Params; - Instances.AsstProxy.AsstSetTaskParamsEncoded(id, taskParams); + return Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.FightRemainingSanity, type, task.Serialize().Params); } - public bool AppendInfrast() + return mainFightRet; + } + + public bool EnableSetFightParams { get; set; } = true; + + /// + /// Sets parameters. + /// + public void SetFightParams() + { + var type = TaskType.Fight; + var id = Instances.AsstProxy.TasksStatus.FirstOrDefault(t => t.Value.Type == type).Key; + if (!EnableSetFightParams || id == 0) { - if (InfrastTask.InfrastMode == InfrastMode.Custom && (!File.Exists(InfrastTask.CustomInfrastFile) || InfrastTask.CustomInfrastPlanInfoList.Count == 0)) + return; + } + + var taskParams = FightTask.Serialize().Params; + Instances.AsstProxy.AsstSetTaskParamsEncoded(id, taskParams); + } + + public static void SetFightRemainingSanityParams() + { + var type = TaskType.FightRemainingSanity; + var id = Instances.AsstProxy.TasksStatus.FirstOrDefault(t => t.Value.Type == type).Key; + if (id == 0) + { + return; + } + + var task = new AsstFightTask() + { + Stage = FightTask.RemainingSanityStage ?? string.Empty, + MaxTimes = int.MaxValue, + Series = 0, + IsDrGrandet = FightTask.IsDrGrandet, + ReportToPenguin = SettingsViewModel.GameSettings.EnablePenguin, + ReportToYituliu = SettingsViewModel.GameSettings.EnableYituliu, + PenguinId = SettingsViewModel.GameSettings.PenguinId, + YituliuId = SettingsViewModel.GameSettings.PenguinId, + ServerType = Instances.SettingsViewModel.ServerType, + ClientType = SettingsViewModel.GameSettings.ClientType, + }; + + var taskParams = task.Serialize().Params; + Instances.AsstProxy.AsstSetTaskParamsEncoded(id, taskParams); + } + + public bool AppendInfrast() + { + if (InfrastTask.InfrastMode == InfrastMode.Custom && (!File.Exists(InfrastTask.CustomInfrastFile) || InfrastTask.CustomInfrastPlanInfoList.Count == 0)) + { + AddLog(LocalizationHelper.GetString("CustomizeInfrastSelectionEmpty"), UiLogColor.Error); + return false; + } + + return Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Infrast, InfrastTask.Serialize()); + } + + private bool _inited = false; + + public bool Inited + { + get => _inited; + set => SetAndNotify(ref _inited, value); + } + + private bool _idle; + + /// + /// Gets or sets a value indicating whether it is idle. + /// + public bool Idle + { + get => _idle; + set + { + SetAndNotify(ref _idle, value); + if (!value) { - AddLog(LocalizationHelper.GetString("CustomizeInfrastSelectionEmpty"), UiLogColor.Error); - return false; + return; } - return Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Infrast, InfrastTask.Serialize()); + UpdateMainTasksProgress(0); } + } - private bool _inited = false; + private bool _stopping; - public bool Inited + /// + /// Gets a value indicating whether `stop` is awaiting. + /// + public bool Stopping + { + get => _stopping; + private set => SetAndNotify(ref _stopping, value); + } + + private bool _waiting; + + /// + /// Gets a value indicating whether waiting for roguelike combat complete. + /// + public bool Waiting + { + // UI 会根据这个值来改变 Visibility + [UsedImplicitly] + get => _waiting; + private set => SetAndNotify(ref _waiting, value); + } + + /* + private bool _shutdown = false; + + public bool Shutdown + { + get => return _shutdown; + set { - get => _inited; - set => SetAndNotify(ref _inited, value); - } + SetAndNotify(ref _shutdown, value); - private bool _idle; - - /// - /// Gets or sets a value indicating whether it is idle. - /// - public bool Idle - { - get => _idle; - set + if (value) { - SetAndNotify(ref _idle, value); - if (!value) - { - return; - } - - UpdateMainTasksProgress(0); - } - } - - private bool _stopping; - - /// - /// Gets a value indicating whether `stop` is awaiting. - /// - public bool Stopping - { - get => _stopping; - private set => SetAndNotify(ref _stopping, value); - } - - private bool _waiting; - - /// - /// Gets a value indicating whether waiting for roguelike combat complete. - /// - public bool Waiting - { - // UI 会根据这个值来改变 Visibility - [UsedImplicitly] - get => _waiting; - private set => SetAndNotify(ref _waiting, value); - } - - /* - private bool _shutdown = false; - - public bool Shutdown - { - get => return _shutdown; - set - { - SetAndNotify(ref _shutdown, value); - - if (value) - { - Hibernate = false; - Suspend = false; - } - } - } - - private bool _hibernate = false; // 休眠 - - public bool Hibernate - { - get => return _hibernate; - set - { - SetAndNotify(ref _hibernate, value); - - if (value) - { - Shutdown = false; - Suspend = false; - } - } - } - - private bool _suspend = false; // 待机 - - public bool Suspend - { - get => return _suspend; - set - { - SetAndNotify(ref _suspend, value); - - if (value) - { - Shutdown = false; - Hibernate = false; - } - } - } - */ - - private static IEnumerable InitTaskViewModelList() - { - var types = Assembly.GetExecutingAssembly() - .GetTypes() - .Where(t => t is { Namespace: "MaaWpfGui.ViewModels.UserControl.TaskQueue", IsClass: true, IsAbstract: false } && t.IsSubclassOf(typeof(TaskViewModel))); - foreach (var type in types) - { - // 获取 Instance 字段 - if (type.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static) is not { } property) - { - continue; - } - - // 获取实例 - if (property.GetValue(null) is TaskViewModel instance) - { - yield return instance; - } - } - } - - public static void InvokeProcSubTaskMsg(AsstMsg msg, JObject details) - { - foreach (var instance in _taskViewModelTypes) - { - // 调用 ProcSubTaskMsg 方法 - instance.ProcSubTaskMsg(msg, details); + Hibernate = false; + Suspend = false; } } } + + private bool _hibernate = false; // 休眠 + + public bool Hibernate + { + get => return _hibernate; + set + { + SetAndNotify(ref _hibernate, value); + + if (value) + { + Shutdown = false; + Suspend = false; + } + } + } + + private bool _suspend = false; // 待机 + + public bool Suspend + { + get => return _suspend; + set + { + SetAndNotify(ref _suspend, value); + + if (value) + { + Shutdown = false; + Hibernate = false; + } + } + } + */ + + private static IEnumerable InitTaskViewModelList() + { + var types = Assembly.GetExecutingAssembly() + .GetTypes() + .Where(t => t is { Namespace: "MaaWpfGui.ViewModels.UserControl.TaskQueue", IsClass: true, IsAbstract: false } && t.IsSubclassOf(typeof(TaskViewModel))); + foreach (var type in types) + { + // 获取 Instance 字段 + if (type.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static) is not { } property) + { + continue; + } + + // 获取实例 + if (property.GetValue(null) is TaskViewModel instance) + { + yield return instance; + } + } + } + + public static void InvokeProcSubTaskMsg(AsstMsg msg, JObject details) + { + foreach (var instance in _taskViewModelTypes) + { + // 调用 ProcSubTaskMsg 方法 + instance.ProcSubTaskMsg(msg, details); + } + } } diff --git a/src/MaaWpfGui/ViewModels/UI/ToolboxViewModel.cs b/src/MaaWpfGui/ViewModels/UI/ToolboxViewModel.cs index fe1b1cc0b9..0e95f9e014 100644 --- a/src/MaaWpfGui/ViewModels/UI/ToolboxViewModel.cs +++ b/src/MaaWpfGui/ViewModels/UI/ToolboxViewModel.cs @@ -39,1260 +39,1259 @@ using Serilog; using Stylet; using Timer = System.Timers.Timer; -namespace MaaWpfGui.ViewModels.UI +namespace MaaWpfGui.ViewModels.UI; + +/// +/// The view model of recruit. +/// +public class ToolboxViewModel : Screen { + private readonly RunningState _runningState; + private static readonly ILogger _logger = Log.ForContext(); + /// - /// The view model of recruit. + /// Initializes a new instance of the class. /// - public class ToolboxViewModel : Screen + public ToolboxViewModel() { - private readonly RunningState _runningState; - private static readonly ILogger _logger = Log.ForContext(); - - /// - /// Initializes a new instance of the class. - /// - public ToolboxViewModel() + DisplayName = LocalizationHelper.GetString("Toolbox"); + _runningState = RunningState.Instance; + _runningState.StateChanged += (__, e) => { - DisplayName = LocalizationHelper.GetString("Toolbox"); - _runningState = RunningState.Instance; - _runningState.StateChanged += (__, e) => + Idle = e.Idle; + Inited = e.Inited; + Stopping = e.Stopping; + + if (e.Stopping && Peeping && !IsPeepTransitioning) { - Idle = e.Idle; - Inited = e.Inited; - Stopping = e.Stopping; + _ = Peep(); + } + }; + _peepImageTimer.Elapsed += PeepImageTimerElapsed; + _peepImageTimer.Interval = 1000d / PeepTargetFps; + _gachaTimer.Tick += RefreshGachaTip; + LoadDepotDetails(); + LoadOperBoxDetails(); + OperBoxSelectedIndex = OperBoxNotHaveList.Count > 0 ? 0 : 1; + } - if (e.Stopping && Peeping && !IsPeepTransitioning) - { - _ = Peep(); - } - }; - _peepImageTimer.Elapsed += PeepImageTimerElapsed; - _peepImageTimer.Interval = 1000d / PeepTargetFps; - _gachaTimer.Tick += RefreshGachaTip; - LoadDepotDetails(); - LoadOperBoxDetails(); - OperBoxSelectedIndex = OperBoxNotHaveList.Count > 0 ? 0 : 1; - } + private bool _idle; - private bool _idle; + /// + /// Gets or sets a value indicating whether it is idle. + /// + public bool Idle + { + get => _idle; + set => SetAndNotify(ref _idle, value); + } - /// - /// Gets or sets a value indicating whether it is idle. - /// - public bool Idle + private bool _inited; + + public bool Inited + { + get => _inited; + set => SetAndNotify(ref _inited, value); + } + + private bool _stopping; + + public bool Stopping + { + get => _stopping; + set => SetAndNotify(ref _stopping, value); + } + + #region Recruit + + private string _recruitInfo = LocalizationHelper.GetString("RecruitmentRecognitionTip"); + + /// + /// Gets or sets the recruit info. + /// + public string RecruitInfo + { + get => _recruitInfo; + set => SetAndNotify(ref _recruitInfo, value); + } + + private ObservableCollection _recruitResultInlines = []; + + public ObservableCollection RecruitResultInlines + { + get => _recruitResultInlines; + set => SetAndNotify(ref _recruitResultInlines, value); + } + + public void UpdateRecruitResult(JArray? resultArray) + { + ObservableCollection recruitResultInlines = []; + + foreach (var combs in resultArray ?? []) { - get => _idle; - set => SetAndNotify(ref _idle, value); - } + int tagLevel = (int)(combs["level"] ?? -1); + var tagStr = $"{tagLevel}★ Tags: "; + tagStr = ((JArray?)combs["tags"] ?? []).Aggregate(tagStr, (current, tag) => current + $"{tag} "); + var tagRun = new Run(tagStr); + tagRun.SetResourceReference(TextElement.ForegroundProperty, UiLogColor.Text); + tagRun.Tag = UiLogColor.Text; - private bool _inited; + recruitResultInlines.Add(tagRun); - public bool Inited - { - get => _inited; - set => SetAndNotify(ref _inited, value); - } + recruitResultInlines.Add(new LineBreak()); - private bool _stopping; + var opersArray = (JArray?)combs["opers"] ?? []; - public bool Stopping - { - get => _stopping; - set => SetAndNotify(ref _stopping, value); - } - - #region Recruit - - private string _recruitInfo = LocalizationHelper.GetString("RecruitmentRecognitionTip"); - - /// - /// Gets or sets the recruit info. - /// - public string RecruitInfo - { - get => _recruitInfo; - set => SetAndNotify(ref _recruitInfo, value); - } - - private ObservableCollection _recruitResultInlines = []; - - public ObservableCollection RecruitResultInlines - { - get => _recruitResultInlines; - set => SetAndNotify(ref _recruitResultInlines, value); - } - - public void UpdateRecruitResult(JArray? resultArray) - { - ObservableCollection recruitResultInlines = []; - - foreach (var combs in resultArray ?? []) + var opersWithPotential = opersArray.Select(oper => { - int tagLevel = (int)(combs["level"] ?? -1); - var tagStr = $"{tagLevel}★ Tags: "; - tagStr = ((JArray?)combs["tags"] ?? []).Aggregate(tagStr, (current, tag) => current + $"{tag} "); - var tagRun = new Run(tagStr); - tagRun.SetResourceReference(TextElement.ForegroundProperty, UiLogColor.Text); - tagRun.Tag = UiLogColor.Text; + int operLevel = (int)(oper["level"] ?? -1); + var operId = oper["id"]?.ToString(); - recruitResultInlines.Add(tagRun); - - recruitResultInlines.Add(new LineBreak()); - - var opersArray = (JArray?)combs["opers"] ?? []; - - var opersWithPotential = opersArray.Select(oper => + int pot = -1; + if (RecruitmentShowPotential && OperBoxPotential != null && operId != null && (tagLevel >= 4 || operLevel == 1)) { - int operLevel = (int)(oper["level"] ?? -1); - var operId = oper["id"]?.ToString(); - - int pot = -1; - if (RecruitmentShowPotential && OperBoxPotential != null && operId != null && (tagLevel >= 4 || operLevel == 1)) + if (OperBoxPotential.TryGetValue(operId, out var potentialValue)) { - if (OperBoxPotential.TryGetValue(operId, out var potentialValue)) - { - pot = potentialValue; - } + pot = potentialValue; } - - return new { Oper = oper, Potential = pot, OperLevel = operLevel }; - }) - .OrderByDescending(x => x.OperLevel) - .ThenBy(x => x.Potential) - .ToList(); - - foreach (var x in opersWithPotential) - { - var oper = x.Oper; - int operLevel = x.OperLevel; - var operId = oper["id"]?.ToString(); - var operName = DataHelper.GetLocalizedCharacterName(oper["name"]?.ToString()); - - bool isMaxPot = false; - string potentialText = string.Empty; - - if (RecruitmentShowPotential && OperBoxPotential != null && operId != null && (tagLevel >= 4 || operLevel == 1)) - { - if (OperBoxPotential.TryGetValue(operId, out var pot)) - { - potentialText = $" ( {pot} )"; - if (pot == 6) - { - isMaxPot = true; - potentialText = " ( MAX )"; - } - } - else - { - potentialText = " ( !!! NEW !!! )"; - } - } - - var run = new Run($"{operName}{potentialText} "); - var brushKey = GetBrushKeyByStar(operLevel, isMaxPot); - run.SetResourceReference(TextElement.ForegroundProperty, brushKey); - run.Tag = brushKey; - - recruitResultInlines.Add(run); } - recruitResultInlines.Add(new LineBreak()); - recruitResultInlines.Add(new LineBreak()); + return new { Oper = oper, Potential = pot, OperLevel = operLevel }; + }) + .OrderByDescending(x => x.OperLevel) + .ThenBy(x => x.Potential) + .ToList(); + + foreach (var x in opersWithPotential) + { + var oper = x.Oper; + int operLevel = x.OperLevel; + var operId = oper["id"]?.ToString(); + var operName = DataHelper.GetLocalizedCharacterName(oper["name"]?.ToString()); + + bool isMaxPot = false; + string potentialText = string.Empty; + + if (RecruitmentShowPotential && OperBoxPotential != null && operId != null && (tagLevel >= 4 || operLevel == 1)) + { + if (OperBoxPotential.TryGetValue(operId, out var pot)) + { + potentialText = $" ( {pot} )"; + if (pot == 6) + { + isMaxPot = true; + potentialText = " ( MAX )"; + } + } + else + { + potentialText = " ( !!! NEW !!! )"; + } + } + + var run = new Run($"{operName}{potentialText} "); + var brushKey = GetBrushKeyByStar(operLevel, isMaxPot); + run.SetResourceReference(TextElement.ForegroundProperty, brushKey); + run.Tag = brushKey; + + recruitResultInlines.Add(run); } - RecruitResultInlines = recruitResultInlines; + recruitResultInlines.Add(new LineBreak()); + recruitResultInlines.Add(new LineBreak()); + } + + RecruitResultInlines = recruitResultInlines; + return; + + string GetBrushKeyByStar(int level, bool isMax) + { + return (level, isMax) switch + { + (6, true) => UiLogColor.Star6OperatorPotentialFull, + (6, false) => UiLogColor.Star6Operator, + (5, true) => UiLogColor.Star5OperatorPotentialFull, + (5, false) => UiLogColor.Star5Operator, + (4, true) => UiLogColor.Star4OperatorPotentialFull, + (4, false) => UiLogColor.Star4Operator, + (3, true) => UiLogColor.Star3OperatorPotentialFull, + (3, false) => UiLogColor.Star3Operator, + (2, true) => UiLogColor.Star2OperatorPotentialFull, + (2, false) => UiLogColor.Star2Operator, + (1, true) => UiLogColor.Star1OperatorPotentialFull, + (1, false) => UiLogColor.Star1Operator, + _ => UiLogColor.Text, + }; + } + } + + private bool _chooseLevel3 = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ChooseLevel3, bool.FalseString)); + + /// + /// Gets or sets a value indicating whether to choose level 3. + /// + public bool ChooseLevel3 + { + get => _chooseLevel3; + set + { + SetAndNotify(ref _chooseLevel3, value); + ConfigurationHelper.SetValue(ConfigurationKeys.ChooseLevel3, value.ToString()); + } + } + + private bool _chooseLevel4 = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ChooseLevel4, bool.TrueString)); + + /// + /// Gets or sets a value indicating whether to choose level 4. + /// + public bool ChooseLevel4 + { + get => _chooseLevel4; + set + { + SetAndNotify(ref _chooseLevel4, value); + ConfigurationHelper.SetValue(ConfigurationKeys.ChooseLevel4, value.ToString()); + } + } + + private bool _chooseLevel5 = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ChooseLevel5, bool.TrueString)); + + /// + /// Gets or sets a value indicating whether to choose level 5. + /// + public bool ChooseLevel5 + { + get => _chooseLevel5; + set + { + SetAndNotify(ref _chooseLevel5, value); + ConfigurationHelper.SetValue(ConfigurationKeys.ChooseLevel5, value.ToString()); + } + } + + private bool _chooseLevel6 = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ChooseLevel6, bool.TrueString)); + + /// + /// Gets or sets a value indicating whether to choose level 6. + /// + public bool ChooseLevel6 + { + get => _chooseLevel6; + set + { + SetAndNotify(ref _chooseLevel6, value); + ConfigurationHelper.SetValue(ConfigurationKeys.ChooseLevel6, value.ToString()); + } + } + + private bool _autoSetTime = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.AutoSetTime, bool.TrueString)); + + /// + /// Gets or sets a value indicating whether to set time automatically. + /// + public bool RecruitAutoSetTime + { + get => _autoSetTime; + set + { + SetAndNotify(ref _autoSetTime, value); + ConfigurationHelper.SetValue(ConfigurationKeys.AutoSetTime, value.ToString()); + } + } + + /// + /// Starts calculation. + /// UI 绑定的方法 + /// + /// Task + [UsedImplicitly] + public async Task RecruitStartCalc() + { + string errMsg = string.Empty; + RecruitInfo = LocalizationHelper.GetString("ConnectingToEmulator"); + _runningState.SetIdle(false); + var recruitCaught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + if (!recruitCaught) + { + RecruitInfo = errMsg; + _runningState.SetIdle(true); return; + } - string GetBrushKeyByStar(int level, bool isMax) - { - return (level, isMax) switch + RecruitInfo = LocalizationHelper.GetString("Identifying"); + + var levelList = new List(); + + if (ChooseLevel3) + { + levelList.Add(3); + } + + if (ChooseLevel4) + { + levelList.Add(4); + } + + if (ChooseLevel5) + { + levelList.Add(5); + } + + if (ChooseLevel6) + { + levelList.Add(6); + } + + var task = new AsstRecruitTask() + { + SelectList = levelList, + ConfirmList = [-1], // 仅公招识别时将-1加入comfirm_level + SetRecruitTime = RecruitAutoSetTime, + ChooseLevel3Time = TaskQueueViewModel.RecruitTask.ChooseLevel3Time, + ChooseLevel4Time = TaskQueueViewModel.RecruitTask.ChooseLevel4Time, + ChooseLevel5Time = TaskQueueViewModel.RecruitTask.ChooseLevel5Time, + ServerType = Instances.SettingsViewModel.ServerType, + }; + var (type, taskParams) = task.Serialize(); + bool ret = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.RecruitCalc, type, taskParams); + ret &= Instances.AsstProxy.AsstStart(); + } + + private bool _recruitmentShowPotential = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.RecruitmentShowPotential, bool.TrueString)); + + public bool RecruitmentShowPotential + { + get => _recruitmentShowPotential; + set + { + SetAndNotify(ref _recruitmentShowPotential, value); + ConfigurationHelper.SetValue(ConfigurationKeys.RecruitmentShowPotential, value.ToString()); + } + } + + public void ProcRecruitMsg(JObject details) + { + string? what = details["what"]?.ToString(); + var subTaskDetails = details["details"]; + + switch (what) + { + case "RecruitTagsDetected": { - (6, true) => UiLogColor.Star6OperatorPotentialFull, - (6, false) => UiLogColor.Star6Operator, - (5, true) => UiLogColor.Star5OperatorPotentialFull, - (5, false) => UiLogColor.Star5Operator, - (4, true) => UiLogColor.Star4OperatorPotentialFull, - (4, false) => UiLogColor.Star4Operator, - (3, true) => UiLogColor.Star3OperatorPotentialFull, - (3, false) => UiLogColor.Star3Operator, - (2, true) => UiLogColor.Star2OperatorPotentialFull, - (2, false) => UiLogColor.Star2Operator, - (1, true) => UiLogColor.Star1OperatorPotentialFull, - (1, false) => UiLogColor.Star1Operator, - _ => UiLogColor.Text, - }; - } - } + JArray? tags = (JArray?)subTaskDetails?["tags"]; + string infoContent = LocalizationHelper.GetString("RecruitTagsDetected"); + tags ??= []; + infoContent = tags.Select(tagName => tagName.ToString()).Aggregate(infoContent, (current, tagStr) => current + (tagStr + " ")); - private bool _chooseLevel3 = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ChooseLevel3, bool.FalseString)); - - /// - /// Gets or sets a value indicating whether to choose level 3. - /// - public bool ChooseLevel3 - { - get => _chooseLevel3; - set - { - SetAndNotify(ref _chooseLevel3, value); - ConfigurationHelper.SetValue(ConfigurationKeys.ChooseLevel3, value.ToString()); - } - } - - private bool _chooseLevel4 = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ChooseLevel4, bool.TrueString)); - - /// - /// Gets or sets a value indicating whether to choose level 4. - /// - public bool ChooseLevel4 - { - get => _chooseLevel4; - set - { - SetAndNotify(ref _chooseLevel4, value); - ConfigurationHelper.SetValue(ConfigurationKeys.ChooseLevel4, value.ToString()); - } - } - - private bool _chooseLevel5 = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ChooseLevel5, bool.TrueString)); - - /// - /// Gets or sets a value indicating whether to choose level 5. - /// - public bool ChooseLevel5 - { - get => _chooseLevel5; - set - { - SetAndNotify(ref _chooseLevel5, value); - ConfigurationHelper.SetValue(ConfigurationKeys.ChooseLevel5, value.ToString()); - } - } - - private bool _chooseLevel6 = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ChooseLevel6, bool.TrueString)); - - /// - /// Gets or sets a value indicating whether to choose level 6. - /// - public bool ChooseLevel6 - { - get => _chooseLevel6; - set - { - SetAndNotify(ref _chooseLevel6, value); - ConfigurationHelper.SetValue(ConfigurationKeys.ChooseLevel6, value.ToString()); - } - } - - private bool _autoSetTime = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.AutoSetTime, bool.TrueString)); - - /// - /// Gets or sets a value indicating whether to set time automatically. - /// - public bool RecruitAutoSetTime - { - get => _autoSetTime; - set - { - SetAndNotify(ref _autoSetTime, value); - ConfigurationHelper.SetValue(ConfigurationKeys.AutoSetTime, value.ToString()); - } - } - - /// - /// Starts calculation. - /// UI 绑定的方法 - /// - /// Task - [UsedImplicitly] - public async Task RecruitStartCalc() - { - string errMsg = string.Empty; - RecruitInfo = LocalizationHelper.GetString("ConnectingToEmulator"); - _runningState.SetIdle(false); - var recruitCaught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); - if (!recruitCaught) - { - RecruitInfo = errMsg; - _runningState.SetIdle(true); - return; - } - - RecruitInfo = LocalizationHelper.GetString("Identifying"); - - var levelList = new List(); - - if (ChooseLevel3) - { - levelList.Add(3); - } - - if (ChooseLevel4) - { - levelList.Add(4); - } - - if (ChooseLevel5) - { - levelList.Add(5); - } - - if (ChooseLevel6) - { - levelList.Add(6); - } - - var task = new AsstRecruitTask() - { - SelectList = levelList, - ConfirmList = [-1], // 仅公招识别时将-1加入comfirm_level - SetRecruitTime = RecruitAutoSetTime, - ChooseLevel3Time = TaskQueueViewModel.RecruitTask.ChooseLevel3Time, - ChooseLevel4Time = TaskQueueViewModel.RecruitTask.ChooseLevel4Time, - ChooseLevel5Time = TaskQueueViewModel.RecruitTask.ChooseLevel5Time, - ServerType = Instances.SettingsViewModel.ServerType, - }; - var (type, taskParams) = task.Serialize(); - bool ret = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.RecruitCalc, type, taskParams); - ret &= Instances.AsstProxy.AsstStart(); - } - - private bool _recruitmentShowPotential = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.RecruitmentShowPotential, bool.TrueString)); - - public bool RecruitmentShowPotential - { - get => _recruitmentShowPotential; - set - { - SetAndNotify(ref _recruitmentShowPotential, value); - ConfigurationHelper.SetValue(ConfigurationKeys.RecruitmentShowPotential, value.ToString()); - } - } - - public void ProcRecruitMsg(JObject details) - { - string? what = details["what"]?.ToString(); - var subTaskDetails = details["details"]; - - switch (what) - { - case "RecruitTagsDetected": - { - JArray? tags = (JArray?)subTaskDetails?["tags"]; - string infoContent = LocalizationHelper.GetString("RecruitTagsDetected"); - tags ??= []; - infoContent = tags.Select(tagName => tagName.ToString()).Aggregate(infoContent, (current, tagStr) => current + (tagStr + " ")); - - RecruitInfo = infoContent; - } - - break; - - case "RecruitResult": - { - JArray? resultArray = (JArray?)subTaskDetails?["result"]; - UpdateRecruitResult(resultArray); - } - - break; - } - } - - #endregion Recruit - - #region Depot - - private string _depotInfo = string.Empty; - - /// - /// Gets or sets the depot info. - /// - public string DepotInfo - { - get => _depotInfo; - set => SetAndNotify(ref _depotInfo, value); - } - - private ObservableCollection _depotResult = []; - - /// - /// Gets or sets the depot result. - /// - public ObservableCollection DepotResult - { - get => _depotResult; - set => SetAndNotify(ref _depotResult, value); - } - - public class DepotResultDate - { - public string? Name { get; set; } - - public string Id { get; set; } = null!; - - public BitmapSource? Image { get; set; } - - public string? Count { get; set; } - } - - private void SaveDepotDetails(JObject details) - { - // var json = details.ToString(Formatting.None); - // ConfigurationHelper.SetValue(ConfigurationKeys.DepotResult, json); - JsonDataHelper.Set(JsonDataKey.DepotData, details); - } - - private void LoadDepotDetails() - { - // TODO: 删除老数据节省 gui.json 的大小,后续版本可以删除 - // var json = ConfigurationHelper.GetValue(ConfigurationKeys.DepotResult, string.Empty); - ConfigurationHelper.DeleteValue(ConfigurationKeys.DepotResult); - var json = JsonDataHelper.Get(JsonDataKey.DepotData, string.Empty); - if (string.IsNullOrWhiteSpace(json)) - { - return; - } - - try - { - var details = JObject.Parse(json); - DepotParse(details); - } - catch - { - // 兼容老数据或异常时忽略 - } - } - - /// - /// Gets or sets the ArkPlanner result. - /// - public string ArkPlannerResult { get; set; } = string.Empty; - - /// - /// Gets or sets the Lolicon result. - /// - public string LoliconResult { get; set; } = string.Empty; - - /// - /// parse of depot recognition result - /// - /// detailed json-style parameters - /// Success or not - public bool DepotParse(JObject? details) - { - if (details == null) - { - return false; - } - - DepotResult.Clear(); - - var sortedItems = details["arkplanner"]?["object"]?["items"] - ?.Cast() - .OrderBy(item => (string?)item["id"]) - ?? Enumerable.Empty(); - - foreach (var item in sortedItems) - { - var id = (string?)item["id"]; - if (id == null) - { - continue; + RecruitInfo = infoContent; } - DepotResultDate result = new() + break; + + case "RecruitResult": { - Id = id, - Name = ItemListHelper.GetItemName(id), - Image = ItemListHelper.GetItemImage(id), - Count = item["have"] != null && int.TryParse(item["have"]?.ToString() ?? "-1", out int haveValue) - ? haveValue.FormatNumber(false) - : "-1", - }; + JArray? resultArray = (JArray?)subTaskDetails?["result"]; + UpdateRecruitResult(resultArray); + } - DepotResult.Add(result); - } + break; + } + } - ArkPlannerResult = details["arkplanner"]?["data"]?.ToString() ?? string.Empty; - LoliconResult = details["lolicon"]?["data"]?.ToString() ?? string.Empty; + #endregion Recruit - bool done = (bool)(details["done"] ?? false); - if (!done) + #region Depot + + private string _depotInfo = string.Empty; + + /// + /// Gets or sets the depot info. + /// + public string DepotInfo + { + get => _depotInfo; + set => SetAndNotify(ref _depotInfo, value); + } + + private ObservableCollection _depotResult = []; + + /// + /// Gets or sets the depot result. + /// + public ObservableCollection DepotResult + { + get => _depotResult; + set => SetAndNotify(ref _depotResult, value); + } + + public class DepotResultDate + { + public string? Name { get; set; } + + public string Id { get; set; } = null!; + + public BitmapSource? Image { get; set; } + + public string? Count { get; set; } + } + + private void SaveDepotDetails(JObject details) + { + // var json = details.ToString(Formatting.None); + // ConfigurationHelper.SetValue(ConfigurationKeys.DepotResult, json); + JsonDataHelper.Set(JsonDataKey.DepotData, details); + } + + private void LoadDepotDetails() + { + // TODO: 删除老数据节省 gui.json 的大小,后续版本可以删除 + // var json = ConfigurationHelper.GetValue(ConfigurationKeys.DepotResult, string.Empty); + ConfigurationHelper.DeleteValue(ConfigurationKeys.DepotResult); + var json = JsonDataHelper.Get(JsonDataKey.DepotData, string.Empty); + if (string.IsNullOrWhiteSpace(json)) + { + return; + } + + try + { + var details = JObject.Parse(json); + DepotParse(details); + } + catch + { + // 兼容老数据或异常时忽略 + } + } + + /// + /// Gets or sets the ArkPlanner result. + /// + public string ArkPlannerResult { get; set; } = string.Empty; + + /// + /// Gets or sets the Lolicon result. + /// + public string LoliconResult { get; set; } = string.Empty; + + /// + /// parse of depot recognition result + /// + /// detailed json-style parameters + /// Success or not + public bool DepotParse(JObject? details) + { + if (details == null) + { + return false; + } + + DepotResult.Clear(); + + var sortedItems = details["arkplanner"]?["object"]?["items"] + ?.Cast() + .OrderBy(item => (string?)item["id"]) + ?? Enumerable.Empty(); + + foreach (var item in sortedItems) + { + var id = (string?)item["id"]; + if (id == null) { - return true; + continue; } - DepotInfo = LocalizationHelper.GetString("IdentificationCompleted"); - SaveDepotDetails(details); + DepotResultDate result = new() + { + Id = id, + Name = ItemListHelper.GetItemName(id), + Image = ItemListHelper.GetItemImage(id), + Count = item["have"] != null && int.TryParse(item["have"]?.ToString() ?? "-1", out int haveValue) + ? haveValue.FormatNumber(false) + : "-1", + }; + DepotResult.Add(result); + } + + ArkPlannerResult = details["arkplanner"]?["data"]?.ToString() ?? string.Empty; + LoliconResult = details["lolicon"]?["data"]?.ToString() ?? string.Empty; + + bool done = (bool)(details["done"] ?? false); + if (!done) + { return true; } - /// - /// Export depot info to ArkPlanner. - /// UI 绑定的方法 - /// - [UsedImplicitly] - public void ExportToArkplanner() + DepotInfo = LocalizationHelper.GetString("IdentificationCompleted"); + SaveDepotDetails(details); + + return true; + } + + /// + /// Export depot info to ArkPlanner. + /// UI 绑定的方法 + /// + [UsedImplicitly] + public void ExportToArkplanner() + { + System.Windows.Forms.Clipboard.Clear(); + System.Windows.Forms.Clipboard.SetDataObject(ArkPlannerResult); + DepotInfo = LocalizationHelper.GetString("CopiedToClipboard"); + } + + /// + /// Export depot info to Lolicon. + /// UI 绑定的方法 + /// + [UsedImplicitly] + public void ExportToLolicon() + { + System.Windows.Forms.Clipboard.Clear(); + System.Windows.Forms.Clipboard.SetDataObject(LoliconResult); + DepotInfo = LocalizationHelper.GetString("CopiedToClipboard"); + } + + private void DepotClear() + { + DepotResult.Clear(); + ArkPlannerResult = string.Empty; + LoliconResult = string.Empty; + } + + /// + /// Starts depot recognition. + /// UI 绑定的方法 + /// + /// Task + [UsedImplicitly] + public async Task StartDepot() + { + _runningState.SetIdle(false); + string errMsg = string.Empty; + DepotInfo = LocalizationHelper.GetString("ConnectingToEmulator"); + bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + if (!caught) { - System.Windows.Forms.Clipboard.Clear(); - System.Windows.Forms.Clipboard.SetDataObject(ArkPlannerResult); - DepotInfo = LocalizationHelper.GetString("CopiedToClipboard"); + DepotInfo = errMsg; + _runningState.SetIdle(true); + return; } - /// - /// Export depot info to Lolicon. - /// UI 绑定的方法 - /// - [UsedImplicitly] - public void ExportToLolicon() - { - System.Windows.Forms.Clipboard.Clear(); - System.Windows.Forms.Clipboard.SetDataObject(LoliconResult); - DepotInfo = LocalizationHelper.GetString("CopiedToClipboard"); - } + DepotInfo = LocalizationHelper.GetString("Identifying"); + DepotClear(); - private void DepotClear() - { - DepotResult.Clear(); - ArkPlannerResult = string.Empty; - LoliconResult = string.Empty; - } + Instances.AsstProxy.AsstStartDepot(); + } - /// - /// Starts depot recognition. - /// UI 绑定的方法 - /// - /// Task - [UsedImplicitly] - public async Task StartDepot() + #endregion Depot + + #region OperBox + + private int _operBoxSelectedIndex = 0; + + public int OperBoxSelectedIndex + { + get => _operBoxSelectedIndex; + set => SetAndNotify(ref _operBoxSelectedIndex, value); + } + + private string _operBoxInfo = LocalizationHelper.GetString("OperBoxRecognitionTip"); + + public string OperBoxInfo + { + get => _operBoxInfo; + set => SetAndNotify(ref _operBoxInfo, value); + } + + private List _operBoxDataArray = []; + + public List OperBoxDataArray + { + get => _operBoxDataArray; + set { - _runningState.SetIdle(false); - string errMsg = string.Empty; - DepotInfo = LocalizationHelper.GetString("ConnectingToEmulator"); - bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); - if (!caught) + SetAndNotify(ref _operBoxDataArray, value); + _operBoxPotential = null; // reset + } + } + + private Dictionary? _operBoxPotential; + + public Dictionary? OperBoxPotential + { + get + { + if (_operBoxPotential != null) { - DepotInfo = errMsg; - _runningState.SetIdle(true); - return; - } - - DepotInfo = LocalizationHelper.GetString("Identifying"); - DepotClear(); - - Instances.AsstProxy.AsstStartDepot(); - } - - #endregion Depot - - #region OperBox - - private int _operBoxSelectedIndex = 0; - - public int OperBoxSelectedIndex - { - get => _operBoxSelectedIndex; - set => SetAndNotify(ref _operBoxSelectedIndex, value); - } - - private string _operBoxInfo = LocalizationHelper.GetString("OperBoxRecognitionTip"); - - public string OperBoxInfo - { - get => _operBoxInfo; - set => SetAndNotify(ref _operBoxInfo, value); - } - - private List _operBoxDataArray = []; - - public List OperBoxDataArray - { - get => _operBoxDataArray; - set - { - SetAndNotify(ref _operBoxDataArray, value); - _operBoxPotential = null; // reset - } - } - - private Dictionary? _operBoxPotential; - - public Dictionary? OperBoxPotential - { - get - { - if (_operBoxPotential != null) - { - return _operBoxPotential; - } - - _operBoxPotential = OperBoxDataArray.ToDictionary(oper => oper.Id, oper => oper.Potential); return _operBoxPotential; } + + _operBoxPotential = OperBoxDataArray.ToDictionary(oper => oper.Id, oper => oper.Potential); + return _operBoxPotential; + } + } + + public class Operator(string id, string name, int rarity) + { + [JsonProperty("id")] + public string Id { get; } = id; + + [JsonProperty("name")] + public string Name { get; } = name; + + [JsonProperty("rarity")] + public int Rarity { get; } = rarity; + + public bool Equals(Operator? other) => other != null && Name == other.Name && Rarity == other.Rarity; + + public override bool Equals(object? obj) => obj is Operator other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(Id, Name, Rarity); + + public override string ToString() => $"{Name} (★{Rarity})"; + } + + private ObservableCollection _operBoxHaveList = []; + + public ObservableCollection OperBoxHaveList + { + get => _operBoxHaveList; + set => SetAndNotify(ref _operBoxHaveList, value); + } + + private ObservableCollection _operBoxNotHaveList = []; + + public ObservableCollection OperBoxNotHaveList + { + get => _operBoxNotHaveList; + set => SetAndNotify(ref _operBoxNotHaveList, value); + } + + private static void SaveOperBoxDetails(List details) + { + // var json = details.ToString(Formatting.None); + // ConfigurationHelper.SetValue(ConfigurationKeys.OperBoxData, json); + JsonDataHelper.Set(JsonDataKey.OperBoxData, JArray.FromObject(details)); + } + + private void LoadOperBoxDetails() + { + // TODO: 删除老数据节省 gui.json 的大小,后续版本可以删除 + // var json = ConfigurationHelper.GetValue(ConfigurationKeys.OperBoxData, string.Empty); + ConfigurationHelper.DeleteValue(ConfigurationKeys.OperBoxData); + var json = JsonDataHelper.Get(JsonDataKey.OperBoxData, string.Empty); + if (string.IsNullOrWhiteSpace(json)) + { + return; } - public class Operator(string id, string name, int rarity) + try { - [JsonProperty("id")] - public string Id { get; } = id; - - [JsonProperty("name")] - public string Name { get; } = name; - - [JsonProperty("rarity")] - public int Rarity { get; } = rarity; - - public bool Equals(Operator? other) => other != null && Name == other.Name && Rarity == other.Rarity; - - public override bool Equals(object? obj) => obj is Operator other && Equals(other); - - public override int GetHashCode() => HashCode.Combine(Id, Name, Rarity); - - public override string ToString() => $"{Name} (★{Rarity})"; - } - - private ObservableCollection _operBoxHaveList = []; - - public ObservableCollection OperBoxHaveList - { - get => _operBoxHaveList; - set => SetAndNotify(ref _operBoxHaveList, value); - } - - private ObservableCollection _operBoxNotHaveList = []; - - public ObservableCollection OperBoxNotHaveList - { - get => _operBoxNotHaveList; - set => SetAndNotify(ref _operBoxNotHaveList, value); - } - - private static void SaveOperBoxDetails(List details) - { - // var json = details.ToString(Formatting.None); - // ConfigurationHelper.SetValue(ConfigurationKeys.OperBoxData, json); - JsonDataHelper.Set(JsonDataKey.OperBoxData, JArray.FromObject(details)); - } - - private void LoadOperBoxDetails() - { - // TODO: 删除老数据节省 gui.json 的大小,后续版本可以删除 - // var json = ConfigurationHelper.GetValue(ConfigurationKeys.OperBoxData, string.Empty); - ConfigurationHelper.DeleteValue(ConfigurationKeys.OperBoxData); - var json = JsonDataHelper.Get(JsonDataKey.OperBoxData, string.Empty); - if (string.IsNullOrWhiteSpace(json)) + var ownOpers = JsonConvert.DeserializeObject>(json)?.Where(i => !string.IsNullOrEmpty(i.Id)).ToList(); + if (ownOpers is null) { return; } - try + OperBoxDataArray = ownOpers; + var ids = ownOpers.Select(o => o.Id).ToHashSet(); + foreach (var (id, oper) in DataHelper.Operators) { - var ownOpers = JsonConvert.DeserializeObject>(json)?.Where(i => !string.IsNullOrEmpty(i.Id)).ToList(); - if (ownOpers is null) + if (DataHelper.IsCharacterAvailableInClient(oper, SettingsViewModel.GameSettings.ClientType)) { - return; - } - - OperBoxDataArray = ownOpers; - var ids = ownOpers.Select(o => o.Id).ToHashSet(); - foreach (var (id, oper) in DataHelper.Operators) - { - if (DataHelper.IsCharacterAvailableInClient(oper, SettingsViewModel.GameSettings.ClientType)) + var name = DataHelper.GetLocalizedCharacterName(oper) ?? "???"; + if (ids.Contains(id)) { - var name = DataHelper.GetLocalizedCharacterName(oper) ?? "???"; - if (ids.Contains(id)) - { - OperBoxHaveList.Add(new Operator(id, name, oper.Rarity)); - } - else - { - OperBoxNotHaveList.Add(new Operator(id, name, oper.Rarity)); - } + OperBoxHaveList.Add(new Operator(id, name, oper.Rarity)); + } + else + { + OperBoxNotHaveList.Add(new Operator(id, name, oper.Rarity)); } } } - catch + } + catch + { + // 兼容老数据或异常时忽略 + } + } + + /// + /// 每次传进来的都是完整数据, 临时缓存去重 + /// + private HashSet _tempOperHaveSet = []; + + public bool OperBoxParse(JObject? details) + { + if (details == null) + { + return false; + } + + var ownOpers = (details["own_opers"] as JArray)?.ToObject>()?.Where(o => !string.IsNullOrEmpty(o.Id)).ToList(); + if (ownOpers is null) + { + return false; + } + + foreach (var oper in ownOpers) + { + if (_tempOperHaveSet.Add(oper.Id)) { - // 兼容老数据或异常时忽略 + var name = DataHelper.GetLocalizedCharacterName(DataHelper.Operators.FirstOrDefault(i => i.Key == oper.Id).Value) ?? "???"; + OperBoxHaveList.Add(new Operator(oper.Id, name, oper.Rarity)); } } - /// - /// 每次传进来的都是完整数据, 临时缓存去重 - /// - private HashSet _tempOperHaveSet = []; - - public bool OperBoxParse(JObject? details) + bool done = (bool)(details["done"] ?? false); + if (!done) { - if (details == null) - { - return false; - } - - var ownOpers = (details["own_opers"] as JArray)?.ToObject>()?.Where(o => !string.IsNullOrEmpty(o.Id)).ToList(); - if (ownOpers is null) - { - return false; - } - - foreach (var oper in ownOpers) - { - if (_tempOperHaveSet.Add(oper.Id)) - { - var name = DataHelper.GetLocalizedCharacterName(DataHelper.Operators.FirstOrDefault(i => i.Key == oper.Id).Value) ?? "???"; - OperBoxHaveList.Add(new Operator(oper.Id, name, oper.Rarity)); - } - } - - bool done = (bool)(details["done"] ?? false); - if (!done) - { - return true; - } - - foreach (var (id, oper) in DataHelper.Operators) - { - if (!_tempOperHaveSet.Contains(id) && DataHelper.IsCharacterAvailableInClient(oper, SettingsViewModel.GameSettings.ClientType)) - { - var name = DataHelper.GetLocalizedCharacterName(oper) ?? "???"; - OperBoxNotHaveList.Add(new Operator(id, name, oper.Rarity)); - } - } - - if (OperBoxNotHaveList.Count > 0) - { - OperBoxSelectedIndex = 0; - } - - OperBoxInfo = $"{LocalizationHelper.GetString("IdentificationCompleted")}\n{LocalizationHelper.GetString("OperBoxRecognitionTip")}"; - OperBoxDataArray = ownOpers; - SaveOperBoxDetails(ownOpers); - _tempOperHaveSet = []; return true; } - /// - /// 开始识别干员 - /// UI 绑定的方法 - /// - /// Task - [UsedImplicitly] - public async Task StartOperBox() + foreach (var (id, oper) in DataHelper.Operators) { - OperBoxSelectedIndex = 1; - _tempOperHaveSet = []; - OperBoxHaveList = []; - OperBoxNotHaveList = []; - _runningState.SetIdle(false); - - string errMsg = string.Empty; - OperBoxInfo = LocalizationHelper.GetString("ConnectingToEmulator"); - bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); - if (!caught) + if (!_tempOperHaveSet.Contains(id) && DataHelper.IsCharacterAvailableInClient(oper, SettingsViewModel.GameSettings.ClientType)) { - OperBoxInfo = errMsg; - _runningState.SetIdle(true); - return; - } - - if (Instances.AsstProxy.AsstStartOperBox()) - { - OperBoxInfo = LocalizationHelper.GetString("Identifying"); + var name = DataHelper.GetLocalizedCharacterName(oper) ?? "???"; + OperBoxNotHaveList.Add(new Operator(id, name, oper.Rarity)); } } - // UI 绑定的方法 - [UsedImplicitly] - public void ExportOperBox() + if (OperBoxNotHaveList.Count > 0) { - if (OperBoxDataArray.Count == 0) - { - return; - } - - System.Windows.Forms.Clipboard.Clear(); - System.Windows.Forms.Clipboard.SetDataObject(JsonConvert.SerializeObject(OperBoxDataArray, Formatting.Indented)); - OperBoxInfo = LocalizationHelper.GetString("CopiedToClipboard"); + OperBoxSelectedIndex = 0; } - #endregion OperBox - - #region Gacha - - private string _gachaInfo = LocalizationHelper.GetString("GachaInitTip"); - - public string GachaInfo - { - get => _gachaInfo; - set => SetAndNotify(ref _gachaInfo, value); - } - - // UI 绑定的方法 - public async Task GachaOnce() - { - await StartGacha(); - } - - // UI 绑定的方法 - public async Task GachaTenTimes() - { - await StartGacha(false); - } - - private bool _isGachaInProgress; - - public bool IsGachaInProgress - { - get => _isGachaInProgress; - set - { - if (!SetAndNotify(ref _isGachaInProgress, value)) - { - return; - } - - if (!value) - { - _gachaTimer.Stop(); - } - } - } - - public async Task StartGacha(bool once = true) - { - _runningState.SetIdle(false); - - string errMsg = string.Empty; - GachaInfo = LocalizationHelper.GetString("ConnectingToEmulator"); - bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg) && Instances.AsstProxy.AsstStartGacha(once)); - if (!caught) - { - GachaInfo = errMsg; - _runningState.SetIdle(true); - return; - } - - _gachaTimer.Interval = TimeSpan.FromSeconds(5); - _gachaTimer.Start(); - - RefreshGachaTip(null, null); - IsGachaInProgress = true; - _ = Peep(); - } - - private void RefreshGachaTip(object? sender, EventArgs? e) - { - var rd = new Random(); - GachaInfo = LocalizationHelper.GetString("GachaTip" + rd.Next(1, 18)); - } - - // DO NOT CHANGE - // 请勿更改 - // 請勿更改 - // このコードを変更しないでください - // 변경하지 마십시오 - private bool _gachaShowDisclaimer = true; // !Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ShowDisclaimerNoMore, bool.FalseString)); - - public bool GachaShowDisclaimer - { - get => _gachaShowDisclaimer; - set - { - SetAndNotify(ref _gachaShowDisclaimer, value); - } - } - - private bool _gachaShowDisclaimerNoMore = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.GachaShowDisclaimerNoMore, bool.FalseString)); - - public bool GachaShowDisclaimerNoMore - { - get => _gachaShowDisclaimerNoMore; - set - { - SetAndNotify(ref _gachaShowDisclaimerNoMore, value); - ConfigurationHelper.SetValue(ConfigurationKeys.GachaShowDisclaimerNoMore, value.ToString()); - } - } - - // UI 绑定的方法 - [UsedImplicitly] - public void GachaAgreeDisclaimer() - { - var result = MessageBoxHelper.Show( - LocalizationHelper.GetString("GachaWarning"), - LocalizationHelper.GetString("Warning"), - MessageBoxButton.YesNo, - MessageBoxImage.Warning, - no: LocalizationHelper.GetString("Confirm"), - yes: LocalizationHelper.GetString("Cancel"), - iconBrushKey: "DangerBrush"); - if (result != MessageBoxResult.No) - { - return; - } - - AchievementTrackerHelper.Instance.Unlock(AchievementIds.RealGacha); - - GachaShowDisclaimer = false; - } - - #endregion Gacha - - #region Peep - - private bool _peeping; - - public bool Peeping - { - get => _peeping; - set - { - if (!SetAndNotify(ref _peeping, value)) - { - return; - } - - if (!value) - { - _peepImageTimer.Stop(); - } - } - } - - private bool _isPeepInProgress; - - /// - /// Gets or sets a value indicating whether由 Peep 方法启动的 Peep - /// - public bool IsPeepInProgress - { - get => _isPeepInProgress; - set - { - if (!SetAndNotify(ref _isPeepInProgress, value)) - { - return; - } - - if (!value) - { - _peepImageTimer.Stop(); - } - } - } - - private WriteableBitmap? _peepImage; - - public WriteableBitmap? PeepImage - { - get => _peepImage; - set => SetAndNotify(ref _peepImage, value); - } - - private double _peepScreenFpf; - - public double PeepScreenFpf - { - get => _peepScreenFpf; - set => SetAndNotify(ref _peepScreenFpf, value); - } - - private int _peepTargetFps = int.Parse(ConfigurationHelper.GetValue(ConfigurationKeys.PeepTargetFps, "20")); - - public int PeepTargetFps - { - get - { - return _peepTargetFps; - } - - set - { - value = value switch - { - > 600 => 600, - < 1 => 1, - _ => value, - }; - - SetAndNotify(ref _peepTargetFps, value); - _peepImageTimer.Interval = 1000d / _peepTargetFps; - ConfigurationHelper.SetValue(ConfigurationKeys.PeepTargetFps, value.ToString()); - } - } - - private DateTime _lastFpsUpdateTime = DateTime.MinValue; - private int _frameCount; - - private readonly Timer _peepImageTimer = new(); - private readonly DispatcherTimer _gachaTimer = new() { Interval = TimeSpan.FromSeconds(5) }; - - private int _peepImageCount; - private int _peepImageNewestCount; - - private static int _peepImageSemaphoreCurrentCount = 2; - private const int PeepImageSemaphoreMaxCount = 5; - private static int _peepImageSemaphoreFailCount = 0; - private static readonly SemaphoreSlim _peepImageSemaphore = new(_peepImageSemaphoreCurrentCount, PeepImageSemaphoreMaxCount); - - private async void PeepImageTimerElapsed(object? sender, EventArgs? e) - { - try - { - await RefreshPeepImageAsync(); - } - catch - { - // ignored - } - } - - private readonly WriteableBitmap?[] _peepImageCache = new WriteableBitmap?[PeepImageSemaphoreMaxCount]; - - private async Task RefreshPeepImageAsync() - { - if (!await _peepImageSemaphore.WaitAsync(0)) - { - // 一秒内连续三次未能获取信号量,降低 FPS - if (++_peepImageSemaphoreFailCount < 3) - { - return; - } - - _peepImageSemaphoreFailCount = 0; - - if (_peepImageSemaphoreCurrentCount < PeepImageSemaphoreMaxCount) - { - _peepImageSemaphoreCurrentCount++; - _peepImageSemaphore.Release(); - _logger.Information("Screenshot Semaphore Full, increase semaphore count to {PeepImageSemaphoreCurrentCount}", _peepImageSemaphoreCurrentCount); - return; - } - - _logger.Warning("Screenshot Semaphore Full, Reduce Target FPS count to {PeepTargetFps}", --PeepTargetFps); - _ = Execute.OnUIThreadAsync(() => - { - Growl.Clear(); - Growl.Warning($"Screenshot taking too long, reduce Target FPS to {PeepTargetFps}"); - }); - return; - } - - try - { - var count = Interlocked.Increment(ref _peepImageCount); - var index = count % _peepImageCache.Length; - var frameData = await Instances.AsstProxy.AsstGetFreshImageBgrDataAsync(); - if (frameData is null || frameData.Length == 0) - { - _logger.Warning("Peep image data is null or empty."); - return; - } - - // 若不满足条件,提前释放 frameData 避免内存泄露 - if (!Peeping || count <= _peepImageNewestCount) - { - _logger.Debug("Peep image count {Count} is not the newest, skip updating image.", count); - ArrayPool.Shared.Return(frameData); - return; - } - - await Execute.OnUIThreadAsync(() => - { - _peepImageCache[index] = AsstProxy.WriteBgrToBitmap(frameData, _peepImageCache[index]); - }); - - PeepImage = _peepImageCache[index]; - ArrayPool.Shared.Return(frameData); - Interlocked.Exchange(ref _peepImageNewestCount, count); - - var now = DateTime.Now; - Interlocked.Increment(ref _frameCount); - var totalSeconds = (now - _lastFpsUpdateTime).TotalSeconds; - if (totalSeconds < 1) - { - return; - } - - var frameCount = Interlocked.Exchange(ref _frameCount, 0); - _lastFpsUpdateTime = now; - PeepScreenFpf = frameCount / totalSeconds; - _peepImageSemaphoreFailCount = 0; - } - finally - { - _peepImageSemaphore.Release(); - } - } - - private bool _isPeepTransitioning; - - public bool IsPeepTransitioning - { - get => _isPeepTransitioning; - set => SetAndNotify(ref _isPeepTransitioning, value); - } - - /// - /// 获取或停止获取实时截图,在抽卡时额外停止抽卡 - /// - /// Task - public async Task Peep() - { - if (IsPeepTransitioning) - { - return; - } - - IsPeepTransitioning = true; - - try - { - // 正在 Peep 时,点击按钮停止 Peep - if (Peeping) - { - Peeping = false; - _peepImageTimer.Stop(); - Array.Fill(_peepImageCache, null); - - // 由 Peep() 方法启动的 Peep 也需要停止,Block 不会自动停止 - if (IsGachaInProgress || IsPeepInProgress) - { - await Instances.TaskQueueViewModel.Stop(); - Instances.TaskQueueViewModel.SetStopped(); - } - - IsPeepInProgress = false; - return; - } - - // 点击按钮开始 Peep - Peeping = true; - - AchievementTrackerHelper.Instance.Unlock(AchievementIds.PeekScreen); - - // 如果没任务在运行,需要先连接,并标记是由 Peep() 方法启动的 Peep - if (Idle) - { - _runningState.SetIdle(false); - string errMsg = string.Empty; - bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); - if (!caught) - { - GachaInfo = errMsg; - _runningState.SetIdle(true); - return; - } - - IsPeepInProgress = true; - } - - PeepScreenFpf = 0; - _peepImageCount = 0; - _peepImageNewestCount = 0; - _peepImageTimer.Start(); - } - finally - { - IsPeepTransitioning = false; - } - } - - #endregion - - #region MiniGame - - public static ObservableCollection MiniGameTaskList { get; } = - [ - new() { Display = LocalizationHelper.GetString("MiniGame@RM-TR-1"), Value = "MiniGame@RM-TR-1@Begin" }, - new() { Display = LocalizationHelper.GetString("MiniGame@RM-1"), Value = "MiniGame@RM-1@Begin" }, - new() { Display = LocalizationHelper.GetString("MiniGameNameSsStore"), Value = "SS@Store@Begin" }, - new() { Display = LocalizationHelper.GetString("MiniGameNameGreenTicketStore"), Value = "GreenTicket@Store@Begin" }, - new() { Display = LocalizationHelper.GetString("MiniGameNameYellowTicketStore"), Value = "YellowTicket@Store@Begin" }, - new() { Display = LocalizationHelper.GetString("MiniGameNameRAStore"), Value = "RA@Store@Begin" }, - new() { Display = LocalizationHelper.GetString("MiniGame@AT@ConversationRoom"), Value = "MiniGame@AT@ConversationRoom" }, - new() { Display = LocalizationHelper.GetString("MiniGame@ALL@HoneyFruit"), Value = "MiniGame@ALL@GreenGrass@DuelChannel@Begin" }, - new() { Display = LocalizationHelper.GetString("MiniGame@ALL@GreenGrass"), Value = "MiniGame@ALL@GreenGrass@DuelChannel@Begin" }, - ]; - - private string _miniGameTaskName = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.MiniGameTaskName, "SS@Store@Begin"); - - public string MiniGameTaskName - { - get => _miniGameTaskName; - set - { - SetAndNotify(ref _miniGameTaskName, value); - ConfigurationHelper.SetGlobalValue(ConfigurationKeys.MiniGameTaskName, value); - MiniGameTip = GetMiniGameTip(value); - } - } - - private string? _miniGameTip; - - public string MiniGameTip - { - get - { - _miniGameTip ??= GetMiniGameTip(MiniGameTaskName); - return _miniGameTip; - } - set => SetAndNotify(ref _miniGameTip, value); - } - - private static string GetMiniGameTip(string name) - { - return name switch - { - "MiniGame@RM-TR-1@Begin" => LocalizationHelper.GetString("MiniGame@RM-TR-1Tip"), - "MiniGame@RM-1@Begin" => LocalizationHelper.GetString("MiniGame@RM-1Tip"), - "SS@Store@Begin" => LocalizationHelper.GetString("MiniGameNameSsStoreTip"), - "GreenTicket@Store@Begin" => LocalizationHelper.GetString("MiniGameNameGreenTicketStoreTip"), - "YellowTicket@Store@Begin" => LocalizationHelper.GetString("MiniGameNameYellowTicketStoreTip"), - "RA@Store@Begin" => LocalizationHelper.GetString("MiniGameNameRAStoreTip"), - "MiniGame@AT@ConversationRoom" => LocalizationHelper.GetString("MiniGame@AT@ConversationRoomTip"), - "MiniGame@ALL@GreenGrass@DuelChannel@Begin" => LocalizationHelper.GetString("MiniGame@ALL@GreenGrassTip"), - _ => string.Empty, - }; - } - - public void StartMiniGame() - { - _ = StartMiniGameAsync(); - } - - private async Task StartMiniGameAsync() - { - if (!Idle) - { - await Instances.TaskQueueViewModel.Stop(); - return; - } - - _runningState.SetIdle(false); - string errMsg = string.Empty; - bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); - if (!caught) - { - _runningState.SetIdle(true); - } - - if (_runningState.GetStopping()) - { - Instances.TaskQueueViewModel.SetStopped(); - return; - } - - caught = Instances.AsstProxy.AsstMiniGame(MiniGameTaskName); - if (!caught) - { - _runningState.SetIdle(true); - } - } - - #endregion + OperBoxInfo = $"{LocalizationHelper.GetString("IdentificationCompleted")}\n{LocalizationHelper.GetString("OperBoxRecognitionTip")}"; + OperBoxDataArray = ownOpers; + SaveOperBoxDetails(ownOpers); + _tempOperHaveSet = []; + return true; } + + /// + /// 开始识别干员 + /// UI 绑定的方法 + /// + /// Task + [UsedImplicitly] + public async Task StartOperBox() + { + OperBoxSelectedIndex = 1; + _tempOperHaveSet = []; + OperBoxHaveList = []; + OperBoxNotHaveList = []; + _runningState.SetIdle(false); + + string errMsg = string.Empty; + OperBoxInfo = LocalizationHelper.GetString("ConnectingToEmulator"); + bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + if (!caught) + { + OperBoxInfo = errMsg; + _runningState.SetIdle(true); + return; + } + + if (Instances.AsstProxy.AsstStartOperBox()) + { + OperBoxInfo = LocalizationHelper.GetString("Identifying"); + } + } + + // UI 绑定的方法 + [UsedImplicitly] + public void ExportOperBox() + { + if (OperBoxDataArray.Count == 0) + { + return; + } + + System.Windows.Forms.Clipboard.Clear(); + System.Windows.Forms.Clipboard.SetDataObject(JsonConvert.SerializeObject(OperBoxDataArray, Formatting.Indented)); + OperBoxInfo = LocalizationHelper.GetString("CopiedToClipboard"); + } + + #endregion OperBox + + #region Gacha + + private string _gachaInfo = LocalizationHelper.GetString("GachaInitTip"); + + public string GachaInfo + { + get => _gachaInfo; + set => SetAndNotify(ref _gachaInfo, value); + } + + // UI 绑定的方法 + public async Task GachaOnce() + { + await StartGacha(); + } + + // UI 绑定的方法 + public async Task GachaTenTimes() + { + await StartGacha(false); + } + + private bool _isGachaInProgress; + + public bool IsGachaInProgress + { + get => _isGachaInProgress; + set + { + if (!SetAndNotify(ref _isGachaInProgress, value)) + { + return; + } + + if (!value) + { + _gachaTimer.Stop(); + } + } + } + + public async Task StartGacha(bool once = true) + { + _runningState.SetIdle(false); + + string errMsg = string.Empty; + GachaInfo = LocalizationHelper.GetString("ConnectingToEmulator"); + bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg) && Instances.AsstProxy.AsstStartGacha(once)); + if (!caught) + { + GachaInfo = errMsg; + _runningState.SetIdle(true); + return; + } + + _gachaTimer.Interval = TimeSpan.FromSeconds(5); + _gachaTimer.Start(); + + RefreshGachaTip(null, null); + IsGachaInProgress = true; + _ = Peep(); + } + + private void RefreshGachaTip(object? sender, EventArgs? e) + { + var rd = new Random(); + GachaInfo = LocalizationHelper.GetString("GachaTip" + rd.Next(1, 18)); + } + + // DO NOT CHANGE + // 请勿更改 + // 請勿更改 + // このコードを変更しないでください + // 변경하지 마십시오 + private bool _gachaShowDisclaimer = true; // !Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.ShowDisclaimerNoMore, bool.FalseString)); + + public bool GachaShowDisclaimer + { + get => _gachaShowDisclaimer; + set + { + SetAndNotify(ref _gachaShowDisclaimer, value); + } + } + + private bool _gachaShowDisclaimerNoMore = Convert.ToBoolean(ConfigurationHelper.GetValue(ConfigurationKeys.GachaShowDisclaimerNoMore, bool.FalseString)); + + public bool GachaShowDisclaimerNoMore + { + get => _gachaShowDisclaimerNoMore; + set + { + SetAndNotify(ref _gachaShowDisclaimerNoMore, value); + ConfigurationHelper.SetValue(ConfigurationKeys.GachaShowDisclaimerNoMore, value.ToString()); + } + } + + // UI 绑定的方法 + [UsedImplicitly] + public void GachaAgreeDisclaimer() + { + var result = MessageBoxHelper.Show( + LocalizationHelper.GetString("GachaWarning"), + LocalizationHelper.GetString("Warning"), + MessageBoxButton.YesNo, + MessageBoxImage.Warning, + no: LocalizationHelper.GetString("Confirm"), + yes: LocalizationHelper.GetString("Cancel"), + iconBrushKey: "DangerBrush"); + if (result != MessageBoxResult.No) + { + return; + } + + AchievementTrackerHelper.Instance.Unlock(AchievementIds.RealGacha); + + GachaShowDisclaimer = false; + } + + #endregion Gacha + + #region Peep + + private bool _peeping; + + public bool Peeping + { + get => _peeping; + set + { + if (!SetAndNotify(ref _peeping, value)) + { + return; + } + + if (!value) + { + _peepImageTimer.Stop(); + } + } + } + + private bool _isPeepInProgress; + + /// + /// Gets or sets a value indicating whether由 Peep 方法启动的 Peep + /// + public bool IsPeepInProgress + { + get => _isPeepInProgress; + set + { + if (!SetAndNotify(ref _isPeepInProgress, value)) + { + return; + } + + if (!value) + { + _peepImageTimer.Stop(); + } + } + } + + private WriteableBitmap? _peepImage; + + public WriteableBitmap? PeepImage + { + get => _peepImage; + set => SetAndNotify(ref _peepImage, value); + } + + private double _peepScreenFpf; + + public double PeepScreenFpf + { + get => _peepScreenFpf; + set => SetAndNotify(ref _peepScreenFpf, value); + } + + private int _peepTargetFps = int.Parse(ConfigurationHelper.GetValue(ConfigurationKeys.PeepTargetFps, "20")); + + public int PeepTargetFps + { + get + { + return _peepTargetFps; + } + + set + { + value = value switch + { + > 600 => 600, + < 1 => 1, + _ => value, + }; + + SetAndNotify(ref _peepTargetFps, value); + _peepImageTimer.Interval = 1000d / _peepTargetFps; + ConfigurationHelper.SetValue(ConfigurationKeys.PeepTargetFps, value.ToString()); + } + } + + private DateTime _lastFpsUpdateTime = DateTime.MinValue; + private int _frameCount; + + private readonly Timer _peepImageTimer = new(); + private readonly DispatcherTimer _gachaTimer = new() { Interval = TimeSpan.FromSeconds(5) }; + + private int _peepImageCount; + private int _peepImageNewestCount; + + private static int _peepImageSemaphoreCurrentCount = 2; + private const int PeepImageSemaphoreMaxCount = 5; + private static int _peepImageSemaphoreFailCount = 0; + private static readonly SemaphoreSlim _peepImageSemaphore = new(_peepImageSemaphoreCurrentCount, PeepImageSemaphoreMaxCount); + + private async void PeepImageTimerElapsed(object? sender, EventArgs? e) + { + try + { + await RefreshPeepImageAsync(); + } + catch + { + // ignored + } + } + + private readonly WriteableBitmap?[] _peepImageCache = new WriteableBitmap?[PeepImageSemaphoreMaxCount]; + + private async Task RefreshPeepImageAsync() + { + if (!await _peepImageSemaphore.WaitAsync(0)) + { + // 一秒内连续三次未能获取信号量,降低 FPS + if (++_peepImageSemaphoreFailCount < 3) + { + return; + } + + _peepImageSemaphoreFailCount = 0; + + if (_peepImageSemaphoreCurrentCount < PeepImageSemaphoreMaxCount) + { + _peepImageSemaphoreCurrentCount++; + _peepImageSemaphore.Release(); + _logger.Information("Screenshot Semaphore Full, increase semaphore count to {PeepImageSemaphoreCurrentCount}", _peepImageSemaphoreCurrentCount); + return; + } + + _logger.Warning("Screenshot Semaphore Full, Reduce Target FPS count to {PeepTargetFps}", --PeepTargetFps); + _ = Execute.OnUIThreadAsync(() => + { + Growl.Clear(); + Growl.Warning($"Screenshot taking too long, reduce Target FPS to {PeepTargetFps}"); + }); + return; + } + + try + { + var count = Interlocked.Increment(ref _peepImageCount); + var index = count % _peepImageCache.Length; + var frameData = await Instances.AsstProxy.AsstGetFreshImageBgrDataAsync(); + if (frameData is null || frameData.Length == 0) + { + _logger.Warning("Peep image data is null or empty."); + return; + } + + // 若不满足条件,提前释放 frameData 避免内存泄露 + if (!Peeping || count <= _peepImageNewestCount) + { + _logger.Debug("Peep image count {Count} is not the newest, skip updating image.", count); + ArrayPool.Shared.Return(frameData); + return; + } + + await Execute.OnUIThreadAsync(() => + { + _peepImageCache[index] = AsstProxy.WriteBgrToBitmap(frameData, _peepImageCache[index]); + }); + + PeepImage = _peepImageCache[index]; + ArrayPool.Shared.Return(frameData); + Interlocked.Exchange(ref _peepImageNewestCount, count); + + var now = DateTime.Now; + Interlocked.Increment(ref _frameCount); + var totalSeconds = (now - _lastFpsUpdateTime).TotalSeconds; + if (totalSeconds < 1) + { + return; + } + + var frameCount = Interlocked.Exchange(ref _frameCount, 0); + _lastFpsUpdateTime = now; + PeepScreenFpf = frameCount / totalSeconds; + _peepImageSemaphoreFailCount = 0; + } + finally + { + _peepImageSemaphore.Release(); + } + } + + private bool _isPeepTransitioning; + + public bool IsPeepTransitioning + { + get => _isPeepTransitioning; + set => SetAndNotify(ref _isPeepTransitioning, value); + } + + /// + /// 获取或停止获取实时截图,在抽卡时额外停止抽卡 + /// + /// Task + public async Task Peep() + { + if (IsPeepTransitioning) + { + return; + } + + IsPeepTransitioning = true; + + try + { + // 正在 Peep 时,点击按钮停止 Peep + if (Peeping) + { + Peeping = false; + _peepImageTimer.Stop(); + Array.Fill(_peepImageCache, null); + + // 由 Peep() 方法启动的 Peep 也需要停止,Block 不会自动停止 + if (IsGachaInProgress || IsPeepInProgress) + { + await Instances.TaskQueueViewModel.Stop(); + Instances.TaskQueueViewModel.SetStopped(); + } + + IsPeepInProgress = false; + return; + } + + // 点击按钮开始 Peep + Peeping = true; + + AchievementTrackerHelper.Instance.Unlock(AchievementIds.PeekScreen); + + // 如果没任务在运行,需要先连接,并标记是由 Peep() 方法启动的 Peep + if (Idle) + { + _runningState.SetIdle(false); + string errMsg = string.Empty; + bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + if (!caught) + { + GachaInfo = errMsg; + _runningState.SetIdle(true); + return; + } + + IsPeepInProgress = true; + } + + PeepScreenFpf = 0; + _peepImageCount = 0; + _peepImageNewestCount = 0; + _peepImageTimer.Start(); + } + finally + { + IsPeepTransitioning = false; + } + } + + #endregion + + #region MiniGame + + public static ObservableCollection MiniGameTaskList { get; } = + [ + new() { Display = LocalizationHelper.GetString("MiniGame@RM-TR-1"), Value = "MiniGame@RM-TR-1@Begin" }, + new() { Display = LocalizationHelper.GetString("MiniGame@RM-1"), Value = "MiniGame@RM-1@Begin" }, + new() { Display = LocalizationHelper.GetString("MiniGameNameSsStore"), Value = "SS@Store@Begin" }, + new() { Display = LocalizationHelper.GetString("MiniGameNameGreenTicketStore"), Value = "GreenTicket@Store@Begin" }, + new() { Display = LocalizationHelper.GetString("MiniGameNameYellowTicketStore"), Value = "YellowTicket@Store@Begin" }, + new() { Display = LocalizationHelper.GetString("MiniGameNameRAStore"), Value = "RA@Store@Begin" }, + new() { Display = LocalizationHelper.GetString("MiniGame@AT@ConversationRoom"), Value = "MiniGame@AT@ConversationRoom" }, + new() { Display = LocalizationHelper.GetString("MiniGame@ALL@HoneyFruit"), Value = "MiniGame@ALL@GreenGrass@DuelChannel@Begin" }, + new() { Display = LocalizationHelper.GetString("MiniGame@ALL@GreenGrass"), Value = "MiniGame@ALL@GreenGrass@DuelChannel@Begin" }, + ]; + + private string _miniGameTaskName = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.MiniGameTaskName, "SS@Store@Begin"); + + public string MiniGameTaskName + { + get => _miniGameTaskName; + set + { + SetAndNotify(ref _miniGameTaskName, value); + ConfigurationHelper.SetGlobalValue(ConfigurationKeys.MiniGameTaskName, value); + MiniGameTip = GetMiniGameTip(value); + } + } + + private string? _miniGameTip; + + public string MiniGameTip + { + get + { + _miniGameTip ??= GetMiniGameTip(MiniGameTaskName); + return _miniGameTip; + } + set => SetAndNotify(ref _miniGameTip, value); + } + + private static string GetMiniGameTip(string name) + { + return name switch + { + "MiniGame@RM-TR-1@Begin" => LocalizationHelper.GetString("MiniGame@RM-TR-1Tip"), + "MiniGame@RM-1@Begin" => LocalizationHelper.GetString("MiniGame@RM-1Tip"), + "SS@Store@Begin" => LocalizationHelper.GetString("MiniGameNameSsStoreTip"), + "GreenTicket@Store@Begin" => LocalizationHelper.GetString("MiniGameNameGreenTicketStoreTip"), + "YellowTicket@Store@Begin" => LocalizationHelper.GetString("MiniGameNameYellowTicketStoreTip"), + "RA@Store@Begin" => LocalizationHelper.GetString("MiniGameNameRAStoreTip"), + "MiniGame@AT@ConversationRoom" => LocalizationHelper.GetString("MiniGame@AT@ConversationRoomTip"), + "MiniGame@ALL@GreenGrass@DuelChannel@Begin" => LocalizationHelper.GetString("MiniGame@ALL@GreenGrassTip"), + _ => string.Empty, + }; + } + + public void StartMiniGame() + { + _ = StartMiniGameAsync(); + } + + private async Task StartMiniGameAsync() + { + if (!Idle) + { + await Instances.TaskQueueViewModel.Stop(); + return; + } + + _runningState.SetIdle(false); + string errMsg = string.Empty; + bool caught = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + if (!caught) + { + _runningState.SetIdle(true); + } + + if (_runningState.GetStopping()) + { + Instances.TaskQueueViewModel.SetStopped(); + return; + } + + caught = Instances.AsstProxy.AsstMiniGame(MiniGameTaskName); + if (!caught) + { + _runningState.SetIdle(true); + } + } + + #endregion } diff --git a/src/MaaWpfGui/Views/UI/AchievementListWindow.xaml.cs b/src/MaaWpfGui/Views/UI/AchievementListWindow.xaml.cs index c6c4cbf17b..5d2ce8cf7d 100644 --- a/src/MaaWpfGui/Views/UI/AchievementListWindow.xaml.cs +++ b/src/MaaWpfGui/Views/UI/AchievementListWindow.xaml.cs @@ -14,24 +14,23 @@ using System.ComponentModel; using MaaWpfGui.Helper; -namespace MaaWpfGui.Views.UI -{ - /// - /// AchievementListWindow.xaml 的交互逻辑 - /// - public partial class AchievementListWindow - { - public AchievementListWindow() - { - InitializeComponent(); - DataContext = AchievementTrackerHelper.Instance; - Closing += OnWindowClosing; - } +namespace MaaWpfGui.Views.UI; - private static void OnWindowClosing(object sender, CancelEventArgs e) - { - // 关闭窗口时执行一次空搜索,重置可见性 - AchievementTrackerHelper.Instance.Search(); - } +/// +/// AchievementListWindow.xaml 的交互逻辑 +/// +public partial class AchievementListWindow +{ + public AchievementListWindow() + { + InitializeComponent(); + DataContext = AchievementTrackerHelper.Instance; + Closing += OnWindowClosing; + } + + private static void OnWindowClosing(object sender, CancelEventArgs e) + { + // 关闭窗口时执行一次空搜索,重置可见性 + AchievementTrackerHelper.Instance.Search(); } } diff --git a/src/MaaWpfGui/Views/UI/ErrorView.xaml.cs b/src/MaaWpfGui/Views/UI/ErrorView.xaml.cs index 53c48b8e2f..4168e7d131 100644 --- a/src/MaaWpfGui/Views/UI/ErrorView.xaml.cs +++ b/src/MaaWpfGui/Views/UI/ErrorView.xaml.cs @@ -23,182 +23,181 @@ using System.Windows.Documents; using MaaWpfGui.Constants; using MaaWpfGui.Helper; -namespace MaaWpfGui.Views.UI +namespace MaaWpfGui.Views.UI; + +/// +/// ErrorView.xaml 的交互逻辑 +/// +public partial class ErrorView : INotifyPropertyChanged { + protected bool ShouldExit { get; set; } = true; + + public string ExceptionMessage { get; set; } = string.Empty; + + public string PossibleSolution { get; set; } = string.Empty; + + public string ExceptionDetails { get; set; } = string.Empty; + + private bool _congratulationsOnError = true; + + public string ErrorString { get; set; } = LocalizationHelper.GetString("Error"); + + public string ErrorCongratulationsString { get; set; } = LocalizationHelper.GetString("ErrorCongratulations"); + /// - /// ErrorView.xaml 的交互逻辑 + /// Gets or sets a value indicating whether to enable congratulation mode for ErrorView. /// - public partial class ErrorView : INotifyPropertyChanged + public bool CongratulationsOnError { - protected bool ShouldExit { get; set; } = true; - - public string ExceptionMessage { get; set; } = string.Empty; - - public string PossibleSolution { get; set; } = string.Empty; - - public string ExceptionDetails { get; set; } = string.Empty; - - private bool _congratulationsOnError = true; - - public string ErrorString { get; set; } = LocalizationHelper.GetString("Error"); - - public string ErrorCongratulationsString { get; set; } = LocalizationHelper.GetString("ErrorCongratulations"); - - /// - /// Gets or sets a value indicating whether to enable congratulation mode for ErrorView. - /// - public bool CongratulationsOnError + get => _congratulationsOnError; + set { - get => _congratulationsOnError; - set + _congratulationsOnError = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CongratulationsOnError))); + } + } + + public ErrorView() + { + InitializeComponent(); + } + + public ErrorView(Exception exc, bool shouldExit) + { + InitializeComponent(); + var exc0 = exc; + var errorStr = new StringBuilder(); + while (true) + { + errorStr.Append(exc.Message); + if (exc.InnerException != null) { - _congratulationsOnError = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CongratulationsOnError))); + errorStr.AppendLine(); + exc = exc.InnerException; + } + else + { + break; } } - public ErrorView() + var error = errorStr.ToString(); + var details = exc0.ToString(); + ExceptionMessage = error; + ExceptionDetails = details; + PossibleSolution = GetSolution(error, details); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ExceptionMessage))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PossibleSolution))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ExceptionDetails))); + ShouldExit = shouldExit; + + var isZhCn = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Localization, LocalizationHelper.DefaultLanguage) == "zh-cn"; + ErrorQqGroupLink.Visibility = isZhCn ? Visibility.Visible : Visibility.Collapsed; + + try { - InitializeComponent(); + AchievementTrackerHelper.Instance.Unlock(AchievementIds.CongratulationError); + } + catch + { + // ignored + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + private static string GetSolution(string error, string details) + { + _ = error; // To avoid warning + if (details.Contains("MaaCore.dll not found!") || + details.Contains("resource folder not found!")) + { + return LocalizationHelper.GetString("ErrorSolutionMoveMaaExeOutOfFolder"); } - public ErrorView(Exception exc, bool shouldExit) + if (details.Contains("AsstGetVersion()") || + details.Contains("DllNotFoundException") || + details.Contains("lambda_method") || + details.Contains("HandyControl") || + (details.Contains("System.Net.Http") && details.Contains("Version"))) { - InitializeComponent(); - var exc0 = exc; - var errorStr = new StringBuilder(); - while (true) - { - errorStr.Append(exc.Message); - if (exc.InnerException != null) - { - errorStr.AppendLine(); - exc = exc.InnerException; - } - else - { - break; - } - } - - var error = errorStr.ToString(); - var details = exc0.ToString(); - ExceptionMessage = error; - ExceptionDetails = details; - PossibleSolution = GetSolution(error, details); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ExceptionMessage))); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PossibleSolution))); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ExceptionDetails))); - ShouldExit = shouldExit; - - var isZhCn = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.Localization, LocalizationHelper.DefaultLanguage) == "zh-cn"; - ErrorQqGroupLink.Visibility = isZhCn ? Visibility.Visible : Visibility.Collapsed; - - try - { - AchievementTrackerHelper.Instance.Unlock(AchievementIds.CongratulationError); - } - catch - { - // ignored - } + return LocalizationHelper.GetString("ErrorSolutionCrash"); } - public event PropertyChangedEventHandler? PropertyChanged; - - private static string GetSolution(string error, string details) + if (details.Contains("CheckAndUpdateNow()") && (details.Contains("MoveFile") || details.Contains("DeleteFile"))) { - _ = error; // To avoid warning - if (details.Contains("MaaCore.dll not found!") || - details.Contains("resource folder not found!")) - { - return LocalizationHelper.GetString("ErrorSolutionMoveMaaExeOutOfFolder"); - } - - if (details.Contains("AsstGetVersion()") || - details.Contains("DllNotFoundException") || - details.Contains("lambda_method") || - details.Contains("HandyControl") || - (details.Contains("System.Net.Http") && details.Contains("Version"))) - { - return LocalizationHelper.GetString("ErrorSolutionCrash"); - } - - if (details.Contains("CheckAndUpdateNow()") && (details.Contains("MoveFile") || details.Contains("DeleteFile"))) - { - return LocalizationHelper.GetString("ErrorSolutionUpdatePackageExtractionFailed"); - } - - if (details.Contains("Hyperlink_Click") && details.Contains("StartWithShellExecuteEx")) - { - return LocalizationHelper.GetString("ErrorSolutionSelectDefaultBrowser"); - } - - // ReSharper disable once ConvertIfStatementToReturnStatement - if (details.Contains("System.IO.File.InternalMove")) - { - return LocalizationHelper.GetString("ErrorSolutionFailedToMove"); - } - - AchievementTrackerHelper.Instance.Unlock(AchievementIds.UnexpectedCrash); - - return $"{LocalizationHelper.GetString("UnknownErrorOccurs")}\n" + - $"{LocalizationHelper.GetString("ErrorCrashMessageOpenLog")}\n" + - $"{LocalizationHelper.GetString("ErrorCrashMessageGenerateReport")}\n" + - $"{LocalizationHelper.GetString("ErrorCrashMessageHelpTip")}"; + return LocalizationHelper.GetString("ErrorSolutionUpdatePackageExtractionFailed"); } - protected override void OnClosed(EventArgs e) + if (details.Contains("Hyperlink_Click") && details.Contains("StartWithShellExecuteEx")) { - if (ShouldExit) - { - Environment.Exit(0); - } - - base.OnClosed(e); + return LocalizationHelper.GetString("ErrorSolutionSelectDefaultBrowser"); } - private void Hyperlink_OnClick(object sender, RoutedEventArgs e) + // ReSharper disable once ConvertIfStatementToReturnStatement + if (details.Contains("System.IO.File.InternalMove")) { - Process.Start(new ProcessStartInfo(((Hyperlink)sender).NavigateUri.AbsoluteUri) { UseShellExecute = true }); + return LocalizationHelper.GetString("ErrorSolutionFailedToMove"); } - private void CopyToClipboard() - { - var data = new DataObject(); - var textToCopy = - $"{LocalizationHelper.GetString("ErrorProlog")}\n" + - $"\t{ExceptionMessage}\n" + - $"{LocalizationHelper.GetString("ErrorSolution")}\n" + - $"\t{PossibleSolution}\n" + - $"{LocalizationHelper.GetString("ErrorDetails")}\n" + - $"{ExceptionDetails}"; - data.SetText(textToCopy); + AchievementTrackerHelper.Instance.Unlock(AchievementIds.UnexpectedCrash); - try - { - System.Windows.Forms.Clipboard.Clear(); - System.Windows.Forms.Clipboard.SetDataObject(data, true); - } - catch - { - // 有时候报错了也能复制上去,这个时候复制不了也没办法了 - } + return $"{LocalizationHelper.GetString("UnknownErrorOccurs")}\n" + + $"{LocalizationHelper.GetString("ErrorCrashMessageOpenLog")}\n" + + $"{LocalizationHelper.GetString("ErrorCrashMessageGenerateReport")}\n" + + $"{LocalizationHelper.GetString("ErrorCrashMessageHelpTip")}"; + } + + protected override void OnClosed(EventArgs e) + { + if (ShouldExit) + { + Environment.Exit(0); } - private async void CopyErrorMessage_Click(object sender, RoutedEventArgs e) + base.OnClosed(e); + } + + private void Hyperlink_OnClick(object sender, RoutedEventArgs e) + { + Process.Start(new ProcessStartInfo(((Hyperlink)sender).NavigateUri.AbsoluteUri) { UseShellExecute = true }); + } + + private void CopyToClipboard() + { + var data = new DataObject(); + var textToCopy = + $"{LocalizationHelper.GetString("ErrorProlog")}\n" + + $"\t{ExceptionMessage}\n" + + $"{LocalizationHelper.GetString("ErrorSolution")}\n" + + $"\t{PossibleSolution}\n" + + $"{LocalizationHelper.GetString("ErrorDetails")}\n" + + $"{ExceptionDetails}"; + data.SetText(textToCopy); + + try { - try - { - CopyToClipboard(); - CopiedTip.IsOpen = true; - await Task.Delay(3000); - CopiedTip.IsOpen = false; - } - catch - { - // ignored - } + System.Windows.Forms.Clipboard.Clear(); + System.Windows.Forms.Clipboard.SetDataObject(data, true); + } + catch + { + // 有时候报错了也能复制上去,这个时候复制不了也没办法了 + } + } + + private async void CopyErrorMessage_Click(object sender, RoutedEventArgs e) + { + try + { + CopyToClipboard(); + CopiedTip.IsOpen = true; + await Task.Delay(3000); + CopiedTip.IsOpen = false; + } + catch + { + // ignored } } } diff --git a/src/MaaWpfGui/Views/UI/NotifyIcon.xaml.cs b/src/MaaWpfGui/Views/UI/NotifyIcon.xaml.cs index 24d3b4d778..e47752caec 100644 --- a/src/MaaWpfGui/Views/UI/NotifyIcon.xaml.cs +++ b/src/MaaWpfGui/Views/UI/NotifyIcon.xaml.cs @@ -22,152 +22,151 @@ using MaaWpfGui.Main; using MaaWpfGui.ViewModels.UI; using Serilog; -namespace MaaWpfGui.Views.UI +namespace MaaWpfGui.Views.UI; + +/// +/// 托盘图标。 +/// +public partial class NotifyIcon { - /// - /// 托盘图标。 - /// - public partial class NotifyIcon + private static readonly ILogger _logger = Log.ForContext(); + private readonly int _menuItemNum; + private static Timer _clickTimer; + private static bool _canClick = true; + + [DllImport("user32.dll")] + private static extern uint GetDoubleClickTime(); + + public NotifyIcon() { - private static readonly ILogger _logger = Log.ForContext(); - private readonly int _menuItemNum; - private static Timer _clickTimer; - private static bool _canClick = true; + InitializeComponent(); - [DllImport("user32.dll")] - private static extern uint GetDoubleClickTime(); - - public NotifyIcon() + uint doubleClickTime = GetDoubleClickTime(); + _clickTimer = new(doubleClickTime); + _clickTimer.AutoReset = false; + _clickTimer.Elapsed += (s, e) => { - InitializeComponent(); + _canClick = true; + }; - uint doubleClickTime = GetDoubleClickTime(); - _clickTimer = new(doubleClickTime); - _clickTimer.AutoReset = false; - _clickTimer.Elapsed += (s, e) => - { - _canClick = true; - }; - - InitIcon(); - if (notifyIcon.ContextMenu is not null) - { - _menuItemNum = notifyIcon.ContextMenu.Items.Count; - } - } - - private void InitIcon() + InitIcon(); + if (notifyIcon.ContextMenu is not null) { - notifyIcon.Icon = AppIcon.GetIcon(); - notifyIcon.Visibility = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.UseTray, bool.TrueString)) ? Visibility.Visible : Visibility.Collapsed; - - notifyIcon.Click += NotifyIcon_MouseClick; - notifyIcon.MouseDoubleClick += NotifyIcon_MouseClick; - - startMenu.Click += StartTask; - stopMenu.Click += StopTask; - forceShowMenu.Click += ForceShow; - hideTrayMenu.Click += HideTray; - restartMenu.Click += App_restart; - exitMenu.Click += App_exit; - - foreach (var lang in LocalizationHelper.SupportedLanguages) - { - if (lang.Key == SettingsViewModel.PallasLangKey) - { - continue; - } - - var langMenu = new MenuItem() { Header = lang.Value }; - langMenu.Click += (_, _) => - { - SettingsViewModel.GuiSettings.Language = lang.Key; - }; - - switchLangMenu.Items.Add(langMenu); - } - } - - // 不知道是干嘛的,先留着 - // ReSharper disable once UnusedMember.Local - private void AddMenuItemOnFirst(string text, Action action) - { - var menuItem = new MenuItem { Header = text }; - menuItem.Click += (_, _) => { action?.Invoke(); }; - if (notifyIcon.ContextMenu is null) - { - return; - } - - if (notifyIcon.ContextMenu.Items.Count == _menuItemNum) - { - notifyIcon.ContextMenu.Items.Insert(0, menuItem); - } - else - { - notifyIcon.ContextMenu.Items[0] = menuItem; - } - } - - private static void NotifyIcon_MouseClick(object sender, RoutedEventArgs e) - { - if (!_canClick) - { - return; - } - - _clickTimer.Start(); - _canClick = false; - Instances.MainWindowManager?.Show(); - } - - private static void StartTask(object sender, RoutedEventArgs e) - { - // taskQueueViewModel意外为null了是不是也可以考虑Log一下 - // 先放个log点方便跟踪 - Instances.TaskQueueViewModel?.LinkStart(); - _logger.Information("Tray service task started."); - } - - private static void StopTask(object sender, RoutedEventArgs e) - { - Instances.TaskQueueViewModel?.ManualStop(); - _logger.Information("Tray service task stop."); - } - - private static void ForceShow(object sender, RoutedEventArgs e) - { - Instances.MainWindowManager?.ForceShow(); - _logger.Information("WindowManager force show."); - } - - private static void HideTray(object sender, RoutedEventArgs e) - { - Instances.MainWindowManager?.Show(); - - SettingsViewModel.GuiSettings.UseTray = !SettingsViewModel.GuiSettings.UseTray; - _logger.Information("Use tray icon: {0}", SettingsViewModel.GuiSettings.UseTray); - } - - private static void App_restart(object sender, RoutedEventArgs e) - { - if (Instances.TaskQueueViewModel.ConfirmExit()) - { - Bootstrapper.ShutdownAndRestartWithoutArgs(); - } - } - - private static void App_exit(object sender, RoutedEventArgs e) - { - if (Instances.TaskQueueViewModel.ConfirmExit()) - { - Bootstrapper.Shutdown(); - } - } - - private static void App_show(object sender, RoutedEventArgs e) - { - Instances.MainWindowManager?.Show(); + _menuItemNum = notifyIcon.ContextMenu.Items.Count; } } + + private void InitIcon() + { + notifyIcon.Icon = AppIcon.GetIcon(); + notifyIcon.Visibility = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.UseTray, bool.TrueString)) ? Visibility.Visible : Visibility.Collapsed; + + notifyIcon.Click += NotifyIcon_MouseClick; + notifyIcon.MouseDoubleClick += NotifyIcon_MouseClick; + + startMenu.Click += StartTask; + stopMenu.Click += StopTask; + forceShowMenu.Click += ForceShow; + hideTrayMenu.Click += HideTray; + restartMenu.Click += App_restart; + exitMenu.Click += App_exit; + + foreach (var lang in LocalizationHelper.SupportedLanguages) + { + if (lang.Key == SettingsViewModel.PallasLangKey) + { + continue; + } + + var langMenu = new MenuItem() { Header = lang.Value }; + langMenu.Click += (_, _) => + { + SettingsViewModel.GuiSettings.Language = lang.Key; + }; + + switchLangMenu.Items.Add(langMenu); + } + } + + // 不知道是干嘛的,先留着 + // ReSharper disable once UnusedMember.Local + private void AddMenuItemOnFirst(string text, Action action) + { + var menuItem = new MenuItem { Header = text }; + menuItem.Click += (_, _) => { action?.Invoke(); }; + if (notifyIcon.ContextMenu is null) + { + return; + } + + if (notifyIcon.ContextMenu.Items.Count == _menuItemNum) + { + notifyIcon.ContextMenu.Items.Insert(0, menuItem); + } + else + { + notifyIcon.ContextMenu.Items[0] = menuItem; + } + } + + private static void NotifyIcon_MouseClick(object sender, RoutedEventArgs e) + { + if (!_canClick) + { + return; + } + + _clickTimer.Start(); + _canClick = false; + Instances.MainWindowManager?.Show(); + } + + private static void StartTask(object sender, RoutedEventArgs e) + { + // taskQueueViewModel意外为null了是不是也可以考虑Log一下 + // 先放个log点方便跟踪 + Instances.TaskQueueViewModel?.LinkStart(); + _logger.Information("Tray service task started."); + } + + private static void StopTask(object sender, RoutedEventArgs e) + { + Instances.TaskQueueViewModel?.ManualStop(); + _logger.Information("Tray service task stop."); + } + + private static void ForceShow(object sender, RoutedEventArgs e) + { + Instances.MainWindowManager?.ForceShow(); + _logger.Information("WindowManager force show."); + } + + private static void HideTray(object sender, RoutedEventArgs e) + { + Instances.MainWindowManager?.Show(); + + SettingsViewModel.GuiSettings.UseTray = !SettingsViewModel.GuiSettings.UseTray; + _logger.Information("Use tray icon: {0}", SettingsViewModel.GuiSettings.UseTray); + } + + private static void App_restart(object sender, RoutedEventArgs e) + { + if (Instances.TaskQueueViewModel.ConfirmExit()) + { + Bootstrapper.ShutdownAndRestartWithoutArgs(); + } + } + + private static void App_exit(object sender, RoutedEventArgs e) + { + if (Instances.TaskQueueViewModel.ConfirmExit()) + { + Bootstrapper.Shutdown(); + } + } + + private static void App_show(object sender, RoutedEventArgs e) + { + Instances.MainWindowManager?.Show(); + } } diff --git a/src/MaaWpfGui/Views/UI/SettingsView.xaml.cs b/src/MaaWpfGui/Views/UI/SettingsView.xaml.cs index e20c0f8724..07e3ccb7fe 100644 --- a/src/MaaWpfGui/Views/UI/SettingsView.xaml.cs +++ b/src/MaaWpfGui/Views/UI/SettingsView.xaml.cs @@ -14,21 +14,20 @@ using MaaWpfGui.Helper; using MaaWpfGui.Styles.Properties; -namespace MaaWpfGui.Views.UI -{ - /// - /// 用于接收布局变化的广播事件 - /// - public partial class SettingsView - { - public SettingsView() - { - InitializeComponent(); +namespace MaaWpfGui.Views.UI; - Instances.SettingsViewModel.RefreshDividerOffsetsRequested += (_, _) => - { - ScrollViewerBinding.RefreshDividerOffsets(SettingsScrollViewer); - }; - } +/// +/// 用于接收布局变化的广播事件 +/// +public partial class SettingsView +{ + public SettingsView() + { + InitializeComponent(); + + Instances.SettingsViewModel.RefreshDividerOffsetsRequested += (_, _) => + { + ScrollViewerBinding.RefreshDividerOffsets(SettingsScrollViewer); + }; } } diff --git a/src/MaaWpfGui/Views/UserControl/ConfigurationMgrUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/ConfigurationMgrUserControl.xaml.cs index 9162c17aef..082427a406 100644 --- a/src/MaaWpfGui/Views/UserControl/ConfigurationMgrUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/ConfigurationMgrUserControl.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl +namespace MaaWpfGui.Views.UserControl; + +/// +/// ConfigurationMgrUserControl.xaml 的交互逻辑 +/// +public partial class ConfigurationMgrUserControl : System.Windows.Controls.UserControl { /// - /// ConfigurationMgrUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class ConfigurationMgrUserControl : System.Windows.Controls.UserControl + public ConfigurationMgrUserControl() { - /// - /// Initializes a new instance of the class. - /// - public ConfigurationMgrUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/GuideUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/GuideUserControl.xaml.cs index 8a77e20e0c..7e2cef1f42 100644 --- a/src/MaaWpfGui/Views/UserControl/GuideUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/GuideUserControl.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl +namespace MaaWpfGui.Views.UserControl; + +/// +/// GuideUserControl.xaml 的交互逻辑 +/// +public partial class GuideUserControl : System.Windows.Controls.UserControl { /// - /// GuideUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class GuideUserControl : System.Windows.Controls.UserControl + public GuideUserControl() { - /// - /// Initializes a new instance of the class. - /// - public GuideUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/HotKeyEditorUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/HotKeyEditorUserControl.xaml.cs index 48312407da..e15bf73848 100644 --- a/src/MaaWpfGui/Views/UserControl/HotKeyEditorUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/HotKeyEditorUserControl.xaml.cs @@ -17,75 +17,74 @@ using System.Windows; using System.Windows.Input; using MaaWpfGui.Services.HotKeys; -namespace MaaWpfGui.Views.UserControl +namespace MaaWpfGui.Views.UserControl; + +/// +/// Represents a user control for editing and configuring hotkey settings. +/// Provides a UI interface for users to view and modify keyboard shortcut combinations. +/// +public partial class HotKeyEditorUserControl : System.Windows.Controls.UserControl { - /// - /// Represents a user control for editing and configuring hotkey settings. - /// Provides a UI interface for users to view and modify keyboard shortcut combinations. - /// - public partial class HotKeyEditorUserControl : System.Windows.Controls.UserControl + public static readonly DependencyProperty HotKeyProperty = + DependencyProperty.Register(nameof(HotKey), typeof(MaaHotKey), + typeof(HotKeyEditorUserControl), + new FrameworkPropertyMetadata(null, + FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + + public MaaHotKey? HotKey { - public static readonly DependencyProperty HotKeyProperty = - DependencyProperty.Register(nameof(HotKey), typeof(MaaHotKey), - typeof(HotKeyEditorUserControl), - new FrameworkPropertyMetadata(null, - FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + get => (MaaHotKey)GetValue(HotKeyProperty); + set => SetValue(HotKeyProperty, value); + } - public MaaHotKey? HotKey + public HotKeyEditorUserControl() + { + InitializeComponent(); + } + + private void HotKeyTextBox_PreviewKeyDown(object sender, KeyEventArgs e) + { + // Don't let the event pass further + // because we don't want standard textBox shortcuts working + e.Handled = true; + + // Get modifiers and key data + var modifiers = Keyboard.Modifiers; + bool isWindowsKeyPressed = Keyboard.IsKeyDown(Key.LWin) || Keyboard.IsKeyDown(Key.RWin); + modifiers |= isWindowsKeyPressed ? ModifierKeys.Windows : ModifierKeys.None; + var key = e.Key; + + // When Alt is pressed, SystemKey is used instead + if (key == Key.System) { - get => (MaaHotKey)GetValue(HotKeyProperty); - set => SetValue(HotKeyProperty, value); + key = e.SystemKey; } - public HotKeyEditorUserControl() + // Remove hotKey if no modifiers + if (modifiers == ModifierKeys.None) { - InitializeComponent(); + HotKey = null; + return; } - private void HotKeyTextBox_PreviewKeyDown(object sender, KeyEventArgs e) + // If no actual key was pressed - return + if (key is + Key.LeftCtrl or + Key.RightCtrl or + Key.LeftAlt or + Key.RightAlt or + Key.LeftShift or + Key.RightShift or + Key.LWin or + Key.RWin or + Key.Clear or + Key.OemClear or + Key.Apps) { - // Don't let the event pass further - // because we don't want standard textBox shortcuts working - e.Handled = true; - - // Get modifiers and key data - var modifiers = Keyboard.Modifiers; - bool isWindowsKeyPressed = Keyboard.IsKeyDown(Key.LWin) || Keyboard.IsKeyDown(Key.RWin); - modifiers |= isWindowsKeyPressed ? ModifierKeys.Windows : ModifierKeys.None; - var key = e.Key; - - // When Alt is pressed, SystemKey is used instead - if (key == Key.System) - { - key = e.SystemKey; - } - - // Remove hotKey if no modifiers - if (modifiers == ModifierKeys.None) - { - HotKey = null; - return; - } - - // If no actual key was pressed - return - if (key is - Key.LeftCtrl or - Key.RightCtrl or - Key.LeftAlt or - Key.RightAlt or - Key.LeftShift or - Key.RightShift or - Key.LWin or - Key.RWin or - Key.Clear or - Key.OemClear or - Key.Apps) - { - return; - } - - // Update the value - HotKey = new MaaHotKey(key, modifiers); + return; } + + // Update the value + HotKey = new MaaHotKey(key, modifiers); } } diff --git a/src/MaaWpfGui/Views/UserControl/HotKeySettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/HotKeySettingsUserControl.xaml.cs index f295405f69..1aeca90cc5 100644 --- a/src/MaaWpfGui/Views/UserControl/HotKeySettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/HotKeySettingsUserControl.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl +namespace MaaWpfGui.Views.UserControl; + +/// +/// HotKeySettingsUserControl.xaml 的交互逻辑 +/// +public partial class HotKeySettingsUserControl : System.Windows.Controls.UserControl { /// - /// HotKeySettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class HotKeySettingsUserControl : System.Windows.Controls.UserControl + public HotKeySettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public HotKeySettingsUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/Settings/AboutUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/Settings/AboutUserControl.xaml.cs index ad68acf3aa..bbd86cb364 100644 --- a/src/MaaWpfGui/Views/UserControl/Settings/AboutUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/Settings/AboutUserControl.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl.Settings +namespace MaaWpfGui.Views.UserControl.Settings; + +/// +/// AboutUserControl.xaml 的交互逻辑 +/// +public partial class AboutUserControl : System.Windows.Controls.UserControl { /// - /// AboutUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class AboutUserControl : System.Windows.Controls.UserControl + public AboutUserControl() { - /// - /// Initializes a new instance of the class. - /// - public AboutUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/Settings/AchievementSettingsUserControl.cs b/src/MaaWpfGui/Views/UserControl/Settings/AchievementSettingsUserControl.cs index 9480936b6b..ce37751ce6 100644 --- a/src/MaaWpfGui/Views/UserControl/Settings/AchievementSettingsUserControl.cs +++ b/src/MaaWpfGui/Views/UserControl/Settings/AchievementSettingsUserControl.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl.Settings +namespace MaaWpfGui.Views.UserControl.Settings; + +/// +/// AchievementSettings.xaml 的交互逻辑 +/// +public partial class AchievementSettingsUserControl : System.Windows.Controls.UserControl { /// - /// AchievementSettings.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class AchievementSettingsUserControl : System.Windows.Controls.UserControl + public AchievementSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public AchievementSettingsUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/Settings/BackgroundSettings.xaml.cs b/src/MaaWpfGui/Views/UserControl/Settings/BackgroundSettings.xaml.cs index e504abe89d..242707b238 100644 --- a/src/MaaWpfGui/Views/UserControl/Settings/BackgroundSettings.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/Settings/BackgroundSettings.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl.Settings +namespace MaaWpfGui.Views.UserControl.Settings; + +/// +/// BackgroundSettingsUserControl.xaml 的交互逻辑 +/// +public partial class BackgroundSettingsUserControl : System.Windows.Controls.UserControl { /// - /// BackgroundSettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class BackgroundSettingsUserControl : System.Windows.Controls.UserControl + public BackgroundSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public BackgroundSettingsUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/Settings/ConnectSettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/Settings/ConnectSettingsUserControl.xaml.cs index b3eec77c45..ae38c1230e 100644 --- a/src/MaaWpfGui/Views/UserControl/Settings/ConnectSettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/Settings/ConnectSettingsUserControl.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl.Settings +namespace MaaWpfGui.Views.UserControl.Settings; + +/// +/// ConnectSettingsUserControl.xaml 的交互逻辑 +/// +public partial class ConnectSettingsUserControl : System.Windows.Controls.UserControl { /// - /// ConnectSettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class ConnectSettingsUserControl : System.Windows.Controls.UserControl + public ConnectSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public ConnectSettingsUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/Settings/ExternalNotificationSettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/Settings/ExternalNotificationSettingsUserControl.xaml.cs index fc859c0db3..c01f15d2f4 100644 --- a/src/MaaWpfGui/Views/UserControl/Settings/ExternalNotificationSettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/Settings/ExternalNotificationSettingsUserControl.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl.Settings +namespace MaaWpfGui.Views.UserControl.Settings; + +/// +/// ExternalNotificationSettingsUserControl.xaml 的交互逻辑 +/// +public partial class ExternalNotificationSettingsUserControl : System.Windows.Controls.UserControl { /// - /// ExternalNotificationSettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class ExternalNotificationSettingsUserControl : System.Windows.Controls.UserControl + public ExternalNotificationSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public ExternalNotificationSettingsUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/Settings/GameSettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/Settings/GameSettingsUserControl.xaml.cs index ea7db7374f..36c4b7a300 100644 --- a/src/MaaWpfGui/Views/UserControl/Settings/GameSettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/Settings/GameSettingsUserControl.xaml.cs @@ -14,48 +14,47 @@ using System.Windows; using MaaWpfGui.ViewModels.UI; -namespace MaaWpfGui.Views.UserControl.Settings +namespace MaaWpfGui.Views.UserControl.Settings; + +/// +/// GameSettingsUserControl.xaml 的交互逻辑 +/// +public partial class GameSettingsUserControl : System.Windows.Controls.UserControl { /// - /// GameSettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class GameSettingsUserControl : System.Windows.Controls.UserControl + public GameSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public GameSettingsUserControl() + InitializeComponent(); + } + + private void StartsWithScript_Drop(object sender, DragEventArgs e) + { + if (!e.Data.GetDataPresent(DataFormats.FileDrop)) { - InitializeComponent(); + return; } - private void StartsWithScript_Drop(object sender, DragEventArgs e) - { - if (!e.Data.GetDataPresent(DataFormats.FileDrop)) - { - return; - } + // Note that you can have more than one file. + var files = (string[])e.Data.GetData(DataFormats.FileDrop); + SettingsViewModel.GameSettings.StartsWithScript = files?[0] ?? string.Empty; + } - // Note that you can have more than one file. - var files = (string[])e.Data.GetData(DataFormats.FileDrop); - SettingsViewModel.GameSettings.StartsWithScript = files?[0] ?? string.Empty; + private void EndsWithScript_Drop(object sender, DragEventArgs e) + { + if (!e.Data.GetDataPresent(DataFormats.FileDrop)) + { + return; } - private void EndsWithScript_Drop(object sender, DragEventArgs e) - { - if (!e.Data.GetDataPresent(DataFormats.FileDrop)) - { - return; - } + // Note that you can have more than one file. + var files = (string[])e.Data.GetData(DataFormats.FileDrop); + SettingsViewModel.GameSettings.EndsWithScript = files?[0] ?? string.Empty; + } - // Note that you can have more than one file. - var files = (string[])e.Data.GetData(DataFormats.FileDrop); - SettingsViewModel.GameSettings.EndsWithScript = files?[0] ?? string.Empty; - } - - private void TextBox_PreviewDragOver(object sender, DragEventArgs e) - { - e.Handled = true; - } + private void TextBox_PreviewDragOver(object sender, DragEventArgs e) + { + e.Handled = true; } } diff --git a/src/MaaWpfGui/Views/UserControl/Settings/GuiSettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/Settings/GuiSettingsUserControl.xaml.cs index eb58086754..a9aaa65dfa 100644 --- a/src/MaaWpfGui/Views/UserControl/Settings/GuiSettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/Settings/GuiSettingsUserControl.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl.Settings +namespace MaaWpfGui.Views.UserControl.Settings; + +/// +/// GUISettingsUserControl.xaml 的交互逻辑 +/// +public partial class GuiSettingsUserControl : System.Windows.Controls.UserControl { /// - /// GUISettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class GuiSettingsUserControl : System.Windows.Controls.UserControl + public GuiSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public GuiSettingsUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/Settings/IssueReportUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/Settings/IssueReportUserControl.xaml.cs index 7ba39b4bca..16092f9f57 100644 --- a/src/MaaWpfGui/Views/UserControl/Settings/IssueReportUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/Settings/IssueReportUserControl.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl.Settings +namespace MaaWpfGui.Views.UserControl.Settings; + +/// +/// Interaction logic for IssueReportUserControl.xaml +/// +public partial class IssueReportUserControl : System.Windows.Controls.UserControl { /// - /// Interaction logic for IssueReportUserControl.xaml + /// Initializes a new instance of the class. /// - public partial class IssueReportUserControl : System.Windows.Controls.UserControl + public IssueReportUserControl() { - /// - /// Initializes a new instance of the class. - /// - public IssueReportUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/Settings/PerformanceUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/Settings/PerformanceUserControl.xaml.cs index cefaee86a5..b86d978c77 100644 --- a/src/MaaWpfGui/Views/UserControl/Settings/PerformanceUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/Settings/PerformanceUserControl.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl.Settings +namespace MaaWpfGui.Views.UserControl.Settings; + +/// +/// GameSettingsUserControl.xaml 的交互逻辑 +/// +public partial class PerformanceUserControl : System.Windows.Controls.UserControl { /// - /// GameSettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class PerformanceUserControl : System.Windows.Controls.UserControl + public PerformanceUserControl() { - /// - /// Initializes a new instance of the class. - /// - public PerformanceUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/Settings/RemoteControlUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/Settings/RemoteControlUserControl.xaml.cs index 122d3b4745..e9010da046 100644 --- a/src/MaaWpfGui/Views/UserControl/Settings/RemoteControlUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/Settings/RemoteControlUserControl.xaml.cs @@ -11,31 +11,15 @@ // but WITHOUT ANY WARRANTY // -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; +namespace MaaWpfGui.Views.UserControl.Settings; -namespace MaaWpfGui.Views.UserControl.Settings +/// +/// RemoteControlUserControl.xaml 的交互逻辑 +/// +public partial class RemoteControlUserControl { - /// - /// RemoteControlUserControl.xaml 的交互逻辑 - /// - public partial class RemoteControlUserControl + public RemoteControlUserControl() { - public RemoteControlUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/Settings/StartSettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/Settings/StartSettingsUserControl.xaml.cs index 7d7c6df185..172ba1e878 100644 --- a/src/MaaWpfGui/Views/UserControl/Settings/StartSettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/Settings/StartSettingsUserControl.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl.Settings +namespace MaaWpfGui.Views.UserControl.Settings; + +/// +/// StartSettingsUserControl.xaml 的交互逻辑 +/// +public partial class StartSettingsUserControl : System.Windows.Controls.UserControl { /// - /// StartSettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class StartSettingsUserControl : System.Windows.Controls.UserControl + public StartSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public StartSettingsUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/Settings/TimerSettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/Settings/TimerSettingsUserControl.xaml.cs index 9f7d23578c..119b1f9e5b 100644 --- a/src/MaaWpfGui/Views/UserControl/Settings/TimerSettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/Settings/TimerSettingsUserControl.xaml.cs @@ -11,22 +11,18 @@ // but WITHOUT ANY WARRANTY // -using System.Text.RegularExpressions; -using System.Windows.Input; +namespace MaaWpfGui.Views.UserControl.Settings; -namespace MaaWpfGui.Views.UserControl.Settings +/// +/// TimerSettingsUserControl.xaml 的交互逻辑 +/// +public partial class TimerSettingsUserControl : System.Windows.Controls.UserControl { /// - /// TimerSettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class TimerSettingsUserControl : System.Windows.Controls.UserControl + public TimerSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public TimerSettingsUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/Settings/VersionUpdateSettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/Settings/VersionUpdateSettingsUserControl.xaml.cs index 95961ac736..a901c7d51a 100644 --- a/src/MaaWpfGui/Views/UserControl/Settings/VersionUpdateSettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/Settings/VersionUpdateSettingsUserControl.xaml.cs @@ -26,126 +26,125 @@ using MaaWpfGui.ViewModels.UI; using MaaWpfGui.ViewModels.UserControl.Settings; using Serilog; -namespace MaaWpfGui.Views.UserControl.Settings +namespace MaaWpfGui.Views.UserControl.Settings; + +/// +/// VersionUpdateSettingsUserControl.xaml 的交互逻辑 +/// +public partial class VersionUpdateSettingsUserControl : System.Windows.Controls.UserControl { + private static readonly ILogger _logger = Log.ForContext(); + /// - /// VersionUpdateSettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class VersionUpdateSettingsUserControl : System.Windows.Controls.UserControl + public VersionUpdateSettingsUserControl() { - private static readonly ILogger _logger = Log.ForContext(); - - /// - /// Initializes a new instance of the class. - /// - public VersionUpdateSettingsUserControl() + InitializeComponent(); + _timer.Tick += (s, e1) => { - InitializeComponent(); - _timer.Tick += (s, e1) => - { - _easterEggsCount = 0; - _timer.IsEnabled = false; - }; - } - - private readonly DispatcherTimer _timer = new() - { - Interval = new(0, 0, 6), + _easterEggsCount = 0; + _timer.IsEnabled = false; }; + } - private void MaaVersionClick(object sender, MouseButtonEventArgs e) + private readonly DispatcherTimer _timer = new() + { + Interval = new(0, 0, 6), + }; + + private void MaaVersionClick(object sender, MouseButtonEventArgs e) + { + CopyToClipboardAsync($"UI Version: {VersionUpdateSettingsUserControlModel.UiVersion}\nCore Version: {VersionUpdateSettingsUserControlModel.CoreVersion}\nBuild Time: {VersionUpdateSettingsUserControlModel.BuildDateTimeCurrentCultureString}"); + EasterEggs(); + } + + private void CoreVersionClick(object sender, MouseButtonEventArgs e) + { + CopyToClipboardAsync("Core Version: " + VersionUpdateSettingsUserControlModel.CoreVersion); + EasterEggs(); + } + + private void UiVersionClick(object sender, MouseButtonEventArgs e) + { + CopyToClipboardAsync("UI Version: " + VersionUpdateSettingsUserControlModel.UiVersion); + } + + private void ResourceVersionClick(object sender, MouseButtonEventArgs e) + { + CopyToClipboardAsync($"Resource Version: {SettingsViewModel.VersionUpdateSettings.ResourceVersion}\nResource Time: {SettingsViewModel.VersionUpdateSettings.ResourceDateTimeCurrentCultureString}"); + } + + private static void CopyToClipboardAsync(string text) + { + if (string.IsNullOrEmpty(text)) { - CopyToClipboardAsync($"UI Version: {VersionUpdateSettingsUserControlModel.UiVersion}\nCore Version: {VersionUpdateSettingsUserControlModel.CoreVersion}\nBuild Time: {VersionUpdateSettingsUserControlModel.BuildDateTimeCurrentCultureString}"); - EasterEggs(); + return; } - private void CoreVersionClick(object sender, MouseButtonEventArgs e) + try { - CopyToClipboardAsync("Core Version: " + VersionUpdateSettingsUserControlModel.CoreVersion); - EasterEggs(); - } - - private void UiVersionClick(object sender, MouseButtonEventArgs e) - { - CopyToClipboardAsync("UI Version: " + VersionUpdateSettingsUserControlModel.UiVersion); - } - - private void ResourceVersionClick(object sender, MouseButtonEventArgs e) - { - CopyToClipboardAsync($"Resource Version: {SettingsViewModel.VersionUpdateSettings.ResourceVersion}\nResource Time: {SettingsViewModel.VersionUpdateSettings.ResourceDateTimeCurrentCultureString}"); - } - - private static void CopyToClipboardAsync(string text) - { - if (string.IsNullOrEmpty(text)) + var clipboardThread = new Thread(() => { - return; - } - - try - { - var clipboardThread = new Thread(() => + try { - try - { - System.Windows.Forms.Clipboard.Clear(); - System.Windows.Forms.Clipboard.SetDataObject(text, true); - } - catch (Exception e) - { - _logger.Error("Clipboard operation failed: " + e.Message); - } - }); + System.Windows.Forms.Clipboard.Clear(); + System.Windows.Forms.Clipboard.SetDataObject(text, true); + } + catch (Exception e) + { + _logger.Error("Clipboard operation failed: " + e.Message); + } + }); - clipboardThread.SetApartmentState(ApartmentState.STA); - clipboardThread.Start(); - } - catch (Exception e) - { - _logger.Error("Clipboard operation failed: " + e.Message); - } + clipboardThread.SetApartmentState(ApartmentState.STA); + clipboardThread.Start(); } - - private int _easterEggsCount; - - private void EasterEggs() + catch (Exception e) { - if (!_timer.IsEnabled) - { - _timer.IsEnabled = true; - } - - AchievementTrackerHelper.Instance.Unlock(AchievementIds.VersionClick); - - if (_easterEggsCount < 5) - { - ++_easterEggsCount; - return; - } - - MessageBoxInfo info = new() - { - Message = LocalizationHelper.GetString("EasterEggsRules"), - Caption = LocalizationHelper.GetString("EmployeeGuidelines"), - IconKey = "EasterEggsRulesIcon", - IconBrushKey = "PrimaryTextBrush", - ConfirmContent = LocalizationHelper.GetString("ConfirmExitText"), - }; - MessageBoxHelper.Show(info); - - AchievementTrackerHelper.Instance.Unlock(AchievementIds.Rules); - - /* - var growlInfo = new GrowlInfo - { - IsCustom = true, - Message = LocalizationHelper.GetString("BuyWineOnAprilFoolsDay"), - IconKey = "EasterEggsRulesIcon", - IconBrushKey = "TextIconBrush", - }; - - Growl.Info(growlInfo); - */ + _logger.Error("Clipboard operation failed: " + e.Message); } } + + private int _easterEggsCount; + + private void EasterEggs() + { + if (!_timer.IsEnabled) + { + _timer.IsEnabled = true; + } + + AchievementTrackerHelper.Instance.Unlock(AchievementIds.VersionClick); + + if (_easterEggsCount < 5) + { + ++_easterEggsCount; + return; + } + + MessageBoxInfo info = new() + { + Message = LocalizationHelper.GetString("EasterEggsRules"), + Caption = LocalizationHelper.GetString("EmployeeGuidelines"), + IconKey = "EasterEggsRulesIcon", + IconBrushKey = "PrimaryTextBrush", + ConfirmContent = LocalizationHelper.GetString("ConfirmExitText"), + }; + MessageBoxHelper.Show(info); + + AchievementTrackerHelper.Instance.Unlock(AchievementIds.Rules); + + /* + var growlInfo = new GrowlInfo + { + IsCustom = true, + Message = LocalizationHelper.GetString("BuyWineOnAprilFoolsDay"), + IconKey = "EasterEggsRulesIcon", + IconBrushKey = "TextIconBrush", + }; + + Growl.Info(growlInfo); + */ + } } diff --git a/src/MaaWpfGui/Views/UserControl/TaskQueue/AwardSettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/TaskQueue/AwardSettingsUserControl.xaml.cs index c41b92634d..3c5604736f 100644 --- a/src/MaaWpfGui/Views/UserControl/TaskQueue/AwardSettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/TaskQueue/AwardSettingsUserControl.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl.TaskQueue +namespace MaaWpfGui.Views.UserControl.TaskQueue; + +/// +/// AwardSettingsUserControl.xaml 的交互逻辑 +/// +public partial class AwardSettingsUserControl : System.Windows.Controls.UserControl { /// - /// AwardSettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class AwardSettingsUserControl : System.Windows.Controls.UserControl + public AwardSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public AwardSettingsUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/TaskQueue/FightSettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/TaskQueue/FightSettingsUserControl.xaml.cs index e4d2b664ca..eb4145e67e 100644 --- a/src/MaaWpfGui/Views/UserControl/TaskQueue/FightSettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/TaskQueue/FightSettingsUserControl.xaml.cs @@ -11,24 +11,18 @@ // but WITHOUT ANY WARRANTY // -using System.Reflection; -using System.Windows; -using System.Windows.Input; -using MaaWpfGui.Helper; +namespace MaaWpfGui.Views.UserControl.TaskQueue; -namespace MaaWpfGui.Views.UserControl.TaskQueue +/// +/// ParamSettingUserControl.xaml 的交互逻辑 +/// +public partial class FightSettingsUserControl : System.Windows.Controls.UserControl { /// - /// ParamSettingUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class FightSettingsUserControl : System.Windows.Controls.UserControl + public FightSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public FightSettingsUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/TaskQueue/InfrastSettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/TaskQueue/InfrastSettingsUserControl.xaml.cs index 59452bea54..60b6f49721 100644 --- a/src/MaaWpfGui/Views/UserControl/TaskQueue/InfrastSettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/TaskQueue/InfrastSettingsUserControl.xaml.cs @@ -11,23 +11,18 @@ // but WITHOUT ANY WARRANTY // -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; +namespace MaaWpfGui.Views.UserControl.TaskQueue; -namespace MaaWpfGui.Views.UserControl.TaskQueue +/// +/// InfrastSettingsUserControl.xaml 的交互逻辑 +/// +public partial class InfrastSettingsUserControl : System.Windows.Controls.UserControl { /// - /// InfrastSettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class InfrastSettingsUserControl : System.Windows.Controls.UserControl + public InfrastSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public InfrastSettingsUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/TaskQueue/MallSettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/TaskQueue/MallSettingsUserControl.xaml.cs index 2d35978bc7..30710a0658 100644 --- a/src/MaaWpfGui/Views/UserControl/TaskQueue/MallSettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/TaskQueue/MallSettingsUserControl.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl.TaskQueue +namespace MaaWpfGui.Views.UserControl.TaskQueue; + +/// +/// MallSettingsUserControl.xaml 的交互逻辑 +/// +public partial class MallSettingsUserControl : System.Windows.Controls.UserControl { /// - /// MallSettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class MallSettingsUserControl : System.Windows.Controls.UserControl + public MallSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public MallSettingsUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/TaskQueue/ReclamationSettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/TaskQueue/ReclamationSettingsUserControl.xaml.cs index 33c65028a1..4089f8da39 100644 --- a/src/MaaWpfGui/Views/UserControl/TaskQueue/ReclamationSettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/TaskQueue/ReclamationSettingsUserControl.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl.TaskQueue +namespace MaaWpfGui.Views.UserControl.TaskQueue; + +/// +/// ReclamationSettingsUserControl.xaml 的交互逻辑 +/// +public partial class ReclamationSettingsUserControl : System.Windows.Controls.UserControl { /// - /// ReclamationSettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class ReclamationSettingsUserControl : System.Windows.Controls.UserControl + public ReclamationSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public ReclamationSettingsUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/TaskQueue/RecruitSettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/TaskQueue/RecruitSettingsUserControl.xaml.cs index 287a571941..9e56e05d78 100644 --- a/src/MaaWpfGui/Views/UserControl/TaskQueue/RecruitSettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/TaskQueue/RecruitSettingsUserControl.xaml.cs @@ -11,21 +11,18 @@ // but WITHOUT ANY WARRANTY // -using System.Reflection; +namespace MaaWpfGui.Views.UserControl.TaskQueue; -namespace MaaWpfGui.Views.UserControl.TaskQueue +/// +/// RecruitSettingsUserControl.xaml 的交互逻辑 +/// +public partial class RecruitSettingsUserControl : System.Windows.Controls.UserControl { /// - /// RecruitSettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class RecruitSettingsUserControl : System.Windows.Controls.UserControl + public RecruitSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public RecruitSettingsUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/TaskQueue/RoguelikeSettingsUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/TaskQueue/RoguelikeSettingsUserControl.xaml.cs index be995472ba..b021331a54 100644 --- a/src/MaaWpfGui/Views/UserControl/TaskQueue/RoguelikeSettingsUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/TaskQueue/RoguelikeSettingsUserControl.xaml.cs @@ -19,68 +19,67 @@ using System.Windows.Controls; using MaaWpfGui.Helper; using MaaWpfGui.ViewModels.UI; -namespace MaaWpfGui.Views.UserControl.TaskQueue +namespace MaaWpfGui.Views.UserControl.TaskQueue; + +/// +/// RoguelikeSettingsUserControl.xaml 的交互逻辑 +/// +public partial class RoguelikeSettingsUserControl : System.Windows.Controls.UserControl { /// - /// RoguelikeSettingsUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class RoguelikeSettingsUserControl : System.Windows.Controls.UserControl + public RoguelikeSettingsUserControl() { - /// - /// Initializes a new instance of the class. - /// - public RoguelikeSettingsUserControl() - { - InitializeComponent(); - _current = this; - } + InitializeComponent(); + _current = this; + } - private static RoguelikeSettingsUserControl _current; - private static bool _isValidResult; + private static RoguelikeSettingsUserControl _current; + private static bool _isValidResult; - internal static bool IsValidResult - { - get => _isValidResult; - set - { - _isValidResult = value; - if (!IsValidResult) - { - _current.StartingCoreCharComboBox.ItemsSource = DataHelper.CharacterNames; - } - } - } - - private void StartingCoreCharComboBox_DropDownClosed(object sender, EventArgs e) + internal static bool IsValidResult + { + get => _isValidResult; + set { + _isValidResult = value; if (!IsValidResult) { - return; + _current.StartingCoreCharComboBox.ItemsSource = DataHelper.CharacterNames; } - - var name = StartingCoreCharComboBox.Text; - StartingCoreCharComboBox.ItemsSource = TaskQueueViewModel.RoguelikeTask.RoguelikeCoreCharList; - StartingCoreCharComboBox.Text = name; } } - public class StartingCoreCharRule : ValidationRule + private void StartingCoreCharComboBox_DropDownClosed(object sender, EventArgs e) { - public override ValidationResult Validate(object value, CultureInfo cultureInfo) + if (!IsValidResult) { - if (value is not string stringValue) - { - return new ValidationResult(false, HandyControl.Properties.Langs.Lang.FormatError); - } - - if (!string.IsNullOrEmpty(stringValue) && DataHelper.GetCharacterByNameOrAlias(stringValue) is null) - { - RoguelikeSettingsUserControl.IsValidResult = false; - return new ValidationResult(false, LocalizationHelper.GetString("RoguelikeStartingCoreCharNotFound")); - } - - RoguelikeSettingsUserControl.IsValidResult = true; - return ValidationResult.ValidResult; + return; } + + var name = StartingCoreCharComboBox.Text; + StartingCoreCharComboBox.ItemsSource = TaskQueueViewModel.RoguelikeTask.RoguelikeCoreCharList; + StartingCoreCharComboBox.Text = name; + } +} + +public class StartingCoreCharRule : ValidationRule +{ + public override ValidationResult Validate(object value, CultureInfo cultureInfo) + { + if (value is not string stringValue) + { + return new ValidationResult(false, HandyControl.Properties.Langs.Lang.FormatError); + } + + if (!string.IsNullOrEmpty(stringValue) && DataHelper.GetCharacterByNameOrAlias(stringValue) is null) + { + RoguelikeSettingsUserControl.IsValidResult = false; + return new ValidationResult(false, LocalizationHelper.GetString("RoguelikeStartingCoreCharNotFound")); + } + + RoguelikeSettingsUserControl.IsValidResult = true; + return ValidationResult.ValidResult; } } diff --git a/src/MaaWpfGui/Views/UserControl/TaskQueue/StartUpTaskUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/TaskQueue/StartUpTaskUserControl.xaml.cs index fc74d65509..1d64ed6f95 100644 --- a/src/MaaWpfGui/Views/UserControl/TaskQueue/StartUpTaskUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/TaskQueue/StartUpTaskUserControl.xaml.cs @@ -11,19 +11,18 @@ // but WITHOUT ANY WARRANTY // -namespace MaaWpfGui.Views.UserControl.TaskQueue +namespace MaaWpfGui.Views.UserControl.TaskQueue; + +/// +/// StartUpTaskUserControl.xaml 的交互逻辑 +/// +public partial class StartUpTaskUserControl : System.Windows.Controls.UserControl { /// - /// StartUpTaskUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class StartUpTaskUserControl : System.Windows.Controls.UserControl + public StartUpTaskUserControl() { - /// - /// Initializes a new instance of the class. - /// - public StartUpTaskUserControl() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/src/MaaWpfGui/Views/UserControl/TextDialogWithTimerUserControl.xaml.cs b/src/MaaWpfGui/Views/UserControl/TextDialogWithTimerUserControl.xaml.cs index 40242a06a4..075e7d51df 100644 --- a/src/MaaWpfGui/Views/UserControl/TextDialogWithTimerUserControl.xaml.cs +++ b/src/MaaWpfGui/Views/UserControl/TextDialogWithTimerUserControl.xaml.cs @@ -16,36 +16,35 @@ using System.Windows; using System.Windows.Controls.Primitives; using HandyControl.Tools; -namespace MaaWpfGui.Views.UserControl +namespace MaaWpfGui.Views.UserControl; + +/// +/// TextDialogWithTimerUserControl.xaml 的交互逻辑 +/// +public partial class TextDialogWithTimerUserControl : System.Windows.Controls.Border { /// - /// TextDialogWithTimerUserControl.xaml 的交互逻辑 + /// Initializes a new instance of the class. /// - public partial class TextDialogWithTimerUserControl : System.Windows.Controls.Border + /// 主要内容 + /// 提示内容 + /// 按钮内容 + /// 倒计时 + public TextDialogWithTimerUserControl(string content, string tipContent, string buttonContent, double milliseconds) { - /// - /// Initializes a new instance of the class. - /// - /// 主要内容 - /// 提示内容 - /// 按钮内容 - /// 倒计时 - public TextDialogWithTimerUserControl(string content, string tipContent, string buttonContent, double milliseconds) - { - InitializeComponent(); + InitializeComponent(); - var animation = AnimationHelper.CreateAnimation(100, milliseconds); - animation.EasingFunction = null; - animation.Completed += (s, e) => Completed?.Invoke(s, e); - Button.Click += (s, e) => Click?.Invoke(s, e); - Button.Content = buttonContent; - Content.Text = content; - Tip.Text = tipContent; - ProgressBarTimer.BeginAnimation(RangeBase.ValueProperty, animation); - } - - public event RoutedEventHandler Click; - - public event EventHandler Completed; + var animation = AnimationHelper.CreateAnimation(100, milliseconds); + animation.EasingFunction = null; + animation.Completed += (s, e) => Completed?.Invoke(s, e); + Button.Click += (s, e) => Click?.Invoke(s, e); + Button.Content = buttonContent; + Content.Text = content; + Tip.Text = tipContent; + ProgressBarTimer.BeginAnimation(RangeBase.ValueProperty, animation); } + + public event RoutedEventHandler Click; + + public event EventHandler Completed; } diff --git a/src/MaaWpfGui/WineCompat/GObject.cs b/src/MaaWpfGui/WineCompat/GObject.cs index 05f11c2ef3..442ae1d4c5 100644 --- a/src/MaaWpfGui/WineCompat/GObject.cs +++ b/src/MaaWpfGui/WineCompat/GObject.cs @@ -11,24 +11,21 @@ // but WITHOUT ANY WARRANTY // -using System; - using static MaaWpfGui.WineCompat.MaaDesktopIntegration; -namespace MaaWpfGui.WineCompat +namespace MaaWpfGui.WineCompat; + +internal class GObject { - internal class GObject + public nint Handle { get; } + + protected GObject(nint handle) { - public nint Handle { get; } + Handle = handle; + } - protected GObject(nint handle) - { - Handle = handle; - } - - ~GObject() - { - g_object_unref(Handle); - } + ~GObject() + { + g_object_unref(Handle); } } diff --git a/src/MaaWpfGui/WineCompat/WineRuntimeInformation.cs b/src/MaaWpfGui/WineCompat/WineRuntimeInformation.cs index 776b0d145c..09b0d095f7 100644 --- a/src/MaaWpfGui/WineCompat/WineRuntimeInformation.cs +++ b/src/MaaWpfGui/WineCompat/WineRuntimeInformation.cs @@ -14,55 +14,50 @@ #nullable enable using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -namespace MaaWpfGui.WineCompat +namespace MaaWpfGui.WineCompat; + +internal class WineRuntimeInformation { - internal class WineRuntimeInformation + public static bool IsRunningUnderWine { get; } + + public static string? WineVersion { get; } + + public static string? HostSystemName { get; } + + static unsafe WineRuntimeInformation() { - public static bool IsRunningUnderWine { get; } - - public static string? WineVersion { get; } - - public static string? HostSystemName { get; } - - static unsafe WineRuntimeInformation() + var ntdll = NativeLibrary.Load("ntdll.dll"); + try { - var ntdll = NativeLibrary.Load("ntdll.dll"); - try + if (NativeLibrary.TryGetExport(ntdll, "wine_get_version", out var fn)) { - if (NativeLibrary.TryGetExport(ntdll, "wine_get_version", out var fn)) + var wine_get_version = (delegate* unmanaged[Cdecl])fn; + var str = wine_get_version(); + if (str != IntPtr.Zero) { - var wine_get_version = (delegate* unmanaged[Cdecl])fn; - var str = wine_get_version(); - if (str != IntPtr.Zero) - { - IsRunningUnderWine = true; - } - - WineVersion = Marshal.PtrToStringUTF8(str); - - if (NativeLibrary.TryGetExport(ntdll, "wine_get_host_version", out fn)) - { - var wine_get_host_version = (delegate* unmanaged[Cdecl])fn; - nint sysname = 0, release = 0; - wine_get_host_version(&sysname, &release); - HostSystemName = Marshal.PtrToStringUTF8(sysname); - } + IsRunningUnderWine = true; } - else + + WineVersion = Marshal.PtrToStringUTF8(str); + + if (NativeLibrary.TryGetExport(ntdll, "wine_get_host_version", out fn)) { - IsRunningUnderWine = false; + var wine_get_host_version = (delegate* unmanaged[Cdecl])fn; + nint sysname = 0, release = 0; + wine_get_host_version(&sysname, &release); + HostSystemName = Marshal.PtrToStringUTF8(sysname); } } - finally + else { - NativeLibrary.Free(ntdll); + IsRunningUnderWine = false; } } + finally + { + NativeLibrary.Free(ntdll); + } } }