diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs index fa1a7c77..b8a4deb0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs @@ -4,8 +4,6 @@ using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; using Snap.Hutao.Core.Caching; -using Snap.Hutao.Model.Metadata.Converter; -using Snap.Hutao.Web; using System.Runtime.InteropServices; namespace Snap.Hutao.Control.Image; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/SpinWaitPolyfill.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/SpinWaitPolyfill.cs new file mode 100644 index 00000000..32265943 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/SpinWaitPolyfill.cs @@ -0,0 +1,16 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.Threading; + +internal static class SpinWaitPolyfill +{ + public static void SpinUntil(T state, Func condition) + { + SpinWait spinner = default; + while (!condition(state)) + { + spinner.SpinOnce(); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordController.cs b/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordController.cs index 22f3d9c6..70340061 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordController.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordController.cs @@ -1,10 +1,10 @@ // 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; +using System.Text.Unicode; namespace Snap.Hutao.Service.Discord; @@ -18,75 +18,94 @@ internal static class DiscordController private static readonly CancellationTokenSource StopTokenSource = new(); private static readonly object SyncRoot = new(); - private static Snap.Discord.GameSDK.Discord? discordManager; + private static long currentClientId; + private static unsafe IDiscordCore* discordCorePtr; private static bool isInitialized; - public static async ValueTask SetDefaultActivityAsync(DateTimeOffset startTime) + public static async ValueTask SetDefaultActivityAsync(DateTimeOffset startTime) { - ResetManagerOrIgnore(HutaoAppId); + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + return SetDefaultActivity(startTime); - if (discordManager is null) + static unsafe DiscordResult SetDefaultActivity(in DateTimeOffset startTime) { - return Result.Ok; + ResetManagerOrIgnore(HutaoAppId); + + if (discordCorePtr is null) + { + return DiscordResult.Ok; + } + + IDiscordActivityManager* activityManagerPtr = discordCorePtr->get_activity_manager(discordCorePtr); + + DiscordActivity activity = default; + activity.timestamps.start = startTime.ToUnixTimeSeconds(); + SetString(activity.assets.large_image, 128, "icon"u8); + SetString(activity.assets.large_text, 128, SH.AppName); + + return new DiscordUpdateActivityAsyncAction(activityManagerPtr).WaitUpdateActivity(activity); } - - ActivityManager activityManager = discordManager.GetActivityManager(); - - 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() + public static async ValueTask SetPlayingYuanShenAsync() { - ResetManagerOrIgnore(YuanshenId); + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + return SetPlayingYuanShen(); - if (discordManager is null) + static unsafe DiscordResult SetPlayingYuanShen() { - return Result.Ok; + ResetManagerOrIgnore(YuanshenId); + + if (discordCorePtr is null) + { + return DiscordResult.Ok; + } + + IDiscordActivityManager* activityManagerPtr = discordCorePtr->get_activity_manager(discordCorePtr); + + DiscordActivity activity = default; + SetString(activity.state, 128, SH.FormatServiceDiscordGameLaunchedBy(SH.AppName)); + SetString(activity.details, 128, SH.ServiceDiscordGameActivityDetails); + activity.timestamps.start = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + SetString(activity.assets.large_image, 128, "icon"u8); + SetString(activity.assets.large_text, 128, "原神"u8); + SetString(activity.assets.small_image, 128, "app"u8); + SetString(activity.assets.small_text, 128, SH.AppName); + + return new DiscordUpdateActivityAsyncAction(activityManagerPtr).WaitUpdateActivity(activity); } - - 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() + public static async ValueTask SetPlayingGenshinImpactAsync() { - ResetManagerOrIgnore(GenshinImpactId); + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + return SetPlayingGenshinImpact(); - if (discordManager is null) + static unsafe DiscordResult SetPlayingGenshinImpact() { - return Result.Ok; + ResetManagerOrIgnore(GenshinImpactId); + + if (discordCorePtr is null) + { + return DiscordResult.Ok; + } + + IDiscordActivityManager* activityManagerPtr = discordCorePtr->get_activity_manager(discordCorePtr); + + DiscordActivity activity = default; + SetString(activity.state, 128, SH.FormatServiceDiscordGameLaunchedBy(SH.AppName)); + SetString(activity.details, 128, SH.ServiceDiscordGameActivityDetails); + activity.timestamps.start = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + SetString(activity.assets.large_image, 128, "icon"u8); + SetString(activity.assets.large_text, 128, "Genshin Impact"u8); + SetString(activity.assets.small_image, 128, "app"u8); + SetString(activity.assets.small_text, 128, SH.AppName); + + return new DiscordUpdateActivityAsyncAction(activityManagerPtr).WaitUpdateActivity(activity); } - - 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() + public static unsafe void Stop() { if (!isInitialized) { @@ -98,7 +117,7 @@ internal static class DiscordController StopTokenSource.Cancel(); try { - discordManager?.Dispose(); + discordCorePtr = default; } catch (SEHException) { @@ -108,23 +127,30 @@ internal static class DiscordController private static unsafe void ResetManagerOrIgnore(long clientId) { - if (discordManager?.ClientId == clientId) + if (currentClientId == clientId) { return; } // Actually requires a discord client to be running on Windows platform. // If not, the following creation code will throw. - if (System.Diagnostics.Process.GetProcessesByName("Discord").Length == 0) + if (System.Diagnostics.Process.GetProcessesByName("Discord").Length <= 0) { return; } lock (SyncRoot) { - discordManager?.Dispose(); - discordManager = new(clientId, CreateFlags.NoRequireDiscord); - discordManager.SetLogHook(Snap.Discord.GameSDK.LogLevel.Debug, SetLogHookHandler.Create(&DebugWriteDiscordMessage)); + DiscordCreateParams @params = default; + Methods.DiscordCreateParamsSetDefault(&@params); + @params.client_id = clientId; + @params.flags = (uint)DiscordCreateFlags.Default; + IDiscordCore* ptr = default; + Methods.DiscordCreate(3, &@params, &ptr); + + currentClientId = clientId; + discordCorePtr = ptr; + discordCorePtr->set_log_hook(discordCorePtr, DiscordLogLevel.Debug, default, &DebugWriteDiscordMessage); } if (isInitialized) @@ -135,10 +161,10 @@ internal static class DiscordController DiscordRunCallbacksAsync(StopTokenSource.Token).SafeForget(); isInitialized = true; - [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])] - static unsafe void DebugWriteDiscordMessage(Snap.Discord.GameSDK.LogLevel logLevel, byte* ptr) + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + static unsafe void DebugWriteDiscordMessage(void* state, DiscordLogLevel logLevel, sbyte* ptr) { - ReadOnlySpan utf8 = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(ptr); + ReadOnlySpan utf8 = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)ptr); string message = System.Text.Encoding.UTF8.GetString(utf8); System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK]:[{logLevel}]:{message}"); } @@ -161,15 +187,10 @@ internal static class DiscordController { try { - discordManager?.RunCallbacks(); - } - catch (ResultException ex) - { - // If result is Ok - // Maybe the connection is reset. - if (ex.Result is not Result.Ok) + DiscordResult result = DiscordCoreRunRunCallbacks(); + if (result is not DiscordResult.Ok) { - System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK ERROR]:{ex.Result:D} {ex.Result}"); + System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK ERROR]:{result:D} {result}"); } } catch (SEHException ex) @@ -185,5 +206,65 @@ internal static class DiscordController { } } + + unsafe DiscordResult DiscordCoreRunRunCallbacks() + { + if (discordCorePtr is not null) + { + return discordCorePtr->run_callbacks(discordCorePtr); + } + + return DiscordResult.Ok; + } + } + + private static unsafe void SetString(sbyte* reference, int length, string source) + { + Span sbytes = new(reference, length); + sbytes.Clear(); + Utf8.FromUtf16(source.AsSpan(), MemoryMarshal.Cast(sbytes), out _, out _); + } + + private static unsafe void SetString(sbyte* reference, int length, in ReadOnlySpan source) + { + Span sbytes = new(reference, length); + sbytes.Clear(); + source.CopyTo(MemoryMarshal.Cast(sbytes)); + } + + private struct DiscordAsyncAction + { + public DiscordResult Result; + public bool IsCompleted; + } + + private unsafe readonly struct DiscordUpdateActivityAsyncAction + { + private readonly DiscordAsyncAction discordAsyncAction; + private readonly IDiscordActivityManager* activityManagerPtr; + + public DiscordUpdateActivityAsyncAction(IDiscordActivityManager* activityManagerPtr) + { + this.activityManagerPtr = activityManagerPtr; + } + + public DiscordResult WaitUpdateActivity(DiscordActivity activity) + { + fixed (DiscordAsyncAction* actionPtr = &discordAsyncAction) + { + activityManagerPtr->update_activity(activityManagerPtr, &activity, actionPtr, &HandleResult); + } + + SpinWaitPolyfill.SpinUntil(discordAsyncAction, (state) => state.IsCompleted); + return discordAsyncAction.Result; + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static void HandleResult(void* state, DiscordResult result) + { + DiscordAsyncAction* action = (DiscordAsyncAction*)state; + action->Result = result; + action->IsCompleted = true; + } } } \ 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 4ae4a866..d22d2b04 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -311,7 +311,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs index 436613ad..32be4c21 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs @@ -12,7 +12,6 @@ using Snap.Hutao.Web.Response; using System.Collections.Specialized; using System.IO; using System.Web; -using Windows.Foundation; namespace Snap.Hutao.View.Dialog; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/Abstraction/IBuilder.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/Abstraction/IBuilder.cs index 7818bc4b..70932709 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/Abstraction/IBuilder.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/Abstraction/IBuilder.cs @@ -3,6 +3,4 @@ namespace Snap.Hutao.Web.Request.Builder.Abstraction; -internal interface IBuilder -{ -} \ No newline at end of file +internal interface IBuilder; \ No newline at end of file