mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
impl #1082
This commit is contained in:
@@ -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<MainWindow>();
|
||||
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
|
||||
serviceProvider
|
||||
.GetRequiredService<IMetadataService>()
|
||||
.As<IMetadataServiceInitialization>()?
|
||||
@@ -176,6 +179,11 @@ internal sealed partial class Activation : IActivation
|
||||
.As<IHutaoUserServiceInitialization>()?
|
||||
.InitializeInternalAsync()
|
||||
.SafeForget();
|
||||
|
||||
serviceProvider
|
||||
.GetRequiredService<IDiscordService>()
|
||||
.SetNormalActivity()
|
||||
.SafeForget();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ internal sealed class RuntimeOptions : IOptions<RuntimeOptions>
|
||||
{
|
||||
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<RuntimeOptions>
|
||||
/// </summary>
|
||||
public bool IsElevated { get => isElevated ??= GetElevated(); }
|
||||
|
||||
public DateTimeOffset AppLaunchTime { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RuntimeOptions Value { get => this; }
|
||||
|
||||
|
||||
@@ -106,6 +106,8 @@ internal sealed partial class SettingEntry
|
||||
|
||||
public const string LaunchUseStarwardPlayTimeStatistics = "Launch.UseStarwardPlayTimeStatistics";
|
||||
|
||||
public const string LaunchSetDiscordActivityWhenPlaying = "Launch.SetDiscordActivityWhenPlaying";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 多倍启动
|
||||
/// </summary>
|
||||
|
||||
@@ -2057,6 +2057,12 @@
|
||||
<data name="ViewPageLaunchGameConfigurationSaveHint" xml:space="preserve">
|
||||
<value>所有选项仅会在启动游戏成功后保存</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameDiscordActivityDescription" xml:space="preserve">
|
||||
<value>在我游戏时设置 Discord Activity 状态</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameDiscordActivityHeader" xml:space="preserve">
|
||||
<value>Discord Activity</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
|
||||
<value>文件</value>
|
||||
</data>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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<Result> ClearActivityAsync()
|
||||
public static async ValueTask<Result> 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<Result> 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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -5,4 +5,7 @@ namespace Snap.Hutao.Service.Discord;
|
||||
|
||||
internal interface IDiscordService
|
||||
{
|
||||
ValueTask SetNormalActivity();
|
||||
|
||||
ValueTask SetPlayingActivity(bool isOversea);
|
||||
}
|
||||
@@ -38,6 +38,7 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
private bool? isMonitorEnabled;
|
||||
private AspectRatio? selectedAspectRatio;
|
||||
private bool? useStarwardPlayTimeStatistics;
|
||||
private bool? setDiscordActivityWhenPlaying;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的启动游戏选项
|
||||
@@ -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<NameValue<int>> monitors)
|
||||
{
|
||||
// This list can't use foreach
|
||||
|
||||
@@ -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<UnlockerStatus> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IDisposable> disposables = [];
|
||||
|
||||
public TDisposable Track<TDisposable>(TDisposable disposable)
|
||||
where TDisposable : IDisposable
|
||||
{
|
||||
disposables.Push(disposable);
|
||||
return disposable;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
while (disposables.TryPop(out IDisposable? disposable))
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,11 +288,11 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.8.8" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231115000" />
|
||||
<PackageReference Include="Snap.Discord.GameSDK" Version="1.2.0" />
|
||||
<PackageReference Include="Snap.Discord.GameSDK" Version="1.4.0" />
|
||||
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.507">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -290,6 +290,12 @@
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding Options.UseStarwardPlayTimeStatistics, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard
|
||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding Options.SetDiscordActivityWhenPlaying, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
Reference in New Issue
Block a user