This commit is contained in:
Lightczx
2023-11-22 16:59:01 +08:00
parent 9fb79a9fbd
commit ee86f12168
13 changed files with 144 additions and 23 deletions

View File

@@ -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();
}
}

View File

@@ -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; }

View File

@@ -106,6 +106,8 @@ internal sealed partial class SettingEntry
public const string LaunchUseStarwardPlayTimeStatistics = "Launch.UseStarwardPlayTimeStatistics";
public const string LaunchSetDiscordActivityWhenPlaying = "Launch.SetDiscordActivityWhenPlaying";
/// <summary>
/// 启动游戏 多倍启动
/// </summary>

View File

@@ -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>

View File

@@ -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))

View File

@@ -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);

View File

@@ -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();

View File

@@ -5,4 +5,7 @@ namespace Snap.Hutao.Service.Discord;
internal interface IDiscordService
{
ValueTask SetNormalActivity();
ValueTask SetPlayingActivity(bool isOversea);
}

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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>

View File

@@ -290,6 +290,12 @@
HeaderIcon="{shcm:FontIcon Glyph=&#xEC92;}">
<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=&#xE8CF;}">
<ToggleSwitch Width="120" IsOn="{Binding Options.SetDiscordActivityWhenPlaying, Mode=TwoWay}"/>
</cwc:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>