Added more UE4 blueprint support
Added blueprint support for JoinRequest() and Respond, added latent blueprint node for getting Discord profile images
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
#include "DiscordRpcBlueprint.h"
|
#include "DiscordRpcBlueprint.h"
|
||||||
|
#include "Blueprint/AsyncTaskDownloadImage.h"
|
||||||
#include "discord-rpc.h"
|
#include "discord-rpc.h"
|
||||||
|
|
||||||
DEFINE_LOG_CATEGORY(Discord)
|
DEFINE_LOG_CATEGORY(Discord)
|
||||||
@@ -9,122 +9,211 @@ DEFINE_LOG_CATEGORY(Discord)
|
|||||||
static UDiscordRpc* self = nullptr;
|
static UDiscordRpc* self = nullptr;
|
||||||
static void ReadyHandler()
|
static void ReadyHandler()
|
||||||
{
|
{
|
||||||
UE_LOG(Discord, Log, TEXT("Discord connected"));
|
UE_LOG(Discord, Log, TEXT("Discord connected"));
|
||||||
if (self) {
|
if (self) {
|
||||||
self->IsConnected = true;
|
self->IsConnected = true;
|
||||||
self->OnConnected.Broadcast();
|
self->OnConnected.Broadcast();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void DisconnectHandler(int errorCode, const char* message)
|
static void DisconnectHandler(int errorCode, const char* message)
|
||||||
{
|
{
|
||||||
auto msg = FString(message);
|
auto msg = FString(message);
|
||||||
UE_LOG(Discord, Log, TEXT("Discord disconnected (%d): %s"), errorCode, *msg);
|
UE_LOG(Discord, Log, TEXT("Discord disconnected (%d): %s"), errorCode, *msg);
|
||||||
if (self) {
|
if (self) {
|
||||||
self->IsConnected = false;
|
self->IsConnected = false;
|
||||||
self->OnDisconnected.Broadcast(errorCode, msg);
|
self->OnDisconnected.Broadcast(errorCode, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ErroredHandler(int errorCode, const char* message)
|
static void ErroredHandler(int errorCode, const char* message)
|
||||||
{
|
{
|
||||||
auto msg = FString(message);
|
auto msg = FString(message);
|
||||||
UE_LOG(Discord, Log, TEXT("Discord error (%d): %s"), errorCode, *msg);
|
UE_LOG(Discord, Log, TEXT("Discord error (%d): %s"), errorCode, *msg);
|
||||||
if (self) {
|
if (self) {
|
||||||
self->OnErrored.Broadcast(errorCode, msg);
|
self->OnErrored.Broadcast(errorCode, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void JoinGameHandler(const char* joinSecret)
|
static void JoinGameHandler(const char* joinSecret)
|
||||||
{
|
{
|
||||||
auto secret = FString(joinSecret);
|
auto secret = FString(joinSecret);
|
||||||
UE_LOG(Discord, Log, TEXT("Discord join %s"), *secret);
|
UE_LOG(Discord, Log, TEXT("Discord join %s"), *secret);
|
||||||
if (self) {
|
if (self) {
|
||||||
self->OnJoin.Broadcast(secret);
|
self->OnJoin.Broadcast(secret);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void SpectateGameHandler(const char* spectateSecret)
|
static void SpectateGameHandler(const char* spectateSecret)
|
||||||
{
|
{
|
||||||
auto secret = FString(spectateSecret);
|
auto secret = FString(spectateSecret);
|
||||||
UE_LOG(Discord, Log, TEXT("Discord spectate %s"), *secret);
|
UE_LOG(Discord, Log, TEXT("Discord spectate %s"), *secret);
|
||||||
if (self) {
|
if (self) {
|
||||||
self->OnSpectate.Broadcast(secret);
|
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,
|
void UDiscordRpc::Initialize(const FString& applicationId,
|
||||||
bool autoRegister,
|
bool autoRegister,
|
||||||
const FString& optionalSteamId)
|
const FString& optionalSteamId)
|
||||||
{
|
{
|
||||||
self = this;
|
self = this;
|
||||||
IsConnected = false;
|
IsConnected = false;
|
||||||
DiscordEventHandlers handlers{};
|
DiscordEventHandlers handlers{};
|
||||||
handlers.ready = ReadyHandler;
|
handlers.ready = ReadyHandler;
|
||||||
handlers.disconnected = DisconnectHandler;
|
handlers.disconnected = DisconnectHandler;
|
||||||
handlers.errored = ErroredHandler;
|
handlers.errored = ErroredHandler;
|
||||||
if (OnJoin.IsBound()) {
|
if (OnJoinRequest.IsBound())
|
||||||
handlers.joinGame = JoinGameHandler;
|
{
|
||||||
}
|
handlers.joinRequest = JoinRequestReceiveHandler;
|
||||||
if (OnSpectate.IsBound()) {
|
}
|
||||||
handlers.spectateGame = SpectateGameHandler;
|
if (OnJoin.IsBound()) {
|
||||||
}
|
handlers.joinGame = JoinGameHandler;
|
||||||
auto appId = StringCast<ANSICHAR>(*applicationId);
|
}
|
||||||
auto steamId = StringCast<ANSICHAR>(*optionalSteamId);
|
if (OnSpectate.IsBound()) {
|
||||||
Discord_Initialize(
|
handlers.spectateGame = SpectateGameHandler;
|
||||||
(const char*)appId.Get(), &handlers, autoRegister, (const char*)steamId.Get());
|
}
|
||||||
|
auto appId = StringCast<ANSICHAR>(*applicationId);
|
||||||
|
auto steamId = StringCast<ANSICHAR>(*optionalSteamId);
|
||||||
|
Discord_Initialize(
|
||||||
|
(const char*)appId.Get(), &handlers, autoRegister, (const char*)steamId.Get());
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDiscordRpc::Shutdown()
|
void UDiscordRpc::Shutdown()
|
||||||
{
|
{
|
||||||
Discord_Shutdown();
|
Discord_Shutdown();
|
||||||
self = nullptr;
|
self = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDiscordRpc::RunCallbacks()
|
void UDiscordRpc::RunCallbacks()
|
||||||
{
|
{
|
||||||
Discord_RunCallbacks();
|
Discord_RunCallbacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDiscordRpc::UpdatePresence()
|
void UDiscordRpc::UpdatePresence()
|
||||||
{
|
{
|
||||||
DiscordRichPresence rp{};
|
DiscordRichPresence rp{};
|
||||||
|
|
||||||
auto state = StringCast<ANSICHAR>(*RichPresence.state);
|
auto state = StringCast<ANSICHAR>(*RichPresence.state);
|
||||||
rp.state = state.Get();
|
rp.state = state.Get();
|
||||||
|
|
||||||
auto details = StringCast<ANSICHAR>(*RichPresence.details);
|
auto details = StringCast<ANSICHAR>(*RichPresence.details);
|
||||||
rp.details = details.Get();
|
rp.details = details.Get();
|
||||||
|
|
||||||
auto largeImageKey = StringCast<ANSICHAR>(*RichPresence.largeImageKey);
|
auto largeImageKey = StringCast<ANSICHAR>(*RichPresence.largeImageKey);
|
||||||
rp.largeImageKey = largeImageKey.Get();
|
rp.largeImageKey = largeImageKey.Get();
|
||||||
|
|
||||||
auto largeImageText = StringCast<ANSICHAR>(*RichPresence.largeImageText);
|
auto largeImageText = StringCast<ANSICHAR>(*RichPresence.largeImageText);
|
||||||
rp.largeImageText = largeImageText.Get();
|
rp.largeImageText = largeImageText.Get();
|
||||||
|
|
||||||
auto smallImageKey = StringCast<ANSICHAR>(*RichPresence.smallImageKey);
|
auto smallImageKey = StringCast<ANSICHAR>(*RichPresence.smallImageKey);
|
||||||
rp.smallImageKey = smallImageKey.Get();
|
rp.smallImageKey = smallImageKey.Get();
|
||||||
|
|
||||||
auto smallImageText = StringCast<ANSICHAR>(*RichPresence.smallImageText);
|
auto smallImageText = StringCast<ANSICHAR>(*RichPresence.smallImageText);
|
||||||
rp.smallImageText = smallImageText.Get();
|
rp.smallImageText = smallImageText.Get();
|
||||||
|
|
||||||
auto partyId = StringCast<ANSICHAR>(*RichPresence.partyId);
|
auto partyId = StringCast<ANSICHAR>(*RichPresence.partyId);
|
||||||
rp.partyId = partyId.Get();
|
rp.partyId = partyId.Get();
|
||||||
|
|
||||||
auto matchSecret = StringCast<ANSICHAR>(*RichPresence.matchSecret);
|
auto matchSecret = StringCast<ANSICHAR>(*RichPresence.matchSecret);
|
||||||
rp.matchSecret = matchSecret.Get();
|
rp.matchSecret = matchSecret.Get();
|
||||||
|
|
||||||
auto joinSecret = StringCast<ANSICHAR>(*RichPresence.joinSecret);
|
auto joinSecret = StringCast<ANSICHAR>(*RichPresence.joinSecret);
|
||||||
rp.joinSecret = joinSecret.Get();
|
rp.joinSecret = joinSecret.Get();
|
||||||
|
|
||||||
auto spectateSecret = StringCast<ANSICHAR>(*RichPresence.spectateSecret);
|
auto spectateSecret = StringCast<ANSICHAR>(*RichPresence.spectateSecret);
|
||||||
rp.spectateSecret = spectateSecret.Get();
|
rp.spectateSecret = spectateSecret.Get();
|
||||||
|
|
||||||
rp.startTimestamp = RichPresence.startTimestamp;
|
rp.startTimestamp = RichPresence.startTimestamp;
|
||||||
rp.endTimestamp = RichPresence.endTimestamp;
|
rp.endTimestamp = RichPresence.endTimestamp;
|
||||||
rp.partySize = RichPresence.partySize;
|
rp.partySize = RichPresence.partySize;
|
||||||
rp.partyMax = RichPresence.partyMax;
|
rp.partyMax = RichPresence.partyMax;
|
||||||
rp.instance = RichPresence.instance;
|
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<int>(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<UAsyncDiscordFetchAvatar>();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -3,19 +3,57 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
|
#include "Kismet/BlueprintAsyncActionBase.h"
|
||||||
|
#include "discord-rpc.h"
|
||||||
#include "DiscordRpcBlueprint.generated.h"
|
#include "DiscordRpcBlueprint.generated.h"
|
||||||
#include "Engine.h"
|
|
||||||
|
|
||||||
// unreal's header tool hates clang-format
|
// unreal's header tool hates clang-format
|
||||||
// clang-format off
|
// clang-format off
|
||||||
|
|
||||||
DECLARE_LOG_CATEGORY_EXTERN(Discord, Log, All);
|
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(FDiscordConnected);
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDiscordDisconnected, int, errorCode, const FString&, errorMessage);
|
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_TwoParams(FDiscordErrored, int, errorCode, const FString&, errorMessage);
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordJoin, const FString&, joinSecret);
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordJoin, const FString&, joinSecret);
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordSpectate, const FString&, spectateSecret);
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordSpectate, const FString&, spectateSecret);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordJoinRequest, FDiscordJoinRequestPayload, request);
|
||||||
|
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
@@ -114,6 +152,12 @@ public:
|
|||||||
Category = "Discord")
|
Category = "Discord")
|
||||||
FDiscordJoin OnJoin;
|
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,
|
UPROPERTY(BlueprintAssignable,
|
||||||
meta = (DisplayName = "When Discord user presses spectate", Keywords = "Discord rpc"),
|
meta = (DisplayName = "When Discord user presses spectate", Keywords = "Discord rpc"),
|
||||||
Category = "Discord")
|
Category = "Discord")
|
||||||
@@ -123,4 +167,46 @@ public:
|
|||||||
meta = (DisplayName = "Rich presence info", Keywords = "Discord rpc"),
|
meta = (DisplayName = "Rich presence info", Keywords = "Discord rpc"),
|
||||||
Category = "Discord")
|
Category = "Discord")
|
||||||
FDiscordRichPresence RichPresence;
|
FDiscordRichPresence RichPresence;
|
||||||
|
|
||||||
|
/** This map stores all of the current pending join requests*/
|
||||||
|
UPROPERTY(BlueprintReadOnly, meta = (Keywords = "Discord rpc"), Category = "Discord")
|
||||||
|
TMap<FString, FDiscordJoinRequestPayload> 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);
|
||||||
};
|
};
|
||||||
|
@@ -41,7 +41,8 @@ public class discordrpc : ModuleRules
|
|||||||
"SlateCore",
|
"SlateCore",
|
||||||
"Core",
|
"Core",
|
||||||
"discordrpcLibrary",
|
"discordrpcLibrary",
|
||||||
"Projects"
|
"Projects",
|
||||||
|
"UMG"
|
||||||
// ... add other public dependencies that you statically link with here ...
|
// ... add other public dependencies that you statically link with here ...
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user