diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
index d2afe321..2d5ebdb6 100644
--- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
+++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
@@ -791,6 +791,12 @@
参量质变仪已准备完成
+
+ 正在提瓦特大陆中探索
+
+
+ 由 {0} 启动
+
无法获取祈愿记录:{0}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordController.cs b/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordController.cs
new file mode 100644
index 00000000..8c0bf567
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordController.cs
@@ -0,0 +1,120 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Discord.GameSDK;
+using Snap.Discord.GameSDK.ABI;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Snap.Hutao.Service.Discord;
+
+internal static class DiscordController
+{
+ private const long HutaoAppId = 1173950861647552623L;
+ private const long YuanshenId = 1175743396028088370L;
+ private const long GenshinImpactId = 1175747474384760962L;
+
+ private static readonly WaitCallback RunDiscordRunCallbacks = DiscordRunCallbacks;
+ private static readonly CancellationTokenSource StopTokenSource = new();
+ private static readonly object SyncRoot = new();
+
+ private static Snap.Discord.GameSDK.Discord? discordManager;
+ private static bool isInitialized;
+
+ public static async ValueTask ClearActivityAsync()
+ {
+ ResetManager(HutaoAppId);
+ ActivityManager activityManager = discordManager.GetActivityManager();
+ return await activityManager.ClearActivityAsync().ConfigureAwait(false);
+ }
+
+ public static async ValueTask SetPlayingYuanShenAsync()
+ {
+ ResetManager(YuanshenId);
+ ActivityManager activityManager = discordManager.GetActivityManager();
+
+ Activity activity = default;
+ activity.State = SH.FormatServiceDiscordGameLaunchedBy(SH.AppName);
+ activity.Details = SH.ServiceDiscordGameActivityDetails;
+ activity.Timestamps.Start = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+ activity.Assets.LargeImage = "icon";
+ activity.Assets.LargeText = "原神";
+ activity.Assets.SmallImage = "app";
+ activity.Assets.SmallText = SH.AppName;
+
+ return await activityManager.UpdateActivityAsync(activity).ConfigureAwait(false);
+ }
+
+ public static async ValueTask SetPlayingGenshinImpactAsync()
+ {
+ ResetManager(GenshinImpactId);
+ ActivityManager activityManager = discordManager.GetActivityManager();
+
+ Activity activity = default;
+ activity.State = SH.FormatServiceDiscordGameLaunchedBy(SH.AppName);
+ activity.Details = SH.ServiceDiscordGameActivityDetails;
+ activity.Timestamps.Start = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+ activity.Assets.LargeImage = "icon";
+ activity.Assets.LargeText = "Genshin Impact";
+ activity.Assets.SmallImage = "app";
+ activity.Assets.SmallText = SH.AppName;
+
+ return await activityManager.UpdateActivityAsync(activity).ConfigureAwait(false);
+ }
+
+ public static void Stop()
+ {
+ if (!isInitialized)
+ {
+ return;
+ }
+
+ lock (SyncRoot)
+ {
+ StopTokenSource.Cancel();
+ discordManager?.Dispose();
+ }
+ }
+
+ [MemberNotNull(nameof(discordManager))]
+ private static unsafe void ResetManager(long clientId)
+ {
+ lock (SyncRoot)
+ {
+ discordManager?.Dispose();
+ }
+
+ discordManager = new(clientId, CreateFlags.NoRequireDiscord);
+ discordManager.SetLogHook(Snap.Discord.GameSDK.LogLevel.Debug, SetLogHookHandler.Create(&DebugLogDiscordMessage));
+
+ if (isInitialized)
+ {
+ return;
+ }
+
+ ThreadPool.UnsafeQueueUserWorkItem(RunDiscordRunCallbacks, StopTokenSource.Token);
+ isInitialized = true;
+
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
+ static unsafe void DebugLogDiscordMessage(Snap.Discord.GameSDK.LogLevel logLevel, byte* ptr)
+ {
+ ReadOnlySpan utf8 = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(ptr);
+ string message = System.Text.Encoding.UTF8.GetString(utf8);
+ System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK]:[{logLevel}]:{message}");
+ }
+ }
+
+ private static void DiscordRunCallbacks(object? state)
+ {
+ CancellationToken cancellationToken = (CancellationToken)state!;
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ lock (SyncRoot)
+ {
+ discordManager?.RunCallbacks();
+ }
+
+ Thread.Sleep(100);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs
new file mode 100644
index 00000000..16de3814
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs
@@ -0,0 +1,14 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.Discord;
+
+[ConstructorGenerated]
+[Injection(InjectAs.Singleton, typeof(IDiscordService))]
+internal sealed partial class DiscordService : IDiscordService, IDisposable
+{
+ public void Dispose()
+ {
+ DiscordController.Stop();
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs
new file mode 100644
index 00000000..e80f0a1f
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs
@@ -0,0 +1,8 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.Discord;
+
+internal interface IDiscordService
+{
+}
\ No newline at end of file