@@ -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 < Result > SetDefaultActivityAsync ( DateTimeOffset startTime )
public static async ValueTask < Discord Result> 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 < Result > SetPlayingYuanShenAsync ( )
public static async ValueTask < Discord Result> 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 < Result > SetPlayingGenshinImpactAsync ( )
public static async ValueTask < Discord Result> 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 ( current ClientId = = 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 )
{
d iscordManager ? . Dispose ( ) ;
discordManager = new ( clientId , CreateFlags . NoRequireDiscord ) ;
discordManager . SetLogHook ( Snap . Discord . GameSDK . LogLevel . Debug , SetLogHookHandler . Create ( & DebugWriteDiscordMessage ) ) ;
D iscordCreateParams @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(CallConvStdcal l)] ) ]
static unsafe void DebugWriteDiscordMessage ( Snap . Discord . GameSDK . LogLevel logLevel , byte * ptr )
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdec l)] ) ]
static unsafe void DebugWriteDiscordMessage ( void * state , Discord LogLevel logLevel , s byte* ptr )
{
ReadOnlySpan < byte > utf8 = MemoryMarshal . CreateReadOnlySpanFromNullTerminated ( ptr ) ;
ReadOnlySpan < byte > 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
{
d iscordManager ? . RunCallbacks( ) ;
}
catch ( ResultException ex )
{
// If result is Ok
// Maybe the connection is reset.
if ( ex . Result is not Result . Ok )
D iscordResult result = DiscordCoreRun RunCallbacks( ) ;
if ( result is not DiscordResult . Ok )
{
System . Diagnostics . Debug . WriteLine ( $"[Discord.GameSDK ERROR]:{ex.R esult:D} {ex.R esult}" ) ;
System . Diagnostics . Debug . WriteLine ( $"[Discord.GameSDK ERROR]:{r esult:D} {r esult}" ) ;
}
}
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 < sbyte > sbytes = new ( reference , length ) ;
sbytes . Clear ( ) ;
Utf8 . FromUtf16 ( source . AsSpan ( ) , MemoryMarshal . Cast < sbyte , byte > ( sbytes ) , out _ , out _ ) ;
}
private static unsafe void SetString ( sbyte * reference , int length , in ReadOnlySpan < byte > source )
{
Span < sbyte > sbytes = new ( reference , length ) ;
sbytes . Clear ( ) ;
source . CopyTo ( MemoryMarshal . Cast < sbyte , byte > ( 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 ;
}
}
}