From 48850065ee4836fa9f406bb82d0babda2c479e75 Mon Sep 17 00:00:00 2001 From: cyberblaststudios Date: Sun, 19 Nov 2017 19:29:39 -0600 Subject: [PATCH] Added more UE4 blueprint support Added blueprint support for JoinRequest() and Respond, added latent blueprint node for getting Discord profile images --- .../Private/DiscordRpcBlueprint.cpp | 235 ++++++++++++------ .../discordrpc/Public/DiscordRpcBlueprint.h | 88 ++++++- .../Source/discordrpc/discordrpc.Build.cs | 3 +- 3 files changed, 251 insertions(+), 75 deletions(-) diff --git a/examples/unrealstatus/Plugins/discordrpc/Source/discordrpc/Private/DiscordRpcBlueprint.cpp b/examples/unrealstatus/Plugins/discordrpc/Source/discordrpc/Private/DiscordRpcBlueprint.cpp index ec06144..2530020 100644 --- a/examples/unrealstatus/Plugins/discordrpc/Source/discordrpc/Private/DiscordRpcBlueprint.cpp +++ b/examples/unrealstatus/Plugins/discordrpc/Source/discordrpc/Private/DiscordRpcBlueprint.cpp @@ -1,7 +1,7 @@ #include "DiscordRpcBlueprint.h" - +#include "Blueprint/AsyncTaskDownloadImage.h" #include "discord-rpc.h" DEFINE_LOG_CATEGORY(Discord) @@ -9,122 +9,211 @@ DEFINE_LOG_CATEGORY(Discord) static UDiscordRpc* self = nullptr; static void ReadyHandler() { - UE_LOG(Discord, Log, TEXT("Discord connected")); - if (self) { - self->IsConnected = true; - self->OnConnected.Broadcast(); - } + UE_LOG(Discord, Log, TEXT("Discord connected")); + if (self) { + self->IsConnected = true; + self->OnConnected.Broadcast(); + } } static void DisconnectHandler(int errorCode, const char* message) { - auto msg = FString(message); - UE_LOG(Discord, Log, TEXT("Discord disconnected (%d): %s"), errorCode, *msg); - if (self) { - self->IsConnected = false; - self->OnDisconnected.Broadcast(errorCode, msg); - } + auto msg = FString(message); + UE_LOG(Discord, Log, TEXT("Discord disconnected (%d): %s"), errorCode, *msg); + if (self) { + self->IsConnected = false; + self->OnDisconnected.Broadcast(errorCode, msg); + } } static void ErroredHandler(int errorCode, const char* message) { - auto msg = FString(message); - UE_LOG(Discord, Log, TEXT("Discord error (%d): %s"), errorCode, *msg); - if (self) { - self->OnErrored.Broadcast(errorCode, msg); - } + auto msg = FString(message); + UE_LOG(Discord, Log, TEXT("Discord error (%d): %s"), errorCode, *msg); + if (self) { + self->OnErrored.Broadcast(errorCode, msg); + } } static void JoinGameHandler(const char* joinSecret) { - auto secret = FString(joinSecret); - UE_LOG(Discord, Log, TEXT("Discord join %s"), *secret); - if (self) { - self->OnJoin.Broadcast(secret); - } + auto secret = FString(joinSecret); + UE_LOG(Discord, Log, TEXT("Discord join %s"), *secret); + if (self) { + self->OnJoin.Broadcast(secret); + } } static void SpectateGameHandler(const char* spectateSecret) { - auto secret = FString(spectateSecret); - UE_LOG(Discord, Log, TEXT("Discord spectate %s"), *secret); - if (self) { - self->OnSpectate.Broadcast(secret); - } + auto secret = FString(spectateSecret); + UE_LOG(Discord, Log, TEXT("Discord spectate %s"), *secret); + if (self) { + self->OnSpectate.Broadcast(secret); + } +} + +static void JoinRequestReceiveHandler(const DiscordJoinRequest* joinRequestPayload) +{ + if (!joinRequestPayload) + { + UE_LOG(Discord, Log, TEXT("received invalid Discord join request payload")); + return; + } + + // wrap the payload with USTRUCT + FDiscordJoinRequestPayload payloadContainer(joinRequestPayload); + + UE_LOG(Discord, Log, TEXT("Discord join request from username: %s, UserID: %s, Avatar: %s"), *payloadContainer.Username, + *payloadContainer.UserID, + *payloadContainer.Avatar); + + if (self) { + // add to pending requests map + self->PendingJoinRequests.Add(payloadContainer.UserID, payloadContainer); + + self->OnJoinRequest.Broadcast(payloadContainer); + } } void UDiscordRpc::Initialize(const FString& applicationId, bool autoRegister, const FString& optionalSteamId) { - self = this; - IsConnected = false; - DiscordEventHandlers handlers{}; - handlers.ready = ReadyHandler; - handlers.disconnected = DisconnectHandler; - handlers.errored = ErroredHandler; - if (OnJoin.IsBound()) { - handlers.joinGame = JoinGameHandler; - } - if (OnSpectate.IsBound()) { - handlers.spectateGame = SpectateGameHandler; - } - auto appId = StringCast(*applicationId); - auto steamId = StringCast(*optionalSteamId); - Discord_Initialize( - (const char*)appId.Get(), &handlers, autoRegister, (const char*)steamId.Get()); + self = this; + IsConnected = false; + DiscordEventHandlers handlers{}; + handlers.ready = ReadyHandler; + handlers.disconnected = DisconnectHandler; + handlers.errored = ErroredHandler; + if (OnJoinRequest.IsBound()) + { + handlers.joinRequest = JoinRequestReceiveHandler; + } + if (OnJoin.IsBound()) { + handlers.joinGame = JoinGameHandler; + } + if (OnSpectate.IsBound()) { + handlers.spectateGame = SpectateGameHandler; + } + auto appId = StringCast(*applicationId); + auto steamId = StringCast(*optionalSteamId); + Discord_Initialize( + (const char*)appId.Get(), &handlers, autoRegister, (const char*)steamId.Get()); } void UDiscordRpc::Shutdown() { - Discord_Shutdown(); - self = nullptr; + Discord_Shutdown(); + self = nullptr; } void UDiscordRpc::RunCallbacks() { - Discord_RunCallbacks(); + Discord_RunCallbacks(); } void UDiscordRpc::UpdatePresence() { - DiscordRichPresence rp{}; + DiscordRichPresence rp{}; - auto state = StringCast(*RichPresence.state); - rp.state = state.Get(); + auto state = StringCast(*RichPresence.state); + rp.state = state.Get(); - auto details = StringCast(*RichPresence.details); - rp.details = details.Get(); + auto details = StringCast(*RichPresence.details); + rp.details = details.Get(); - auto largeImageKey = StringCast(*RichPresence.largeImageKey); - rp.largeImageKey = largeImageKey.Get(); + auto largeImageKey = StringCast(*RichPresence.largeImageKey); + rp.largeImageKey = largeImageKey.Get(); - auto largeImageText = StringCast(*RichPresence.largeImageText); - rp.largeImageText = largeImageText.Get(); + auto largeImageText = StringCast(*RichPresence.largeImageText); + rp.largeImageText = largeImageText.Get(); - auto smallImageKey = StringCast(*RichPresence.smallImageKey); - rp.smallImageKey = smallImageKey.Get(); + auto smallImageKey = StringCast(*RichPresence.smallImageKey); + rp.smallImageKey = smallImageKey.Get(); - auto smallImageText = StringCast(*RichPresence.smallImageText); - rp.smallImageText = smallImageText.Get(); + auto smallImageText = StringCast(*RichPresence.smallImageText); + rp.smallImageText = smallImageText.Get(); - auto partyId = StringCast(*RichPresence.partyId); - rp.partyId = partyId.Get(); + auto partyId = StringCast(*RichPresence.partyId); + rp.partyId = partyId.Get(); - auto matchSecret = StringCast(*RichPresence.matchSecret); - rp.matchSecret = matchSecret.Get(); + auto matchSecret = StringCast(*RichPresence.matchSecret); + rp.matchSecret = matchSecret.Get(); - auto joinSecret = StringCast(*RichPresence.joinSecret); - rp.joinSecret = joinSecret.Get(); + auto joinSecret = StringCast(*RichPresence.joinSecret); + rp.joinSecret = joinSecret.Get(); - auto spectateSecret = StringCast(*RichPresence.spectateSecret); - rp.spectateSecret = spectateSecret.Get(); + auto spectateSecret = StringCast(*RichPresence.spectateSecret); + rp.spectateSecret = spectateSecret.Get(); - rp.startTimestamp = RichPresence.startTimestamp; - rp.endTimestamp = RichPresence.endTimestamp; - rp.partySize = RichPresence.partySize; - rp.partyMax = RichPresence.partyMax; - rp.instance = RichPresence.instance; + rp.startTimestamp = RichPresence.startTimestamp; + rp.endTimestamp = RichPresence.endTimestamp; + rp.partySize = RichPresence.partySize; + rp.partyMax = RichPresence.partyMax; + rp.instance = RichPresence.instance; - Discord_UpdatePresence(&rp); + Discord_UpdatePresence(&rp); } + +void UDiscordRpc::RespondToDiscordJoinRequest(FString DiscordUserID, EDiscordJoinRequestResponse response) +{ + // send the response to discord + Discord_Respond(TCHAR_TO_ANSI(*DiscordUserID), static_cast(response)); + + // remove userID entry from current join requests map + PendingJoinRequests.FindAndRemoveChecked(DiscordUserID); +} + +UAsyncDiscordFetchAvatar::UAsyncDiscordFetchAvatar(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + if (HasAnyFlags(RF_ClassDefaultObject) == false) + { + AddToRoot(); + } +} + +UAsyncDiscordFetchAvatar* UAsyncDiscordFetchAvatar::DiscordFetchAvatar(FString UserID, FString Avatar) +{ + FString userProfileUrl = FString("http://cdn.discordapp.com/avatars/") + UserID + FString("/") + Avatar + FString(".jpg?size=512"); + + UE_LOG(Discord, Log, TEXT("Pulling Discord profile avatar image from %s"), *userProfileUrl); + + // create the download image async task + UAsyncTaskDownloadImage* asyncImageDl = UAsyncTaskDownloadImage::DownloadImage(userProfileUrl); + + UAsyncDiscordFetchAvatar* fetchAsyncTask = NewObject(); + + if (fetchAsyncTask) + { + fetchAsyncTask->BindDelegates(asyncImageDl); + fetchAsyncTask->ImageTask = asyncImageDl; + } + + return fetchAsyncTask; +} + +void UAsyncDiscordFetchAvatar::BindDelegates(UAsyncTaskDownloadImage* imageTask) +{ + if (imageTask) + { + imageTask->OnSuccess.AddDynamic(this, &UAsyncDiscordFetchAvatar::OnImageDownloadSuccess); + imageTask->OnFail.AddDynamic(this, &UAsyncDiscordFetchAvatar::OnImageDownloadFailed); + } +} + +void UAsyncDiscordFetchAvatar::OnImageDownloadSuccess(UTexture2DDynamic* texture) +{ + Successful.Broadcast(texture, UserID); + + RemoveFromRoot(); +} + +void UAsyncDiscordFetchAvatar::OnImageDownloadFailed(UTexture2DDynamic* texture) +{ + Failed.Broadcast(texture, UserID); + + RemoveFromRoot(); +} + diff --git a/examples/unrealstatus/Plugins/discordrpc/Source/discordrpc/Public/DiscordRpcBlueprint.h b/examples/unrealstatus/Plugins/discordrpc/Source/discordrpc/Public/DiscordRpcBlueprint.h index 0d6fc29..0bb41df 100644 --- a/examples/unrealstatus/Plugins/discordrpc/Source/discordrpc/Public/DiscordRpcBlueprint.h +++ b/examples/unrealstatus/Plugins/discordrpc/Source/discordrpc/Public/DiscordRpcBlueprint.h @@ -3,19 +3,57 @@ #pragma once #include "CoreMinimal.h" +#include "Kismet/BlueprintAsyncActionBase.h" +#include "discord-rpc.h" #include "DiscordRpcBlueprint.generated.h" -#include "Engine.h" // unreal's header tool hates clang-format // clang-format off DECLARE_LOG_CATEGORY_EXTERN(Discord, Log, All); +USTRUCT(BlueprintType) +struct FDiscordJoinRequestPayload +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite) + FString UserID; + UPROPERTY(BlueprintReadWrite) + FString Username; + UPROPERTY(BlueprintReadWrite) + FString Avatar; + + FDiscordJoinRequestPayload() + { + + } + + FDiscordJoinRequestPayload(const DiscordJoinRequest* request) + { + if (request) + { + UserID = FString(ANSI_TO_TCHAR(request->userId)); + Username = FString(ANSI_TO_TCHAR(request->username)); + Avatar = FString(ANSI_TO_TCHAR(request->avatar)); + } + } +}; + +UENUM(BlueprintType) +enum class EDiscordJoinRequestResponse : uint8 +{ + DJR_NO = DISCORD_REPLY_NO UMETA(DisplayName = "Yes"), + DJR_YES = DISCORD_REPLY_YES UMETA(DisplayName = "No"), + DJR_IGNORE = DISCORD_REPLY_IGNORE UMETA(DisplayName = "Ignore") +}; + DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDiscordConnected); DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDiscordDisconnected, int, errorCode, const FString&, errorMessage); DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDiscordErrored, int, errorCode, const FString&, errorMessage); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordJoin, const FString&, joinSecret); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordSpectate, const FString&, spectateSecret); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordJoinRequest, FDiscordJoinRequestPayload, request); // clang-format on @@ -114,6 +152,12 @@ public: Category = "Discord") FDiscordJoin OnJoin; + // when a user presses ask to join, this will trigger on the user that is getting the request + UPROPERTY(BlueprintAssignable, + meta = (DisplayName = "When Discord user presses Ask to join, this will be called on the client requested", Keywords = "Discord rpc"), + Category = "Discord") + FDiscordJoinRequest OnJoinRequest; + UPROPERTY(BlueprintAssignable, meta = (DisplayName = "When Discord user presses spectate", Keywords = "Discord rpc"), Category = "Discord") @@ -123,4 +167,46 @@ public: meta = (DisplayName = "Rich presence info", Keywords = "Discord rpc"), Category = "Discord") FDiscordRichPresence RichPresence; + + /** This map stores all of the current pending join requests*/ + UPROPERTY(BlueprintReadOnly, meta = (Keywords = "Discord rpc"), Category = "Discord") + TMap PendingJoinRequests; + + UFUNCTION(BlueprintCallable, meta = (Keywords = "Discord rpc"), Category = "Discord") + void RespondToDiscordJoinRequest(FString DiscordUserID, EDiscordJoinRequestResponse response); +}; + +class UTexture2DDynamic; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDiscordFetchAvatarCompleted, UTexture2DDynamic*, Texture, FString, UserID); + +UCLASS() +class DISCORDRPC_API UAsyncDiscordFetchAvatar : public UBlueprintAsyncActionBase +{ + GENERATED_UCLASS_BODY() + +public: + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", Keywords = "Discord rpc"), Category = "Discord") + static UAsyncDiscordFetchAvatar* DiscordFetchAvatar(FString UserID, FString Avatar); + + UFUNCTION() + void BindDelegates(UAsyncTaskDownloadImage* imageTask); + + UPROPERTY(BlueprintAssignable) + FDiscordFetchAvatarCompleted Successful; + + UPROPERTY(BlueprintAssignable) + FDiscordFetchAvatarCompleted Failed; + + UPROPERTY() + UAsyncTaskDownloadImage* ImageTask; + + UPROPERTY() + FString UserID; + + UFUNCTION() + void OnImageDownloadSuccess(UTexture2DDynamic* texture); + + UFUNCTION() + void OnImageDownloadFailed(UTexture2DDynamic* texture); }; diff --git a/examples/unrealstatus/Plugins/discordrpc/Source/discordrpc/discordrpc.Build.cs b/examples/unrealstatus/Plugins/discordrpc/Source/discordrpc/discordrpc.Build.cs index d34e809..44d39db 100644 --- a/examples/unrealstatus/Plugins/discordrpc/Source/discordrpc/discordrpc.Build.cs +++ b/examples/unrealstatus/Plugins/discordrpc/Source/discordrpc/discordrpc.Build.cs @@ -41,7 +41,8 @@ public class discordrpc : ModuleRules "SlateCore", "Core", "discordrpcLibrary", - "Projects" + "Projects", + "UMG" // ... add other public dependencies that you statically link with here ... } );