diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6fe1934 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +## [0.1.0] 2024-09-03 + +- Initial release of the Ready Player Me NextGen SDK for Unreal Engine. + +### Added + +- Custom RPM Developer window +- BasicUI sample map +- BasicLoader sample map +- Character base models can be loaded as UAssets from Developer Window + + + + diff --git a/Content/Samples/BasicLoader/BP_RpmActor.uasset b/Content/Samples/BasicLoader/BP_RpmActor.uasset new file mode 100644 index 0000000..b37b7db Binary files /dev/null and b/Content/Samples/BasicLoader/BP_RpmActor.uasset differ diff --git a/Content/Samples/BasicLoader/BP_RpmActor_C.uasset b/Content/Samples/BasicLoader/BP_RpmActor_C.uasset new file mode 100644 index 0000000..5de0f0e Binary files /dev/null and b/Content/Samples/BasicLoader/BP_RpmActor_C.uasset differ diff --git a/Content/Samples/BasicLoader/BasicLoaderSample.umap b/Content/Samples/BasicLoader/BasicLoaderSample.umap new file mode 100644 index 0000000..f9ca51c Binary files /dev/null and b/Content/Samples/BasicLoader/BasicLoaderSample.umap differ diff --git a/Content/Samples/BasicLoader/Blueprints/BP_LoaderDemoUI.uasset b/Content/Samples/BasicLoader/Blueprints/BP_LoaderDemoUI.uasset new file mode 100644 index 0000000..0cc5e93 Binary files /dev/null and b/Content/Samples/BasicLoader/Blueprints/BP_LoaderDemoUI.uasset differ diff --git a/Content/Samples/BasicLoader/Blueprints/BP_LoaderDemoUI_C.uasset b/Content/Samples/BasicLoader/Blueprints/BP_LoaderDemoUI_C.uasset new file mode 100644 index 0000000..055f579 Binary files /dev/null and b/Content/Samples/BasicLoader/Blueprints/BP_LoaderDemoUI_C.uasset differ diff --git a/Content/Samples/BasicLoader/Blueprints/BP_RpmPreviewActor.uasset b/Content/Samples/BasicLoader/Blueprints/BP_RpmPreviewActor.uasset new file mode 100644 index 0000000..33daf78 Binary files /dev/null and b/Content/Samples/BasicLoader/Blueprints/BP_RpmPreviewActor.uasset differ diff --git a/Content/Samples/BasicLoader/Blueprints/Default__BP_LoaderDemoUI_C.uasset b/Content/Samples/BasicLoader/Blueprints/Default__BP_LoaderDemoUI_C.uasset new file mode 100644 index 0000000..64cf9ec Binary files /dev/null and b/Content/Samples/BasicLoader/Blueprints/Default__BP_LoaderDemoUI_C.uasset differ diff --git a/Content/Samples/BasicLoader/Blueprints/WBP_LoaderDemoUI.uasset b/Content/Samples/BasicLoader/Blueprints/WBP_LoaderDemoUI.uasset new file mode 100644 index 0000000..2e1f211 Binary files /dev/null and b/Content/Samples/BasicLoader/Blueprints/WBP_LoaderDemoUI.uasset differ diff --git a/Content/Samples/BasicLoader/Blueprints/WBP_LoaderUI.uasset b/Content/Samples/BasicLoader/Blueprints/WBP_LoaderUI.uasset new file mode 100644 index 0000000..a022f5a Binary files /dev/null and b/Content/Samples/BasicLoader/Blueprints/WBP_LoaderUI.uasset differ diff --git a/Content/Samples/BasicLoader/Default__BP_RpmActor_C.uasset b/Content/Samples/BasicLoader/Default__BP_RpmActor_C.uasset new file mode 100644 index 0000000..6253a3c Binary files /dev/null and b/Content/Samples/BasicLoader/Default__BP_RpmActor_C.uasset differ diff --git a/Content/Samples/BasicLoader/LoaderSample.umap b/Content/Samples/BasicLoader/LoaderSample.umap new file mode 100644 index 0000000..87bde8b Binary files /dev/null and b/Content/Samples/BasicLoader/LoaderSample.umap differ diff --git a/Content/Samples/BasicLoader/RpmBasicLoaderSample.umap b/Content/Samples/BasicLoader/RpmBasicLoaderSample.umap new file mode 100644 index 0000000..bb88ae8 Binary files /dev/null and b/Content/Samples/BasicLoader/RpmBasicLoaderSample.umap differ diff --git a/Content/Samples/BasicUI/Blueprints/WBP_AssetPanel.uasset b/Content/Samples/BasicUI/Blueprints/WBP_AssetPanel.uasset new file mode 100644 index 0000000..2fe6816 Binary files /dev/null and b/Content/Samples/BasicUI/Blueprints/WBP_AssetPanel.uasset differ diff --git a/Content/Samples/BasicUI/Blueprints/WBP_BasicUI.uasset b/Content/Samples/BasicUI/Blueprints/WBP_BasicUI.uasset new file mode 100644 index 0000000..9036355 Binary files /dev/null and b/Content/Samples/BasicUI/Blueprints/WBP_BasicUI.uasset differ diff --git a/Content/Samples/BasicUI/Blueprints/WBP_BasicUISample.uasset b/Content/Samples/BasicUI/Blueprints/WBP_BasicUISample.uasset new file mode 100644 index 0000000..a0e8efa Binary files /dev/null and b/Content/Samples/BasicUI/Blueprints/WBP_BasicUISample.uasset differ diff --git a/Content/Samples/BasicUI/Blueprints/WBP_CategoryButton.uasset b/Content/Samples/BasicUI/Blueprints/WBP_CategoryButton.uasset new file mode 100644 index 0000000..faf6e70 Binary files /dev/null and b/Content/Samples/BasicUI/Blueprints/WBP_CategoryButton.uasset differ diff --git a/Content/Samples/BasicUI/Blueprints/WBP_RpmAssetButton.uasset b/Content/Samples/BasicUI/Blueprints/WBP_RpmAssetButton.uasset new file mode 100644 index 0000000..cd7dc7b Binary files /dev/null and b/Content/Samples/BasicUI/Blueprints/WBP_RpmAssetButton.uasset differ diff --git a/Content/Samples/BasicUI/Blueprints/WBP_RpmAssetCard.uasset b/Content/Samples/BasicUI/Blueprints/WBP_RpmAssetCard.uasset new file mode 100644 index 0000000..e634b5f Binary files /dev/null and b/Content/Samples/BasicUI/Blueprints/WBP_RpmAssetCard.uasset differ diff --git a/Content/Samples/BasicUI/Blueprints/WBP_RpmCategoryPanel.uasset b/Content/Samples/BasicUI/Blueprints/WBP_RpmCategoryPanel.uasset new file mode 100644 index 0000000..63c1b44 Binary files /dev/null and b/Content/Samples/BasicUI/Blueprints/WBP_RpmCategoryPanel.uasset differ diff --git a/Content/Samples/BasicUI/Icons/T-rpm-baseModel.uasset b/Content/Samples/BasicUI/Icons/T-rpm-baseModel.uasset new file mode 100644 index 0000000..2536a4f Binary files /dev/null and b/Content/Samples/BasicUI/Icons/T-rpm-baseModel.uasset differ diff --git a/Content/Samples/BasicUI/Icons/T-rpm-bottom.uasset b/Content/Samples/BasicUI/Icons/T-rpm-bottom.uasset new file mode 100644 index 0000000..cfde8a4 Binary files /dev/null and b/Content/Samples/BasicUI/Icons/T-rpm-bottom.uasset differ diff --git a/Content/Samples/BasicUI/Icons/T-rpm-custom.uasset b/Content/Samples/BasicUI/Icons/T-rpm-custom.uasset new file mode 100644 index 0000000..fd63df0 Binary files /dev/null and b/Content/Samples/BasicUI/Icons/T-rpm-custom.uasset differ diff --git a/Content/Samples/BasicUI/Icons/T-rpm-facial-hair.uasset b/Content/Samples/BasicUI/Icons/T-rpm-facial-hair.uasset new file mode 100644 index 0000000..bb1e8e8 Binary files /dev/null and b/Content/Samples/BasicUI/Icons/T-rpm-facial-hair.uasset differ diff --git a/Content/Samples/BasicUI/Icons/T-rpm-glasses.uasset b/Content/Samples/BasicUI/Icons/T-rpm-glasses.uasset new file mode 100644 index 0000000..19b0ae4 Binary files /dev/null and b/Content/Samples/BasicUI/Icons/T-rpm-glasses.uasset differ diff --git a/Content/Samples/BasicUI/Icons/T-rpm-hair.uasset b/Content/Samples/BasicUI/Icons/T-rpm-hair.uasset new file mode 100644 index 0000000..f2daba7 Binary files /dev/null and b/Content/Samples/BasicUI/Icons/T-rpm-hair.uasset differ diff --git a/Content/Samples/BasicUI/Icons/T-rpm-shoes.uasset b/Content/Samples/BasicUI/Icons/T-rpm-shoes.uasset new file mode 100644 index 0000000..5d7def3 Binary files /dev/null and b/Content/Samples/BasicUI/Icons/T-rpm-shoes.uasset differ diff --git a/Content/Samples/BasicUI/Icons/T-rpm-top.uasset b/Content/Samples/BasicUI/Icons/T-rpm-top.uasset new file mode 100644 index 0000000..b6acb8e Binary files /dev/null and b/Content/Samples/BasicUI/Icons/T-rpm-top.uasset differ diff --git a/Content/Samples/BasicUI/RpmBasicUISample.umap b/Content/Samples/BasicUI/RpmBasicUISample.umap new file mode 100644 index 0000000..f81c34e Binary files /dev/null and b/Content/Samples/BasicUI/RpmBasicUISample.umap differ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..3c76baa --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,4 @@ +The MIT License (MIT) +===================== + +This software is provided under the MIT License. diff --git a/README.md b/README.md index 5d91149..3558607 100644 --- a/README.md +++ b/README.md @@ -1 +1,16 @@ -# rpm-unreal-next-gen-sdk \ No newline at end of file +# Ready Player Me NextGen SDK for Unreal Engine + +This is the Unreal Engine SDK for Ready Player Me NextGen avatars. It allows you to easily integrate our avatars into your Unreal Engine project. + +### Requirements +- Unreal Engine 5.0 or later +- Visual Studio 2022 +- glTFRuntime plugin available here: https://github.com/rdeioris/glTFRuntime +- TransientObjectSaver plugin available here: https://github.com/readyplayerme/TransientObjectSaver + +### Installation +1. Download the latest release from the [Releases](https://github.com/readyplayerme/rpm-unreal-next-gen-sdk/releases) page. +2. Extract the contents of the zip file into your project's Plugins directory. If the Plugins directory does not exist, create it. +3. Open your project's .sln file in your IDE and build. +4. Run your project and launch the Unreal Editor. +5. In the Editor, go to Edit -> Plugins and enable the Ready Player Me NextGen SDK plugin. \ No newline at end of file diff --git a/Resources/Icon128.png b/Resources/Icon128.png new file mode 100644 index 0000000..1231d4a Binary files /dev/null and b/Resources/Icon128.png differ diff --git a/Resources/PlaceholderButtonIcon.svg b/Resources/PlaceholderButtonIcon.svg new file mode 100644 index 0000000..7302447 --- /dev/null +++ b/Resources/PlaceholderButtonIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/RpmNextGen.uplugin b/RpmNextGen.uplugin new file mode 100644 index 0000000..2091eb5 --- /dev/null +++ b/RpmNextGen.uplugin @@ -0,0 +1,51 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "0.1", + "FriendlyName": "RpmNextGen", + "Description": "Ready Player Me Next Gen SDK", + "Category": "Other", + "CreatedBy": "Ready Player Me", + "CreatedByURL": "https://readyplayer.me", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "CanContainContent": true, + "IsBetaVersion": false, + "IsExperimentalVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "RpmNextGen", + "Type": "Runtime", + "LoadingPhase": "Default", + "WhitelistPlatforms": [ + "Win64", + "Mac", + "Linux", + "Android", + "IOS" + ] + }, + { + "Name": "RpmNextGenEditor", + "Type": "Editor", + "LoadingPhase": "Default", + "WhitelistPlatforms": [ + "Win64", + "Mac", + "Linux" + ] + } + ], + "Plugins": [ + { + "Name": "glTFRuntime", + "Enabled": true + }, + { + "Name": "TransientObjectSaver", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp new file mode 100644 index 0000000..d22540b --- /dev/null +++ b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp @@ -0,0 +1,52 @@ +#include "Api/Assets/AssetApi.h" +#include "Settings/RpmDeveloperSettings.h" +#include "Api/Assets/Models/AssetListRequest.h" +#include "Api/Assets/Models/AssetListResponse.h" + +FAssetApi::FAssetApi() +{ + OnApiResponse.BindRaw(this, &FAssetApi::HandleListAssetResponse); +} + + +void FAssetApi::ListAssetsAsync(const FAssetListRequest& Request) +{ + const URpmDeveloperSettings* RpmSettings = GetDefault(); + ApiBaseUrl = RpmSettings->GetApiBaseUrl(); + if(RpmSettings->ApplicationId.IsEmpty()) + { + UE_LOG(LogTemp, Error, TEXT("Application ID is empty")); + OnListAssetsResponse.ExecuteIfBound(FAssetListResponse(), false); + return; + } + FString QueryString = Request.BuildQueryString(); + const FString Url = FString::Printf(TEXT("%s/v1/phoenix-assets%s"), *ApiBaseUrl, *QueryString); + FApiRequest ApiRequest = FApiRequest(); + ApiRequest.Url = Url; + ApiRequest.Method = GET; + + DispatchRawWithAuth(ApiRequest); +} + +void FAssetApi::HandleListAssetResponse(FString Response, bool bWasSuccessful) +{ + if(bWasSuccessful) + { + FAssetListResponse AssetListResponse = FAssetListResponse(); + if(FJsonObjectConverter::JsonObjectStringToUStruct(Response, &AssetListResponse, 0, 0)) + { + OnListAssetsResponse.ExecuteIfBound(AssetListResponse, true); + return; + } + UE_LOG(LogTemp, Error, TEXT("Failed to parse API response")); + } + else + { + UE_LOG(LogTemp, Error, TEXT("API Response was unsuccessful")); + } + OnListAssetsResponse.ExecuteIfBound(FAssetListResponse(), false); +} + +void FAssetApi::HandleListAssetTypeResponse(FString Response, bool bWasSuccessful) +{ +} diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp new file mode 100644 index 0000000..88f8f09 --- /dev/null +++ b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp @@ -0,0 +1,71 @@ +#include "Api/Assets/AssetLoader.h" +#include "HttpModule.h" +#include "RpmNextGen.h" +#include "glTFRuntime/Public/glTFRuntimeFunctionLibrary.h" +#include "Interfaces/IHttpResponse.h" +#include "Misc/FileHelper.h" +#include "HAL/PlatformFilemanager.h" + +FAssetLoader::FAssetLoader() +{ + GltfConfig = new FglTFRuntimeConfig(); + GltfConfig->TransformBaseType = EglTFRuntimeTransformBaseType::YForward; + DownloadDirectory = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/"); + + // Ensure the directory exists + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + if (!PlatformFile.DirectoryExists(*DownloadDirectory)) + { + PlatformFile.CreateDirectory(*DownloadDirectory); + } +} + +FAssetLoader::FAssetLoader(FglTFRuntimeConfig* Config) : GltfConfig(Config) +{ +} + +FAssetLoader::~FAssetLoader() +{ +} + +void FAssetLoader::LoadGLBFromURL(const FString& URL) +{ + // TODO replace this with use of WebApi class + TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); + HttpRequest->OnProcessRequestComplete().BindRaw(this, &FAssetLoader::OnLoadComplete); + HttpRequest->SetURL(URL); + HttpRequest->SetVerb(TEXT("GET")); + HttpRequest->ProcessRequest(); +} + +void FAssetLoader::OnLoadComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) +{ + TArray Content = TArray(); + UglTFRuntimeAsset* gltfAsset = nullptr; + bool AssetFileSaved = false; + if (bWasSuccessful && Response.IsValid()) + { + Content = Response->GetContent(); + + const FString FileName = FPaths::GetCleanFilename(Request->GetURL()); + const FString FilePath = DownloadDirectory / FileName; + + if(bSaveToDisk && FFileHelper::SaveArrayToFile(Response->GetContent(), *FilePath)) + { + UE_LOG(LogReadyPlayerMe, Log, TEXT("Downloaded GLB file to %s"), *FilePath); + AssetFileSaved = true; + } + if(OnGLtfAssetLoaded.IsBound()) + { + gltfAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(Content, *GltfConfig); + } + OnRequestDataReceived.ExecuteIfBound(Content, Content.Num() > 0); + OnGLtfAssetLoaded.ExecuteIfBound(gltfAsset, gltfAsset != nullptr); + OnAssetSaved.ExecuteIfBound(FilePath, AssetFileSaved); + return; + } + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load GLB from URL")); + OnRequestDataReceived.ExecuteIfBound(Content, Content.Num() > 0); + OnGLtfAssetLoaded.ExecuteIfBound(gltfAsset, gltfAsset != nullptr); + OnAssetSaved.ExecuteIfBound("", AssetFileSaved); +} diff --git a/Source/RpmNextGen/Private/Api/Auth/ApiKeyAuthStrategy.cpp b/Source/RpmNextGen/Private/Api/Auth/ApiKeyAuthStrategy.cpp new file mode 100644 index 0000000..897fbd2 --- /dev/null +++ b/Source/RpmNextGen/Private/Api/Auth/ApiKeyAuthStrategy.cpp @@ -0,0 +1,32 @@ +#include "Api/Auth/ApiKeyAuthStrategy.h" +#include "Settings/RpmDeveloperSettings.h" + +class URpmSettings; + +FApiKeyAuthStrategy::FApiKeyAuthStrategy() +{ +} + +void FApiKeyAuthStrategy::AddAuthToRequest(TSharedPtr Request) +{ + const URpmDeveloperSettings* RpmSettings = GetDefault(); + if(RpmSettings->ApiKey.IsEmpty()) + { + UE_LOG(LogTemp, Error, TEXT("API Key is empty")); + OnAuthComplete.ExecuteIfBound(false); + return; + } + Request->Headers.Add(TEXT("X-API-KEY"), RpmSettings->ApiKey); + OnAuthComplete.ExecuteIfBound(true); +} + +void FApiKeyAuthStrategy::OnRefreshTokenResponse(const FRefreshTokenResponse& Response, bool bWasSuccessful) +{ +} + +void FApiKeyAuthStrategy::TryRefresh(TSharedPtr Request) +{ +} + + + diff --git a/Source/RpmNextGen/Private/Api/Auth/AuthApi.cpp b/Source/RpmNextGen/Private/Api/Auth/AuthApi.cpp new file mode 100644 index 0000000..fa43fa2 --- /dev/null +++ b/Source/RpmNextGen/Private/Api/Auth/AuthApi.cpp @@ -0,0 +1,34 @@ +#include "Api/Auth/AuthApi.h" +#include "Interfaces/IHttpResponse.h" +#include "Api/Auth/Models/RefreshTokenRequest.h" +#include "Api/Auth/Models/RefreshTokenResponse.h" +#include "Settings/RpmDeveloperSettings.h" + +FAuthApi::FAuthApi() +{ + const URpmDeveloperSettings* RpmSettings = GetDefault(); + ApiUrl = FString::Printf(TEXT("%s/refresh"), *RpmSettings->ApiBaseAuthUrl); +} + +void FAuthApi::RefreshToken(const FRefreshTokenRequest& Request) +{ + FApiRequest ApiRequest = FApiRequest(); + ApiRequest.Url = ApiUrl; + ApiRequest.Method = POST; + ApiRequest.Headers.Add(TEXT("Content-Type"), TEXT("application/json")); + ApiRequest.Payload = Request.ToJsonString(); + DispatchRaw(ApiRequest); +} + +void FAuthApi::OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) +{ + FString data = Response->GetContentAsString(); + + FRefreshTokenResponse TokenResponse; + if (bWasSuccessful && !data.IsEmpty() && FJsonObjectConverter::JsonObjectStringToUStruct(data, &TokenResponse, 0, 0)) + { + OnRefreshTokenResponse.ExecuteIfBound(TokenResponse, true); + return; + } + OnRefreshTokenResponse.ExecuteIfBound(FRefreshTokenResponse(), false); +} diff --git a/Source/RpmNextGen/Private/Api/Characters/CharacterApi.cpp b/Source/RpmNextGen/Private/Api/Characters/CharacterApi.cpp new file mode 100644 index 0000000..8245e5d --- /dev/null +++ b/Source/RpmNextGen/Private/Api/Characters/CharacterApi.cpp @@ -0,0 +1,102 @@ +#include "Api/Characters/CharacterApi.h" +#include "HttpModule.h" +#include "Api/Auth/ApiKeyAuthStrategy.h" +#include "Api/Characters/Models/CharacterFindByIdRequest.h" +#include "Api/Characters/Models/CharacterPreviewRequest.h" +#include "Api/Characters/Models/CharacterUpdateRequest.h" +#include "Interfaces/IHttpResponse.h" +#include "Settings/RpmDeveloperSettings.h" + +FCharacterApi::FCharacterApi() +{ + const URpmDeveloperSettings* RpmSettings = GetDefault(); + BaseUrl = FString::Printf(TEXT("%s/v1/characters"), *RpmSettings->GetApiBaseUrl()); + Http = &FHttpModule::Get(); + SetAuthenticationStrategy(nullptr); + if(!RpmSettings->ApiKey.IsEmpty() && RpmSettings->ApiProxyUrl.IsEmpty()) + { + SetAuthenticationStrategy(new FApiKeyAuthStrategy()); + } +} + +FCharacterApi::~FCharacterApi() +{ +} + +void FCharacterApi::CreateAsync(const FCharacterCreateRequest& Request) +{ + FApiRequest ApiRequest; + ApiRequest.Url = FString::Printf(TEXT("%s"), *BaseUrl); + ApiRequest.Method = POST; + ApiRequest.Payload = ConvertToJsonString(Request); + ApiRequest.Headers.Add(TEXT("Content-Type"), TEXT("application/json")); + DispatchRawWithAuth(ApiRequest); +} + +void FCharacterApi::UpdateAsync(const FCharacterUpdateRequest& Request) +{ + FApiRequest ApiRequest; + ApiRequest.Url = FString::Printf(TEXT("%s/%s"), *BaseUrl, *Request.Id); + ApiRequest.Method = PATCH; + ApiRequest.Payload = ConvertToJsonString(Request); + ApiRequest.Headers.Add(TEXT("Content-Type"), TEXT("application/json")); + DispatchRawWithAuth(ApiRequest); +} + +void FCharacterApi::FindByIdAsync(const FCharacterFindByIdRequest& Request) +{ + FApiRequest ApiRequest; + ApiRequest.Url = FString::Printf(TEXT("%s/%s"), *BaseUrl, *Request.Id); + ApiRequest.Method = GET; + ApiRequest.Headers.Add(TEXT("Content-Type"), TEXT("application/json")); + DispatchRawWithAuth(ApiRequest); +} + +FString FCharacterApi::GeneratePreviewUrl(const FCharacterPreviewRequest& Request) +{ + FString QueryString = BuildQueryString(Request.Params.Assets); + FString url = FString::Printf(TEXT("%s/%s/preview%s"), *BaseUrl, *Request.Id, *QueryString); + return url; +} + +void FCharacterApi::OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) +{ + FWebApiWithAuth::OnProcessResponse(Request, Response, bWasSuccessful); + bool bSuccess = bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode()); + if (Response->GetResponseCode() == 401) + { + URpmDeveloperSettings* Settings = GetMutableDefault(); + + UE_LOG(LogTemp, Error,TEXT("The request to the character API failed with a 401 response code. Please ensure that your API Key or proxy is correctly configured.")); + UE_LOG(LogTemp, Error,TEXT("API Key: %s. Proxy url = %s"), *Settings->ApiKey, *Settings->ApiProxyUrl); + return; + } + + const FString Verb = Request->GetVerb(); + if (Verb == "POST") + { + FCharacterCreateResponse CharacterCreateResponse; + bSuccess = bSuccess && FJsonObjectConverter::JsonObjectStringToUStruct( + Response->GetContentAsString(), &CharacterCreateResponse, 0, 0); + OnCharacterCreateResponse.ExecuteIfBound(CharacterCreateResponse, bSuccess); + } + else if (Verb == "PATCH") + { + FCharacterUpdateResponse CharacterUpdateResponse; + bSuccess = bSuccess && FJsonObjectConverter::JsonObjectStringToUStruct( + Response->GetContentAsString(), &CharacterUpdateResponse, 0, 0); + OnCharacterUpdateResponse.ExecuteIfBound(CharacterUpdateResponse, bSuccess); + } + else if (Verb == "GET") + { + FCharacterFindByIdResponse CharacterFindByIdResponse; + bSuccess = bSuccess && FJsonObjectConverter::JsonObjectStringToUStruct( + Response->GetContentAsString(), &CharacterFindByIdResponse, 0, 0); + OnCharacterFindResponse.ExecuteIfBound(CharacterFindByIdResponse, bSuccess); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("Unhandled verb")); + } +} + diff --git a/Source/RpmNextGen/Private/Api/Common/WebApi.cpp b/Source/RpmNextGen/Private/Api/Common/WebApi.cpp new file mode 100644 index 0000000..8d53855 --- /dev/null +++ b/Source/RpmNextGen/Private/Api/Common/WebApi.cpp @@ -0,0 +1,58 @@ +#include "Api/Common/WebApi.h" +#include "HttpModule.h" +#include "GenericPlatform/GenericPlatformHttp.h" +#include "Interfaces/IHttpResponse.h" + +FWebApi::FWebApi() +{ + Http = &FHttpModule::Get(); +} + +FWebApi::~FWebApi() {} + +void FWebApi::DispatchRaw(const FApiRequest& Data) +{ + TSharedRef Request = Http->CreateRequest(); + Request->SetURL(Data.Url); + Request->SetVerb(Data.GetVerb()); + + for (const auto& Header : Data.Headers) + { + Request->SetHeader(Header.Key, Header.Value); + } + + if (!Data.Payload.IsEmpty()) + { + Request->SetContentAsString(Data.Payload); + } + + Request->OnProcessRequestComplete().BindRaw(this, &FWebApi::OnProcessResponse); + Request->ProcessRequest(); +} + +void FWebApi::OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) +{ + if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) + { + OnApiResponse.ExecuteIfBound(Response->GetContentAsString(), true); + return; + } + FString ErrorMessage = Response.IsValid() ? Response->GetContentAsString() : TEXT("Request failed"); + UE_LOG(LogTemp, Warning, TEXT("WebApi from URL %s request failed: %s"), *Request->GetURL(), *ErrorMessage); + OnApiResponse.ExecuteIfBound(Response->GetContentAsString(), false); +} + +FString FWebApi::BuildQueryString(const TMap& QueryParams) +{ + FString QueryString; + if (QueryParams.Num() > 0) + { + QueryString.Append(TEXT("?")); + for (const auto& Param : QueryParams) + { + QueryString.Append(FString::Printf(TEXT("%s=%s&"), *FGenericPlatformHttp::UrlEncode(Param.Key), *FGenericPlatformHttp::UrlEncode(Param.Value))); + } + QueryString.RemoveFromEnd(TEXT("&")); + } + return QueryString; +} diff --git a/Source/RpmNextGen/Private/Api/Common/WebApiWithAuth.cpp b/Source/RpmNextGen/Private/Api/Common/WebApiWithAuth.cpp new file mode 100644 index 0000000..5877a38 --- /dev/null +++ b/Source/RpmNextGen/Private/Api/Common/WebApiWithAuth.cpp @@ -0,0 +1,75 @@ +#include "Api/Common/WebApiWithAuth.h" +#include "Interfaces/IHttpResponse.h" + +FWebApiWithAuth::FWebApiWithAuth() : ApiRequestData(nullptr), AuthenticationStrategy(nullptr) +{ + FWebApi(); +} + +FWebApiWithAuth::FWebApiWithAuth(IAuthenticationStrategy* InAuthenticationStrategy) : AuthenticationStrategy(nullptr) +{ + SetAuthenticationStrategy(InAuthenticationStrategy); +} + +void FWebApiWithAuth::SetAuthenticationStrategy(IAuthenticationStrategy* InAuthenticationStrategy) +{ + AuthenticationStrategy = InAuthenticationStrategy; + + if (AuthenticationStrategy != nullptr) + { + AuthenticationStrategy->OnAuthComplete.BindRaw(this, &FWebApiWithAuth::OnAuthComplete); + AuthenticationStrategy->OnTokenRefreshed.BindRaw(this, &FWebApiWithAuth::OnAuthTokenRefreshed); + } +} + +void FWebApiWithAuth::OnAuthComplete(bool bWasSuccessful) +{ + if(bWasSuccessful && ApiRequestData != nullptr) + { + DispatchRaw(*ApiRequestData); + return; + } + OnApiResponse.ExecuteIfBound(TEXT("Auth failed"), false); +} + +void FWebApiWithAuth::OnAuthTokenRefreshed(const FRefreshTokenResponseBody& Response, bool bWasSuccessful) +{ + if(bWasSuccessful) + { + const FString Key = TEXT("Authorization"); + if (ApiRequestData->Headers.Contains(Key)) + { + ApiRequestData->Headers.Remove(Key); + } + ApiRequestData->Headers.Add(Key, FString::Printf(TEXT("Bearer %s"), *Response.Token)); + DispatchRaw(*ApiRequestData); + return; + } + + OnApiResponse.ExecuteIfBound(TEXT("Auth failed"), false); +} + +void FWebApiWithAuth::DispatchRawWithAuth(FApiRequest& Data) +{ + this->ApiRequestData = MakeShared(Data); + if (AuthenticationStrategy == nullptr) + { + DispatchRaw(Data); + return; + } + + AuthenticationStrategy->AddAuthToRequest(this->ApiRequestData); +} + +void FWebApiWithAuth::OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) +{ + if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) + { + OnApiResponse.ExecuteIfBound(Response->GetContentAsString(), true); + return; + } + if(EHttpResponseCodes::Denied == Response->GetResponseCode() && AuthenticationStrategy != nullptr) + { + AuthenticationStrategy->TryRefresh(ApiRequestData); + } +} diff --git a/Source/RpmNextGen/Private/RpmActor.cpp b/Source/RpmNextGen/Private/RpmActor.cpp new file mode 100644 index 0000000..5a0aef0 --- /dev/null +++ b/Source/RpmNextGen/Private/RpmActor.cpp @@ -0,0 +1,278 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "RpmActor.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "Components/SkeletalMeshComponent.h" +#include "Animation/AnimSequence.h" +#include "glTFRuntimeSkeletalMeshComponent.h" + +// Sets default values +ARpmActor::ARpmActor() +{ + // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. + PrimaryActorTick.bCanEverTick = true; + AssetRoot = CreateDefaultSubobject(TEXT("AssetRoot")); + RootComponent = AssetRoot; + RootNodeIndex = INDEX_NONE; + bStaticMeshesAsSkeletalOnMorphTargets = true; +} + +// Called when the game starts or when spawned +void ARpmActor::BeginPlay() +{ + Super::BeginPlay(); + + if (!Asset) + { + return; + } + + SetupAsset(); +} + +void ARpmActor::LoadGltfAsset(UglTFRuntimeAsset* GltfAsset) +{ + // Before loading a new asset, clear existing components + ClearLoadedComponents(); + + Asset = GltfAsset; + SetupAsset(); +} + +void ARpmActor::ClearLoadedComponents() +{ + if (RootComponent) + { + TArray ChildComponents; + RootComponent->GetChildrenComponents(true, ChildComponents); + + for (USceneComponent* ChildComponent : ChildComponents) + { + if (ChildComponent && ChildComponent != RootComponent) + { + ChildComponent->DestroyComponent(); + } + } + } +} + +void ARpmActor::SetupAsset() +{ + if (!Asset) + { + UE_LOG(LogGLTFRuntime, Warning, TEXT("No asset to setup")); + return; + } + + double LoadingStartTime = FPlatformTime::Seconds(); + + if (RootNodeIndex > INDEX_NONE) + { + FglTFRuntimeNode Node; + if (!Asset->GetNode(RootNodeIndex, Node)) + { + return; + } + AssetRoot = nullptr; + ProcessNode(nullptr, NAME_None, Node); + } + else + { + TArray Scenes = Asset->GetScenes(); + for (FglTFRuntimeScene& Scene : Scenes) + { + USceneComponent* SceneComponent = NewObject(this, *FString::Printf(TEXT("Scene %d"), Scene.Index)); + SceneComponent->SetupAttachment(RootComponent); + SceneComponent->RegisterComponent(); + AddInstanceComponent(SceneComponent); + for (int32 NodeIndex : Scene.RootNodesIndices) + { + FglTFRuntimeNode Node; + if (!Asset->GetNode(NodeIndex, Node)) + { + return; + } + ProcessNode(SceneComponent, NAME_None, Node); + } + } + } + + for (TPair& Pair : SocketMapping) + { + for (USkeletalMeshComponent* SkeletalMeshComponent : DiscoveredSkeletalMeshComponents) + { + if (SkeletalMeshComponent->DoesSocketExist(Pair.Value)) + { + Pair.Key->AttachToComponent(SkeletalMeshComponent, FAttachmentTransformRules::KeepRelativeTransform, Pair.Value); + Pair.Key->SetRelativeTransform(FTransform::Identity); + break; + } + } + } + + UE_LOG(LogGLTFRuntime, Log, TEXT("Asset loaded in %f seconds"), FPlatformTime::Seconds() - LoadingStartTime); +} + + +void ARpmActor::ProcessNode(USceneComponent* NodeParentComponent, const FName SocketName, FglTFRuntimeNode& Node) +{ + if (Asset->NodeIsBone(Node.Index)) + { + ProcessBoneNode(NodeParentComponent, Node); + return; + } + + USceneComponent* NewComponent = CreateNewComponent(NodeParentComponent, Node); + + if (!NewComponent) + { + return; + } + + SetupComponentTags(NewComponent, Node, SocketName); + ProcessChildNodes(NewComponent, Node); +} + +void ARpmActor::ProcessBoneNode(USceneComponent* NodeParentComponent, FglTFRuntimeNode& Node) +{ + for (int32 ChildIndex : Node.ChildrenIndices) + { + FglTFRuntimeNode Child; + if (!Asset->GetNode(ChildIndex, Child)) + { + return; + } + ProcessNode(NodeParentComponent, *Child.Name, Child); + } +} + +USceneComponent* ARpmActor::CreateNewComponent(USceneComponent* NodeParentComponent, FglTFRuntimeNode& Node) +{ + USceneComponent* NewComponent = nullptr; + + // Check if the node should be a skeletal mesh component + if (Node.SkinIndex >= 0 || (bStaticMeshesAsSkeletalOnMorphTargets && Asset->MeshHasMorphTargets(Node.MeshIndex))) + { + // Create a skeletal mesh component + USkeletalMeshComponent* SkeletalMeshComponent = nullptr; + + if (SkeletalMeshConfig.bPerPolyCollision) + { + SkeletalMeshComponent = NewObject(this, GetSafeNodeName(Node)); + SkeletalMeshComponent->bEnablePerPolyCollision = true; + SkeletalMeshComponent->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); + } + else + { + SkeletalMeshComponent = NewObject(this, GetSafeNodeName(Node)); + } + + // Load and set the skeletal mesh + USkeletalMesh* SkeletalMesh = Asset->LoadSkeletalMesh(Node.MeshIndex, Node.SkinIndex, SkeletalMeshConfig); + SkeletalMeshComponent->SetSkeletalMesh(SkeletalMesh); + + // Attach and register the component + SkeletalMeshComponent->SetupAttachment(NodeParentComponent ? NodeParentComponent : RootComponent.Get()); + SkeletalMeshComponent->RegisterComponent(); + SkeletalMeshComponent->SetRelativeTransform(Node.Transform); + + // Add the component to the list of discovered skeletal mesh components + DiscoveredSkeletalMeshComponents.Add(SkeletalMeshComponent); + + NewComponent = SkeletalMeshComponent; + + // Custom event handling for when a skeletal mesh component is created + ReceiveOnSkeletalMeshComponentCreated(SkeletalMeshComponent, Node); + } + else + { + // Create a static mesh component + UStaticMeshComponent* StaticMeshComponent = nullptr; + TArray GPUInstancingTransforms; + + if (Asset->GetNodeGPUInstancingTransforms(Node.Index, GPUInstancingTransforms)) + { + UInstancedStaticMeshComponent* InstancedStaticMeshComponent = NewObject(this, GetSafeNodeName(Node)); + for (const FTransform& GPUInstanceTransform : GPUInstancingTransforms) + { + InstancedStaticMeshComponent->AddInstance(GPUInstanceTransform); + } + StaticMeshComponent = InstancedStaticMeshComponent; + } + else + { + StaticMeshComponent = NewObject(this, GetSafeNodeName(Node)); + } + + // Load and set the static mesh + UStaticMesh* StaticMesh = Asset->LoadStaticMeshLODs({Node.MeshIndex}, StaticMeshConfig); + StaticMeshComponent->SetStaticMesh(StaticMesh); + + // Attach and register the component + StaticMeshComponent->SetupAttachment(NodeParentComponent ? NodeParentComponent : RootComponent.Get()); + StaticMeshComponent->RegisterComponent(); + StaticMeshComponent->SetRelativeTransform(Node.Transform); + + NewComponent = StaticMeshComponent; + + // Custom event handling for when a static mesh component is created + ReceiveOnStaticMeshComponentCreated(StaticMeshComponent, Node); + } + + // Add the component to the actor's list of instance components + AddInstanceComponent(NewComponent); + + return NewComponent; +} + + +void ARpmActor::SetupComponentTags(USceneComponent* Component, FglTFRuntimeNode& Node, const FName SocketName) +{ + Component->ComponentTags.Add(*FString::Printf(TEXT("glTFRuntime:NodeName:%s"), *Node.Name)); + Component->ComponentTags.Add(*FString::Printf(TEXT("glTFRuntime:NodeIndex:%d"), Node.Index)); + + if (SocketName != NAME_None) + { + SocketMapping.Add(Component, SocketName); + } +} + +void ARpmActor::ProcessChildNodes(USceneComponent* NodeParentComponent, FglTFRuntimeNode& Node) +{ + for (int32 ChildIndex : Node.ChildrenIndices) + { + FglTFRuntimeNode Child; + if (!Asset->GetNode(ChildIndex, Child)) + { + return; + } + ProcessNode(NodeParentComponent, NAME_None, Child); + } +} + +// Called every frame +void ARpmActor::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); +} + +void ARpmActor::ReceiveOnStaticMeshComponentCreated_Implementation(UStaticMeshComponent* StaticMeshComponent, const FglTFRuntimeNode& Node) +{ + +} + +void ARpmActor::ReceiveOnSkeletalMeshComponentCreated_Implementation(USkeletalMeshComponent* SkeletalMeshComponent, const FglTFRuntimeNode& Node) +{ + +} + +void ARpmActor::PostUnregisterAllComponents() +{ + if (Asset) + { + Asset->ClearCache(); + Asset = nullptr; + } + Super::PostUnregisterAllComponents(); +} diff --git a/Source/RpmNextGen/Private/RpmAssetLoaderComponent.cpp b/Source/RpmNextGen/Private/RpmAssetLoaderComponent.cpp new file mode 100644 index 0000000..3bc7f19 --- /dev/null +++ b/Source/RpmNextGen/Private/RpmAssetLoaderComponent.cpp @@ -0,0 +1,52 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "RpmAssetLoaderComponent.h" +#include "RpmNextGen.h" +#include "Api/Assets/AssetLoader.h" + +class URpmDeveloperSettings; + +// Sets default values for this component's properties +URpmAssetLoaderComponent::URpmAssetLoaderComponent() +{ + // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features + // off to improve performance if you don't need them. + PrimaryComponentTick.bCanEverTick = false; + AssetLoader = MakeShared(); + AssetLoader->OnGLtfAssetLoaded.BindUObject( + this, + &URpmAssetLoaderComponent::HandleGLtfAssetLoaded + ); +} + +// Called when the game starts +void URpmAssetLoaderComponent::BeginPlay() +{ + Super::BeginPlay(); +} + +void URpmAssetLoaderComponent::LoadCharacterFromUrl(const FString Url) +{ + AssetLoader->LoadGLBFromURL(Url); +} + +void URpmAssetLoaderComponent::HandleGLtfAssetLoaded(UglTFRuntimeAsset* gltfAsset, bool bWasSuccessful) +{ + if (!gltfAsset || !bWasSuccessful) + { + UE_LOG(LogReadyPlayerMe, Log, TEXT("Failed to load gltf asset")); + OnGltfAssetLoaded.Broadcast(nullptr); + return; + } + OnGltfAssetLoaded.Broadcast(gltfAsset); +} + +// Called every frame +void URpmAssetLoaderComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + // ... +} + diff --git a/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp b/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp new file mode 100644 index 0000000..54e4c42 --- /dev/null +++ b/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp @@ -0,0 +1,42 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "RpmFunctionLibrary.h" +#include "Api/Assets/AssetApi.h" +#include "Api/Assets/Models/AssetListRequest.h" +#include "Api/Assets/Models/AssetListResponse.h" +#include "Api/Auth/ApiKeyAuthStrategy.h" +#include "Settings/RpmDeveloperSettings.h" + +void URpmFunctionLibrary::FetchFirstAssetId(UObject* WorldContextObject, const FString& AssetType, FOnAssetIdFetched OnAssetIdFetched) +{ + TSharedPtr AssetApi = MakeShared(); + const URpmDeveloperSettings* RpmSettings = GetDefault(); + if(!RpmSettings->ApiKey.IsEmpty() || RpmSettings->ApiProxyUrl.IsEmpty()) + { + AssetApi->SetAuthenticationStrategy(new FApiKeyAuthStrategy()); + } + + FAssetListQueryParams QueryParams; + QueryParams.Type = AssetType; + QueryParams.ApplicationId = RpmSettings->ApplicationId; + FAssetListRequest AssetListRequest = FAssetListRequest(QueryParams); + + if (!WorldContextObject) + { + UE_LOG(LogTemp, Error, TEXT("WorldContextObject is null")); + return; + } + + AssetApi->OnListAssetsResponse.BindLambda([OnAssetIdFetched, AssetApi](const FAssetListResponse& Response, bool bWasSuccessful) + { + FString FirstAssetId; + if (bWasSuccessful && Response.Data.Num() > 0) + { + FirstAssetId = Response.Data[0].Id; + } + OnAssetIdFetched.ExecuteIfBound(FirstAssetId); + }); + + AssetApi->ListAssetsAsync(AssetListRequest); +} diff --git a/Source/RpmNextGen/Private/RpmImageLoader.cpp b/Source/RpmNextGen/Private/RpmImageLoader.cpp new file mode 100644 index 0000000..9a32b8c --- /dev/null +++ b/Source/RpmNextGen/Private/RpmImageLoader.cpp @@ -0,0 +1,119 @@ +#include "RpmImageLoader.h" +#include "HttpModule.h" +#include "Interfaces/IHttpRequest.h" +#include "Interfaces/IHttpResponse.h" +#include "IImageWrapper.h" +#include "IImageWrapperModule.h" +#include "Async/Async.h" +#include "Modules/ModuleManager.h" +#include "Engine/Texture2D.h" +#include "Components/Image.h" +#include "Widgets/Images/SImage.h" + +void FRpmImageLoader::LoadUImageFromURL(UImage* Image, const FString& URL) +{ + if (!Image) + { + UE_LOG(LogTemp, Error, TEXT("LoadImageFromURL: Image is null")); + return; + } + + TWeakObjectPtr WeakImagePtr(Image); + DownloadImage(URL, [WeakImagePtr](UTexture2D* Texture) + { + if (WeakImagePtr.IsValid() && Texture) + { + FSlateBrush Brush; + Brush.SetResourceObject(Texture); + Brush.ImageSize = WeakImagePtr->Brush.ImageSize; + WeakImagePtr->SetBrush(Brush); + } + }); +} + +void FRpmImageLoader::LoadSImageFromURL(TSharedPtr ImageWidget, const FString& URL, + TFunction OnImageUpdated) +{ + if (!ImageWidget.IsValid()) + { + UE_LOG(LogTemp, Error, TEXT("LoadImageFromURL: SImage widget is invalid")); + return; + } + + DownloadImage(URL, [ImageWidget, OnImageUpdated](UTexture2D* Texture) + { + if (ImageWidget.IsValid() && Texture) + { + FSlateBrush* Brush = new FSlateBrush(); + Brush->SetResourceObject(Texture); + Brush->ImageSize = FVector2D(100.0f, 100.0f); // Adjust size as needed + ImageWidget->SetImage(Brush); + OnImageUpdated(Texture); + } + }); +} + +void FRpmImageLoader::DownloadImage(const FString& URL, TFunction OnImageDownloaded) +{ + TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); + HttpRequest->OnProcessRequestComplete().BindRaw(this, &FRpmImageLoader::OnImageDownloadComplete, OnImageDownloaded); + HttpRequest->SetURL(URL); + HttpRequest->SetVerb("GET"); + HttpRequest->ProcessRequest(); +} + +void FRpmImageLoader::OnImageDownloadComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, TFunction OnImageDownloaded) +{ + UTexture2D* Texture = nullptr; + if (bWasSuccessful && Response.IsValid() && Response->GetContentLength() > 0) + { + Texture = CreateTextureFromImageData(Response->GetContent()); + } + + if (!Texture) + { + UE_LOG(LogTemp, Error, TEXT("Failed to download or create texture from URL: %s"), *Request->GetURL()); + } + + AsyncTask(ENamedThreads::GameThread, [OnImageDownloaded, Texture]() + { + OnImageDownloaded(Texture); + }); +} + +UTexture2D* FRpmImageLoader::CreateTextureFromImageData(const TArray& ImageData) +{ + IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked( + FName("ImageWrapper")); + EImageFormat ImageFormat = ImageWrapperModule.DetectImageFormat(ImageData.GetData(), ImageData.Num()); + + if (ImageFormat == EImageFormat::Invalid) + { + return nullptr; + } + + TSharedPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper(ImageFormat); + if (!ImageWrapper.IsValid() || !ImageWrapper->SetCompressed(ImageData.GetData(), ImageData.Num())) + { + return nullptr; + } + + TArray UncompressedBGRA; + if (!ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedBGRA)) + { + return nullptr; + } + + UTexture2D* Texture = UTexture2D::CreateTransient(ImageWrapper->GetWidth(), ImageWrapper->GetHeight(), PF_B8G8R8A8); + if (!Texture) + { + return nullptr; + } + + void* TextureData = Texture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE); + FMemory::Memcpy(TextureData, UncompressedBGRA.GetData(), UncompressedBGRA.Num()); + Texture->GetPlatformData()->Mips[0].BulkData.Unlock(); + Texture->UpdateResource(); + + return Texture; +} diff --git a/Source/RpmNextGen/Private/RpmNextGen.cpp b/Source/RpmNextGen/Private/RpmNextGen.cpp new file mode 100644 index 0000000..818e442 --- /dev/null +++ b/Source/RpmNextGen/Private/RpmNextGen.cpp @@ -0,0 +1,22 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "RpmNextGen.h" + +DEFINE_LOG_CATEGORY(LogReadyPlayerMe); + +#define LOCTEXT_NAMESPACE "FRpmNextGenModule" + +void FRpmNextGenModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +} + +void FRpmNextGenModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FRpmNextGenModule, RpmNextGen) \ No newline at end of file diff --git a/Source/RpmNextGen/Private/RpmPreviewLoaderComponent.cpp b/Source/RpmNextGen/Private/RpmPreviewLoaderComponent.cpp new file mode 100644 index 0000000..7ebf553 --- /dev/null +++ b/Source/RpmNextGen/Private/RpmPreviewLoaderComponent.cpp @@ -0,0 +1,78 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "RpmPreviewLoaderComponent.h" + +#include "RpmNextGen.h" +#include "Api/Assets/Models/Asset.h" +#include "Api/Characters/CharacterApi.h" +#include "Api/Characters/Models/CharacterCreateResponse.h" +#include "Api/Characters/Models/CharacterFindByIdResponse.h" +#include "Api/Characters/Models/CharacterUpdateResponse.h" +#include "Settings/RpmDeveloperSettings.h" + +class URpmDeveloperSettings; + +// Sets default values for this component's properties +URpmPreviewLoaderComponent::URpmPreviewLoaderComponent() +{ + PrimaryComponentTick.bCanEverTick = false; + const URpmDeveloperSettings* RpmSettings = GetDefault(); + AppId = RpmSettings->ApplicationId; + CharacterApi = MakeShared(); + PreviewAssetMap = TMap(); + CharacterApi->OnCharacterCreateResponse.BindUObject(this, &URpmPreviewLoaderComponent::HandleCharacterCreateResponse); + CharacterApi->OnCharacterUpdateResponse.BindUObject(this, &URpmPreviewLoaderComponent::HandleCharacterUpdateResponse); + CharacterApi->OnCharacterFindResponse.BindUObject(this, &URpmPreviewLoaderComponent::HandleCharacterFindResponse); +} + +void URpmPreviewLoaderComponent::CreateCharacter(const FString& BaseModelId) +{ + FCharacterCreateRequest CharacterCreateRequest = FCharacterCreateRequest(); + CharacterCreateRequest.Data.Assets = TMap(); + CharacterCreateRequest.Data.Assets.Add("baseModel", BaseModelId); + CharacterCreateRequest.Data.ApplicationId = AppId; + + CharacterApi->CreateAsync(CharacterCreateRequest); +} + +void URpmPreviewLoaderComponent::HandleCharacterCreateResponse(FCharacterCreateResponse CharacterCreateResponse, + bool bWasSuccessful) +{ + Character = CharacterCreateResponse.Data; + LoadCharacter(Character); +} + +void URpmPreviewLoaderComponent::HandleCharacterUpdateResponse(FCharacterUpdateResponse CharacterUpdateResponse, + bool bWasSuccessful) +{ + Character = CharacterUpdateResponse.Data; +} + +void URpmPreviewLoaderComponent::HandleCharacterFindResponse(FCharacterFindByIdResponse CharacterFindByIdResponse, + bool bWasSuccessful) +{ + Character = CharacterFindByIdResponse.Data; +} + +void URpmPreviewLoaderComponent::LoadCharacter(FRpmCharacter CharacterData) +{ + LoadCharacterFromUrl(CharacterData.GlbUrl); +} + +void URpmPreviewLoaderComponent::LoadAssetPreview(FAsset AssetData) +{ + if (Character.Id.IsEmpty()) + { + UE_LOG(LogReadyPlayerMe, Warning, TEXT("Character Id is empty")); + return; + } + + PreviewAssetMap.Add(AssetData.Type, AssetData.Id); + FCharacterPreviewRequest PreviewRequest; + PreviewRequest.Id = Character.Id; + PreviewRequest.Params.Assets = PreviewAssetMap; + const FString& Url = CharacterApi->GeneratePreviewUrl(PreviewRequest); + LoadCharacterFromUrl(Url); +} + diff --git a/Source/RpmNextGen/Private/Samples/RpmAssetButtonWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmAssetButtonWidget.cpp new file mode 100644 index 0000000..b168236 --- /dev/null +++ b/Source/RpmNextGen/Private/Samples/RpmAssetButtonWidget.cpp @@ -0,0 +1,47 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "Samples/RpmAssetButtonWidget.h" +#include "RpmImageLoader.h" +#include "Components/Button.h" +#include "Components/Image.h" +#include "Components/SizeBox.h" + +void URpmAssetButtonWidget::NativeConstruct() +{ + Super::NativeConstruct(); + if (AssetButton) + { + AssetButton->OnClicked.AddDynamic(this, &URpmAssetButtonWidget::HandleButtonClicked); + DefaultColor = AssetButton->BackgroundColor; + } +} + +void URpmAssetButtonWidget::InitializeButton(const FAsset& InAssetData, const FVector2D& InImageSize) +{ + AssetData = InAssetData; + + if (AssetImage) + { + AssetImage->SetDesiredSizeOverride(InImageSize); + + FRpmImageLoader ImageLoader; + ImageLoader.LoadUImageFromURL(AssetImage, AssetData.IconUrl); + } +} + +void URpmAssetButtonWidget::HandleButtonClicked() +{ + SetSelected(!bIsSelected); + OnAssetButtonClicked.Broadcast(this); +} + +void URpmAssetButtonWidget::SetSelected(const bool bInIsSelected) +{ + bIsSelected = bInIsSelected; + + if (AssetButton) + { + const FLinearColor NewColor = bInIsSelected ? SelectedColor : DefaultColor; + AssetButton->SetBackgroundColor(NewColor); + } +} diff --git a/Source/RpmNextGen/Private/Samples/RpmAssetCardWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmAssetCardWidget.cpp new file mode 100644 index 0000000..10da885 --- /dev/null +++ b/Source/RpmNextGen/Private/Samples/RpmAssetCardWidget.cpp @@ -0,0 +1,32 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "Samples/RpmAssetCardWidget.h" +#include "RpmImageLoader.h" +#include "Api/Assets/Models/Asset.h" +#include "Components/TextBlock.h" + +void URpmAssetCardWidget::NativeConstruct() +{ + Super::NativeConstruct(); + this->SetVisibility(ESlateVisibility::Hidden); +} + +void URpmAssetCardWidget::InitializeCard(const FAsset& Asset) +{ + this->SetVisibility(ESlateVisibility::Visible); + AssetData = Asset; + + AssetCategoryText->SetText(FText::FromString(AssetData.Type)); + AssetNameText->SetText(FText::FromString(AssetData.Name)); + AssetNameText->SetVisibility( AssetData.Name.IsEmpty() ? ESlateVisibility::Hidden : ESlateVisibility::Visible ); + AssetIdText->SetText(FText::FromString(AssetData.Id)); + + LoadImage(AssetData.IconUrl); +} + +void URpmAssetCardWidget::LoadImage(const FString& URL) +{ + FRpmImageLoader ImageLoader; + ImageLoader.LoadUImageFromURL(AssetImage, URL); +} + diff --git a/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp new file mode 100644 index 0000000..a8dc7a4 --- /dev/null +++ b/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp @@ -0,0 +1,117 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "Samples/RpmAssetPanelWidget.h" +#include "Api/Assets/AssetApi.h" +#include "Api/Assets/Models/AssetListRequest.h" +#include "Api/Auth/ApiKeyAuthStrategy.h" +#include "Components/PanelWidget.h" +#include "Components/SizeBox.h" +#include "Samples/RpmAssetButtonWidget.h" +#include "Settings/RpmDeveloperSettings.h" + +void URpmAssetPanelWidget::NativeConstruct() +{ + Super::NativeConstruct(); + const URpmDeveloperSettings* RpmSettings = GetDefault(); + AssetApi = MakeShared(); + // TODO - add smarter setting of auth strategy + if(!RpmSettings->ApiKey.IsEmpty() || RpmSettings->ApiProxyUrl.IsEmpty()) + { + AssetApi->SetAuthenticationStrategy(new FApiKeyAuthStrategy()); + } + + AssetApi->OnListAssetsResponse.BindUObject(this, &URpmAssetPanelWidget::OnAssetListResponse); + + ButtonSize = FVector2D(200, 200); + ImageSize = FVector2D(200, 200); +} + +void URpmAssetPanelWidget::OnAssetListResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful) +{ + if(bWasSuccessful && AssetListResponse.Data.Num() > 0) + { + CreateButtonsFromAssets(AssetListResponse.Data); + return; + } + UE_LOG(LogTemp, Error, TEXT("Failed to fetch assets")); +} + +void URpmAssetPanelWidget::CreateButtonsFromAssets(TArray Assets) +{ + for (auto Asset : Assets) + { + CreateButton(Asset); + } + UE_LOG(LogTemp, Warning, TEXT("No assets found") ); +} + +void URpmAssetPanelWidget::ClearAllButtons() +{ + if(!AssetButtons.IsEmpty()) + { + AssetButtons.Empty(); + } + SelectedAssetButton = nullptr; +} + +void URpmAssetPanelWidget::UpdateSelectedButton(URpmAssetButtonWidget* AssetButton) +{ + if(SelectedAssetButton && SelectedAssetButton != AssetButton) + { + SelectedAssetButton->SetSelected(false); + } + SelectedAssetButton = AssetButton; +} + +void URpmAssetPanelWidget::CreateButton(const FAsset& AssetData) +{ + if (AssetButtonBlueprint) + { + UWorld* World = GetWorld(); + + if (World) + { + if (URpmAssetButtonWidget* AssetButtonInstance = CreateWidget(World, AssetButtonBlueprint)) + { + USizeBox* ButtonSizeBox = NewObject(this); + if (ButtonSizeBox && ButtonContainer) + { + ButtonSizeBox->SetWidthOverride(ButtonSize.X); + ButtonSizeBox->SetHeightOverride(ButtonSize.Y); + ButtonSizeBox->AddChild(AssetButtonInstance); + ButtonContainer->AddChild(ButtonSizeBox); + } + + AssetButtonInstance->InitializeButton(AssetData, ImageSize); + AssetButtons.Add(AssetButtonBlueprint); + AssetButtonInstance->OnAssetButtonClicked.AddDynamic(this, &URpmAssetPanelWidget::OnAssetButtonClicked); + } + } + } + else + { + UE_LOG(LogTemp, Error, TEXT("AssetButtonBlueprint is not set!")); + } +} + +void URpmAssetPanelWidget::OnAssetButtonClicked(const URpmAssetButtonWidget* AssetButton) +{ + UpdateSelectedButton(const_cast(AssetButton)); + OnAssetSelected.Broadcast(AssetButton->GetAssetData()); +} + +void URpmAssetPanelWidget::LoadAssetsOfType(const FString& AssetType) +{ + if (!AssetApi.IsValid()) + { + UE_LOG(LogTemp, Error, TEXT("AssetApi is null or invalid")); + return; + } + const URpmDeveloperSettings* RpmSettings = GetDefault(); + FAssetListQueryParams QueryParams; + QueryParams.Type = AssetType; + QueryParams.ApplicationId = RpmSettings->ApplicationId; + FAssetListRequest AssetListRequest = FAssetListRequest(QueryParams); + AssetApi->ListAssetsAsync(AssetListRequest); +} diff --git a/Source/RpmNextGen/Private/Samples/RpmCategoryButtonWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmCategoryButtonWidget.cpp new file mode 100644 index 0000000..119bd8c --- /dev/null +++ b/Source/RpmNextGen/Private/Samples/RpmCategoryButtonWidget.cpp @@ -0,0 +1,78 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "Samples/RpmCategoryButtonWidget.h" +#include "Components/Button.h" +#include "Components/Image.h" + +void URpmCategoryButtonWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + if (CategoryButton) + { + CategoryButton->OnClicked.AddDynamic(this, &URpmCategoryButtonWidget::HandleButtonClicked); + DefaultColor = CategoryButton->BackgroundColor; + } + if (CategoryImageTexture) + { + CategoryImage->SetBrushFromTexture(CategoryImageTexture); + } +} + +void URpmCategoryButtonWidget::InitializeButton(FString Category, UTexture2D* Image) +{ + AssetCategoryName = Category; + + if (CategoryImage) + { + CategoryImageTexture = Image; + CategoryImage->SetBrushFromTexture(CategoryImageTexture); + } +} + +void URpmCategoryButtonWidget::SetSelected(bool bIsSelected) +{ + if (CategoryButton) + { + const FLinearColor NewColor = bIsSelected ? SelectedColor : DefaultColor; + CategoryButton->SetBackgroundColor(NewColor); + } +} + +void URpmCategoryButtonWidget::HandleButtonClicked() +{ + SetSelected(true); + + OnCategoryClicked.Broadcast(this); +} + +#if WITH_EDITOR +// This function is called when a property is changed in the editor, it enables the CategoryImageTexture to be updated in the editor +void URpmCategoryButtonWidget::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + FName PropertyName = (PropertyChangedEvent.Property != nullptr) ? PropertyChangedEvent.Property->GetFName() : NAME_None; + + // If the property that was changed is the CategoryImageTexture apply the new texture to the CategoryImage + if (PropertyName == GET_MEMBER_NAME_CHECKED(URpmCategoryButtonWidget, CategoryImageTexture)) + { + if (CategoryImage && CategoryImageTexture) + { + CategoryImage->SetBrushFromTexture(CategoryImageTexture); + SynchronizeProperties(); + } + + } +} +#endif + +// This is also required to update the CategoryImageTexture in the editor +void URpmCategoryButtonWidget::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + if (CategoryImage && CategoryImageTexture) + { + CategoryImage->SetBrushFromTexture(CategoryImageTexture); + } +} diff --git a/Source/RpmNextGen/Private/Samples/RpmCategoryPanelWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmCategoryPanelWidget.cpp new file mode 100644 index 0000000..b25abcd --- /dev/null +++ b/Source/RpmNextGen/Private/Samples/RpmCategoryPanelWidget.cpp @@ -0,0 +1,42 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "Samples/RpmCategoryPanelWidget.h" +#include "Blueprint/WidgetTree.h" +#include "Samples/RpmCategoryButtonWidget.h" + +void URpmCategoryPanelWidget::NativeConstruct() +{ + Super::NativeConstruct(); + InitializeCategoryButtons(); +} + +void URpmCategoryPanelWidget::InitializeCategoryButtons() +{ + TArray Widgets; + WidgetTree->GetAllWidgets(Widgets); + + for (UWidget* Widget : Widgets) + { + if (URpmCategoryButtonWidget* CategoryButton = Cast(Widget)) + { + CategoryButton->InitializeButton(CategoryButton->AssetCategoryName, CategoryButton->CategoryImageTexture); + CategoryButton->OnCategoryClicked.AddDynamic(this, &URpmCategoryPanelWidget::OnCategoryButtonClicked); + } + } +} + +void URpmCategoryPanelWidget::UpdateSelectedButton(URpmCategoryButtonWidget* CategoryButton) +{ + if(SelectedCategoryButton && SelectedCategoryButton != CategoryButton) + { + SelectedCategoryButton->SetSelected(false); + } + SelectedCategoryButton = CategoryButton; +} + +void URpmCategoryPanelWidget::OnCategoryButtonClicked(URpmCategoryButtonWidget* CategoryButton) +{ + UpdateSelectedButton(CategoryButton); + OnCategorySelected.Broadcast(CategoryButton->AssetCategoryName); +} + diff --git a/Source/RpmNextGen/Private/Settings/RpmDeveloperSettings.cpp b/Source/RpmNextGen/Private/Settings/RpmDeveloperSettings.cpp new file mode 100644 index 0000000..51fb4b9 --- /dev/null +++ b/Source/RpmNextGen/Private/Settings/RpmDeveloperSettings.cpp @@ -0,0 +1,60 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "Settings/RpmDeveloperSettings.h" + +URpmDeveloperSettings::URpmDeveloperSettings() : ApiBaseUrl(TEXT("https://api.readyplayer.me")), ApiBaseAuthUrl(TEXT("https://readyplayer.me/api/auth")) +{ + LoadConfig(); +} + +void URpmDeveloperSettings::PostInitProperties() +{ + Super::PostInitProperties(); +} + +void URpmDeveloperSettings::PreSave(const ITargetPlatform* TargetPlatform) +{ + Super::PreSave(TargetPlatform); + + if(ApiKey.IsEmpty() && ApiProxyUrl.IsEmpty() && !ApplicationId.IsEmpty()) + { + return; + } + + // Ensure settings are saved before the build + SaveConfig(CPF_Config, *GetDefaultConfigFilename()); +} + +#if WITH_EDITOR +void URpmDeveloperSettings::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + SaveConfig(CPF_Config, *GetDefaultConfigFilename()); +} +#endif // WITH_EDITOR + + +void URpmDeveloperSettings::SetupDemoAccount() +{ + ApplicationId = DemoAppId; + ApiProxyUrl = DemoProxyUrl; + SaveConfig(CPF_Config, *GetDefaultConfigFilename()); +} + +void URpmDeveloperSettings::Reset() +{ + if(ApplicationId == DemoAppId) + { + ApplicationId = TEXT(""); + } + if(ApiProxyUrl == DemoProxyUrl) + { + ApiProxyUrl = TEXT(""); + } + SaveConfig(CPF_Config, *GetDefaultConfigFilename()); +} + +FString URpmDeveloperSettings::GetApiBaseUrl() const +{ + return ApiProxyUrl.IsEmpty() ? ApiBaseUrl : ApiProxyUrl; +} diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetApi.h b/Source/RpmNextGen/Public/Api/Assets/AssetApi.h new file mode 100644 index 0000000..a8d7548 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Assets/AssetApi.h @@ -0,0 +1,22 @@ +#pragma once +#include "Api/Common/WebApiWithAuth.h" + +struct FAssetListRequest; +struct FAssetListResponse; + +DECLARE_DELEGATE_TwoParams(FOnListAssetsResponse, const FAssetListResponse&, bool); +DECLARE_DELEGATE_TwoParams(FOnListAssetTypeResponse, const FAssetListResponse&, bool); + +class RPMNEXTGEN_API FAssetApi : public FWebApiWithAuth +{ +public: + FAssetApi(); + void ListAssetsAsync(const FAssetListRequest& Request); + FOnListAssetsResponse OnListAssetsResponse; + FOnListAssetTypeResponse OnListAssetTypeResponse; +private: + void HandleListAssetResponse(FString Response, bool bWasSuccessful); + void HandleListAssetTypeResponse(FString Response, bool bWasSuccessful); + + FString ApiBaseUrl; +}; diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h b/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h new file mode 100644 index 0000000..75cf1c3 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h @@ -0,0 +1,34 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Common/WebApi.h" +#include "HAL/PlatformFilemanager.h" +#include "Interfaces/IHttpRequest.h" + +struct FglTFRuntimeConfig; +class UglTFRuntimeAsset; + +DECLARE_DELEGATE_TwoParams(FOnAssetDataReceived, TArray, bool); +DECLARE_DELEGATE_TwoParams(FOnAssetSaved, FString, bool); +DECLARE_DELEGATE_TwoParams(FOnAssetDownloaded, UglTFRuntimeAsset*, bool); + + +class RPMNEXTGEN_API FAssetLoader : public FWebApi +{ +public: + FAssetLoader(); + FAssetLoader(FglTFRuntimeConfig* Config); + virtual ~FAssetLoader() override; + + void LoadGLBFromURL(const FString& URL); + + FOnAssetDataReceived OnRequestDataReceived; + FOnAssetDownloaded OnGLtfAssetLoaded; + FOnAssetSaved OnAssetSaved; + bool bSaveToDisk = false; + +protected: + FglTFRuntimeConfig* GltfConfig; + void virtual OnLoadComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); + FString DownloadDirectory; +}; diff --git a/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h b/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h new file mode 100644 index 0000000..edffd8a --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h @@ -0,0 +1,32 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Common/Models/ApiResponse.h" +#include "Asset.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FAsset : public FApiResponse +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "id")) + FString Id; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "name")) + FString Name; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "glbUrl")) + FString GlbUrl; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "iconUrl")) + FString IconUrl; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "createdAt")) + FString Type; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "createdAt")) + FDateTime CreatedAt; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "updatedAt")) + FDateTime UpdatedAt; +}; \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Api/Assets/Models/AssetListRequest.h b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListRequest.h new file mode 100644 index 0000000..982517c --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListRequest.h @@ -0,0 +1,62 @@ +#pragma once + +#include "CoreMinimal.h" +#include "AssetListRequest.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FAssetListQueryParams +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "applicationId")) + FString ApplicationId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "type")) + FString Type; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "excludeTypes")) + FString ExcludeTypes; + +}; + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FAssetListRequest +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FAssetListQueryParams Params; + + // Default constructor + FAssetListRequest() + { + } + + // Constructor that accepts FAssetListQueryParams + FAssetListRequest(const FAssetListQueryParams& InParams) + : Params(InParams) + { + } + + FString BuildQueryString() const; +}; + +inline FString FAssetListRequest::BuildQueryString() const +{ + if (Params.ApplicationId.IsEmpty() && Params.Type.IsEmpty() && Params.ExcludeTypes.IsEmpty()) return FString(); + FString QueryString = TEXT("?"); + if (!Params.ApplicationId.IsEmpty()) + { + QueryString += TEXT("applicationId=") + Params.ApplicationId + TEXT("&"); + } + if (!Params.Type.IsEmpty()) + { + QueryString += TEXT("type=") + Params.Type + TEXT("&"); + } + if (!Params.ExcludeTypes.IsEmpty()) + { + QueryString += TEXT("excludeTypes=") + Params.ExcludeTypes + TEXT("&"); + } + QueryString.RemoveFromEnd(TEXT("&")); + return QueryString; +} diff --git a/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h new file mode 100644 index 0000000..d31cd77 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h @@ -0,0 +1,17 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Assets/Models/Asset.h" +#include "AssetListResponse.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FAssetListResponse +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "data")) + TArray Data; + bool bSuccess; + int64 Status; + FString Error; +}; diff --git a/Source/RpmNextGen/Public/Api/Auth/ApiKeyAuthStrategy.h b/Source/RpmNextGen/Public/Api/Auth/ApiKeyAuthStrategy.h new file mode 100644 index 0000000..273693d --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Auth/ApiKeyAuthStrategy.h @@ -0,0 +1,14 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Auth/IAuthenticationStrategy.h" +#include "Api/Common/WebApi.h" + +class RPMNEXTGEN_API FApiKeyAuthStrategy : public IAuthenticationStrategy +{ +public: + FApiKeyAuthStrategy(); + virtual void AddAuthToRequest(TSharedPtr Request) override; + virtual void OnRefreshTokenResponse(const FRefreshTokenResponse& Response, bool bWasSuccessful) override; + virtual void TryRefresh(TSharedPtr Request) override; +}; diff --git a/Source/RpmNextGen/Public/Api/Auth/ApiRequest.h b/Source/RpmNextGen/Public/Api/Auth/ApiRequest.h new file mode 100644 index 0000000..45ba306 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Auth/ApiRequest.h @@ -0,0 +1,56 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UObject/ObjectMacros.h" +#include "ApiRequest.generated.h" + +UENUM(BlueprintType) +enum ERequestMethod { + GET, + POST, + PUT, + DELETE, + PATCH +}; + + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FApiRequest +{ + GENERATED_BODY() + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString Url; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + TEnumAsByte Method = GET; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + TMap Headers; + + FString Payload; + + FString GetVerb() const + { + switch (Method) + { + case GET: default: + return TEXT("GET"); + case POST: + return TEXT("POST"); + case PUT: + return TEXT("PUT"); + case PATCH: + return TEXT("PATCH"); + case DELETE: + return TEXT("DELETE"); + } + } +}; + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FApiRequestBody +{ + GENERATED_BODY() + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "data")) + FString Data; +}; \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Api/Auth/AuthApi.h b/Source/RpmNextGen/Public/Api/Auth/AuthApi.h new file mode 100644 index 0000000..8093000 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Auth/AuthApi.h @@ -0,0 +1,21 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Common/WebApi.h" + +struct FRefreshTokenResponse; +struct FRefreshTokenRequest; + +DECLARE_DELEGATE_TwoParams(FOnRefreshTokenResponse, const FRefreshTokenResponse&, bool); + +class RPMNEXTGEN_API FAuthApi : public FWebApi +{ +public: + FAuthApi(); + void RefreshToken(const FRefreshTokenRequest& Request); + FOnRefreshTokenResponse OnRefreshTokenResponse; + virtual void OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) override; + +private: + FString ApiUrl; +}; diff --git a/Source/RpmNextGen/Public/Api/Auth/IAuthenticationStrategy.h b/Source/RpmNextGen/Public/Api/Auth/IAuthenticationStrategy.h new file mode 100644 index 0000000..651827b --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Auth/IAuthenticationStrategy.h @@ -0,0 +1,20 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Auth/ApiRequest.h" +#include "Models/RefreshTokenResponse.h" + +DECLARE_DELEGATE_OneParam(FOnAuthComplete, bool); +DECLARE_DELEGATE_TwoParams(FOnTokenRefreshed, const FRefreshTokenResponseBody&, bool); + +class RPMNEXTGEN_API IAuthenticationStrategy +{ +public: + FOnAuthComplete OnAuthComplete; + FOnTokenRefreshed OnTokenRefreshed; + + virtual ~IAuthenticationStrategy() = default; + virtual void AddAuthToRequest(TSharedPtr Request) = 0; + virtual void TryRefresh(TSharedPtr Request) = 0; + virtual void OnRefreshTokenResponse(const FRefreshTokenResponse& Response, bool bWasSuccessful) = 0; +}; \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Api/Auth/Models/RefreshTokenRequest.h b/Source/RpmNextGen/Public/Api/Auth/Models/RefreshTokenRequest.h new file mode 100644 index 0000000..a6c08ce --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Auth/Models/RefreshTokenRequest.h @@ -0,0 +1,52 @@ +#pragma once + +#include "CoreMinimal.h" +#include "JsonObjectConverter.h" +#include "RefreshTokenRequest.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FRefreshTokenRequestBody +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString Token; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString RefreshToken; + + FString ToJsonString() const + { + FString OutputString; + FJsonObjectConverter::UStructToJsonObjectString(*this, OutputString); + return OutputString; + } + + static bool FromJsonString(const FString& JsonString, FRefreshTokenRequestBody& OutStruct) + { + return FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &OutStruct, 0, 0); + } +}; + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FRefreshTokenRequest +{ + GENERATED_BODY() + + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FRefreshTokenRequestBody Data; + + FString ToJsonString() const + { + FString OutputString; + FJsonObjectConverter::UStructToJsonObjectString(*this, OutputString); + return OutputString; + } + + static bool FromJsonString(const FString& JsonString, FRefreshTokenRequest& OutStruct) + { + return FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &OutStruct, 0, 0); + } +}; diff --git a/Source/RpmNextGen/Public/Api/Auth/Models/RefreshTokenResponse.h b/Source/RpmNextGen/Public/Api/Auth/Models/RefreshTokenResponse.h new file mode 100644 index 0000000..8a2f7b7 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Auth/Models/RefreshTokenResponse.h @@ -0,0 +1,53 @@ +#pragma once + +#include "CoreMinimal.h" +#include "JsonObjectConverter.h" +#include "Api/Common/Models/ApiResponse.h" +#include "RefreshTokenResponse.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FRefreshTokenResponseBody : public FApiResponse +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me|Auth|Response") + FString Token; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me|Auth|Response") + FString RefreshToken; + + FString ToJsonString() const + { + FString OutputString; + FJsonObjectConverter::UStructToJsonObjectString(*this, OutputString); + return OutputString; + } + + static bool FromJsonString(const FString& JsonString, FRefreshTokenResponseBody& OutStruct) + { + return FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &OutStruct, 0, 0); + } +}; + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FRefreshTokenResponse : public FApiResponse +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FRefreshTokenResponseBody Data; + + FString ToJsonString() const + { + FString OutputString; + FJsonObjectConverter::UStructToJsonObjectString(*this, OutputString); + return OutputString; + } + + static bool FromJsonString(const FString& JsonString, FRefreshTokenResponse& OutStruct) + { + return FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &OutStruct, 0, 0); + } +}; + + diff --git a/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h b/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h new file mode 100644 index 0000000..2dd5d0c --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h @@ -0,0 +1,53 @@ +#pragma once + +#include "CoreMinimal.h" +#include "JsonObjectConverter.h" +#include "Api/Common/WebApi.h" +#include "Api/Common/WebApiWithAuth.h" +#include "Models/CharacterCreateRequest.h" +#include "Models/CharacterCreateResponse.h" +#include "Models/CharacterFindByIdRequest.h" +#include "Models/CharacterFindByIdResponse.h" +#include "Models/CharacterPreviewRequest.h" +#include "Models/CharacterUpdateRequest.h" +#include "Models/CharacterUpdateResponse.h" + +DECLARE_DELEGATE_TwoParams(FOnCharacterCreateResponse, FCharacterCreateResponse, bool); +DECLARE_DELEGATE_TwoParams(FOnCharacterUpdatResponse, FCharacterUpdateResponse, bool); +DECLARE_DELEGATE_TwoParams(FOnCharacterFindResponse, FCharacterFindByIdResponse, bool); + +class RPMNEXTGEN_API FCharacterApi : public TSharedFromThis, public FWebApiWithAuth +{ +public: + FCharacterApi(); + virtual ~FCharacterApi() override; + FOnWebApiResponse OnApiResponse; + + void CreateAsync(const FCharacterCreateRequest& Request); + void UpdateAsync(const FCharacterUpdateRequest& Request); + void FindByIdAsync(const FCharacterFindByIdRequest& Request); + FString GeneratePreviewUrl(const FCharacterPreviewRequest& Request); + + FOnCharacterCreateResponse OnCharacterCreateResponse; + FOnCharacterUpdatResponse OnCharacterUpdateResponse; + FOnCharacterFindResponse OnCharacterFindResponse; + +protected: + virtual void OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) override; + + template + FString ConvertToJsonString(const T& Data); + + FHttpModule* Http; + +private: + FString BaseUrl; +}; + +template +FString FCharacterApi::ConvertToJsonString(const T& Data) +{ + FString JsonString; + FJsonObjectConverter::UStructToJsonObjectString(Data, JsonString); + return JsonString; +} diff --git a/Source/RpmNextGen/Public/Api/Characters/Models/CharacterCreateRequest.h b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterCreateRequest.h new file mode 100644 index 0000000..fefa7f9 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterCreateRequest.h @@ -0,0 +1,25 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Common/Models/ApiResponse.h" +#include "CharacterCreateRequest.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FCharacterCreateRequestBody +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "applicationId")) + FString ApplicationId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "assets")) + TMap Assets; +}; + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FCharacterCreateRequest +{ + GENERATED_BODY() + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FCharacterCreateRequestBody Data; +}; \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Api/Characters/Models/CharacterCreateResponse.h b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterCreateResponse.h new file mode 100644 index 0000000..321ca68 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterCreateResponse.h @@ -0,0 +1,15 @@ +#pragma once + +#include "CoreMinimal.h" +#include "RpmCharacter.h" +#include "Api/Common/Models/ApiResponse.h" +#include "CharacterCreateResponse.generated.h" + +USTRUCT() +struct RPMNEXTGEN_API FCharacterCreateResponse : public FApiResponse +{ + GENERATED_BODY() + + UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "data")) + FRpmCharacter Data; +}; diff --git a/Source/RpmNextGen/Public/Api/Characters/Models/CharacterFindByIdRequest.h b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterFindByIdRequest.h new file mode 100644 index 0000000..24ee0be --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterFindByIdRequest.h @@ -0,0 +1,13 @@ +#pragma once + +#include "CoreMinimal.h" +#include "CharacterFindByIdRequest.generated.h" + +USTRUCT(BlueprintType) +struct FCharacterFindByIdRequest +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString Id; +}; diff --git a/Source/RpmNextGen/Public/Api/Characters/Models/CharacterFindByIdResponse.h b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterFindByIdResponse.h new file mode 100644 index 0000000..aa9ed26 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterFindByIdResponse.h @@ -0,0 +1,15 @@ +#pragma once + +#include "CoreMinimal.h" +#include "RpmCharacter.h" +#include "Api/Common/Models/ApiResponse.h" +#include "CharacterFindByIdResponse.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FCharacterFindByIdResponse : public FApiResponse +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "data")) + FRpmCharacter Data; +}; diff --git a/Source/RpmNextGen/Public/Api/Characters/Models/CharacterPreviewRequest.h b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterPreviewRequest.h new file mode 100644 index 0000000..71fda5a --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterPreviewRequest.h @@ -0,0 +1,27 @@ +#pragma once + +#include "CoreMinimal.h" +#include "CharacterPreviewRequest.generated.h" + + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FCharacterPreviewQueryParams +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "assets")) + TMap Assets; +}; + + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FCharacterPreviewRequest +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString Id; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FCharacterPreviewQueryParams Params; +}; diff --git a/Source/RpmNextGen/Public/Api/Characters/Models/CharacterUpdateRequest.h b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterUpdateRequest.h new file mode 100644 index 0000000..c73cfea --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterUpdateRequest.h @@ -0,0 +1,26 @@ +#pragma once + +#include "CoreMinimal.h" +#include "CharacterUpdateRequest.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FCharacterUpdateRequestBody +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "assets")) + TMap Assets; +}; + + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FCharacterUpdateRequest +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "applicationId")) + FString Id; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FCharacterUpdateRequestBody Payload; +}; diff --git a/Source/RpmNextGen/Public/Api/Characters/Models/CharacterUpdateResponse.h b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterUpdateResponse.h new file mode 100644 index 0000000..5b01792 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterUpdateResponse.h @@ -0,0 +1,15 @@ +#pragma once + +#include "CoreMinimal.h" +#include "RpmCharacter.h" +#include "Api/Common/Models/ApiResponse.h" +#include "CharacterUpdateResponse.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FCharacterUpdateResponse : public FApiResponse +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "data")) + FRpmCharacter Data; +}; diff --git a/Source/RpmNextGen/Public/Api/Characters/Models/RpmCharacter.h b/Source/RpmNextGen/Public/Api/Characters/Models/RpmCharacter.h new file mode 100644 index 0000000..eec8bfd --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Characters/Models/RpmCharacter.h @@ -0,0 +1,32 @@ +#pragma once + +#include "CoreMinimal.h" +#include "RpmCharacter.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FRpmCharacter +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "id")) + FString Id; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "createdByApplicationId")) + FString CreatedByApplicationId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "glbUrl")) + FString GlbUrl; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "iconUrl")) + FString IconUrl; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "assets")) + TMap Assets; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "createdAt")) + FDateTime CreatedAt; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "updatedAt")) + FDateTime UpdatedAt; +}; + \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Api/Common/Models/ApiResponse.h b/Source/RpmNextGen/Public/Api/Common/Models/ApiResponse.h new file mode 100644 index 0000000..8411e98 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Common/Models/ApiResponse.h @@ -0,0 +1,19 @@ +#pragma once + +#include "CoreMinimal.h" +#include "ApiResponse.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FApiResponse +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + bool IsSuccess = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + int64 Status = 200; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString Error; +}; \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Api/Common/WebApi.h b/Source/RpmNextGen/Public/Api/Common/WebApi.h new file mode 100644 index 0000000..f611615 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Common/WebApi.h @@ -0,0 +1,41 @@ +#pragma once + +#include "CoreMinimal.h" +#include "JsonObjectConverter.h" +#include "Api/Auth/ApiRequest.h" +#include "Interfaces/IHttpRequest.h" +#include "Misc/ScopeExit.h" + +class FHttpModule; +DECLARE_DELEGATE_TwoParams(FOnWebApiResponse, FString, bool); + +class RPMNEXTGEN_API FWebApi +{ +public: + FWebApi(); + virtual ~FWebApi(); + + FOnWebApiResponse OnApiResponse; + +protected: + void DispatchRaw( + const FApiRequest& Data + ); + + virtual void OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); + + FString BuildQueryString(const TMap& QueryParams); + + template + FString ConvertToJsonString(const T& Data); + + FHttpModule* Http; +}; + +template +FString FWebApi::ConvertToJsonString(const T& Data) +{ + FString JsonString; + FJsonObjectConverter::UStructToJsonObjectString(Data, JsonString); + return JsonString; +} \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Api/Common/WebApiWithAuth.h b/Source/RpmNextGen/Public/Api/Common/WebApiWithAuth.h new file mode 100644 index 0000000..dd52dca --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Common/WebApiWithAuth.h @@ -0,0 +1,27 @@ +#pragma once + +#include "CoreMinimal.h" +#include "WebApi.h" +#include "Api/Auth/IAuthenticationStrategy.h" + + +class RPMNEXTGEN_API FWebApiWithAuth : public FWebApi +{ +public: + FWebApiWithAuth(); + FWebApiWithAuth(IAuthenticationStrategy* InAuthenticationStrategy); + + void SetAuthenticationStrategy(IAuthenticationStrategy* InAuthenticationStrategy); + + void OnAuthComplete(bool bWasSuccessful); + void OnAuthTokenRefreshed(const FRefreshTokenResponseBody& Response, bool bWasSuccessful); + + void DispatchRawWithAuth(FApiRequest& Data); +protected: + TSharedPtr ApiRequestData; + + virtual void OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) override; + +private: + IAuthenticationStrategy* AuthenticationStrategy; +}; diff --git a/Source/RpmNextGen/Public/RpmActor.h b/Source/RpmNextGen/Public/RpmActor.h new file mode 100644 index 0000000..5bd9bb0 --- /dev/null +++ b/Source/RpmNextGen/Public/RpmActor.h @@ -0,0 +1,83 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "glTFRuntimeAsset.h" +#include "RpmActor.generated.h" + +UCLASS() +class RPMNEXTGEN_API ARpmActor : public AActor +{ + GENERATED_BODY() + +public: + // Sets default values for this actor's properties + ARpmActor(); + +protected: + // Called when the game starts or when spawned + virtual void BeginPlay() override; + + virtual void ProcessNode(USceneComponent* NodeParentComponent, const FName SocketName, FglTFRuntimeNode& Node); + + template + FName GetSafeNodeName(const FglTFRuntimeNode& Node) + { + return MakeUniqueObjectName(this, T::StaticClass(), *Node.Name); + } + + UPROPERTY() + TMap SocketMapping; + UPROPERTY() + TArray DiscoveredSkeletalMeshComponents; + +public: + // Called every frame + virtual void Tick(float DeltaTime) override; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (ExposeOnSpawn = true), Category = "Ready Player Me|glTFRuntime") + UglTFRuntimeAsset* Asset; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (ExposeOnSpawn = true), Category = "Ready Player Me|glTFRuntime") + FglTFRuntimeStaticMeshConfig StaticMeshConfig; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (ExposeOnSpawn = true), Category = "Ready Player Me|glTFRuntime") + FglTFRuntimeSkeletalMeshConfig SkeletalMeshConfig; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (ExposeOnSpawn = true), Category = "Ready Player Me|glTFRuntime") + FglTFRuntimeSkeletalAnimationConfig SkeletalAnimationConfig; + + UFUNCTION(BlueprintNativeEvent, Category = "Ready Player Me|glTFRuntime", meta = (DisplayName = "On StaticMeshComponent Created")) + void ReceiveOnStaticMeshComponentCreated(UStaticMeshComponent* StaticMeshComponent, const FglTFRuntimeNode& Node); + + UFUNCTION(BlueprintNativeEvent, Category = "Ready Player Me|glTFRuntime", meta = (DisplayName = "On SkeletalMeshComponent Created")) + void ReceiveOnSkeletalMeshComponentCreated(USkeletalMeshComponent* SkeletalMeshComponent, const FglTFRuntimeNode& Node); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (ExposeOnSpawn = true), Category = "Ready Player Me|glTFRuntime") + int32 RootNodeIndex; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (ExposeOnSpawn = true), Category = "Ready Player Me|glTFRuntime") + bool bStaticMeshesAsSkeletalOnMorphTargets; + + DECLARE_MULTICAST_DELEGATE_TwoParams(FglTFRuntimeAssetActorNodeProcessed, const FglTFRuntimeNode&, USceneComponent*); + FglTFRuntimeAssetActorNodeProcessed OnNodeProcessed; + + virtual void PostUnregisterAllComponents() override; + + UFUNCTION(BlueprintCallable, Category = "Ready Player Me") + virtual void LoadGltfAsset(UglTFRuntimeAsset* GltfAsset); + void ClearLoadedComponents(); + + virtual void SetupAsset(); + +private: + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"), Category="Ready Player Me|glTFRuntime") + USceneComponent* AssetRoot; + + void ProcessBoneNode(USceneComponent* NodeParentComponent, FglTFRuntimeNode& Node); + USceneComponent* CreateNewComponent(USceneComponent* NodeParentComponent, FglTFRuntimeNode& Node); + void SetupComponentTags(USceneComponent* Component, FglTFRuntimeNode& Node, const FName SocketName); + void ProcessChildNodes(USceneComponent* NodeParentComponent, FglTFRuntimeNode& Node); +}; diff --git a/Source/RpmNextGen/Public/RpmAssetLoaderComponent.h b/Source/RpmNextGen/Public/RpmAssetLoaderComponent.h new file mode 100644 index 0000000..2c4b1f5 --- /dev/null +++ b/Source/RpmNextGen/Public/RpmAssetLoaderComponent.h @@ -0,0 +1,39 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "RpmAssetLoaderComponent.generated.h" + +class UglTFRuntimeAsset; +class FAssetLoader; +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGltfAssetLoaded, UglTFRuntimeAsset*, Asset); + +UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) +class RPMNEXTGEN_API URpmAssetLoaderComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + // Sets default values for this component's properties + URpmAssetLoaderComponent(); + + virtual void LoadCharacterFromUrl(FString Url); + + + + UPROPERTY(BlueprintAssignable, Category = "Ready Player Me" ) + FOnGltfAssetLoaded OnGltfAssetLoaded; + +protected: + // Called when the game starts + virtual void BeginPlay() override; + + virtual void HandleGLtfAssetLoaded(UglTFRuntimeAsset* gltfAsset, bool bWasSuccessful); +public: + // Called every frame + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; +private: + TSharedPtr AssetLoader; +}; diff --git a/Source/RpmNextGen/Public/RpmFunctionLibrary.h b/Source/RpmNextGen/Public/RpmFunctionLibrary.h new file mode 100644 index 0000000..e0ffbdc --- /dev/null +++ b/Source/RpmNextGen/Public/RpmFunctionLibrary.h @@ -0,0 +1,22 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "RpmFunctionLibrary.generated.h" + +DECLARE_DYNAMIC_DELEGATE_OneParam(FOnAssetIdFetched, FString, AssetId); + +/** + * + */ +UCLASS() +class RPMNEXTGEN_API URpmFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = "ReadyPlayerMe", meta = (WorldContext = "WorldContextObject")) + static void FetchFirstAssetId(UObject* WorldContextObject, const FString& AssetType, FOnAssetIdFetched OnAssetIdFetched); +}; diff --git a/Source/RpmNextGen/Public/RpmImageLoader.h b/Source/RpmNextGen/Public/RpmImageLoader.h new file mode 100644 index 0000000..43d4619 --- /dev/null +++ b/Source/RpmNextGen/Public/RpmImageLoader.h @@ -0,0 +1,21 @@ +#pragma once + +#pragma once + +#include "CoreMinimal.h" +#include "Interfaces/IHttpRequest.h" +#include "Engine/Texture2D.h" +#include "Components/Image.h" + +class RPMNEXTGEN_API FRpmImageLoader +{ +public: + FRpmImageLoader() = default; + void LoadUImageFromURL(UImage* Image, const FString& URL); + void LoadSImageFromURL(TSharedPtr ImageWidget, const FString& URL, TFunction OnImageUpdated); + +private: + void DownloadImage(const FString& URL, TFunction OnImageDownloaded); + void OnImageDownloadComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, TFunction OnImageDownloaded); + UTexture2D* CreateTextureFromImageData(const TArray& ImageData); +}; diff --git a/Source/RpmNextGen/Public/RpmNextGen.h b/Source/RpmNextGen/Public/RpmNextGen.h new file mode 100644 index 0000000..48fa711 --- /dev/null +++ b/Source/RpmNextGen/Public/RpmNextGen.h @@ -0,0 +1,17 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +RPMNEXTGEN_API DECLARE_LOG_CATEGORY_EXTERN(LogReadyPlayerMe, Log, All); + +class FRpmNextGenModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Source/RpmNextGen/Public/RpmPreviewLoaderComponent.h b/Source/RpmNextGen/Public/RpmPreviewLoaderComponent.h new file mode 100644 index 0000000..e52e9ec --- /dev/null +++ b/Source/RpmNextGen/Public/RpmPreviewLoaderComponent.h @@ -0,0 +1,49 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "RpmAssetLoaderComponent.h" +#include "Api/Characters/Models/RpmCharacter.h" +#include "RpmPreviewLoaderComponent.generated.h" + +class FCharacterApi; +struct FCharacterCreateResponse; +struct FCharacterUpdateResponse; +struct FCharacterFindByIdResponse; +struct FAsset; + +/** + * + */ +UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) +class RPMNEXTGEN_API URpmPreviewLoaderComponent : public URpmAssetLoaderComponent +{ + GENERATED_BODY() + +public: + URpmPreviewLoaderComponent(); + + UFUNCTION(BlueprintCallable, Category = "Ready Player Me") + virtual void CreateCharacter(const FString& BaseModelId); + + UFUNCTION(BlueprintCallable, Category = "Ready Player Me") + virtual void LoadCharacter(FRpmCharacter CharacterData); + + UFUNCTION(BlueprintCallable, Category = "Ready Player Me") + virtual void LoadAssetPreview(FAsset AssetData); + + UFUNCTION() + virtual void HandleCharacterCreateResponse(FCharacterCreateResponse CharacterCreateResponse, bool bWasSuccessful); + UFUNCTION() + virtual void HandleCharacterUpdateResponse(FCharacterUpdateResponse CharacterUpdateResponse, bool bWasSuccessful); + UFUNCTION() + virtual void HandleCharacterFindResponse(FCharacterFindByIdResponse CharacterFindByIdResponse, bool bWasSuccessful); + +protected: + FString AppId; + FRpmCharacter Character; + TMap PreviewAssetMap; +private: + TSharedPtr CharacterApi; +}; diff --git a/Source/RpmNextGen/Public/Samples/RpmAssetButtonWidget.h b/Source/RpmNextGen/Public/Samples/RpmAssetButtonWidget.h new file mode 100644 index 0000000..715f93e --- /dev/null +++ b/Source/RpmNextGen/Public/Samples/RpmAssetButtonWidget.h @@ -0,0 +1,58 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Api/Assets/Models/Asset.h" +#include "Blueprint/UserWidget.h" +#include "RpmAssetButtonWidget.generated.h" + +class USizeBox; +class UBorder; +class UImage; +class UButton; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAssetButtonClicked, const URpmAssetButtonWidget*, AssetData); + +/** + * + */ +UCLASS() +class RPMNEXTGEN_API URpmAssetButtonWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + + UPROPERTY(meta = (BindWidget)) + UButton* AssetButton; + + UPROPERTY(meta = (BindWidget)) + UImage* AssetImage; + + UPROPERTY(BlueprintAssignable, Category = "Events") + FOnAssetButtonClicked OnAssetButtonClicked; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Asset Button" ) + FLinearColor SelectedColor = FLinearColor::Yellow; + + UFUNCTION(BlueprintCallable, Category = "Ready Player Me|Asset Button") + virtual void InitializeButton(const FAsset& InAssetData, const FVector2D& InImageSize); + + UFUNCTION(BlueprintCallable, Category = "Ready Player Me|Asset Button") + virtual void SetSelected(const bool bInIsSelected); + + FAsset GetAssetData() const { return AssetData; } +protected: + virtual void NativeConstruct() override; + +private: + FLinearColor DefaultColor; + + FAsset AssetData; + + UFUNCTION() + virtual void HandleButtonClicked(); + + bool bIsSelected; +}; diff --git a/Source/RpmNextGen/Public/Samples/RpmAssetCardWidget.h b/Source/RpmNextGen/Public/Samples/RpmAssetCardWidget.h new file mode 100644 index 0000000..5d9c3d2 --- /dev/null +++ b/Source/RpmNextGen/Public/Samples/RpmAssetCardWidget.h @@ -0,0 +1,41 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Api/Assets/Models/Asset.h" +#include "Blueprint/UserWidget.h" +#include "RpmAssetCardWidget.generated.h" + +class UImage; +class UTextBlock; + +UCLASS() +class RPMNEXTGEN_API URpmAssetCardWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + virtual void NativeConstruct() override; + + UFUNCTION(BlueprintCallable, Category = "Asset Card") + virtual void InitializeCard(const FAsset& Asset); + + UFUNCTION(BlueprintCallable, Category = "Asset Card") + void LoadImage(const FString& URL); + + UPROPERTY(meta = (BindWidget)) + UTextBlock* AssetCategoryText; + + UPROPERTY(meta = (BindWidget)) + UImage* AssetImage; + + UPROPERTY(meta = (BindWidget)) + UTextBlock* AssetNameText; + + UPROPERTY(meta = (BindWidget)) + UTextBlock* AssetIdText; + +private: + FAsset AssetData; +}; diff --git a/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h b/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h new file mode 100644 index 0000000..0b50b3a --- /dev/null +++ b/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h @@ -0,0 +1,66 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Api/Assets/Models/AssetListResponse.h" +#include "Blueprint/UserWidget.h" +#include "RpmAssetPanelWidget.generated.h" + +class FAssetApi; +struct FAsset; +class URpmAssetButtonWidget; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAssetSelected, const FAsset&, AssetData); + +/** + * + */ +UCLASS() +class RPMNEXTGEN_API URpmAssetPanelWidget : public UUserWidget +{ + GENERATED_BODY() +public: + virtual void NativeConstruct() override; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Asset Panel" ) + TSubclassOf AssetButtonBlueprint; + + UPROPERTY(meta = (BindWidget)) + UPanelWidget* ButtonContainer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Asset Panel") + URpmAssetButtonWidget* SelectedAssetButton; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Asset Button" ) + FVector2D ButtonSize; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Asset Button" ) + FVector2D ImageSize; + + UPROPERTY(BlueprintAssignable, Category = "Events" ) + FOnAssetSelected OnAssetSelected; + + UFUNCTION(BlueprintCallable, Category = "Asset Panel") + void CreateButtonsFromAssets(TArray Assets); + + UFUNCTION(BlueprintCallable, Category = "Asset Panel") + void ClearAllButtons(); + + UFUNCTION(BlueprintCallable, Category = "Asset Panel") + void UpdateSelectedButton(URpmAssetButtonWidget* AssetButton); + + UFUNCTION() + void OnAssetButtonClicked(const URpmAssetButtonWidget* AssetButtonWidget); + + UFUNCTION() + void OnAssetListResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful); + + UFUNCTION(BlueprintCallable, Category = "Asset Panel") + void LoadAssetsOfType(const FString& AssetType); + + void CreateButton(const FAsset& AssetData); +private: + TArray> AssetButtons; + TSharedPtr AssetApi; +}; diff --git a/Source/RpmNextGen/Public/Samples/RpmCategoryButtonWidget.h b/Source/RpmNextGen/Public/Samples/RpmCategoryButtonWidget.h new file mode 100644 index 0000000..3e22e0f --- /dev/null +++ b/Source/RpmNextGen/Public/Samples/RpmCategoryButtonWidget.h @@ -0,0 +1,61 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "RpmCategoryButtonWidget.generated.h" + +class UImage; +class UBorder; +class UButton; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCategoryClicked, URpmCategoryButtonWidget*, CategoryButton); + +/** + * + */ +UCLASS() +class RPMNEXTGEN_API URpmCategoryButtonWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + virtual void NativeConstruct() override; + + UPROPERTY(meta = (BindWidget)) + UImage* CategoryImage; + + UPROPERTY(meta = (BindWidget)) + UButton* CategoryButton; + + UPROPERTY(BlueprintAssignable, Category = "Events") + FOnCategoryClicked OnCategoryClicked; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Category Button" ) + FLinearColor SelectedColor = FLinearColor::Yellow; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Category Button") + UTexture2D* CategoryImageTexture; + + UFUNCTION(BlueprintCallable, Category = "Category Button") + virtual void InitializeButton(FString Category, UTexture2D* Image); + + UFUNCTION(BlueprintCallable, Category = "Category Button") + virtual void SetSelected(bool bIsSelected); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Category Button" ) + FString AssetCategoryName; + +#if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + + virtual void SynchronizeProperties() override; + +private: + UFUNCTION() + virtual void HandleButtonClicked(); + + FLinearColor DefaultColor; +}; diff --git a/Source/RpmNextGen/Public/Samples/RpmCategoryPanelWidget.h b/Source/RpmNextGen/Public/Samples/RpmCategoryPanelWidget.h new file mode 100644 index 0000000..feae846 --- /dev/null +++ b/Source/RpmNextGen/Public/Samples/RpmCategoryPanelWidget.h @@ -0,0 +1,38 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "RpmCategoryPanelWidget.generated.h" + +class URpmCategoryButtonWidget; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCategorySelected, const FString&, CategoryName); + +/** + * + */ +UCLASS() +class RPMNEXTGEN_API URpmCategoryPanelWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + virtual void NativeConstruct() override; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Category Panel") + URpmCategoryButtonWidget* SelectedCategoryButton; + + UPROPERTY(BlueprintAssignable, Category = "Events") + FOnCategorySelected OnCategorySelected; + + UFUNCTION(BlueprintCallable, Category = "Category Panel") + virtual void UpdateSelectedButton(URpmCategoryButtonWidget* CategoryButton); + + UFUNCTION() + virtual void OnCategoryButtonClicked(URpmCategoryButtonWidget* CategoryButton); + +private: + void InitializeCategoryButtons(); +}; diff --git a/Source/RpmNextGen/Public/Settings/RpmDeveloperSettings.h b/Source/RpmNextGen/Public/Settings/RpmDeveloperSettings.h new file mode 100644 index 0000000..ecaeeaf --- /dev/null +++ b/Source/RpmNextGen/Public/Settings/RpmDeveloperSettings.h @@ -0,0 +1,52 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DeveloperSettings.h" +#include "RpmDeveloperSettings.generated.h" + +/** + * + */ +UCLASS(config = Game, defaultconfig, meta = (DisplayName = "Ready Player Me")) +class RPMNEXTGEN_API URpmDeveloperSettings : public UDeveloperSettings +{ + GENERATED_BODY() + + UPROPERTY(VisibleAnywhere, Config, Category = "Auth Settings", meta = (ReadOnly = "true", ToolTip = "Base URL for requests.")) + FString ApiBaseUrl; + +public: + URpmDeveloperSettings(); + + UPROPERTY(VisibleAnywhere, Config, Category = "Auth Settings", meta = (ReadOnly = "true", ToolTip = "Base URL for authentication requests.")) + FString ApiBaseAuthUrl; + + UPROPERTY(EditAnywhere, Config, Category = "Auth Settings", meta = (ToolTip = "Application ID used for authentication.")) + FString ApplicationId; + + UPROPERTY(EditAnywhere, Config, Category = "Auth Settings", meta = (ToolTip = "API key used for authentication.")) + FString ApiKey; + + UPROPERTY(EditAnywhere, Config, Category = "Auth Settings", meta = (ToolTip = "Proxy URL for API requests. If empty, the base URL will be used.")) + FString ApiProxyUrl; + + void SetupDemoAccount(); + void Reset(); + FString GetApiBaseUrl() const; + + bool IsValid() const + { + return !ApplicationId.IsEmpty() && (!ApiKey.IsEmpty() || !ApiProxyUrl.IsEmpty()); + } + + virtual void PostInitProperties() override; + virtual void PreSave(const ITargetPlatform* TargetPlatform) override; +private: + const FString DemoAppId = TEXT("665e05a50c62c921e5a6ab84"); + const FString DemoProxyUrl = TEXT("https://api.readyplayer.me/demo"); +#if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif // WITH_EDITOR +}; diff --git a/Source/RpmNextGen/RpmNextGen.Build.cs b/Source/RpmNextGen/RpmNextGen.Build.cs new file mode 100644 index 0000000..4ad0e8a --- /dev/null +++ b/Source/RpmNextGen/RpmNextGen.Build.cs @@ -0,0 +1,58 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class RpmNextGen : ModuleRules +{ + public RpmNextGen(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "glTFRuntime", + "DeveloperSettings", + "Slate", + "SlateCore", + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Json", + "JsonUtilities", + "HTTP", + "UMG", + "ImageWrapper", + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} diff --git a/Source/RpmNextGenEditor/Private/AssetNameGenerator.cpp b/Source/RpmNextGenEditor/Private/AssetNameGenerator.cpp new file mode 100644 index 0000000..bbb8e00 --- /dev/null +++ b/Source/RpmNextGenEditor/Private/AssetNameGenerator.cpp @@ -0,0 +1,24 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "AssetNameGenerator.h" + +UAssetNameGenerator::UAssetNameGenerator() +{ + MaterialNameGeneratorDelegate.BindUFunction(this, "GenerateMaterialName"); + TextureNameGeneratorDelegate.BindUFunction(this, "GenerateTextureName"); +} + +void UAssetNameGenerator::SetPath(FString NewPath) +{ + this->path = NewPath; +} + +FString UAssetNameGenerator::GenerateMaterialName(UMaterialInterface* Material, const int32 MaterialIndex, const FString& SlotName) const +{ + return FString::Printf(TEXT("%sMaterial_%d"), *path, MaterialIndex); +} + +FString UAssetNameGenerator::GenerateTextureName(UTexture* Texture, UMaterialInterface* Material, const FString& MaterialPath, const FString& ParamName) const +{ + return FString::Printf(TEXT("%s%s%s"), *path, *Material->GetName(), *ParamName); +} diff --git a/Source/RpmNextGenEditor/Private/Auth/DevAuthTokenCache.cpp b/Source/RpmNextGenEditor/Private/Auth/DevAuthTokenCache.cpp new file mode 100644 index 0000000..d30fba9 --- /dev/null +++ b/Source/RpmNextGenEditor/Private/Auth/DevAuthTokenCache.cpp @@ -0,0 +1,50 @@ +#include "Auth/DevAuthTokenCache.h" +#include "EditorCache.h" +#include "Auth/Models/DeveloperAuth.h" +#include "Misc/ConfigCacheIni.h" + +FDeveloperAuth FDevAuthTokenCache::AuthData = FDeveloperAuth(); +bool FDevAuthTokenCache::bIsInitialized = false; + +void FDevAuthTokenCache::Initialize() +{ + if (!bIsInitialized) + { + AuthData.Name = FEditorCache::GetString(CacheKeyName, FString()); + AuthData.Token = FEditorCache::GetString(CacheKeyToken, FString()); + AuthData.RefreshToken = FEditorCache::GetString(CacheKeyRefreshToken, FString()); + AuthData.IsDemo = FEditorCache::GetBool(CacheKeyIsDemo, false); + + if (!AuthData.IsValid()) + { + UE_LOG(LogTemp, Warning, TEXT("DevAuthTokenCache: Invalid AuthData %s"), *AuthData.ToJsonString()); + ClearAuthData(); + } + + bIsInitialized = true; + } +} + +FDeveloperAuth FDevAuthTokenCache::GetAuthData() +{ + Initialize(); + return AuthData; +} + +void FDevAuthTokenCache::SetAuthData(const FDeveloperAuth& DevAuthData) +{ + AuthData = DevAuthData; + FEditorCache::SetString( CacheKeyName, AuthData.Name); + FEditorCache::SetString( CacheKeyToken, AuthData.Token); + FEditorCache::SetString( CacheKeyRefreshToken, AuthData.RefreshToken); + FEditorCache::SetBool( CacheKeyIsDemo, AuthData.IsDemo); +} + +void FDevAuthTokenCache::ClearAuthData() +{ + AuthData = FDeveloperAuth(); + FEditorCache::RemoveKey(CacheKeyName); + FEditorCache::RemoveKey(CacheKeyToken); + FEditorCache::RemoveKey(CacheKeyRefreshToken); + FEditorCache::RemoveKey(CacheKeyIsDemo); +} diff --git a/Source/RpmNextGenEditor/Private/Auth/DeveloperAuthApi.cpp b/Source/RpmNextGenEditor/Private/Auth/DeveloperAuthApi.cpp new file mode 100644 index 0000000..37d4bc6 --- /dev/null +++ b/Source/RpmNextGenEditor/Private/Auth/DeveloperAuthApi.cpp @@ -0,0 +1,32 @@ +#include "Auth/DeveloperAuthApi.h" +#include "Auth/Models/DeveloperLoginRequest.h" +#include "Auth/Models/DeveloperLoginResponse.h" +#include "Settings/RpmDeveloperSettings.h" + +FDeveloperAuthApi::FDeveloperAuthApi() +{ + const URpmDeveloperSettings* RpmSettings = GetDefault(); + ApiUrl = FString::Printf(TEXT("%s/login"), *RpmSettings->ApiBaseAuthUrl); + OnApiResponse.BindRaw(this, &FDeveloperAuthApi::HandleLoginResponse); +} + +void FDeveloperAuthApi::HandleLoginResponse(FString JsonData, bool bIsSuccessful) const +{ + FDeveloperLoginResponse Response; + if (bIsSuccessful && !JsonData.IsEmpty() && FJsonObjectConverter::JsonObjectStringToUStruct(JsonData, &Response, 0, 0)) + { + OnLoginResponse.ExecuteIfBound(Response, true); + return; + } + OnLoginResponse.ExecuteIfBound(Response, bIsSuccessful); +} + +void FDeveloperAuthApi::LoginWithEmail(FDeveloperLoginRequest Request) +{ + FApiRequest ApiRequest = FApiRequest(); + ApiRequest.Url = ApiUrl; + ApiRequest.Method = POST; + ApiRequest.Headers.Add(TEXT("Content-Type"), TEXT("application/json")); + ApiRequest.Payload = Request.ToJsonString(); + DispatchRaw(ApiRequest); +} diff --git a/Source/RpmNextGenEditor/Private/Auth/DeveloperTokenAuthStrategy.cpp b/Source/RpmNextGenEditor/Private/Auth/DeveloperTokenAuthStrategy.cpp new file mode 100644 index 0000000..365ea42 --- /dev/null +++ b/Source/RpmNextGenEditor/Private/Auth/DeveloperTokenAuthStrategy.cpp @@ -0,0 +1,61 @@ +#include "Auth/DeveloperTokenAuthStrategy.h" +#include "Auth/DevAuthTokenCache.h" +#include "Api/Auth/ApiRequest.h" +#include "Api/Auth/Models/RefreshTokenRequest.h" +#include "Api/Auth/Models/RefreshTokenResponse.h" +#include "Auth/Models/DeveloperAuth.h" + +DeveloperTokenAuthStrategy::DeveloperTokenAuthStrategy() +{ + AuthApi = FAuthApi(); + AuthApi.OnRefreshTokenResponse.BindRaw(this, &DeveloperTokenAuthStrategy::OnRefreshTokenResponse); +} + +void DeveloperTokenAuthStrategy::AddAuthToRequest(TSharedPtr Request) +{ + const FString Key = TEXT("Authorization"); + const FString Token = FDevAuthTokenCache::GetAuthData().Token; + if(Token.IsEmpty()) + { + UE_LOG(LogTemp, Error, TEXT("Token is empty")); + OnAuthComplete.ExecuteIfBound(false); + return; + } + if (Request->Headers.Contains(Key)) + { + Request->Headers.Remove(Key); + } + Request->Headers.Add(Key, FString::Printf(TEXT("Bearer %s"), *Token)); + + OnAuthComplete.ExecuteIfBound(true); +} + +void DeveloperTokenAuthStrategy::TryRefresh(TSharedPtr Request) +{ + FRefreshTokenRequest RefreshRequest; + RefreshRequest.Data.Token = FDevAuthTokenCache::GetAuthData().Token; + RefreshRequest.Data.RefreshToken = FDevAuthTokenCache::GetAuthData().RefreshToken; + + RefreshTokenAsync(RefreshRequest); +} + +void DeveloperTokenAuthStrategy::OnRefreshTokenResponse(const FRefreshTokenResponse& Response, bool bWasSuccessful) +{ + if (bWasSuccessful && !Response.Data.Token.IsEmpty()) + { + FDeveloperAuth DeveloperAuth = FDevAuthTokenCache::GetAuthData(); + DeveloperAuth.Token = Response.Data.Token; + DeveloperAuth.RefreshToken = Response.Data.RefreshToken; + FDevAuthTokenCache::SetAuthData(DeveloperAuth); + OnTokenRefreshed.ExecuteIfBound(Response.Data, true); + return; + } + UE_LOG(LogTemp, Error, TEXT("Failed to refresh token")); + OnTokenRefreshed.ExecuteIfBound(Response.Data, false); +} + + +void DeveloperTokenAuthStrategy::RefreshTokenAsync(const FRefreshTokenRequest& Request) +{ + AuthApi.RefreshToken(Request); +} diff --git a/Source/RpmNextGenEditor/Private/DeveloperAccounts/DeveloperAccountApi.cpp b/Source/RpmNextGenEditor/Private/DeveloperAccounts/DeveloperAccountApi.cpp new file mode 100644 index 0000000..570dbf3 --- /dev/null +++ b/Source/RpmNextGenEditor/Private/DeveloperAccounts/DeveloperAccountApi.cpp @@ -0,0 +1,77 @@ +#include "DeveloperAccounts/DeveloperAccountApi.h" +#include "JsonObjectConverter.h" +#include "DeveloperAccounts/Models/ApplicationListRequest.h" +#include "DeveloperAccounts/Models/ApplicationListResponse.h" +#include "DeveloperAccounts/Models/OrganizationListRequest.h" +#include "DeveloperAccounts/Models/OrganizationListResponse.h" +#include "Settings/RpmDeveloperSettings.h" + +FDeveloperAccountApi::FDeveloperAccountApi(IAuthenticationStrategy* InAuthenticationStrategy) : FWebApiWithAuth(InAuthenticationStrategy) +{ + if (URpmDeveloperSettings* Settings = GetMutableDefault()) + { + ApiBaseUrl = Settings->GetApiBaseUrl(); + } +} + +void FDeveloperAccountApi::ListApplicationsAsync(const FApplicationListRequest& Request) +{ + // TODO find better way to get settings (or move to editor only code) + const URpmDeveloperSettings* RpmSettings = GetDefault(); + ApiBaseUrl = RpmSettings->GetApiBaseUrl(); + const FString QueryString = BuildQueryString(Request.Params); + const FString Url = FString::Printf(TEXT("%s/v1/applications%s"), *ApiBaseUrl, *QueryString); + FApiRequest ApiRequest; + ApiRequest.Url = Url; + OnApiResponse.BindRaw(this, &FDeveloperAccountApi::HandleAppListResponse); + DispatchRawWithAuth(ApiRequest); +} + +void FDeveloperAccountApi::ListOrganizationsAsync(const FOrganizationListRequest& Request) +{ + // TODO find better way to get settings (or move to editor only code) + const URpmDeveloperSettings* RpmSettings = GetDefault(); + ApiBaseUrl = RpmSettings->GetApiBaseUrl(); + const FString QueryString = BuildQueryString(Request.Params); + const FString Url = FString::Printf(TEXT("%s/v1/organizations%s"), *ApiBaseUrl, *QueryString); + FApiRequest ApiRequest; + ApiRequest.Url = Url; + OnApiResponse.BindRaw(this, &FDeveloperAccountApi::HandleOrgListResponse); + DispatchRawWithAuth(ApiRequest); +} + + +void FDeveloperAccountApi::HandleAppListResponse(FString Data, bool bWasSuccessful) +{ + FApplicationListResponse ApplicationListResponse; + if (bWasSuccessful && !Data.IsEmpty() && FJsonObjectConverter::JsonObjectStringToUStruct(Data, &ApplicationListResponse, 0, 0)) + { + OnApplicationListResponse.ExecuteIfBound(ApplicationListResponse, true); + return; + } + OnApplicationListResponse.ExecuteIfBound(ApplicationListResponse, false); +} + +void FDeveloperAccountApi::HandleOrgListResponse(FString Data, bool bWasSuccessful) +{ + FOrganizationListResponse OrganizationListResponse; + if (bWasSuccessful && !Data.IsEmpty() && FJsonObjectConverter::JsonObjectStringToUStruct(Data, &OrganizationListResponse, 0, 0)) + { + OnOrganizationResponse.ExecuteIfBound(OrganizationListResponse, true); + return; + } + OnOrganizationResponse.ExecuteIfBound(OrganizationListResponse, false); +} + + +FString FDeveloperAccountApi::BuildQueryString(const TMap& Params) +{ + if (Params.Num() == 0) return FString(); + FString QueryString = TEXT("?"); + for (const auto& Param : Params) + { + QueryString += Param.Key + TEXT("=") + Param.Value + TEXT("&"); + } + QueryString.RemoveFromEnd(TEXT("&")); + return QueryString; +} diff --git a/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp b/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp new file mode 100644 index 0000000..2bc520c --- /dev/null +++ b/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp @@ -0,0 +1,124 @@ +#include "EditorAssetLoader.h" +#include "TransientObjectSaverLibrary.h" +#include "AssetNameGenerator.h" +#include "RpmActor.h" +#include "RpmNextGen.h" + +FEditorAssetLoader::FEditorAssetLoader() +{ + SkeletonToCopy = nullptr; +} + +FEditorAssetLoader::~FEditorAssetLoader() +{ +} + +void FEditorAssetLoader::OnAssetLoadComplete(UglTFRuntimeAsset* gltfAsset, bool bWasSuccessful, + FString LoadedAssetId) +{ + if (bWasSuccessful) + { + gltfAsset->AddToRoot(); + SaveAsUAsset(gltfAsset, LoadedAssetId); + LoadAssetToWorldAsURpmActor(gltfAsset, LoadedAssetId); + gltfAsset->RemoveFromRoot(); + } +} + +USkeletalMesh* FEditorAssetLoader::SaveAsUAsset(UglTFRuntimeAsset* GltfAsset, const FString& LoadedAssetId) const +{ + const FglTFRuntimeSkeletonConfig SkeletonConfig = FglTFRuntimeSkeletonConfig(); + USkeleton* Skeleton = GltfAsset->LoadSkeleton(0, SkeletonConfig); + + FglTFRuntimeSkeletalMeshConfig meshConfig = FglTFRuntimeSkeletalMeshConfig(); + meshConfig.Skeleton = Skeleton; + + USkeletalMesh* skeletalMesh = GltfAsset->LoadSkeletalMeshRecursive(TEXT(""), {}, meshConfig); + skeletalMesh->SetSkeleton(Skeleton); + Skeleton->SetPreviewMesh(skeletalMesh); + + const FString CoreAssetPath = FString::Printf(TEXT("/Game/ReadyPlayerMe/%s/"), *LoadedAssetId); + const FString SkeletonAssetPath = FString::Printf(TEXT("%s%s_Skeleton"), *CoreAssetPath, *LoadedAssetId); + const FString SkeletalMeshAssetPath = FString::Printf(TEXT("%s%s_SkeletalMesh"), *CoreAssetPath, *LoadedAssetId); + + const auto NameGenerator = NewObject(); + NameGenerator->SetPath(CoreAssetPath); + + UTransientObjectSaverLibrary::SaveTransientSkeletalMesh(skeletalMesh, SkeletalMeshAssetPath, SkeletonAssetPath, TEXT(""), NameGenerator->MaterialNameGeneratorDelegate, NameGenerator->TextureNameGeneratorDelegate); + + UE_LOG(LogReadyPlayerMe, Log, TEXT("Character model saved: %s"), *LoadedAssetId); + return skeletalMesh; +} + +void FEditorAssetLoader::LoadGLBFromURLWithId(const FString& URL, FString LoadedAssetId) +{ + OnGLtfAssetLoaded.BindLambda( + [LoadedAssetId, this]( UglTFRuntimeAsset* gltfAsset, + bool bWasSuccessful) + { + if (!gltfAsset) + { + UE_LOG(LogReadyPlayerMe, Log, TEXT("No gltf asset")); + return; + } + OnAssetLoadComplete(gltfAsset, bWasSuccessful, LoadedAssetId); + }); + LoadGLBFromURL(URL); +} + +void FEditorAssetLoader::LoadAssetToWorldAsURpmActor(UglTFRuntimeAsset* gltfAsset, FString AssetId) +{ + this->LoadAssetToWorld(AssetId, gltfAsset); +} + + +void FEditorAssetLoader::LoadAssetToWorld(FString AssetId, UglTFRuntimeAsset* gltfAsset) +{ + if (!GEditor) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("GEditor is not available.")); + return; + } + + UWorld* EditorWorld = GEditor->GetEditorWorldContext().World(); + if (!EditorWorld) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("No valid editor world found.")); + return; + } + + if (gltfAsset) + { + FTransform Transform = FTransform::Identity; + + + ARpmActor* NewActor = EditorWorld->SpawnActorDeferred(ARpmActor::StaticClass(), Transform); + + if (NewActor) + { + NewActor->SetFlags(RF_Transient); + NewActor->Rename(*AssetId); + if (SkeletonToCopy) + { + NewActor->SkeletalMeshConfig.SkeletonConfig.CopyRotationsFrom = SkeletonToCopy; + } + NewActor->FinishSpawning(Transform); + NewActor->DispatchBeginPlay(); + GEditor->SelectNone(true, true, true); + GEditor->SelectActor(NewActor, true, true, false, true); + + // Register the actor in the editor world and update the editor + GEditor->SelectActor(NewActor, true, true); + GEditor->EditorUpdateComponents(); + if (gltfAsset) + { + NewActor->LoadGltfAsset(gltfAsset); + } + UE_LOG(LogReadyPlayerMe, Log, TEXT("Successfully loaded GLB asset into the editor world")); + return; + } + + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to spawn ARpmActor in the editor world")); + } + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load GLB asset from file")); +} diff --git a/Source/RpmNextGenEditor/Private/EditorCache.cpp b/Source/RpmNextGenEditor/Private/EditorCache.cpp new file mode 100644 index 0000000..e7f8f93 --- /dev/null +++ b/Source/RpmNextGenEditor/Private/EditorCache.cpp @@ -0,0 +1,74 @@ +#include "EditorCache.h" +#include "Misc/ConfigCacheIni.h" + +#define RPM_CACHE_SECTION TEXT("ReadyPlayerMeCache") + +void FEditorCache::SetString( const FString& Key, const FString& Value) +{ + GConfig->SetString(RPM_CACHE_SECTION, *Key, *Value, GEditorPerProjectIni); + GConfig->Flush(false, GEditorPerProjectIni); +} + +void FEditorCache::SetInt(const FString& Key, int32 Value) +{ + GConfig->SetInt(RPM_CACHE_SECTION, *Key, Value, GEditorPerProjectIni); + GConfig->Flush(false, GEditorPerProjectIni); +} + +void FEditorCache::SetFloat(const FString& Key, float Value) +{ + GConfig->SetFloat(RPM_CACHE_SECTION, *Key, Value, GEditorPerProjectIni); + GConfig->Flush(false, GEditorPerProjectIni); +} + +void FEditorCache::SetBool(const FString& Key, bool Value) +{ + GConfig->SetBool(RPM_CACHE_SECTION, *Key, Value, GEditorPerProjectIni); + GConfig->Flush(false, GEditorPerProjectIni); +} + +FString FEditorCache::GetString(const FString& Key, const FString& DefaultValue) +{ + FString Value; + if (GConfig->GetString(RPM_CACHE_SECTION, *Key, Value, GEditorPerProjectIni)) + { + return Value; + } + return DefaultValue; +} + +int32 FEditorCache::GetInt(const FString& Key, int32 DefaultValue) +{ + int32 Value; + if (GConfig->GetInt(RPM_CACHE_SECTION, *Key, Value, GEditorPerProjectIni)) + { + return Value; + } + return DefaultValue; +} + +float FEditorCache::GetFloat(const FString& Key, float DefaultValue) +{ + float Value; + if (GConfig->GetFloat(RPM_CACHE_SECTION, *Key, Value, GEditorPerProjectIni)) + { + return Value; + } + return DefaultValue; +} + +bool FEditorCache::GetBool(const FString& Key, bool DefaultValue) +{ + bool Value; + if (GConfig->GetBool(RPM_CACHE_SECTION, *Key, Value, GEditorPerProjectIni)) + { + return Value; + } + return DefaultValue; +} + +void FEditorCache::RemoveKey(const FString& Key) +{ + GConfig->RemoveKey(RPM_CACHE_SECTION, *Key, GEditorPerProjectIni); + GConfig->Flush(false, GEditorPerProjectIni); +} \ No newline at end of file diff --git a/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp b/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp new file mode 100644 index 0000000..d9e6dee --- /dev/null +++ b/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp @@ -0,0 +1,148 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "RpmNextGenEditor.h" + +#include "UI/CharacterLoaderWidget.h" +#include "UI/Commands/LoaderWindowCommands.h" +#include "UI/Commands/LoginWindowCommands.h" +#include "UI/SRpmDeveloperLoginWidget.h" +#include "Widgets/Docking/SDockTab.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/Text/STextBlock.h" +#include "ToolMenus.h" + +static const FName DeveloperWindowName("LoginWindow"); +static const FName LoaderWinderName("LoaderWindow"); +#define LOCTEXT_NAMESPACE "RpmNextGenEditorModule" + +void FRpmNextGenEditorModule::StartupModule() +{ + FLoginWindowStyle::Initialize(); + FLoginWindowStyle::ReloadTextures(); + + FLoginWindowCommands::Register(); + FLoaderWindowCommands::Register(); // Don't forget to register the other command set + + PluginCommands = MakeShareable(new FUICommandList); + + PluginCommands->MapAction( + FLoginWindowCommands::Get().OpenPluginWindow, + FExecuteAction::CreateRaw(this, &FRpmNextGenEditorModule::PluginButtonClicked), + FCanExecuteAction()); + + // Don't show Loader window in the menu + // PluginCommands->MapAction( + // FLoaderWindowCommands::Get().OpenPluginWindow, + // FExecuteAction::CreateRaw(this, &FRpmNextGenEditorModule::OpenLoaderWindow), + // FCanExecuteAction()); + + UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FRpmNextGenEditorModule::RegisterMenus)); + + FGlobalTabmanager::Get()->RegisterNomadTabSpawner(DeveloperWindowName, FOnSpawnTab::CreateRaw(this, &FRpmNextGenEditorModule::OnSpawnPluginTab)) + .SetDisplayName(LOCTEXT("DeveloperLoginWidget", "Ready Player Me")) + .SetMenuType(ETabSpawnerMenuType::Hidden); + + // Don't show Loader window in the menu + // FGlobalTabmanager::Get()->RegisterNomadTabSpawner(NewWindowTabName, FOnSpawnTab::CreateRaw(this, &FRpmNextGenEditorModule::OnSpawnLoaderWindow)) + // .SetDisplayName(LOCTEXT("CharacterLoaderWidget", "Avatar Loader")) + // .SetMenuType(ETabSpawnerMenuType::Hidden); +} + +void FRpmNextGenEditorModule::RegisterMenus() +{ + FToolMenuOwnerScoped OwnerScoped(this); + + // Create a new main menu entry called "ReadyPlayerMe" + UToolMenu* MainMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu"); + + // Add a new top-level menu "Ready Player Me" + FToolMenuSection& Section = MainMenu->AddSection("ReadyPlayerMeTopMenu", LOCTEXT("ReadyPlayerMeMenuSection", "Ready Player Me")); + + // Add a sub-menu for "Ready Player Me" + FToolMenuEntry& SubMenuEntry = Section.AddSubMenu( + "ReadyPlayerMe", + LOCTEXT("ReadyPlayerMeMenu", "Ready Player Me"), + LOCTEXT("ReadyPlayerMeMenu_ToolTip", "Open Ready Player Me tools"), + FNewToolMenuDelegate::CreateRaw(this, &FRpmNextGenEditorModule::FillReadyPlayerMeMenu), + false, // Don't open on hover + FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.User") // Optional icon for the top-level menu + ); +} + +void FRpmNextGenEditorModule::FillReadyPlayerMeMenu(UToolMenu* Menu) +{ + FToolMenuSection& Section = Menu->AddSection("ReadyPlayerMeSubSection"); + + Section.AddMenuEntry( + "OpenLoginWindow", + LOCTEXT("OpenLoginWindow", "Developer Window"), + LOCTEXT("OpenLoginWindowToolTip", "Open the RPM Developer Window."), + FSlateIcon(), + FUIAction(FExecuteAction::CreateRaw(this, &FRpmNextGenEditorModule::PluginButtonClicked)) + ); + + // Don't show Loader window in the menu + // Section.AddMenuEntry( + // "OpenLoaderWindow", + // LOCTEXT("OpenLoaderWindow", "Glb Loader"), + // LOCTEXT("OpenLoaderWindowToolTip", "Avatar Loader Window."), + // FSlateIcon(), + // FUIAction(FExecuteAction::CreateRaw(this, &FRpmNextGenEditorModule::OpenLoaderWindow)) + // ); +} + +void FRpmNextGenEditorModule::ShutdownModule() +{ + UToolMenus::UnRegisterStartupCallback(this); + UToolMenus::UnregisterOwner(this); + + FLoginWindowStyle::Shutdown(); + + FLoginWindowCommands::Unregister(); + // Don't show Loader window in the menu + //FLoaderWindowCommands::Unregister(); + + FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(DeveloperWindowName); + // Don't show Loader window in the menu + //FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(LoaderWinderName); +} + +TSharedRef FRpmNextGenEditorModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs) +{ + FText WidgetText = FText::Format( + LOCTEXT("WindowWidgetText", "Add code to {0} in {1} to override this window's contents"), + FText::FromString(TEXT("FRpmNextGenEditorModule::OnSpawnPluginTab")), + FText::FromString(TEXT("RpmNextGenEditor.cpp")) + ); + + return SNew(SDockTab) + .TabRole(NomadTab) + [ + SNew(SRpmDeveloperLoginWidget) + ]; + +} + +TSharedRef FRpmNextGenEditorModule::OnSpawnLoaderWindow(const FSpawnTabArgs& SpawnTabArgs) +{ + return SNew(SDockTab) + .TabRole(NomadTab) + [ + SNew(SCharacterLoaderWidget) + ]; +} + +void FRpmNextGenEditorModule::PluginButtonClicked() +{ + FGlobalTabmanager::Get()->TryInvokeTab(DeveloperWindowName); +} + + +void FRpmNextGenEditorModule::OpenLoaderWindow() +{ + FGlobalTabmanager::Get()->TryInvokeTab(LoaderWinderName); +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FRpmNextGenEditorModule, RpmNextGenEditor) \ No newline at end of file diff --git a/Source/RpmNextGenEditor/Private/UI/CharacterLoaderWidget.cpp b/Source/RpmNextGenEditor/Private/UI/CharacterLoaderWidget.cpp new file mode 100644 index 0000000..f300345 --- /dev/null +++ b/Source/RpmNextGenEditor/Private/UI/CharacterLoaderWidget.cpp @@ -0,0 +1,122 @@ +#include "UI/CharacterLoaderWidget.h" + +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Text/STextBlock.h" +#include "EditorStyleSet.h" +#include "glTFRuntimeFunctionLibrary.h" +#include "Api/Assets/AssetLoader.h" +#include "PropertyCustomizationHelpers.h" +#include "AssetRegistry/AssetData.h" + +// Configuration section and key names +static const FString ConfigSection = TEXT("/Script/ReadyPlayerMe.CharacterLoaderSettings"); +static const FString PathKeyName = TEXT("LastFilePath"); + +void SCharacterLoaderWidget::Construct(const FArguments& InArgs) +{ + FString LastSavedPath; + GConfig->GetString(*ConfigSection, *PathKeyName, LastSavedPath, GGameIni); + PathText = FText::FromString(LastSavedPath); + AssetLoader = FEditorAssetLoader(); + ChildSlot + [ + SNew(SVerticalBox) + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(STextBlock) + .Text(FText::FromString("Enter Path:")) + ] + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SAssignNew(PathTextBox, SEditableTextBox) + .Text(FText::FromString(LastSavedPath)) // Set the loaded path as the initial text + .OnTextChanged(this, &SCharacterLoaderWidget::OnPathTextChanged) + ] + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(SButton) + .Text(FText::FromString("Load Glb")) + .OnClicked(this, &SCharacterLoaderWidget::OnButtonClick) + ] + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(STextBlock) + .Text(FText::FromString("Select Skeleton:")) + ] + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(SObjectPropertyEntryBox) + .AllowedClass(USkeleton::StaticClass()) // Only allow USkeleton assets + .OnObjectChanged(this, &SCharacterLoaderWidget::OnSkeletonSelected) // Handle the asset selection + .ObjectPath(this, &SCharacterLoaderWidget::GetCurrentSkeletonPath) // Optionally provide a current value + ] + ]; +} + + +void SCharacterLoaderWidget::OnSkeletonSelected(const FAssetData& AssetData) +{ + SelectedSkeleton = Cast(AssetData.GetAsset()); + if (SelectedSkeleton) + { + UE_LOG(LogTemp, Log, TEXT("Selected Skeleton: %s"), *SelectedSkeleton->GetName()); + } +} + + +FString SCharacterLoaderWidget::GetCurrentSkeletonPath() const +{ + return SelectedSkeleton ? SelectedSkeleton->GetPathName() : FString(); +} + +void SCharacterLoaderWidget::OnPathTextChanged(const FText& NewText) +{ + PathText = NewText; + + // Save the path to the configuration file + GConfig->SetString(*ConfigSection, *PathKeyName, *PathText.ToString(), GGameIni); + GConfig->Flush(false, GGameIni); // Ensure the config is saved immediately +} + + +FReply SCharacterLoaderWidget::OnButtonClick() +{ + FString Path = PathText.ToString(); + if (Path.IsEmpty()) + { + UE_LOG(LogTemp, Error, TEXT("Path is empty")); + return FReply::Handled(); + } + LoadAsset(Path); + + return FReply::Handled(); +} + +void SCharacterLoaderWidget::LoadAsset(const FString& Path) +{ + FglTFRuntimeConfig Config = FglTFRuntimeConfig(); + Config.SceneScale = 100.0f; + Config.TransformBaseType = EglTFRuntimeTransformBaseType::YForward; + UglTFRuntimeAsset* gltfAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromFilename(Path, true, Config); + if (SelectedSkeleton) + { + AssetLoader.SkeletonToCopy = SelectedSkeleton; + } + AssetLoader.LoadAssetToWorldAsURpmActor(gltfAsset); + AssetLoader.SaveAsUAsset(gltfAsset, TEXT("/Game/ReadyPlayerMe/TestSkeleton")); +} diff --git a/Source/RpmNextGenEditor/Private/UI/Commands/LoaderWindowCommands.cpp b/Source/RpmNextGenEditor/Private/UI/Commands/LoaderWindowCommands.cpp new file mode 100644 index 0000000..e37aa1c --- /dev/null +++ b/Source/RpmNextGenEditor/Private/UI/Commands/LoaderWindowCommands.cpp @@ -0,0 +1,9 @@ +#include "UI/Commands/LoaderWindowCommands.h" + +#define LOCTEXT_NAMESPACE "FRpmNextGenEditorModule" + +void FLoaderWindowCommands::RegisterCommands() +{ + UI_COMMAND(OpenPluginWindow, "Open Loader Window", "Open the custom loader window", EUserInterfaceActionType::Button, FInputGesture()); +} +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/RpmNextGenEditor/Private/UI/Commands/LoginWindowCommands.cpp b/Source/RpmNextGenEditor/Private/UI/Commands/LoginWindowCommands.cpp new file mode 100644 index 0000000..48d9937 --- /dev/null +++ b/Source/RpmNextGenEditor/Private/UI/Commands/LoginWindowCommands.cpp @@ -0,0 +1,10 @@ +#include "UI/Commands/LoginWindowCommands.h" + +#define LOCTEXT_NAMESPACE "FRpmNextGenEditorModule" + +void FLoginWindowCommands::RegisterCommands() +{ + UI_COMMAND(OpenPluginWindow, "Rpm Developer window", "Bring up RPM Developer window", EUserInterfaceActionType::Button, FInputChord()); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/RpmNextGenEditor/Private/UI/LoginWindowStyle.cpp b/Source/RpmNextGenEditor/Private/UI/LoginWindowStyle.cpp new file mode 100644 index 0000000..1d86263 --- /dev/null +++ b/Source/RpmNextGenEditor/Private/UI/LoginWindowStyle.cpp @@ -0,0 +1,58 @@ +#include "UI/LoginWindowStyle.h" +#include "Styling/SlateStyleRegistry.h" +#include "Framework/Application/SlateApplication.h" +#include "Slate/SlateGameResources.h" +#include "Interfaces/IPluginManager.h" +#include "Styling/SlateStyleMacros.h" + +#define RootToContentDir Style->RootToContentDir + +TSharedPtr FLoginWindowStyle::StyleInstance = nullptr; + +void FLoginWindowStyle::Initialize() +{ + if (!StyleInstance.IsValid()) + { + StyleInstance = Create(); + FSlateStyleRegistry::RegisterSlateStyle(*StyleInstance); + } +} + +void FLoginWindowStyle::Shutdown() +{ + FSlateStyleRegistry::UnRegisterSlateStyle(*StyleInstance); + ensure(StyleInstance.IsUnique()); + StyleInstance.Reset(); +} + +FName FLoginWindowStyle::GetStyleSetName() +{ + static FName StyleSetName(TEXT("LoginWindowStyle")); + return StyleSetName; +} + +const FVector2D Icon16x16(16.0f, 16.0f); +const FVector2D Icon20x20(20.0f, 20.0f); + +TSharedRef< FSlateStyleSet > FLoginWindowStyle::Create() +{ + TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("LoginWindowStyle")); + Style->SetContentRoot(IPluginManager::Get().FindPlugin("RpmNextGen")->GetBaseDir() / TEXT("Resources")); + + Style->Set("RpmNextGenEditor.OpenPluginWindow", new IMAGE_BRUSH_SVG(TEXT("PlaceholderButtonIcon"), Icon20x20)); + + return Style; +} + +void FLoginWindowStyle::ReloadTextures() +{ + if (FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); + } +} + +const ISlateStyle& FLoginWindowStyle::Get() +{ + return *StyleInstance; +} diff --git a/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp new file mode 100644 index 0000000..7020835 --- /dev/null +++ b/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp @@ -0,0 +1,519 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "UI/SRpmDeveloperLoginWidget.h" +#include "Auth/DevAuthTokenCache.h" +#include "EditorCache.h" +#include "SlateOptMacros.h" +#include "Api/Assets/Models/AssetListRequest.h" +#include "Api/Assets/Models/AssetListResponse.h" +#include "DeveloperAccounts/DeveloperAccountApi.h" +#include "Auth/DeveloperTokenAuthStrategy.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "RpmImageLoader.h" +#include "Auth/DeveloperAuthApi.h" +#include "Auth/Models/DeveloperAuth.h" +#include "Auth/Models/DeveloperLoginRequest.h" +#include "DeveloperAccounts/Models/ApplicationListRequest.h" +#include "DeveloperAccounts/Models/ApplicationListResponse.h" +#include "DeveloperAccounts/Models/OrganizationListRequest.h" +#include "DeveloperAccounts/Models/OrganizationListResponse.h" +#include "Settings/RpmDeveloperSettings.h" +#include "Widgets/Layout/SScrollBox.h" + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION + +void SRpmDeveloperLoginWidget::Construct(const FArguments& InArgs) +{ + FDeveloperAuth AuthData = FDevAuthTokenCache::GetAuthData(); + FDevAuthTokenCache::SetAuthData(AuthData); + + bIsLoggedIn = AuthData.IsValid(); + UserName = AuthData.Name; + + ChildSlot + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .Padding(10) + .AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString("Sign in with your Ready Player Me Studio account")) + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoginViewVisibility) + ] + + SVerticalBox::Slot() + .Padding(10) + .AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString("Email:")) + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoginViewVisibility) + ] + + SVerticalBox::Slot() + .Padding(10) + .AutoHeight() + [ + SAssignNew(EmailTextBox, SEditableTextBox) + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoginViewVisibility) + ] + + SVerticalBox::Slot() + .Padding(10) + .AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString("Password:")) + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoginViewVisibility) + ] + + SVerticalBox::Slot() + .Padding(10) + .AutoHeight() + [ + SAssignNew(PasswordTextBox, SEditableTextBox) + .IsPassword(true) + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoginViewVisibility) + ] + + SVerticalBox::Slot() + .Padding(10) + .AutoHeight() + [ + SNew(SButton) + .Text(FText::FromString("Login")) + .OnClicked(this, &SRpmDeveloperLoginWidget::OnLoginClicked) + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoginViewVisibility) + ] + + SVerticalBox::Slot() + .Padding(10) + .AutoHeight() + [ + SNew(SButton) + .Text(FText::FromString("Use Demo Account")) + .OnClicked(this, &SRpmDeveloperLoginWidget::OnUseDemoAccountClicked) + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoginViewVisibility) + ] + + SVerticalBox::Slot() + .Padding(10) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0) + [ + SNew(STextBlock) + .Text(this, &SRpmDeveloperLoginWidget::GetWelcomeText) + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoggedInViewVisibility) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Right) + [ + SNew(SButton) + .Text(FText::FromString("Logout")) + .OnClicked(this, &SRpmDeveloperLoginWidget::OnLogoutClicked) + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoggedInViewVisibility) + ] + ] + + SVerticalBox::Slot() + .Padding(10) + .AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString("Project Settings")) + .Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Regular.ttf"), 16)) + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoggedInViewVisibility) + ] + + SVerticalBox::Slot() + .Padding(10) + .AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString("Select the Ready Player Me application to link to project")) + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoggedInViewVisibility) + ] + + + SVerticalBox::Slot() + .Padding(10) + .AutoHeight() + [ + SNew(SComboBox>) + .OptionsSource(&ComboBoxItems) + .OnSelectionChanged(this, &SRpmDeveloperLoginWidget::OnComboBoxSelectionChanged) + .OnGenerateWidget_Lambda([](TSharedPtr Item) + { + return SNew(STextBlock).Text(FText::FromString(*Item)); + }) + [ + SAssignNew(SelectedApplicationTextBlock, STextBlock).Text( + this, &SRpmDeveloperLoginWidget::GetSelectedComboBoxItemText) + ] + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoggedInViewVisibility) + ] + + SVerticalBox::Slot() + .Padding(10) + .AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString("Character Styles")) + .Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Regular.ttf"), 16)) + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoggedInViewVisibility) + ] + + SVerticalBox::Slot() + .Padding(10) + .AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString("Here you can import your character styles from Studio")) + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoggedInViewVisibility) + ] + + SVerticalBox::Slot() + .Padding(10) + .FillHeight(1.0f) // Allows the scroll box to take up remaining space + [ + SNew(SScrollBox) + .Visibility(this, &SRpmDeveloperLoginWidget::GetLoggedInViewVisibility) + + SScrollBox::Slot() + [ + SAssignNew(ContentBox, SVerticalBox) + ] + ] + ]; + + EmailTextBox->SetText(FText::FromString(FEditorCache::GetString(CacheKeyEmail))); + Initialize(); +} + +void SRpmDeveloperLoginWidget::Initialize() +{ + if (bIsInitialized) + { + return; + } + const FDeveloperAuth DevAuthData = FDevAuthTokenCache::GetAuthData(); + if (!DeveloperAuthApi.IsValid()) + { + DeveloperAuthApi = MakeUnique(); + + DeveloperAuthApi->OnLoginResponse.BindRaw(this, &SRpmDeveloperLoginWidget::HandleLoginResponse); + } + + if (!AssetApi.IsValid()) + { + AssetApi = MakeUnique(); + if (!DevAuthData.IsDemo) + { + AssetApi->SetAuthenticationStrategy(new DeveloperTokenAuthStrategy()); + } + AssetApi->OnListAssetsResponse.BindRaw(this, &SRpmDeveloperLoginWidget::HandleBaseModelListResponse); + } + if (!DeveloperAccountApi.IsValid()) + { + DeveloperAccountApi = MakeUnique(nullptr); + if (!DevAuthData.IsDemo) + { + DeveloperAccountApi->SetAuthenticationStrategy(new DeveloperTokenAuthStrategy()); + } + + DeveloperAccountApi->OnOrganizationResponse.BindRaw( + this, &SRpmDeveloperLoginWidget::HandleOrganizationListResponse); + DeveloperAccountApi->OnApplicationListResponse.BindRaw( + this, &SRpmDeveloperLoginWidget::HandleApplicationListResponse); + } + bIsInitialized = true; + if (bIsLoggedIn) + { + GetOrgList(); + return; + } + OnLogoutClicked(); +} + +SRpmDeveloperLoginWidget::~SRpmDeveloperLoginWidget() +{ + ClearLoadedCharacterModelImages(); +} + +void SRpmDeveloperLoginWidget::ClearLoadedCharacterModelImages() +{ + for (const auto Texture : CharacterStyleTextures) + { + Texture->RemoveFromRoot(); + } + CharacterStyleTextures.Empty(); +} + +void SRpmDeveloperLoginWidget::AddCharacterStyle(const FAsset& StyleAsset) +{ + TSharedPtr ImageWidget; + const FVector2D ImageSize(100.0f, 100.0f); + + ContentBox->AddSlot() + .AutoHeight() + .Padding(5) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(5) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .HAlign(HAlign_Left) + [ + SAssignNew(ImageWidget, SImage). + DesiredSizeOverride(ImageSize) + ] + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5, 5) + [ + SNew(SBox) + .WidthOverride(100.0f) + [ + SNew(SButton) + .Text(FText::FromString("Load Style")) + .OnClicked_Lambda([this, StyleAsset]() -> FReply + { + OnLoadStyleClicked(StyleAsset.Id); + return FReply::Handled(); + }) + ] + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Top) + .Padding(10, 10, 0, 0) + [ + SNew(SEditableText) + .Text(FText::FromString(FString::Printf(TEXT("ID: %s"), *StyleAsset.Id))) + .IsReadOnly(true) + .IsCaretMovedWhenGainFocus(false) + .SelectAllTextWhenFocused(false) + .MinDesiredWidth(100.0f) + ] + ]; + + FRpmImageLoader ImageLoader; + ImageLoader.LoadSImageFromURL(ImageWidget, StyleAsset.IconUrl, [this](UTexture2D* texture) + { + texture->AddToRoot(); + CharacterStyleTextures.Add(texture); + }); +} + +void SRpmDeveloperLoginWidget::OnLoadStyleClicked(const FString& StyleId) +{ + AssetLoader = FEditorAssetLoader(); + AssetLoader.LoadGLBFromURLWithId(CharacterStyleAssets[StyleId].GlbUrl, *StyleId); +} + +EVisibility SRpmDeveloperLoginWidget::GetLoginViewVisibility() const +{ + return bIsLoggedIn ? EVisibility::Collapsed : EVisibility::Visible; +} + +EVisibility SRpmDeveloperLoginWidget::GetLoggedInViewVisibility() const +{ + return bIsLoggedIn ? EVisibility::Visible : EVisibility::Collapsed; +} + +FText SRpmDeveloperLoginWidget::GetWelcomeText() const +{ + return FText::Format(FText::FromString("Welcome {0}"), FText::FromString(UserName)); +} + +FReply SRpmDeveloperLoginWidget::OnLoginClicked() +{ + URpmDeveloperSettings* RpmSettings = GetMutableDefault(); + RpmSettings->Reset(); + FString Email = EmailTextBox->GetText().ToString(); + FString Password = PasswordTextBox->GetText().ToString(); + FEditorCache::SetString(CacheKeyEmail, Email); + Email = Email.TrimStartAndEnd(); + Password = Password.TrimStartAndEnd(); + DeveloperAccountApi->SetAuthenticationStrategy(new DeveloperTokenAuthStrategy()); + AssetApi->SetAuthenticationStrategy(new DeveloperTokenAuthStrategy()); + FDeveloperLoginRequest LoginRequest = FDeveloperLoginRequest(Email, Password); + DeveloperAuthApi->LoginWithEmail(LoginRequest); + return FReply::Handled(); +} + +void SRpmDeveloperLoginWidget::GetOrgList() +{ + FOrganizationListRequest OrgRequest; + DeveloperAccountApi->ListOrganizationsAsync(OrgRequest); +} + +void SRpmDeveloperLoginWidget::HandleLoginResponse(const FDeveloperLoginResponse& Response, bool bWasSuccessful) +{ + if (bWasSuccessful) + { + UserName = Response.Data.Name; + FDeveloperAuth AuthData = FDeveloperAuth(Response.Data, false); + FDevAuthTokenCache::SetAuthData(AuthData); + SetLoggedInState(true); + GetOrgList(); + return; + } + UE_LOG(LogTemp, Error, TEXT("Login request failed")); + FDevAuthTokenCache::ClearAuthData(); +} + +void SRpmDeveloperLoginWidget::HandleOrganizationListResponse(const FOrganizationListResponse& Response, + bool bWasSuccessful) +{ + if (bWasSuccessful) + { + if (Response.Data.Num() == 0) + { + UE_LOG(LogTemp, Error, TEXT("No organizations found")); + return; + } + FApplicationListRequest Request; + Request.Params.Add("organizationId", Response.Data[0].Id); + DeveloperAccountApi->ListApplicationsAsync(Request); + return; + } + + UE_LOG(LogTemp, Error, TEXT("Failed to list organizations")); +} + + +void SRpmDeveloperLoginWidget::HandleApplicationListResponse(const FApplicationListResponse& Response, bool bWasSuccessful) +{ + if (bWasSuccessful) + { + const URpmDeveloperSettings* RpmSettings = GetDefault(); + UserApplications = Response.Data; + FString Active; + TArray Items; + for (const FApplication& App : UserApplications) + { + Items.Add(App.Name); + if (App.Id == RpmSettings->ApplicationId) + { + Active = App.Name; + } + } + if (Active.IsEmpty() && Items.Num() > 0) + { + const auto NewActiveItem = MakeShared(Items[0]); + OnComboBoxSelectionChanged(NewActiveItem, ESelectInfo::Direct); + SelectedApplicationTextBlock->SetText(FText::FromString(*NewActiveItem)); + } + PopulateComboBoxItems(Items, Active); + } + else + { + UE_LOG(LogTemp, Error, TEXT("Failed to list applications")); + } + LoadBaseModelList(); +} + + +void SRpmDeveloperLoginWidget::PopulateComboBoxItems(const TArray& Items, const FString ActiveItem) +{ + ComboBoxItems.Empty(); + for (const FString& Item : Items) + { + ComboBoxItems.Add(MakeShared(Item)); + } + SelectedComboBoxItem = MakeShared(ActiveItem); +} + + +FText SRpmDeveloperLoginWidget::GetSelectedComboBoxItemText() const +{ + return SelectedComboBoxItem.IsValid() && !SelectedComboBoxItem->IsEmpty() + ? FText::FromString(*SelectedComboBoxItem) + : FText::FromString("Select an option"); +} + + +void SRpmDeveloperLoginWidget::OnComboBoxSelectionChanged(TSharedPtr NewValue, ESelectInfo::Type SelectInfo) +{ + SelectedComboBoxItem = NewValue; + FApplication* application = UserApplications.FindByPredicate([&](FApplication item) + { + return item.Name == *NewValue; + }); + if (application) + { + URpmDeveloperSettings* RpmSettings = GetMutableDefault(); + RpmSettings->ApplicationId = application->Id; + RpmSettings->SaveConfig(); + } +} + +FReply SRpmDeveloperLoginWidget::OnUseDemoAccountClicked() +{ + URpmDeveloperSettings* RpmSettings = GetMutableDefault(); + RpmSettings->SetupDemoAccount(); + FDeveloperAuth AuthData = FDeveloperAuth(); + AuthData.Name = DemoUserName; + AuthData.IsDemo = true; + UserName = AuthData.Name; + FDevAuthTokenCache::SetAuthData(AuthData); + SetLoggedInState(true); + + // Unset the authentication strategy for the APIs + DeveloperAccountApi->SetAuthenticationStrategy(nullptr); + AssetApi->SetAuthenticationStrategy(nullptr); + GetOrgList(); + return FReply::Handled(); +} + +FReply SRpmDeveloperLoginWidget::OnLogoutClicked() +{ + URpmDeveloperSettings* RpmSettings = GetMutableDefault(); + RpmSettings->Reset(); + + // Clear the content box to remove all child widgets + if (ContentBox.IsValid()) + { + ContentBox->ClearChildren(); + } + ComboBoxItems.Empty(); + + ClearLoadedCharacterModelImages(); + FDevAuthTokenCache::ClearAuthData(); + SetLoggedInState(false); + return FReply::Handled(); +} + +void SRpmDeveloperLoginWidget::LoadBaseModelList() +{ + const URpmDeveloperSettings* RpmSettings = GetDefault(); + if (RpmSettings->ApplicationId.IsEmpty()) + { + UE_LOG(LogTemp, Error, TEXT("Application ID is empty, unable to load base models.")); + return; + } + FAssetListRequest Request = FAssetListRequest(); + FAssetListQueryParams Params = FAssetListQueryParams(); + Params.ApplicationId = RpmSettings->ApplicationId; + Params.Type = "baseModel"; + Request.Params = Params; + AssetApi->ListAssetsAsync(Request); +} + +void SRpmDeveloperLoginWidget::HandleBaseModelListResponse(const FAssetListResponse& Response, bool bWasSuccessful) +{ + CharacterStyleAssets.Empty(); + for (FAsset Asset : Response.Data) + { + CharacterStyleAssets.Add(Asset.Id, Asset); + AddCharacterStyle(Asset); + } +} + + +void SRpmDeveloperLoginWidget::SetLoggedInState(const bool IsLoggedIn) +{ + this->bIsLoggedIn = IsLoggedIn; + + // Force the UI to refresh + Invalidate(EInvalidateWidget::Layout); +} + +END_SLATE_FUNCTION_BUILD_OPTIMIZATION diff --git a/Source/RpmNextGenEditor/Public/AssetNameGenerator.h b/Source/RpmNextGenEditor/Public/AssetNameGenerator.h new file mode 100644 index 0000000..346cd99 --- /dev/null +++ b/Source/RpmNextGenEditor/Public/AssetNameGenerator.h @@ -0,0 +1,32 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "TransientObjectSaverLibrary.h" +#include "UObject/Object.h" +#include "AssetNameGenerator.generated.h" + +/** + * + */ +UCLASS() +class RPMNEXTGENEDITOR_API UAssetNameGenerator : public UObject +{ + GENERATED_BODY() + +public: + UAssetNameGenerator(); + void SetPath(FString path); + UFUNCTION() + FString GenerateMaterialName(UMaterialInterface* Material, int32 MaterialIndex, const FString& SlotName) const; + UFUNCTION() + FString GenerateTextureName(UTexture* Texture, UMaterialInterface* Material, const FString& MaterialPath, + const FString& ParamName) const; + + FTransientObjectSaverMaterialNameGenerator MaterialNameGeneratorDelegate; + FTransientObjectSaverTextureNameGenerator TextureNameGeneratorDelegate; + +private: + FString path; +}; diff --git a/Source/RpmNextGenEditor/Public/Auth/DevAuthTokenCache.h b/Source/RpmNextGenEditor/Public/Auth/DevAuthTokenCache.h new file mode 100644 index 0000000..316d9a6 --- /dev/null +++ b/Source/RpmNextGenEditor/Public/Auth/DevAuthTokenCache.h @@ -0,0 +1,22 @@ +#pragma once + +#include "CoreMinimal.h" + +struct FDeveloperAuth; + +class RPMNEXTGENEDITOR_API FDevAuthTokenCache +{ +public: + static FDeveloperAuth GetAuthData(); + static void SetAuthData(const FDeveloperAuth& DevAuthData); + static void ClearAuthData(); +private: + static constexpr const TCHAR* CacheKeyName = TEXT("Name"); + static constexpr const TCHAR* CacheKeyToken = TEXT("Token"); + static constexpr const TCHAR* CacheKeyRefreshToken = TEXT("RefreshToken"); + static constexpr const TCHAR* CacheKeyIsDemo = TEXT("IsDemo"); + + static FDeveloperAuth AuthData; + static bool bIsInitialized; + static void Initialize(); +}; diff --git a/Source/RpmNextGenEditor/Public/Auth/DeveloperAuthApi.h b/Source/RpmNextGenEditor/Public/Auth/DeveloperAuthApi.h new file mode 100644 index 0000000..e8084b3 --- /dev/null +++ b/Source/RpmNextGenEditor/Public/Auth/DeveloperAuthApi.h @@ -0,0 +1,21 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Common/WebApi.h" + +struct FDeveloperLoginResponse; +struct FDeveloperLoginRequest; + +DECLARE_DELEGATE_TwoParams(FOnDeveloperLoginResponse, const FDeveloperLoginResponse&, bool); + +class RPMNEXTGENEDITOR_API FDeveloperAuthApi : public FWebApi +{ +public: + FDeveloperAuthApi(); + + void HandleLoginResponse(FString JsonData, bool bIsSuccessful) const; + void LoginWithEmail(FDeveloperLoginRequest Request); + FOnDeveloperLoginResponse OnLoginResponse; +private: + FString ApiUrl; +}; diff --git a/Source/RpmNextGenEditor/Public/Auth/DeveloperTokenAuthStrategy.h b/Source/RpmNextGenEditor/Public/Auth/DeveloperTokenAuthStrategy.h new file mode 100644 index 0000000..da3bf06 --- /dev/null +++ b/Source/RpmNextGenEditor/Public/Auth/DeveloperTokenAuthStrategy.h @@ -0,0 +1,22 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Auth/ApiRequest.h" +#include "Api/Auth/AuthApi.h" +#include "Api/Auth/IAuthenticationStrategy.h" + +struct FRefreshTokenResponse; + + +class RPMNEXTGENEDITOR_API DeveloperTokenAuthStrategy : public IAuthenticationStrategy +{ +public: + DeveloperTokenAuthStrategy(); + virtual void AddAuthToRequest(TSharedPtr Request) override; + virtual void OnRefreshTokenResponse(const FRefreshTokenResponse& Response, bool bWasSuccessful) override; + virtual void TryRefresh(TSharedPtr Request) override; +private: + void RefreshTokenAsync(const FRefreshTokenRequest& Request); + FOnWebApiResponse OnWebApiResponse; + FAuthApi AuthApi; +}; diff --git a/Source/RpmNextGenEditor/Public/Auth/Models/DeveloperAuth.h b/Source/RpmNextGenEditor/Public/Auth/Models/DeveloperAuth.h new file mode 100644 index 0000000..aa7a092 --- /dev/null +++ b/Source/RpmNextGenEditor/Public/Auth/Models/DeveloperAuth.h @@ -0,0 +1,57 @@ +#pragma once + +#include "CoreMinimal.h" +#include "DeveloperLoginResponse.h" +#include "DeveloperAuth.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGENEDITOR_API FDeveloperAuth +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "token")) + FString Token; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "refreshToken")) + FString RefreshToken; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "name")) + FString Name; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "isDemo")) + bool IsDemo; + + FDeveloperAuth() = default; + + FDeveloperAuth(FDeveloperLoginResponseBody Data, bool bIsDemo) + { + Token = Data.Token; + RefreshToken = Data.RefreshToken; + Name = Data.Name; + IsDemo = bIsDemo; + } + + FString ToJsonString() const + { + FString OutputString; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&OutputString); + Writer->WriteObjectStart(); + Writer->WriteValue(TEXT("token"), Token); + Writer->WriteValue(TEXT("refreshToken"), RefreshToken); + Writer->WriteValue(TEXT("name"), Name); + Writer->WriteValue(TEXT("isDemo"), IsDemo); + Writer->WriteObjectEnd(); + Writer->Close(); + return OutputString; + } + + static bool FromJsonString(const FString& JsonString, FDeveloperAuth& OutStruct) + { + return FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &OutStruct, 0, 0); + } + + bool IsValid() const + { + return IsDemo || !Token.IsEmpty(); + } +}; diff --git a/Source/RpmNextGenEditor/Public/Auth/Models/DeveloperLoginRequest.h b/Source/RpmNextGenEditor/Public/Auth/Models/DeveloperLoginRequest.h new file mode 100644 index 0000000..369453c --- /dev/null +++ b/Source/RpmNextGenEditor/Public/Auth/Models/DeveloperLoginRequest.h @@ -0,0 +1,58 @@ +#pragma once + +#include "CoreMinimal.h" +#include "JsonObjectConverter.h" +#include "DeveloperLoginRequest.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGENEDITOR_API FDeveloperLoginRequestBody +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString LoginId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString Password; + + FString ToJsonString() const + { + FString OutputString; + FJsonObjectConverter::UStructToJsonObjectString(*this, OutputString); + return OutputString; + } + + static bool FromJsonString(const FString& JsonString, FDeveloperLoginRequestBody& OutStruct) + { + return FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &OutStruct, 0, 0); + } +}; + +USTRUCT(BlueprintType) +struct RPMNEXTGENEDITOR_API FDeveloperLoginRequest +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Request") + FDeveloperLoginRequestBody Data; + + FString ToJsonString() const + { + FString OutputString; + FJsonObjectConverter::UStructToJsonObjectString(*this, OutputString); + return OutputString; + } + + static bool FromJsonString(const FString& JsonString, FDeveloperLoginRequest& OutStruct) + { + return FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &OutStruct, 0, 0); + } + + FDeveloperLoginRequest() = default; + explicit FDeveloperLoginRequest(const FDeveloperLoginRequestBody& DeveloperLoginRequestBody); + FDeveloperLoginRequest(const FString& Email, const FString& String) + { + Data.LoginId = Email; + Data.Password = String; + } +}; diff --git a/Source/RpmNextGenEditor/Public/Auth/Models/DeveloperLoginResponse.h b/Source/RpmNextGenEditor/Public/Auth/Models/DeveloperLoginResponse.h new file mode 100644 index 0000000..6af03f5 --- /dev/null +++ b/Source/RpmNextGenEditor/Public/Auth/Models/DeveloperLoginResponse.h @@ -0,0 +1,48 @@ +#pragma once + +#include "CoreMinimal.h" +#include "JsonObjectConverter.h" +#include "Api/Common/Models/ApiResponse.h" +#include "DeveloperLoginResponse.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGENEDITOR_API FDeveloperLoginResponseBody : public FApiResponse +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "token")) + FString Token; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "refreshToken")) + FString RefreshToken; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "name")) + FString Name; + +}; + +USTRUCT(BlueprintType) +struct RPMNEXTGENEDITOR_API FDeveloperLoginResponse : public FApiResponse +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "data")) + FDeveloperLoginResponseBody Data; + + FString ToJsonString() const + { + FString OutputString; + FJsonObjectConverter::UStructToJsonObjectString(*this, OutputString); + return OutputString; + } + + static bool FromJsonObject(const TSharedPtr& JsonObject, FDeveloperLoginResponse& OutObject) + { + if (JsonObject.IsValid()) + { + return FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), StaticStruct(), &OutObject, 0, 0); + } + UE_LOG(LogTemp, Warning, TEXT("JsonObject Invalid")); + return false; + } +}; diff --git a/Source/RpmNextGenEditor/Public/DeveloperAccounts/DeveloperAccountApi.h b/Source/RpmNextGenEditor/Public/DeveloperAccounts/DeveloperAccountApi.h new file mode 100644 index 0000000..21565be --- /dev/null +++ b/Source/RpmNextGenEditor/Public/DeveloperAccounts/DeveloperAccountApi.h @@ -0,0 +1,29 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Common/WebApiWithAuth.h" + +struct FOrganizationListResponse; +struct FOrganizationListRequest; +struct FApplicationListResponse; +struct FApplicationListRequest; + +DECLARE_DELEGATE_TwoParams(FOnApplicationListResponse, const FApplicationListResponse&, bool); +DECLARE_DELEGATE_TwoParams(FOnOrganizationListResponse, const FOrganizationListResponse&, bool); + +class RPMNEXTGENEDITOR_API FDeveloperAccountApi : public FWebApiWithAuth +{ +public: + FDeveloperAccountApi(IAuthenticationStrategy* InAuthenticationStrategy); + void ListApplicationsAsync(const FApplicationListRequest& Request); + void ListOrganizationsAsync(const FOrganizationListRequest& Request); + + FOnApplicationListResponse OnApplicationListResponse; + FOnOrganizationListResponse OnOrganizationResponse; +private: + static FString BuildQueryString(const TMap& Params); + void HandleOrgListResponse(FString Data, bool bWasSuccessful); + void HandleAppListResponse(FString Data, bool bWasSuccessful); + + FString ApiBaseUrl; +}; diff --git a/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/ApplicationListRequest.h b/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/ApplicationListRequest.h new file mode 100644 index 0000000..12b49a8 --- /dev/null +++ b/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/ApplicationListRequest.h @@ -0,0 +1,14 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Auth/ApiRequest.h" +#include "ApplicationListRequest.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGENEDITOR_API FApplicationListRequest : public FApiRequest +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "API") + TMap Params; +}; diff --git a/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/ApplicationListResponse.h b/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/ApplicationListResponse.h new file mode 100644 index 0000000..96cf4e2 --- /dev/null +++ b/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/ApplicationListResponse.h @@ -0,0 +1,24 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Common/Models/ApiResponse.h" +#include "ApplicationListResponse.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGENEDITOR_API FApplication +{ + GENERATED_BODY() + UPROPERTY(meta = (JsonName = "id")) + FString Id; + UPROPERTY(meta=(Jsonname = "name" )) + FString Name; +}; + +USTRUCT(BlueprintType) +struct RPMNEXTGENEDITOR_API FApplicationListResponse : public FApiResponse +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "API", meta = (JsonName = "data")) + TArray Data; +}; diff --git a/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/OrganizationListRequest.h b/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/OrganizationListRequest.h new file mode 100644 index 0000000..0a83cd0 --- /dev/null +++ b/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/OrganizationListRequest.h @@ -0,0 +1,14 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Auth/ApiRequest.h" +#include "OrganizationListRequest.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGENEDITOR_API FOrganizationListRequest : public FApiRequest +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "API") + TMap Params; +}; diff --git a/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/OrganizationListResponse.h b/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/OrganizationListResponse.h new file mode 100644 index 0000000..8c3355e --- /dev/null +++ b/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/OrganizationListResponse.h @@ -0,0 +1,26 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Common/Models/ApiResponse.h" +#include "Api/Common/WebApiWithAuth.h" +#include "OrganizationListResponse.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGENEDITOR_API FOrganization +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (JsonName = "id")) + FString Id; + UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (JsonName = "name")) + FString Name; +}; + +USTRUCT(BlueprintType) +struct RPMNEXTGENEDITOR_API FOrganizationListResponse : public FApiResponse +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "API", meta = ( JsonName = "data")) + TArray Data; +}; diff --git a/Source/RpmNextGenEditor/Public/EditorAssetLoader.h b/Source/RpmNextGenEditor/Public/EditorAssetLoader.h new file mode 100644 index 0000000..2dfa540 --- /dev/null +++ b/Source/RpmNextGenEditor/Public/EditorAssetLoader.h @@ -0,0 +1,22 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Assets/AssetLoader.h" +#include "HAL/PlatformFilemanager.h" + +class RPMNEXTGENEDITOR_API FEditorAssetLoader : public FAssetLoader +{ +public: + void OnAssetLoadComplete(UglTFRuntimeAsset* gltfAsset, bool bWasSuccessful, + FString LoadedAssetId); + FEditorAssetLoader(); + virtual ~FEditorAssetLoader() override; + + void LoadAssetToWorldAsURpmActor(UglTFRuntimeAsset* gltfAsset, FString AssetId = ""); + void LoadGLBFromURLWithId(const FString& URL, const FString AssetId); + USkeletalMesh* SaveAsUAsset(UglTFRuntimeAsset* GltfAsset, const FString& LoadedAssetId) const; + USkeleton* SkeletonToCopy; + +private: + void LoadAssetToWorld(FString AssetId, UglTFRuntimeAsset* gltfAsset); +}; diff --git a/Source/RpmNextGenEditor/Public/EditorCache.h b/Source/RpmNextGenEditor/Public/EditorCache.h new file mode 100644 index 0000000..b1ae63e --- /dev/null +++ b/Source/RpmNextGenEditor/Public/EditorCache.h @@ -0,0 +1,19 @@ +#pragma once + +#include "CoreMinimal.h" + +class RPMNEXTGENEDITOR_API FEditorCache +{ +public: + static void SetString(const FString& Key, const FString& Value); + static void SetInt(const FString& Key, int32 Value); + static void SetFloat(const FString& Key, float Value); + static void SetBool(const FString& Key, bool Value); + + static FString GetString(const FString& Key, const FString& DefaultValue = TEXT("")); + static int32 GetInt(const FString& Key, int32 DefaultValue = 0); + static float GetFloat(const FString& Key, float DefaultValue = 0.0f); + static bool GetBool(const FString& Key, bool DefaultValue = false); + + static void RemoveKey(const FString& Key); +}; diff --git a/Source/RpmNextGenEditor/Public/RpmNextGenEditor.h b/Source/RpmNextGenEditor/Public/RpmNextGenEditor.h new file mode 100644 index 0000000..96034cf --- /dev/null +++ b/Source/RpmNextGenEditor/Public/RpmNextGenEditor.h @@ -0,0 +1,28 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class RPMNEXTGENEDITOR_API FRpmNextGenEditorModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + /** This function will be bound to Command (by default it will bring up plugin window) */ + void PluginButtonClicked(); + +private: + + void RegisterMenus(); + void FillReadyPlayerMeMenu(UToolMenu* Menu); + void OpenLoaderWindow(); + TSharedRef OnSpawnLoaderWindow(const FSpawnTabArgs& SpawnTabArgs); + TSharedRef OnSpawnPluginTab(const class FSpawnTabArgs& SpawnTabArgs); + TSharedPtr PluginCommands; + +}; diff --git a/Source/RpmNextGenEditor/Public/UI/CharacterLoaderWidget.h b/Source/RpmNextGenEditor/Public/UI/CharacterLoaderWidget.h new file mode 100644 index 0000000..b4a61ce --- /dev/null +++ b/Source/RpmNextGenEditor/Public/UI/CharacterLoaderWidget.h @@ -0,0 +1,40 @@ +#pragma once + +#include "CoreMinimal.h" +#include "EditorAssetLoader.h" +#include "Api/Assets/AssetLoader.h" +#include "Widgets/SCompoundWidget.h" +#include "Widgets/DeclarativeSyntaxSupport.h" + +class FEditorAssetLoader; +class FAssetLoader; + +class SCharacterLoaderWidget : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SCharacterLoaderWidget) {} + SLATE_END_ARGS() + + void OnSkeletonSelected(const FAssetData& AssetData); + /** Constructs this widget with InArgs */ + void Construct(const FArguments& InArgs); + +private: + /** Callback for when the button is clicked */ + FReply OnButtonClick(); + + /** Stores the text input by the user */ + FText PathText; + + /** Callback for when the text in the input field changes */ + void OnPathTextChanged(const FText& NewText); + + /** Function that gets called when the button is pressed */ + void LoadAsset(const FString& Path); + FEditorAssetLoader AssetLoader; + TSharedPtr PathTextBox; + + // Store the selected skeleton + USkeleton* SelectedSkeleton; + FString GetCurrentSkeletonPath() const; +}; diff --git a/Source/RpmNextGenEditor/Public/UI/Commands/LoaderWindowCommands.h b/Source/RpmNextGenEditor/Public/UI/Commands/LoaderWindowCommands.h new file mode 100644 index 0000000..df9a697 --- /dev/null +++ b/Source/RpmNextGenEditor/Public/UI/Commands/LoaderWindowCommands.h @@ -0,0 +1,19 @@ +#pragma once + +#include "CoreMinimal.h" +#include "EditorStyleSet.h" +#include "Framework/Commands/Commands.h" + +class FLoaderWindowCommands : public TCommands +{ +public: + FLoaderWindowCommands() + : TCommands(TEXT("LoaderWindow"), NSLOCTEXT("Contexts", "LoaderWindow", "Loader Window Plugin"), NAME_None, FEditorStyle::GetStyleSetName()) + { + } + + virtual void RegisterCommands() override; + +public: + TSharedPtr OpenPluginWindow; +}; diff --git a/Source/RpmNextGenEditor/Public/UI/Commands/LoginWindowCommands.h b/Source/RpmNextGenEditor/Public/UI/Commands/LoginWindowCommands.h new file mode 100644 index 0000000..86dc7de --- /dev/null +++ b/Source/RpmNextGenEditor/Public/UI/Commands/LoginWindowCommands.h @@ -0,0 +1,20 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Framework/Commands/Commands.h" +#include "UI/LoginWindowStyle.h" + +class RPMNEXTGENEDITOR_API FLoginWindowCommands: public TCommands +{ +public: + + FLoginWindowCommands() + : TCommands(TEXT("RpmNextGenEditor"), NSLOCTEXT("Contexts", "RpmNextGenEditor", "RpmNextGen Plugin"), NAME_None, FLoginWindowStyle::GetStyleSetName()) + { + } + + // TCommands<> interface + virtual void RegisterCommands() override; + + TSharedPtr< FUICommandInfo > OpenPluginWindow; +}; diff --git a/Source/RpmNextGenEditor/Public/UI/LoginWindowStyle.h b/Source/RpmNextGenEditor/Public/UI/LoginWindowStyle.h new file mode 100644 index 0000000..75812ef --- /dev/null +++ b/Source/RpmNextGenEditor/Public/UI/LoginWindowStyle.h @@ -0,0 +1,28 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Styling/SlateStyle.h" + +class RPMNEXTGENEDITOR_API FLoginWindowStyle +{ +public: + + static void Initialize(); + + static void Shutdown(); + + /** reloads textures used by slate renderer */ + static void ReloadTextures(); + + /** @return The Slate style set for the Shooter game */ + static const ISlateStyle& Get(); + + static FName GetStyleSetName(); + +private: + + static TSharedRef< class FSlateStyleSet > Create(); + + static TSharedPtr< class FSlateStyleSet > StyleInstance; + +}; diff --git a/Source/RpmNextGenEditor/Public/UI/SRpmDeveloperLoginWidget.h b/Source/RpmNextGenEditor/Public/UI/SRpmDeveloperLoginWidget.h new file mode 100644 index 0000000..c61bfa9 --- /dev/null +++ b/Source/RpmNextGenEditor/Public/UI/SRpmDeveloperLoginWidget.h @@ -0,0 +1,79 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "EditorAssetLoader.h" +#include "Api/Assets/AssetApi.h" +#include "Api/Assets/Models/AssetListResponse.h" +#include "Auth/DeveloperAuthApi.h" +#include "DeveloperAccounts/DeveloperAccountApi.h" +#include "DeveloperAccounts/Models/ApplicationListResponse.h" +#include "Widgets/SCompoundWidget.h" +#include "Containers/Map.h" + + +struct FDeveloperLoginResponse; +class URpmDeveloperSettings; +class UDeveloperAuthApi; +class SEditableTextBox; + +/** + * + */ +class RPMNEXTGENEDITOR_API SRpmDeveloperLoginWidget : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SRpmDeveloperLoginWidget) + { + } + + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + virtual ~SRpmDeveloperLoginWidget() override; + +private: + TSharedPtr ContentBox; + TSharedPtr EmailTextBox; + TSharedPtr PasswordTextBox; + TSharedPtr SelectedApplicationTextBlock; + TSharedPtr SelectedComboBoxItem; + TArray> ComboBoxItems; + TArray CharacterStyleTextures; + TMap CharacterStyleAssets; + + EVisibility GetLoginViewVisibility() const; + EVisibility GetLoggedInViewVisibility() const; + + FEditorAssetLoader AssetLoader; + TUniquePtr AssetApi; + TUniquePtr DeveloperAccountApi; + TUniquePtr DeveloperAuthApi; + static constexpr const TCHAR* CacheKeyEmail = TEXT("Email"); + bool bIsLoggedIn = false; + bool bIsInitialized = false; + FString UserName; + TArray UserApplications; + FText GetWelcomeText() const; + const FString DemoUserName = TEXT("Guest user"); + FText GetSelectedComboBoxItemText() const; + + FReply OnLoginClicked(); + FReply OnUseDemoAccountClicked(); + FReply OnLogoutClicked(); + + void Initialize(); + void GetOrgList(); + void ClearLoadedCharacterModelImages(); + void LoadBaseModelList(); + void HandleLoginResponse(const FDeveloperLoginResponse& Response, bool bWasSuccessful); + void HandleOrganizationListResponse(const FOrganizationListResponse& Response, bool bWasSuccessful); + void HandleApplicationListResponse(const FApplicationListResponse& Response, bool bWasSuccessful); + void HandleBaseModelListResponse(const FAssetListResponse& Response, bool bWasSuccessful); + void OnLoadStyleClicked(const FString& StyleId); + void SetLoggedInState(const bool IsLoggedIn); + void PopulateComboBoxItems(const TArray& Items, const FString ActiveItem); + void OnComboBoxSelectionChanged(TSharedPtr NewValue, ESelectInfo::Type SelectInfo); + void AddCharacterStyle(const FAsset& StyleAsset); +}; diff --git a/Source/RpmNextGenEditor/RpmNextGenEditor.Build.cs b/Source/RpmNextGenEditor/RpmNextGenEditor.Build.cs new file mode 100644 index 0000000..1fe1b7b --- /dev/null +++ b/Source/RpmNextGenEditor/RpmNextGenEditor.Build.cs @@ -0,0 +1,65 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class RpmNextGenEditor : ModuleRules +{ + public RpmNextGenEditor(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + "HTTP", + "JsonUtilities", + "RpmNextGen", + "EditorStyle", + "glTFRuntime", + "TransientObjectSaver", + "UnrealEd", + "PropertyEditor", + "Slate", + "SlateCore", + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "Projects", + "InputCore", + "EditorFramework", + "UnrealEd", + "ToolMenus", + "CoreUObject", + "Json", + "UMG", + "ImageWrapper", + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + } + ); + } +}