From ee86f12168ed166d6d5a527f09090df7789907aa Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Wed, 22 Nov 2023 16:59:01 +0800 Subject: [PATCH] impl #1082 --- .../Snap.Hutao/Core/LifeCycle/Activation.cs | 8 +++ .../Snap.Hutao/Core/RuntimeOptions.cs | 4 ++ .../Model/Entity/SettingEntry.Constant.cs | 2 + .../Snap.Hutao/Resource/Localization/SH.resx | 6 ++ .../Snap.Hutao/Service/AppOptionsExtension.cs | 4 +- .../Service/Discord/DiscordController.cs | 27 +++++++-- .../Service/Discord/DiscordService.cs | 17 ++++++ .../Service/Discord/IDiscordService.cs | 3 + .../Snap.Hutao/Service/Game/LaunchOptions.cs | 7 +++ .../Game/Process/GameProcessService.cs | 55 ++++++++++++++----- .../Game/Process/GameProcessTracker.cs | 24 ++++++++ src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 4 +- .../Snap.Hutao/View/Page/LaunchGamePage.xaml | 6 ++ 13 files changed, 144 insertions(+), 23 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessTracker.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs index 186f6917..45906a33 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Windows.AppLifecycle; using Snap.Hutao.Core.Setting; using Snap.Hutao.Service.DailyNote; +using Snap.Hutao.Service.Discord; using Snap.Hutao.Service.Hutao; using Snap.Hutao.Service.Metadata; using Snap.Hutao.Service.Navigation; @@ -165,6 +166,8 @@ internal sealed partial class Activation : IActivation serviceProvider.GetRequiredService(); + await taskContext.SwitchToBackgroundAsync(); + serviceProvider .GetRequiredService() .As()? @@ -176,6 +179,11 @@ internal sealed partial class Activation : IActivation .As()? .InitializeInternalAsync() .SafeForget(); + + serviceProvider + .GetRequiredService() + .SetNormalActivity() + .SafeForget(); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs index f5a89430..49c50cab 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs @@ -34,6 +34,8 @@ internal sealed class RuntimeOptions : IOptions { this.logger = logger; + AppLaunchTime = DateTimeOffset.UtcNow; + DataFolder = GetDataFolderPath(); LocalCache = ApplicationData.Current.LocalCacheFolder.Path; InstalledLocation = Package.Current.InstalledLocation.Path; @@ -96,6 +98,8 @@ internal sealed class RuntimeOptions : IOptions /// public bool IsElevated { get => isElevated ??= GetElevated(); } + public DateTimeOffset AppLaunchTime { get; } + /// public RuntimeOptions Value { get => this; } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs index b6e00850..97474eb7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs @@ -106,6 +106,8 @@ internal sealed partial class SettingEntry public const string LaunchUseStarwardPlayTimeStatistics = "Launch.UseStarwardPlayTimeStatistics"; + public const string LaunchSetDiscordActivityWhenPlaying = "Launch.SetDiscordActivityWhenPlaying"; + /// /// 启动游戏 多倍启动 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 5ff9d3c8..da77f1ef 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -2057,6 +2057,12 @@ 所有选项仅会在启动游戏成功后保存 + + 在我游戏时设置 Discord Activity 状态 + + + Discord Activity + 文件 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs index 70e34e60..0359ed1d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs @@ -29,9 +29,9 @@ internal static class AppOptionsExtension return true; } - public static bool TryGetGameFileName(this AppOptions appOptions, [NotNullWhen(true)] out string? gameFileName) + public static bool TryGetGamePathAndGameFileName(this AppOptions appOptions, out string gamePath, [NotNullWhen(true)] out string? gameFileName) { - string gamePath = appOptions.GamePath; + gamePath = appOptions.GamePath; gameFileName = Path.GetFileName(gamePath); if (string.IsNullOrEmpty(gameFileName)) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordController.cs b/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordController.cs index 13c79ef0..35887797 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordController.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordController.cs @@ -10,6 +10,7 @@ namespace Snap.Hutao.Service.Discord; internal static class DiscordController { + // https://discord.com/developers/applications private const long HutaoAppId = 1173950861647552623L; private const long YuanshenId = 1175743396028088370L; private const long GenshinImpactId = 1175747474384760962L; @@ -21,11 +22,17 @@ internal static class DiscordController private static Snap.Discord.GameSDK.Discord? discordManager; private static bool isInitialized; - public static async ValueTask ClearActivityAsync() + public static async ValueTask SetDefaultActivityAsync(DateTimeOffset startTime) { ResetManagerOrIgnore(HutaoAppId); ActivityManager activityManager = discordManager.GetActivityManager(); - return await activityManager.ClearActivityAsync().ConfigureAwait(false); + + Activity activity = default; + activity.Timestamps.Start = startTime.ToUnixTimeSeconds(); + activity.Assets.LargeImage = "icon"; + activity.Assets.LargeText = SH.AppName; + + return await activityManager.UpdateActivityAsync(activity).ConfigureAwait(false); } public static async ValueTask SetPlayingYuanShenAsync() @@ -87,11 +94,10 @@ internal static class DiscordController lock (SyncRoot) { discordManager?.Dispose(); + discordManager = new(clientId, CreateFlags.NoRequireDiscord); + discordManager.SetLogHook(Snap.Discord.GameSDK.LogLevel.Debug, SetLogHookHandler.Create(&DebugWriteDiscordMessage)); } - discordManager = new(clientId, CreateFlags.NoRequireDiscord); - discordManager.SetLogHook(Snap.Discord.GameSDK.LogLevel.Debug, SetLogHookHandler.Create(&DebugWriteDiscordMessage)); - if (isInitialized) { return; @@ -117,7 +123,16 @@ internal static class DiscordController { lock (SyncRoot) { - discordManager?.RunCallbacks(); + try + { + discordManager?.RunCallbacks(); + } + catch (SEHException ex) + { + // Known error codes: + // 0x80004005 E_FAIL + System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK]:[ERROR]:0x{ex.ErrorCode:X}"); + } } Thread.Sleep(100); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs index 16de3814..4fb0880e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs @@ -1,12 +1,29 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Discord.GameSDK; +using Snap.Hutao.Core; + namespace Snap.Hutao.Service.Discord; [ConstructorGenerated] [Injection(InjectAs.Singleton, typeof(IDiscordService))] internal sealed partial class DiscordService : IDiscordService, IDisposable { + private readonly RuntimeOptions runtimeOptions; + + public async ValueTask SetPlayingActivity(bool isOversea) + { + _ = isOversea + ? await DiscordController.SetPlayingGenshinImpactAsync().ConfigureAwait(false) + : await DiscordController.SetPlayingYuanShenAsync().ConfigureAwait(false); + } + + public async ValueTask SetNormalActivity() + { + _ = await DiscordController.SetDefaultActivityAsync(runtimeOptions.AppLaunchTime).ConfigureAwait(false); + } + public void Dispose() { DiscordController.Stop(); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs index e80f0a1f..46717554 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs @@ -5,4 +5,7 @@ namespace Snap.Hutao.Service.Discord; internal interface IDiscordService { + ValueTask SetNormalActivity(); + + ValueTask SetPlayingActivity(bool isOversea); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs index aa265ef3..5876b870 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs @@ -38,6 +38,7 @@ internal sealed class LaunchOptions : DbStoreOptions private bool? isMonitorEnabled; private AspectRatio? selectedAspectRatio; private bool? useStarwardPlayTimeStatistics; + private bool? setDiscordActivityWhenPlaying; /// /// 构造一个新的启动游戏选项 @@ -190,6 +191,12 @@ internal sealed class LaunchOptions : DbStoreOptions set => SetOption(ref useStarwardPlayTimeStatistics, SettingEntry.LaunchUseStarwardPlayTimeStatistics, value); } + public bool SetDiscordActivityWhenPlaying + { + get => GetOption(ref setDiscordActivityWhenPlaying, SettingEntry.LaunchSetDiscordActivityWhenPlaying, true); + set => SetOption(ref setDiscordActivityWhenPlaying, SettingEntry.LaunchSetDiscordActivityWhenPlaying, value); + } + private static void InitializeMonitors(List> monitors) { // This list can't use foreach diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs index a30c6c77..ed27178f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. using Snap.Hutao.Core; +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Service.Discord; using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Service.Game.Unlocker; using System.IO; @@ -17,17 +19,18 @@ namespace Snap.Hutao.Service.Game.Process; internal sealed partial class GameProcessService : IGameProcessService { private readonly IServiceProvider serviceProvider; + private readonly IDiscordService discordService; private readonly RuntimeOptions runtimeOptions; private readonly LaunchOptions launchOptions; private readonly AppOptions appOptions; - private volatile int runningGamesCounter; + private volatile bool isGameRunning; public bool IsGameRunning() { - if (runningGamesCounter == 0) + if (isGameRunning) { - return false; + return true; } return System.Diagnostics.Process.GetProcessesByName(YuanShenProcessName).Length > 0 @@ -41,21 +44,24 @@ internal sealed partial class GameProcessService : IGameProcessService return; } - string gamePath = appOptions.GamePath; - ArgumentException.ThrowIfNullOrEmpty(gamePath); + if (!appOptions.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName)) + { + ArgumentException.ThrowIfNullOrEmpty(gamePath); + return; // null check passing, actually never reach. + } + + bool isOversea = LaunchScheme.ExecutableIsOversea(gameFileName); progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing)); using (System.Diagnostics.Process game = InitializeGameProcess(gamePath)) { - try + using (new GameRunningTracker(this, isOversea)) { - Interlocked.Increment(ref runningGamesCounter); game.Start(); progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted)); - if (launchOptions.UseStarwardPlayTimeStatistics && appOptions.TryGetGameFileName(out string? gameFileName)) + if (launchOptions.UseStarwardPlayTimeStatistics) { - bool isOversea = LaunchScheme.ExecutableIsOversea(gameFileName); await Starward.LaunchForPlayTimeStatisticsAsync(isOversea).ConfigureAwait(false); } @@ -84,10 +90,6 @@ internal sealed partial class GameProcessService : IGameProcessService progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited)); } } - finally - { - Interlocked.Decrement(ref runningGamesCounter); - } } } @@ -131,4 +133,31 @@ internal sealed partial class GameProcessService : IGameProcessService Progress lockerProgress = new(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus))); return unlocker.UnlockAsync(options, lockerProgress, token); } + + private class GameRunningTracker : IDisposable + { + private readonly GameProcessService service; + + public GameRunningTracker(GameProcessService service, bool isOversea) + { + service.isGameRunning = true; + + if (service.launchOptions.SetDiscordActivityWhenPlaying) + { + service.discordService.SetPlayingActivity(isOversea); + } + + this.service = service; + } + + public void Dispose() + { + if (service.launchOptions.SetDiscordActivityWhenPlaying) + { + service.discordService.SetNormalActivity(); + } + + service.isGameRunning = false; + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessTracker.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessTracker.cs new file mode 100644 index 00000000..2c698e61 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessTracker.cs @@ -0,0 +1,24 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Process; + +internal sealed class GameProcessTracker : IDisposable +{ + private readonly Stack disposables = []; + + public TDisposable Track(TDisposable disposable) + where TDisposable : IDisposable + { + disposables.Push(disposable); + return disposable; + } + + public void Dispose() + { + while (disposables.TryPop(out IDisposable? disposable)) + { + disposable.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 6f9e593b..71d0bc9e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -288,11 +288,11 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index 8566e7f2..ffc86e3e 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -290,6 +290,12 @@ HeaderIcon="{shcm:FontIcon Glyph=}"> + + +