From 4e9c2e3a4d7775e3c82709e364d2620187c7c2cc Mon Sep 17 00:00:00 2001 From: Harrison Hough Date: Wed, 28 Aug 2024 18:47:56 +0300 Subject: [PATCH 01/54] feat: reorg and WIP cache window --- .../Private/RpmNextGenEditor.cpp | 66 +++++-- .../UI/Commands/CacheWindowCommands.cpp | 10 + .../UI/Commands}/LoaderWindowCommands.cpp | 2 +- .../UI/{ => Commands}/LoginWindowCommands.cpp | 2 +- .../Private/UI/SCacheEditorWidget.cpp | 183 ++++++++++++++++++ .../Public/RpmNextGenEditor.h | 6 +- .../Public/UI/Commands/CacheWindowCommands.h | 18 ++ .../UI/{ => Commands}/LoaderWindowCommands.h | 0 .../UI/{ => Commands}/LoginWindowCommands.h | 2 +- .../Public/UI/SCacheEditorWidget.h | 44 +++++ 10 files changed, 315 insertions(+), 18 deletions(-) create mode 100644 Source/RpmNextGenEditor/Private/UI/Commands/CacheWindowCommands.cpp rename Source/RpmNextGenEditor/{Public/UI => Private/UI/Commands}/LoaderWindowCommands.cpp (84%) rename Source/RpmNextGenEditor/Private/UI/{ => Commands}/LoginWindowCommands.cpp (84%) create mode 100644 Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp create mode 100644 Source/RpmNextGenEditor/Public/UI/Commands/CacheWindowCommands.h rename Source/RpmNextGenEditor/Public/UI/{ => Commands}/LoaderWindowCommands.h (100%) rename Source/RpmNextGenEditor/Public/UI/{ => Commands}/LoginWindowCommands.h (93%) create mode 100644 Source/RpmNextGenEditor/Public/UI/SCacheEditorWidget.h diff --git a/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp b/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp index 1bf9fad..b1dc1bb 100644 --- a/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp +++ b/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp @@ -1,20 +1,23 @@ // Copyright Epic Games, Inc. All Rights Reserved. #include "RpmNextGenEditor.h" - #include "UI/CharacterLoaderWidget.h" -#include "UI/LoaderWindowCommands.h" -#include "UI/LoginWindowCommands.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" +#include "UI/LoginWindowStyle.h" +#include "UI/SCacheEditorWidget.h" +#include "UI/Commands/CacheWindowCommands.h" -static const FName TestWindowTabName("LoginWindow"); - +static const FName DeveloperWindowName("LoginWindow"); +static const FName LoaderWindowName("LoaderWindow"); +static const FName CacheWindowName("CacheWindow"); #define LOCTEXT_NAMESPACE "RpmNextGenEditorModule" -static const FName NewWindowTabName("CustomEditorWindow"); + void FRpmNextGenEditorModule::StartupModule() { @@ -22,7 +25,8 @@ void FRpmNextGenEditorModule::StartupModule() FLoginWindowStyle::ReloadTextures(); FLoginWindowCommands::Register(); - FLoaderWindowCommands::Register(); // Don't forget to register the other command set + FLoaderWindowCommands::Register(); + FCacheWindowCommands::Register(); PluginCommands = MakeShareable(new FUICommandList); @@ -31,6 +35,12 @@ void FRpmNextGenEditorModule::StartupModule() FExecuteAction::CreateRaw(this, &FRpmNextGenEditorModule::PluginButtonClicked), FCanExecuteAction()); + + PluginCommands->MapAction( + FCacheWindowCommands::Get().OpenPluginWindow, + FExecuteAction::CreateRaw(this, &FRpmNextGenEditorModule::OpenCacheEditorWindow), + FCanExecuteAction()); + // Don't show Loader window in the menu // PluginCommands->MapAction( // FLoaderWindowCommands::Get().OpenPluginWindow, @@ -39,10 +49,14 @@ void FRpmNextGenEditorModule::StartupModule() UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FRpmNextGenEditorModule::RegisterMenus)); - FGlobalTabmanager::Get()->RegisterNomadTabSpawner(TestWindowTabName, FOnSpawnTab::CreateRaw(this, &FRpmNextGenEditorModule::OnSpawnPluginTab)) + FGlobalTabmanager::Get()->RegisterNomadTabSpawner(DeveloperWindowName, FOnSpawnTab::CreateRaw(this, &FRpmNextGenEditorModule::OnSpawnPluginTab)) .SetDisplayName(LOCTEXT("DeveloperLoginWidget", "RPM Dev Login")) .SetMenuType(ETabSpawnerMenuType::Hidden); + FGlobalTabmanager::Get()->RegisterNomadTabSpawner(CacheWindowName, FOnSpawnTab::CreateRaw(this, &FRpmNextGenEditorModule::OnSpawnCacheWindow)) + .SetDisplayName(LOCTEXT("CacheEditorWidget", "Cache Editor")) + .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")) @@ -81,6 +95,15 @@ void FRpmNextGenEditorModule::FillReadyPlayerMeMenu(UToolMenu* Menu) FSlateIcon(), FUIAction(FExecuteAction::CreateRaw(this, &FRpmNextGenEditorModule::PluginButtonClicked)) ); + + + Section.AddMenuEntry( + "OpenCacheEditorWindow", + LOCTEXT("OpenCacheEditorWindow", "Cache Editor"), + LOCTEXT("OpenLoaderWindowToolTip", "Cache Editor Window."), + FSlateIcon(), + FUIAction(FExecuteAction::CreateRaw(this, &FRpmNextGenEditorModule::OpenCacheEditorWindow)) + ); // Don't show Loader window in the menu // Section.AddMenuEntry( @@ -100,10 +123,13 @@ void FRpmNextGenEditorModule::ShutdownModule() FLoginWindowStyle::Shutdown(); FLoginWindowCommands::Unregister(); - FLoaderWindowCommands::Unregister(); // Unregister custom commands + FLoaderWindowCommands::Unregister(); - FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(TestWindowTabName); - FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(NewWindowTabName); + FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(DeveloperWindowName); + FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(CacheWindowName); + + // Don't show Loader window in the menu + //FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(LoaderWindowName); } TSharedRef FRpmNextGenEditorModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs) @@ -131,15 +157,29 @@ TSharedRef FRpmNextGenEditorModule::OnSpawnLoaderWindow(const FSpawnTa ]; } +TSharedRef FRpmNextGenEditorModule::OnSpawnCacheWindow(const FSpawnTabArgs& SpawnTabArgs) +{ + return SNew(SDockTab) + .TabRole(NomadTab) + [ + SNew(SCacheEditorWidget) + ]; +} + void FRpmNextGenEditorModule::PluginButtonClicked() { - FGlobalTabmanager::Get()->TryInvokeTab(TestWindowTabName); + FGlobalTabmanager::Get()->TryInvokeTab(DeveloperWindowName); } void FRpmNextGenEditorModule::OpenLoaderWindow() { - FGlobalTabmanager::Get()->TryInvokeTab(NewWindowTabName); + FGlobalTabmanager::Get()->TryInvokeTab(LoaderWindowName); +} + +void FRpmNextGenEditorModule::OpenCacheEditorWindow() +{ + FGlobalTabmanager::Get()->TryInvokeTab(CacheWindowName); } #undef LOCTEXT_NAMESPACE diff --git a/Source/RpmNextGenEditor/Private/UI/Commands/CacheWindowCommands.cpp b/Source/RpmNextGenEditor/Private/UI/Commands/CacheWindowCommands.cpp new file mode 100644 index 0000000..b91b53f --- /dev/null +++ b/Source/RpmNextGenEditor/Private/UI/Commands/CacheWindowCommands.cpp @@ -0,0 +1,10 @@ +#include "UI/Commands/CacheWindowCommands.h" + +#define LOCTEXT_NAMESPACE "FRpmNextGenEditorModule" + +void FCacheWindowCommands::RegisterCommands() +{ + UI_COMMAND(OpenPluginWindow, "Cache window", "Bring up RPM Cache window", EUserInterfaceActionType::Button, FInputChord()); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/RpmNextGenEditor/Public/UI/LoaderWindowCommands.cpp b/Source/RpmNextGenEditor/Private/UI/Commands/LoaderWindowCommands.cpp similarity index 84% rename from Source/RpmNextGenEditor/Public/UI/LoaderWindowCommands.cpp rename to Source/RpmNextGenEditor/Private/UI/Commands/LoaderWindowCommands.cpp index 657ce9d..e37aa1c 100644 --- a/Source/RpmNextGenEditor/Public/UI/LoaderWindowCommands.cpp +++ b/Source/RpmNextGenEditor/Private/UI/Commands/LoaderWindowCommands.cpp @@ -1,4 +1,4 @@ -#include "LoaderWindowCommands.h" +#include "UI/Commands/LoaderWindowCommands.h" #define LOCTEXT_NAMESPACE "FRpmNextGenEditorModule" diff --git a/Source/RpmNextGenEditor/Private/UI/LoginWindowCommands.cpp b/Source/RpmNextGenEditor/Private/UI/Commands/LoginWindowCommands.cpp similarity index 84% rename from Source/RpmNextGenEditor/Private/UI/LoginWindowCommands.cpp rename to Source/RpmNextGenEditor/Private/UI/Commands/LoginWindowCommands.cpp index b182c6e..48d9937 100644 --- a/Source/RpmNextGenEditor/Private/UI/LoginWindowCommands.cpp +++ b/Source/RpmNextGenEditor/Private/UI/Commands/LoginWindowCommands.cpp @@ -1,4 +1,4 @@ -#include "UI/LoginWindowCommands.h" +#include "UI/Commands/LoginWindowCommands.h" #define LOCTEXT_NAMESPACE "FRpmNextGenEditorModule" diff --git a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp new file mode 100644 index 0000000..84c8b8e --- /dev/null +++ b/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp @@ -0,0 +1,183 @@ +#include "UI/SCacheEditorWidget.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SSlider.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "EditorStyleSet.h" +#include "Widgets/Input/SNumericEntryBox.h" +#include "Widgets/Layout/SScrollBox.h" + +void SCacheEditorWidget::Construct(const FArguments& InArgs) +{ + ChildSlot + [ + SNew(SScrollBox) // Make the entire content scrollable + + SScrollBox::Slot() + [ + SNew(SVerticalBox) + + // Title/Label "Get Cache from Remote URL" + + SVerticalBox::Slot() + .Padding(5) + .AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString("Get Cache from Remote URL")) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 24)) + ] + + // Editable text field with label "Cache Url" + + SVerticalBox::Slot() + .Padding(5) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString("Cache URL:")) + ] + + SHorizontalBox::Slot() + .Padding(5, 0, 0, 0) + .FillWidth(1.0f) + [ + SNew(SBox) + .HeightOverride(30) // Set text box height + [ + SNew(SEditableTextBox) + .Text(FText::FromString("http://")) + .OnTextCommitted(this, &SCacheEditorWidget::OnCacheUrlTextCommitted) + ] + ] + ] + + // Button "Download Remote Cache" + + SVerticalBox::Slot() + .Padding(5) + .AutoHeight() + [ + SNew(SBox) + .HeightOverride(40) // Set button height + [ + SNew(SButton) + .Text(FText::FromString("Download Remote Cache")) + .OnClicked(this, &SCacheEditorWidget::OnDownloadRemoteCacheClicked) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + ] + ] + + // Integer Slider with label "Items per category" + + SVerticalBox::Slot() + .Padding(5) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString("Items per category:")) + ] + + SHorizontalBox::Slot() + .Padding(5, 0, 0, 0) + .FillWidth(1.0f) + [ + SNew(SNumericEntryBox) + .Value(this, &SCacheEditorWidget::GetItemsPerCategory) + .OnValueChanged(this, &SCacheEditorWidget::OnItemsPerCategoryChanged) + .AllowSpin(true) // Slider-like behavior + .MinValue(1) + .MaxValue(100) + ] + ] + + // Button "Generate offline cache" + + SVerticalBox::Slot() + .Padding(5) + .AutoHeight() + [ + SNew(SBox) + .HeightOverride(40) // Set button height + [ + SNew(SButton) + .Text(FText::FromString("Generate offline cache")) + .OnClicked(this, &SCacheEditorWidget::OnGenerateOfflineCacheClicked) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + ] + ] + + // Button "Extract Cache to local folder" + + SVerticalBox::Slot() + .Padding(5) + .AutoHeight() + [ + SNew(SBox) + .HeightOverride(40) // Set button height + [ + SNew(SButton) + .Text(FText::FromString("Extract Cache to local folder")) + .OnClicked(this, &SCacheEditorWidget::OnExtractCacheClicked) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + ] + ] + + // Button "Open Local Cache Folder" + + SVerticalBox::Slot() + .Padding(5) + .AutoHeight() + [ + SNew(SBox) + .HeightOverride(40) // Set button height + [ + SNew(SButton) + .Text(FText::FromString("Open Local Cache Folder")) + .OnClicked(this, &SCacheEditorWidget::OnOpenLocalCacheFolderClicked) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + ] + ] + ] + ]; +} + + +FReply SCacheEditorWidget::OnGenerateOfflineCacheClicked() +{ + // Handle generating the offline cache + return FReply::Handled(); +} + +FReply SCacheEditorWidget::OnExtractCacheClicked() +{ + // Handle extracting the cache + return FReply::Handled(); +} + +FReply SCacheEditorWidget::OnOpenLocalCacheFolderClicked() +{ + // Handle opening the local cache folder + return FReply::Handled(); +} + +FReply SCacheEditorWidget::OnDownloadRemoteCacheClicked() +{ + // Handle downloading the remote cache + return FReply::Handled(); +} + +void SCacheEditorWidget::OnItemsPerCategoryChanged(float NewValue) +{ + ItemsPerCategory = NewValue; + // Handle slider value change +} + +void SCacheEditorWidget::OnCacheUrlChanged(const FText& NewText) +{ + CacheUrl = NewText.ToString(); + // Handle cache URL text change +} diff --git a/Source/RpmNextGenEditor/Public/RpmNextGenEditor.h b/Source/RpmNextGenEditor/Public/RpmNextGenEditor.h index 96034cf..6bd1790 100644 --- a/Source/RpmNextGenEditor/Public/RpmNextGenEditor.h +++ b/Source/RpmNextGenEditor/Public/RpmNextGenEditor.h @@ -21,8 +21,10 @@ class RPMNEXTGENEDITOR_API FRpmNextGenEditorModule : public IModuleInterface void RegisterMenus(); void FillReadyPlayerMeMenu(UToolMenu* Menu); void OpenLoaderWindow(); + void OpenCacheEditorWindow(); TSharedRef OnSpawnLoaderWindow(const FSpawnTabArgs& SpawnTabArgs); - TSharedRef OnSpawnPluginTab(const class FSpawnTabArgs& SpawnTabArgs); - TSharedPtr PluginCommands; + TSharedRef OnSpawnCacheWindow(const FSpawnTabArgs& SpawnTabArgs); + TSharedRef OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs); + TSharedPtr PluginCommands; }; diff --git a/Source/RpmNextGenEditor/Public/UI/Commands/CacheWindowCommands.h b/Source/RpmNextGenEditor/Public/UI/Commands/CacheWindowCommands.h new file mode 100644 index 0000000..a592147 --- /dev/null +++ b/Source/RpmNextGenEditor/Public/UI/Commands/CacheWindowCommands.h @@ -0,0 +1,18 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Framework/Commands/Commands.h" + +class RPMNEXTGENEDITOR_API FCacheWindowCommands : public TCommands +{ +public: + FCacheWindowCommands() + : TCommands(TEXT("CacheWindow"), NSLOCTEXT("Contexts", "CacheWindow", "Cache Window Plugin"), NAME_None, FEditorStyle::GetStyleSetName()) + { + } + + virtual void RegisterCommands() override; + +public: + TSharedPtr OpenPluginWindow; +}; \ No newline at end of file diff --git a/Source/RpmNextGenEditor/Public/UI/LoaderWindowCommands.h b/Source/RpmNextGenEditor/Public/UI/Commands/LoaderWindowCommands.h similarity index 100% rename from Source/RpmNextGenEditor/Public/UI/LoaderWindowCommands.h rename to Source/RpmNextGenEditor/Public/UI/Commands/LoaderWindowCommands.h diff --git a/Source/RpmNextGenEditor/Public/UI/LoginWindowCommands.h b/Source/RpmNextGenEditor/Public/UI/Commands/LoginWindowCommands.h similarity index 93% rename from Source/RpmNextGenEditor/Public/UI/LoginWindowCommands.h rename to Source/RpmNextGenEditor/Public/UI/Commands/LoginWindowCommands.h index cb92740..86dc7de 100644 --- a/Source/RpmNextGenEditor/Public/UI/LoginWindowCommands.h +++ b/Source/RpmNextGenEditor/Public/UI/Commands/LoginWindowCommands.h @@ -2,7 +2,7 @@ #include "CoreMinimal.h" #include "Framework/Commands/Commands.h" -#include "LoginWindowStyle.h" +#include "UI/LoginWindowStyle.h" class RPMNEXTGENEDITOR_API FLoginWindowCommands: public TCommands { diff --git a/Source/RpmNextGenEditor/Public/UI/SCacheEditorWidget.h b/Source/RpmNextGenEditor/Public/UI/SCacheEditorWidget.h new file mode 100644 index 0000000..743d037 --- /dev/null +++ b/Source/RpmNextGenEditor/Public/UI/SCacheEditorWidget.h @@ -0,0 +1,44 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Widgets/SCompoundWidget.h" + +class SCacheEditorWidget : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SCacheEditorWidget) {} + SLATE_END_ARGS() + + /** Constructs this widget with InArgs */ + void Construct(const FArguments& InArgs); + +private: + // Callback functions for your buttons + FReply OnGenerateOfflineCacheClicked(); + FReply OnExtractCacheClicked(); + FReply OnOpenLocalCacheFolderClicked(); + FReply OnDownloadRemoteCacheClicked(); + + TOptional GetItemsPerCategory() const + { + return ItemsPerCategory; + } + + void OnItemsPerCategoryChanged(int32 NewValue) + { + ItemsPerCategory = NewValue; + } + + void OnCacheUrlTextCommitted(const FText& NewText, ETextCommit::Type CommitType) + { + CacheUrl = NewText.ToString(); + } + + // Slider value handling + float ItemsPerCategory = 10.0f; + void OnItemsPerCategoryChanged(float NewValue); + + // Cache URL handling + FString CacheUrl; + void OnCacheUrlChanged(const FText& NewText); +}; From 1d0a84a0bf52f0cef10160a01bf3c83c99488b32 Mon Sep 17 00:00:00 2001 From: Harrison Date: Thu, 29 Aug 2024 11:58:20 +0300 Subject: [PATCH 02/54] chore: refactor and WIP cache generator --- .../Private/Api/Assets/AssetApi.cpp | 29 +++- .../Private/Cache/CacheGenerator.cpp | 145 ++++++++++++++++++ .../Private/RpmPreviewLoaderComponent.cpp | 3 +- .../RpmNextGen/Public/Api/Assets/AssetApi.h | 7 +- .../Api/Assets/Models/AssetListResponse.h | 6 +- .../Api/Assets/Models/AssetTypeListRequest.h | 40 +++++ .../Api/Assets/Models/AssetTypeListResponse.h | 14 ++ .../RpmNextGen/Public/Cache/CacheGenerator.h | 49 ++++++ .../Private/RpmNextGenEditor.cpp | 2 +- .../Private/UI/SCacheEditorWidget.cpp | 105 +++++++------ ...rWidget.cpp => SCharacterLoaderWidget.cpp} | 2 +- .../Private/UI/SRpmDeveloperLoginWidget.cpp | 2 +- ...oaderWidget.h => SCharacterLoaderWidget.h} | 3 +- 13 files changed, 346 insertions(+), 61 deletions(-) create mode 100644 Source/RpmNextGen/Private/Cache/CacheGenerator.cpp create mode 100644 Source/RpmNextGen/Public/Api/Assets/Models/AssetTypeListRequest.h create mode 100644 Source/RpmNextGen/Public/Api/Assets/Models/AssetTypeListResponse.h create mode 100644 Source/RpmNextGen/Public/Cache/CacheGenerator.h rename Source/RpmNextGenEditor/Private/UI/{CharacterLoaderWidget.cpp => SCharacterLoaderWidget.cpp} (98%) rename Source/RpmNextGenEditor/Public/UI/{CharacterLoaderWidget.h => SCharacterLoaderWidget.h} (93%) diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp index b204b39..8b65a9a 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp @@ -3,17 +3,44 @@ #include "Api/Assets/Models/AssetListRequest.h" #include "Api/Assets/Models/AssetListResponse.h" +const FString FAssetApi::BaseModelType = TEXT("baseModel"); + FAssetApi::FAssetApi() { OnApiResponse.BindRaw(this, &FAssetApi::HandleListAssetResponse); -} + const URpmDeveloperSettings* Settings = GetMutableDefault(); + if(Settings->ApplicationId.IsEmpty()) + { + UE_LOG(LogTemp, Error, TEXT("Application ID is empty. Please set the Application ID in the Ready Player Me Developer Settings")); + } +} void FAssetApi::ListAssetsAsync(const FAssetListRequest& Request) { URpmDeveloperSettings* Settings = GetMutableDefault(); ApiBaseUrl = Settings->GetApiBaseUrl(); + if(Settings->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/types%s"), *ApiBaseUrl, *QueryString); + FApiRequest ApiRequest = FApiRequest(); + ApiRequest.Url = Url; + ApiRequest.Method = GET; + + DispatchRawWithAuth(ApiRequest); +} + +void FAssetApi::ListAssetTypesAsync(const FAssetListRequest& Request) +{ + URpmDeveloperSettings* Settings = GetMutableDefault(); + ApiBaseUrl = Settings->GetApiBaseUrl(); + if(Settings->ApplicationId.IsEmpty()) { UE_LOG(LogTemp, Error, TEXT("Application ID is empty")); diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp new file mode 100644 index 0000000..f20d46a --- /dev/null +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -0,0 +1,145 @@ +#include "Cache/CacheGenerator.h" +#include "HttpModule.h" +#include "RpmNextGen.h" +#include "Api/Assets/AssetApi.h" +#include "Api/Assets/Models/AssetListRequest.h" +#include "Interfaces/IHttpRequest.h" +#include "Interfaces/IHttpResponse.h" +#include "Misc/Paths.h" +#include "HAL/PlatformFilemanager.h" +#include "Misc/FileHelper.h" +#include "Misc/ScopeExit.h" +#include "Settings/RpmDeveloperSettings.h" + +const FString FCacheGenerator::CacheFolderPath = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/LocalCache"); +const FString FCacheGenerator::ZipFileName = TEXT("LocalCacheAssets.zip"); + +FCacheGenerator::FCacheGenerator() +{ + Http = &FHttpModule::Get(); +} + +void FCacheGenerator::DownloadRemoteCacheFromUrl(const FString& Url) +{ + TSharedRef Request = Http->CreateRequest(); + Request->SetURL(Url); + Request->SetVerb(TEXT("GET")); + + Request->OnProcessRequestComplete().BindRaw(this, &FCacheGenerator::OnDownloadRemoteCacheComplete); + Request->ProcessRequest(); +} + +void FCacheGenerator::GenerateLocalCache(int InItemsPerCategory) +{ + ItemsPerCategory = InItemsPerCategory; + FetchBaseModels(); +} + +void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful) +{ + if(bWasSuccessful && AssetListResponse.IsSuccess) + { + if (AssetListResponse.Data.Num() > 0 && AssetListResponse.Data[0].Type == FAssetApi::BaseModelType) + { + BaseModelAssets.Empty(); + BaseModelAssets.Append(AssetListResponse.Data); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Fetched %d Base models"), AssetListResponse.Data.Num()); + FetchAssetTypes(); + return; + } + + Assets.Append(AssetListResponse.Data); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Fetched %d assets"), AssetListResponse.Data.Num()); + return; + } + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to fetch assets")); + OnGenerateLocalCacheDelegate.ExecuteIfBound(false); + +} + +void FCacheGenerator::OnListAssetTypesResponse(const FAssetTypeListResponse& AssetListResponse, bool bWasSuccessful) +{ + if(bWasSuccessful && AssetListResponse.IsSuccess) + { + Assets.Append(AssetListResponse.Data); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Fetched %d asset types"), AssetListResponse.Data.Num()); + return; + } + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to fetch asset types")); + OnGenerateLocalCacheDelegate.ExecuteIfBound(false); +} + +void FCacheGenerator::OnDownloadRemoteCacheComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful) +{ + if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) + { + // Get the response data + const TArray& Data = Response->GetContent(); + + // Define the path to save the ZIP file + const FString SavePath = CacheFolderPath / TEXT("/") / ZipFileName; + + // Ensure the directory exists + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + const FString DirectoryPath = FPaths::GetPath(SavePath); + if (!PlatformFile.DirectoryExists(*DirectoryPath)) + { + PlatformFile.CreateDirectoryTree(*DirectoryPath); + } + + // Save the data as a .zip file + if (FFileHelper::SaveArrayToFile(Data, *SavePath)) + { + UE_LOG(LogReadyPlayerMe, Log, TEXT("Successfully saved the remote cache to: %s"), *SavePath); + OnDownloadRemoteCacheDelegate.ExecuteIfBound(true); + } + else + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to save the remote cache to: %s"), *SavePath); + OnDownloadRemoteCacheDelegate.ExecuteIfBound(false); + } + return; + } + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to download the remote cache")); + OnDownloadRemoteCacheDelegate.ExecuteIfBound(false); +} + +void FCacheGenerator::OnRequestCacheAssetsComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful) +{ + if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) + { + OnGenerateLocalCacheDelegate.ExecuteIfBound(true); + return; + } + OnGenerateLocalCacheDelegate.ExecuteIfBound(false); +} + +void FCacheGenerator::ExtractCache() +{ + +} + +void FCacheGenerator::FetchBaseModels() +{ + URpmDeveloperSettings* Settings = GetMutableDefault(); + FAssetListRequest AssetListRequest; + FAssetListQueryParams QueryParams; + QueryParams.ApplicationId = Settings->ApplicationId; + QueryParams.Type = FAssetApi::BaseModelType; + AssetListRequest.Params = QueryParams; + AssetApi->ListAssetsAsync(AssetListRequest); +} + +void FCacheGenerator::FetchAssetTypes() +{ + AssetApi->ListAssetTypesAsync(FAssetListRequest()); +} + +void FCacheGenerator::FetchAssetsForBaseModel(const FString& BaseModelID) +{ + URpmDeveloperSettings *Settings = GetMutableDefault(); + FAssetListQueryParams QueryParams; + QueryParams.Type = AssetType; + QueryParams.ApplicationId = Settings->ApplicationId; + FAssetListRequest AssetListRequest = FAssetListRequest(QueryParams); +} diff --git a/Source/RpmNextGen/Private/RpmPreviewLoaderComponent.cpp b/Source/RpmNextGen/Private/RpmPreviewLoaderComponent.cpp index 47c5159..55db13c 100644 --- a/Source/RpmNextGen/Private/RpmPreviewLoaderComponent.cpp +++ b/Source/RpmNextGen/Private/RpmPreviewLoaderComponent.cpp @@ -4,6 +4,7 @@ #include "RpmPreviewLoaderComponent.h" #include "RpmNextGen.h" +#include "Api/Assets/AssetApi.h" #include "Api/Assets/Models/Asset.h" #include "Api/Characters/CharacterApi.h" #include "Api/Characters/Models/CharacterCreateResponse.h" @@ -35,7 +36,7 @@ void URpmPreviewLoaderComponent::CreateCharacter() } FCharacterCreateRequest CharacterCreateRequest = FCharacterCreateRequest(); CharacterCreateRequest.Data.Assets = TMap(); - CharacterCreateRequest.Data.Assets.Add("baseModel", BaseModelId); + CharacterCreateRequest.Data.Assets.Add(FAssetApi::BaseModelType, BaseModelId); CharacterCreateRequest.Data.ApplicationId = AppId; CharacterApi->CreateAsync(CharacterCreateRequest); diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetApi.h b/Source/RpmNextGen/Public/Api/Assets/AssetApi.h index a8d7548..b4c7801 100644 --- a/Source/RpmNextGen/Public/Api/Assets/AssetApi.h +++ b/Source/RpmNextGen/Public/Api/Assets/AssetApi.h @@ -1,22 +1,25 @@ #pragma once #include "Api/Common/WebApiWithAuth.h" +#include "Models/AssetTypeListResponse.h" struct FAssetListRequest; struct FAssetListResponse; DECLARE_DELEGATE_TwoParams(FOnListAssetsResponse, const FAssetListResponse&, bool); -DECLARE_DELEGATE_TwoParams(FOnListAssetTypeResponse, const FAssetListResponse&, bool); +DECLARE_DELEGATE_TwoParams(FOnListAssetTypeResponse, const FAssetTypeListResponse&, bool); class RPMNEXTGEN_API FAssetApi : public FWebApiWithAuth { public: FAssetApi(); void ListAssetsAsync(const FAssetListRequest& Request); + void ListAssetTypesAsync(const FAssetListRequest& Request); FOnListAssetsResponse OnListAssetsResponse; FOnListAssetTypeResponse OnListAssetTypeResponse; + static const FString BaseModelType; private: void HandleListAssetResponse(FString Response, bool bWasSuccessful); void HandleListAssetTypeResponse(FString Response, bool bWasSuccessful); - FString ApiBaseUrl; + }; diff --git a/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h index d31cd77..f6ade59 100644 --- a/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h +++ b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h @@ -5,13 +5,11 @@ #include "AssetListResponse.generated.h" USTRUCT(BlueprintType) -struct RPMNEXTGEN_API FAssetListResponse +struct RPMNEXTGEN_API FAssetListResponse : public FApiResponse { 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/Assets/Models/AssetTypeListRequest.h b/Source/RpmNextGen/Public/Api/Assets/Models/AssetTypeListRequest.h new file mode 100644 index 0000000..b2720ce --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Assets/Models/AssetTypeListRequest.h @@ -0,0 +1,40 @@ +#pragma once + +#include "CoreMinimal.h" +#include "AssetTypeListRequest.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FAssetTypeListQueryParams +{ + 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 FAssetTypeListRequest +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FAssetTypeListQueryParams Params; + + // Default constructor + FAssetTypeListRequest() + { + } + + // Constructor that accepts FAssetListQueryParams + FAssetTypeListRequest(const FAssetTypeListQueryParams& InParams) + : Params(InParams) + { + } +}; diff --git a/Source/RpmNextGen/Public/Api/Assets/Models/AssetTypeListResponse.h b/Source/RpmNextGen/Public/Api/Assets/Models/AssetTypeListResponse.h new file mode 100644 index 0000000..932ae01 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Assets/Models/AssetTypeListResponse.h @@ -0,0 +1,14 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Common/Models/ApiResponse.h" +#include "AssetTypeListResponse.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FAssetTypeListResponse : public FApiResponse +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "data")) + TArray Data; +}; diff --git a/Source/RpmNextGen/Public/Cache/CacheGenerator.h b/Source/RpmNextGen/Public/Cache/CacheGenerator.h new file mode 100644 index 0000000..f63acec --- /dev/null +++ b/Source/RpmNextGen/Public/Cache/CacheGenerator.h @@ -0,0 +1,49 @@ +#pragma once +#include "Api/Assets/Models/AssetListResponse.h" + +struct FAssetTypeListResponse; +class FAssetApi; +struct FAsset; +class IHttpResponse; +class IHttpRequest; +class FHttpModule; + +DECLARE_DELEGATE_OneParam(FOnGenerateLocalCache, bool); +DECLARE_DELEGATE_OneParam(FOnDownloadRemoteCache, bool); + +class RPMNEXTGEN_API FCacheGenerator +{ +public: + FCacheGenerator(); + void DownloadRemoteCacheFromUrl(const FString& Url); + void GenerateLocalCache(int InItemsPerCategory); + void ExtractCache(); + + FOnDownloadRemoteCache OnDownloadRemoteCacheDelegate; + FOnGenerateLocalCache OnGenerateLocalCacheDelegate; +protected: + void FetchBaseModels(); + void FetchAssetTypes(); + void FetchAssetsForBaseModel(const FString& BaseModelID); + + virtual void OnRequestCacheAssetsComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful); + virtual void OnDownloadRemoteCacheComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful); + + TArray Assets = TArray(); + TSharedPtr AssetApi; + + TArray BaseModelAssets; + TArray AssetTypes; + TMap> BaseModelAssetsMap; + int32 CurrentBaseModelIndex; + +private: + + void OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful); + void OnListAssetTypesResponse(const FAssetTypeListResponse& AssetTypeListResponse, bool bWasSuccessful); + static const FString CacheFolderPath; + static const FString ZipFileName; + int ItemsPerCategory; + + FHttpModule* Http; +}; diff --git a/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp b/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp index b1dc1bb..ca788b8 100644 --- a/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp +++ b/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp @@ -1,7 +1,7 @@ // Copyright Epic Games, Inc. All Rights Reserved. #include "RpmNextGenEditor.h" -#include "UI/CharacterLoaderWidget.h" +#include "UI/SCharacterLoaderWidget.h" #include "UI/Commands/LoaderWindowCommands.h" #include "UI/Commands/LoginWindowCommands.h" #include "UI/SRpmDeveloperLoginWidget.h" diff --git a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp index 84c8b8e..61ca310 100644 --- a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp @@ -1,6 +1,5 @@ #include "UI/SCacheEditorWidget.h" #include "Widgets/Input/SButton.h" -#include "Widgets/Input/SSlider.h" #include "Widgets/Input/SEditableTextBox.h" #include "EditorStyleSet.h" #include "Widgets/Input/SNumericEntryBox.h" @@ -12,62 +11,19 @@ void SCacheEditorWidget::Construct(const FArguments& InArgs) [ SNew(SScrollBox) // Make the entire content scrollable + SScrollBox::Slot() + .Padding(10) [ SNew(SVerticalBox) - // Title/Label "Get Cache from Remote URL" + // Title/Label "Local Cache Generator" + SVerticalBox::Slot() .Padding(5) .AutoHeight() [ SNew(STextBlock) - .Text(FText::FromString("Get Cache from Remote URL")) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 24)) + .Text(FText::FromString("Local Cache Generator")) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 16)) ] - - // Editable text field with label "Cache Url" - + SVerticalBox::Slot() - .Padding(5) - .AutoHeight() - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - [ - SNew(STextBlock) - .Text(FText::FromString("Cache URL:")) - ] - + SHorizontalBox::Slot() - .Padding(5, 0, 0, 0) - .FillWidth(1.0f) - [ - SNew(SBox) - .HeightOverride(30) // Set text box height - [ - SNew(SEditableTextBox) - .Text(FText::FromString("http://")) - .OnTextCommitted(this, &SCacheEditorWidget::OnCacheUrlTextCommitted) - ] - ] - ] - - // Button "Download Remote Cache" - + SVerticalBox::Slot() - .Padding(5) - .AutoHeight() - [ - SNew(SBox) - .HeightOverride(40) // Set button height - [ - SNew(SButton) - .Text(FText::FromString("Download Remote Cache")) - .OnClicked(this, &SCacheEditorWidget::OnDownloadRemoteCacheClicked) - .HAlign(HAlign_Center) - .VAlign(VAlign_Center) - ] - ] - // Integer Slider with label "Items per category" + SVerticalBox::Slot() .Padding(5) @@ -141,6 +97,59 @@ void SCacheEditorWidget::Construct(const FArguments& InArgs) .VAlign(VAlign_Center) ] ] + + // Title/Label "Remote Cache Downloader" + + SVerticalBox::Slot() + .Padding(5) + .AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString("Remote Cache Downloader")) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 16)) + ] + + // Editable text field with label "Cache Url" + + SVerticalBox::Slot() + .Padding(5) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString("Cache URL:")) + ] + + SHorizontalBox::Slot() + .Padding(5, 0, 0, 0) + .FillWidth(1.0f) + [ + SNew(SBox) + .HeightOverride(30) // Set text box height + [ + SNew(SEditableTextBox) + .Text(FText::FromString("http://")) + .OnTextCommitted(this, &SCacheEditorWidget::OnCacheUrlTextCommitted) + ] + ] + ] + + // Button "Download Remote Cache" + + SVerticalBox::Slot() + .Padding(5) + .AutoHeight() + [ + SNew(SBox) + .HeightOverride(40) // Set button height + [ + SNew(SButton) + .Text(FText::FromString("Download Remote Cache")) + .OnClicked(this, &SCacheEditorWidget::OnDownloadRemoteCacheClicked) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + ] + ] ] ]; } diff --git a/Source/RpmNextGenEditor/Private/UI/CharacterLoaderWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SCharacterLoaderWidget.cpp similarity index 98% rename from Source/RpmNextGenEditor/Private/UI/CharacterLoaderWidget.cpp rename to Source/RpmNextGenEditor/Private/UI/SCharacterLoaderWidget.cpp index f300345..5d40a8d 100644 --- a/Source/RpmNextGenEditor/Private/UI/CharacterLoaderWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SCharacterLoaderWidget.cpp @@ -1,4 +1,4 @@ -#include "UI/CharacterLoaderWidget.h" +#include "UI/SCharacterLoaderWidget.h" #include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Input/SButton.h" diff --git a/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp index 6b93425..880abeb 100644 --- a/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp @@ -491,7 +491,7 @@ void SRpmDeveloperLoginWidget::LoadBaseModelList() FAssetListRequest Request = FAssetListRequest(); FAssetListQueryParams Params = FAssetListQueryParams(); Params.ApplicationId = RpmSettings->ApplicationId; - Params.Type = "baseModel"; + Params.Type = FAssetApi::BaseModelType; Request.Params = Params; AssetApi->ListAssetsAsync(Request); } diff --git a/Source/RpmNextGenEditor/Public/UI/CharacterLoaderWidget.h b/Source/RpmNextGenEditor/Public/UI/SCharacterLoaderWidget.h similarity index 93% rename from Source/RpmNextGenEditor/Public/UI/CharacterLoaderWidget.h rename to Source/RpmNextGenEditor/Public/UI/SCharacterLoaderWidget.h index b4a61ce..8057f15 100644 --- a/Source/RpmNextGenEditor/Public/UI/CharacterLoaderWidget.h +++ b/Source/RpmNextGenEditor/Public/UI/SCharacterLoaderWidget.h @@ -2,7 +2,6 @@ #include "CoreMinimal.h" #include "EditorAssetLoader.h" -#include "Api/Assets/AssetLoader.h" #include "Widgets/SCompoundWidget.h" #include "Widgets/DeclarativeSyntaxSupport.h" @@ -35,6 +34,6 @@ class SCharacterLoaderWidget : public SCompoundWidget TSharedPtr PathTextBox; // Store the selected skeleton - USkeleton* SelectedSkeleton; + USkeleton* SelectedSkeleton = nullptr; FString GetCurrentSkeletonPath() const; }; From 5b6e760316ea9401daa90cd0d054f83723793a5d Mon Sep 17 00:00:00 2001 From: Harrison Date: Thu, 29 Aug 2024 15:41:47 +0300 Subject: [PATCH 03/54] chore: minor fixes and added pagination --- .../Private/Api/Assets/AssetApi.cpp | 4 ++- .../RpmNextGen/Public/Api/Assets/AssetApi.h | 3 +- .../Api/Assets/Models/AssetListRequest.h | 11 ++++++-- .../Api/Assets/Models/AssetTypeListRequest.h | 28 ++++++++++++++++--- .../Api/Common/Models/PaginationQueryParams.h | 19 +++++++++++++ 5 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 Source/RpmNextGen/Public/Api/Common/Models/PaginationQueryParams.h diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp index 8b65a9a..3fa41e5 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp @@ -2,7 +2,9 @@ #include "Settings/RpmDeveloperSettings.h" #include "Api/Assets/Models/AssetListRequest.h" #include "Api/Assets/Models/AssetListResponse.h" +#include "Api/Assets/Models/AssetTypeListRequest.h" +struct FAssetTypeListRequest; const FString FAssetApi::BaseModelType = TEXT("baseModel"); FAssetApi::FAssetApi() @@ -36,7 +38,7 @@ void FAssetApi::ListAssetsAsync(const FAssetListRequest& Request) DispatchRawWithAuth(ApiRequest); } -void FAssetApi::ListAssetTypesAsync(const FAssetListRequest& Request) +void FAssetApi::ListAssetTypesAsync(const FAssetTypeListRequest& Request) { URpmDeveloperSettings* Settings = GetMutableDefault(); ApiBaseUrl = Settings->GetApiBaseUrl(); diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetApi.h b/Source/RpmNextGen/Public/Api/Assets/AssetApi.h index b4c7801..6a62795 100644 --- a/Source/RpmNextGen/Public/Api/Assets/AssetApi.h +++ b/Source/RpmNextGen/Public/Api/Assets/AssetApi.h @@ -2,6 +2,7 @@ #include "Api/Common/WebApiWithAuth.h" #include "Models/AssetTypeListResponse.h" +struct FAssetTypeListRequest; struct FAssetListRequest; struct FAssetListResponse; @@ -13,7 +14,7 @@ class RPMNEXTGEN_API FAssetApi : public FWebApiWithAuth public: FAssetApi(); void ListAssetsAsync(const FAssetListRequest& Request); - void ListAssetTypesAsync(const FAssetListRequest& Request); + void ListAssetTypesAsync(const FAssetTypeListRequest& Request); FOnListAssetsResponse OnListAssetsResponse; FOnListAssetTypeResponse OnListAssetTypeResponse; static const FString BaseModelType; diff --git a/Source/RpmNextGen/Public/Api/Assets/Models/AssetListRequest.h b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListRequest.h index 982517c..2fda87e 100644 --- a/Source/RpmNextGen/Public/Api/Assets/Models/AssetListRequest.h +++ b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListRequest.h @@ -1,10 +1,11 @@ #pragma once #include "CoreMinimal.h" +#include "Api/Common/Models/PaginationQueryParams.h" #include "AssetListRequest.generated.h" USTRUCT(BlueprintType) -struct RPMNEXTGEN_API FAssetListQueryParams +struct RPMNEXTGEN_API FAssetListQueryParams : public FPaginationQueryParams { GENERATED_BODY() @@ -17,10 +18,12 @@ struct RPMNEXTGEN_API FAssetListQueryParams UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "excludeTypes")) FString ExcludeTypes; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "characterModelAssetId")) + FString CharacterModelAssetId; }; USTRUCT(BlueprintType) -struct RPMNEXTGEN_API FAssetListRequest +struct RPMNEXTGEN_API FAssetListRequest { GENERATED_BODY() @@ -57,6 +60,10 @@ inline FString FAssetListRequest::BuildQueryString() const { QueryString += TEXT("excludeTypes=") + Params.ExcludeTypes + TEXT("&"); } + if (!Params.CharacterModelAssetId.IsEmpty()) + { + QueryString += TEXT("characterModelAssetId=") + Params.CharacterModelAssetId + TEXT("&"); + } QueryString.RemoveFromEnd(TEXT("&")); return QueryString; } diff --git a/Source/RpmNextGen/Public/Api/Assets/Models/AssetTypeListRequest.h b/Source/RpmNextGen/Public/Api/Assets/Models/AssetTypeListRequest.h index b2720ce..c91d397 100644 --- a/Source/RpmNextGen/Public/Api/Assets/Models/AssetTypeListRequest.h +++ b/Source/RpmNextGen/Public/Api/Assets/Models/AssetTypeListRequest.h @@ -16,7 +16,6 @@ struct RPMNEXTGEN_API FAssetTypeListQueryParams UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "excludeTypes")) FString ExcludeTypes; - }; USTRUCT(BlueprintType) @@ -27,14 +26,35 @@ struct RPMNEXTGEN_API FAssetTypeListRequest UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") FAssetTypeListQueryParams Params; - // Default constructor + FAssetTypeListRequest() { } - - // Constructor that accepts FAssetListQueryParams + FAssetTypeListRequest(const FAssetTypeListQueryParams& InParams) : Params(InParams) { } + + FString BuildQueryString() const; }; + +inline FString FAssetTypeListRequest::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; +} \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Api/Common/Models/PaginationQueryParams.h b/Source/RpmNextGen/Public/Api/Common/Models/PaginationQueryParams.h new file mode 100644 index 0000000..20613bf --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Common/Models/PaginationQueryParams.h @@ -0,0 +1,19 @@ +#pragma once + +#include "CoreMinimal.h" +#include "PaginationQueryParams.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FPaginationQueryParams +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "limit")) + int Limit; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "page")) + int Page; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "order")) + FString Order; +}; From 53a2c6aeca62847756e2a5d47fc5ddb92e1de01f Mon Sep 17 00:00:00 2001 From: Harrison Date: Thu, 29 Aug 2024 15:42:56 +0300 Subject: [PATCH 04/54] feat: WIP request logic --- .../Private/Cache/CacheGenerator.cpp | 68 ++++++++++++++++--- .../RpmNextGen/Public/Cache/CacheGenerator.h | 7 +- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index f20d46a..2345494 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -3,6 +3,7 @@ #include "RpmNextGen.h" #include "Api/Assets/AssetApi.h" #include "Api/Assets/Models/AssetListRequest.h" +#include "Api/Assets/Models/AssetTypeListRequest.h" #include "Interfaces/IHttpRequest.h" #include "Interfaces/IHttpResponse.h" #include "Misc/Paths.h" @@ -15,6 +16,7 @@ const FString FCacheGenerator::CacheFolderPath = FPaths::ProjectContentDir() / T const FString FCacheGenerator::ZipFileName = TEXT("LocalCacheAssets.zip"); FCacheGenerator::FCacheGenerator() + : CurrentBaseModelIndex(0), ItemsPerCategory(10) { Http = &FHttpModule::Get(); } @@ -35,6 +37,11 @@ void FCacheGenerator::GenerateLocalCache(int InItemsPerCategory) FetchBaseModels(); } +void FCacheGenerator::ProcessNextRequest() +{ + +} + void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful) { if(bWasSuccessful && AssetListResponse.IsSuccess) @@ -47,9 +54,32 @@ void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListRe FetchAssetTypes(); return; } - - Assets.Append(AssetListResponse.Data); UE_LOG(LogReadyPlayerMe, Log, TEXT("Fetched %d assets"), AssetListResponse.Data.Num()); + + if(AssetListResponse.Data.Num() > 0) + { + FAsset BaseModelID = BaseModelAssets[CurrentBaseModelIndex]; + if (!BaseModelAssetsMap.Contains(BaseModelID.Id)) + { + BaseModelAssetsMap.Add(BaseModelID.Id, AssetListResponse.Data); + } + else + { + BaseModelAssetsMap[BaseModelID.Id].Append(AssetListResponse.Data); + } + } + + // // Check if more asset types need to be requested for the current base model + // CurrentBaseModelIndex++; + // if (CurrentBaseModelIndex < BaseModelAssets.Num()) + // { + // FetchAssetsForBaseModel(BaseModelAssets[CurrentBaseModelIndex].Id); + // } + // else + // { + // UE_LOG(LogTemp, Log, TEXT("All assets have been fetched successfully.")); + // // Final processing or callback can go here + // } return; } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to fetch assets")); @@ -57,12 +87,24 @@ void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListRe } +void FCacheGenerator::FetchAssetsForEachBaseModel() +{ + for (FAsset& BaseModel : BaseModelAssets) + { + for(FString AssetType : AssetTypes) + { + FetchAssetsForBaseModel(BaseModel.Id, AssetType); + } + } +} + void FCacheGenerator::OnListAssetTypesResponse(const FAssetTypeListResponse& AssetListResponse, bool bWasSuccessful) { if(bWasSuccessful && AssetListResponse.IsSuccess) { - Assets.Append(AssetListResponse.Data); UE_LOG(LogReadyPlayerMe, Log, TEXT("Fetched %d asset types"), AssetListResponse.Data.Num()); + AssetTypes.Append(AssetListResponse.Data); + FetchAssetsForEachBaseModel(); return; } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to fetch asset types")); @@ -122,8 +164,8 @@ void FCacheGenerator::ExtractCache() void FCacheGenerator::FetchBaseModels() { URpmDeveloperSettings* Settings = GetMutableDefault(); - FAssetListRequest AssetListRequest; - FAssetListQueryParams QueryParams; + FAssetListRequest AssetListRequest = FAssetListRequest(); + FAssetListQueryParams QueryParams = FAssetListQueryParams(); QueryParams.ApplicationId = Settings->ApplicationId; QueryParams.Type = FAssetApi::BaseModelType; AssetListRequest.Params = QueryParams; @@ -131,15 +173,23 @@ void FCacheGenerator::FetchBaseModels() } void FCacheGenerator::FetchAssetTypes() -{ - AssetApi->ListAssetTypesAsync(FAssetListRequest()); +{ + URpmDeveloperSettings* Settings = GetMutableDefault(); + FAssetTypeListRequest AssetListRequest; + FAssetTypeListQueryParams QueryParams = FAssetTypeListQueryParams(); + QueryParams.ApplicationId = Settings->ApplicationId; + AssetListRequest.Params = QueryParams; + AssetApi->ListAssetTypesAsync(AssetListRequest); } -void FCacheGenerator::FetchAssetsForBaseModel(const FString& BaseModelID) +void FCacheGenerator::FetchAssetsForBaseModel(const FString& BaseModelID, const FString& AssetType) { URpmDeveloperSettings *Settings = GetMutableDefault(); - FAssetListQueryParams QueryParams; + FAssetListQueryParams QueryParams = FAssetListQueryParams(); QueryParams.Type = AssetType; QueryParams.ApplicationId = Settings->ApplicationId; + QueryParams.CharacterModelAssetId = BaseModelID; + QueryParams.Limit = ItemsPerCategory; FAssetListRequest AssetListRequest = FAssetListRequest(QueryParams); + AssetApi->ListAssetsAsync(AssetListRequest); } diff --git a/Source/RpmNextGen/Public/Cache/CacheGenerator.h b/Source/RpmNextGen/Public/Cache/CacheGenerator.h index f63acec..f45a4a5 100644 --- a/Source/RpmNextGen/Public/Cache/CacheGenerator.h +++ b/Source/RpmNextGen/Public/Cache/CacheGenerator.h @@ -24,12 +24,11 @@ class RPMNEXTGEN_API FCacheGenerator protected: void FetchBaseModels(); void FetchAssetTypes(); - void FetchAssetsForBaseModel(const FString& BaseModelID); + void FetchAssetsForBaseModel(const FString& BaseModelID, const FString& AssetType); virtual void OnRequestCacheAssetsComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful); virtual void OnDownloadRemoteCacheComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful); - - TArray Assets = TArray(); + TSharedPtr AssetApi; TArray BaseModelAssets; @@ -38,8 +37,10 @@ class RPMNEXTGEN_API FCacheGenerator int32 CurrentBaseModelIndex; private: + void ProcessNextRequest(); void OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful); + void FetchAssetsForEachBaseModel(); void OnListAssetTypesResponse(const FAssetTypeListResponse& AssetTypeListResponse, bool bWasSuccessful); static const FString CacheFolderPath; static const FString ZipFileName; From 177c69a2d779e806cca8af4728a600a204d95ab3 Mon Sep 17 00:00:00 2001 From: Harrison Date: Tue, 3 Sep 2024 12:22:38 +0300 Subject: [PATCH 05/54] feat: finish first pass on cache generator --- .../Private/Api/Assets/AssetApi.cpp | 94 ++++++++++--- .../Private/Cache/CacheGenerator.cpp | 127 +++++++++++++----- .../RpmNextGen/Public/Cache/CacheGenerator.h | 22 +-- .../Private/UI/SCacheEditorWidget.cpp | 74 +++++++++- .../Public/UI/SCacheEditorWidget.h | 7 +- 5 files changed, 257 insertions(+), 67 deletions(-) diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp index 581b2a0..beddb17 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp @@ -58,27 +58,77 @@ void FAssetApi::ListAssetTypesAsync(const FAssetTypeListRequest& Request) void FAssetApi::HandleListAssetResponse(FString Response, bool bWasSuccessful) { - if(bWasSuccessful) - { - FAssetListResponse AssetListResponse = FAssetListResponse(); - FAssetTypeListResponse AssetTypeListResponse = FAssetTypeListResponse(); - if(FJsonObjectConverter::JsonObjectStringToUStruct(Response, &AssetListResponse, 0, 0)) - { - OnListAssetsResponse.ExecuteIfBound(AssetListResponse, true); - return; - } - if(FJsonObjectConverter::JsonObjectStringToUStruct(Response, &AssetTypeListResponse, 0, 0)) - { - OnListAssetTypeResponse.ExecuteIfBound(AssetTypeListResponse, true); - return; - } - UE_LOG(LogTemp, Error, TEXT("Failed to parse API from response %s"), *Response);; - } - else - { - UE_LOG(LogTemp, Error, TEXT("API Response was unsuccessful")); - } - OnListAssetsResponse.ExecuteIfBound(FAssetListResponse(), false); - OnListAssetTypeResponse.ExecuteIfBound(FAssetTypeListResponse(), false); + if (bWasSuccessful) + { + #if ENGINE_MAJOR_VERSION < 5 || ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 1 // Manual parsing for Unreal Engine 5.0 and earlier + + TSharedPtr JsonObject; + TSharedRef> Reader = TJsonReaderFactory<>::Create(Response); + + if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid()) + { + // Check if the "data" field is an array + const TArray>* DataArray; + if (JsonObject->TryGetArrayField(TEXT("data"), DataArray)) + { + if (DataArray->Num() > 0) + { + // Check the type of the first element to determine the response type + const TSharedPtr& FirstElement = (*DataArray)[0]; + + if (FirstElement->Type == EJson::Object) + { + // Assume this is an FAssetListResponse + FAssetListResponse AssetListResponse; + if (FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), &AssetListResponse, 0, 0)) + { + OnListAssetsResponse.ExecuteIfBound(AssetListResponse, true); + return; + } + } + else if (FirstElement->Type == EJson::String) + { + // Assume this is an FAssetTypeListResponse + FAssetTypeListResponse AssetTypeListResponse; + if (FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), &AssetTypeListResponse, 0, 0)) + { + OnListAssetTypeResponse.ExecuteIfBound(AssetTypeListResponse, true); + return; + } + } + } + } + } + + UE_LOG(LogTemp, Error, TEXT("Failed to parse JSON into known structs from response: %s"), *Response); + + #else + + // Use EStructJsonFlags::SkipMissingProperties for Unreal Engine 5.1 and later + FAssetListResponse AssetListResponse = FAssetListResponse(); + FAssetTypeListResponse AssetTypeListResponse = FAssetTypeListResponse(); + if (FJsonObjectConverter::JsonObjectStringToUStruct(Response, &AssetListResponse, 0, EStructJsonFlags::SkipMissingProperties)) + { + OnListAssetsResponse.ExecuteIfBound(AssetListResponse, true); + return; + } + if (FJsonObjectConverter::JsonObjectStringToUStruct(Response, &AssetTypeListResponse, 0, EStructJsonFlags::SkipMissingProperties)) + { + OnListAssetTypeResponse.ExecuteIfBound(AssetTypeListResponse, true); + return; + } + + UE_LOG(LogTemp, Error, TEXT("Failed to parse API from response %s"), *Response); + + #endif + } + else + { + UE_LOG(LogTemp, Error, TEXT("API Response was unsuccessful")); + } + + // If all parsing attempts fail, execute with default/empty responses + OnListAssetsResponse.ExecuteIfBound(FAssetListResponse(), false); + OnListAssetTypeResponse.ExecuteIfBound(FAssetTypeListResponse(), false); } diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index 1a34d8b..59191a0 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -17,7 +17,7 @@ const FString FCacheGenerator::CacheFolderPath = FPaths::ProjectContentDir() / T const FString FCacheGenerator::ZipFileName = TEXT("LocalCacheAssets.zip"); FCacheGenerator::FCacheGenerator() - : CurrentBaseModelIndex(0), ItemsPerCategory(10) + : CurrentBaseModelIndex(0), MaxItemsPerCategory(10) { Http = &FHttpModule::Get(); AssetApi = MakeUnique(); @@ -42,18 +42,96 @@ void FCacheGenerator::DownloadRemoteCacheFromUrl(const FString& Url) void FCacheGenerator::GenerateLocalCache(int InItemsPerCategory) { - ItemsPerCategory = InItemsPerCategory; + MaxItemsPerCategory = InItemsPerCategory; FetchBaseModels(); } -void FCacheGenerator::ProcessNextRequest() +void FCacheGenerator::LoadAndStoreAssets() { - + int RefittedAssetCount = 0; + for (auto BaseModel : BaseModelAssets) + { + for (auto Pairs : BaseModelAssetsMap) + { + RefittedAssetCount += Pairs.Value.Num(); + } + } + RequiredAssetDownloadRequest = 2 * RefittedAssetCount + 2 * BaseModelAssets.Num(); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Total assets to download: %d. Total refitted assets to fetch: %d"), RequiredAssetDownloadRequest, RequiredRefittedAssetRequests); + for (auto BaseModel : BaseModelAssets) + { + const FString BaseModeFolder = CacheFolderPath / BaseModel.Id; + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + const FString DirectoryPath = FPaths::GetPath(BaseModeFolder); + if (!PlatformFile.DirectoryExists(*DirectoryPath)) + { + PlatformFile.CreateDirectoryTree(*DirectoryPath); + } + + LoadAndStoreAssetFromUrl(BaseModel.GlbUrl, BaseModeFolder / FString::Printf( TEXT("%s.glb"), *BaseModel.Id)); + LoadAndStoreAssetFromUrl(BaseModel.IconUrl, BaseModeFolder / FString::Printf( TEXT("%s.png"), *BaseModel.Id)); + for (auto Pairs : BaseModelAssetsMap) + { + for (auto AssetForBaseModel : Pairs.Value) + { + LoadAndStoreAssetFromUrl(AssetForBaseModel.GlbUrl, BaseModeFolder / FString::Printf( TEXT("%s.glb"), *AssetForBaseModel.Id)); + LoadAndStoreAssetFromUrl(AssetForBaseModel.IconUrl, BaseModeFolder / FString::Printf( TEXT("%s.png"), *AssetForBaseModel.Id)); + } + } + } +} + +void FCacheGenerator::LoadAndStoreAssetFromUrl(const FString& AssetFileUrl, const FString& FilePath) +{ + TSharedRef Request = Http->CreateRequest(); + Request->SetURL(AssetFileUrl); + Request->SetVerb(TEXT("GET")); + Request->OnProcessRequestComplete().BindLambda([this, FilePath](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) + { + this->OnAssetDataLoaded(Request, Response, bWasSuccessful, FilePath); + }); + Request->ProcessRequest(); +} + +void FCacheGenerator::OnAssetDataLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FString& FilePath) +{ + if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) + { + // Get the response data + const TArray& Data = Response->GetContent(); + + // Save the data as a .zip file + if (FFileHelper::SaveArrayToFile(Data, *FilePath)) + { + UE_LOG(LogReadyPlayerMe, Log, TEXT("Successfully saved asset in local cache to: %s"), *FilePath); + } + else + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to saved asset local cache to: %s"), *FilePath); + + } + AssetDownloadRequestsCompleted++; + if(AssetDownloadRequestsCompleted >= RequiredAssetDownloadRequest) + { + UE_LOG(LogReadyPlayerMe, Log, TEXT("OnLocalCacheGenerated Total assets to download: %d. Asset download requests completed: %d"), RequiredAssetDownloadRequest, AssetDownloadRequestsCompleted); + + OnLocalCacheGenerated.ExecuteIfBound(true); + } + return; + } + AssetDownloadRequestsCompleted++; + if(AssetDownloadRequestsCompleted >= RequiredAssetDownloadRequest) + { + UE_LOG(LogReadyPlayerMe, Log, TEXT("OnLocalCacheGenerated Total assets to download: %d. Asset download requests completed: %d"), RequiredAssetDownloadRequest, AssetDownloadRequestsCompleted); + OnLocalCacheGenerated.ExecuteIfBound(true); + } + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to download the remote cache")); + } void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful) { - UE_LOG(LogReadyPlayerMe, Log, TEXT("OnListAssetsResponse ") ); + UE_LOG(LogReadyPlayerMe, Log, TEXT("OnListAssetsResponse") ); if(bWasSuccessful && AssetListResponse.IsSuccess) { UE_LOG(LogReadyPlayerMe, Log, TEXT("Success ") ); @@ -79,27 +157,24 @@ void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListRe BaseModelAssetsMap[BaseModelID.Id].Append(AssetListResponse.Data); } } - - // // Check if more asset types need to be requested for the current base model - // CurrentBaseModelIndex++; - // if (CurrentBaseModelIndex < BaseModelAssets.Num()) - // { - // FetchAssetsForBaseModel(BaseModelAssets[CurrentBaseModelIndex].Id); - // } - // else - // { - // UE_LOG(LogTemp, Log, TEXT("All assets have been fetched successfully.")); - // // Final processing or callback can go here - // } + RefittedAssetRequestsCompleted++; + if(RefittedAssetRequestsCompleted >= RequiredRefittedAssetRequests) + { + OnCacheDataLoaded.ExecuteIfBound(true); + } return; } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to fetch assets")); - OnGenerateLocalCacheDelegate.ExecuteIfBound(false); - + RefittedAssetRequestsCompleted++; + if(RefittedAssetRequestsCompleted >= RequiredRefittedAssetRequests) + { + OnCacheDataLoaded.ExecuteIfBound(true); + } } void FCacheGenerator::FetchAssetsForEachBaseModel() { + RequiredRefittedAssetRequests = AssetTypes.Num() * BaseModelAssets.Num(); for (FAsset& BaseModel : BaseModelAssets) { for(FString AssetType : AssetTypes) @@ -119,7 +194,7 @@ void FCacheGenerator::OnListAssetTypesResponse(const FAssetTypeListResponse& Ass return; } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to fetch asset types")); - OnGenerateLocalCacheDelegate.ExecuteIfBound(false); + OnCacheDataLoaded.ExecuteIfBound(false); } void FCacheGenerator::OnDownloadRemoteCacheComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful) @@ -157,16 +232,6 @@ void FCacheGenerator::OnDownloadRemoteCacheComplete(TSharedPtr Req OnDownloadRemoteCacheDelegate.ExecuteIfBound(false); } -void FCacheGenerator::OnRequestCacheAssetsComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful) -{ - if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) - { - OnGenerateLocalCacheDelegate.ExecuteIfBound(true); - return; - } - OnGenerateLocalCacheDelegate.ExecuteIfBound(false); -} - void FCacheGenerator::ExtractCache() { @@ -202,7 +267,7 @@ void FCacheGenerator::FetchAssetsForBaseModel(const FString& BaseModelID, const QueryParams.Type = AssetType; QueryParams.ApplicationId = Settings->ApplicationId; QueryParams.CharacterModelAssetId = BaseModelID; - QueryParams.Limit = ItemsPerCategory; + QueryParams.Limit = MaxItemsPerCategory; FAssetListRequest AssetListRequest = FAssetListRequest(QueryParams); AssetApi->ListAssetsAsync(AssetListRequest); } diff --git a/Source/RpmNextGen/Public/Cache/CacheGenerator.h b/Source/RpmNextGen/Public/Cache/CacheGenerator.h index 707c4d9..bc31c45 100644 --- a/Source/RpmNextGen/Public/Cache/CacheGenerator.h +++ b/Source/RpmNextGen/Public/Cache/CacheGenerator.h @@ -8,7 +8,8 @@ class IHttpResponse; class IHttpRequest; class FHttpModule; -DECLARE_DELEGATE_OneParam(FOnGenerateLocalCache, bool); +DECLARE_DELEGATE_OneParam(FOnCacheDataLoaded, bool); +DECLARE_DELEGATE_OneParam(FOnLocalCacheGenerated, bool); DECLARE_DELEGATE_OneParam(FOnDownloadRemoteCache, bool); class RPMNEXTGEN_API FCacheGenerator @@ -21,14 +22,19 @@ class RPMNEXTGEN_API FCacheGenerator void ExtractCache(); FOnDownloadRemoteCache OnDownloadRemoteCacheDelegate; - FOnGenerateLocalCache OnGenerateLocalCacheDelegate; + FOnCacheDataLoaded OnCacheDataLoaded; + FOnLocalCacheGenerated OnLocalCacheGenerated; + + void LoadAndStoreAssets(); + void LoadAndStoreAssetFromUrl(const FString& AssetFileUrl, const FString& FilePath); + protected: void FetchBaseModels(); void FetchAssetTypes(); void FetchAssetsForBaseModel(const FString& BaseModelID, const FString& AssetType); - virtual void OnRequestCacheAssetsComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful); virtual void OnDownloadRemoteCacheComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful); + virtual void OnAssetDataLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FString& FilePath); TUniquePtr AssetApi; @@ -36,16 +42,16 @@ class RPMNEXTGEN_API FCacheGenerator TArray AssetTypes; TMap> BaseModelAssetsMap; int32 CurrentBaseModelIndex; - private: - void ProcessNextRequest(); - void OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful); void FetchAssetsForEachBaseModel(); void OnListAssetTypesResponse(const FAssetTypeListResponse& AssetTypeListResponse, bool bWasSuccessful); static const FString CacheFolderPath; static const FString ZipFileName; - int ItemsPerCategory; - + int MaxItemsPerCategory; + int RequiredRefittedAssetRequests = 0; + int RefittedAssetRequestsCompleted = 0; + int RequiredAssetDownloadRequest = 0; + int AssetDownloadRequestsCompleted = 0; FHttpModule* Http; }; diff --git a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp index 38d712d..0775744 100644 --- a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp @@ -2,7 +2,10 @@ #include "Widgets/Input/SButton.h" #include "Widgets/Input/SEditableTextBox.h" #include "EditorStyleSet.h" +#include "IPlatformFilePak.h" +#include "RpmNextGen.h" #include "Cache/CacheGenerator.h" +#include "Misc/FileHelper.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Layout/SScrollBox.h" @@ -11,8 +14,9 @@ void SCacheEditorWidget::Construct(const FArguments& InArgs) if(!CacheGenerator) { CacheGenerator = MakeUnique(); - CacheGenerator->OnGenerateLocalCacheDelegate.BindRaw(this, &SCacheEditorWidget::OnGenerateLocalCacheComplete); + CacheGenerator->OnCacheDataLoaded.BindRaw(this, &SCacheEditorWidget::OnFetchCacheDataComplete); CacheGenerator->OnDownloadRemoteCacheDelegate.BindRaw(this, &SCacheEditorWidget::OnDownloadRemoteCacheComplete); + CacheGenerator->OnLocalCacheGenerated.BindRaw(this, &SCacheEditorWidget::OnGenerateLocalCacheCompleted); } ChildSlot [ @@ -42,7 +46,7 @@ void SCacheEditorWidget::Construct(const FArguments& InArgs) .VAlign(VAlign_Center) [ SNew(STextBlock) - .Text(FText::FromString("Items per category:")) + .Text(FText::FromString("Max items per category:")) ] + SHorizontalBox::Slot() .Padding(5, 0, 0, 0) @@ -53,7 +57,8 @@ void SCacheEditorWidget::Construct(const FArguments& InArgs) .OnValueChanged(this, &SCacheEditorWidget::OnItemsPerCategoryChanged) .AllowSpin(true) // Slider-like behavior .MinValue(1) - .MaxValue(100) + .MaxValue(30) + .SliderExponent(1.0f) ] ] @@ -186,14 +191,31 @@ FReply SCacheEditorWidget::OnDownloadRemoteCacheClicked() return FReply::Handled(); } -void SCacheEditorWidget::OnGenerateLocalCacheComplete(bool bWasSuccessful) +void SCacheEditorWidget::OnFetchCacheDataComplete(bool bWasSuccessful) { + UE_LOG(LogReadyPlayerMe, Log, TEXT("Completed fetching assets")); + CacheGenerator->LoadAndStoreAssets(); } void SCacheEditorWidget::OnDownloadRemoteCacheComplete(bool bWasSuccessful) { } +void SCacheEditorWidget::OnGenerateLocalCacheCompleted(bool bWasSuccessful) +{ + UE_LOG(LogReadyPlayerMe, Log, TEXT("Local cache generated successfully")); + FString FolderToPak = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/LocalCache"); + FString PakFilePath = FPaths::ProjectSavedDir() / TEXT("LocalCacheAssets.pak"); + FString ResponseFilePath = FPaths::ProjectSavedDir() / TEXT("RpmCache_ResponseFile.txt"); + + // const FString FCacheGenerator::CacheFolderPath = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/LocalCache"); + // const FString FCacheGenerator::ZipFileName = TEXT("LocalCacheAssets.zip"); + // Generate the response file + GeneratePakResponseFile(ResponseFilePath, FolderToPak); + + // Create the pak file using the UnrealPak tool + CreatePakFile(PakFilePath, ResponseFilePath); +} void SCacheEditorWidget::OnItemsPerCategoryChanged(float NewValue) { @@ -206,3 +228,47 @@ void SCacheEditorWidget::OnCacheUrlChanged(const FText& NewText) CacheUrl = NewText.ToString(); // Handle cache URL text change } + +void SCacheEditorWidget::CreatePakFile(const FString& PakFilePath, const FString& ResponseFilePath) +{ + // Path to the UnrealPak executable + FString UnrealPakPath = FPaths::ConvertRelativePathToFull(FPaths::EngineDir() / TEXT("Binaries/Win64/UnrealPak.exe")); + + // Arguments for the UnrealPak tool + FString CommandLineArgs = FString::Printf(TEXT("%s -Create=%s"), *PakFilePath, *ResponseFilePath); + + // Launch the UnrealPak process + FProcHandle ProcHandle = FPlatformProcess::CreateProc(*UnrealPakPath, *CommandLineArgs, true, false, false, nullptr, 0, nullptr, nullptr); + + if (ProcHandle.IsValid()) + { + FPlatformProcess::WaitForProc(ProcHandle); + FPlatformProcess::CloseProc(ProcHandle); + + UE_LOG(LogTemp, Log, TEXT("Pak file created successfully: %s"), *PakFilePath); + } + else + { + UE_LOG(LogTemp, Error, TEXT("Failed to create Pak file: %s"), *PakFilePath); + } +} + + +void SCacheEditorWidget::GeneratePakResponseFile(const FString& ResponseFilePath, const FString& FolderToPak) +{ + TArray Files; + IFileManager::Get().FindFilesRecursive(Files, *FolderToPak, TEXT("*.*"), true, false); + + FString ResponseFileContent; + int FileCount = 0; + for (const FString& File : Files) + { + FString RelativePath = File; + FPaths::MakePathRelativeTo(RelativePath, *FolderToPak); + ResponseFileContent += FString::Printf(TEXT("\"%s\" \"%s\"\n"), *File, *RelativePath); + FileCount++; + } + FFileHelper::SaveStringToFile(ResponseFileContent, *ResponseFilePath); + // print number of files added to the response file + UE_LOG(LogTemp, Log, TEXT("Response file created with %d files"), FileCount); +} diff --git a/Source/RpmNextGenEditor/Public/UI/SCacheEditorWidget.h b/Source/RpmNextGenEditor/Public/UI/SCacheEditorWidget.h index d7547b1..0e9f0a4 100644 --- a/Source/RpmNextGenEditor/Public/UI/SCacheEditorWidget.h +++ b/Source/RpmNextGenEditor/Public/UI/SCacheEditorWidget.h @@ -22,9 +22,10 @@ class SCacheEditorWidget : public SCompoundWidget FReply OnOpenLocalCacheFolderClicked(); FReply OnDownloadRemoteCacheClicked(); - void OnGenerateLocalCacheComplete(bool bWasSuccessful); + void OnFetchCacheDataComplete(bool bWasSuccessful); void OnDownloadRemoteCacheComplete(bool bWasSuccessful); - + void OnGenerateLocalCacheCompleted(bool bWasSuccessful); + TOptional GetItemsPerCategory() const { return ItemsPerCategory; @@ -47,4 +48,6 @@ class SCacheEditorWidget : public SCompoundWidget // Cache URL handling FString CacheUrl; void OnCacheUrlChanged(const FText& NewText); + void CreatePakFile(const FString& PakFilePath, const FString& ResponseFilePath); + void GeneratePakResponseFile(const FString& ResponseFilePath, const FString& FolderToPak); }; From 9ad48ef951f24658b32a8473aeb890fb84899294 Mon Sep 17 00:00:00 2001 From: Harrison Date: Tue, 3 Sep 2024 12:29:45 +0300 Subject: [PATCH 06/54] chore: temporarily remove unfinished UI and functionality --- .../Private/UI/SCacheEditorWidget.cpp | 151 ++++++++---------- 1 file changed, 71 insertions(+), 80 deletions(-) diff --git a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp index 0775744..54a5d16 100644 --- a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp @@ -61,8 +61,6 @@ void SCacheEditorWidget::Construct(const FArguments& InArgs) .SliderExponent(1.0f) ] ] - - // Button "Generate offline cache" + SVerticalBox::Slot() .Padding(5) .AutoHeight() @@ -77,24 +75,21 @@ void SCacheEditorWidget::Construct(const FArguments& InArgs) .VAlign(VAlign_Center) ] ] - - // Button "Extract Cache to local folder" - + SVerticalBox::Slot() - .Padding(5) - .AutoHeight() - [ - SNew(SBox) - .HeightOverride(40) // Set button height - [ - SNew(SButton) - .Text(FText::FromString("Extract Cache to local folder")) - .OnClicked(this, &SCacheEditorWidget::OnExtractCacheClicked) - .HAlign(HAlign_Center) - .VAlign(VAlign_Center) - ] - ] - - // Button "Open Local Cache Folder" + // TODO re-enable once we have added unzip logic + // + SVerticalBox::Slot() + // .Padding(5) + // .AutoHeight() + // [ + // SNew(SBox) + // .HeightOverride(40) // Set button height + // [ + // SNew(SButton) + // .Text(FText::FromString("Extract Cache to local folder")) + // .OnClicked(this, &SCacheEditorWidget::OnExtractCacheClicked) + // .HAlign(HAlign_Center) + // .VAlign(VAlign_Center) + // ] + // ] + SVerticalBox::Slot() .Padding(5) .AutoHeight() @@ -109,59 +104,59 @@ void SCacheEditorWidget::Construct(const FArguments& InArgs) .VAlign(VAlign_Center) ] ] - - // Title/Label "Remote Cache Downloader" - + SVerticalBox::Slot() - .Padding(5) - .AutoHeight() - [ - SNew(STextBlock) - .Text(FText::FromString("Remote Cache Downloader")) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 16)) - ] - - // Editable text field with label "Cache Url" - + SVerticalBox::Slot() - .Padding(5) - .AutoHeight() - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - [ - SNew(STextBlock) - .Text(FText::FromString("Cache URL:")) - ] - + SHorizontalBox::Slot() - .Padding(5, 0, 0, 0) - .FillWidth(1.0f) - [ - SNew(SBox) - .HeightOverride(30) // Set text box height - [ - SNew(SEditableTextBox) - .Text(FText::FromString("http://")) - .OnTextCommitted(this, &SCacheEditorWidget::OnCacheUrlTextCommitted) - ] - ] - ] - - // Button "Download Remote Cache" - + SVerticalBox::Slot() - .Padding(5) - .AutoHeight() - [ - SNew(SBox) - .HeightOverride(40) // Set button height - [ - SNew(SButton) - .Text(FText::FromString("Download Remote Cache")) - .OnClicked(this, &SCacheEditorWidget::OnDownloadRemoteCacheClicked) - .HAlign(HAlign_Center) - .VAlign(VAlign_Center) - ] - ] + // TODO implement remote cache download and unzip logic + // // Title/Label "Remote Cache Downloader" + // + SVerticalBox::Slot() + // .Padding(5) + // .AutoHeight() + // [ + // SNew(STextBlock) + // .Text(FText::FromString("Remote Cache Downloader")) + // .Font(FCoreStyle::GetDefaultFontStyle("Bold", 16)) + // ] + // + // // Editable text field with label "Cache Url" + // + SVerticalBox::Slot() + // .Padding(5) + // .AutoHeight() + // [ + // SNew(SHorizontalBox) + // + SHorizontalBox::Slot() + // .AutoWidth() + // .VAlign(VAlign_Center) + // [ + // SNew(STextBlock) + // .Text(FText::FromString("Cache URL:")) + // ] + // + SHorizontalBox::Slot() + // .Padding(5, 0, 0, 0) + // .FillWidth(1.0f) + // [ + // SNew(SBox) + // .HeightOverride(30) // Set text box height + // [ + // SNew(SEditableTextBox) + // .Text(FText::FromString("http://")) + // .OnTextCommitted(this, &SCacheEditorWidget::OnCacheUrlTextCommitted) + // ] + // ] + // ] + // + // // Button "Download Remote Cache" + // + SVerticalBox::Slot() + // .Padding(5) + // .AutoHeight() + // [ + // SNew(SBox) + // .HeightOverride(40) // Set button height + // [ + // SNew(SButton) + // .Text(FText::FromString("Download Remote Cache")) + // .OnClicked(this, &SCacheEditorWidget::OnDownloadRemoteCacheClicked) + // .HAlign(HAlign_Center) + // .VAlign(VAlign_Center) + // ] + // ] ] ]; } @@ -194,7 +189,8 @@ FReply SCacheEditorWidget::OnDownloadRemoteCacheClicked() void SCacheEditorWidget::OnFetchCacheDataComplete(bool bWasSuccessful) { UE_LOG(LogReadyPlayerMe, Log, TEXT("Completed fetching assets")); - CacheGenerator->LoadAndStoreAssets(); + //TODO re-nable once zip extraction is implemented + //CacheGenerator->LoadAndStoreAssets(); } void SCacheEditorWidget::OnDownloadRemoteCacheComplete(bool bWasSuccessful) @@ -207,13 +203,8 @@ void SCacheEditorWidget::OnGenerateLocalCacheCompleted(bool bWasSuccessful) FString FolderToPak = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/LocalCache"); FString PakFilePath = FPaths::ProjectSavedDir() / TEXT("LocalCacheAssets.pak"); FString ResponseFilePath = FPaths::ProjectSavedDir() / TEXT("RpmCache_ResponseFile.txt"); - - // const FString FCacheGenerator::CacheFolderPath = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/LocalCache"); - // const FString FCacheGenerator::ZipFileName = TEXT("LocalCacheAssets.zip"); - // Generate the response file + GeneratePakResponseFile(ResponseFilePath, FolderToPak); - - // Create the pak file using the UnrealPak tool CreatePakFile(PakFilePath, ResponseFilePath); } From a2535546719b9672f83659664d8b24c0581c8e53 Mon Sep 17 00:00:00 2001 From: Harrison Date: Tue, 3 Sep 2024 12:51:53 +0300 Subject: [PATCH 07/54] chore: fix path and renable assets saving --- .../Private/Cache/CacheGenerator.cpp | 2 +- .../Private/UI/SCacheEditorWidget.cpp | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index 59191a0..4bb69fc 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -13,7 +13,7 @@ #include "Misc/ScopeExit.h" #include "Settings/RpmDeveloperSettings.h" -const FString FCacheGenerator::CacheFolderPath = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/LocalCache"); +const FString FCacheGenerator::CacheFolderPath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/LocalCache"); const FString FCacheGenerator::ZipFileName = TEXT("LocalCacheAssets.zip"); FCacheGenerator::FCacheGenerator() diff --git a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp index 54a5d16..b5d766f 100644 --- a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp @@ -189,8 +189,7 @@ FReply SCacheEditorWidget::OnDownloadRemoteCacheClicked() void SCacheEditorWidget::OnFetchCacheDataComplete(bool bWasSuccessful) { UE_LOG(LogReadyPlayerMe, Log, TEXT("Completed fetching assets")); - //TODO re-nable once zip extraction is implemented - //CacheGenerator->LoadAndStoreAssets(); + CacheGenerator->LoadAndStoreAssets(); } void SCacheEditorWidget::OnDownloadRemoteCacheComplete(bool bWasSuccessful) @@ -199,13 +198,16 @@ void SCacheEditorWidget::OnDownloadRemoteCacheComplete(bool bWasSuccessful) void SCacheEditorWidget::OnGenerateLocalCacheCompleted(bool bWasSuccessful) { - UE_LOG(LogReadyPlayerMe, Log, TEXT("Local cache generated successfully")); - FString FolderToPak = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/LocalCache"); - FString PakFilePath = FPaths::ProjectSavedDir() / TEXT("LocalCacheAssets.pak"); - FString ResponseFilePath = FPaths::ProjectSavedDir() / TEXT("RpmCache_ResponseFile.txt"); - - GeneratePakResponseFile(ResponseFilePath, FolderToPak); - CreatePakFile(PakFilePath, ResponseFilePath); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Completed generating cache")); + + //TODO re-nable once zip extraction is implemented + // UE_LOG(LogReadyPlayerMe, Log, TEXT("Local cache generated successfully")); + // FString FolderToPak = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/LocalCache"); + // FString PakFilePath = FPaths::ProjectPersistentDownloadDir() / TEXT("LocalCacheAssets.pak"); + // FString ResponseFilePath = FPaths::ProjectPersistentDownloadDir() / TEXT("RpmCache_ResponseFile.txt"); + // + // GeneratePakResponseFile(ResponseFilePath, FolderToPak); + // CreatePakFile(PakFilePath, ResponseFilePath); } void SCacheEditorWidget::OnItemsPerCategoryChanged(float NewValue) From 7fc4aeb87b01821a2a9fc690238bd9f9c4482208 Mon Sep 17 00:00:00 2001 From: Harrison Date: Tue, 3 Sep 2024 15:35:34 +0300 Subject: [PATCH 08/54] feat: added AssetSaver --- .../Private/Cache/CacheGenerator.cpp | 39 +++++--- Source/RpmNextGen/Public/Cache/AssetSaver.cpp | 91 +++++++++++++++++++ Source/RpmNextGen/Public/Cache/AssetSaver.h | 30 ++++++ .../RpmNextGen/Public/Cache/CacheGenerator.h | 6 +- 4 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 Source/RpmNextGen/Public/Cache/AssetSaver.cpp create mode 100644 Source/RpmNextGen/Public/Cache/AssetSaver.h diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index 4bb69fc..77eadea 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -5,6 +5,7 @@ #include "Api/Assets/Models/AssetListRequest.h" #include "Api/Assets/Models/AssetTypeListRequest.h" #include "Api/Auth/ApiKeyAuthStrategy.h" +#include "Cache/AssetSaver.h" #include "Interfaces/IHttpRequest.h" #include "Interfaces/IHttpResponse.h" #include "Misc/Paths.h" @@ -56,7 +57,7 @@ void FCacheGenerator::LoadAndStoreAssets() RefittedAssetCount += Pairs.Value.Num(); } } - RequiredAssetDownloadRequest = 2 * RefittedAssetCount + 2 * BaseModelAssets.Num(); + RequiredAssetDownloadRequest = RefittedAssetCount + BaseModelAssets.Num(); UE_LOG(LogReadyPlayerMe, Log, TEXT("Total assets to download: %d. Total refitted assets to fetch: %d"), RequiredAssetDownloadRequest, RequiredRefittedAssetRequests); for (auto BaseModel : BaseModelAssets) { @@ -68,29 +69,39 @@ void FCacheGenerator::LoadAndStoreAssets() PlatformFile.CreateDirectoryTree(*DirectoryPath); } - LoadAndStoreAssetFromUrl(BaseModel.GlbUrl, BaseModeFolder / FString::Printf( TEXT("%s.glb"), *BaseModel.Id)); - LoadAndStoreAssetFromUrl(BaseModel.IconUrl, BaseModeFolder / FString::Printf( TEXT("%s.png"), *BaseModel.Id)); + // LoadAndStoreAssetFromUrl(BaseModel.GlbUrl, BaseModeFolder / FString::Printf( TEXT("%s.glb"), *BaseModel.Id)); + //LoadAndStoreAssetFromUrl(BaseModel.IconUrl, BaseModeFolder / FString::Printf( TEXT("%s.png"), *BaseModel.Id)); + LoadAndStoreAssetFromUrl(BaseModel.Id, &BaseModel); for (auto Pairs : BaseModelAssetsMap) { for (auto AssetForBaseModel : Pairs.Value) { - LoadAndStoreAssetFromUrl(AssetForBaseModel.GlbUrl, BaseModeFolder / FString::Printf( TEXT("%s.glb"), *AssetForBaseModel.Id)); - LoadAndStoreAssetFromUrl(AssetForBaseModel.IconUrl, BaseModeFolder / FString::Printf( TEXT("%s.png"), *AssetForBaseModel.Id)); + LoadAndStoreAssetFromUrl(BaseModel.Id, &AssetForBaseModel); } } } } -void FCacheGenerator::LoadAndStoreAssetFromUrl(const FString& AssetFileUrl, const FString& FilePath) +void FCacheGenerator::LoadAndStoreAssetFromUrl(const FString& BaseModelId, const FAsset* Asset) { - TSharedRef Request = Http->CreateRequest(); - Request->SetURL(AssetFileUrl); - Request->SetVerb(TEXT("GET")); - Request->OnProcessRequestComplete().BindLambda([this, FilePath](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) + // Use TSharedPtr instead of TSharedRef + TSharedPtr AssetSaver = MakeShared(); + AssetSaver->OnAssetSaved.BindRaw(this, &FCacheGenerator::OnAssetSaved); + AssetSaver->SaveAssetToCache(BaseModelId, Asset); + + // Store the AssetSaver in a TArray or other container to ensure its lifetime + //ActiveAssetSavers.Add(AssetSaver); +} + +void FCacheGenerator::OnAssetSaved(bool bWasSuccessful) +{ + AssetDownloadRequestsCompleted++; + if(AssetDownloadRequestsCompleted >= RequiredAssetDownloadRequest) { - this->OnAssetDataLoaded(Request, Response, bWasSuccessful, FilePath); - }); - Request->ProcessRequest(); + UE_LOG(LogReadyPlayerMe, Log, TEXT("OnLocalCacheGenerated Total assets to download: %d. Asset download requests completed: %d"), RequiredAssetDownloadRequest, AssetDownloadRequestsCompleted); + + OnLocalCacheGenerated.ExecuteIfBound(true); + } } void FCacheGenerator::OnAssetDataLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FString& FilePath) @@ -234,7 +245,7 @@ void FCacheGenerator::OnDownloadRemoteCacheComplete(TSharedPtr Req void FCacheGenerator::ExtractCache() { - + // TODO add implementation } void FCacheGenerator::FetchBaseModels() diff --git a/Source/RpmNextGen/Public/Cache/AssetSaver.cpp b/Source/RpmNextGen/Public/Cache/AssetSaver.cpp new file mode 100644 index 0000000..16d9374 --- /dev/null +++ b/Source/RpmNextGen/Public/Cache/AssetSaver.cpp @@ -0,0 +1,91 @@ +#include "AssetSaver.h" +#include "HttpModule.h" +#include "RpmNextGen.h" +#include "Api/Assets/Models/Asset.h" +#include "Interfaces/IHttpResponse.h" + +const FString FAssetSaver::CacheFolderPath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/LocalCache"); + + +FAssetSaver::FAssetSaver(): bIsImageLoaded(false), bIsGlbLoaded(false) +{ + Http = &FHttpModule::Get(); +} + +FAssetSaver::~FAssetSaver() +{ +} + +void FAssetSaver::SaveAssetToCache(const FString& BaseModelId, const FAsset* Asset) +{ + const FString Path = CacheFolderPath / BaseModelId; + LoadAndSaveGlb(Asset->IconUrl, FString::Printf(TEXT("%s/%s.glb"), *Path, *Asset->Id)); + LoadAndSaveImage(Asset->IconUrl, FString::Printf(TEXT("%s/%s.png"), *Path, *Asset->Id)); +} + +void FAssetSaver::LoadAndSaveImage(const FString& Url, const FString& FilePath) +{ + TSharedRef Request = Http->CreateRequest(); + Request->SetURL(Url); + Request->SetVerb(TEXT("GET")); + + // Capture TSharedPtr in the lambda to ensure it is not destroyed prematurely + TSharedPtr ThisPtr = SharedThis(this); + Request->OnProcessRequestComplete().BindLambda([ThisPtr, FilePath](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) + { + ThisPtr->OnAssetLoaded(Request, Response, bWasSuccessful, FilePath); + }); + + Request->ProcessRequest(); +} + +void FAssetSaver::LoadAndSaveGlb(const FString& Url, const FString& FilePath) +{ + TSharedRef Request = Http->CreateRequest(); + Request->SetURL(Url); + Request->SetVerb(TEXT("GET")); + + // Capture TSharedPtr in the lambda to ensure it is not destroyed prematurely + TSharedPtr ThisPtr = SharedThis(this); + Request->OnProcessRequestComplete().BindLambda([ThisPtr, FilePath](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) + { + ThisPtr->OnAssetLoaded(Request, Response, bWasSuccessful, FilePath); + }); + + Request->ProcessRequest(); +} + +void FAssetSaver::OnAssetLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FString& FilePath) +{ + if(bWasSuccessful && Response.IsValid()) + { + SaveToFile(FilePath, Response->GetContent()); + } + else + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load image from url: %s"), *Request->GetURL()); + } + if(FilePath.EndsWith(TEXT(".glb"))) + { + bIsGlbLoaded = true; + } + else + { + bIsImageLoaded = true; + } + if(bIsImageLoaded && bIsGlbLoaded) + { + UE_LOG(LogReadyPlayerMe, Log, TEXT("Asset saved to cache")); + OnAssetSaved.ExecuteIfBound(true); + } +} + +void FAssetSaver::SaveToFile(const FString& FilePath, const TArray& Data) +{ + if (FFileHelper::SaveArrayToFile(Data, *FilePath)) + { + UE_LOG(LogReadyPlayerMe, Log, TEXT("Successfully saved asset in local cache to: %s"), *FilePath); + return; + } + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to saved asset local cache to: %s"), *FilePath); +} diff --git a/Source/RpmNextGen/Public/Cache/AssetSaver.h b/Source/RpmNextGen/Public/Cache/AssetSaver.h new file mode 100644 index 0000000..302a9a3 --- /dev/null +++ b/Source/RpmNextGen/Public/Cache/AssetSaver.h @@ -0,0 +1,30 @@ +#pragma once + +#include "CoreMinimal.h" + +class FHttpModule; +class IHttpResponse; +class IHttpRequest; +struct FAsset; + +DECLARE_DELEGATE_OneParam(FOnAssetSavedToCache, bool); + +class RPMNEXTGEN_API FAssetSaver : public TSharedFromThis +{ + +public: + FAssetSaver(); + virtual ~FAssetSaver(); + void SaveAssetToCache(const FString& BaseModelId, const FAsset* Asset); + void LoadAndSaveImage(const FString& Url, const FString& FilePath); + void LoadAndSaveGlb(const FString& Url, const FString& FilePath); + void OnAssetLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FString& FilePath); + void SaveToFile(const FString& FilePath, const TArray& Data); + + FOnAssetSavedToCache OnAssetSaved; +private: + bool bIsImageLoaded; + bool bIsGlbLoaded; + static const FString CacheFolderPath; + FHttpModule* Http; +}; diff --git a/Source/RpmNextGen/Public/Cache/CacheGenerator.h b/Source/RpmNextGen/Public/Cache/CacheGenerator.h index bc31c45..6ee8efe 100644 --- a/Source/RpmNextGen/Public/Cache/CacheGenerator.h +++ b/Source/RpmNextGen/Public/Cache/CacheGenerator.h @@ -1,6 +1,7 @@ #pragma once #include "Api/Assets/Models/AssetListResponse.h" +class FAssetSaver; struct FAssetTypeListResponse; class FAssetApi; struct FAsset; @@ -26,7 +27,7 @@ class RPMNEXTGEN_API FCacheGenerator FOnLocalCacheGenerated OnLocalCacheGenerated; void LoadAndStoreAssets(); - void LoadAndStoreAssetFromUrl(const FString& AssetFileUrl, const FString& FilePath); + void LoadAndStoreAssetFromUrl(const FString& BaseModelId, const FAsset* Asset); protected: void FetchBaseModels(); @@ -35,9 +36,8 @@ class RPMNEXTGEN_API FCacheGenerator virtual void OnDownloadRemoteCacheComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful); virtual void OnAssetDataLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FString& FilePath); - + void OnAssetSaved(bool bWasSuccessful); TUniquePtr AssetApi; - TArray BaseModelAssets; TArray AssetTypes; TMap> BaseModelAssetsMap; From 3ecacaefd19d38e8bd990a1b4eb5cbe1b945fbf5 Mon Sep 17 00:00:00 2001 From: Harrison Hough Date: Tue, 3 Sep 2024 20:01:59 +0300 Subject: [PATCH 09/54] chore: small refactor --- Source/RpmNextGen/{Public => Private}/Cache/AssetSaver.cpp | 4 ++-- Source/RpmNextGen/Private/Cache/CacheGenerator.cpp | 2 +- Source/RpmNextGen/Public/Cache/AssetSaver.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename Source/RpmNextGen/{Public => Private}/Cache/AssetSaver.cpp (95%) diff --git a/Source/RpmNextGen/Public/Cache/AssetSaver.cpp b/Source/RpmNextGen/Private/Cache/AssetSaver.cpp similarity index 95% rename from Source/RpmNextGen/Public/Cache/AssetSaver.cpp rename to Source/RpmNextGen/Private/Cache/AssetSaver.cpp index 16d9374..c5100fd 100644 --- a/Source/RpmNextGen/Public/Cache/AssetSaver.cpp +++ b/Source/RpmNextGen/Private/Cache/AssetSaver.cpp @@ -1,4 +1,4 @@ -#include "AssetSaver.h" +#include "Cache/AssetSaver.h" #include "HttpModule.h" #include "RpmNextGen.h" #include "Api/Assets/Models/Asset.h" @@ -16,7 +16,7 @@ FAssetSaver::~FAssetSaver() { } -void FAssetSaver::SaveAssetToCache(const FString& BaseModelId, const FAsset* Asset) +void FAssetSaver::LoadSaveAssetToCache(const FString& BaseModelId, const FAsset* Asset) { const FString Path = CacheFolderPath / BaseModelId; LoadAndSaveGlb(Asset->IconUrl, FString::Printf(TEXT("%s/%s.glb"), *Path, *Asset->Id)); diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index 77eadea..792aee0 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -87,7 +87,7 @@ void FCacheGenerator::LoadAndStoreAssetFromUrl(const FString& BaseModelId, const // Use TSharedPtr instead of TSharedRef TSharedPtr AssetSaver = MakeShared(); AssetSaver->OnAssetSaved.BindRaw(this, &FCacheGenerator::OnAssetSaved); - AssetSaver->SaveAssetToCache(BaseModelId, Asset); + AssetSaver->LoadSaveAssetToCache(BaseModelId, Asset); // Store the AssetSaver in a TArray or other container to ensure its lifetime //ActiveAssetSavers.Add(AssetSaver); diff --git a/Source/RpmNextGen/Public/Cache/AssetSaver.h b/Source/RpmNextGen/Public/Cache/AssetSaver.h index 302a9a3..5153849 100644 --- a/Source/RpmNextGen/Public/Cache/AssetSaver.h +++ b/Source/RpmNextGen/Public/Cache/AssetSaver.h @@ -15,7 +15,7 @@ class RPMNEXTGEN_API FAssetSaver : public TSharedFromThis public: FAssetSaver(); virtual ~FAssetSaver(); - void SaveAssetToCache(const FString& BaseModelId, const FAsset* Asset); + void LoadSaveAssetToCache(const FString& BaseModelId, const FAsset* Asset); void LoadAndSaveImage(const FString& Url, const FString& FilePath); void LoadAndSaveGlb(const FString& Url, const FString& FilePath); void OnAssetLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FString& FilePath); From 9ddaa8f1058fd0339573417a826d32f33fdb9814 Mon Sep 17 00:00:00 2001 From: Harrison Date: Wed, 4 Sep 2024 10:12:16 +0300 Subject: [PATCH 10/54] chore: update license --- LICENSE.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index 3c76baa..6edc407 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,9 @@ +Copyright 2024 Ready Player Me + The MIT License (MIT) ===================== +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -This software is provided under the MIT License. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file From 3ed14ca2084c70536b5108424ce576230f724472 Mon Sep 17 00:00:00 2001 From: Harrison Date: Wed, 4 Sep 2024 14:23:56 +0300 Subject: [PATCH 11/54] feat: .glb fetching rework --- .../Private/Api/Assets/AssetLoader.cpp | 80 +++++----------- .../RpmNextGen/Private/Api/Files/FileApi.cpp | 32 +++++++ .../Private/Api/Files/GlbLoader.cpp | 61 ++++++++++++ .../RpmNextGen/Private/Cache/AssetSaver.cpp | 64 +++++++++---- .../Private/Cache/CacheGenerator.cpp | 44 +-------- .../Private/RpmAssetLoaderComponent.cpp | 6 +- .../Public/Api/Assets/AssetLoader.h | 37 +++----- Source/RpmNextGen/Public/Api/Files/FileApi.h | 17 ++++ .../RpmNextGen/Public/Api/Files/GlbLoader.h | 32 +++++++ Source/RpmNextGen/Public/Cache/AssetSaver.h | 7 +- .../Public/Cache/AssetStorageManager.h | 93 +++++++++++++++++++ .../RpmNextGen/Public/Cache/CacheGenerator.h | 1 - Source/RpmNextGen/Public/Cache/StoredAsset.h | 47 ++++++++++ .../Public/RpmAssetLoaderComponent.h | 4 +- Source/RpmNextGen/Public/RpmImageLoader.h | 3 +- .../Private/EditorAssetLoader.cpp | 2 +- .../Private/UI/SCharacterLoaderWidget.cpp | 2 +- .../Public/EditorAssetLoader.h | 4 +- .../Public/UI/SCharacterLoaderWidget.h | 2 +- 19 files changed, 380 insertions(+), 158 deletions(-) create mode 100644 Source/RpmNextGen/Private/Api/Files/FileApi.cpp create mode 100644 Source/RpmNextGen/Private/Api/Files/GlbLoader.cpp create mode 100644 Source/RpmNextGen/Public/Api/Files/FileApi.h create mode 100644 Source/RpmNextGen/Public/Api/Files/GlbLoader.h create mode 100644 Source/RpmNextGen/Public/Cache/AssetStorageManager.h create mode 100644 Source/RpmNextGen/Public/Cache/StoredAsset.h diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp index 88f8f09..e5ac999 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp @@ -1,71 +1,35 @@ #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" +#include "Api/Assets/Models/Asset.h" +#include "RpmNextGenEditor/Public/EditorAssetLoader.h" -FAssetLoader::FAssetLoader() +void FAssetLoader::LoadAsset(FAsset* Asset, bool bStoreInCache) { - 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); - } + LoadAssetModel(Asset, bStoreInCache); + LoadAssetImage(Asset, bStoreInCache); } -FAssetLoader::FAssetLoader(FglTFRuntimeConfig* Config) : GltfConfig(Config) +void FAssetLoader::FileRequestComplete(TArray* Data, FAsset* Asset) { } -FAssetLoader::~FAssetLoader() +void FAssetLoader::LoadAssetModel(FAsset* Asset, bool bStoreInCache) { + TSharedPtr FileApi = MakeShareable(new FFileApi()); + TSharedPtr ThisPtr = SharedThis(this); + FileApi->OnFileRequestComplete.BindLambda([ThisPtr, Asset](TArray* Data) + { + ThisPtr->FileRequestComplete(Data, Asset); + }); + FileApi->RequestFromUrl(Asset->GlbUrl); } -void FAssetLoader::LoadGLBFromURL(const FString& URL) +void FAssetLoader::LoadAssetImage(FAsset* Asset, bool bStoreInCache) { - // 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); + TSharedPtr FileApi = MakeShareable(new FFileApi()); + TSharedPtr ThisPtr = SharedThis(this); + FileApi->OnFileRequestComplete.BindLambda([ThisPtr, Asset](TArray* Data) + { + ThisPtr->FileRequestComplete(Data, Asset); + }); + FileApi->RequestFromUrl(Asset->IconUrl); } diff --git a/Source/RpmNextGen/Private/Api/Files/FileApi.cpp b/Source/RpmNextGen/Private/Api/Files/FileApi.cpp new file mode 100644 index 0000000..a465e1d --- /dev/null +++ b/Source/RpmNextGen/Private/Api/Files/FileApi.cpp @@ -0,0 +1,32 @@ +#include "Api/Files/FileApi.h" + +#include "HttpModule.h" +#include "Interfaces/IHttpResponse.h" + +FFileApi::FFileApi() +{ +} + +FFileApi::~FFileApi() +{ +} + +void FFileApi::RequestFromUrl(const FString& URL) +{ + TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); + HttpRequest->OnProcessRequestComplete().BindRaw(this, &FFileApi::FileRequestComplete); + HttpRequest->SetURL(URL); + HttpRequest->SetVerb("GET"); + HttpRequest->ProcessRequest(); +} + +void FFileApi::FileRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) +{ + if (bWasSuccessful && Response.IsValid() && Response->GetContentLength() > 0) + { + TArray Content = Response->GetContent(); + OnFileRequestComplete.ExecuteIfBound(&Content); + return; + } + OnFileRequestComplete.ExecuteIfBound(nullptr); +} diff --git a/Source/RpmNextGen/Private/Api/Files/GlbLoader.cpp b/Source/RpmNextGen/Private/Api/Files/GlbLoader.cpp new file mode 100644 index 0000000..d7b6632 --- /dev/null +++ b/Source/RpmNextGen/Private/Api/Files/GlbLoader.cpp @@ -0,0 +1,61 @@ +#include "Api/Files/GlbLoader.h" +#include "RpmNextGen.h" +#include "glTFRuntime/Public/glTFRuntimeFunctionLibrary.h" +#include "Interfaces/IHttpResponse.h" +#include "Misc/FileHelper.h" +#include "HAL/PlatformFilemanager.h" + +FGlbLoader::FGlbLoader() : GltfConfig(nullptr) +{ + GltfConfig = new FglTFRuntimeConfig(); + GltfConfig->TransformBaseType = EglTFRuntimeTransformBaseType::YForward; + DownloadDirectory = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/AvatarCache"); + + // Ensure the directory exists + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + if (!PlatformFile.DirectoryExists(*DownloadDirectory)) + { + PlatformFile.CreateDirectory(*DownloadDirectory); + } +} + +FGlbLoader::FGlbLoader(FglTFRuntimeConfig* Config) : GltfConfig(Config) +{ + FGlbLoader(); +} + +FGlbLoader::~FGlbLoader() +{ +} + +void FGlbLoader::FileRequestComplete(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(Content, *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/Cache/AssetSaver.cpp b/Source/RpmNextGen/Private/Cache/AssetSaver.cpp index c5100fd..a42b04c 100644 --- a/Source/RpmNextGen/Private/Cache/AssetSaver.cpp +++ b/Source/RpmNextGen/Private/Cache/AssetSaver.cpp @@ -2,9 +2,10 @@ #include "HttpModule.h" #include "RpmNextGen.h" #include "Api/Assets/Models/Asset.h" +#include "Cache/AssetStorageManager.h" #include "Interfaces/IHttpResponse.h" -const FString FAssetSaver::CacheFolderPath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/LocalCache"); +const FString FAssetSaver::CacheFolderPath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache"); FAssetSaver::FAssetSaver(): bIsImageLoaded(false), bIsGlbLoaded(false) @@ -14,58 +15,87 @@ FAssetSaver::FAssetSaver(): bIsImageLoaded(false), bIsGlbLoaded(false) FAssetSaver::~FAssetSaver() { + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + if (!PlatformFile.DirectoryExists(*CacheFolderPath)) + { + PlatformFile.CreateDirectory(*CacheFolderPath); + } } void FAssetSaver::LoadSaveAssetToCache(const FString& BaseModelId, const FAsset* Asset) { const FString Path = CacheFolderPath / BaseModelId; - LoadAndSaveGlb(Asset->IconUrl, FString::Printf(TEXT("%s/%s.glb"), *Path, *Asset->Id)); - LoadAndSaveImage(Asset->IconUrl, FString::Printf(TEXT("%s/%s.png"), *Path, *Asset->Id)); + FStoredAsset StoredAsset = FStoredAsset(); + StoredAsset.Asset = *Asset; + StoredAsset.GlbFilePath = FString::Printf(TEXT("%s/%s.glb"), *Path, *Asset->Id); + StoredAsset.IconFilePath = FString::Printf(TEXT("%s/%s.png"), *Path, *Asset->Id); + LoadAndSaveGlb(StoredAsset); + LoadAndSaveImage(StoredAsset); } -void FAssetSaver::LoadAndSaveImage(const FString& Url, const FString& FilePath) +void FAssetSaver::LoadAndSaveImage(const FStoredAsset& StoredAsset) { + if(StoredAsset.Asset.IconUrl.IsEmpty()) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("Icon URL is empty for asset: %s assetname: %s"), *StoredAsset.Asset.Id, *StoredAsset.Asset.Name); + bIsImageLoaded = true; + return; + } TSharedRef Request = Http->CreateRequest(); - Request->SetURL(Url); + Request->SetURL(StoredAsset.Asset.IconUrl); Request->SetVerb(TEXT("GET")); - // Capture TSharedPtr in the lambda to ensure it is not destroyed prematurely TSharedPtr ThisPtr = SharedThis(this); - Request->OnProcessRequestComplete().BindLambda([ThisPtr, FilePath](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) + Request->OnProcessRequestComplete().BindLambda([ThisPtr, StoredAsset](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { - ThisPtr->OnAssetLoaded(Request, Response, bWasSuccessful, FilePath); + ThisPtr->OnAssetLoaded(Request, Response, bWasSuccessful, &StoredAsset); }); Request->ProcessRequest(); } -void FAssetSaver::LoadAndSaveGlb(const FString& Url, const FString& FilePath) +void FAssetSaver::LoadAndSaveGlb(const FStoredAsset& StoredAsset) { + if(StoredAsset.Asset.GlbUrl.IsEmpty()) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("Glb URL is empty for asset: %s assetname: %s"), *StoredAsset.Asset.Id, *StoredAsset.Asset.Type); + bIsImageLoaded = true; + return; + } TSharedRef Request = Http->CreateRequest(); - Request->SetURL(Url); + Request->SetURL(StoredAsset.Asset.GlbUrl); Request->SetVerb(TEXT("GET")); // Capture TSharedPtr in the lambda to ensure it is not destroyed prematurely TSharedPtr ThisPtr = SharedThis(this); - Request->OnProcessRequestComplete().BindLambda([ThisPtr, FilePath](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) + Request->OnProcessRequestComplete().BindLambda([ThisPtr, StoredAsset](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { - ThisPtr->OnAssetLoaded(Request, Response, bWasSuccessful, FilePath); + ThisPtr->OnAssetLoaded(Request, Response, bWasSuccessful, &StoredAsset); }); Request->ProcessRequest(); } -void FAssetSaver::OnAssetLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FString& FilePath) +void FAssetSaver::OnAssetLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FStoredAsset* StoredAsset) { + const bool bIsGlb = Request->GetURL().EndsWith(TEXT(".glb")); if(bWasSuccessful && Response.IsValid()) { - SaveToFile(FilePath, Response->GetContent()); + if(bIsGlb) + { + SaveToFile(StoredAsset->GlbFilePath, Response->GetContent()); + } + else + { + SaveToFile(StoredAsset->IconFilePath, Response->GetContent()); + + } } else { - UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load image from url: %s"), *Request->GetURL()); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load image or .glb from url: %s"), *Request->GetURL()); } - if(FilePath.EndsWith(TEXT(".glb"))) + if(bIsGlb) { bIsGlbLoaded = true; } @@ -75,6 +105,8 @@ void FAssetSaver::OnAssetLoaded(TSharedPtr Request, TSharedPtr AssetSaver = MakeShared(); AssetSaver->OnAssetSaved.BindRaw(this, &FCacheGenerator::OnAssetSaved); AssetSaver->LoadSaveAssetToCache(BaseModelId, Asset); - - // Store the AssetSaver in a TArray or other container to ensure its lifetime - //ActiveAssetSavers.Add(AssetSaver); } void FCacheGenerator::OnAssetSaved(bool bWasSuccessful) @@ -104,42 +100,6 @@ void FCacheGenerator::OnAssetSaved(bool bWasSuccessful) } } -void FCacheGenerator::OnAssetDataLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FString& FilePath) -{ - if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) - { - // Get the response data - const TArray& Data = Response->GetContent(); - - // Save the data as a .zip file - if (FFileHelper::SaveArrayToFile(Data, *FilePath)) - { - UE_LOG(LogReadyPlayerMe, Log, TEXT("Successfully saved asset in local cache to: %s"), *FilePath); - } - else - { - UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to saved asset local cache to: %s"), *FilePath); - - } - AssetDownloadRequestsCompleted++; - if(AssetDownloadRequestsCompleted >= RequiredAssetDownloadRequest) - { - UE_LOG(LogReadyPlayerMe, Log, TEXT("OnLocalCacheGenerated Total assets to download: %d. Asset download requests completed: %d"), RequiredAssetDownloadRequest, AssetDownloadRequestsCompleted); - - OnLocalCacheGenerated.ExecuteIfBound(true); - } - return; - } - AssetDownloadRequestsCompleted++; - if(AssetDownloadRequestsCompleted >= RequiredAssetDownloadRequest) - { - UE_LOG(LogReadyPlayerMe, Log, TEXT("OnLocalCacheGenerated Total assets to download: %d. Asset download requests completed: %d"), RequiredAssetDownloadRequest, AssetDownloadRequestsCompleted); - OnLocalCacheGenerated.ExecuteIfBound(true); - } - UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to download the remote cache")); - -} - void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful) { UE_LOG(LogReadyPlayerMe, Log, TEXT("OnListAssetsResponse") ); diff --git a/Source/RpmNextGen/Private/RpmAssetLoaderComponent.cpp b/Source/RpmNextGen/Private/RpmAssetLoaderComponent.cpp index 3bc7f19..3cec01a 100644 --- a/Source/RpmNextGen/Private/RpmAssetLoaderComponent.cpp +++ b/Source/RpmNextGen/Private/RpmAssetLoaderComponent.cpp @@ -3,7 +3,7 @@ #include "RpmAssetLoaderComponent.h" #include "RpmNextGen.h" -#include "Api/Assets/AssetLoader.h" +#include "Api/Files//GlbLoader.h" class URpmDeveloperSettings; @@ -13,7 +13,7 @@ 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 = MakeShared(); AssetLoader->OnGLtfAssetLoaded.BindUObject( this, &URpmAssetLoaderComponent::HandleGLtfAssetLoaded @@ -28,7 +28,7 @@ void URpmAssetLoaderComponent::BeginPlay() void URpmAssetLoaderComponent::LoadCharacterFromUrl(const FString Url) { - AssetLoader->LoadGLBFromURL(Url); + AssetLoader->RequestFromUrl(Url); } void URpmAssetLoaderComponent::HandleGLtfAssetLoaded(UglTFRuntimeAsset* gltfAsset, bool bWasSuccessful) diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h b/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h index 75cf1c3..12ea514 100644 --- a/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h +++ b/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h @@ -1,34 +1,19 @@ #pragma once #include "CoreMinimal.h" -#include "Api/Common/WebApi.h" -#include "HAL/PlatformFilemanager.h" -#include "Interfaces/IHttpRequest.h" -struct FglTFRuntimeConfig; -class UglTFRuntimeAsset; +struct FAsset; -DECLARE_DELEGATE_TwoParams(FOnAssetDataReceived, TArray, bool); -DECLARE_DELEGATE_TwoParams(FOnAssetSaved, FString, bool); -DECLARE_DELEGATE_TwoParams(FOnAssetDownloaded, UglTFRuntimeAsset*, bool); - - -class RPMNEXTGEN_API FAssetLoader : public FWebApi +class RPMNEXTGEN_API FAssetLoader : public TSharedFromThis { 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; + FAssetLoader() = default; + virtual ~FAssetLoader() = default; + void LoadAsset(FAsset* Asset, bool bStoreInCache); + virtual void FileRequestComplete(TArray* Data, FAsset* Asset); +private: + void LoadAssetModel(FAsset* Asset, bool bStoreInCache); + void LoadAssetImage(FAsset* Asset, bool bStoreInCache); + bool bIsModelLoaded; + bool bIsImageLoaded; }; diff --git a/Source/RpmNextGen/Public/Api/Files/FileApi.h b/Source/RpmNextGen/Public/Api/Files/FileApi.h new file mode 100644 index 0000000..4367a24 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Files/FileApi.h @@ -0,0 +1,17 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Interfaces/IHttpRequest.h" + +DECLARE_DELEGATE_OneParam(FOnFileRequestComplete, TArray*); + +class RPMNEXTGEN_API FFileApi : public TSharedFromThis +{ +public: + FFileApi(); + virtual ~FFileApi(); + virtual void RequestFromUrl(const FString& URL); + virtual void FileRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); + + FOnFileRequestComplete OnFileRequestComplete; +}; diff --git a/Source/RpmNextGen/Public/Api/Files/GlbLoader.h b/Source/RpmNextGen/Public/Api/Files/GlbLoader.h new file mode 100644 index 0000000..a8fd07c --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Files/GlbLoader.h @@ -0,0 +1,32 @@ +#pragma once + +#include "CoreMinimal.h" +#include "FileApi.h" +#include "HAL/PlatformFilemanager.h" +#include "Interfaces/IHttpRequest.h" + +struct FglTFRuntimeConfig; +class UglTFRuntimeAsset; + +DECLARE_DELEGATE_TwoParams(FOnGlbDataReceived, TArray, bool); +DECLARE_DELEGATE_TwoParams(FOnGlbSaved, FString, bool); +DECLARE_DELEGATE_TwoParams(FOnGlbDownloaded, UglTFRuntimeAsset*, bool); + + +class RPMNEXTGEN_API FGlbLoader : public FFileApi +{ +public: + FGlbLoader(); + FGlbLoader(FglTFRuntimeConfig* Config); + virtual ~FGlbLoader() override; + + FOnGlbDataReceived OnRequestDataReceived; + FOnGlbDownloaded OnGLtfAssetLoaded; + FOnGlbSaved OnAssetSaved; + bool bSaveToDisk = false; + +protected: + FglTFRuntimeConfig* GltfConfig; + virtual void FileRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) override; + FString DownloadDirectory; +}; diff --git a/Source/RpmNextGen/Public/Cache/AssetSaver.h b/Source/RpmNextGen/Public/Cache/AssetSaver.h index 5153849..a8f117a 100644 --- a/Source/RpmNextGen/Public/Cache/AssetSaver.h +++ b/Source/RpmNextGen/Public/Cache/AssetSaver.h @@ -2,6 +2,7 @@ #include "CoreMinimal.h" +struct FStoredAsset; class FHttpModule; class IHttpResponse; class IHttpRequest; @@ -16,9 +17,9 @@ class RPMNEXTGEN_API FAssetSaver : public TSharedFromThis FAssetSaver(); virtual ~FAssetSaver(); void LoadSaveAssetToCache(const FString& BaseModelId, const FAsset* Asset); - void LoadAndSaveImage(const FString& Url, const FString& FilePath); - void LoadAndSaveGlb(const FString& Url, const FString& FilePath); - void OnAssetLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FString& FilePath); + void LoadAndSaveImage(const FStoredAsset& StoredAsset); + void LoadAndSaveGlb(const FStoredAsset& StoredAsset); + void OnAssetLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FStoredAsset* StoredAsset); void SaveToFile(const FString& FilePath, const TArray& Data); FOnAssetSavedToCache OnAssetSaved; diff --git a/Source/RpmNextGen/Public/Cache/AssetStorageManager.h b/Source/RpmNextGen/Public/Cache/AssetStorageManager.h new file mode 100644 index 0000000..9459d39 --- /dev/null +++ b/Source/RpmNextGen/Public/Cache/AssetStorageManager.h @@ -0,0 +1,93 @@ +#pragma once +#include "RpmNextGen.h" +#include "StoredAsset.h" + +class FAssetStorageManager +{ +public: + static FAssetStorageManager& Get() + { + static FAssetStorageManager Instance; + return Instance; + } + + void TrackStoredAsset(const FAsset& Asset, const FString& GlbFilePath, const FString& IconFilePath, const bool bSaveManifest = true) + { + FStoredAsset& StoredAsset = StoredAssets.FindOrAdd(Asset.Id); + StoredAsset.Asset = Asset; + StoredAsset.GlbFilePath = GlbFilePath; + StoredAsset.IconFilePath = IconFilePath; + + if(bSaveManifest) + { + SaveManifest(); + } + UE_LOG(LogReadyPlayerMe, Log, TEXT("Tracked asset: AssetId=%s, GlbFilePath=%s, IconFilePath=%s"), *Asset.Id, *GlbFilePath, *IconFilePath); + } + + void TrackStoredAsset(const FStoredAsset& StoredAsset, const bool bSaveManifest = true) + { + StoredAssets.Add(StoredAsset.Asset.Id, StoredAsset); + + if(bSaveManifest) + { + SaveManifest(); + } + UE_LOG(LogReadyPlayerMe, Log, TEXT("Tracked asset: AssetId=%s, GlbFilePath=%s, IconFilePath=%s"), *StoredAsset.Asset.Id, *StoredAsset.GlbFilePath, *StoredAsset.IconFilePath); + } + + void LoadManifest() + { + const FString ManifestFilePath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache/AssetManifest.json"); + FString ManifestContent; + + if (FFileHelper::LoadFileToString(ManifestContent, *ManifestFilePath)) + { + TSharedPtr ManifestJson; + TSharedRef> Reader = TJsonReaderFactory<>::Create(ManifestContent); + + if (FJsonSerializer::Deserialize(Reader, ManifestJson) && ManifestJson.IsValid()) + { + TArray> AssetsArray = ManifestJson->GetArrayField(TEXT("TrackedAssets")); + for (const TSharedPtr& AssetValue : AssetsArray) + { + FStoredAsset StoredAsset = FStoredAsset::FromJson(AssetValue->AsObject()); + StoredAssets.Add(StoredAsset.Asset.Id, StoredAsset); + } + + UE_LOG(LogReadyPlayerMe, Log, TEXT("Loaded manifest with %d assets"), StoredAssets.Num()); + } + } + } + + void SaveManifest() + { + const FString ManifestFilePath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache/AssetManifest.json"); + + TSharedPtr ManifestJson = MakeShared(); + TArray> AssetsArray; + + for (const auto& Entry : StoredAssets) + { + TSharedPtr AssetJson = Entry.Value.ToJson(); + AssetsArray.Add(MakeShared(AssetJson)); + } + + ManifestJson->SetArrayField(TEXT("TrackedAssets"), AssetsArray); + + FString OutputString; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&OutputString); + FJsonSerializer::Serialize(ManifestJson.ToSharedRef(), Writer); + + FFileHelper::SaveStringToFile(OutputString, *ManifestFilePath); + } + + const TMap& GetStoredAssets() const + { + return StoredAssets; + } + +private: + FAssetStorageManager() {} + TMap StoredAssets; +}; diff --git a/Source/RpmNextGen/Public/Cache/CacheGenerator.h b/Source/RpmNextGen/Public/Cache/CacheGenerator.h index 6ee8efe..1f333d5 100644 --- a/Source/RpmNextGen/Public/Cache/CacheGenerator.h +++ b/Source/RpmNextGen/Public/Cache/CacheGenerator.h @@ -35,7 +35,6 @@ class RPMNEXTGEN_API FCacheGenerator void FetchAssetsForBaseModel(const FString& BaseModelID, const FString& AssetType); virtual void OnDownloadRemoteCacheComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful); - virtual void OnAssetDataLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FString& FilePath); void OnAssetSaved(bool bWasSuccessful); TUniquePtr AssetApi; TArray BaseModelAssets; diff --git a/Source/RpmNextGen/Public/Cache/StoredAsset.h b/Source/RpmNextGen/Public/Cache/StoredAsset.h new file mode 100644 index 0000000..221d361 --- /dev/null +++ b/Source/RpmNextGen/Public/Cache/StoredAsset.h @@ -0,0 +1,47 @@ +#pragma once + +#include "CoreMinimal.h" +#include "JsonObjectConverter.h" +#include "Api/Assets/Models/Asset.h" +#include "StoredAsset.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FStoredAsset +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FAsset Asset; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString GlbFilePath; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString IconFilePath; + + TSharedPtr ToJson() const + { + TSharedPtr JsonObject = MakeShared(); + JsonObject->SetStringField(TEXT("GlbFilePath"), GlbFilePath); + JsonObject->SetStringField(TEXT("IconFilePath"), IconFilePath); + + // Add the Asset fields as a sub-object + TSharedPtr AssetJson = MakeShared(); + FJsonObjectConverter::UStructToJsonObject(FAsset::StaticStruct(), &Asset, AssetJson.ToSharedRef(), 0, 0); + JsonObject->SetObjectField(TEXT("Asset"), AssetJson); + + return JsonObject; + } + + static FStoredAsset FromJson(TSharedPtr JsonObject) + { + FStoredAsset StoredAsset; + StoredAsset.GlbFilePath = JsonObject->GetStringField(TEXT("GlbFilePath")); + StoredAsset.IconFilePath = JsonObject->GetStringField(TEXT("IconFilePath")); + + const TSharedPtr AssetJson = JsonObject->GetObjectField(TEXT("Asset")); + FJsonObjectConverter::JsonObjectToUStruct(AssetJson.ToSharedRef(), FAsset::StaticStruct(), &StoredAsset.Asset, 0, 0); + + return StoredAsset; + } +}; diff --git a/Source/RpmNextGen/Public/RpmAssetLoaderComponent.h b/Source/RpmNextGen/Public/RpmAssetLoaderComponent.h index 2c4b1f5..fe112f0 100644 --- a/Source/RpmNextGen/Public/RpmAssetLoaderComponent.h +++ b/Source/RpmNextGen/Public/RpmAssetLoaderComponent.h @@ -7,7 +7,7 @@ #include "RpmAssetLoaderComponent.generated.h" class UglTFRuntimeAsset; -class FAssetLoader; +class FGlbLoader; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGltfAssetLoaded, UglTFRuntimeAsset*, Asset); UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) @@ -35,5 +35,5 @@ class RPMNEXTGEN_API URpmAssetLoaderComponent : public UActorComponent // Called every frame virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; private: - TSharedPtr AssetLoader; + TSharedPtr AssetLoader; }; diff --git a/Source/RpmNextGen/Public/RpmImageLoader.h b/Source/RpmNextGen/Public/RpmImageLoader.h index 43d4619..e3512ca 100644 --- a/Source/RpmNextGen/Public/RpmImageLoader.h +++ b/Source/RpmNextGen/Public/RpmImageLoader.h @@ -1,7 +1,5 @@ #pragma once -#pragma once - #include "CoreMinimal.h" #include "Interfaces/IHttpRequest.h" #include "Engine/Texture2D.h" @@ -11,6 +9,7 @@ class RPMNEXTGEN_API FRpmImageLoader { public: FRpmImageLoader() = default; + void LoadUImageFromURL(UImage* Image, const FString& URL); void LoadSImageFromURL(TSharedPtr ImageWidget, const FString& URL, TFunction OnImageUpdated); diff --git a/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp b/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp index 0b469ae..fc20291 100644 --- a/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp +++ b/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp @@ -62,7 +62,7 @@ void FEditorAssetLoader::LoadGLBFromURLWithId(const FString& URL, FString Loaded } OnAssetLoadComplete(gltfAsset, bWasSuccessful, LoadedAssetId); }); - LoadGLBFromURL(URL); + RequestFromUrl(URL); } void FEditorAssetLoader::LoadAssetToWorldAsURpmActor(UglTFRuntimeAsset* gltfAsset, FString AssetId) diff --git a/Source/RpmNextGenEditor/Private/UI/SCharacterLoaderWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SCharacterLoaderWidget.cpp index 5d40a8d..ea2ee6b 100644 --- a/Source/RpmNextGenEditor/Private/UI/SCharacterLoaderWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SCharacterLoaderWidget.cpp @@ -5,7 +5,7 @@ #include "Widgets/Text/STextBlock.h" #include "EditorStyleSet.h" #include "glTFRuntimeFunctionLibrary.h" -#include "Api/Assets/AssetLoader.h" +#include "Api/Files/GlbLoader.h" #include "PropertyCustomizationHelpers.h" #include "AssetRegistry/AssetData.h" diff --git a/Source/RpmNextGenEditor/Public/EditorAssetLoader.h b/Source/RpmNextGenEditor/Public/EditorAssetLoader.h index 2dfa540..e142f21 100644 --- a/Source/RpmNextGenEditor/Public/EditorAssetLoader.h +++ b/Source/RpmNextGenEditor/Public/EditorAssetLoader.h @@ -1,10 +1,10 @@ #pragma once #include "CoreMinimal.h" -#include "Api/Assets/AssetLoader.h" +#include "Api/Files/GlbLoader.h" #include "HAL/PlatformFilemanager.h" -class RPMNEXTGENEDITOR_API FEditorAssetLoader : public FAssetLoader +class RPMNEXTGENEDITOR_API FEditorAssetLoader : public FGlbLoader { public: void OnAssetLoadComplete(UglTFRuntimeAsset* gltfAsset, bool bWasSuccessful, diff --git a/Source/RpmNextGenEditor/Public/UI/SCharacterLoaderWidget.h b/Source/RpmNextGenEditor/Public/UI/SCharacterLoaderWidget.h index 8885a55..04a2038 100644 --- a/Source/RpmNextGenEditor/Public/UI/SCharacterLoaderWidget.h +++ b/Source/RpmNextGenEditor/Public/UI/SCharacterLoaderWidget.h @@ -6,7 +6,7 @@ #include "Widgets/DeclarativeSyntaxSupport.h" class FEditorAssetLoader; -class FAssetLoader; +class FGlbLoader; class SCharacterLoaderWidget : public SCompoundWidget { From 91f7c81b91543e5eb4ada5a53e0184db8b00ccde Mon Sep 17 00:00:00 2001 From: Harrison Date: Thu, 5 Sep 2024 08:50:01 +0300 Subject: [PATCH 12/54] chore: simplify assetsaver requests --- .../RpmNextGen/Private/Cache/AssetSaver.cpp | 85 ++++++++++--------- Source/RpmNextGen/Public/Cache/AssetSaver.h | 5 +- 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/Source/RpmNextGen/Private/Cache/AssetSaver.cpp b/Source/RpmNextGen/Private/Cache/AssetSaver.cpp index a42b04c..4b1a065 100644 --- a/Source/RpmNextGen/Private/Cache/AssetSaver.cpp +++ b/Source/RpmNextGen/Private/Cache/AssetSaver.cpp @@ -8,7 +8,7 @@ const FString FAssetSaver::CacheFolderPath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache"); -FAssetSaver::FAssetSaver(): bIsImageLoaded(false), bIsGlbLoaded(false) +FAssetSaver::FAssetSaver() { Http = &FHttpModule::Get(); } @@ -29,89 +29,92 @@ void FAssetSaver::LoadSaveAssetToCache(const FString& BaseModelId, const FAsset* StoredAsset.Asset = *Asset; StoredAsset.GlbFilePath = FString::Printf(TEXT("%s/%s.glb"), *Path, *Asset->Id); StoredAsset.IconFilePath = FString::Printf(TEXT("%s/%s.png"), *Path, *Asset->Id); + + // Start by loading and saving the GLB file, then chain the image loading LoadAndSaveGlb(StoredAsset); - LoadAndSaveImage(StoredAsset); } -void FAssetSaver::LoadAndSaveImage(const FStoredAsset& StoredAsset) +void FAssetSaver::LoadAndSaveGlb(const FStoredAsset& StoredAsset) { - if(StoredAsset.Asset.IconUrl.IsEmpty()) + if (StoredAsset.Asset.GlbUrl.IsEmpty()) { - UE_LOG(LogReadyPlayerMe, Error, TEXT("Icon URL is empty for asset: %s assetname: %s"), *StoredAsset.Asset.Id, *StoredAsset.Asset.Name); - bIsImageLoaded = true; + UE_LOG(LogReadyPlayerMe, Error, TEXT("Glb URL is empty for asset: %s assetname: %s"), *StoredAsset.Asset.Id, *StoredAsset.Asset.Name); + // Directly load the image if GLB URL is empty + LoadAndSaveImage(StoredAsset); return; } + TSharedRef Request = Http->CreateRequest(); - Request->SetURL(StoredAsset.Asset.IconUrl); + Request->SetURL(StoredAsset.Asset.GlbUrl); Request->SetVerb(TEXT("GET")); + // Capture TSharedPtr in the lambda to ensure it is not destroyed prematurely TSharedPtr ThisPtr = SharedThis(this); Request->OnProcessRequestComplete().BindLambda([ThisPtr, StoredAsset](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { - ThisPtr->OnAssetLoaded(Request, Response, bWasSuccessful, &StoredAsset); + ThisPtr->OnGlbLoaded(Request, Response, bWasSuccessful, &StoredAsset); }); Request->ProcessRequest(); } -void FAssetSaver::LoadAndSaveGlb(const FStoredAsset& StoredAsset) +void FAssetSaver::OnGlbLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FStoredAsset* StoredAsset) +{ + if (bWasSuccessful && Response.IsValid()) + { + SaveToFile(StoredAsset->GlbFilePath, Response->GetContent()); + } + else + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load GLB from url: %s"), *Request->GetURL()); + } + + // After the GLB is loaded (or failed), load and save the image + LoadAndSaveImage(*StoredAsset); +} + +void FAssetSaver::LoadAndSaveImage(const FStoredAsset& StoredAsset) { - if(StoredAsset.Asset.GlbUrl.IsEmpty()) + if (StoredAsset.Asset.IconUrl.IsEmpty()) { - UE_LOG(LogReadyPlayerMe, Error, TEXT("Glb URL is empty for asset: %s assetname: %s"), *StoredAsset.Asset.Id, *StoredAsset.Asset.Type); - bIsImageLoaded = true; + UE_LOG(LogReadyPlayerMe, Error, TEXT("Icon URL is empty for asset: %s assetname: %s"), *StoredAsset.Asset.Id, *StoredAsset.Asset.Name); + OnAssetSaved.ExecuteIfBound(false); // If both are empty, we still execute the delegate return; } + TSharedRef Request = Http->CreateRequest(); - Request->SetURL(StoredAsset.Asset.GlbUrl); + Request->SetURL(StoredAsset.Asset.IconUrl); Request->SetVerb(TEXT("GET")); // Capture TSharedPtr in the lambda to ensure it is not destroyed prematurely TSharedPtr ThisPtr = SharedThis(this); Request->OnProcessRequestComplete().BindLambda([ThisPtr, StoredAsset](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { - ThisPtr->OnAssetLoaded(Request, Response, bWasSuccessful, &StoredAsset); + ThisPtr->OnImageLoaded(Request, Response, bWasSuccessful, &StoredAsset); }); Request->ProcessRequest(); } -void FAssetSaver::OnAssetLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FStoredAsset* StoredAsset) +void FAssetSaver::OnImageLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FStoredAsset* StoredAsset) { - const bool bIsGlb = Request->GetURL().EndsWith(TEXT(".glb")); - if(bWasSuccessful && Response.IsValid()) - { - if(bIsGlb) - { - SaveToFile(StoredAsset->GlbFilePath, Response->GetContent()); - } - else - { - SaveToFile(StoredAsset->IconFilePath, Response->GetContent()); - - } - } - else - { - UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load image or .glb from url: %s"), *Request->GetURL()); - } - if(bIsGlb) + if (bWasSuccessful && Response.IsValid()) { - bIsGlbLoaded = true; + SaveToFile(StoredAsset->IconFilePath, Response->GetContent()); } else { - bIsImageLoaded = true; + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load image from url: %s"), *Request->GetURL()); } - if(bIsImageLoaded && bIsGlbLoaded) - { - FAssetStorageManager::Get().TrackStoredAsset(*StoredAsset); - UE_LOG(LogReadyPlayerMe, Log, TEXT("Asset saved to cache")); - OnAssetSaved.ExecuteIfBound(true); - } + // After the image is loaded, the process is complete + FAssetStorageManager::Get().TrackStoredAsset(*StoredAsset); + + UE_LOG(LogReadyPlayerMe, Log, TEXT("Asset saved to cache")); + OnAssetSaved.ExecuteIfBound(true); } + void FAssetSaver::SaveToFile(const FString& FilePath, const TArray& Data) { if (FFileHelper::SaveArrayToFile(Data, *FilePath)) diff --git a/Source/RpmNextGen/Public/Cache/AssetSaver.h b/Source/RpmNextGen/Public/Cache/AssetSaver.h index a8f117a..4c63b4c 100644 --- a/Source/RpmNextGen/Public/Cache/AssetSaver.h +++ b/Source/RpmNextGen/Public/Cache/AssetSaver.h @@ -19,13 +19,12 @@ class RPMNEXTGEN_API FAssetSaver : public TSharedFromThis void LoadSaveAssetToCache(const FString& BaseModelId, const FAsset* Asset); void LoadAndSaveImage(const FStoredAsset& StoredAsset); void LoadAndSaveGlb(const FStoredAsset& StoredAsset); - void OnAssetLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FStoredAsset* StoredAsset); + void OnGlbLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FStoredAsset* StoredAsset); + void OnImageLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FStoredAsset* StoredAsset); void SaveToFile(const FString& FilePath, const TArray& Data); FOnAssetSavedToCache OnAssetSaved; private: - bool bIsImageLoaded; - bool bIsGlbLoaded; static const FString CacheFolderPath; FHttpModule* Http; }; From 5383ab56bed8b24f8fa5097721b002cc1310e238 Mon Sep 17 00:00:00 2001 From: Harrison Date: Thu, 5 Sep 2024 13:40:02 +0300 Subject: [PATCH 13/54] chore: reworked asset loading and storing --- .../Private/Api/Assets/AssetApi.cpp | 5 + .../Private/Api/Assets/AssetLoader.cpp | 78 +++++++++-- .../RpmNextGen/Private/Cache/AssetSaver.cpp | 124 ------------------ .../Private/Cache/CacheGenerator.cpp | 22 ++-- .../Public/Api/Assets/AssetLoader.h | 42 ++++-- .../Public/Api/Assets/Models/Asset.h | 3 +- .../RpmNextGen/Public/Cache/AssetSaveData.h | 76 +++++++++++ Source/RpmNextGen/Public/Cache/AssetSaver.h | 32 ++--- .../Public/Cache/AssetStorageManager.h | 47 ++++--- .../RpmNextGen/Public/Cache/CacheGenerator.h | 11 +- Source/RpmNextGen/Public/Cache/StoredAsset.h | 47 ------- .../Private/EditorAssetLoader.cpp | 24 ++-- .../Private/UI/SCacheEditorWidget.cpp | 18 ++- .../Private/UI/SRpmDeveloperLoginWidget.cpp | 2 +- .../Public/EditorAssetLoader.h | 12 +- 15 files changed, 274 insertions(+), 269 deletions(-) create mode 100644 Source/RpmNextGen/Public/Cache/AssetSaveData.h delete mode 100644 Source/RpmNextGen/Public/Cache/StoredAsset.h diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp index beddb17..ba15c7a 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp @@ -132,3 +132,8 @@ void FAssetApi::HandleListAssetResponse(FString Response, bool bWasSuccessful) OnListAssetTypeResponse.ExecuteIfBound(FAssetTypeListResponse(), 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 index e5ac999..b4a4778 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp @@ -1,35 +1,85 @@ #include "Api/Assets/AssetLoader.h" + +#include "HttpModule.h" #include "Api/Assets/Models/Asset.h" +#include "Cache/AssetStorageManager.h" +#include "Interfaces/IHttpResponse.h" #include "RpmNextGenEditor/Public/EditorAssetLoader.h" -void FAssetLoader::LoadAsset(FAsset* Asset, bool bStoreInCache) +FAssetLoader::FAssetLoader() { - LoadAssetModel(Asset, bStoreInCache); - LoadAssetImage(Asset, bStoreInCache); + Http = &FHttpModule::Get(); +} + +FAssetLoader::~FAssetLoader() +{ } -void FAssetLoader::FileRequestComplete(TArray* Data, FAsset* Asset) +void FAssetLoader::LoadAsset(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache) { + const TSharedRef Context = MakeShared(Asset, BaseModelId, bStoreInCache); + LoadAssetImage(Context); } -void FAssetLoader::LoadAssetModel(FAsset* Asset, bool bStoreInCache) +void FAssetLoader::LoadAssetImage(TSharedRef Context) { - TSharedPtr FileApi = MakeShareable(new FFileApi()); + TSharedRef Request = Http->CreateRequest(); + Request->SetURL(Context->Asset.IconUrl); + Request->SetVerb(TEXT("GET")); + TSharedPtr ThisPtr = SharedThis(this); - FileApi->OnFileRequestComplete.BindLambda([ThisPtr, Asset](TArray* Data) + Request->OnProcessRequestComplete().BindLambda([ThisPtr, Context](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { - ThisPtr->FileRequestComplete(Data, Asset); + ThisPtr->AssetImageLoaded(Response, bWasSuccessful, Context); }); - FileApi->RequestFromUrl(Asset->GlbUrl); + Request->ProcessRequest(); +} + +void FAssetLoader::AssetImageLoaded(TSharedPtr Response, const bool bWasSuccessful, const TSharedRef& Context) +{ + if (bWasSuccessful && Response.IsValid()) + { + Context->ImageData = Response->GetContent(); + LoadAssetModel(Context); + OnAssetImageLoaded.ExecuteIfBound(Context->ImageData); + return; + } + UE_LOG(LogTemp, Error, TEXT("Failed to load image from URL: %s"), *Context->Asset.IconUrl); + LoadAssetModel(Context); + OnAssetImageLoaded.ExecuteIfBound(TArray()); } -void FAssetLoader::LoadAssetImage(FAsset* Asset, bool bStoreInCache) +void FAssetLoader::LoadAssetModel(TSharedRef Context) { - TSharedPtr FileApi = MakeShareable(new FFileApi()); + TSharedRef Request = Http->CreateRequest(); + Request->SetURL(Context->Asset.GlbUrl); + Request->SetVerb(TEXT("GET")); + TSharedPtr ThisPtr = SharedThis(this); - FileApi->OnFileRequestComplete.BindLambda([ThisPtr, Asset](TArray* Data) + Request->OnProcessRequestComplete().BindLambda([ThisPtr, Context](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { - ThisPtr->FileRequestComplete(Data, Asset); + ThisPtr->AssetModelLoaded(Response, bWasSuccessful, Context); }); - FileApi->RequestFromUrl(Asset->IconUrl); + Request->ProcessRequest(); +} + +void FAssetLoader::AssetModelLoaded(TSharedPtr Response, const bool bWasSuccessful, const TSharedRef& Context) +{ + if (bWasSuccessful && Response.IsValid()) + { + Context->GlbData = Response->GetContent(); + + if (Context->bStoreInCache) + { + FAssetStorageManager::Get().SaveAssetAndTrack(*Context); + OnAssetSaved.ExecuteIfBound(FAssetSaveData(Context->Asset, Context->BaseModelId)); + } + OnAssetGlbLoaded.ExecuteIfBound(Context->GlbData); + + UE_LOG(LogTemp, Log, TEXT("Asset loaded successfully.")); + return; + } + UE_LOG(LogTemp, Error, TEXT("Failed to load .glb model from URL: %s"), *Context->Asset.GlbUrl); + OnAssetGlbLoaded.ExecuteIfBound(TArray()); + OnAssetSaved.ExecuteIfBound(FAssetSaveData()); } diff --git a/Source/RpmNextGen/Private/Cache/AssetSaver.cpp b/Source/RpmNextGen/Private/Cache/AssetSaver.cpp index 4b1a065..0551a98 100644 --- a/Source/RpmNextGen/Private/Cache/AssetSaver.cpp +++ b/Source/RpmNextGen/Private/Cache/AssetSaver.cpp @@ -1,126 +1,2 @@ #include "Cache/AssetSaver.h" -#include "HttpModule.h" -#include "RpmNextGen.h" -#include "Api/Assets/Models/Asset.h" -#include "Cache/AssetStorageManager.h" -#include "Interfaces/IHttpResponse.h" -const FString FAssetSaver::CacheFolderPath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache"); - - -FAssetSaver::FAssetSaver() -{ - Http = &FHttpModule::Get(); -} - -FAssetSaver::~FAssetSaver() -{ - IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); - if (!PlatformFile.DirectoryExists(*CacheFolderPath)) - { - PlatformFile.CreateDirectory(*CacheFolderPath); - } -} - -void FAssetSaver::LoadSaveAssetToCache(const FString& BaseModelId, const FAsset* Asset) -{ - const FString Path = CacheFolderPath / BaseModelId; - FStoredAsset StoredAsset = FStoredAsset(); - StoredAsset.Asset = *Asset; - StoredAsset.GlbFilePath = FString::Printf(TEXT("%s/%s.glb"), *Path, *Asset->Id); - StoredAsset.IconFilePath = FString::Printf(TEXT("%s/%s.png"), *Path, *Asset->Id); - - // Start by loading and saving the GLB file, then chain the image loading - LoadAndSaveGlb(StoredAsset); -} - -void FAssetSaver::LoadAndSaveGlb(const FStoredAsset& StoredAsset) -{ - if (StoredAsset.Asset.GlbUrl.IsEmpty()) - { - UE_LOG(LogReadyPlayerMe, Error, TEXT("Glb URL is empty for asset: %s assetname: %s"), *StoredAsset.Asset.Id, *StoredAsset.Asset.Name); - // Directly load the image if GLB URL is empty - LoadAndSaveImage(StoredAsset); - return; - } - - TSharedRef Request = Http->CreateRequest(); - Request->SetURL(StoredAsset.Asset.GlbUrl); - Request->SetVerb(TEXT("GET")); - - // Capture TSharedPtr in the lambda to ensure it is not destroyed prematurely - TSharedPtr ThisPtr = SharedThis(this); - Request->OnProcessRequestComplete().BindLambda([ThisPtr, StoredAsset](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) - { - ThisPtr->OnGlbLoaded(Request, Response, bWasSuccessful, &StoredAsset); - }); - - Request->ProcessRequest(); -} - -void FAssetSaver::OnGlbLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FStoredAsset* StoredAsset) -{ - if (bWasSuccessful && Response.IsValid()) - { - SaveToFile(StoredAsset->GlbFilePath, Response->GetContent()); - } - else - { - UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load GLB from url: %s"), *Request->GetURL()); - } - - // After the GLB is loaded (or failed), load and save the image - LoadAndSaveImage(*StoredAsset); -} - -void FAssetSaver::LoadAndSaveImage(const FStoredAsset& StoredAsset) -{ - if (StoredAsset.Asset.IconUrl.IsEmpty()) - { - UE_LOG(LogReadyPlayerMe, Error, TEXT("Icon URL is empty for asset: %s assetname: %s"), *StoredAsset.Asset.Id, *StoredAsset.Asset.Name); - OnAssetSaved.ExecuteIfBound(false); // If both are empty, we still execute the delegate - return; - } - - TSharedRef Request = Http->CreateRequest(); - Request->SetURL(StoredAsset.Asset.IconUrl); - Request->SetVerb(TEXT("GET")); - - // Capture TSharedPtr in the lambda to ensure it is not destroyed prematurely - TSharedPtr ThisPtr = SharedThis(this); - Request->OnProcessRequestComplete().BindLambda([ThisPtr, StoredAsset](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) - { - ThisPtr->OnImageLoaded(Request, Response, bWasSuccessful, &StoredAsset); - }); - - Request->ProcessRequest(); -} - -void FAssetSaver::OnImageLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FStoredAsset* StoredAsset) -{ - if (bWasSuccessful && Response.IsValid()) - { - SaveToFile(StoredAsset->IconFilePath, Response->GetContent()); - } - else - { - UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load image from url: %s"), *Request->GetURL()); - } - - // After the image is loaded, the process is complete - FAssetStorageManager::Get().TrackStoredAsset(*StoredAsset); - - UE_LOG(LogReadyPlayerMe, Log, TEXT("Asset saved to cache")); - OnAssetSaved.ExecuteIfBound(true); -} - - -void FAssetSaver::SaveToFile(const FString& FilePath, const TArray& Data) -{ - if (FFileHelper::SaveArrayToFile(Data, *FilePath)) - { - UE_LOG(LogReadyPlayerMe, Log, TEXT("Successfully saved asset in local cache to: %s"), *FilePath); - return; - } - UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to saved asset local cache to: %s"), *FilePath); -} diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index a614de0..5aebf74 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -2,10 +2,10 @@ #include "HttpModule.h" #include "RpmNextGen.h" #include "Api/Assets/AssetApi.h" +#include "Api/Assets/AssetLoader.h" #include "Api/Assets/Models/AssetListRequest.h" #include "Api/Assets/Models/AssetTypeListRequest.h" #include "Api/Auth/ApiKeyAuthStrategy.h" -#include "Cache/AssetSaver.h" #include "Interfaces/IHttpRequest.h" #include "Interfaces/IHttpResponse.h" #include "Misc/Paths.h" @@ -14,7 +14,7 @@ #include "Misc/ScopeExit.h" #include "Settings/RpmDeveloperSettings.h" -const FString FCacheGenerator::CacheFolderPath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/LocalCache/Assets"); +const FString FCacheGenerator::CacheFolderPath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache"); const FString FCacheGenerator::ZipFileName = TEXT("CacheAssets.zip"); FCacheGenerator::FCacheGenerator() @@ -68,9 +68,7 @@ void FCacheGenerator::LoadAndStoreAssets() { PlatformFile.CreateDirectoryTree(*DirectoryPath); } - - // LoadAndStoreAssetFromUrl(BaseModel.GlbUrl, BaseModeFolder / FString::Printf( TEXT("%s.glb"), *BaseModel.Id)); - //LoadAndStoreAssetFromUrl(BaseModel.IconUrl, BaseModeFolder / FString::Printf( TEXT("%s.png"), *BaseModel.Id)); + LoadAndStoreAssetFromUrl(BaseModel.Id, &BaseModel); for (auto Pairs : BaseModelAssetsMap) { @@ -84,12 +82,12 @@ void FCacheGenerator::LoadAndStoreAssets() void FCacheGenerator::LoadAndStoreAssetFromUrl(const FString& BaseModelId, const FAsset* Asset) { - TSharedPtr AssetSaver = MakeShared(); - AssetSaver->OnAssetSaved.BindRaw(this, &FCacheGenerator::OnAssetSaved); - AssetSaver->LoadSaveAssetToCache(BaseModelId, Asset); + TSharedPtr AssetLoader = MakeShared(); + AssetLoader->OnAssetSaved.BindRaw( this, &FCacheGenerator::OnAssetSaved); + AssetLoader->LoadAsset(*Asset, BaseModelId, true); } -void FCacheGenerator::OnAssetSaved(bool bWasSuccessful) +void FCacheGenerator::OnAssetSaved(const FAssetSaveData& AssetSaveData) { AssetDownloadRequestsCompleted++; if(AssetDownloadRequestsCompleted >= RequiredAssetDownloadRequest) @@ -208,7 +206,7 @@ void FCacheGenerator::ExtractCache() // TODO add implementation } -void FCacheGenerator::FetchBaseModels() +void FCacheGenerator::FetchBaseModels() const { URpmDeveloperSettings* Settings = GetMutableDefault(); FAssetListRequest AssetListRequest = FAssetListRequest(); @@ -220,7 +218,7 @@ void FCacheGenerator::FetchBaseModels() UE_LOG(LogReadyPlayerMe, Log, TEXT("Fetching base models") ); } -void FCacheGenerator::FetchAssetTypes() +void FCacheGenerator::FetchAssetTypes() const { URpmDeveloperSettings* Settings = GetMutableDefault(); FAssetTypeListRequest AssetListRequest; @@ -231,7 +229,7 @@ void FCacheGenerator::FetchAssetTypes() AssetApi->ListAssetTypesAsync(AssetListRequest); } -void FCacheGenerator::FetchAssetsForBaseModel(const FString& BaseModelID, const FString& AssetType) +void FCacheGenerator::FetchAssetsForBaseModel(const FString& BaseModelID, const FString& AssetType) const { URpmDeveloperSettings *Settings = GetMutableDefault(); FAssetListQueryParams QueryParams = FAssetListQueryParams(); diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h b/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h index 12ea514..f6ef6ad 100644 --- a/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h +++ b/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h @@ -1,19 +1,45 @@ #pragma once #include "CoreMinimal.h" +#include "Models/Asset.h" +struct FAssetSaveData; +class IHttpResponse; +class FHttpModule; struct FAsset; +struct FAssetLoadingContext +{ + FAsset Asset; + FString BaseModelId; + TArray ImageData; + TArray GlbData; + bool bStoreInCache; + FAssetLoadingContext(const FAsset& InAsset, const FString& InBaseModelId, bool bInStoreInCache) + : Asset(InAsset), BaseModelId(InBaseModelId), bStoreInCache(bInStoreInCache) {} +}; + + + class RPMNEXTGEN_API FAssetLoader : public TSharedFromThis { public: - FAssetLoader() = default; - virtual ~FAssetLoader() = default; - void LoadAsset(FAsset* Asset, bool bStoreInCache); - virtual void FileRequestComplete(TArray* Data, FAsset* Asset); + + DECLARE_DELEGATE_OneParam(FOnAssetGlbLoaded, const TArray&); + DECLARE_DELEGATE_OneParam(FOnAsseImageLoaded, const TArray&); + DECLARE_DELEGATE_OneParam(FOnAssetSaved, const FAssetSaveData&); + + FAssetLoader(); + virtual ~FAssetLoader(); + void LoadAsset(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache); + FOnAssetGlbLoaded OnAssetGlbLoaded; + FOnAsseImageLoaded OnAssetImageLoaded; + FOnAssetSaved OnAssetSaved; private: - void LoadAssetModel(FAsset* Asset, bool bStoreInCache); - void LoadAssetImage(FAsset* Asset, bool bStoreInCache); - bool bIsModelLoaded; - bool bIsImageLoaded; + void LoadAssetModel(TSharedRef Context); + void LoadAssetImage(TSharedRef Context); + void AssetModelLoaded(TSharedPtr Response, bool bWasSuccessful, const TSharedRef& Context); + void AssetImageLoaded(TSharedPtr Response, bool bWasSuccessful, const TSharedRef& Context); + + FHttpModule* Http; }; diff --git a/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h b/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h index edffd8a..b1735e7 100644 --- a/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h +++ b/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h @@ -1,11 +1,10 @@ #pragma once #include "CoreMinimal.h" -#include "Api/Common/Models/ApiResponse.h" #include "Asset.generated.h" USTRUCT(BlueprintType) -struct RPMNEXTGEN_API FAsset : public FApiResponse +struct RPMNEXTGEN_API FAsset { GENERATED_BODY() diff --git a/Source/RpmNextGen/Public/Cache/AssetSaveData.h b/Source/RpmNextGen/Public/Cache/AssetSaveData.h new file mode 100644 index 0000000..d8b9735 --- /dev/null +++ b/Source/RpmNextGen/Public/Cache/AssetSaveData.h @@ -0,0 +1,76 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Assets/Models/Asset.h" +#include "AssetSaveData.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FAssetSaveData : public FAsset +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString BaseModelId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString GlbFilePath; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString IconFilePath; + + FAssetSaveData() + { + BaseModelId = FString(); + IconFilePath = FString(); + GlbFilePath = FString(); + } + + FAssetSaveData(const FAsset& InAsset, const FString& InBaseModelId) + { + const FString CacheFolderPath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache"); + Id = InAsset.Id; + Name = InAsset.Name; + GlbUrl = InAsset.GlbUrl; + IconUrl = InAsset.IconUrl; + Type = InAsset.Type; + CreatedAt = InAsset.CreatedAt; + UpdatedAt = InAsset.UpdatedAt; + BaseModelId = InBaseModelId; + IconFilePath = FString::Printf(TEXT("%s/%s/%s.png"), *CacheFolderPath, *BaseModelId, *Id); + GlbFilePath = FString::Printf(TEXT("%s/%s/%s.glb"), *CacheFolderPath, *BaseModelId, *Id); + } + + TSharedPtr ToJson() const + { + TSharedPtr JsonObject = MakeShared(); + JsonObject->SetStringField(TEXT("GlbFilePath"), GlbFilePath); + JsonObject->SetStringField(TEXT("IconFilePath"), IconFilePath); + JsonObject->SetStringField(TEXT("BaseModelId"), BaseModelId); + JsonObject->SetStringField(TEXT("Id"), Id); + JsonObject->SetStringField(TEXT("Name"), Name); + JsonObject->SetStringField(TEXT("GlbUrl"), GlbUrl); + JsonObject->SetStringField(TEXT("IconUrl"), IconUrl); + JsonObject->SetStringField(TEXT("Type"), Type); + JsonObject->SetStringField(TEXT("CreatedAt"), CreatedAt.ToString()); + JsonObject->SetStringField(TEXT("UpdatedAt"), UpdatedAt.ToString()); + + return JsonObject; + } + + static FAssetSaveData FromJson(const TSharedPtr& JsonObject) + { + FAssetSaveData StoredAsset; + StoredAsset.GlbFilePath = JsonObject->GetStringField(TEXT("GlbFilePath")); + StoredAsset.IconFilePath = JsonObject->GetStringField(TEXT("IconFilePath")); + StoredAsset.BaseModelId = JsonObject->GetStringField(TEXT("BaseModelId")); + StoredAsset.Id = JsonObject->GetStringField(TEXT("Id")); + StoredAsset.Name = JsonObject->GetStringField(TEXT("Name")); + StoredAsset.GlbUrl = JsonObject->GetStringField(TEXT("GlbUrl")); + StoredAsset.IconUrl = JsonObject->GetStringField(TEXT("IconUrl")); + StoredAsset.Type = JsonObject->GetStringField(TEXT("Type")); + FDateTime::Parse(JsonObject->GetStringField(TEXT("CreatedAt")), StoredAsset.CreatedAt); + FDateTime::Parse(JsonObject->GetStringField(TEXT("UpdatedAt")), StoredAsset.UpdatedAt); + + return StoredAsset; + } +}; diff --git a/Source/RpmNextGen/Public/Cache/AssetSaver.h b/Source/RpmNextGen/Public/Cache/AssetSaver.h index 4c63b4c..3805c80 100644 --- a/Source/RpmNextGen/Public/Cache/AssetSaver.h +++ b/Source/RpmNextGen/Public/Cache/AssetSaver.h @@ -2,29 +2,23 @@ #include "CoreMinimal.h" -struct FStoredAsset; -class FHttpModule; -class IHttpResponse; -class IHttpRequest; -struct FAsset; - DECLARE_DELEGATE_OneParam(FOnAssetSavedToCache, bool); class RPMNEXTGEN_API FAssetSaver : public TSharedFromThis { public: - FAssetSaver(); - virtual ~FAssetSaver(); - void LoadSaveAssetToCache(const FString& BaseModelId, const FAsset* Asset); - void LoadAndSaveImage(const FStoredAsset& StoredAsset); - void LoadAndSaveGlb(const FStoredAsset& StoredAsset); - void OnGlbLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FStoredAsset* StoredAsset); - void OnImageLoaded(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful, const FStoredAsset* StoredAsset); - void SaveToFile(const FString& FilePath, const TArray& Data); + void SaveToFile(const FString& FilePath, const TArray& Data) + { + if (FFileHelper::SaveArrayToFile(Data, *FilePath)) + { + UE_LOG(LogTemp, Log, TEXT("Successfully saved asset to: %s"), *FilePath); + OnAssetSavedToCache.ExecuteIfBound(true); + return; + } + UE_LOG(LogTemp, Error, TEXT("Failed to save asset to: %s"), *FilePath); + OnAssetSavedToCache.ExecuteIfBound(false); + } - FOnAssetSavedToCache OnAssetSaved; -private: - static const FString CacheFolderPath; - FHttpModule* Http; -}; + FOnAssetSavedToCache OnAssetSavedToCache; +}; \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Cache/AssetStorageManager.h b/Source/RpmNextGen/Public/Cache/AssetStorageManager.h index 9459d39..4fcd44b 100644 --- a/Source/RpmNextGen/Public/Cache/AssetStorageManager.h +++ b/Source/RpmNextGen/Public/Cache/AssetStorageManager.h @@ -1,6 +1,8 @@ #pragma once #include "RpmNextGen.h" -#include "StoredAsset.h" +#include "AssetSaveData.h" +#include "AssetSaver.h" +#include "Api/Assets/AssetLoader.h" class FAssetStorageManager { @@ -11,29 +13,38 @@ class FAssetStorageManager return Instance; } - void TrackStoredAsset(const FAsset& Asset, const FString& GlbFilePath, const FString& IconFilePath, const bool bSaveManifest = true) + void SaveAssetAndTrack(const FAssetLoadingContext& Context) { - FStoredAsset& StoredAsset = StoredAssets.FindOrAdd(Asset.Id); - StoredAsset.Asset = Asset; - StoredAsset.GlbFilePath = GlbFilePath; - StoredAsset.IconFilePath = IconFilePath; + const FAssetSaveData& StoredAsset = FAssetSaveData(Context.Asset, Context.BaseModelId); - if(bSaveManifest) - { - SaveManifest(); - } - UE_LOG(LogReadyPlayerMe, Log, TEXT("Tracked asset: AssetId=%s, GlbFilePath=%s, IconFilePath=%s"), *Asset.Id, *GlbFilePath, *IconFilePath); + FAssetSaver AssetSaver = FAssetSaver(); + AssetSaver.SaveToFile(StoredAsset.IconFilePath, Context.ImageData); + AssetSaver.SaveToFile(StoredAsset.GlbFilePath, Context.GlbData); + TrackStoredAsset(StoredAsset); } - void TrackStoredAsset(const FStoredAsset& StoredAsset, const bool bSaveManifest = true) + void TrackStoredAsset(const FAssetSaveData& StoredAsset, const bool bSaveManifest = true) { - StoredAssets.Add(StoredAsset.Asset.Id, StoredAsset); + FAssetSaveData* ExistingStoredAsset = StoredAssets.Find(StoredAsset.Id); + if(ExistingStoredAsset != nullptr) + { + // Update existing stored asset with new values if present + if(ExistingStoredAsset->GlbFilePath.IsEmpty() && !StoredAsset.GlbFilePath.IsEmpty()) + { + ExistingStoredAsset->GlbFilePath = StoredAsset.GlbFilePath; + } + if(ExistingStoredAsset->IconFilePath.IsEmpty() && !StoredAsset.IconFilePath.IsEmpty()) + { + ExistingStoredAsset->IconFilePath = StoredAsset.IconFilePath; + } + } + StoredAssets.Add(StoredAsset.Id, ExistingStoredAsset ? *ExistingStoredAsset : StoredAsset); if(bSaveManifest) { SaveManifest(); } - UE_LOG(LogReadyPlayerMe, Log, TEXT("Tracked asset: AssetId=%s, GlbFilePath=%s, IconFilePath=%s"), *StoredAsset.Asset.Id, *StoredAsset.GlbFilePath, *StoredAsset.IconFilePath); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Tracked asset: AssetId=%s, GlbFilePath=%s, IconFilePath=%s"), *StoredAsset.Id, *StoredAsset.GlbFilePath, *StoredAsset.IconFilePath); } void LoadManifest() @@ -51,8 +62,8 @@ class FAssetStorageManager TArray> AssetsArray = ManifestJson->GetArrayField(TEXT("TrackedAssets")); for (const TSharedPtr& AssetValue : AssetsArray) { - FStoredAsset StoredAsset = FStoredAsset::FromJson(AssetValue->AsObject()); - StoredAssets.Add(StoredAsset.Asset.Id, StoredAsset); + FAssetSaveData StoredAsset = FAssetSaveData::FromJson(AssetValue->AsObject()); + StoredAssets.Add(StoredAsset.Id, StoredAsset); } UE_LOG(LogReadyPlayerMe, Log, TEXT("Loaded manifest with %d assets"), StoredAssets.Num()); @@ -82,12 +93,12 @@ class FAssetStorageManager FFileHelper::SaveStringToFile(OutputString, *ManifestFilePath); } - const TMap& GetStoredAssets() const + const TMap& GetStoredAssets() const { return StoredAssets; } private: FAssetStorageManager() {} - TMap StoredAssets; + TMap StoredAssets; }; diff --git a/Source/RpmNextGen/Public/Cache/CacheGenerator.h b/Source/RpmNextGen/Public/Cache/CacheGenerator.h index 1f333d5..01d9926 100644 --- a/Source/RpmNextGen/Public/Cache/CacheGenerator.h +++ b/Source/RpmNextGen/Public/Cache/CacheGenerator.h @@ -1,6 +1,7 @@ #pragma once #include "Api/Assets/Models/AssetListResponse.h" +struct FAssetSaveData; class FAssetSaver; struct FAssetTypeListResponse; class FAssetApi; @@ -13,7 +14,7 @@ DECLARE_DELEGATE_OneParam(FOnCacheDataLoaded, bool); DECLARE_DELEGATE_OneParam(FOnLocalCacheGenerated, bool); DECLARE_DELEGATE_OneParam(FOnDownloadRemoteCache, bool); -class RPMNEXTGEN_API FCacheGenerator +class RPMNEXTGEN_API FCacheGenerator : public TSharedFromThis { public: virtual ~FCacheGenerator() = default; @@ -30,12 +31,12 @@ class RPMNEXTGEN_API FCacheGenerator void LoadAndStoreAssetFromUrl(const FString& BaseModelId, const FAsset* Asset); protected: - void FetchBaseModels(); - void FetchAssetTypes(); - void FetchAssetsForBaseModel(const FString& BaseModelID, const FString& AssetType); + void FetchBaseModels() const; + void FetchAssetTypes() const; + void FetchAssetsForBaseModel(const FString& BaseModelID, const FString& AssetType) const; virtual void OnDownloadRemoteCacheComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful); - void OnAssetSaved(bool bWasSuccessful); + void OnAssetSaved(const FAssetSaveData& AssetSaveData); TUniquePtr AssetApi; TArray BaseModelAssets; TArray AssetTypes; diff --git a/Source/RpmNextGen/Public/Cache/StoredAsset.h b/Source/RpmNextGen/Public/Cache/StoredAsset.h deleted file mode 100644 index 221d361..0000000 --- a/Source/RpmNextGen/Public/Cache/StoredAsset.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "JsonObjectConverter.h" -#include "Api/Assets/Models/Asset.h" -#include "StoredAsset.generated.h" - -USTRUCT(BlueprintType) -struct RPMNEXTGEN_API FStoredAsset -{ - GENERATED_BODY() - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") - FAsset Asset; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") - FString GlbFilePath; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") - FString IconFilePath; - - TSharedPtr ToJson() const - { - TSharedPtr JsonObject = MakeShared(); - JsonObject->SetStringField(TEXT("GlbFilePath"), GlbFilePath); - JsonObject->SetStringField(TEXT("IconFilePath"), IconFilePath); - - // Add the Asset fields as a sub-object - TSharedPtr AssetJson = MakeShared(); - FJsonObjectConverter::UStructToJsonObject(FAsset::StaticStruct(), &Asset, AssetJson.ToSharedRef(), 0, 0); - JsonObject->SetObjectField(TEXT("Asset"), AssetJson); - - return JsonObject; - } - - static FStoredAsset FromJson(TSharedPtr JsonObject) - { - FStoredAsset StoredAsset; - StoredAsset.GlbFilePath = JsonObject->GetStringField(TEXT("GlbFilePath")); - StoredAsset.IconFilePath = JsonObject->GetStringField(TEXT("IconFilePath")); - - const TSharedPtr AssetJson = JsonObject->GetObjectField(TEXT("Asset")); - FJsonObjectConverter::JsonObjectToUStruct(AssetJson.ToSharedRef(), FAsset::StaticStruct(), &StoredAsset.Asset, 0, 0); - - return StoredAsset; - } -}; diff --git a/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp b/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp index fc20291..4c9f015 100644 --- a/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp +++ b/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp @@ -13,14 +13,14 @@ FEditorAssetLoader::~FEditorAssetLoader() { } -void FEditorAssetLoader::OnAssetLoadComplete(UglTFRuntimeAsset* gltfAsset, bool bWasSuccessful, FString LoadedAssetId) +void FEditorAssetLoader::OnAssetLoadComplete(UglTFRuntimeAsset* GltfAsset, bool bWasSuccessful, FString LoadedAssetId) { if (bWasSuccessful) { - gltfAsset->AddToRoot(); - SaveAsUAsset(gltfAsset, LoadedAssetId); - LoadAssetToWorldAsURpmActor(gltfAsset, LoadedAssetId); - gltfAsset->RemoveFromRoot(); + GltfAsset->AddToRoot(); + SaveAsUAsset(GltfAsset, LoadedAssetId); + LoadAssetToWorldAsURpmActor(GltfAsset, LoadedAssetId); + GltfAsset->RemoveFromRoot(); } } @@ -49,7 +49,7 @@ USkeletalMesh* FEditorAssetLoader::SaveAsUAsset(UglTFRuntimeAsset* GltfAsset, co return skeletalMesh; } -void FEditorAssetLoader::LoadGLBFromURLWithId(const FString& URL, FString LoadedAssetId) +void FEditorAssetLoader::LoadGlbFromURLWithId(const FString& URL, FString LoadedAssetId) { OnGLtfAssetLoaded.BindLambda( [LoadedAssetId, this]( UglTFRuntimeAsset* gltfAsset, @@ -65,13 +65,13 @@ void FEditorAssetLoader::LoadGLBFromURLWithId(const FString& URL, FString Loaded RequestFromUrl(URL); } -void FEditorAssetLoader::LoadAssetToWorldAsURpmActor(UglTFRuntimeAsset* gltfAsset, FString AssetId) +void FEditorAssetLoader::LoadAssetToWorldAsURpmActor(UglTFRuntimeAsset* GltfAsset, FString AssetId) { - this->LoadAssetToWorld(AssetId, gltfAsset); + this->LoadAssetToWorld(AssetId, GltfAsset); } -void FEditorAssetLoader::LoadAssetToWorld(FString AssetId, UglTFRuntimeAsset* gltfAsset) +void FEditorAssetLoader::LoadAssetToWorld(const FString& AssetId, UglTFRuntimeAsset* GltfAsset) { if (!GEditor) { @@ -86,7 +86,7 @@ void FEditorAssetLoader::LoadAssetToWorld(FString AssetId, UglTFRuntimeAsset* gl return; } - if (gltfAsset) + if (GltfAsset) { FTransform Transform = FTransform::Identity; @@ -109,9 +109,9 @@ void FEditorAssetLoader::LoadAssetToWorld(FString AssetId, UglTFRuntimeAsset* gl // Register the actor in the editor world and update the editor GEditor->SelectActor(NewActor, true, true); GEditor->EditorUpdateComponents(); - if (gltfAsset) + if (GltfAsset) { - NewActor->LoadGltfAsset(gltfAsset); + NewActor->LoadGltfAsset(GltfAsset); } UE_LOG(LogReadyPlayerMe, Log, TEXT("Successfully loaded GLB asset into the editor world")); return; diff --git a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp index b5d766f..a9d4f53 100644 --- a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp @@ -176,7 +176,23 @@ FReply SCacheEditorWidget::OnExtractCacheClicked() FReply SCacheEditorWidget::OnOpenLocalCacheFolderClicked() { - // Handle opening the local cache folder + // Define the folder path you want to open (relative path) + FString FolderPath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache"); + + // Convert relative path to full absolute path + FString AbsoluteFolderPath = FPaths::ConvertRelativePathToFull(FolderPath); + + // Check if the folder exists + if (FPaths::DirectoryExists(AbsoluteFolderPath)) + { + // Open the folder in the file explorer + FPlatformProcess::LaunchFileInDefaultExternalApplication(*AbsoluteFolderPath); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("Folder does not exist: %s"), *AbsoluteFolderPath); + } + return FReply::Handled(); } diff --git a/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp index 15d1a85..2331db6 100644 --- a/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp @@ -304,7 +304,7 @@ void SRpmDeveloperLoginWidget::AddCharacterStyle(const FAsset& StyleAsset) void SRpmDeveloperLoginWidget::OnLoadStyleClicked(const FString& StyleId) { AssetLoader = FEditorAssetLoader(); - AssetLoader.LoadGLBFromURLWithId(CharacterStyleAssets[StyleId].GlbUrl, *StyleId); + AssetLoader.LoadGlbFromURLWithId(CharacterStyleAssets[StyleId].GlbUrl, *StyleId); } EVisibility SRpmDeveloperLoginWidget::GetLoginViewVisibility() const diff --git a/Source/RpmNextGenEditor/Public/EditorAssetLoader.h b/Source/RpmNextGenEditor/Public/EditorAssetLoader.h index e142f21..8e2b991 100644 --- a/Source/RpmNextGenEditor/Public/EditorAssetLoader.h +++ b/Source/RpmNextGenEditor/Public/EditorAssetLoader.h @@ -4,19 +4,19 @@ #include "Api/Files/GlbLoader.h" #include "HAL/PlatformFilemanager.h" -class RPMNEXTGENEDITOR_API FEditorAssetLoader : public FGlbLoader +class FEditorAssetLoader : public FGlbLoader { 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); + void LoadAssetToWorldAsURpmActor(UglTFRuntimeAsset* GltfAsset, FString AssetId = ""); + void LoadGlbFromURLWithId(const FString& URL, const FString AssetId); + void OnAssetLoadComplete(UglTFRuntimeAsset* GltfAsset, bool bWasSuccessful, FString LoadedAssetId); + USkeletalMesh* SaveAsUAsset(UglTFRuntimeAsset* GltfAsset, const FString& LoadedAssetId) const; USkeleton* SkeletonToCopy; private: - void LoadAssetToWorld(FString AssetId, UglTFRuntimeAsset* gltfAsset); + void LoadAssetToWorld(const FString& AssetId, UglTFRuntimeAsset* GltfAsset); }; From e92bebc687c157f42330fc215f8479aa28bed51c Mon Sep 17 00:00:00 2001 From: Harrison Date: Thu, 5 Sep 2024 15:21:00 +0300 Subject: [PATCH 14/54] chore: wip refactor --- .../Private/Api/Assets/AssetLoader.cpp | 53 ++++++++++++++++--- .../Private/Api/Characters/CharacterApi.cpp | 2 + .../Public/Api/Assets/AssetLoader.h | 10 ++-- .../Public/Api/Characters/CharacterApi.h | 1 + .../Public/Cache/AssetStorageManager.h | 21 +++++++- .../Public/RpmAssetLoaderComponent.h | 2 - 6 files changed, 76 insertions(+), 13 deletions(-) diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp index b4a4778..0cb65a9 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp @@ -18,9 +18,41 @@ FAssetLoader::~FAssetLoader() void FAssetLoader::LoadAsset(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache) { const TSharedRef Context = MakeShared(Asset, BaseModelId, bStoreInCache); + Context->bLoadGlb = true; + Context->bLoadImage = true; LoadAssetImage(Context); } +void FAssetLoader::LoadAssetGlb(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache) +{ + FAssetSaveData StoredAsset; + if(FAssetStorageManager::Get().GetCachedAsset(Asset.Id, StoredAsset)) + { + TArray glbData; + FFileHelper::LoadFileToArray(glbData, *StoredAsset.GlbFilePath); + OnAssetGlbLoaded.ExecuteIfBound(Asset, glbData); + return; + } + const TSharedRef Context = MakeShared(Asset, BaseModelId, bStoreInCache); + Context->bLoadGlb = true; + LoadAssetImage(Context); +} + +void FAssetLoader::LoadAssetIcon(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache) +{ + FAssetSaveData StoredAsset; + if(FAssetStorageManager::Get().GetCachedAsset(Asset.Id, StoredAsset)) + { + TArray iconData; + FFileHelper::LoadFileToArray(iconData, *StoredAsset.IconFilePath); + OnAssetGlbLoaded.ExecuteIfBound(Asset, iconData); + return; + } + const TSharedRef Context = MakeShared(Asset, BaseModelId, bStoreInCache); + Context->bLoadImage = true; + LoadAssetGlb(Context); +} + void FAssetLoader::LoadAssetImage(TSharedRef Context) { TSharedRef Request = Http->CreateRequest(); @@ -40,16 +72,23 @@ void FAssetLoader::AssetImageLoaded(TSharedPtr Response, const bo if (bWasSuccessful && Response.IsValid()) { Context->ImageData = Response->GetContent(); - LoadAssetModel(Context); - OnAssetImageLoaded.ExecuteIfBound(Context->ImageData); + if(!Context->bLoadGlb) + { + FAssetStorageManager::Get().SaveAssetAndTrack(*Context); + OnAssetImageLoaded.ExecuteIfBound(Context->Asset, Context->ImageData); + OnAssetSaved.ExecuteIfBound(FAssetSaveData(Context->Asset, Context->BaseModelId)); + return; + } + LoadAssetGlb(Context); + OnAssetImageLoaded.ExecuteIfBound(Context->Asset, Context->ImageData); return; } UE_LOG(LogTemp, Error, TEXT("Failed to load image from URL: %s"), *Context->Asset.IconUrl); - LoadAssetModel(Context); - OnAssetImageLoaded.ExecuteIfBound(TArray()); + LoadAssetGlb(Context); + OnAssetImageLoaded.ExecuteIfBound(Context->Asset, TArray()); } -void FAssetLoader::LoadAssetModel(TSharedRef Context) +void FAssetLoader::LoadAssetGlb(TSharedRef Context) { TSharedRef Request = Http->CreateRequest(); Request->SetURL(Context->Asset.GlbUrl); @@ -74,12 +113,12 @@ void FAssetLoader::AssetModelLoaded(TSharedPtr Response, const bo FAssetStorageManager::Get().SaveAssetAndTrack(*Context); OnAssetSaved.ExecuteIfBound(FAssetSaveData(Context->Asset, Context->BaseModelId)); } - OnAssetGlbLoaded.ExecuteIfBound(Context->GlbData); + OnAssetGlbLoaded.ExecuteIfBound(Context->Asset, Context->GlbData); UE_LOG(LogTemp, Log, TEXT("Asset loaded successfully.")); return; } UE_LOG(LogTemp, Error, TEXT("Failed to load .glb model from URL: %s"), *Context->Asset.GlbUrl); - OnAssetGlbLoaded.ExecuteIfBound(TArray()); + OnAssetGlbLoaded.ExecuteIfBound(Context->Asset, TArray()); OnAssetSaved.ExecuteIfBound(FAssetSaveData()); } diff --git a/Source/RpmNextGen/Private/Api/Characters/CharacterApi.cpp b/Source/RpmNextGen/Private/Api/Characters/CharacterApi.cpp index 8245e5d..3e129d9 100644 --- a/Source/RpmNextGen/Private/Api/Characters/CharacterApi.cpp +++ b/Source/RpmNextGen/Private/Api/Characters/CharacterApi.cpp @@ -25,6 +25,7 @@ FCharacterApi::~FCharacterApi() void FCharacterApi::CreateAsync(const FCharacterCreateRequest& Request) { + AssetByType.Append(Request.Data.Assets); FApiRequest ApiRequest; ApiRequest.Url = FString::Printf(TEXT("%s"), *BaseUrl); ApiRequest.Method = POST; @@ -35,6 +36,7 @@ void FCharacterApi::CreateAsync(const FCharacterCreateRequest& Request) void FCharacterApi::UpdateAsync(const FCharacterUpdateRequest& Request) { + AssetByType.Append(Request.Payload.Assets); FApiRequest ApiRequest; ApiRequest.Url = FString::Printf(TEXT("%s/%s"), *BaseUrl, *Request.Id); ApiRequest.Method = PATCH; diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h b/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h index f6ef6ad..b47aed2 100644 --- a/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h +++ b/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h @@ -15,6 +15,8 @@ struct FAssetLoadingContext TArray ImageData; TArray GlbData; bool bStoreInCache; + bool bLoadGlb; + bool bLoadImage; FAssetLoadingContext(const FAsset& InAsset, const FString& InBaseModelId, bool bInStoreInCache) : Asset(InAsset), BaseModelId(InBaseModelId), bStoreInCache(bInStoreInCache) {} }; @@ -25,18 +27,20 @@ class RPMNEXTGEN_API FAssetLoader : public TSharedFromThis { public: - DECLARE_DELEGATE_OneParam(FOnAssetGlbLoaded, const TArray&); - DECLARE_DELEGATE_OneParam(FOnAsseImageLoaded, const TArray&); + DECLARE_DELEGATE_TwoParams(FOnAssetGlbLoaded, const FAsset&, const TArray&); + DECLARE_DELEGATE_TwoParams(FOnAsseImageLoaded, const FAsset&, const TArray&); DECLARE_DELEGATE_OneParam(FOnAssetSaved, const FAssetSaveData&); FAssetLoader(); virtual ~FAssetLoader(); void LoadAsset(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache); + void LoadAssetGlb(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache); FOnAssetGlbLoaded OnAssetGlbLoaded; FOnAsseImageLoaded OnAssetImageLoaded; FOnAssetSaved OnAssetSaved; private: - void LoadAssetModel(TSharedRef Context); + void LoadAssetGlb(TSharedRef Context); + void LoadAssetIcon(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache); void LoadAssetImage(TSharedRef Context); void AssetModelLoaded(TSharedPtr Response, bool bWasSuccessful, const TSharedRef& Context); void AssetImageLoaded(TSharedPtr Response, bool bWasSuccessful, const TSharedRef& Context); diff --git a/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h b/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h index 2dd5d0c..d7d001a 100644 --- a/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h +++ b/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h @@ -42,6 +42,7 @@ class RPMNEXTGEN_API FCharacterApi : public TSharedFromThis AssetByType = TMap(); }; template diff --git a/Source/RpmNextGen/Public/Cache/AssetStorageManager.h b/Source/RpmNextGen/Public/Cache/AssetStorageManager.h index 4fcd44b..b25f266 100644 --- a/Source/RpmNextGen/Public/Cache/AssetStorageManager.h +++ b/Source/RpmNextGen/Public/Cache/AssetStorageManager.h @@ -98,7 +98,26 @@ class FAssetStorageManager return StoredAssets; } + bool IsAssetCached(const FString& AssetId) const + { + return StoredAssets.Contains(AssetId); + } + + bool GetCachedAsset(const FString& AssetId, FAssetSaveData& OutAsset) const + { + const FAssetSaveData* StoredAsset = StoredAssets.Find(AssetId); + if(StoredAsset != nullptr) + { + OutAsset = *StoredAsset; + return true; + } + return false; + } + private: - FAssetStorageManager() {} + FAssetStorageManager() + { + LoadManifest(); + } TMap StoredAssets; }; diff --git a/Source/RpmNextGen/Public/RpmAssetLoaderComponent.h b/Source/RpmNextGen/Public/RpmAssetLoaderComponent.h index fe112f0..0f538ec 100644 --- a/Source/RpmNextGen/Public/RpmAssetLoaderComponent.h +++ b/Source/RpmNextGen/Public/RpmAssetLoaderComponent.h @@ -21,8 +21,6 @@ class RPMNEXTGEN_API URpmAssetLoaderComponent : public UActorComponent virtual void LoadCharacterFromUrl(FString Url); - - UPROPERTY(BlueprintAssignable, Category = "Ready Player Me" ) FOnGltfAssetLoaded OnGltfAssetLoaded; From 625076bcdacaeee3540655211aae88befc531753 Mon Sep 17 00:00:00 2001 From: Harrison Date: Fri, 6 Sep 2024 11:18:13 +0300 Subject: [PATCH 15/54] feat: added asset type storing --- .../Private/Cache/CacheGenerator.cpp | 7 +-- .../Public/Cache/AssetStorageManager.h | 52 ++++++++++++++----- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index 5aebf74..f15fd56 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -1,4 +1,4 @@ -#include "Cache/CacheGenerator.h" +#include "Cache/CacheGenerator.h" #include "HttpModule.h" #include "RpmNextGen.h" #include "Api/Assets/AssetApi.h" @@ -143,7 +143,8 @@ void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListRe void FCacheGenerator::FetchAssetsForEachBaseModel() { - RequiredRefittedAssetRequests = AssetTypes.Num() * BaseModelAssets.Num(); + const int TypesExcludingBaseModel = AssetTypes.Num() - 1; + RequiredRefittedAssetRequests = TypesExcludingBaseModel * BaseModelAssets.Num(); for (FAsset& BaseModel : BaseModelAssets) { for(FString AssetType : AssetTypes) @@ -159,6 +160,7 @@ void FCacheGenerator::OnListAssetTypesResponse(const FAssetTypeListResponse& Ass { UE_LOG(LogReadyPlayerMe, Log, TEXT("Fetched %d asset types"), AssetListResponse.Data.Num()); AssetTypes.Append(AssetListResponse.Data); + FAssetStorageManager::Get().StoreAssetTypes(AssetTypes); FetchAssetsForEachBaseModel(); return; } @@ -224,7 +226,6 @@ void FCacheGenerator::FetchAssetTypes() const FAssetTypeListRequest AssetListRequest; FAssetTypeListQueryParams QueryParams = FAssetTypeListQueryParams(); QueryParams.ApplicationId = Settings->ApplicationId; - QueryParams.ExcludeTypes = "baseModel"; AssetListRequest.Params = QueryParams; AssetApi->ListAssetTypesAsync(AssetListRequest); } diff --git a/Source/RpmNextGen/Public/Cache/AssetStorageManager.h b/Source/RpmNextGen/Public/Cache/AssetStorageManager.h index b25f266..ee72a73 100644 --- a/Source/RpmNextGen/Public/Cache/AssetStorageManager.h +++ b/Source/RpmNextGen/Public/Cache/AssetStorageManager.h @@ -13,19 +13,41 @@ class FAssetStorageManager return Instance; } - void SaveAssetAndTrack(const FAssetLoadingContext& Context) + static void StoreAssetTypes(const TArray& TypeList) + { + FString TypeListFilePath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache/TypeList.json"); + + // Convert the TArray to TArray> + TArray> JsonValues; + for (const FString& Type : TypeList) + { + JsonValues.Add(MakeShared(Type)); + } + + FString OutputString; + const TSharedRef> Writer = TJsonWriterFactory<>::Create(&OutputString); + + // Serialize the array of TSharedPtr + FJsonSerializer::Serialize(JsonValues, Writer); + + // Save the resulting JSON string to file + FFileHelper::SaveStringToFile(OutputString, *TypeListFilePath); + } + + void StoreAndTrackAsset(const FAssetLoadingContext& Context) { const FAssetSaveData& StoredAsset = FAssetSaveData(Context.Asset, Context.BaseModelId); FAssetSaver AssetSaver = FAssetSaver(); - AssetSaver.SaveToFile(StoredAsset.IconFilePath, Context.ImageData); - AssetSaver.SaveToFile(StoredAsset.GlbFilePath, Context.GlbData); - TrackStoredAsset(StoredAsset); + AssetSaver.SaveToFile(StoredAsset.IconFilePath, Context.D); + AssetSaver.SaveToFile(StoredAsset.GlbFilePath, Context.Data); + StoreAndTrackAsset(StoredAsset); } - void TrackStoredAsset(const FAssetSaveData& StoredAsset, const bool bSaveManifest = true) + void StoreAndTrackAsset(const FAssetSaveData& StoredAsset, const bool bSaveManifest = true) { - FAssetSaveData* ExistingStoredAsset = StoredAssets.Find(StoredAsset.Id); + FString CombinedId = StoredAsset.Id + StoredAsset.BaseModelId; + FAssetSaveData* ExistingStoredAsset = StoredAssets.Find(CombinedId); if(ExistingStoredAsset != nullptr) { // Update existing stored asset with new values if present @@ -38,7 +60,7 @@ class FAssetStorageManager ExistingStoredAsset->IconFilePath = StoredAsset.IconFilePath; } } - StoredAssets.Add(StoredAsset.Id, ExistingStoredAsset ? *ExistingStoredAsset : StoredAsset); + StoredAssets.Add(CombinedId, ExistingStoredAsset ? *ExistingStoredAsset : StoredAsset); if(bSaveManifest) { @@ -59,11 +81,13 @@ class FAssetStorageManager if (FJsonSerializer::Deserialize(Reader, ManifestJson) && ManifestJson.IsValid()) { - TArray> AssetsArray = ManifestJson->GetArrayField(TEXT("TrackedAssets")); - for (const TSharedPtr& AssetValue : AssetsArray) + const TSharedPtr TrackedAssetsJson = ManifestJson->GetObjectField(TEXT("TrackedAssets")); + for (const auto& Entry : TrackedAssetsJson->Values) { - FAssetSaveData StoredAsset = FAssetSaveData::FromJson(AssetValue->AsObject()); - StoredAssets.Add(StoredAsset.Id, StoredAsset); + const FString& AssetId = Entry.Key; + const TSharedPtr AssetData = Entry.Value->AsObject(); + FAssetSaveData StoredAsset = FAssetSaveData::FromJson(AssetData); + StoredAssets.Add(AssetId, StoredAsset); } UE_LOG(LogReadyPlayerMe, Log, TEXT("Loaded manifest with %d assets"), StoredAssets.Num()); @@ -76,15 +100,15 @@ class FAssetStorageManager const FString ManifestFilePath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache/AssetManifest.json"); TSharedPtr ManifestJson = MakeShared(); - TArray> AssetsArray; + TSharedPtr TrackedAssetsJson = MakeShared(); for (const auto& Entry : StoredAssets) { TSharedPtr AssetJson = Entry.Value.ToJson(); - AssetsArray.Add(MakeShared(AssetJson)); + TrackedAssetsJson->SetObjectField(Entry.Key, AssetJson); } - ManifestJson->SetArrayField(TEXT("TrackedAssets"), AssetsArray); + ManifestJson->SetObjectField(TEXT("TrackedAssets"), TrackedAssetsJson); FString OutputString; TSharedRef> Writer = TJsonWriterFactory<>::Create(&OutputString); From 0d0eef31d1842e8cd0363fdf008368c29e8d1be3 Mon Sep 17 00:00:00 2001 From: Harrison Date: Fri, 6 Sep 2024 12:06:24 +0300 Subject: [PATCH 16/54] chore: fix errors --- Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp | 4 ++-- Source/RpmNextGen/Private/Cache/CacheGenerator.cpp | 5 +++++ Source/RpmNextGen/Public/Cache/AssetSaveData.h | 2 +- Source/RpmNextGen/Public/Cache/AssetSaver.h | 7 +++++++ Source/RpmNextGen/Public/Cache/AssetStorageManager.h | 4 ++-- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp index 0cb65a9..53a08e5 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp @@ -74,7 +74,7 @@ void FAssetLoader::AssetImageLoaded(TSharedPtr Response, const bo Context->ImageData = Response->GetContent(); if(!Context->bLoadGlb) { - FAssetStorageManager::Get().SaveAssetAndTrack(*Context); + FAssetStorageManager::Get().StoreAndTrackAsset(*Context); OnAssetImageLoaded.ExecuteIfBound(Context->Asset, Context->ImageData); OnAssetSaved.ExecuteIfBound(FAssetSaveData(Context->Asset, Context->BaseModelId)); return; @@ -110,7 +110,7 @@ void FAssetLoader::AssetModelLoaded(TSharedPtr Response, const bo if (Context->bStoreInCache) { - FAssetStorageManager::Get().SaveAssetAndTrack(*Context); + FAssetStorageManager::Get().StoreAndTrackAsset(*Context); OnAssetSaved.ExecuteIfBound(FAssetSaveData(Context->Asset, Context->BaseModelId)); } OnAssetGlbLoaded.ExecuteIfBound(Context->Asset, Context->GlbData); diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index f15fd56..7da49f9 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -6,6 +6,7 @@ #include "Api/Assets/Models/AssetListRequest.h" #include "Api/Assets/Models/AssetTypeListRequest.h" #include "Api/Auth/ApiKeyAuthStrategy.h" +#include "Cache/AssetStorageManager.h" #include "Interfaces/IHttpRequest.h" #include "Interfaces/IHttpResponse.h" #include "Misc/Paths.h" @@ -149,6 +150,10 @@ void FCacheGenerator::FetchAssetsForEachBaseModel() { for(FString AssetType : AssetTypes) { + if(AssetType == FAssetApi::BaseModelType) + { + continue; + } FetchAssetsForBaseModel(BaseModel.Id, AssetType); } } diff --git a/Source/RpmNextGen/Public/Cache/AssetSaveData.h b/Source/RpmNextGen/Public/Cache/AssetSaveData.h index d8b9735..d3c538e 100644 --- a/Source/RpmNextGen/Public/Cache/AssetSaveData.h +++ b/Source/RpmNextGen/Public/Cache/AssetSaveData.h @@ -36,7 +36,7 @@ struct RPMNEXTGEN_API FAssetSaveData : public FAsset CreatedAt = InAsset.CreatedAt; UpdatedAt = InAsset.UpdatedAt; BaseModelId = InBaseModelId; - IconFilePath = FString::Printf(TEXT("%s/%s/%s.png"), *CacheFolderPath, *BaseModelId, *Id); + IconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *CacheFolderPath, *Id); GlbFilePath = FString::Printf(TEXT("%s/%s/%s.glb"), *CacheFolderPath, *BaseModelId, *Id); } diff --git a/Source/RpmNextGen/Public/Cache/AssetSaver.h b/Source/RpmNextGen/Public/Cache/AssetSaver.h index 3805c80..86dee1f 100644 --- a/Source/RpmNextGen/Public/Cache/AssetSaver.h +++ b/Source/RpmNextGen/Public/Cache/AssetSaver.h @@ -10,6 +10,13 @@ class RPMNEXTGEN_API FAssetSaver : public TSharedFromThis public: void SaveToFile(const FString& FilePath, const TArray& Data) { + // skip if file already exists + if (FPaths::FileExists(FilePath)) + { + UE_LOG(LogTemp, Log, TEXT("Asset already exists at: %s"), *FilePath); + OnAssetSavedToCache.ExecuteIfBound(true); + return; + } if (FFileHelper::SaveArrayToFile(Data, *FilePath)) { UE_LOG(LogTemp, Log, TEXT("Successfully saved asset to: %s"), *FilePath); diff --git a/Source/RpmNextGen/Public/Cache/AssetStorageManager.h b/Source/RpmNextGen/Public/Cache/AssetStorageManager.h index ee72a73..5c37feb 100644 --- a/Source/RpmNextGen/Public/Cache/AssetStorageManager.h +++ b/Source/RpmNextGen/Public/Cache/AssetStorageManager.h @@ -39,8 +39,8 @@ class FAssetStorageManager const FAssetSaveData& StoredAsset = FAssetSaveData(Context.Asset, Context.BaseModelId); FAssetSaver AssetSaver = FAssetSaver(); - AssetSaver.SaveToFile(StoredAsset.IconFilePath, Context.D); - AssetSaver.SaveToFile(StoredAsset.GlbFilePath, Context.Data); + AssetSaver.SaveToFile(StoredAsset.IconFilePath, Context.ImageData); + AssetSaver.SaveToFile(StoredAsset.GlbFilePath, Context.GlbData); StoreAndTrackAsset(StoredAsset); } From 043067089157e6086cab7fafa1248baa98b58ade Mon Sep 17 00:00:00 2001 From: Harrison Date: Fri, 6 Sep 2024 13:14:47 +0300 Subject: [PATCH 17/54] chore: updates --- .../Private/Api/Assets/AssetApi.cpp | 9 ++- .../Private/Api/Assets/AssetLoader.cpp | 57 +++++++------------ .../Private/Cache/CacheGenerator.cpp | 12 +++- .../Public/Api/Assets/AssetLoader.h | 23 ++++---- .../Api/Assets/Models/AssetListResponse.h | 1 + .../Public/Cache/AssetStorageManager.h | 11 +++- .../RpmNextGen/Public/Cache/CacheGenerator.h | 5 +- 7 files changed, 62 insertions(+), 56 deletions(-) diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp index ba15c7a..5ac7078 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp @@ -3,6 +3,7 @@ #include "Api/Assets/Models/AssetListRequest.h" #include "Api/Assets/Models/AssetListResponse.h" #include "Api/Assets/Models/AssetTypeListRequest.h" +#include "Api/Auth/ApiKeyAuthStrategy.h" struct FAssetTypeListRequest; const FString FAssetApi::BaseModelType = TEXT("baseModel"); @@ -10,12 +11,18 @@ const FString FAssetApi::BaseModelType = TEXT("baseModel"); FAssetApi::FAssetApi() { OnApiResponse.BindRaw(this, &FAssetApi::HandleListAssetResponse); - const URpmDeveloperSettings* Settings = GetMutableDefault(); + + const URpmDeveloperSettings* Settings = GetDefault(); if(Settings->ApplicationId.IsEmpty()) { UE_LOG(LogTemp, Error, TEXT("Application ID is empty. Please set the Application ID in the Ready Player Me Developer Settings")); } + + if(!Settings->ApiKey.IsEmpty()) + { + SetAuthenticationStrategy(new FApiKeyAuthStrategy()); + } } void FAssetApi::ListAssetsAsync(const FAssetListRequest& Request) diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp index 53a08e5..5903dce 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp @@ -15,27 +15,19 @@ FAssetLoader::~FAssetLoader() { } -void FAssetLoader::LoadAsset(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache) -{ - const TSharedRef Context = MakeShared(Asset, BaseModelId, bStoreInCache); - Context->bLoadGlb = true; - Context->bLoadImage = true; - LoadAssetImage(Context); -} - void FAssetLoader::LoadAssetGlb(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache) { FAssetSaveData StoredAsset; if(FAssetStorageManager::Get().GetCachedAsset(Asset.Id, StoredAsset)) { - TArray glbData; - FFileHelper::LoadFileToArray(glbData, *StoredAsset.GlbFilePath); - OnAssetGlbLoaded.ExecuteIfBound(Asset, glbData); + TArray GlbData; + FFileHelper::LoadFileToArray(GlbData, *StoredAsset.GlbFilePath); + OnAssetGlbLoaded.ExecuteIfBound(Asset, GlbData); return; } const TSharedRef Context = MakeShared(Asset, BaseModelId, bStoreInCache); - Context->bLoadGlb = true; - LoadAssetImage(Context); + Context->bIsGLb = true; + LoadAssetGlb(Context); } void FAssetLoader::LoadAssetIcon(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache) @@ -43,17 +35,17 @@ void FAssetLoader::LoadAssetIcon(const FAsset& Asset, const FString& BaseModelId FAssetSaveData StoredAsset; if(FAssetStorageManager::Get().GetCachedAsset(Asset.Id, StoredAsset)) { - TArray iconData; - FFileHelper::LoadFileToArray(iconData, *StoredAsset.IconFilePath); - OnAssetGlbLoaded.ExecuteIfBound(Asset, iconData); + TArray IconData; + FFileHelper::LoadFileToArray(IconData, *StoredAsset.IconFilePath); + OnAssetGlbLoaded.ExecuteIfBound(Asset, IconData); return; } const TSharedRef Context = MakeShared(Asset, BaseModelId, bStoreInCache); - Context->bLoadImage = true; - LoadAssetGlb(Context); + Context->bIsGLb = false; + LoadAssetIcon(Context); } -void FAssetLoader::LoadAssetImage(TSharedRef Context) +void FAssetLoader::LoadAssetIcon(TSharedRef Context) { TSharedRef Request = Http->CreateRequest(); Request->SetURL(Context->Asset.IconUrl); @@ -62,30 +54,25 @@ void FAssetLoader::LoadAssetImage(TSharedRef Context) TSharedPtr ThisPtr = SharedThis(this); Request->OnProcessRequestComplete().BindLambda([ThisPtr, Context](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { - ThisPtr->AssetImageLoaded(Response, bWasSuccessful, Context); + ThisPtr->AssetIconLoaded(Response, bWasSuccessful, Context); }); Request->ProcessRequest(); } -void FAssetLoader::AssetImageLoaded(TSharedPtr Response, const bool bWasSuccessful, const TSharedRef& Context) +void FAssetLoader::AssetIconLoaded(TSharedPtr Response, const bool bWasSuccessful, const TSharedRef& Context) { if (bWasSuccessful && Response.IsValid()) { - Context->ImageData = Response->GetContent(); - if(!Context->bLoadGlb) + Context->Data = Response->GetContent(); + if(!Context->bStoreInCache) { FAssetStorageManager::Get().StoreAndTrackAsset(*Context); - OnAssetImageLoaded.ExecuteIfBound(Context->Asset, Context->ImageData); - OnAssetSaved.ExecuteIfBound(FAssetSaveData(Context->Asset, Context->BaseModelId)); - return; } - LoadAssetGlb(Context); - OnAssetImageLoaded.ExecuteIfBound(Context->Asset, Context->ImageData); + OnAssetIconLoaded.ExecuteIfBound(Context->Asset, Context->Data); return; } UE_LOG(LogTemp, Error, TEXT("Failed to load image from URL: %s"), *Context->Asset.IconUrl); - LoadAssetGlb(Context); - OnAssetImageLoaded.ExecuteIfBound(Context->Asset, TArray()); + OnAssetIconLoaded.ExecuteIfBound(Context->Asset, TArray()); } void FAssetLoader::LoadAssetGlb(TSharedRef Context) @@ -97,28 +84,26 @@ void FAssetLoader::LoadAssetGlb(TSharedRef Context) TSharedPtr ThisPtr = SharedThis(this); Request->OnProcessRequestComplete().BindLambda([ThisPtr, Context](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { - ThisPtr->AssetModelLoaded(Response, bWasSuccessful, Context); + ThisPtr->AssetGlbLoaded(Response, bWasSuccessful, Context); }); Request->ProcessRequest(); } -void FAssetLoader::AssetModelLoaded(TSharedPtr Response, const bool bWasSuccessful, const TSharedRef& Context) +void FAssetLoader::AssetGlbLoaded(TSharedPtr Response, const bool bWasSuccessful, const TSharedRef& Context) { if (bWasSuccessful && Response.IsValid()) { - Context->GlbData = Response->GetContent(); + Context->Data = Response->GetContent(); if (Context->bStoreInCache) { FAssetStorageManager::Get().StoreAndTrackAsset(*Context); - OnAssetSaved.ExecuteIfBound(FAssetSaveData(Context->Asset, Context->BaseModelId)); } - OnAssetGlbLoaded.ExecuteIfBound(Context->Asset, Context->GlbData); + OnAssetGlbLoaded.ExecuteIfBound(Context->Asset, Context->Data); UE_LOG(LogTemp, Log, TEXT("Asset loaded successfully.")); return; } UE_LOG(LogTemp, Error, TEXT("Failed to load .glb model from URL: %s"), *Context->Asset.GlbUrl); OnAssetGlbLoaded.ExecuteIfBound(Context->Asset, TArray()); - OnAssetSaved.ExecuteIfBound(FAssetSaveData()); } diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index 7da49f9..7f2ddaf 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -57,8 +57,16 @@ void FCacheGenerator::LoadAndStoreAssets() { RefittedAssetCount += Pairs.Value.Num(); } - } - RequiredAssetDownloadRequest = RefittedAssetCount + BaseModelAssets.Num(); + } + const int AssetIconRequestCount = BaseModelAssetsMap[BaseModelAssets[0].Id].Num(); + for (auto Asset : BaseModelAssetsMap[BaseModelAssets[0].Id]) + { + LoadAndStoreAssetIcon(BaseModelAssets[0].Id, &Asset); + } + UE_LOG(LogReadyPlayerMe, Log, TEXT("RefittedAssetCount: %d. AssetIconRequestCount: %d BaseModelAssetsCount %d"), RefittedAssetCount, AssetIconRequestCount, BaseModelAssets.Num()); + + RequiredAssetDownloadRequest = RefittedAssetCount + BaseModelAssets.Num() + AssetIconRequestCount; + return; UE_LOG(LogReadyPlayerMe, Log, TEXT("Total assets to download: %d. Total refitted assets to fetch: %d"), RequiredAssetDownloadRequest, RequiredRefittedAssetRequests); for (auto BaseModel : BaseModelAssets) { diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h b/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h index b47aed2..ed50494 100644 --- a/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h +++ b/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h @@ -12,17 +12,15 @@ struct FAssetLoadingContext { FAsset Asset; FString BaseModelId; - TArray ImageData; - TArray GlbData; + TArray Data; bool bStoreInCache; - bool bLoadGlb; - bool bLoadImage; + bool bIsGLb; FAssetLoadingContext(const FAsset& InAsset, const FString& InBaseModelId, bool bInStoreInCache) - : Asset(InAsset), BaseModelId(InBaseModelId), bStoreInCache(bInStoreInCache) {} + : Asset(InAsset), BaseModelId(InBaseModelId), bStoreInCache(bInStoreInCache), bIsGLb(false) + { + } }; - - class RPMNEXTGEN_API FAssetLoader : public TSharedFromThis { public: @@ -33,17 +31,16 @@ class RPMNEXTGEN_API FAssetLoader : public TSharedFromThis FAssetLoader(); virtual ~FAssetLoader(); - void LoadAsset(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache); + void LoadAssetIcon(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache); void LoadAssetGlb(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache); FOnAssetGlbLoaded OnAssetGlbLoaded; - FOnAsseImageLoaded OnAssetImageLoaded; + FOnAsseImageLoaded OnAssetIconLoaded; FOnAssetSaved OnAssetSaved; private: void LoadAssetGlb(TSharedRef Context); - void LoadAssetIcon(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache); - void LoadAssetImage(TSharedRef Context); - void AssetModelLoaded(TSharedPtr Response, bool bWasSuccessful, const TSharedRef& Context); - void AssetImageLoaded(TSharedPtr Response, bool bWasSuccessful, const TSharedRef& Context); + void LoadAssetIcon(TSharedRef Context); + void AssetGlbLoaded(TSharedPtr Response, bool bWasSuccessful, const TSharedRef& Context); + void AssetIconLoaded(TSharedPtr Response, bool bWasSuccessful, const TSharedRef& Context); FHttpModule* Http; }; diff --git a/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h index f6ade59..e6bfa81 100644 --- a/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h +++ b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h @@ -2,6 +2,7 @@ #include "CoreMinimal.h" #include "Api/Assets/Models/Asset.h" +#include "Api/Common/Models/ApiResponse.h" #include "AssetListResponse.generated.h" USTRUCT(BlueprintType) diff --git a/Source/RpmNextGen/Public/Cache/AssetStorageManager.h b/Source/RpmNextGen/Public/Cache/AssetStorageManager.h index 5c37feb..fb06cfb 100644 --- a/Source/RpmNextGen/Public/Cache/AssetStorageManager.h +++ b/Source/RpmNextGen/Public/Cache/AssetStorageManager.h @@ -39,8 +39,15 @@ class FAssetStorageManager const FAssetSaveData& StoredAsset = FAssetSaveData(Context.Asset, Context.BaseModelId); FAssetSaver AssetSaver = FAssetSaver(); - AssetSaver.SaveToFile(StoredAsset.IconFilePath, Context.ImageData); - AssetSaver.SaveToFile(StoredAsset.GlbFilePath, Context.GlbData); + if(Context.bIsGLb) + { + AssetSaver.SaveToFile(StoredAsset.GlbFilePath, Context.Data); + } + else + { + AssetSaver.SaveToFile(StoredAsset.IconFilePath, Context.Data); + } + StoreAndTrackAsset(StoredAsset); } diff --git a/Source/RpmNextGen/Public/Cache/CacheGenerator.h b/Source/RpmNextGen/Public/Cache/CacheGenerator.h index 01d9926..7f02bff 100644 --- a/Source/RpmNextGen/Public/Cache/CacheGenerator.h +++ b/Source/RpmNextGen/Public/Cache/CacheGenerator.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "Api/Assets/Models/AssetListResponse.h" struct FAssetSaveData; @@ -28,7 +28,8 @@ class RPMNEXTGEN_API FCacheGenerator : public TSharedFromThis FOnLocalCacheGenerated OnLocalCacheGenerated; void LoadAndStoreAssets(); - void LoadAndStoreAssetFromUrl(const FString& BaseModelId, const FAsset* Asset); + void LoadAndStoreAssetGlb(const FString& BaseModelId, const FAsset* Asset); + void LoadAndStoreAssetIcon(const FString& BaseModelId, const FAsset* Asset); protected: void FetchBaseModels() const; From 6451d72a2458ad8fa936f4ea93b9388807ff23d8 Mon Sep 17 00:00:00 2001 From: Harrison Date: Fri, 6 Sep 2024 14:27:53 +0300 Subject: [PATCH 18/54] chore: fix caching issues --- .../Private/Cache/CacheGenerator.cpp | 67 ++++++++++--------- .../Public/Api/Assets/AssetLoader.h | 2 - .../RpmNextGen/Public/Cache/CacheGenerator.h | 5 +- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index 7f2ddaf..da6c283 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -23,12 +23,6 @@ FCacheGenerator::FCacheGenerator() { Http = &FHttpModule::Get(); AssetApi = MakeUnique(); - const URpmDeveloperSettings* Settings = GetDefault(); - - if(!Settings->ApiKey.IsEmpty()) - { - AssetApi->SetAuthenticationStrategy(new FApiKeyAuthStrategy()); - } AssetApi->OnListAssetsResponse.BindRaw(this, &FCacheGenerator::OnListAssetsResponse); AssetApi->OnListAssetTypeResponse.BindRaw(this, &FCacheGenerator::OnListAssetTypesResponse); } @@ -51,22 +45,19 @@ void FCacheGenerator::GenerateLocalCache(int InItemsPerCategory) void FCacheGenerator::LoadAndStoreAssets() { int RefittedAssetCount = 0; - for (auto BaseModel : BaseModelAssets) + for (auto Pairs : RefittedAssetMapByBaseModelId) { - for (auto Pairs : BaseModelAssetsMap) - { - RefittedAssetCount += Pairs.Value.Num(); - } + RefittedAssetCount += Pairs.Value.Num(); } - const int AssetIconRequestCount = BaseModelAssetsMap[BaseModelAssets[0].Id].Num(); - for (auto Asset : BaseModelAssetsMap[BaseModelAssets[0].Id]) + const int AssetIconRequestCount = RefittedAssetMapByBaseModelId[BaseModelAssets[0].Id].Num(); + for (auto Asset : RefittedAssetMapByBaseModelId[BaseModelAssets[0].Id]) { LoadAndStoreAssetIcon(BaseModelAssets[0].Id, &Asset); } UE_LOG(LogReadyPlayerMe, Log, TEXT("RefittedAssetCount: %d. AssetIconRequestCount: %d BaseModelAssetsCount %d"), RefittedAssetCount, AssetIconRequestCount, BaseModelAssets.Num()); RequiredAssetDownloadRequest = RefittedAssetCount + BaseModelAssets.Num() + AssetIconRequestCount; - return; + UE_LOG(LogReadyPlayerMe, Log, TEXT("Total assets to download: %d. Total refitted assets to fetch: %d"), RequiredAssetDownloadRequest, RequiredRefittedAssetRequests); for (auto BaseModel : BaseModelAssets) { @@ -77,26 +68,29 @@ void FCacheGenerator::LoadAndStoreAssets() { PlatformFile.CreateDirectoryTree(*DirectoryPath); } - - LoadAndStoreAssetFromUrl(BaseModel.Id, &BaseModel); - for (auto Pairs : BaseModelAssetsMap) + LoadAndStoreAssetGlb(BaseModel.Id, &BaseModel); + for (auto RefittedAsset : RefittedAssetMapByBaseModelId[BaseModel.Id]) { - for (auto AssetForBaseModel : Pairs.Value) - { - LoadAndStoreAssetFromUrl(BaseModel.Id, &AssetForBaseModel); - } + LoadAndStoreAssetGlb(BaseModel.Id, &RefittedAsset); } - } + } } -void FCacheGenerator::LoadAndStoreAssetFromUrl(const FString& BaseModelId, const FAsset* Asset) +void FCacheGenerator::LoadAndStoreAssetGlb(const FString& BaseModelId, const FAsset* Asset) { TSharedPtr AssetLoader = MakeShared(); - AssetLoader->OnAssetSaved.BindRaw( this, &FCacheGenerator::OnAssetSaved); - AssetLoader->LoadAsset(*Asset, BaseModelId, true); + AssetLoader->OnAssetGlbLoaded.BindRaw( this, &FCacheGenerator::OnAssetSaved); + AssetLoader->LoadAssetGlb(*Asset, BaseModelId, true); } -void FCacheGenerator::OnAssetSaved(const FAssetSaveData& AssetSaveData) +void FCacheGenerator::LoadAndStoreAssetIcon(const FString& BaseModelId, const FAsset* Asset) +{ + TSharedPtr AssetLoader = MakeShared(); + AssetLoader->OnAssetIconLoaded.BindRaw( this, &FCacheGenerator::OnAssetSaved); + AssetLoader->LoadAssetIcon(*Asset, BaseModelId, true); +} + +void FCacheGenerator::OnAssetSaved(const FAsset& Asset, const TArray& Data) { AssetDownloadRequestsCompleted++; if(AssetDownloadRequestsCompleted >= RequiredAssetDownloadRequest) @@ -121,25 +115,35 @@ void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListRe FetchAssetTypes(); return; } - UE_LOG(LogReadyPlayerMe, Log, TEXT("Fetched %d assets"), AssetListResponse.Data.Num()); - + UE_LOG(LogReadyPlayerMe, Log, TEXT("Fetched %d assets of type %s"), AssetListResponse.Data.Num(), *AssetListResponse.Data[0].Type); + if(AssetListResponse.Data.Num() > 0) { FAsset BaseModelID = BaseModelAssets[CurrentBaseModelIndex]; - if (!BaseModelAssetsMap.Contains(BaseModelID.Id)) + if (!RefittedAssetMapByBaseModelId.Contains(BaseModelID.Id)) { - BaseModelAssetsMap.Add(BaseModelID.Id, AssetListResponse.Data); + RefittedAssetMapByBaseModelId.Add(BaseModelID.Id, AssetListResponse.Data); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Adding %d assets of type %s"), AssetListResponse.Data.Num(), *AssetListResponse.Data[0].Type); + } else { - BaseModelAssetsMap[BaseModelID.Id].Append(AssetListResponse.Data); + RefittedAssetMapByBaseModelId[BaseModelID.Id].Append(AssetListResponse.Data); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Appending %d assets of type %s"), AssetListResponse.Data.Num(), *AssetListResponse.Data[0].Type); } + } RefittedAssetRequestsCompleted++; if(RefittedAssetRequestsCompleted >= RequiredRefittedAssetRequests) { OnCacheDataLoaded.ExecuteIfBound(true); } + AssetListResponseCounter++; + if(AssetListResponseCounter >= AssetTypes.Num()-1) + { + CurrentBaseModelIndex++; + AssetListResponseCounter = 0; + } return; } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to fetch assets")); @@ -154,6 +158,7 @@ void FCacheGenerator::FetchAssetsForEachBaseModel() { const int TypesExcludingBaseModel = AssetTypes.Num() - 1; RequiredRefittedAssetRequests = TypesExcludingBaseModel * BaseModelAssets.Num(); + for (FAsset& BaseModel : BaseModelAssets) { for(FString AssetType : AssetTypes) diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h b/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h index ed50494..656a451 100644 --- a/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h +++ b/Source/RpmNextGen/Public/Api/Assets/AssetLoader.h @@ -27,7 +27,6 @@ class RPMNEXTGEN_API FAssetLoader : public TSharedFromThis DECLARE_DELEGATE_TwoParams(FOnAssetGlbLoaded, const FAsset&, const TArray&); DECLARE_DELEGATE_TwoParams(FOnAsseImageLoaded, const FAsset&, const TArray&); - DECLARE_DELEGATE_OneParam(FOnAssetSaved, const FAssetSaveData&); FAssetLoader(); virtual ~FAssetLoader(); @@ -35,7 +34,6 @@ class RPMNEXTGEN_API FAssetLoader : public TSharedFromThis void LoadAssetGlb(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache); FOnAssetGlbLoaded OnAssetGlbLoaded; FOnAsseImageLoaded OnAssetIconLoaded; - FOnAssetSaved OnAssetSaved; private: void LoadAssetGlb(TSharedRef Context); void LoadAssetIcon(TSharedRef Context); diff --git a/Source/RpmNextGen/Public/Cache/CacheGenerator.h b/Source/RpmNextGen/Public/Cache/CacheGenerator.h index 7f02bff..f55d3b1 100644 --- a/Source/RpmNextGen/Public/Cache/CacheGenerator.h +++ b/Source/RpmNextGen/Public/Cache/CacheGenerator.h @@ -37,11 +37,11 @@ class RPMNEXTGEN_API FCacheGenerator : public TSharedFromThis void FetchAssetsForBaseModel(const FString& BaseModelID, const FString& AssetType) const; virtual void OnDownloadRemoteCacheComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful); - void OnAssetSaved(const FAssetSaveData& AssetSaveData); + void OnAssetSaved(const FAsset& Asset, const TArray& Data); TUniquePtr AssetApi; TArray BaseModelAssets; TArray AssetTypes; - TMap> BaseModelAssetsMap; + TMap> RefittedAssetMapByBaseModelId; int32 CurrentBaseModelIndex; private: void OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful); @@ -54,5 +54,6 @@ class RPMNEXTGEN_API FCacheGenerator : public TSharedFromThis int RefittedAssetRequestsCompleted = 0; int RequiredAssetDownloadRequest = 0; int AssetDownloadRequestsCompleted = 0; + int AssetListResponseCounter = 0; FHttpModule* Http; }; From 82746e948c5bb47e469d69bc729914908bb0a7ae Mon Sep 17 00:00:00 2001 From: Harrison Date: Mon, 9 Sep 2024 11:32:33 +0300 Subject: [PATCH 19/54] chore: fix icon caching --- Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp index 5903dce..2d75960 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp @@ -37,7 +37,7 @@ void FAssetLoader::LoadAssetIcon(const FAsset& Asset, const FString& BaseModelId { TArray IconData; FFileHelper::LoadFileToArray(IconData, *StoredAsset.IconFilePath); - OnAssetGlbLoaded.ExecuteIfBound(Asset, IconData); + OnAssetIconLoaded.ExecuteIfBound(Asset, IconData); return; } const TSharedRef Context = MakeShared(Asset, BaseModelId, bStoreInCache); @@ -64,14 +64,14 @@ void FAssetLoader::AssetIconLoaded(TSharedPtr Response, const boo if (bWasSuccessful && Response.IsValid()) { Context->Data = Response->GetContent(); - if(!Context->bStoreInCache) + if(Context->bStoreInCache) { FAssetStorageManager::Get().StoreAndTrackAsset(*Context); } OnAssetIconLoaded.ExecuteIfBound(Context->Asset, Context->Data); return; } - UE_LOG(LogTemp, Error, TEXT("Failed to load image from URL: %s"), *Context->Asset.IconUrl); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load image from URL: %s"), *Context->Asset.IconUrl); OnAssetIconLoaded.ExecuteIfBound(Context->Asset, TArray()); } From d090c7321356480367812e08ba8daef5a59ea8ec Mon Sep 17 00:00:00 2001 From: Harrison Date: Mon, 9 Sep 2024 13:30:58 +0300 Subject: [PATCH 20/54] chore: WIP --- .../Private/Api/Assets/AssetLoader.cpp | 2 +- .../Private/Cache/CacheGenerator.cpp | 14 ++-- Source/RpmNextGen/Private/RpmNextGen.cpp | 12 ++++ .../RpmNextGen/Public/Cache/AssetSaveData.h | 66 ++++++++++++++----- .../Public/Cache/AssetStorageManager.h | 36 +++++++--- .../RpmNextGen/Public/Cache/CacheGenerator.h | 2 +- Source/RpmNextGen/Public/RpmNextGen.h | 15 ++++- .../Private/UI/SCacheEditorWidget.cpp | 14 ++-- 8 files changed, 118 insertions(+), 43 deletions(-) diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp index 2d75960..f5ca5a3 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp @@ -21,7 +21,7 @@ void FAssetLoader::LoadAssetGlb(const FAsset& Asset, const FString& BaseModelId, if(FAssetStorageManager::Get().GetCachedAsset(Asset.Id, StoredAsset)) { TArray GlbData; - FFileHelper::LoadFileToArray(GlbData, *StoredAsset.GlbFilePath); + FFileHelper::LoadFileToArray(GlbData, *StoredAsset.GlbPathsByBaseModelId[BaseModelId]); OnAssetGlbLoaded.ExecuteIfBound(Asset, GlbData); return; } diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index da6c283..8fa2957 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -15,7 +15,6 @@ #include "Misc/ScopeExit.h" #include "Settings/RpmDeveloperSettings.h" -const FString FCacheGenerator::CacheFolderPath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache"); const FString FCacheGenerator::ZipFileName = TEXT("CacheAssets.zip"); FCacheGenerator::FCacheGenerator() @@ -49,7 +48,11 @@ void FCacheGenerator::LoadAndStoreAssets() { RefittedAssetCount += Pairs.Value.Num(); } - const int AssetIconRequestCount = RefittedAssetMapByBaseModelId[BaseModelAssets[0].Id].Num(); + const int AssetIconRequestCount = RefittedAssetMapByBaseModelId[BaseModelAssets[0].Id].Num() + BaseModelAssets.Num(); + for ( auto Asset : BaseModelAssets) + { + LoadAndStoreAssetIcon(BaseModelAssets[0].Id, &Asset); + } for (auto Asset : RefittedAssetMapByBaseModelId[BaseModelAssets[0].Id]) { LoadAndStoreAssetIcon(BaseModelAssets[0].Id, &Asset); @@ -57,11 +60,11 @@ void FCacheGenerator::LoadAndStoreAssets() UE_LOG(LogReadyPlayerMe, Log, TEXT("RefittedAssetCount: %d. AssetIconRequestCount: %d BaseModelAssetsCount %d"), RefittedAssetCount, AssetIconRequestCount, BaseModelAssets.Num()); RequiredAssetDownloadRequest = RefittedAssetCount + BaseModelAssets.Num() + AssetIconRequestCount; - + const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); UE_LOG(LogReadyPlayerMe, Log, TEXT("Total assets to download: %d. Total refitted assets to fetch: %d"), RequiredAssetDownloadRequest, RequiredRefittedAssetRequests); for (auto BaseModel : BaseModelAssets) { - const FString BaseModeFolder = CacheFolderPath / BaseModel.Id; + const FString BaseModeFolder = GlobalCachePath / BaseModel.Id; IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); const FString DirectoryPath = FPaths::GetPath(BaseModeFolder); if (!PlatformFile.DirectoryExists(*DirectoryPath)) @@ -192,9 +195,8 @@ void FCacheGenerator::OnDownloadRemoteCacheComplete(TSharedPtr Req { // Get the response data const TArray& Data = Response->GetContent(); - // Define the path to save the ZIP file - const FString SavePath = CacheFolderPath / TEXT("/") / ZipFileName; + const FString SavePath = FRpmNextGenModule::GetGlobalAssetCachePath() / TEXT("/") / ZipFileName; // Ensure the directory exists IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); diff --git a/Source/RpmNextGen/Private/RpmNextGen.cpp b/Source/RpmNextGen/Private/RpmNextGen.cpp index 818e442..279d599 100644 --- a/Source/RpmNextGen/Private/RpmNextGen.cpp +++ b/Source/RpmNextGen/Private/RpmNextGen.cpp @@ -6,9 +6,12 @@ DEFINE_LOG_CATEGORY(LogReadyPlayerMe); #define LOCTEXT_NAMESPACE "FRpmNextGenModule" +FString FRpmNextGenModule::AssetCachePath = FString(); + 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 + InitializeGlobalPaths(); } void FRpmNextGenModule::ShutdownModule() @@ -17,6 +20,15 @@ void FRpmNextGenModule::ShutdownModule() // we call this function before unloading the module. } +// ReSharper disable once CppMemberFunctionMayBeConst +void FRpmNextGenModule::InitializeGlobalPaths() +{ + const FString RelativePath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache"); + AssetCachePath = FPaths::ConvertRelativePathToFull(RelativePath); + + UE_LOG(LogTemp, Log, TEXT("Initialized Asset Cache Path: %s"), *AssetCachePath); +} + #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FRpmNextGenModule, RpmNextGen) \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Cache/AssetSaveData.h b/Source/RpmNextGen/Public/Cache/AssetSaveData.h index d3c538e..80f2d95 100644 --- a/Source/RpmNextGen/Public/Cache/AssetSaveData.h +++ b/Source/RpmNextGen/Public/Cache/AssetSaveData.h @@ -1,58 +1,89 @@ #pragma once #include "CoreMinimal.h" +#include "RpmNextGen.h" #include "Api/Assets/Models/Asset.h" #include "AssetSaveData.generated.h" USTRUCT(BlueprintType) -struct RPMNEXTGEN_API FAssetSaveData : public FAsset +struct RPMNEXTGEN_API FAssetSaveData { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") - FString BaseModelId; + FString Id; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") - FString GlbFilePath; + 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") + TMap GlbPathsByBaseModelId; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") FString IconFilePath; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + 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; + FAssetSaveData() { - BaseModelId = FString(); + Id = FString(); + Name = FString(); + GlbUrl = FString(); + IconUrl = FString(); + GlbPathsByBaseModelId = TMap(); IconFilePath = FString(); - GlbFilePath = FString(); + Type = FString(); + CreatedAt = FDateTime(); + UpdatedAt = FDateTime(); } FAssetSaveData(const FAsset& InAsset, const FString& InBaseModelId) { - const FString CacheFolderPath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache"); + const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); Id = InAsset.Id; Name = InAsset.Name; GlbUrl = InAsset.GlbUrl; IconUrl = InAsset.IconUrl; + GlbPathsByBaseModelId = TMap(); + GlbPathsByBaseModelId.Add(InBaseModelId, FString::Printf(TEXT("%s/%s/%s.glb"), *GlobalCachePath, *InBaseModelId, *Id)); + IconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *GlobalCachePath, *Id); Type = InAsset.Type; CreatedAt = InAsset.CreatedAt; UpdatedAt = InAsset.UpdatedAt; - BaseModelId = InBaseModelId; - IconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *CacheFolderPath, *Id); - GlbFilePath = FString::Printf(TEXT("%s/%s/%s.glb"), *CacheFolderPath, *BaseModelId, *Id); } TSharedPtr ToJson() const { TSharedPtr JsonObject = MakeShared(); - JsonObject->SetStringField(TEXT("GlbFilePath"), GlbFilePath); - JsonObject->SetStringField(TEXT("IconFilePath"), IconFilePath); - JsonObject->SetStringField(TEXT("BaseModelId"), BaseModelId); + JsonObject->SetStringField(TEXT("Id"), Id); JsonObject->SetStringField(TEXT("Name"), Name); JsonObject->SetStringField(TEXT("GlbUrl"), GlbUrl); JsonObject->SetStringField(TEXT("IconUrl"), IconUrl); + JsonObject->SetStringField(TEXT("IconFilePath"), IconFilePath); JsonObject->SetStringField(TEXT("Type"), Type); JsonObject->SetStringField(TEXT("CreatedAt"), CreatedAt.ToString()); JsonObject->SetStringField(TEXT("UpdatedAt"), UpdatedAt.ToString()); + + TSharedPtr GlbPathsObject = MakeShared(); + for (const auto& Entry : GlbPathsByBaseModelId) + { + GlbPathsObject->SetStringField(Entry.Key, Entry.Value); + } + JsonObject->SetObjectField(TEXT("GlbPathsByBaseModelId"), GlbPathsObject); return JsonObject; } @@ -60,17 +91,22 @@ struct RPMNEXTGEN_API FAssetSaveData : public FAsset static FAssetSaveData FromJson(const TSharedPtr& JsonObject) { FAssetSaveData StoredAsset; - StoredAsset.GlbFilePath = JsonObject->GetStringField(TEXT("GlbFilePath")); - StoredAsset.IconFilePath = JsonObject->GetStringField(TEXT("IconFilePath")); - StoredAsset.BaseModelId = JsonObject->GetStringField(TEXT("BaseModelId")); + StoredAsset.Id = JsonObject->GetStringField(TEXT("Id")); StoredAsset.Name = JsonObject->GetStringField(TEXT("Name")); StoredAsset.GlbUrl = JsonObject->GetStringField(TEXT("GlbUrl")); StoredAsset.IconUrl = JsonObject->GetStringField(TEXT("IconUrl")); + StoredAsset.IconFilePath = JsonObject->GetStringField(TEXT("IconFilePath")); StoredAsset.Type = JsonObject->GetStringField(TEXT("Type")); FDateTime::Parse(JsonObject->GetStringField(TEXT("CreatedAt")), StoredAsset.CreatedAt); FDateTime::Parse(JsonObject->GetStringField(TEXT("UpdatedAt")), StoredAsset.UpdatedAt); + TSharedPtr GlbPathsObject = JsonObject->GetObjectField(TEXT("GlbPathsByBaseModelId")); + for (const auto& Entry : GlbPathsObject->Values) + { + StoredAsset.GlbPathsByBaseModelId.Add(Entry.Key, Entry.Value->AsString()); + } + return StoredAsset; } }; diff --git a/Source/RpmNextGen/Public/Cache/AssetStorageManager.h b/Source/RpmNextGen/Public/Cache/AssetStorageManager.h index fb06cfb..598b98e 100644 --- a/Source/RpmNextGen/Public/Cache/AssetStorageManager.h +++ b/Source/RpmNextGen/Public/Cache/AssetStorageManager.h @@ -15,7 +15,8 @@ class FAssetStorageManager static void StoreAssetTypes(const TArray& TypeList) { - FString TypeListFilePath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache/TypeList.json"); + const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); + const FString TypeListFilePath = GlobalCachePath / TEXT("TypeList.json"); // Convert the TArray to TArray> TArray> JsonValues; @@ -41,10 +42,11 @@ class FAssetStorageManager FAssetSaver AssetSaver = FAssetSaver(); if(Context.bIsGLb) { - AssetSaver.SaveToFile(StoredAsset.GlbFilePath, Context.Data); + AssetSaver.SaveToFile(StoredAsset.GlbPathsByBaseModelId[Context.BaseModelId], Context.Data); } else { + UE_LOG(LogReadyPlayerMe, Warning, TEXT("Storing asset Icon in cache at path %s"), *StoredAsset.IconFilePath); AssetSaver.SaveToFile(StoredAsset.IconFilePath, Context.Data); } @@ -53,32 +55,32 @@ class FAssetStorageManager void StoreAndTrackAsset(const FAssetSaveData& StoredAsset, const bool bSaveManifest = true) { - FString CombinedId = StoredAsset.Id + StoredAsset.BaseModelId; - FAssetSaveData* ExistingStoredAsset = StoredAssets.Find(CombinedId); + FAssetSaveData* ExistingStoredAsset = StoredAssets.Find(StoredAsset.Id); if(ExistingStoredAsset != nullptr) { // Update existing stored asset with new values if present - if(ExistingStoredAsset->GlbFilePath.IsEmpty() && !StoredAsset.GlbFilePath.IsEmpty()) + if(!StoredAsset.GlbPathsByBaseModelId.IsEmpty()) { - ExistingStoredAsset->GlbFilePath = StoredAsset.GlbFilePath; + MergeTMaps(ExistingStoredAsset->GlbPathsByBaseModelId, StoredAsset.GlbPathsByBaseModelId); } if(ExistingStoredAsset->IconFilePath.IsEmpty() && !StoredAsset.IconFilePath.IsEmpty()) { ExistingStoredAsset->IconFilePath = StoredAsset.IconFilePath; } } - StoredAssets.Add(CombinedId, ExistingStoredAsset ? *ExistingStoredAsset : StoredAsset); + StoredAssets.Add(StoredAsset.Id, ExistingStoredAsset ? *ExistingStoredAsset : StoredAsset); if(bSaveManifest) { SaveManifest(); } - UE_LOG(LogReadyPlayerMe, Log, TEXT("Tracked asset: AssetId=%s, GlbFilePath=%s, IconFilePath=%s"), *StoredAsset.Id, *StoredAsset.GlbFilePath, *StoredAsset.IconFilePath); + //UE_LOG(LogReadyPlayerMe, Log, TEXT("Tracked asset: AssetId=%s, GlbFilePath=%s, IconFilePath=%s"), *StoredAsset.Id, *StoredAsset.GlbFilePath, *StoredAsset.IconFilePath); } void LoadManifest() { - const FString ManifestFilePath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache/AssetManifest.json"); + const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); + const FString ManifestFilePath = GlobalCachePath / TEXT("AssetManifest.json"); FString ManifestContent; if (FFileHelper::LoadFileToString(ManifestContent, *ManifestFilePath)) @@ -104,7 +106,8 @@ class FAssetStorageManager void SaveManifest() { - const FString ManifestFilePath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache/AssetManifest.json"); + const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); + const FString ManifestFilePath = GlobalCachePath / TEXT("AssetManifest.json"); TSharedPtr ManifestJson = MakeShared(); TSharedPtr TrackedAssetsJson = MakeShared(); @@ -150,5 +153,18 @@ class FAssetStorageManager { LoadManifest(); } + + template + void MergeTMaps(TMap& DestinationMap, const TMap& SourceMap) + { + for (const TPair& Elem : SourceMap) + { + // Add only if the key doesn't already exist in the destination map + if (!DestinationMap.Contains(Elem.Key)) + { + DestinationMap.Add(Elem.Key, Elem.Value); + } + } + } TMap StoredAssets; }; diff --git a/Source/RpmNextGen/Public/Cache/CacheGenerator.h b/Source/RpmNextGen/Public/Cache/CacheGenerator.h index f55d3b1..c597af4 100644 --- a/Source/RpmNextGen/Public/Cache/CacheGenerator.h +++ b/Source/RpmNextGen/Public/Cache/CacheGenerator.h @@ -1,6 +1,7 @@ #pragma once #include "Api/Assets/Models/AssetListResponse.h" +class FTaskManager; struct FAssetSaveData; class FAssetSaver; struct FAssetTypeListResponse; @@ -47,7 +48,6 @@ class RPMNEXTGEN_API FCacheGenerator : public TSharedFromThis void OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful); void FetchAssetsForEachBaseModel(); void OnListAssetTypesResponse(const FAssetTypeListResponse& AssetTypeListResponse, bool bWasSuccessful); - static const FString CacheFolderPath; static const FString ZipFileName; int MaxItemsPerCategory; int RequiredRefittedAssetRequests = 0; diff --git a/Source/RpmNextGen/Public/RpmNextGen.h b/Source/RpmNextGen/Public/RpmNextGen.h index 48fa711..26ac13a 100644 --- a/Source/RpmNextGen/Public/RpmNextGen.h +++ b/Source/RpmNextGen/Public/RpmNextGen.h @@ -7,11 +7,24 @@ RPMNEXTGEN_API DECLARE_LOG_CATEGORY_EXTERN(LogReadyPlayerMe, Log, All); -class FRpmNextGenModule : public IModuleInterface +class RPMNEXTGEN_API FRpmNextGenModule : public IModuleInterface { public: /** IModuleInterface implementation */ virtual void StartupModule() override; virtual void ShutdownModule() override; + + // Get the global asset cache path + static const FString& GetGlobalAssetCachePath() + { + return AssetCachePath; + } + +private: + // Initialize the asset cache path + void InitializeGlobalPaths(); + + // Store the global asset cache path + static FString AssetCachePath; }; diff --git a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp index a9d4f53..2181274 100644 --- a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp @@ -176,21 +176,17 @@ FReply SCacheEditorWidget::OnExtractCacheClicked() FReply SCacheEditorWidget::OnOpenLocalCacheFolderClicked() { - // Define the folder path you want to open (relative path) - FString FolderPath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache"); - - // Convert relative path to full absolute path - FString AbsoluteFolderPath = FPaths::ConvertRelativePathToFull(FolderPath); - + const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); + // Check if the folder exists - if (FPaths::DirectoryExists(AbsoluteFolderPath)) + if (FPaths::DirectoryExists(GlobalCachePath)) { // Open the folder in the file explorer - FPlatformProcess::LaunchFileInDefaultExternalApplication(*AbsoluteFolderPath); + FPlatformProcess::LaunchFileInDefaultExternalApplication(*GlobalCachePath); } else { - UE_LOG(LogTemp, Warning, TEXT("Folder does not exist: %s"), *AbsoluteFolderPath); + UE_LOG(LogTemp, Warning, TEXT("Folder does not exist: %s"), *GlobalCachePath); } return FReply::Handled(); From 9e1641887fc24acc97ce6c0fd10d25552907f742 Mon Sep 17 00:00:00 2001 From: Harrison Date: Mon, 9 Sep 2024 16:02:27 +0300 Subject: [PATCH 21/54] chore: refactored cache generation logic --- .../Private/Cache/CacheGenerator.cpp | 136 +++++++++--------- .../RpmNextGen/Public/Cache/CacheGenerator.h | 14 +- 2 files changed, 79 insertions(+), 71 deletions(-) diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index 8fa2957..eb89895 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -43,38 +43,53 @@ void FCacheGenerator::GenerateLocalCache(int InItemsPerCategory) void FCacheGenerator::LoadAndStoreAssets() { - int RefittedAssetCount = 0; - for (auto Pairs : RefittedAssetMapByBaseModelId) + TArray BaseModelAssets = TArray(); + int TotalRefittedAssets = 0; + for ( auto BaseModel : AssetMapByBaseModelId) { - RefittedAssetCount += Pairs.Value.Num(); + for (auto Asset : BaseModel.Value) + { + if(Asset.Type == FAssetApi::BaseModelType) + { + BaseModelAssets.Add(Asset); + } + TotalRefittedAssets++; + } } - const int AssetIconRequestCount = RefittedAssetMapByBaseModelId[BaseModelAssets[0].Id].Num() + BaseModelAssets.Num(); + int AssetIconRequestCount = 0; + + // load and store base model assets for ( auto Asset : BaseModelAssets) { - LoadAndStoreAssetIcon(BaseModelAssets[0].Id, &Asset); + LoadAndStoreAssetIcon(Asset.Id, &Asset); + AssetIconRequestCount++; } - for (auto Asset : RefittedAssetMapByBaseModelId[BaseModelAssets[0].Id]) + + + // load and store asset icon (only 1 set is required + for (auto Asset : AssetMapByBaseModelId[BaseModelAssets[0].Id]) { + if(Asset.Type == FAssetApi::BaseModelType) continue; LoadAndStoreAssetIcon(BaseModelAssets[0].Id, &Asset); + AssetIconRequestCount++; } - UE_LOG(LogReadyPlayerMe, Log, TEXT("RefittedAssetCount: %d. AssetIconRequestCount: %d BaseModelAssetsCount %d"), RefittedAssetCount, AssetIconRequestCount, BaseModelAssets.Num()); + + RequiredAssetDownloadRequest = TotalRefittedAssets + AssetIconRequestCount; + const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Total assets to download: %d. Total refitted assets glbs to fetch: %d"), RequiredAssetDownloadRequest, TotalRefittedAssets - BaseModelAssets.Num()); - RequiredAssetDownloadRequest = RefittedAssetCount + BaseModelAssets.Num() + AssetIconRequestCount; - const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); - UE_LOG(LogReadyPlayerMe, Log, TEXT("Total assets to download: %d. Total refitted assets to fetch: %d"), RequiredAssetDownloadRequest, RequiredRefittedAssetRequests); - for (auto BaseModel : BaseModelAssets) + for (auto Pair : AssetMapByBaseModelId) { - const FString BaseModeFolder = GlobalCachePath / BaseModel.Id; + const FString BaseModeFolder = GlobalCachePath / Pair.Key; IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); const FString DirectoryPath = FPaths::GetPath(BaseModeFolder); if (!PlatformFile.DirectoryExists(*DirectoryPath)) { PlatformFile.CreateDirectoryTree(*DirectoryPath); } - LoadAndStoreAssetGlb(BaseModel.Id, &BaseModel); - for (auto RefittedAsset : RefittedAssetMapByBaseModelId[BaseModel.Id]) + for (auto Asset : Pair.Value) { - LoadAndStoreAssetGlb(BaseModel.Id, &RefittedAsset); + LoadAndStoreAssetGlb(Pair.Key, &Asset); } } } @@ -95,10 +110,10 @@ void FCacheGenerator::LoadAndStoreAssetIcon(const FString& BaseModelId, const FA void FCacheGenerator::OnAssetSaved(const FAsset& Asset, const TArray& Data) { - AssetDownloadRequestsCompleted++; - if(AssetDownloadRequestsCompleted >= RequiredAssetDownloadRequest) + NumberOfAssetsSaved++; + if(NumberOfAssetsSaved >= RequiredAssetDownloadRequest) { - UE_LOG(LogReadyPlayerMe, Log, TEXT("OnLocalCacheGenerated Total assets to download: %d. Asset download requests completed: %d"), RequiredAssetDownloadRequest, AssetDownloadRequestsCompleted); + UE_LOG(LogReadyPlayerMe, Log, TEXT("OnLocalCacheGenerated Total assets to download: %d. Asset download requests completed: %d"), RequiredAssetDownloadRequest, NumberOfAssetsSaved); OnLocalCacheGenerated.ExecuteIfBound(true); } @@ -106,15 +121,17 @@ void FCacheGenerator::OnAssetSaved(const FAsset& Asset, const TArray& Dat void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful) { - UE_LOG(LogReadyPlayerMe, Log, TEXT("OnListAssetsResponse") ); if(bWasSuccessful && AssetListResponse.IsSuccess) { - UE_LOG(LogReadyPlayerMe, Log, TEXT("Success ") ); - if (AssetListResponse.Data.Num() > 0 && AssetListResponse.Data[0].Type == FAssetApi::BaseModelType) + if (AssetListResponse.Data[0].Type == FAssetApi::BaseModelType) { - BaseModelAssets.Empty(); - BaseModelAssets.Append(AssetListResponse.Data); - UE_LOG(LogReadyPlayerMe, Log, TEXT("Fetched %d Base models"), AssetListResponse.Data.Num()); + for ( auto BaseModel : AssetListResponse.Data) + { + TArray AssetList = TArray(); + AssetList.Add(BaseModel); + AssetMapByBaseModelId.Add(BaseModel.Id, AssetList); + } + UE_LOG(LogReadyPlayerMe, Log, TEXT("Fetched %d base models"), AssetListResponse.Data.Num()); FetchAssetTypes(); return; } @@ -122,47 +139,33 @@ void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListRe if(AssetListResponse.Data.Num() > 0) { - FAsset BaseModelID = BaseModelAssets[CurrentBaseModelIndex]; - if (!RefittedAssetMapByBaseModelId.Contains(BaseModelID.Id)) + FString BaseModelID = AssetListRequests[RefittedAssetRequestsCompleted].Params.CharacterModelAssetId; + if (!AssetMapByBaseModelId.Contains(BaseModelID)) { - RefittedAssetMapByBaseModelId.Add(BaseModelID.Id, AssetListResponse.Data); - UE_LOG(LogReadyPlayerMe, Log, TEXT("Adding %d assets of type %s"), AssetListResponse.Data.Num(), *AssetListResponse.Data[0].Type); - + AssetMapByBaseModelId.Add(BaseModelID, AssetListResponse.Data); } else { - RefittedAssetMapByBaseModelId[BaseModelID.Id].Append(AssetListResponse.Data); - UE_LOG(LogReadyPlayerMe, Log, TEXT("Appending %d assets of type %s"), AssetListResponse.Data.Num(), *AssetListResponse.Data[0].Type); + AssetMapByBaseModelId[BaseModelID].Append(AssetListResponse.Data); } } RefittedAssetRequestsCompleted++; - if(RefittedAssetRequestsCompleted >= RequiredRefittedAssetRequests) - { - OnCacheDataLoaded.ExecuteIfBound(true); - } - AssetListResponseCounter++; - if(AssetListResponseCounter >= AssetTypes.Num()-1) - { - CurrentBaseModelIndex++; - AssetListResponseCounter = 0; - } + FetchNextRefittedAsset(); return; } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to fetch assets")); RefittedAssetRequestsCompleted++; - if(RefittedAssetRequestsCompleted >= RequiredRefittedAssetRequests) - { - OnCacheDataLoaded.ExecuteIfBound(true); - } + FetchNextRefittedAsset(); } -void FCacheGenerator::FetchAssetsForEachBaseModel() +void FCacheGenerator::StartFetchingRefittedAssets() { - const int TypesExcludingBaseModel = AssetTypes.Num() - 1; - RequiredRefittedAssetRequests = TypesExcludingBaseModel * BaseModelAssets.Num(); - - for (FAsset& BaseModel : BaseModelAssets) + RefittedAssetRequestsCompleted = 0; + + URpmDeveloperSettings *Settings = GetMutableDefault(); + AssetListRequests = TArray(); + for ( auto BaseModel : AssetMapByBaseModelId) { for(FString AssetType : AssetTypes) { @@ -170,9 +173,26 @@ void FCacheGenerator::FetchAssetsForEachBaseModel() { continue; } - FetchAssetsForBaseModel(BaseModel.Id, AssetType); + FAssetListQueryParams QueryParams = FAssetListQueryParams(); + QueryParams.Type = AssetType; + QueryParams.ApplicationId = Settings->ApplicationId; + QueryParams.CharacterModelAssetId = BaseModel.Key; + QueryParams.Limit = MaxItemsPerCategory; + FAssetListRequest AssetListRequest = FAssetListRequest(QueryParams); + AssetListRequests.Add(AssetListRequest); } } + FetchNextRefittedAsset(); +} + +void FCacheGenerator::FetchNextRefittedAsset() +{ + if(RefittedAssetRequestsCompleted >= AssetListRequests.Num()) + { + OnCacheDataLoaded.ExecuteIfBound(true); + return; + } + AssetApi->ListAssetsAsync(AssetListRequests[RefittedAssetRequestsCompleted]); } void FCacheGenerator::OnListAssetTypesResponse(const FAssetTypeListResponse& AssetListResponse, bool bWasSuccessful) @@ -182,7 +202,7 @@ void FCacheGenerator::OnListAssetTypesResponse(const FAssetTypeListResponse& Ass UE_LOG(LogReadyPlayerMe, Log, TEXT("Fetched %d asset types"), AssetListResponse.Data.Num()); AssetTypes.Append(AssetListResponse.Data); FAssetStorageManager::Get().StoreAssetTypes(AssetTypes); - FetchAssetsForEachBaseModel(); + StartFetchingRefittedAssets(); return; } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to fetch asset types")); @@ -249,15 +269,3 @@ void FCacheGenerator::FetchAssetTypes() const AssetListRequest.Params = QueryParams; AssetApi->ListAssetTypesAsync(AssetListRequest); } - -void FCacheGenerator::FetchAssetsForBaseModel(const FString& BaseModelID, const FString& AssetType) const -{ - URpmDeveloperSettings *Settings = GetMutableDefault(); - FAssetListQueryParams QueryParams = FAssetListQueryParams(); - QueryParams.Type = AssetType; - QueryParams.ApplicationId = Settings->ApplicationId; - QueryParams.CharacterModelAssetId = BaseModelID; - QueryParams.Limit = MaxItemsPerCategory; - FAssetListRequest AssetListRequest = FAssetListRequest(QueryParams); - AssetApi->ListAssetsAsync(AssetListRequest); -} diff --git a/Source/RpmNextGen/Public/Cache/CacheGenerator.h b/Source/RpmNextGen/Public/Cache/CacheGenerator.h index c597af4..fcd2d32 100644 --- a/Source/RpmNextGen/Public/Cache/CacheGenerator.h +++ b/Source/RpmNextGen/Public/Cache/CacheGenerator.h @@ -1,6 +1,7 @@ #pragma once #include "Api/Assets/Models/AssetListResponse.h" +struct FAssetListRequest; class FTaskManager; struct FAssetSaveData; class FAssetSaver; @@ -35,25 +36,24 @@ class RPMNEXTGEN_API FCacheGenerator : public TSharedFromThis protected: void FetchBaseModels() const; void FetchAssetTypes() const; - void FetchAssetsForBaseModel(const FString& BaseModelID, const FString& AssetType) const; virtual void OnDownloadRemoteCacheComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful); void OnAssetSaved(const FAsset& Asset, const TArray& Data); + void FetchNextRefittedAsset(); + TUniquePtr AssetApi; - TArray BaseModelAssets; TArray AssetTypes; - TMap> RefittedAssetMapByBaseModelId; + TMap> AssetMapByBaseModelId; + TArray AssetListRequests; int32 CurrentBaseModelIndex; private: void OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful); - void FetchAssetsForEachBaseModel(); + void StartFetchingRefittedAssets(); void OnListAssetTypesResponse(const FAssetTypeListResponse& AssetTypeListResponse, bool bWasSuccessful); static const FString ZipFileName; int MaxItemsPerCategory; - int RequiredRefittedAssetRequests = 0; int RefittedAssetRequestsCompleted = 0; int RequiredAssetDownloadRequest = 0; - int AssetDownloadRequestsCompleted = 0; - int AssetListResponseCounter = 0; + int NumberOfAssetsSaved = 0; FHttpModule* Http; }; From b0af21eda49c3d96e2648bf43373ac1ae964e8a9 Mon Sep 17 00:00:00 2001 From: Harrison Date: Mon, 9 Sep 2024 16:02:46 +0300 Subject: [PATCH 22/54] chore: adding cache debug logs --- Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp index f5ca5a3..eb89e67 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetLoader.cpp @@ -23,6 +23,7 @@ void FAssetLoader::LoadAssetGlb(const FAsset& Asset, const FString& BaseModelId, TArray GlbData; FFileHelper::LoadFileToArray(GlbData, *StoredAsset.GlbPathsByBaseModelId[BaseModelId]); OnAssetGlbLoaded.ExecuteIfBound(Asset, GlbData); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Loading Glb From cache")); return; } const TSharedRef Context = MakeShared(Asset, BaseModelId, bStoreInCache); @@ -38,6 +39,7 @@ void FAssetLoader::LoadAssetIcon(const FAsset& Asset, const FString& BaseModelId TArray IconData; FFileHelper::LoadFileToArray(IconData, *StoredAsset.IconFilePath); OnAssetIconLoaded.ExecuteIfBound(Asset, IconData); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Loading Icon From cache")); return; } const TSharedRef Context = MakeShared(Asset, BaseModelId, bStoreInCache); From 2fd22d39008c555ed7b9c1dbd3c79bf79d3651d0 Mon Sep 17 00:00:00 2001 From: Harrison Hough Date: Tue, 10 Sep 2024 19:11:38 +0300 Subject: [PATCH 23/54] chore: WIP updates --- .../Private/Api/Assets/AssetApi.cpp | 11 +- Source/RpmNextGen/Private/RpmActor.cpp | 265 +++++++++++------- .../RpmNextGen/Private/RpmLoaderComponent.cpp | 102 +++++++ .../RpmNextGen/Public/Api/Assets/AssetApi.h | 4 +- Source/RpmNextGen/Public/RpmActor.h | 9 +- .../Public/RpmAssetLoaderComponent.h | 1 + Source/RpmNextGen/Public/RpmLoaderComponent.h | 87 ++++++ 7 files changed, 366 insertions(+), 113 deletions(-) create mode 100644 Source/RpmNextGen/Private/RpmLoaderComponent.cpp create mode 100644 Source/RpmNextGen/Public/RpmLoaderComponent.h diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp index 5ac7078..8bb0dd1 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp @@ -10,7 +10,7 @@ const FString FAssetApi::BaseModelType = TEXT("baseModel"); FAssetApi::FAssetApi() { - OnApiResponse.BindRaw(this, &FAssetApi::HandleListAssetResponse); + OnApiResponse.BindRaw(this, &FAssetApi::HandleResponse); const URpmDeveloperSettings* Settings = GetDefault(); @@ -63,7 +63,7 @@ void FAssetApi::ListAssetTypesAsync(const FAssetTypeListRequest& Request) DispatchRawWithAuth(ApiRequest); } -void FAssetApi::HandleListAssetResponse(FString Response, bool bWasSuccessful) +void FAssetApi::HandleResponse(FString Response, bool bWasSuccessful) { if (bWasSuccessful) { @@ -113,12 +113,12 @@ void FAssetApi::HandleListAssetResponse(FString Response, bool bWasSuccessful) // Use EStructJsonFlags::SkipMissingProperties for Unreal Engine 5.1 and later FAssetListResponse AssetListResponse = FAssetListResponse(); - FAssetTypeListResponse AssetTypeListResponse = FAssetTypeListResponse(); if (FJsonObjectConverter::JsonObjectStringToUStruct(Response, &AssetListResponse, 0, EStructJsonFlags::SkipMissingProperties)) { OnListAssetsResponse.ExecuteIfBound(AssetListResponse, true); return; } + FAssetTypeListResponse AssetTypeListResponse = FAssetTypeListResponse(); if (FJsonObjectConverter::JsonObjectStringToUStruct(Response, &AssetTypeListResponse, 0, EStructJsonFlags::SkipMissingProperties)) { OnListAssetTypeResponse.ExecuteIfBound(AssetTypeListResponse, true); @@ -139,8 +139,3 @@ void FAssetApi::HandleListAssetResponse(FString Response, bool bWasSuccessful) OnListAssetTypeResponse.ExecuteIfBound(FAssetTypeListResponse(), false); } -void FAssetApi::HandleListAssetTypeResponse(FString Response, bool bWasSuccessful) -{ - -} - diff --git a/Source/RpmNextGen/Private/RpmActor.cpp b/Source/RpmNextGen/Private/RpmActor.cpp index 5a0aef0..d2c1ce1 100644 --- a/Source/RpmNextGen/Private/RpmActor.cpp +++ b/Source/RpmNextGen/Private/RpmActor.cpp @@ -6,6 +6,7 @@ #include "Components/SkeletalMeshComponent.h" #include "Animation/AnimSequence.h" #include "glTFRuntimeSkeletalMeshComponent.h" +#include "RpmNextGen.h" // Sets default values ARpmActor::ARpmActor() @@ -14,7 +15,6 @@ ARpmActor::ARpmActor() PrimaryActorTick.bCanEverTick = true; AssetRoot = CreateDefaultSubobject(TEXT("AssetRoot")); RootComponent = AssetRoot; - RootNodeIndex = INDEX_NONE; bStaticMeshesAsSkeletalOnMorphTargets = true; } @@ -35,7 +35,6 @@ void ARpmActor::LoadGltfAsset(UglTFRuntimeAsset* GltfAsset) { // Before loading a new asset, clear existing components ClearLoadedComponents(); - Asset = GltfAsset; SetupAsset(); } @@ -67,34 +66,18 @@ void ARpmActor::SetupAsset() 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) { - TArray Scenes = Asset->GetScenes(); - for (FglTFRuntimeScene& Scene : Scenes) + for (int32 NodeIndex : Scene.RootNodesIndices) { - 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)) { - FglTFRuntimeNode Node; - if (!Asset->GetNode(NodeIndex, Node)) - { - return; - } - ProcessNode(SceneComponent, NAME_None, Node); + return; } + ProcessNode(AssetRoot, NAME_None, Node); } } @@ -114,9 +97,77 @@ void ARpmActor::SetupAsset() UE_LOG(LogGLTFRuntime, Log, TEXT("Asset loaded in %f seconds"), FPlatformTime::Seconds() - LoadingStartTime); } +void ARpmActor::LoadClothingAsset(UglTFRuntimeAsset* GltfAsset, const FString& ClothingType) +{ + // Remove existing components of this clothing type, if any + RemoveClothingComponents(ClothingType); + + // Create and add the new components for the clothing type (multiple meshes) + TArray NewClothingComponents = CreateClothingMeshComponents(GltfAsset, ClothingType); + if (NewClothingComponents.Num() > 0) + { + LoadedClothingComponents.Add(ClothingType, NewClothingComponents); + } +} + +void ARpmActor::RemoveClothingComponents(const FString& ClothingType) +{ + if (LoadedClothingComponents.Contains(ClothingType)) + { + TArray& OldComponents = LoadedClothingComponents[ClothingType]; + for (USceneComponent* OldComponent : OldComponents) + { + if (OldComponent) + { + OldComponent->DestroyComponent(); + } + } + LoadedClothingComponents.Remove(ClothingType); + } +} + +TArray ARpmActor::CreateClothingMeshComponents(UglTFRuntimeAsset* GltfAsset, const FString& ClothingType) +{ + TArray NewComponents; + + // Assuming the asset contains multiple meshes, loop through and load them + // TArray MeshIndices = GltfAsset->GetMeshIndices(); // Or some other method to get mesh indices + // + // for (int32 MeshIndex : MeshIndices) + // { + // USkeletalMeshComponent* SkeletalMeshComponent = NewObject(this, *FString::Printf(TEXT("%s_Mesh_%d"), *ClothingType, MeshIndex)); + // USkeletalMesh* SkeletalMesh = GltfAsset->LoadSkeletalMesh(MeshIndex); + // + // if (SkeletalMesh) + // { + // SkeletalMeshComponent->SetSkeletalMesh(SkeletalMesh); + // SkeletalMeshComponent->SetupAttachment(RootComponent); + // SkeletalMeshComponent->RegisterComponent(); + // SkeletalMeshComponent->SetRelativeTransform(FTransform::Identity); + // + // NewComponents.Add(SkeletalMeshComponent); + // + // // Custom event handling for when a skeletal mesh component is created + // ReceiveOnSkeletalMeshComponentCreated(SkeletalMeshComponent, MeshIndex); + // } + // } + + return NewComponents; +} + void ARpmActor::ProcessNode(USceneComponent* NodeParentComponent, const FName SocketName, FglTFRuntimeNode& Node) { + // Skip the "Armature" node but still process its children + if (Node.Name.Contains("Armature")) + { + UE_LOG(LogGLTFRuntime, Log, TEXT("Skipping Armature node but processing its children")); + + // Process children of the "Armature" node + ProcessChildNodes(NodeParentComponent, Node); + return; + } + if (Asset->NodeIsBone(Node.Index)) { ProcessBoneNode(NodeParentComponent, Node); @@ -149,81 +200,93 @@ void ARpmActor::ProcessBoneNode(USceneComponent* NodeParentComponent, FglTFRunti 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; + USceneComponent* NewComponent = nullptr; + + if (Node.SkinIndex >= 0 || (bStaticMeshesAsSkeletalOnMorphTargets && Asset->MeshHasMorphTargets(Node.MeshIndex))) + { + NewComponent = CreateSkeletalMeshComponent(NodeParentComponent, Node); + if(Node.Name.Contains("Armature")) + { + UE_LOG(LogReadyPlayerMe, Log, TEXT("Armature found when creating Skeletal mesh")); + } + } + else + { + NewComponent = CreateStaticMeshComponent(NodeParentComponent, Node); + + } + + AddInstanceComponent(NewComponent); + + return NewComponent; +} + + +USkeletalMeshComponent* ARpmActor::CreateSkeletalMeshComponent(USceneComponent* NodeParentComponent, FglTFRuntimeNode& Node) +{ + 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 to AssetRoot and register the component + SkeletalMeshComponent->SetupAttachment(AssetRoot); + SkeletalMeshComponent->RegisterComponent(); + SkeletalMeshComponent->SetRelativeTransform(Node.Transform); + + // Add the component to the list of discovered skeletal mesh components + DiscoveredSkeletalMeshComponents.Add(SkeletalMeshComponent); + + // Custom event handling for when a skeletal mesh component is created + ReceiveOnSkeletalMeshComponentCreated(SkeletalMeshComponent, Node); + + return SkeletalMeshComponent; +} + +UStaticMeshComponent* ARpmActor::CreateStaticMeshComponent(USceneComponent* NodeParentComponent, FglTFRuntimeNode& Node) +{ + 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 to AssetRoot and register the component + StaticMeshComponent->SetupAttachment(AssetRoot); + StaticMeshComponent->RegisterComponent(); + StaticMeshComponent->SetRelativeTransform(Node.Transform); + + // Custom event handling for when a static mesh component is created + ReceiveOnStaticMeshComponentCreated(StaticMeshComponent, Node); + + return StaticMeshComponent; } diff --git a/Source/RpmNextGen/Private/RpmLoaderComponent.cpp b/Source/RpmNextGen/Private/RpmLoaderComponent.cpp new file mode 100644 index 0000000..87a19a4 --- /dev/null +++ b/Source/RpmNextGen/Private/RpmLoaderComponent.cpp @@ -0,0 +1,102 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "RpmLoaderComponent.h" + +#include "Api/Assets/AssetApi.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 "Cache/AssetStorageManager.h" +#include "Settings/RpmDeveloperSettings.h" + +URpmLoaderComponent::URpmLoaderComponent() +{ + PrimaryComponentTick.bCanEverTick = false; + const URpmDeveloperSettings* RpmSettings = GetDefault(); + AppId = RpmSettings->ApplicationId; + CharacterApi = MakeShared(); + PreviewAssetMap = TMap(); + CharacterApi->OnCharacterCreateResponse.BindUObject(this, &URpmLoaderComponent::HandleCharacterCreateResponse); + CharacterApi->OnCharacterUpdateResponse.BindUObject(this, &URpmLoaderComponent::HandleCharacterUpdateResponse); + CharacterApi->OnCharacterFindResponse.BindUObject(this, &URpmLoaderComponent::HandleCharacterFindResponse); +} + +void URpmLoaderComponent::BeginPlay() +{ + Super::BeginPlay(); +} + +void URpmLoaderComponent::CreateCharacter(const FString& BaseModelId, bool bUseCache) +{ + CharacterData.BaseModelId = BaseModelId; + if(bUseCache) + { + FAssetSaveData AssetData; + if(FAssetStorageManager::Get().GetCachedAsset(BaseModelId, AssetData)) + { + + CharacterData.Assets.Add(FAssetApi::BaseModelType, AssetData.Id); + OnCharacterCreated.Broadcast(CharacterData); + } + } + else + { + FCharacterCreateRequest CharacterCreateRequest = FCharacterCreateRequest(); + CharacterCreateRequest.Data.Assets = TMap(); + CharacterCreateRequest.Data.Assets.Add(FAssetApi::BaseModelType, BaseModelId); + CharacterCreateRequest.Data.ApplicationId = AppId; + CharacterApi->CreateAsync(CharacterCreateRequest); + } +} + +void URpmLoaderComponent::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); +} + +void URpmLoaderComponent::HandleCharacterCreateResponse(FCharacterCreateResponse CharacterCreateResponse, + bool bWasSuccessful) +{ + CharacterData.Assets.Append(CharacterCreateResponse.Data.Assets); + CharacterData.Id = CharacterCreateResponse.Data.Id; + OnCharacterCreated.Broadcast(CharacterData); +} + +void URpmLoaderComponent::HandleCharacterUpdateResponse(FCharacterUpdateResponse CharacterUpdateResponse, + bool bWasSuccessful) +{ + CharacterData.Assets.Append(CharacterUpdateResponse.Data.Assets); + OnCharacterUpdated.Broadcast(CharacterData); +} + +void URpmLoaderComponent::HandleCharacterFindResponse(FCharacterFindByIdResponse CharacterFindByIdResponse, + bool bWasSuccessful) +{ + CharacterData.Assets.Append(CharacterFindByIdResponse.Data.Assets); + OnCharacterFound.Broadcast(CharacterData); +} + + +// Called every frame +void URpmLoaderComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + // ... +} + diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetApi.h b/Source/RpmNextGen/Public/Api/Assets/AssetApi.h index 6a62795..24eebc5 100644 --- a/Source/RpmNextGen/Public/Api/Assets/AssetApi.h +++ b/Source/RpmNextGen/Public/Api/Assets/AssetApi.h @@ -19,8 +19,6 @@ class RPMNEXTGEN_API FAssetApi : public FWebApiWithAuth FOnListAssetTypeResponse OnListAssetTypeResponse; static const FString BaseModelType; private: - void HandleListAssetResponse(FString Response, bool bWasSuccessful); - void HandleListAssetTypeResponse(FString Response, bool bWasSuccessful); + void HandleResponse(FString Response, bool bWasSuccessful); FString ApiBaseUrl; - }; diff --git a/Source/RpmNextGen/Public/RpmActor.h b/Source/RpmNextGen/Public/RpmActor.h index 5bd9bb0..59d7c89 100644 --- a/Source/RpmNextGen/Public/RpmActor.h +++ b/Source/RpmNextGen/Public/RpmActor.h @@ -68,16 +68,23 @@ class RPMNEXTGEN_API ARpmActor : public AActor UFUNCTION(BlueprintCallable, Category = "Ready Player Me") virtual void LoadGltfAsset(UglTFRuntimeAsset* GltfAsset); + void ClearLoadedComponents(); virtual void SetupAsset(); + + void LoadClothingAsset(UglTFRuntimeAsset* GltfAsset, const FString& ClothingType); + void RemoveClothingComponents(const FString& ClothingType); private: + TArray CreateClothingMeshComponents(UglTFRuntimeAsset* GltfAsset, const FString& ClothingType); UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"), Category="Ready Player Me|glTFRuntime") USceneComponent* AssetRoot; - + TMap> LoadedClothingComponents; void ProcessBoneNode(USceneComponent* NodeParentComponent, FglTFRuntimeNode& Node); USceneComponent* CreateNewComponent(USceneComponent* NodeParentComponent, FglTFRuntimeNode& Node); + USkeletalMeshComponent* CreateSkeletalMeshComponent(USceneComponent* NodeParentComponent, FglTFRuntimeNode& Node); + UStaticMeshComponent* CreateStaticMeshComponent(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 index 0f538ec..f62b62b 100644 --- a/Source/RpmNextGen/Public/RpmAssetLoaderComponent.h +++ b/Source/RpmNextGen/Public/RpmAssetLoaderComponent.h @@ -8,6 +8,7 @@ class UglTFRuntimeAsset; class FGlbLoader; + DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGltfAssetLoaded, UglTFRuntimeAsset*, Asset); UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) diff --git a/Source/RpmNextGen/Public/RpmLoaderComponent.h b/Source/RpmNextGen/Public/RpmLoaderComponent.h new file mode 100644 index 0000000..ad02949 --- /dev/null +++ b/Source/RpmNextGen/Public/RpmLoaderComponent.h @@ -0,0 +1,87 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Api/Characters/Models/RpmCharacter.h" +#include "Components/ActorComponent.h" +#include "RpmLoaderComponent.generated.h" + +struct FCharacterCreateResponse; +struct FCharacterUpdateResponse; +struct FCharacterFindByIdResponse; +class FCharacterApi; +struct FAsset; + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FRpmCharacterData +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString Id; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString BaseModelId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "assets")) + TMap Assets; + + FRpmCharacterData() + { + Id = ""; + BaseModelId = ""; + Assets = TMap(); + } + + FRpmCharacterData(FRpmCharacter Character) + { + Id = Character.Id; + Assets = Character.Assets; + } +}; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCharacterCreated, FRpmCharacterData, CharacterData); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCharacterUpdated, FRpmCharacterData, CharacterData); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCharacterFound, FRpmCharacterData, CharacterData); + +UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) +class RPMNEXTGEN_API URpmLoaderComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + URpmLoaderComponent(); + FOnCharacterCreated OnCharacterCreated; + FOnCharacterUpdated OnCharacterUpdated; + FOnCharacterFound OnCharacterFound; +protected: + virtual void BeginPlay() override; + + UFUNCTION(BlueprintCallable, Category = "Ready Player Me") + virtual void CreateCharacter(const FString& BaseModelId, bool bUseCache); + + + 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); + +public: + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + +protected: + FString AppId; + FRpmCharacter Character; + TMap PreviewAssetMap; + FRpmCharacterData CharacterData; +private: + TSharedPtr CharacterApi; + +}; From 5cb3d6d71b4dca7322495e32e676a9dc57ad52bd Mon Sep 17 00:00:00 2001 From: Harrison Date: Wed, 11 Sep 2024 14:57:12 +0300 Subject: [PATCH 24/54] feat: first mvp of loading from offline cache --- .../Blueprints/BP_RpmPreviewActor.uasset | Bin 31669 -> 32737 bytes .../BasicLoader/Blueprints/BP_RpmTest.uasset | Bin 0 -> 41705 bytes .../BasicLoader/RpmBasicLoaderSample.umap | Bin 133737 -> 130715 bytes .../Private/Api/Assets/AssetLoader.cpp | 4 +- .../RpmNextGen/Private/Api/Files/FileApi.cpp | 37 ++- .../Private/Api/Files/GlbLoader.cpp | 54 ++-- .../RpmNextGen/Private/Cache/AssetSaver.cpp | 2 - .../RpmNextGen/Private/Cache/FileWriter.cpp | 2 + Source/RpmNextGen/Private/RpmActor.cpp | 286 ++++-------------- .../Private/RpmAssetLoaderComponent.cpp | 15 +- .../RpmNextGen/Private/RpmLoaderComponent.cpp | 106 +++++-- .../Private/RpmPreviewLoaderComponent.cpp | 1 + .../Public/Api/Assets/AssetLoader.h | 2 +- Source/RpmNextGen/Public/Api/Files/FileApi.h | 7 +- .../RpmNextGen/Public/Api/Files/GlbLoader.h | 19 +- Source/RpmNextGen/Public/Cache/AssetSaver.h | 31 -- .../Public/Cache/AssetStorageManager.h | 26 +- .../RpmNextGen/Public/Cache/CacheGenerator.h | 2 +- .../{AssetSaveData.h => CachedAssetData.h} | 12 +- Source/RpmNextGen/Public/Cache/FileWriter.h | 20 ++ Source/RpmNextGen/Public/RpmActor.h | 63 +--- .../Public/RpmAssetLoaderComponent.h | 6 +- Source/RpmNextGen/Public/RpmLoaderComponent.h | 25 +- .../Private/EditorAssetLoader.cpp | 10 +- .../Public/EditorAssetLoader.h | 2 +- 25 files changed, 300 insertions(+), 432 deletions(-) create mode 100644 Content/Samples/BasicLoader/Blueprints/BP_RpmTest.uasset delete mode 100644 Source/RpmNextGen/Private/Cache/AssetSaver.cpp create mode 100644 Source/RpmNextGen/Private/Cache/FileWriter.cpp delete mode 100644 Source/RpmNextGen/Public/Cache/AssetSaver.h rename Source/RpmNextGen/Public/Cache/{AssetSaveData.h => CachedAssetData.h} (92%) create mode 100644 Source/RpmNextGen/Public/Cache/FileWriter.h diff --git a/Content/Samples/BasicLoader/Blueprints/BP_RpmPreviewActor.uasset b/Content/Samples/BasicLoader/Blueprints/BP_RpmPreviewActor.uasset index 33daf78d732bee444791672c214b9747a2988c31..a9e75fce2b0e7b781bc80a2103970732b6824847 100644 GIT binary patch delta 4092 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 diff --git a/Content/Samples/BasicLoader/Blueprints/BP_RpmTest.uasset b/Content/Samples/BasicLoader/Blueprints/BP_RpmTest.uasset new file mode 100644 index 0000000000000000000000000000000000000000..e6a09393bf0581b9f2e5fc4e68f0bdd4fb71dc1a GIT binary patch literal 41705 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 P$ek3;)_i|t>4*OTa1X`s`D-Vpiq4_g?OG!)uR! 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 diff --git a/Content/Samples/BasicUI/RpmBasicUISample.umap b/Content/Samples/BasicUI/RpmBasicUISample.umap index f81c34e23e625f3da32abcd30eec86fddbf170ef..7c25829d7744a1aaa03fc4686edae2975152eced 100644 GIT binary patch literal 78012 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 literal 71843 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 XH-bU>l|dl-|6+2F(EgFL2CVphp;d^< diff --git a/Source/RpmNextGen/Private/Api/Files/FileApi.cpp b/Source/RpmNextGen/Private/Api/Files/FileApi.cpp index f08df34..dc86c67 100644 --- a/Source/RpmNextGen/Private/Api/Files/FileApi.cpp +++ b/Source/RpmNextGen/Private/Api/Files/FileApi.cpp @@ -40,16 +40,14 @@ void FFileApi::FileRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Res OnFileRequestComplete.ExecuteIfBound(nullptr, FileName, AssetType); } -TArray* FFileApi::LoadFileFromPath(const FString& Path) +bool FFileApi::LoadFileFromPath(const FString& Path, TArray& OutContent) { const FString FileName = FPaths::GetCleanFilename(Path); - if(!FPaths::FileExists(Path)) + if (!FPaths::FileExists(Path)) { UE_LOG(LogReadyPlayerMe, Error, TEXT("Path does not exist %s"), *Path); - - return nullptr; + return false; } - TArray Content; - FFileHelper::LoadFileToArray(Content, *Path); - return &Content; + + return FFileHelper::LoadFileToArray(OutContent, *Path); } diff --git a/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp b/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp index 7020fae..18d3e7a 100644 --- a/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp +++ b/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp @@ -2,17 +2,30 @@ #include "RpmFunctionLibrary.h" - #include "RpmNextGen.h" #include "Api/Assets/AssetApi.h" #include "Api/Assets/Models/AssetListRequest.h" #include "Api/Assets/Models/AssetListResponse.h" #include "Api/Auth/ApiKeyAuthStrategy.h" +#include "Cache/AssetCacheManager.h" +#include "Cache/CachedAssetData.h" #include "Settings/RpmDeveloperSettings.h" #include "Utilities/ConnectionManager.h" void URpmFunctionLibrary::FetchFirstAssetId(UObject* WorldContextObject, const FString& AssetType, FOnAssetIdFetched OnAssetIdFetched) { + if(!IsInternetConnected()) + { + TArray CachedAssets = FAssetCacheManager::Get().GetAssetsOfType(AssetType); + if( CachedAssets.Num() > 0) + { + OnAssetIdFetched.ExecuteIfBound(CachedAssets[0].Id); + return; + } + UE_LOG(LogReadyPlayerMe, Warning, TEXT("Unable to fetch first asset from cache.")); + return; + } + TSharedPtr AssetApi = MakeShared(); const URpmDeveloperSettings* RpmSettings = GetDefault(); if(!RpmSettings->ApiKey.IsEmpty() || RpmSettings->ApiProxyUrl.IsEmpty()) @@ -46,7 +59,6 @@ void URpmFunctionLibrary::FetchFirstAssetId(UObject* WorldContextObject, const F bool URpmFunctionLibrary::IsInternetConnected() { - // Use FConnectionManager to get the current internet connection status return FConnectionManager::Get().IsConnected(); } diff --git a/Source/RpmNextGen/Private/RpmLoaderComponent.cpp b/Source/RpmNextGen/Private/RpmLoaderComponent.cpp index fdf93c8..5c2d2b2 100644 --- a/Source/RpmNextGen/Private/RpmLoaderComponent.cpp +++ b/Source/RpmNextGen/Private/RpmLoaderComponent.cpp @@ -4,8 +4,8 @@ #include "RpmLoaderComponent.h" #include "glTFRuntimeFunctionLibrary.h" +#include "RpmFunctionLibrary.h" #include "Api/Assets/AssetApi.h" -#include "Api/Assets/AssetGlbLoader.h" #include "Api/Assets/Models/Asset.h" #include "Api/Characters/CharacterApi.h" #include "Api/Characters/Models/CharacterCreateResponse.h" @@ -50,7 +50,7 @@ void URpmLoaderComponent::BeginPlay() void URpmLoaderComponent::CreateCharacter(const FString& BaseModelId, bool bUseCache) { CharacterData.BaseModelId = BaseModelId; - if(bUseCache) + if(!FConnectionManager::Get().IsConnected() || bUseCache) { FCachedAssetData CachedAssetData; if(FAssetCacheManager::Get().GetCachedAsset(BaseModelId, CachedAssetData)) @@ -58,8 +58,12 @@ void URpmLoaderComponent::CreateCharacter(const FString& BaseModelId, bool bUseC const FAsset AssetFromCache = CachedAssetData.ToAsset(); CharacterData.Assets.Add(FAssetApi::BaseModelType, AssetFromCache); OnCharacterCreated.Broadcast(CharacterData); - UglTFRuntimeAsset* GltfAsset = LoadGltfRuntimeAssetFromCache(AssetFromCache); - HandleGltfAssetLoaded(GltfAsset, FAssetApi::BaseModelType); + TArray Data; + if(FFileApi::LoadFileFromPath(CachedAssetData.GlbPathsByBaseModelId[CharacterData.BaseModelId], Data)) + { + UglTFRuntimeAsset* GltfRuntimeAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(Data, *GltfConfig); + HandleGltfAssetLoaded(GltfRuntimeAsset, FAssetApi::BaseModelType); + } return; } UE_LOG(LogReadyPlayerMe, Warning, TEXT("Unable to create character from cache. Will try to create from Url."), *CachedAssetData.Id); @@ -83,9 +87,12 @@ UglTFRuntimeAsset* URpmLoaderComponent::LoadGltfRuntimeAssetFromCache(const FAss if(FAssetCacheManager::Get().GetCachedAsset(Asset.Id, ExistingAsset)) { CharacterData.Assets.Add(ExistingAsset.Type, Asset); - const TArray* Data = GlbLoader->LoadFileFromPath(ExistingAsset.GlbPathsByBaseModelId[CharacterData.BaseModelId]); - UglTFRuntimeAsset* GltfRuntimeAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(*Data, *GltfConfig); - return GltfRuntimeAsset; + TArray Data; + if(FFileApi::LoadFileFromPath(ExistingAsset.GlbPathsByBaseModelId[CharacterData.BaseModelId], Data)) + { + UglTFRuntimeAsset* GltfRuntimeAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(Data, *GltfConfig); + return GltfRuntimeAsset; + } } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load gltf asset from cache")); return nullptr; @@ -135,7 +142,7 @@ void URpmLoaderComponent::LoadAssetPreview(FAsset AssetData, bool bUseCache) } CharacterData.Assets.Add(AssetData.Type, AssetData); - if(bUseCache) + if(!FConnectionManager::Get().IsConnected() || bUseCache) { if(bIsBaseModel && CharacterData.Assets.Num() > 1) { diff --git a/Source/RpmNextGen/Private/Samples/RpmAssetCardWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmAssetCardWidget.cpp index 6cdcc99..6ae9008 100644 --- a/Source/RpmNextGen/Private/Samples/RpmAssetCardWidget.cpp +++ b/Source/RpmNextGen/Private/Samples/RpmAssetCardWidget.cpp @@ -11,7 +11,7 @@ void URpmAssetCardWidget::NativeConstruct() { Super::NativeConstruct(); this->SetVisibility(ESlateVisibility::Hidden); - if(TextureLoader.IsValid()) + if(!TextureLoader.IsValid()) { TextureLoader = MakeShared(); TextureLoader->OnTextureLoaded.BindUObject(this, &URpmAssetCardWidget::OnTextureLoaded); @@ -34,7 +34,7 @@ void URpmAssetCardWidget::InitializeCard(const FAsset& Asset) void URpmAssetCardWidget::LoadImage(const FAsset& Asset) { AssetData = Asset; - TextureLoader->LoadIconFromAsset(Asset); + TextureLoader->LoadIconFromAsset(AssetData); } void URpmAssetCardWidget::OnTextureLoaded(UTexture2D* Texture2D) diff --git a/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp index c165d46..cbb5ccb 100644 --- a/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp +++ b/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp @@ -7,10 +7,13 @@ #include "Api/Assets/AssetApi.h" #include "Api/Assets/Models/AssetListRequest.h" #include "Api/Auth/ApiKeyAuthStrategy.h" +#include "Cache/AssetCacheManager.h" +#include "Cache/CachedAssetData.h" #include "Components/PanelWidget.h" #include "Components/SizeBox.h" #include "Samples/RpmAssetButtonWidget.h" #include "Settings/RpmDeveloperSettings.h" +#include "Utilities/ConnectionManager.h" void URpmAssetPanelWidget::NativeConstruct() { @@ -24,7 +27,6 @@ void URpmAssetPanelWidget::NativeConstruct() } AssetApi->OnListAssetsResponse.BindUObject(this, &URpmAssetPanelWidget::OnAssetListResponse); - ButtonSize = FVector2D(200, 200); ImageSize = FVector2D(200, 200); } @@ -39,6 +41,15 @@ void URpmAssetPanelWidget::OnAssetListResponse(const FAssetListResponse& AssetLi UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to fetch assets")); } +void URpmAssetPanelWidget::LoadAssetsFromCache(const FString& AssetType) +{ + TArray CachedAssets = FAssetCacheManager::Get().GetAssetsOfType(AssetType); + for (auto CachedAsset : CachedAssets) + { + CreateButton(CachedAsset.ToAsset()); + } +} + void URpmAssetPanelWidget::CreateButtonsFromAssets(TArray Assets) { for (auto Asset : Assets) @@ -110,6 +121,12 @@ void URpmAssetPanelWidget::LoadAssetsOfType(const FString& AssetType) UE_LOG(LogReadyPlayerMe, Error, TEXT("AssetApi is null or invalid")); return; } + if(!FConnectionManager::Get().IsConnected()) + { + UE_LOG(LogReadyPlayerMe, Warning, TEXT("No internet connection, loading assets from cache")); + LoadAssetsFromCache(AssetType); + return; + } const URpmDeveloperSettings* RpmSettings = GetDefault(); FAssetListQueryParams QueryParams; QueryParams.Type = AssetType; diff --git a/Source/RpmNextGen/Public/Api/Files/FileApi.h b/Source/RpmNextGen/Public/Api/Files/FileApi.h index e032c10..d6b5d16 100644 --- a/Source/RpmNextGen/Public/Api/Files/FileApi.h +++ b/Source/RpmNextGen/Public/Api/Files/FileApi.h @@ -1,6 +1,7 @@ #pragma once #include "CoreMinimal.h" +#include "RpmNextGen.h" #include "Interfaces/IHttpRequest.h" DECLARE_DELEGATE_ThreeParams(FOnFileRequestComplete, TArray*, const FString&, const FString&); @@ -12,7 +13,7 @@ class RPMNEXTGEN_API FFileApi : public TSharedFromThis virtual ~FFileApi(); virtual void LoadFileFromUrl(const FString& URL, const FString& AssetType = TEXT("")); virtual void FileRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FString AssetType); - virtual TArray* LoadFileFromPath(const FString& Path); + static bool LoadFileFromPath(const FString& Path, TArray& OutContent); FOnFileRequestComplete OnFileRequestComplete; }; diff --git a/Source/RpmNextGen/Public/Cache/AssetCacheManager.h b/Source/RpmNextGen/Public/Cache/AssetCacheManager.h index 52fe79d..0e65720 100644 --- a/Source/RpmNextGen/Public/Cache/AssetCacheManager.h +++ b/Source/RpmNextGen/Public/Cache/AssetCacheManager.h @@ -56,6 +56,20 @@ class FAssetCacheManager return TArray(); } + TArray GetAssetsOfType(const FString& AssetType) const + { + TArray Assets; + for (const auto& Entry : StoredAssets) + { + const FCachedAssetData& CachedAsset = Entry.Value; + if (CachedAsset.Type == AssetType) + { + Assets.Add(CachedAsset); + } + } + return Assets; + } + void StoreAndTrackIcon(const FAssetLoadingContext& Context, const bool bSaveManifest = true) { const FCachedAssetData& StoredAsset = FCachedAssetData(Context.Asset); diff --git a/Source/RpmNextGen/Public/RpmLoaderComponent.h b/Source/RpmNextGen/Public/RpmLoaderComponent.h index 975ad82..8ea12c2 100644 --- a/Source/RpmNextGen/Public/RpmLoaderComponent.h +++ b/Source/RpmNextGen/Public/RpmLoaderComponent.h @@ -51,7 +51,7 @@ class RPMNEXTGEN_API URpmLoaderComponent : public UActorComponent URpmLoaderComponent(); void SetGltfConfig(FglTFRuntimeConfig* Config) const; - FglTFRuntimeConfig* GltfConfig; + FglTFRuntimeConfig* GltfConfig = nullptr; UPROPERTY(BlueprintAssignable, Category = "Ready Player Me" ) FOnAssetLoaded OnGltfAssetLoaded; diff --git a/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h b/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h index 0b50b3a..3e89ee5 100644 --- a/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h +++ b/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h @@ -56,6 +56,9 @@ class RPMNEXTGEN_API URpmAssetPanelWidget : public UUserWidget UFUNCTION() void OnAssetListResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful); + UFUNCTION(BlueprintCallable, Category = "Asset Panel") + void LoadAssetsFromCache(const FString& AssetType); + UFUNCTION(BlueprintCallable, Category = "Asset Panel") void LoadAssetsOfType(const FString& AssetType); From 3bb669a0a3f28daad5a29ebf156b60d319f81e62 Mon Sep 17 00:00:00 2001 From: Harrison Date: Tue, 17 Sep 2024 14:29:37 +0300 Subject: [PATCH 41/54] chore: rename and added cache generator reset --- .../Private/Cache/CacheGenerator.cpp | 13 ++++++ .../RpmNextGen/Public/Cache/CacheGenerator.h | 2 + .../Private/RpmNextGenEditor.cpp | 14 +++---- .../UI/Commands/CacheWindowCommands.cpp | 2 +- ...orWidget.cpp => SCacheGeneratorWidget.cpp} | 40 +++++++++---------- .../Public/UI/Commands/CacheWindowCommands.h | 2 +- ...EditorWidget.h => SCacheGeneratorWidget.h} | 4 +- 7 files changed, 46 insertions(+), 31 deletions(-) rename Source/RpmNextGenEditor/Private/UI/{SCacheEditorWidget.cpp => SCacheGeneratorWidget.cpp} (84%) rename Source/RpmNextGenEditor/Public/UI/{SCacheEditorWidget.h => SCacheGeneratorWidget.h} (93%) diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index b1cd56f..85b9bf5 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -36,6 +36,7 @@ void FCacheGenerator::DownloadRemoteCacheFromUrl(const FString& Url) void FCacheGenerator::GenerateLocalCache(int InItemsPerCategory) { + Reset(); MaxItemsPerCategory = InItemsPerCategory; FetchBaseModels(); } @@ -106,6 +107,18 @@ void FCacheGenerator::LoadAndStoreAssetIcon(const FString& BaseModelId, const FA AssetLoader->LoadIcon(*Asset, true); } +void FCacheGenerator::Reset() +{ + AssetMapByBaseModelId.Empty(); + AssetListRequests.Empty(); + AssetTypes.Empty(); + CurrentBaseModelIndex = 0; + RefittedAssetRequestsCompleted = 0; + RequiredAssetDownloadRequest = 0; + NumberOfAssetsSaved = 0; + FAssetCacheManager::Get().ClearAllCache(); +} + void FCacheGenerator::OnAssetGlbSaved(const FAsset& Asset, const TArray& Data) { NumberOfAssetsSaved++; diff --git a/Source/RpmNextGen/Public/Cache/CacheGenerator.h b/Source/RpmNextGen/Public/Cache/CacheGenerator.h index 0cdcef6..07adc0e 100644 --- a/Source/RpmNextGen/Public/Cache/CacheGenerator.h +++ b/Source/RpmNextGen/Public/Cache/CacheGenerator.h @@ -33,6 +33,8 @@ class RPMNEXTGEN_API FCacheGenerator : public TSharedFromThis void LoadAndStoreAssetGlb(const FString& BaseModelId, const FAsset* Asset); void LoadAndStoreAssetIcon(const FString& BaseModelId, const FAsset* Asset); + void Reset(); + protected: void FetchBaseModels() const; void FetchAssetTypes() const; diff --git a/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp b/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp index 7c12189..753a8cc 100644 --- a/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp +++ b/Source/RpmNextGenEditor/Private/RpmNextGenEditor.cpp @@ -10,12 +10,12 @@ #include "Widgets/Text/STextBlock.h" #include "ToolMenus.h" #include "UI/LoginWindowStyle.h" -#include "UI/SCacheEditorWidget.h" +#include "UI/SCacheGeneratorWidget.h" #include "UI/Commands/CacheWindowCommands.h" static const FName DeveloperWindowName("LoginWindow"); static const FName LoaderWindowName("LoaderWindow"); -static const FName CacheWindowName("CacheWindow"); +static const FName CacheWindowName("CacheGeneratorWindow"); #define LOCTEXT_NAMESPACE "RpmNextGenEditorModule" @@ -54,7 +54,7 @@ void FRpmNextGenEditorModule::StartupModule() .SetMenuType(ETabSpawnerMenuType::Hidden); FGlobalTabmanager::Get()->RegisterNomadTabSpawner(CacheWindowName, FOnSpawnTab::CreateRaw(this, &FRpmNextGenEditorModule::OnSpawnCacheWindow)) - .SetDisplayName(LOCTEXT("CacheEditorWidget", "Cache Editor")) + .SetDisplayName(LOCTEXT("CacheGeneratorrWidget", "Cache Generator")) .SetMenuType(ETabSpawnerMenuType::Hidden); // Don't show Loader window in the menu @@ -98,9 +98,9 @@ void FRpmNextGenEditorModule::FillReadyPlayerMeMenu(UToolMenu* Menu) Section.AddMenuEntry( - "OpenCacheEditorWindow", - LOCTEXT("OpenCacheEditorWindow", "Cache Editor"), - LOCTEXT("OpenLoaderWindowToolTip", "Cache Editor Window."), + "OpenCacheGeneratorWindow", + LOCTEXT("OpenCacheGeneratorWindow", "Cache Generator"), + LOCTEXT("OpenGeneratorWindowToolTip", "Cache Generator Window."), FSlateIcon(), FUIAction(FExecuteAction::CreateRaw(this, &FRpmNextGenEditorModule::OpenCacheEditorWindow)) ); @@ -162,7 +162,7 @@ TSharedRef FRpmNextGenEditorModule::OnSpawnCacheWindow(const FSpawnTab return SNew(SDockTab) .TabRole(NomadTab) [ - SNew(SCacheEditorWidget) + SNew(SCacheGeneratorWidget) ]; } diff --git a/Source/RpmNextGenEditor/Private/UI/Commands/CacheWindowCommands.cpp b/Source/RpmNextGenEditor/Private/UI/Commands/CacheWindowCommands.cpp index b91b53f..26cc19e 100644 --- a/Source/RpmNextGenEditor/Private/UI/Commands/CacheWindowCommands.cpp +++ b/Source/RpmNextGenEditor/Private/UI/Commands/CacheWindowCommands.cpp @@ -4,7 +4,7 @@ void FCacheWindowCommands::RegisterCommands() { - UI_COMMAND(OpenPluginWindow, "Cache window", "Bring up RPM Cache window", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(OpenPluginWindow, "Cache Generator window", "Bring up RPM Cache Generator window", EUserInterfaceActionType::Button, FInputChord()); } #undef LOCTEXT_NAMESPACE diff --git a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp similarity index 84% rename from Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp rename to Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp index 81b9dbc..d82d2eb 100644 --- a/Source/RpmNextGenEditor/Private/UI/SCacheEditorWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp @@ -1,4 +1,4 @@ -#include "UI/SCacheEditorWidget.h" +#include "UI/SCacheGeneratorWidget.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SEditableTextBox.h" #include "EditorStyleSet.h" @@ -9,14 +9,14 @@ #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Layout/SScrollBox.h" -void SCacheEditorWidget::Construct(const FArguments& InArgs) +void SCacheGeneratorWidget::Construct(const FArguments& InArgs) { if(!CacheGenerator) { CacheGenerator = MakeUnique(); - CacheGenerator->OnCacheDataLoaded.BindRaw(this, &SCacheEditorWidget::OnFetchCacheDataComplete); - CacheGenerator->OnDownloadRemoteCacheDelegate.BindRaw(this, &SCacheEditorWidget::OnDownloadRemoteCacheComplete); - CacheGenerator->OnLocalCacheGenerated.BindRaw(this, &SCacheEditorWidget::OnGenerateLocalCacheCompleted); + CacheGenerator->OnCacheDataLoaded.BindRaw(this, &SCacheGeneratorWidget::OnFetchCacheDataComplete); + CacheGenerator->OnDownloadRemoteCacheDelegate.BindRaw(this, &SCacheGeneratorWidget::OnDownloadRemoteCacheComplete); + CacheGenerator->OnLocalCacheGenerated.BindRaw(this, &SCacheGeneratorWidget::OnGenerateLocalCacheCompleted); } ChildSlot [ @@ -53,8 +53,8 @@ void SCacheEditorWidget::Construct(const FArguments& InArgs) .FillWidth(1.0f) [ SNew(SNumericEntryBox) - .Value(this, &SCacheEditorWidget::GetItemsPerCategory) - .OnValueChanged(this, &SCacheEditorWidget::OnItemsPerCategoryChanged) + .Value(this, &SCacheGeneratorWidget::GetItemsPerCategory) + .OnValueChanged(this, &SCacheGeneratorWidget::OnItemsPerCategoryChanged) .AllowSpin(true) // Slider-like behavior .MinValue(1) .MaxValue(30) @@ -70,7 +70,7 @@ void SCacheEditorWidget::Construct(const FArguments& InArgs) [ SNew(SButton) .Text(FText::FromString("Generate offline cache")) - .OnClicked(this, &SCacheEditorWidget::OnGenerateOfflineCacheClicked) + .OnClicked(this, &SCacheGeneratorWidget::OnGenerateOfflineCacheClicked) .HAlign(HAlign_Center) .VAlign(VAlign_Center) ] @@ -99,7 +99,7 @@ void SCacheEditorWidget::Construct(const FArguments& InArgs) [ SNew(SButton) .Text(FText::FromString("Open Local Cache Folder")) - .OnClicked(this, &SCacheEditorWidget::OnOpenLocalCacheFolderClicked) + .OnClicked(this, &SCacheGeneratorWidget::OnOpenLocalCacheFolderClicked) .HAlign(HAlign_Center) .VAlign(VAlign_Center) ] @@ -162,19 +162,19 @@ void SCacheEditorWidget::Construct(const FArguments& InArgs) } -FReply SCacheEditorWidget::OnGenerateOfflineCacheClicked() +FReply SCacheGeneratorWidget::OnGenerateOfflineCacheClicked() { CacheGenerator->GenerateLocalCache(ItemsPerCategory); return FReply::Handled(); } -FReply SCacheEditorWidget::OnExtractCacheClicked() +FReply SCacheGeneratorWidget::OnExtractCacheClicked() { // Handle extracting the cache return FReply::Handled(); } -FReply SCacheEditorWidget::OnOpenLocalCacheFolderClicked() +FReply SCacheGeneratorWidget::OnOpenLocalCacheFolderClicked() { const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); @@ -192,23 +192,23 @@ FReply SCacheEditorWidget::OnOpenLocalCacheFolderClicked() return FReply::Handled(); } -FReply SCacheEditorWidget::OnDownloadRemoteCacheClicked() +FReply SCacheGeneratorWidget::OnDownloadRemoteCacheClicked() { // Handle downloading the remote cache return FReply::Handled(); } -void SCacheEditorWidget::OnFetchCacheDataComplete(bool bWasSuccessful) +void SCacheGeneratorWidget::OnFetchCacheDataComplete(bool bWasSuccessful) { UE_LOG(LogReadyPlayerMe, Log, TEXT("Completed fetching assets")); CacheGenerator->LoadAndStoreAssets(); } -void SCacheEditorWidget::OnDownloadRemoteCacheComplete(bool bWasSuccessful) +void SCacheGeneratorWidget::OnDownloadRemoteCacheComplete(bool bWasSuccessful) { } -void SCacheEditorWidget::OnGenerateLocalCacheCompleted(bool bWasSuccessful) +void SCacheGeneratorWidget::OnGenerateLocalCacheCompleted(bool bWasSuccessful) { UE_LOG(LogReadyPlayerMe, Log, TEXT("Completed generating cache")); @@ -222,17 +222,17 @@ void SCacheEditorWidget::OnGenerateLocalCacheCompleted(bool bWasSuccessful) // CreatePakFile(PakFilePath, ResponseFilePath); } -void SCacheEditorWidget::OnItemsPerCategoryChanged(float NewValue) +void SCacheGeneratorWidget::OnItemsPerCategoryChanged(float NewValue) { ItemsPerCategory = NewValue; } -void SCacheEditorWidget::OnCacheUrlChanged(const FText& NewText) +void SCacheGeneratorWidget::OnCacheUrlChanged(const FText& NewText) { CacheUrl = NewText.ToString(); } -void SCacheEditorWidget::CreatePakFile(const FString& PakFilePath, const FString& ResponseFilePath) +void SCacheGeneratorWidget::CreatePakFile(const FString& PakFilePath, const FString& ResponseFilePath) { // Path to the UnrealPak executable FString UnrealPakPath = FPaths::ConvertRelativePathToFull(FPaths::EngineDir() / TEXT("Binaries/Win64/UnrealPak.exe")); @@ -257,7 +257,7 @@ void SCacheEditorWidget::CreatePakFile(const FString& PakFilePath, const FString } -void SCacheEditorWidget::GeneratePakResponseFile(const FString& ResponseFilePath, const FString& FolderToPak) +void SCacheGeneratorWidget::GeneratePakResponseFile(const FString& ResponseFilePath, const FString& FolderToPak) { TArray Files; IFileManager::Get().FindFilesRecursive(Files, *FolderToPak, TEXT("*.*"), true, false); diff --git a/Source/RpmNextGenEditor/Public/UI/Commands/CacheWindowCommands.h b/Source/RpmNextGenEditor/Public/UI/Commands/CacheWindowCommands.h index a592147..e9d6ad3 100644 --- a/Source/RpmNextGenEditor/Public/UI/Commands/CacheWindowCommands.h +++ b/Source/RpmNextGenEditor/Public/UI/Commands/CacheWindowCommands.h @@ -7,7 +7,7 @@ class RPMNEXTGENEDITOR_API FCacheWindowCommands : public TCommands(TEXT("CacheWindow"), NSLOCTEXT("Contexts", "CacheWindow", "Cache Window Plugin"), NAME_None, FEditorStyle::GetStyleSetName()) + : TCommands(TEXT("CacheGeneratorWindow"), NSLOCTEXT("Contexts", "CacheGeneratorWindow", "Cache Generator Window"), NAME_None, FEditorStyle::GetStyleSetName()) { } diff --git a/Source/RpmNextGenEditor/Public/UI/SCacheEditorWidget.h b/Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h similarity index 93% rename from Source/RpmNextGenEditor/Public/UI/SCacheEditorWidget.h rename to Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h index 0e9f0a4..d3445d4 100644 --- a/Source/RpmNextGenEditor/Public/UI/SCacheEditorWidget.h +++ b/Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h @@ -5,10 +5,10 @@ class FCacheGenerator; -class SCacheEditorWidget : public SCompoundWidget +class SCacheGeneratorWidget : public SCompoundWidget { public: - SLATE_BEGIN_ARGS(SCacheEditorWidget) {} + SLATE_BEGIN_ARGS(SCacheGeneratorWidget) {} SLATE_END_ARGS() /** Constructs this widget with InArgs */ void Construct(const FArguments& InArgs); From 832019e1f7ab6a5924c77c2bc83b3344107ac828 Mon Sep 17 00:00:00 2001 From: Harrison Date: Tue, 17 Sep 2024 16:49:04 +0300 Subject: [PATCH 42/54] feat: added basic zip and unzip functionality using pak files --- Source/RpmNextGen/Private/RpmNextGen.cpp | 1 + .../Public/Api/Files/PakFileUtility.cpp | 64 +++++++++++++ .../Public/Api/Files/PakFileUtility.h | 12 +++ Source/RpmNextGen/Public/RpmNextGen.h | 4 - Source/RpmNextGen/RpmNextGen.Build.cs | 1 + .../Private/UI/SCacheGeneratorWidget.cpp | 92 +++++-------------- .../Public/UI/SCacheGeneratorWidget.h | 4 - 7 files changed, 102 insertions(+), 76 deletions(-) create mode 100644 Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp create mode 100644 Source/RpmNextGen/Public/Api/Files/PakFileUtility.h diff --git a/Source/RpmNextGen/Private/RpmNextGen.cpp b/Source/RpmNextGen/Private/RpmNextGen.cpp index 34a9117..081caa9 100644 --- a/Source/RpmNextGen/Private/RpmNextGen.cpp +++ b/Source/RpmNextGen/Private/RpmNextGen.cpp @@ -24,6 +24,7 @@ void FRpmNextGenModule::ShutdownModule() void FRpmNextGenModule::InitializeGlobalPaths() { const FString RelativePath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache"); + AssetCachePath = FPaths::ConvertRelativePathToFull(RelativePath); UE_LOG(LogReadyPlayerMe, Log, TEXT("Initialized Asset Cache Path: %s"), *AssetCachePath); diff --git a/Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp b/Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp new file mode 100644 index 0000000..e21995b --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp @@ -0,0 +1,64 @@ +#include "PakFileUtility.h" +#include "IPlatformFilePak.h" +#include "RpmNextGen.h" + +const FString ResponseFilePath = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/RpmCache_ResponseFile.txt"); + +void FPakFileUtility::CreatePakFile(const FString& PakFilePath) +{ + const FString UnrealPakPath = FPaths::ConvertRelativePathToFull(FPaths::EngineDir() / TEXT("Binaries/Win64/UnrealPak.exe")); + const FString CommandLineArgs = FString::Printf(TEXT("%s -Create=%s"), *PakFilePath, *ResponseFilePath); + FProcHandle ProcHandle = FPlatformProcess::CreateProc(*UnrealPakPath, *CommandLineArgs, true, false, false, nullptr, 0, nullptr, nullptr); + + if (ProcHandle.IsValid()) + { + FPlatformProcess::WaitForProc(ProcHandle); + FPlatformProcess::CloseProc(ProcHandle); + + UE_LOG(LogTemp, Log, TEXT("Pak file created successfully: %s"), *PakFilePath); + } + else + { + UE_LOG(LogTemp, Error, TEXT("Failed to create Pak file: %s"), *PakFilePath); + } +} + +void FPakFileUtility::GeneratePakResponseFile(const FString& FolderToPak) +{ + TArray Files; + IFileManager::Get().FindFilesRecursive(Files, *FolderToPak, TEXT("*.*"), true, false); + + FString ResponseFileContent; + int FileCount = 0; + for (const FString& File : Files) + { + FString RelativePath = File; + FPaths::MakePathRelativeTo(RelativePath, *FolderToPak); + ResponseFileContent += FString::Printf(TEXT("\"%s\" \"%s\"\n"), *File, *RelativePath); + FileCount++; + } + + FFileHelper::SaveStringToFile(ResponseFileContent, *ResponseFilePath); + UE_LOG(LogTemp, Log, TEXT("Response file created with %d files"), FileCount); +} + +void FPakFileUtility::ExtractPakFile(const FString& PakFilePath) +{ + const FString UnrealPakPath = FPaths::ConvertRelativePathToFull(FPaths::EngineDir() / TEXT("Binaries/Win64/UnrealPak.exe")); + const FString DestinationPath = FRpmNextGenModule::GetGlobalAssetCachePath(); + const FString CommandLineArgs = FString::Printf(TEXT("%s -Extract %s"), *PakFilePath, *DestinationPath); + + FProcHandle ProcHandle = FPlatformProcess::CreateProc(*UnrealPakPath, *CommandLineArgs, true, false, false, nullptr, 0, nullptr, nullptr); + + if (ProcHandle.IsValid()) + { + FPlatformProcess::WaitForProc(ProcHandle); + FPlatformProcess::CloseProc(ProcHandle); + + UE_LOG(LogTemp, Log, TEXT("Pak file extracted successfully to: %s"), *DestinationPath); + } + else + { + UE_LOG(LogTemp, Error, TEXT("Failed to extract Pak file: %s"), *PakFilePath); + } +} \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Api/Files/PakFileUtility.h b/Source/RpmNextGen/Public/Api/Files/PakFileUtility.h new file mode 100644 index 0000000..1e9caf2 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Files/PakFileUtility.h @@ -0,0 +1,12 @@ +#pragma once + +#include "CoreMinimal.h" + +class RPMNEXTGEN_API FPakFileUtility +{ +public: + static void CreatePakFile(const FString& PakFilePath); + static void GeneratePakResponseFile(const FString& FolderToPak); + static void ExtractPakFile(const FString& PakFilePath); + static void ExtractFilesFromPak(const FString& PakFilePath); +}; \ No newline at end of file diff --git a/Source/RpmNextGen/Public/RpmNextGen.h b/Source/RpmNextGen/Public/RpmNextGen.h index 26ac13a..18749e6 100644 --- a/Source/RpmNextGen/Public/RpmNextGen.h +++ b/Source/RpmNextGen/Public/RpmNextGen.h @@ -15,16 +15,12 @@ class RPMNEXTGEN_API FRpmNextGenModule : public IModuleInterface virtual void StartupModule() override; virtual void ShutdownModule() override; - // Get the global asset cache path static const FString& GetGlobalAssetCachePath() { return AssetCachePath; } private: - // Initialize the asset cache path void InitializeGlobalPaths(); - - // Store the global asset cache path static FString AssetCachePath; }; diff --git a/Source/RpmNextGen/RpmNextGen.Build.cs b/Source/RpmNextGen/RpmNextGen.Build.cs index 4ad0e8a..94fafb2 100644 --- a/Source/RpmNextGen/RpmNextGen.Build.cs +++ b/Source/RpmNextGen/RpmNextGen.Build.cs @@ -30,6 +30,7 @@ public RpmNextGen(ReadOnlyTargetRules Target) : base(Target) "DeveloperSettings", "Slate", "SlateCore", + "PakFile" } ); diff --git a/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp index d82d2eb..904df10 100644 --- a/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp @@ -4,6 +4,7 @@ #include "EditorStyleSet.h" #include "IPlatformFilePak.h" #include "RpmNextGen.h" +#include "Api/Files/PakFileUtility.h" #include "Cache/CacheGenerator.h" #include "Misc/FileHelper.h" #include "Widgets/Input/SNumericEntryBox.h" @@ -75,21 +76,20 @@ void SCacheGeneratorWidget::Construct(const FArguments& InArgs) .VAlign(VAlign_Center) ] ] - // TODO re-enable once we have added unzip logic - // + SVerticalBox::Slot() - // .Padding(5) - // .AutoHeight() - // [ - // SNew(SBox) - // .HeightOverride(40) // Set button height - // [ - // SNew(SButton) - // .Text(FText::FromString("Extract Cache to local folder")) - // .OnClicked(this, &SCacheEditorWidget::OnExtractCacheClicked) - // .HAlign(HAlign_Center) - // .VAlign(VAlign_Center) - // ] - // ] + + SVerticalBox::Slot() + .Padding(5) + .AutoHeight() + [ + SNew(SBox) + .HeightOverride(40) // Set button height + [ + SNew(SButton) + .Text(FText::FromString("Extract Cache to local folder")) + .OnClicked(this, &SCacheGeneratorWidget::OnExtractCacheClicked) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + ] + ] + SVerticalBox::Slot() .Padding(5) .AutoHeight() @@ -170,7 +170,9 @@ FReply SCacheGeneratorWidget::OnGenerateOfflineCacheClicked() FReply SCacheGeneratorWidget::OnExtractCacheClicked() { - // Handle extracting the cache + FString PakFilePath = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/RpmAssetCache.pak"); + FPakFileUtility::ExtractPakFile(PakFilePath); + return FReply::Handled(); } @@ -206,20 +208,18 @@ void SCacheGeneratorWidget::OnFetchCacheDataComplete(bool bWasSuccessful) void SCacheGeneratorWidget::OnDownloadRemoteCacheComplete(bool bWasSuccessful) { + } void SCacheGeneratorWidget::OnGenerateLocalCacheCompleted(bool bWasSuccessful) { UE_LOG(LogReadyPlayerMe, Log, TEXT("Completed generating cache")); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Local cache generated successfully")); + FString FolderToPak = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache"); + FString PakFilePath = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/RpmAssetCache.pak"); - //TODO re-nable once zip extraction is implemented - // UE_LOG(LogReadyPlayerMe, Log, TEXT("Local cache generated successfully")); - // FString FolderToPak = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/LocalCache"); - // FString PakFilePath = FPaths::ProjectPersistentDownloadDir() / TEXT("LocalCacheAssets.pak"); - // FString ResponseFilePath = FPaths::ProjectPersistentDownloadDir() / TEXT("RpmCache_ResponseFile.txt"); - // - // GeneratePakResponseFile(ResponseFilePath, FolderToPak); - // CreatePakFile(PakFilePath, ResponseFilePath); + FPakFileUtility::GeneratePakResponseFile(FolderToPak); + FPakFileUtility::CreatePakFile(PakFilePath); } void SCacheGeneratorWidget::OnItemsPerCategoryChanged(float NewValue) @@ -231,47 +231,3 @@ void SCacheGeneratorWidget::OnCacheUrlChanged(const FText& NewText) { CacheUrl = NewText.ToString(); } - -void SCacheGeneratorWidget::CreatePakFile(const FString& PakFilePath, const FString& ResponseFilePath) -{ - // Path to the UnrealPak executable - FString UnrealPakPath = FPaths::ConvertRelativePathToFull(FPaths::EngineDir() / TEXT("Binaries/Win64/UnrealPak.exe")); - - // Arguments for the UnrealPak tool - FString CommandLineArgs = FString::Printf(TEXT("%s -Create=%s"), *PakFilePath, *ResponseFilePath); - - // Launch the UnrealPak process - FProcHandle ProcHandle = FPlatformProcess::CreateProc(*UnrealPakPath, *CommandLineArgs, true, false, false, nullptr, 0, nullptr, nullptr); - - if (ProcHandle.IsValid()) - { - FPlatformProcess::WaitForProc(ProcHandle); - FPlatformProcess::CloseProc(ProcHandle); - - UE_LOG(LogReadyPlayerMe, Log, TEXT("Pak file created successfully: %s"), *PakFilePath); - } - else - { - UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to create Pak file: %s"), *PakFilePath); - } -} - - -void SCacheGeneratorWidget::GeneratePakResponseFile(const FString& ResponseFilePath, const FString& FolderToPak) -{ - TArray Files; - IFileManager::Get().FindFilesRecursive(Files, *FolderToPak, TEXT("*.*"), true, false); - - FString ResponseFileContent; - int FileCount = 0; - for (const FString& File : Files) - { - FString RelativePath = File; - FPaths::MakePathRelativeTo(RelativePath, *FolderToPak); - ResponseFileContent += FString::Printf(TEXT("\"%s\" \"%s\"\n"), *File, *RelativePath); - FileCount++; - } - FFileHelper::SaveStringToFile(ResponseFileContent, *ResponseFilePath); - // print number of files added to the response file - UE_LOG(LogReadyPlayerMe, Log, TEXT("Response file created with %d files"), FileCount); -} diff --git a/Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h b/Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h index d3445d4..e3ebdd4 100644 --- a/Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h +++ b/Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h @@ -41,13 +41,9 @@ class SCacheGeneratorWidget : public SCompoundWidget CacheUrl = NewText.ToString(); } - // Slider value handling float ItemsPerCategory = 10.0f; void OnItemsPerCategoryChanged(float NewValue); - // Cache URL handling FString CacheUrl; void OnCacheUrlChanged(const FText& NewText); - void CreatePakFile(const FString& PakFilePath, const FString& ResponseFilePath); - void GeneratePakResponseFile(const FString& ResponseFilePath, const FString& FolderToPak); }; From 3d2c7e5677a87ebb67999e303c0567cb9b047910 Mon Sep 17 00:00:00 2001 From: Harrison Date: Thu, 19 Sep 2024 16:50:33 +0300 Subject: [PATCH 43/54] feat: WIP caching fix --- .../Samples/BasicLoader/RpmBasicLoader.umap | Bin 136187 -> 136933 bytes .../Private/Api/Assets/AssetGlbLoader.cpp | 3 +- .../Private/Api/Files/FileUtility.cpp | 3 + .../Private/Cache/CacheGenerator.cpp | 9 +- .../RpmNextGen/Private/RpmFunctionLibrary.cpp | 6 + .../RpmNextGen/Private/RpmLoaderComponent.cpp | 2 +- Source/RpmNextGen/Private/RpmNextGen.cpp | 14 -- .../RpmNextGen/Public/Api/Files/FileUtility.h | 19 ++- .../Public/Api/Files/PakFileUtility.cpp | 150 ++++++++++++++++-- .../Public/Api/Files/PakFileUtility.h | 7 +- .../Public/Cache/AssetCacheManager.h | 20 +-- .../RpmNextGen/Public/Cache/CachedAssetData.h | 19 ++- Source/RpmNextGen/Public/RpmFunctionLibrary.h | 3 + Source/RpmNextGen/Public/RpmNextGen.h | 9 -- Source/RpmNextGen/RpmNextGen.Build.cs | 3 +- .../Private/UI/SCacheGeneratorWidget.cpp | 22 ++- 16 files changed, 217 insertions(+), 72 deletions(-) create mode 100644 Source/RpmNextGen/Private/Api/Files/FileUtility.cpp diff --git a/Content/Samples/BasicLoader/RpmBasicLoader.umap b/Content/Samples/BasicLoader/RpmBasicLoader.umap index d6f1cb3ebc974aa93e92a9ae32b862d548c0cb1f..fb1012375876b200e996321756e625097dac6b04 100644 GIT binary patch literal 136933 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 literal 136187 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 W=wmGD`SkJwh@IB)Y-*$&^I~>CO%OTuOhj5=d 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�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 GlbData; - if(FFileHelper::LoadFileToArray(GlbData, *StoredAsset.GlbPathsByBaseModelId[BaseModelId])) + const FString StoredGlbPath = FFileUtility::GetFullPersistentPath(StoredAsset.GlbPathsByBaseModelId[BaseModelId]); + if(FFileHelper::LoadFileToArray(GlbData, *StoredGlbPath)) { OnGlbLoaded.ExecuteIfBound(Asset, GlbData); UE_LOG(LogReadyPlayerMe, Log, TEXT("Loading GLB from cache")); diff --git a/Source/RpmNextGen/Private/Api/Files/FileUtility.cpp b/Source/RpmNextGen/Private/Api/Files/FileUtility.cpp new file mode 100644 index 0000000..d180e5a --- /dev/null +++ b/Source/RpmNextGen/Private/Api/Files/FileUtility.cpp @@ -0,0 +1,3 @@ +#include "Api/Files/FileUtility.h" + +const FString FFileUtility::RelativeCachePath = TEXT("ReadyPlayerMe/AssetCache"); diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index 85b9bf5..96e770f 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -6,6 +6,7 @@ #include "Api/Assets/AssetIconLoader.h" #include "Api/Assets/Models/AssetListRequest.h" #include "Api/Assets/Models/AssetTypeListRequest.h" +#include "Api/Files/PakFileUtility.h" #include "Cache/AssetCacheManager.h" #include "Interfaces/IHttpRequest.h" #include "Interfaces/IHttpResponse.h" @@ -15,7 +16,7 @@ #include "Misc/ScopeExit.h" #include "Settings/RpmDeveloperSettings.h" -const FString FCacheGenerator::ZipFileName = TEXT("CacheAssets.zip"); +const FString FCacheGenerator::ZipFileName = TEXT("CacheAssets.pak"); FCacheGenerator::FCacheGenerator() : CurrentBaseModelIndex(0), MaxItemsPerCategory(10) { @@ -74,7 +75,7 @@ void FCacheGenerator::LoadAndStoreAssets() } RequiredAssetDownloadRequest = TotalRefittedAssets + AssetIconRequestCount; - const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); + const FString GlobalCachePath = FFileUtility::GetCachePath(); UE_LOG(LogReadyPlayerMe, Log, TEXT("Total assets to download: %d. Total refitted assets glbs to fetch: %d"), RequiredAssetDownloadRequest, TotalRefittedAssets - BaseModelAssets.Num()); for (auto Pair : AssetMapByBaseModelId) @@ -238,7 +239,7 @@ void FCacheGenerator::OnDownloadRemoteCacheComplete(TSharedPtr Req // Get the response data const TArray& Data = Response->GetContent(); // Define the path to save the ZIP file - const FString SavePath = FRpmNextGenModule::GetGlobalAssetCachePath() / TEXT("/") / ZipFileName; + const FString SavePath = FFileUtility::GetCachePath()/ TEXT("/") / ZipFileName; // Ensure the directory exists IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); @@ -267,7 +268,7 @@ void FCacheGenerator::OnDownloadRemoteCacheComplete(TSharedPtr Req void FCacheGenerator::ExtractCache() { - // TODO add implementation + //FPakFileUtility::ExtractFilesFromPak( FRpmNextGenModule::GetGlobalAssetCachePath() / TEXT("/") / ZipFileName); } void FCacheGenerator::FetchBaseModels() const diff --git a/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp b/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp index 18d3e7a..a7d2fbc 100644 --- a/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp +++ b/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp @@ -7,6 +7,7 @@ #include "Api/Assets/Models/AssetListRequest.h" #include "Api/Assets/Models/AssetListResponse.h" #include "Api/Auth/ApiKeyAuthStrategy.h" +#include "Api/Files/PakFileUtility.h" #include "Cache/AssetCacheManager.h" #include "Cache/CachedAssetData.h" #include "Settings/RpmDeveloperSettings.h" @@ -71,3 +72,8 @@ void URpmFunctionLibrary::CheckInternetConnection(const FOnConnectionStatusRefre FConnectionManager::Get().CheckInternetConnection(); } + +void URpmFunctionLibrary::ExtractCachePakFile() +{ + FPakFileUtility::ExtractFilesFromPak(FPakFileUtility::CachePakFilePath); +} diff --git a/Source/RpmNextGen/Private/RpmLoaderComponent.cpp b/Source/RpmNextGen/Private/RpmLoaderComponent.cpp index 5c2d2b2..1272f8e 100644 --- a/Source/RpmNextGen/Private/RpmLoaderComponent.cpp +++ b/Source/RpmNextGen/Private/RpmLoaderComponent.cpp @@ -88,7 +88,7 @@ UglTFRuntimeAsset* URpmLoaderComponent::LoadGltfRuntimeAssetFromCache(const FAss { CharacterData.Assets.Add(ExistingAsset.Type, Asset); TArray Data; - if(FFileApi::LoadFileFromPath(ExistingAsset.GlbPathsByBaseModelId[CharacterData.BaseModelId], Data)) + if(FFileApi::LoadFileFromPath(ExistingAsset.GetGlbPathForBaseModelId(CharacterData.BaseModelId), Data)) { UglTFRuntimeAsset* GltfRuntimeAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(Data, *GltfConfig); return GltfRuntimeAsset; diff --git a/Source/RpmNextGen/Private/RpmNextGen.cpp b/Source/RpmNextGen/Private/RpmNextGen.cpp index 081caa9..f7a9290 100644 --- a/Source/RpmNextGen/Private/RpmNextGen.cpp +++ b/Source/RpmNextGen/Private/RpmNextGen.cpp @@ -6,12 +6,8 @@ DEFINE_LOG_CATEGORY(LogReadyPlayerMe); #define LOCTEXT_NAMESPACE "FRpmNextGenModule" -FString FRpmNextGenModule::AssetCachePath = FString(); - 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 - InitializeGlobalPaths(); } void FRpmNextGenModule::ShutdownModule() @@ -20,16 +16,6 @@ void FRpmNextGenModule::ShutdownModule() // we call this function before unloading the module. } -// ReSharper disable once CppMemberFunctionMayBeConst -void FRpmNextGenModule::InitializeGlobalPaths() -{ - const FString RelativePath = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache"); - - AssetCachePath = FPaths::ConvertRelativePathToFull(RelativePath); - - UE_LOG(LogReadyPlayerMe, Log, TEXT("Initialized Asset Cache Path: %s"), *AssetCachePath); -} - #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FRpmNextGenModule, RpmNextGen) \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Api/Files/FileUtility.h b/Source/RpmNextGen/Public/Api/Files/FileUtility.h index edb5e9b..cfc35d0 100644 --- a/Source/RpmNextGen/Public/Api/Files/FileUtility.h +++ b/Source/RpmNextGen/Public/Api/Files/FileUtility.h @@ -1,12 +1,15 @@ #pragma once #include "CoreMinimal.h" - +#include "Misc/FileHelper.h" +#include "RpmNextGen.h" class RPMNEXTGEN_API FFileUtility { public: + static const FString RelativeCachePath; + static bool SaveToFile(const TArray& Data, const FString& FilePath, const bool bSkipIfFileExists = true) { if (bSkipIfFileExists && FPaths::FileExists(FilePath)) @@ -23,4 +26,16 @@ class RPMNEXTGEN_API FFileUtility UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to save asset to: %s"), *FilePath); return false; } -}; \ No newline at end of file + + static FString GetFullPersistentPath(const FString& RelativePath) + { + const FString PersistentPath = FPaths::ProjectPersistentDownloadDir() / RelativePath; + + return FPaths::ConvertRelativePathToFull(PersistentPath); + } + + static FString GetCachePath() + { + return GetFullPersistentPath(RelativeCachePath); + } +}; diff --git a/Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp b/Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp index e21995b..0b5721c 100644 --- a/Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp +++ b/Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp @@ -1,13 +1,19 @@ #include "PakFileUtility.h" +#include "FileUtility.h" #include "IPlatformFilePak.h" -#include "RpmNextGen.h" -const FString ResponseFilePath = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/RpmCache_ResponseFile.txt"); +const FString ResponseFilePath = FPaths::ProjectContentDir() / TEXT("Paks/RpmCache_ResponseFile.txt"); + +#if WITH_EDITOR +const FString FPakFileUtility::CachePakFilePath = FPaths::ProjectContentDir() / TEXT("Paks/RpmAssetCache.pak"); +#else +const FString FPakFileUtility::CachePakFilePath = FPaths::ProjectDir() / TEXT("Content/Paks/RpmAssetCache.pak"); +#endif void FPakFileUtility::CreatePakFile(const FString& PakFilePath) { const FString UnrealPakPath = FPaths::ConvertRelativePathToFull(FPaths::EngineDir() / TEXT("Binaries/Win64/UnrealPak.exe")); - const FString CommandLineArgs = FString::Printf(TEXT("%s -Create=%s"), *PakFilePath, *ResponseFilePath); + const FString CommandLineArgs = FString::Printf(TEXT("%s -Create=%s -IoStore"), *PakFilePath, *ResponseFilePath); // Add the -IoStore flag FProcHandle ProcHandle = FPlatformProcess::CreateProc(*UnrealPakPath, *CommandLineArgs, true, false, false, nullptr, 0, nullptr, nullptr); if (ProcHandle.IsValid()) @@ -15,17 +21,18 @@ void FPakFileUtility::CreatePakFile(const FString& PakFilePath) FPlatformProcess::WaitForProc(ProcHandle); FPlatformProcess::CloseProc(ProcHandle); - UE_LOG(LogTemp, Log, TEXT("Pak file created successfully: %s"), *PakFilePath); + UE_LOG(LogTemp, Log, TEXT("Pak file and I/O Store files created successfully: %s"), *PakFilePath); } else { - UE_LOG(LogTemp, Error, TEXT("Failed to create Pak file: %s"), *PakFilePath); + UE_LOG(LogTemp, Error, TEXT("Failed to create Pak and I/O Store files: %s"), *PakFilePath); } } -void FPakFileUtility::GeneratePakResponseFile(const FString& FolderToPak) +void FPakFileUtility::GeneratePakResponseFile() { TArray Files; + const FString FolderToPak = FFileUtility::GetCachePath(); IFileManager::Get().FindFilesRecursive(Files, *FolderToPak, TEXT("*.*"), true, false); FString ResponseFileContent; @@ -33,19 +40,48 @@ void FPakFileUtility::GeneratePakResponseFile(const FString& FolderToPak) for (const FString& File : Files) { FString RelativePath = File; - FPaths::MakePathRelativeTo(RelativePath, *FolderToPak); + FPaths::MakePathRelativeTo(RelativePath, *FolderToPak); // Make relative to the root of the folder being packed + + + // Ensure the relative path preserves the subfolder structure ResponseFileContent += FString::Printf(TEXT("\"%s\" \"%s\"\n"), *File, *RelativePath); FileCount++; } FFileHelper::SaveStringToFile(ResponseFileContent, *ResponseFilePath); - UE_LOG(LogTemp, Log, TEXT("Response file created with %d files"), FileCount); +} + +void FPakFileUtility::CreatePakFile() +{ + GeneratePakResponseFile(); + CreatePakFile(CachePakFilePath); + IFileManager& FileManager = IFileManager::Get(); + const FString FolderToPak = FFileUtility::GetCachePath(); + // Ensure the folder exists before attempting to delete it + if (FileManager.DirectoryExists(*FolderToPak)) + { + // Attempt to delete the directory and all its contents + bool bDeleted = FileManager.DeleteDirectory(*FolderToPak, false, true); + + if (bDeleted) + { + UE_LOG(LogReadyPlayerMe, Log, TEXT("Successfully deleted folder: %s"), *FolderToPak); + } + else + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to delete folder: %s"), *FolderToPak); + } + } + else + { + UE_LOG(LogReadyPlayerMe, Warning, TEXT("Folder does not exist: %s"), *FolderToPak); + } } void FPakFileUtility::ExtractPakFile(const FString& PakFilePath) { const FString UnrealPakPath = FPaths::ConvertRelativePathToFull(FPaths::EngineDir() / TEXT("Binaries/Win64/UnrealPak.exe")); - const FString DestinationPath = FRpmNextGenModule::GetGlobalAssetCachePath(); + const FString DestinationPath = FFileUtility::GetFullPersistentPath(""); const FString CommandLineArgs = FString::Printf(TEXT("%s -Extract %s"), *PakFilePath, *DestinationPath); FProcHandle ProcHandle = FPlatformProcess::CreateProc(*UnrealPakPath, *CommandLineArgs, true, false, false, nullptr, 0, nullptr, nullptr); @@ -61,4 +97,98 @@ void FPakFileUtility::ExtractPakFile(const FString& PakFilePath) { UE_LOG(LogTemp, Error, TEXT("Failed to extract Pak file: %s"), *PakFilePath); } -} \ No newline at end of file +} + +void FPakFileUtility::ExtractFilesFromPak(const FString& PakFilePath) +{ + IPlatformFile& InnerPlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + FPakPlatformFile* PakPlatformFile = new FPakPlatformFile(); + FPlatformFileManager::Get().SetPlatformFile(*PakPlatformFile); + PakPlatformFile->Initialize(&InnerPlatformFile, TEXT("")); + + const FString MountPoint = TEXT("/CachePak/"); // Mount in a virtual folder + const FString DestinationPath = FFileUtility::GetFullPersistentPath(FFileUtility::RelativeCachePath); + + if (PakPlatformFile->Mount(*PakFilePath, 0, *MountPoint)) + { + UE_LOG(LogTemp, Log, TEXT("Successfully mounted Pak file: %s"), *PakFilePath); + + // Find and extract files from the mounted pak file + TArray Files; + PakPlatformFile->FindFilesRecursively(Files, *MountPoint, TEXT("")); // Using virtual mount point + + for (const FString& File : Files) + { + // Use relative path within the mounted folder, preserving subdirectories + FString RelativeFilePath = File; + FPaths::MakePathRelativeTo(RelativeFilePath, *MountPoint); + FString DestinationFilePath = DestinationPath / RelativeFilePath; + + if (File.EndsWith(TEXT(".json"))) + { + UE_LOG(LogTemp, Log, TEXT("json File = %s"), *File); + // Handle JSON files + FString JsonContent; + if (FFileHelper::LoadFileToString(JsonContent, *File)) + { + UE_LOG(LogTemp, Log, TEXT("Successfully read JSON file: %s"), *File); + // Ensure destination folder exists + IFileManager::Get().MakeDirectory(*FPaths::GetPath(DestinationFilePath), true); + + TSharedPtr JsonObject; + TSharedRef> Reader = TJsonReaderFactory<>::Create(JsonContent); + + // Try to deserialize the JSON string into a JsonObject + if (FJsonSerializer::Deserialize(Reader, JsonObject)) + { + UE_LOG(LogTemp, Log, TEXT("Json content valid")); + } + else + { + UE_LOG(LogTemp, Log, TEXT("Json content invalid")); + } + + // Attempt to save the JSON content + if (FFileHelper::SaveStringToFile(JsonContent, *DestinationFilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + { + UE_LOG(LogTemp, Log, TEXT("Successfully saved JSON file: %s"), *DestinationFilePath); + } + else + { + UE_LOG(LogTemp, Error, TEXT("Failed to save JSON file: %s"), *DestinationFilePath); + } + } + else + { + UE_LOG(LogTemp, Error, TEXT("Failed to read JSON file from Pak: %s"), *File); + } + } + else + { + // Handle other file types as binary + TArray FileData; + if (FFileHelper::LoadFileToArray(FileData, *File)) + { + if (FFileHelper::SaveArrayToFile(FileData, *DestinationFilePath)) + { + UE_LOG(LogTemp, Log, TEXT("Successfully extracted file: %s"), *DestinationFilePath); + } + else + { + UE_LOG(LogTemp, Error, TEXT("Failed to save file: %s"), *DestinationFilePath); + } + } + else + { + UE_LOG(LogTemp, Error, TEXT("Failed to read file from Pak: %s"), *File); + } + } + } + + UE_LOG(LogTemp, Warning, TEXT("Finished extracting files %d from Pak: %s"), Files.Num(), *PakFilePath); + } + else + { + UE_LOG(LogTemp, Error, TEXT("Failed to mount Pak file: %s"), *PakFilePath); + } +} diff --git a/Source/RpmNextGen/Public/Api/Files/PakFileUtility.h b/Source/RpmNextGen/Public/Api/Files/PakFileUtility.h index 1e9caf2..c2c3f39 100644 --- a/Source/RpmNextGen/Public/Api/Files/PakFileUtility.h +++ b/Source/RpmNextGen/Public/Api/Files/PakFileUtility.h @@ -5,8 +5,11 @@ class RPMNEXTGEN_API FPakFileUtility { public: - static void CreatePakFile(const FString& PakFilePath); - static void GeneratePakResponseFile(const FString& FolderToPak); + static void CreatePakFile(); static void ExtractPakFile(const FString& PakFilePath); static void ExtractFilesFromPak(const FString& PakFilePath); + static const FString CachePakFilePath; +private: + static void CreatePakFile(const FString& PakFilePath); + static void GeneratePakResponseFile(); }; \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Cache/AssetCacheManager.h b/Source/RpmNextGen/Public/Cache/AssetCacheManager.h index 0e65720..a70bfae 100644 --- a/Source/RpmNextGen/Public/Cache/AssetCacheManager.h +++ b/Source/RpmNextGen/Public/Cache/AssetCacheManager.h @@ -15,8 +15,7 @@ class FAssetCacheManager static void StoreAssetTypes(const TArray& TypeList) { - const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); - const FString TypeListFilePath = GlobalCachePath / TEXT("TypeList.json"); + const FString TypeListFilePath = FFileUtility::GetCachePath() / TEXT("TypeList.json"); TArray> JsonValues; for (const FString& Type : TypeList) @@ -29,12 +28,12 @@ class FAssetCacheManager FJsonSerializer::Serialize(JsonValues, Writer); - FFileHelper::SaveStringToFile(OutputString, *TypeListFilePath); + FFileHelper::SaveStringToFile(OutputString, *TypeListFilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM); } static TArray LoadAssetTypes() { - const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); + const FString GlobalCachePath = FFileUtility::GetCachePath(); const FString TypeListFilePath = GlobalCachePath / TEXT("TypeList.json"); FString TypeListContent; @@ -73,7 +72,7 @@ class FAssetCacheManager void StoreAndTrackIcon(const FAssetLoadingContext& Context, const bool bSaveManifest = true) { const FCachedAssetData& StoredAsset = FCachedAssetData(Context.Asset); - FFileUtility::SaveToFile(Context.Data, StoredAsset.IconFilePath); + FFileUtility::SaveToFile(Context.Data, FFileUtility::GetFullPersistentPath(StoredAsset.IconFilePath)); StoreAndTrackAsset(StoredAsset, bSaveManifest); } @@ -81,7 +80,8 @@ class FAssetCacheManager void StoreAndTrackGlb(const FAssetLoadingContext& Context, const bool bSaveManifest = true) { const FCachedAssetData& StoredAsset = FCachedAssetData(Context.Asset, Context.BaseModelId); - FFileUtility::SaveToFile(Context.Data, StoredAsset.GlbPathsByBaseModelId[Context.BaseModelId]); + const FString& GlbPath = FFileUtility::GetFullPersistentPath(StoredAsset.GlbPathsByBaseModelId[Context.BaseModelId]); + FFileUtility::SaveToFile(Context.Data, GlbPath); StoreAndTrackAsset(StoredAsset, bSaveManifest); } @@ -121,7 +121,7 @@ class FAssetCacheManager void LoadManifest() { - const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); + const FString GlobalCachePath = FFileUtility::GetCachePath(); const FString ManifestFilePath = GlobalCachePath / TEXT("AssetManifest.json"); FString ManifestContent; @@ -149,7 +149,7 @@ class FAssetCacheManager void SaveManifest() { - const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); + const FString GlobalCachePath = FFileUtility::GetCachePath(); const FString ManifestFilePath = GlobalCachePath / TEXT("AssetManifest.json"); TSharedPtr ManifestJson = MakeShared(); @@ -167,12 +167,12 @@ class FAssetCacheManager TSharedRef> Writer = TJsonWriterFactory<>::Create(&OutputString); FJsonSerializer::Serialize(ManifestJson.ToSharedRef(), Writer); - FFileHelper::SaveStringToFile(OutputString, *ManifestFilePath); + FFileHelper::SaveStringToFile(OutputString, *ManifestFilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM); } void ClearAllCache() { - const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); + const FString GlobalCachePath = FFileUtility::GetCachePath(); IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); if (PlatformFile.DirectoryExists(*GlobalCachePath)) diff --git a/Source/RpmNextGen/Public/Cache/CachedAssetData.h b/Source/RpmNextGen/Public/Cache/CachedAssetData.h index 38da28c..0b22850 100644 --- a/Source/RpmNextGen/Public/Cache/CachedAssetData.h +++ b/Source/RpmNextGen/Public/Cache/CachedAssetData.h @@ -3,6 +3,7 @@ #include "CoreMinimal.h" #include "RpmNextGen.h" #include "Api/Assets/Models/Asset.h" +#include "Api/Files/FileUtility.h" #include "CachedAssetData.generated.h" USTRUCT(BlueprintType) @@ -51,13 +52,12 @@ struct RPMNEXTGEN_API FCachedAssetData } FCachedAssetData(const FAsset& InAsset) { - const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); Id = InAsset.Id; Name = InAsset.Name; GlbUrl = InAsset.GlbUrl; IconUrl = InAsset.IconUrl; GlbPathsByBaseModelId = TMap(); - IconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *GlobalCachePath, *Id); + IconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *FFileUtility::RelativeCachePath, *Id); Type = InAsset.Type; CreatedAt = InAsset.CreatedAt; UpdatedAt = InAsset.UpdatedAt; @@ -65,7 +65,6 @@ struct RPMNEXTGEN_API FCachedAssetData FCachedAssetData(const FAsset& InAsset, const FString& InBaseModelId) { - const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); Id = InAsset.Id; Name = InAsset.Name; GlbUrl = InAsset.GlbUrl; @@ -73,9 +72,9 @@ struct RPMNEXTGEN_API FCachedAssetData GlbPathsByBaseModelId = TMap(); if(InBaseModelId != FString()) { - GlbPathsByBaseModelId.Add(InBaseModelId, FString::Printf(TEXT("%s/%s/%s.glb"), *GlobalCachePath, *InBaseModelId, *Id)); + GlbPathsByBaseModelId.Add(InBaseModelId, FString::Printf(TEXT("%s/%s/%s.glb"), *FFileUtility::RelativeCachePath, *InBaseModelId, *Id)); } - IconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *GlobalCachePath, *Id); + IconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *FFileUtility::RelativeCachePath, *Id); Type = InAsset.Type; CreatedAt = InAsset.CreatedAt; UpdatedAt = InAsset.UpdatedAt; @@ -152,4 +151,14 @@ struct RPMNEXTGEN_API FCachedAssetData return StoredAsset; } + + FString GetGlbPathForBaseModelId(FString BaseModelId) + { + if(GlbPathsByBaseModelId.Num() > 0 && !BaseModelId.IsEmpty()) + { + return GlbPathsByBaseModelId[BaseModelId]; + } + + return ""; + } }; diff --git a/Source/RpmNextGen/Public/RpmFunctionLibrary.h b/Source/RpmNextGen/Public/RpmFunctionLibrary.h index 8cbf47c..ecda8ed 100644 --- a/Source/RpmNextGen/Public/RpmFunctionLibrary.h +++ b/Source/RpmNextGen/Public/RpmFunctionLibrary.h @@ -27,4 +27,7 @@ class RPMNEXTGEN_API URpmFunctionLibrary : public UBlueprintFunctionLibrary UFUNCTION(BlueprintCallable, Category = "ReadyPlayerMe/Network") static void CheckInternetConnection(const FOnConnectionStatusRefreshedDelegate& OnConnectionStatusRefreshed); + + UFUNCTION(BlueprintCallable, Category = "ReadyPlayerMe/Cache") + static void ExtractCachePakFile(); }; diff --git a/Source/RpmNextGen/Public/RpmNextGen.h b/Source/RpmNextGen/Public/RpmNextGen.h index 18749e6..6be9f9e 100644 --- a/Source/RpmNextGen/Public/RpmNextGen.h +++ b/Source/RpmNextGen/Public/RpmNextGen.h @@ -14,13 +14,4 @@ class RPMNEXTGEN_API FRpmNextGenModule : public IModuleInterface /** IModuleInterface implementation */ virtual void StartupModule() override; virtual void ShutdownModule() override; - - static const FString& GetGlobalAssetCachePath() - { - return AssetCachePath; - } - -private: - void InitializeGlobalPaths(); - static FString AssetCachePath; }; diff --git a/Source/RpmNextGen/RpmNextGen.Build.cs b/Source/RpmNextGen/RpmNextGen.Build.cs index 94fafb2..a0f48fe 100644 --- a/Source/RpmNextGen/RpmNextGen.Build.cs +++ b/Source/RpmNextGen/RpmNextGen.Build.cs @@ -30,7 +30,8 @@ public RpmNextGen(ReadOnlyTargetRules Target) : base(Target) "DeveloperSettings", "Slate", "SlateCore", - "PakFile" + "PakFile", + "StreamingFile" } ); diff --git a/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp index 904df10..377de16 100644 --- a/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp @@ -4,6 +4,7 @@ #include "EditorStyleSet.h" #include "IPlatformFilePak.h" #include "RpmNextGen.h" +#include "Api/Files/FileUtility.h" #include "Api/Files/PakFileUtility.h" #include "Cache/CacheGenerator.h" #include "Misc/FileHelper.h" @@ -170,25 +171,23 @@ FReply SCacheGeneratorWidget::OnGenerateOfflineCacheClicked() FReply SCacheGeneratorWidget::OnExtractCacheClicked() { - FString PakFilePath = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/RpmAssetCache.pak"); - FPakFileUtility::ExtractPakFile(PakFilePath); - + FString PakFilePath = FPaths::ConvertRelativePathToFull(FPakFileUtility::CachePakFilePath); + FPakFileUtility::ExtractFilesFromPak(PakFilePath); return FReply::Handled(); } FReply SCacheGeneratorWidget::OnOpenLocalCacheFolderClicked() { - const FString GlobalCachePath = FRpmNextGenModule::GetGlobalAssetCachePath(); - + const FString CacheRoot = FFileUtility::GetFullPersistentPath(FFileUtility::RelativeCachePath); // Check if the folder exists - if (FPaths::DirectoryExists(GlobalCachePath)) + if (FPaths::DirectoryExists(CacheRoot)) { // Open the folder in the file explorer - FPlatformProcess::LaunchFileInDefaultExternalApplication(*GlobalCachePath); + FPlatformProcess::LaunchFileInDefaultExternalApplication(*CacheRoot); } else { - UE_LOG(LogReadyPlayerMe, Warning, TEXT("Folder does not exist: %s"), *GlobalCachePath); + UE_LOG(LogReadyPlayerMe, Warning, TEXT("Folder does not exist: %s"), *CacheRoot); } return FReply::Handled(); @@ -215,13 +214,10 @@ void SCacheGeneratorWidget::OnGenerateLocalCacheCompleted(bool bWasSuccessful) { UE_LOG(LogReadyPlayerMe, Log, TEXT("Completed generating cache")); UE_LOG(LogReadyPlayerMe, Log, TEXT("Local cache generated successfully")); - FString FolderToPak = FPaths::ProjectPersistentDownloadDir() / TEXT("ReadyPlayerMe/AssetCache"); - FString PakFilePath = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/RpmAssetCache.pak"); - - FPakFileUtility::GeneratePakResponseFile(FolderToPak); - FPakFileUtility::CreatePakFile(PakFilePath); + FPakFileUtility::CreatePakFile(); } + void SCacheGeneratorWidget::OnItemsPerCategoryChanged(float NewValue) { ItemsPerCategory = NewValue; From c22ca18b0e09c76ec3c75939d5caf9ef0fc67287 Mon Sep 17 00:00:00 2001 From: Harrison Date: Mon, 23 Sep 2024 10:18:14 +0300 Subject: [PATCH 44/54] chore: fixed runtime pak extraction and asset loading --- .../Private/Api/Assets/AssetIconLoader.cpp | 2 +- .../RpmNextGen/Private/RpmFunctionLibrary.cpp | 4 +- .../Private/Utilities/RpmImageHelper.cpp | 9 + .../Public/Api/Files/PakFileUtility.cpp | 201 ++++++++++++------ .../RpmNextGen/Public/Cache/CachedAssetData.h | 8 +- 5 files changed, 148 insertions(+), 76 deletions(-) diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetIconLoader.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetIconLoader.cpp index 6114b95..3d21a60 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetIconLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetIconLoader.cpp @@ -19,7 +19,7 @@ void FAssetIconLoader::LoadIcon(const FAsset& Asset, bool bStoreInCache) if (FAssetCacheManager::Get().GetCachedAsset(Asset.Id, StoredAsset)) { TArray IconData; - if(FFileHelper::LoadFileToArray(IconData, *StoredAsset.IconFilePath)) + if(FFileHelper::LoadFileToArray(IconData, * FFileUtility::GetFullPersistentPath(StoredAsset.IconFilePath))) { OnIconLoaded.ExecuteIfBound(Asset, IconData); UE_LOG(LogReadyPlayerMe, Log, TEXT("Loading icon from cache")); diff --git a/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp b/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp index a7d2fbc..798be50 100644 --- a/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp +++ b/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp @@ -75,5 +75,7 @@ void URpmFunctionLibrary::CheckInternetConnection(const FOnConnectionStatusRefre void URpmFunctionLibrary::ExtractCachePakFile() { - FPakFileUtility::ExtractFilesFromPak(FPakFileUtility::CachePakFilePath); + FString PakFilePath = FFileUtility::GetFullPersistentPath(FPakFileUtility::CachePakFilePath); + + FPakFileUtility::ExtractFilesFromPak(PakFilePath); } diff --git a/Source/RpmNextGen/Private/Utilities/RpmImageHelper.cpp b/Source/RpmNextGen/Private/Utilities/RpmImageHelper.cpp index dcb16da..cf9d59f 100644 --- a/Source/RpmNextGen/Private/Utilities/RpmImageHelper.cpp +++ b/Source/RpmNextGen/Private/Utilities/RpmImageHelper.cpp @@ -28,20 +28,29 @@ UTexture2D* FRpmImageHelper::CreateTextureFromData(const TArray& ImageDat return nullptr; } + // Create the texture UTexture2D* Texture = UTexture2D::CreateTransient(ImageWrapper->GetWidth(), ImageWrapper->GetHeight(), PF_B8G8R8A8); if (!Texture) { return nullptr; } + // Disable mipmaps and streaming for icons + Texture->NeverStream = true; + Texture->MipGenSettings = TMGS_NoMipmaps; + + // Lock the texture and copy data void* TextureData = Texture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE); FMemory::Memcpy(TextureData, UncompressedBGRA.GetData(), UncompressedBGRA.Num()); Texture->GetPlatformData()->Mips[0].BulkData.Unlock(); + + // Update texture resource Texture->UpdateResource(); return Texture; } + UImage* FRpmImageHelper::CreateUImageFromData(const TArray& ImageData, const FVector2D& ImageSize) { UImage* Image = NewObject(); diff --git a/Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp b/Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp index 0b5721c..cccda38 100644 --- a/Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp +++ b/Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp @@ -2,12 +2,12 @@ #include "FileUtility.h" #include "IPlatformFilePak.h" -const FString ResponseFilePath = FPaths::ProjectContentDir() / TEXT("Paks/RpmCache_ResponseFile.txt"); +const FString ResponseFilePath = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/Cache/RpmCache_ResponseFile.txt"); #if WITH_EDITOR -const FString FPakFileUtility::CachePakFilePath = FPaths::ProjectContentDir() / TEXT("Paks/RpmAssetCache.pak"); +const FString FPakFileUtility::CachePakFilePath = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/Cache/RpmAssetCache.pak"); #else -const FString FPakFileUtility::CachePakFilePath = FPaths::ProjectDir() / TEXT("Content/Paks/RpmAssetCache.pak"); +const FString FPakFileUtility::CachePakFilePath = FPaths::ProjectDir() / TEXT("Content/ReadyPlayerMe/Cache/RpmAssetCache.pak"); #endif void FPakFileUtility::CreatePakFile(const FString& PakFilePath) @@ -81,7 +81,7 @@ void FPakFileUtility::CreatePakFile() void FPakFileUtility::ExtractPakFile(const FString& PakFilePath) { const FString UnrealPakPath = FPaths::ConvertRelativePathToFull(FPaths::EngineDir() / TEXT("Binaries/Win64/UnrealPak.exe")); - const FString DestinationPath = FFileUtility::GetFullPersistentPath(""); + const FString DestinationPath = FFileUtility::GetFullPersistentPath(FFileUtility::RelativeCachePath); const FString CommandLineArgs = FString::Printf(TEXT("%s -Extract %s"), *PakFilePath, *DestinationPath); FProcHandle ProcHandle = FPlatformProcess::CreateProc(*UnrealPakPath, *CommandLineArgs, true, false, false, nullptr, 0, nullptr, nullptr); @@ -99,96 +99,157 @@ void FPakFileUtility::ExtractPakFile(const FString& PakFilePath) } } +// void FPakFileUtility::ExtractFilesFromPak(const FString& PakFilePath) +// { +// bool bSuccessfulInitialization = false; +// IPlatformFile* LocalPlatformFile = &FPlatformFileManager::Get().GetPlatformFile(); +// if (LocalPlatformFile != nullptr) +// { +// IPlatformFile* PakPlatformFile = FPlatformFileManager::Get().GetPlatformFile(TEXT("PakFile")); +// if (PakPlatformFile != nullptr) +// { +// UE_LOG(LogTemp, Error, TEXT("Successfully initialized PakPlatformFile")); +// bSuccessfulInitialization = true; +// } +// +// } +// if(bSuccessfulInitialization) +// { +// const TCHAR* cmdLine = TEXT(""); +// FPakPlatformFile* PakPlatform = new FPakPlatformFile(); +// IPlatformFile* InnerPlatform = LocalPlatformFile; +// PakPlatform->Initialize(InnerPlatform, cmdLine); +// FPlatformFileManager::Get().SetPlatformFile(*PakPlatform); +// +// FString FilePath = PakFilePath; +// +// UE_LOG(LogTemp, Log, TEXT("Attempting to load pak: %s"), *FilePath); +// +// FPakFile PakFile = FPakFile(InnerPlatform, *FilePath, false); +// +// if (!PakFile.IsValid()) +// { +// UE_LOG(LogTemp, Error, TEXT("Invalid pak file: %s"), *FilePath); +// return ; +// } +// UE_LOG(LogTemp, Log, TEXT("Pak file is VALID! %s"), *FilePath); +// //FCoreDelegates::MountPak.IsBound(); +// } +// } + void FPakFileUtility::ExtractFilesFromPak(const FString& PakFilePath) { + // Step 1: Get the current platform file and initialize the Pak platform IPlatformFile& InnerPlatformFile = FPlatformFileManager::Get().GetPlatformFile(); - FPakPlatformFile* PakPlatformFile = new FPakPlatformFile(); - FPlatformFileManager::Get().SetPlatformFile(*PakPlatformFile); - PakPlatformFile->Initialize(&InnerPlatformFile, TEXT("")); - - const FString MountPoint = TEXT("/CachePak/"); // Mount in a virtual folder + FPakPlatformFile* PakPlatformFile = static_cast(FPlatformFileManager::Get().FindPlatformFile(TEXT("PakFile"))); + + // If the PakPlatformFile is null, initialize it + if (!PakPlatformFile) + { + PakPlatformFile = new FPakPlatformFile(); + FPlatformFileManager::Get().SetPlatformFile(*PakPlatformFile); + if (!PakPlatformFile->Initialize(&InnerPlatformFile, TEXT(""))) + { + UE_LOG(LogTemp, Error, TEXT("Failed to initialize Pak Platform File")); + delete PakPlatformFile; + return; + } + UE_LOG(LogTemp, Log, TEXT("Initializing new Pak Platform File")); + } + + // Step 2: Define mount point and destination paths + const FString MountPoint = TEXT("/AssetCache/"); // Virtual mount point inside Unreal const FString DestinationPath = FFileUtility::GetFullPersistentPath(FFileUtility::RelativeCachePath); - if (PakPlatformFile->Mount(*PakFilePath, 0, *MountPoint)) + // Step 3: Mount the Pak file + if (!PakPlatformFile->Mount(*PakFilePath, 0, *MountPoint)) + { + UE_LOG(LogTemp, Error, TEXT("Failed to mount Pak file: %s"), *PakFilePath); + //return; + } + + UE_LOG(LogTemp, Log, TEXT("Successfully mounted Pak file: %s"), *PakFilePath); + + // Step 4: List files in the mounted Pak file + TArray Files; + PakPlatformFile->FindFilesRecursively(Files, *MountPoint, TEXT("")); + + if (Files.Num() == 0) { - UE_LOG(LogTemp, Log, TEXT("Successfully mounted Pak file: %s"), *PakFilePath); + UE_LOG(LogTemp, Warning, TEXT("No files found in Pak file: %s"), *PakFilePath); + return; + } - // Find and extract files from the mounted pak file - TArray Files; - PakPlatformFile->FindFilesRecursively(Files, *MountPoint, TEXT("")); // Using virtual mount point + UE_LOG(LogTemp, Log, TEXT("Found %d files in Pak file: %s"), Files.Num(), *PakFilePath); - for (const FString& File : Files) - { - // Use relative path within the mounted folder, preserving subdirectories - FString RelativeFilePath = File; - FPaths::MakePathRelativeTo(RelativeFilePath, *MountPoint); - FString DestinationFilePath = DestinationPath / RelativeFilePath; + // Step 5: Extract each file from the Pak + for (const FString& File : Files) + { + // Ensure the file is relative to the mount point + FString RelativeFilePath = File; + FPaths::MakePathRelativeTo(RelativeFilePath, *MountPoint); + FString DestinationFilePath = DestinationPath / RelativeFilePath; - if (File.EndsWith(TEXT(".json"))) + // Step 6: Handle JSON files separately + if (File.EndsWith(TEXT(".json"))) + { + UE_LOG(LogTemp, Log, TEXT("Processing JSON file: %s"), *File); + FString JsonContent; + if (FFileHelper::LoadFileToString(JsonContent, *File)) { - UE_LOG(LogTemp, Log, TEXT("json File = %s"), *File); - // Handle JSON files - FString JsonContent; - if (FFileHelper::LoadFileToString(JsonContent, *File)) + // Ensure destination directory exists + IFileManager::Get().MakeDirectory(*FPaths::GetPath(DestinationFilePath), true); + + // Try to deserialize JSON + TSharedPtr JsonObject; + TSharedRef> Reader = TJsonReaderFactory<>::Create(JsonContent); + + if (FJsonSerializer::Deserialize(Reader, JsonObject)) { - UE_LOG(LogTemp, Log, TEXT("Successfully read JSON file: %s"), *File); - // Ensure destination folder exists - IFileManager::Get().MakeDirectory(*FPaths::GetPath(DestinationFilePath), true); - - TSharedPtr JsonObject; - TSharedRef> Reader = TJsonReaderFactory<>::Create(JsonContent); - - // Try to deserialize the JSON string into a JsonObject - if (FJsonSerializer::Deserialize(Reader, JsonObject)) - { - UE_LOG(LogTemp, Log, TEXT("Json content valid")); - } - else - { - UE_LOG(LogTemp, Log, TEXT("Json content invalid")); - } - - // Attempt to save the JSON content - if (FFileHelper::SaveStringToFile(JsonContent, *DestinationFilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) - { - UE_LOG(LogTemp, Log, TEXT("Successfully saved JSON file: %s"), *DestinationFilePath); - } - else - { - UE_LOG(LogTemp, Error, TEXT("Failed to save JSON file: %s"), *DestinationFilePath); - } + UE_LOG(LogTemp, Log, TEXT("Successfully parsed JSON content")); } else { - UE_LOG(LogTemp, Error, TEXT("Failed to read JSON file from Pak: %s"), *File); + UE_LOG(LogTemp, Warning, TEXT("Invalid JSON content in file: %s"), *File); + } + + // Save JSON file to disk + if (FFileHelper::SaveStringToFile(JsonContent, *DestinationFilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + { + UE_LOG(LogTemp, Log, TEXT("Successfully saved JSON file: %s"), *DestinationFilePath); + } + else + { + UE_LOG(LogTemp, Error, TEXT("Failed to save JSON file: %s"), *DestinationFilePath); } } else { - // Handle other file types as binary - TArray FileData; - if (FFileHelper::LoadFileToArray(FileData, *File)) + UE_LOG(LogTemp, Error, TEXT("Failed to read JSON file from Pak: %s"), *File); + } + } + else + { + // Handle binary files + TArray FileData; + if (FFileHelper::LoadFileToArray(FileData, *File)) + { + if (FFileHelper::SaveArrayToFile(FileData, *DestinationFilePath)) { - if (FFileHelper::SaveArrayToFile(FileData, *DestinationFilePath)) - { - UE_LOG(LogTemp, Log, TEXT("Successfully extracted file: %s"), *DestinationFilePath); - } - else - { - UE_LOG(LogTemp, Error, TEXT("Failed to save file: %s"), *DestinationFilePath); - } + UE_LOG(LogTemp, Log, TEXT("Successfully extracted file: %s"), *DestinationFilePath); } else { - UE_LOG(LogTemp, Error, TEXT("Failed to read file from Pak: %s"), *File); + UE_LOG(LogTemp, Error, TEXT("Failed to save file: %s"), *DestinationFilePath); } } + else + { + UE_LOG(LogTemp, Error, TEXT("Failed to read file from Pak: %s"), *File); + } } - - UE_LOG(LogTemp, Warning, TEXT("Finished extracting files %d from Pak: %s"), Files.Num(), *PakFilePath); - } - else - { - UE_LOG(LogTemp, Error, TEXT("Failed to mount Pak file: %s"), *PakFilePath); } + + UE_LOG(LogTemp, Log, TEXT("Finished extracting files from Pak: %s"), *PakFilePath); } + diff --git a/Source/RpmNextGen/Public/Cache/CachedAssetData.h b/Source/RpmNextGen/Public/Cache/CachedAssetData.h index 0b22850..68c2ed2 100644 --- a/Source/RpmNextGen/Public/Cache/CachedAssetData.h +++ b/Source/RpmNextGen/Public/Cache/CachedAssetData.h @@ -1,7 +1,6 @@ #pragma once #include "CoreMinimal.h" -#include "RpmNextGen.h" #include "Api/Assets/Models/Asset.h" #include "Api/Files/FileUtility.h" #include "CachedAssetData.generated.h" @@ -57,7 +56,7 @@ struct RPMNEXTGEN_API FCachedAssetData GlbUrl = InAsset.GlbUrl; IconUrl = InAsset.IconUrl; GlbPathsByBaseModelId = TMap(); - IconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *FFileUtility::RelativeCachePath, *Id); + IconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *FFileUtility::GetFullPersistentPath(FFileUtility::RelativeCachePath), *Id); Type = InAsset.Type; CreatedAt = InAsset.CreatedAt; UpdatedAt = InAsset.UpdatedAt; @@ -70,11 +69,12 @@ struct RPMNEXTGEN_API FCachedAssetData GlbUrl = InAsset.GlbUrl; IconUrl = InAsset.IconUrl; GlbPathsByBaseModelId = TMap(); + const FString FullPath = FFileUtility::GetFullPersistentPath(FFileUtility::RelativeCachePath); if(InBaseModelId != FString()) { - GlbPathsByBaseModelId.Add(InBaseModelId, FString::Printf(TEXT("%s/%s/%s.glb"), *FFileUtility::RelativeCachePath, *InBaseModelId, *Id)); + GlbPathsByBaseModelId.Add(InBaseModelId, FString::Printf(TEXT("%s/%s/%s.glb"), *FullPath, *InBaseModelId, *Id)); } - IconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *FFileUtility::RelativeCachePath, *Id); + IconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *FullPath, *Id); Type = InAsset.Type; CreatedAt = InAsset.CreatedAt; UpdatedAt = InAsset.UpdatedAt; From 42c5d889384e157cdd51155be7df3de02d70d9e0 Mon Sep 17 00:00:00 2001 From: Harrison Date: Mon, 23 Sep 2024 12:56:21 +0300 Subject: [PATCH 45/54] chore: refactor and fix paths --- .../Private/Api/Assets/AssetGlbLoader.cpp | 2 +- .../Private/Api/Assets/AssetIconLoader.cpp | 2 +- .../RpmNextGen/Private/Api/Files/FileApi.cpp | 1 - .../Private/Cache/CacheGenerator.cpp | 34 ++++++++++++++++++ .../RpmNextGen/Private/RpmLoaderComponent.cpp | 2 +- .../Private/Utilities/RpmImageHelper.cpp | 10 ++++-- .../Public/Cache/AssetCacheManager.h | 27 +++++++------- .../RpmNextGen/Public/Cache/CacheGenerator.h | 1 + .../RpmNextGen/Public/Cache/CachedAssetData.h | 35 +++++++++---------- 9 files changed, 77 insertions(+), 37 deletions(-) diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetGlbLoader.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetGlbLoader.cpp index fe1b7d2..fb8bab6 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetGlbLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetGlbLoader.cpp @@ -19,7 +19,7 @@ void FAssetGlbLoader::LoadGlb(const FAsset& Asset, const FString& BaseModelId, b if (FAssetCacheManager::Get().GetCachedAsset(Asset.Id, StoredAsset)) { TArray GlbData; - const FString StoredGlbPath = FFileUtility::GetFullPersistentPath(StoredAsset.GlbPathsByBaseModelId[BaseModelId]); + const FString StoredGlbPath = StoredAsset.GetGlbPathForBaseModelId(BaseModelId); if(FFileHelper::LoadFileToArray(GlbData, *StoredGlbPath)) { OnGlbLoaded.ExecuteIfBound(Asset, GlbData); diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetIconLoader.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetIconLoader.cpp index 3d21a60..cd0e0f2 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetIconLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetIconLoader.cpp @@ -19,7 +19,7 @@ void FAssetIconLoader::LoadIcon(const FAsset& Asset, bool bStoreInCache) if (FAssetCacheManager::Get().GetCachedAsset(Asset.Id, StoredAsset)) { TArray IconData; - if(FFileHelper::LoadFileToArray(IconData, * FFileUtility::GetFullPersistentPath(StoredAsset.IconFilePath))) + if(FFileHelper::LoadFileToArray(IconData, * FFileUtility::GetFullPersistentPath(StoredAsset.RelativeIconFilePath))) { OnIconLoaded.ExecuteIfBound(Asset, IconData); UE_LOG(LogReadyPlayerMe, Log, TEXT("Loading icon from cache")); diff --git a/Source/RpmNextGen/Private/Api/Files/FileApi.cpp b/Source/RpmNextGen/Private/Api/Files/FileApi.cpp index dc86c67..3aa929f 100644 --- a/Source/RpmNextGen/Private/Api/Files/FileApi.cpp +++ b/Source/RpmNextGen/Private/Api/Files/FileApi.cpp @@ -42,7 +42,6 @@ void FFileApi::FileRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Res bool FFileApi::LoadFileFromPath(const FString& Path, TArray& OutContent) { - const FString FileName = FPaths::GetCleanFilename(Path); if (!FPaths::FileExists(Path)) { UE_LOG(LogReadyPlayerMe, Error, TEXT("Path does not exist %s"), *Path); diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index 96e770f..1d7ac6b 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -128,6 +128,7 @@ void FCacheGenerator::OnAssetGlbSaved(const FAsset& Asset, const TArray& UE_LOG(LogReadyPlayerMe, Log, TEXT("OnLocalCacheGenerated Total assets to download: %d. Asset download requests completed: %d"), RequiredAssetDownloadRequest, NumberOfAssetsSaved); OnLocalCacheGenerated.ExecuteIfBound(true); + AddFolderToNonAssetDirectory(); } } @@ -139,9 +140,42 @@ void FCacheGenerator::OnAssetIconSaved(const FAsset& Asset, const TArray& UE_LOG(LogReadyPlayerMe, Log, TEXT("OnLocalCacheGenerated Total assets to download: %d. Asset download requests completed: %d"), RequiredAssetDownloadRequest, NumberOfAssetsSaved); OnLocalCacheGenerated.ExecuteIfBound(true); + AddFolderToNonAssetDirectory(); } } +void FCacheGenerator::AddFolderToNonAssetDirectory() const +{ + FString ConfigFilePath = FPaths::ProjectConfigDir() / TEXT("DefaultGame.ini"); + + // Section and key for "Additional Non-Asset Directories to Copy" + const FString SectionName = TEXT("/Script/UnrealEd.ProjectPackagingSettings"); + const FString KeyName = TEXT("+DirectoriesToAlwaysStageAsNonUFS"); + + // Folder to add to the non-asset directories + const FString FolderToAdd = FString::Printf(TEXT("(Path=\"%s\")"), *FFileUtility::RelativeCachePath); + + // Check if the folder is already added + FString CurrentValue; + if (GConfig->GetString(*SectionName, *KeyName, CurrentValue, ConfigFilePath)) + { + if (CurrentValue.Contains(FolderToAdd)) + { + // Folder already exists, no need to add it + UE_LOG(LogTemp, Log, TEXT("Folder already added to Additional Non-Asset Directories: %s"), *FolderToAdd); + return; + } + } + + // Add the folder to the config + GConfig->SetString(*SectionName, *KeyName, *FolderToAdd, ConfigFilePath); + + // Force update the config file + GConfig->Flush(false, ConfigFilePath); + + UE_LOG(LogTemp, Log, TEXT("Added folder to Additional Non-Asset Directories: %s"), *FolderToAdd); +} + void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful) { if(bWasSuccessful && AssetListResponse.IsSuccess) diff --git a/Source/RpmNextGen/Private/RpmLoaderComponent.cpp b/Source/RpmNextGen/Private/RpmLoaderComponent.cpp index 1272f8e..8ed92f7 100644 --- a/Source/RpmNextGen/Private/RpmLoaderComponent.cpp +++ b/Source/RpmNextGen/Private/RpmLoaderComponent.cpp @@ -59,7 +59,7 @@ void URpmLoaderComponent::CreateCharacter(const FString& BaseModelId, bool bUseC CharacterData.Assets.Add(FAssetApi::BaseModelType, AssetFromCache); OnCharacterCreated.Broadcast(CharacterData); TArray Data; - if(FFileApi::LoadFileFromPath(CachedAssetData.GlbPathsByBaseModelId[CharacterData.BaseModelId], Data)) + if(FFileApi::LoadFileFromPath(CachedAssetData.GetGlbPathForBaseModelId(CharacterData.BaseModelId), Data)) { UglTFRuntimeAsset* GltfRuntimeAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(Data, *GltfConfig); HandleGltfAssetLoaded(GltfRuntimeAsset, FAssetApi::BaseModelType); diff --git a/Source/RpmNextGen/Private/Utilities/RpmImageHelper.cpp b/Source/RpmNextGen/Private/Utilities/RpmImageHelper.cpp index cf9d59f..c7838a3 100644 --- a/Source/RpmNextGen/Private/Utilities/RpmImageHelper.cpp +++ b/Source/RpmNextGen/Private/Utilities/RpmImageHelper.cpp @@ -4,6 +4,8 @@ #include "RpmNextGen.h" #include "Components/Image.h" #include "Widgets/Images/SImage.h" +#include "Engine/Texture.h" +#include "Engine/Texture2D.h" UTexture2D* FRpmImageHelper::CreateTextureFromData(const TArray& ImageData) { @@ -36,8 +38,12 @@ UTexture2D* FRpmImageHelper::CreateTextureFromData(const TArray& ImageDat } // Disable mipmaps and streaming for icons - Texture->NeverStream = true; - Texture->MipGenSettings = TMGS_NoMipmaps; + Texture->CompressionSettings = TC_EditorIcon; // Optional: Prevent unnecessary compression for icons. + //Texture->MipGenSettings = TMGS_NoMipmaps; + Texture->LODGroup = TEXTUREGROUP_UI; // UI textures typically don’t use mipmaps. + Texture->NeverStream = true; + Texture->SRGB = true; // If you're working with UI icons, they are usually in sRGB space. + Texture->UpdateResource(); // Lock the texture and copy data void* TextureData = Texture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE); diff --git a/Source/RpmNextGen/Public/Cache/AssetCacheManager.h b/Source/RpmNextGen/Public/Cache/AssetCacheManager.h index a70bfae..4ec0683 100644 --- a/Source/RpmNextGen/Public/Cache/AssetCacheManager.h +++ b/Source/RpmNextGen/Public/Cache/AssetCacheManager.h @@ -72,15 +72,15 @@ class FAssetCacheManager void StoreAndTrackIcon(const FAssetLoadingContext& Context, const bool bSaveManifest = true) { const FCachedAssetData& StoredAsset = FCachedAssetData(Context.Asset); - FFileUtility::SaveToFile(Context.Data, FFileUtility::GetFullPersistentPath(StoredAsset.IconFilePath)); + FFileUtility::SaveToFile(Context.Data, FFileUtility::GetFullPersistentPath(StoredAsset.RelativeIconFilePath)); StoreAndTrackAsset(StoredAsset, bSaveManifest); } void StoreAndTrackGlb(const FAssetLoadingContext& Context, const bool bSaveManifest = true) { - const FCachedAssetData& StoredAsset = FCachedAssetData(Context.Asset, Context.BaseModelId); - const FString& GlbPath = FFileUtility::GetFullPersistentPath(StoredAsset.GlbPathsByBaseModelId[Context.BaseModelId]); + FCachedAssetData StoredAsset = FCachedAssetData(Context.Asset, Context.BaseModelId); + const FString& GlbPath = StoredAsset.GetGlbPathForBaseModelId(Context.BaseModelId); FFileUtility::SaveToFile(Context.Data, GlbPath); StoreAndTrackAsset(StoredAsset, bSaveManifest); @@ -88,13 +88,13 @@ class FAssetCacheManager void UpdateExistingCachedAsset(const FCachedAssetData& StoredAsset, FCachedAssetData* ExistingStoredAsset) { - if(!StoredAsset.GlbPathsByBaseModelId.IsEmpty()) + if(!StoredAsset.RelativeGlbPathsByBaseModelId.IsEmpty()) { - MergeTMaps(ExistingStoredAsset->GlbPathsByBaseModelId, StoredAsset.GlbPathsByBaseModelId); + MergeTMaps(ExistingStoredAsset->RelativeGlbPathsByBaseModelId, StoredAsset.RelativeGlbPathsByBaseModelId); } - if(ExistingStoredAsset->IconFilePath.IsEmpty() && !StoredAsset.IconFilePath.IsEmpty()) + if(ExistingStoredAsset->RelativeIconFilePath.IsEmpty() && !StoredAsset.RelativeIconFilePath.IsEmpty()) { - ExistingStoredAsset->IconFilePath = StoredAsset.IconFilePath; + ExistingStoredAsset->RelativeIconFilePath = StoredAsset.RelativeIconFilePath; } } @@ -196,17 +196,18 @@ class FAssetCacheManager FCachedAssetData CachedAsset = StoredAssets[AssetId]; IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); - for (const auto& GlbPath : CachedAsset.GlbPathsByBaseModelId) + for (const auto& GlbPath : CachedAsset.RelativeGlbPathsByBaseModelId) { - if (PlatformFile.FileExists(*GlbPath.Value)) + const FString FullGlbPath = FFileUtility::GetFullPersistentPath(GlbPath.Value); + if (PlatformFile.FileExists(*FullGlbPath)) { - PlatformFile.DeleteFile(*GlbPath.Value); + PlatformFile.DeleteFile(*FullGlbPath); } } - - if (PlatformFile.FileExists(*CachedAsset.IconFilePath)) + const FString FullIconPath = FFileUtility::GetFullPersistentPath(CachedAsset.RelativeIconFilePath); + if (PlatformFile.FileExists(*FullIconPath)) { - PlatformFile.DeleteFile(*CachedAsset.IconFilePath); + PlatformFile.DeleteFile(*FullIconPath); } StoredAssets.Remove(AssetId); diff --git a/Source/RpmNextGen/Public/Cache/CacheGenerator.h b/Source/RpmNextGen/Public/Cache/CacheGenerator.h index 07adc0e..6b75398 100644 --- a/Source/RpmNextGen/Public/Cache/CacheGenerator.h +++ b/Source/RpmNextGen/Public/Cache/CacheGenerator.h @@ -44,6 +44,7 @@ class RPMNEXTGEN_API FCacheGenerator : public TSharedFromThis void OnAssetGlbSaved(const FAsset& Asset, const TArray& Data); UFUNCTION() void OnAssetIconSaved(const FAsset& Asset, const TArray& Data); + void AddFolderToNonAssetDirectory() const; void FetchNextRefittedAsset(); TUniquePtr AssetApi; diff --git a/Source/RpmNextGen/Public/Cache/CachedAssetData.h b/Source/RpmNextGen/Public/Cache/CachedAssetData.h index 68c2ed2..733054b 100644 --- a/Source/RpmNextGen/Public/Cache/CachedAssetData.h +++ b/Source/RpmNextGen/Public/Cache/CachedAssetData.h @@ -23,10 +23,10 @@ struct RPMNEXTGEN_API FCachedAssetData FString IconUrl; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") - TMap GlbPathsByBaseModelId; + TMap RelativeGlbPathsByBaseModelId; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") - FString IconFilePath; + FString RelativeIconFilePath; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") FString Type; @@ -43,8 +43,8 @@ struct RPMNEXTGEN_API FCachedAssetData Name = FString(); GlbUrl = FString(); IconUrl = FString(); - GlbPathsByBaseModelId = TMap(); - IconFilePath = FString(); + RelativeGlbPathsByBaseModelId = TMap(); + RelativeIconFilePath = FString(); Type = FString(); CreatedAt = FDateTime(); UpdatedAt = FDateTime(); @@ -55,8 +55,8 @@ struct RPMNEXTGEN_API FCachedAssetData Name = InAsset.Name; GlbUrl = InAsset.GlbUrl; IconUrl = InAsset.IconUrl; - GlbPathsByBaseModelId = TMap(); - IconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *FFileUtility::GetFullPersistentPath(FFileUtility::RelativeCachePath), *Id); + RelativeGlbPathsByBaseModelId = TMap(); + RelativeIconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *FFileUtility::RelativeCachePath, *Id); Type = InAsset.Type; CreatedAt = InAsset.CreatedAt; UpdatedAt = InAsset.UpdatedAt; @@ -68,13 +68,12 @@ struct RPMNEXTGEN_API FCachedAssetData Name = InAsset.Name; GlbUrl = InAsset.GlbUrl; IconUrl = InAsset.IconUrl; - GlbPathsByBaseModelId = TMap(); - const FString FullPath = FFileUtility::GetFullPersistentPath(FFileUtility::RelativeCachePath); + RelativeGlbPathsByBaseModelId = TMap(); if(InBaseModelId != FString()) { - GlbPathsByBaseModelId.Add(InBaseModelId, FString::Printf(TEXT("%s/%s/%s.glb"), *FullPath, *InBaseModelId, *Id)); + RelativeGlbPathsByBaseModelId.Add(InBaseModelId, FString::Printf(TEXT("%s/%s/%s.glb"), *FFileUtility::RelativeCachePath, *InBaseModelId, *Id)); } - IconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *FullPath, *Id); + RelativeIconFilePath = FString::Printf(TEXT("%s/Icons/%s.png"), *FFileUtility::RelativeCachePath, *Id); Type = InAsset.Type; CreatedAt = InAsset.CreatedAt; UpdatedAt = InAsset.UpdatedAt; @@ -83,11 +82,11 @@ struct RPMNEXTGEN_API FCachedAssetData bool IsValid () const { bool Valid = true; - if(GlbPathsByBaseModelId.Num() == 0) + if(RelativeGlbPathsByBaseModelId.Num() == 0) { Valid = false; } - if (IconFilePath.IsEmpty()) + if (RelativeIconFilePath.IsEmpty()) { Valid = false; } @@ -115,13 +114,13 @@ struct RPMNEXTGEN_API FCachedAssetData JsonObject->SetStringField(TEXT("Name"), Name); JsonObject->SetStringField(TEXT("GlbUrl"), GlbUrl); JsonObject->SetStringField(TEXT("IconUrl"), IconUrl); - JsonObject->SetStringField(TEXT("IconFilePath"), IconFilePath); + JsonObject->SetStringField(TEXT("IconFilePath"), RelativeIconFilePath); JsonObject->SetStringField(TEXT("Type"), Type); JsonObject->SetStringField(TEXT("CreatedAt"), CreatedAt.ToString()); JsonObject->SetStringField(TEXT("UpdatedAt"), UpdatedAt.ToString()); TSharedPtr GlbPathsObject = MakeShared(); - for (const auto& Entry : GlbPathsByBaseModelId) + for (const auto& Entry : RelativeGlbPathsByBaseModelId) { GlbPathsObject->SetStringField(Entry.Key, Entry.Value); } @@ -138,7 +137,7 @@ struct RPMNEXTGEN_API FCachedAssetData StoredAsset.Name = JsonObject->GetStringField(TEXT("Name")); StoredAsset.GlbUrl = JsonObject->GetStringField(TEXT("GlbUrl")); StoredAsset.IconUrl = JsonObject->GetStringField(TEXT("IconUrl")); - StoredAsset.IconFilePath = JsonObject->GetStringField(TEXT("IconFilePath")); + StoredAsset.RelativeIconFilePath = JsonObject->GetStringField(TEXT("IconFilePath")); StoredAsset.Type = JsonObject->GetStringField(TEXT("Type")); FDateTime::Parse(JsonObject->GetStringField(TEXT("CreatedAt")), StoredAsset.CreatedAt); FDateTime::Parse(JsonObject->GetStringField(TEXT("UpdatedAt")), StoredAsset.UpdatedAt); @@ -146,7 +145,7 @@ struct RPMNEXTGEN_API FCachedAssetData TSharedPtr GlbPathsObject = JsonObject->GetObjectField(TEXT("GlbPathsByBaseModelId")); for (const auto& Entry : GlbPathsObject->Values) { - StoredAsset.GlbPathsByBaseModelId.Add(Entry.Key, Entry.Value->AsString()); + StoredAsset.RelativeGlbPathsByBaseModelId.Add(Entry.Key, Entry.Value->AsString()); } return StoredAsset; @@ -154,9 +153,9 @@ struct RPMNEXTGEN_API FCachedAssetData FString GetGlbPathForBaseModelId(FString BaseModelId) { - if(GlbPathsByBaseModelId.Num() > 0 && !BaseModelId.IsEmpty()) + if(RelativeGlbPathsByBaseModelId.Num() > 0 && !BaseModelId.IsEmpty()) { - return GlbPathsByBaseModelId[BaseModelId]; + return FFileUtility::GetFullPersistentPath(RelativeGlbPathsByBaseModelId[BaseModelId]); } return ""; From 6a0d83333f62eb9ead2b62e1101ff4538c4200a1 Mon Sep 17 00:00:00 2001 From: Harrison Date: Mon, 23 Sep 2024 13:21:34 +0300 Subject: [PATCH 46/54] chore: cleanup and handle failed cases --- .../Api/Files/PakFileUtility.cpp | 54 +++++-------------- .../Private/Cache/CacheGenerator.cpp | 52 ++++++++++++++---- .../Private/UI/SCacheGeneratorWidget.cpp | 5 ++ 3 files changed, 62 insertions(+), 49 deletions(-) rename Source/RpmNextGen/{Public => Private}/Api/Files/PakFileUtility.cpp (84%) diff --git a/Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp b/Source/RpmNextGen/Private/Api/Files/PakFileUtility.cpp similarity index 84% rename from Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp rename to Source/RpmNextGen/Private/Api/Files/PakFileUtility.cpp index cccda38..4bbbd20 100644 --- a/Source/RpmNextGen/Public/Api/Files/PakFileUtility.cpp +++ b/Source/RpmNextGen/Private/Api/Files/PakFileUtility.cpp @@ -1,5 +1,6 @@ -#include "PakFileUtility.h" -#include "FileUtility.h" +#include "Api/Files/PakFileUtility.h" + +#include "Api/Files/FileUtility.h" #include "IPlatformFilePak.h" const FString ResponseFilePath = FPaths::ProjectContentDir() / TEXT("ReadyPlayerMe/Cache/RpmCache_ResponseFile.txt"); @@ -80,6 +81,11 @@ void FPakFileUtility::CreatePakFile() void FPakFileUtility::ExtractPakFile(const FString& PakFilePath) { + if(!FPaths::FileExists(PakFilePath) ) + { + UE_LOG(LogTemp, Error, TEXT("Pak file does not exist: %s"), *PakFilePath); + return; + } const FString UnrealPakPath = FPaths::ConvertRelativePathToFull(FPaths::EngineDir() / TEXT("Binaries/Win64/UnrealPak.exe")); const FString DestinationPath = FFileUtility::GetFullPersistentPath(FFileUtility::RelativeCachePath); const FString CommandLineArgs = FString::Printf(TEXT("%s -Extract %s"), *PakFilePath, *DestinationPath); @@ -99,46 +105,14 @@ void FPakFileUtility::ExtractPakFile(const FString& PakFilePath) } } -// void FPakFileUtility::ExtractFilesFromPak(const FString& PakFilePath) -// { -// bool bSuccessfulInitialization = false; -// IPlatformFile* LocalPlatformFile = &FPlatformFileManager::Get().GetPlatformFile(); -// if (LocalPlatformFile != nullptr) -// { -// IPlatformFile* PakPlatformFile = FPlatformFileManager::Get().GetPlatformFile(TEXT("PakFile")); -// if (PakPlatformFile != nullptr) -// { -// UE_LOG(LogTemp, Error, TEXT("Successfully initialized PakPlatformFile")); -// bSuccessfulInitialization = true; -// } -// -// } -// if(bSuccessfulInitialization) -// { -// const TCHAR* cmdLine = TEXT(""); -// FPakPlatformFile* PakPlatform = new FPakPlatformFile(); -// IPlatformFile* InnerPlatform = LocalPlatformFile; -// PakPlatform->Initialize(InnerPlatform, cmdLine); -// FPlatformFileManager::Get().SetPlatformFile(*PakPlatform); -// -// FString FilePath = PakFilePath; -// -// UE_LOG(LogTemp, Log, TEXT("Attempting to load pak: %s"), *FilePath); -// -// FPakFile PakFile = FPakFile(InnerPlatform, *FilePath, false); -// -// if (!PakFile.IsValid()) -// { -// UE_LOG(LogTemp, Error, TEXT("Invalid pak file: %s"), *FilePath); -// return ; -// } -// UE_LOG(LogTemp, Log, TEXT("Pak file is VALID! %s"), *FilePath); -// //FCoreDelegates::MountPak.IsBound(); -// } -// } - void FPakFileUtility::ExtractFilesFromPak(const FString& PakFilePath) { + if(!FPaths::FileExists(PakFilePath) ) + { + UE_LOG(LogTemp, Error, TEXT("Pak file does not exist: %s"), *PakFilePath); + return; + } + // Step 1: Get the current platform file and initialize the Pak platform IPlatformFile& InnerPlatformFile = FPlatformFileManager::Get().GetPlatformFile(); FPakPlatformFile* PakPlatformFile = static_cast(FPlatformFileManager::Get().FindPlatformFile(TEXT("PakFile"))); diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp index 1d7ac6b..a5f7fa3 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp @@ -46,6 +46,13 @@ void FCacheGenerator::LoadAndStoreAssets() { TArray BaseModelAssets = TArray(); int TotalRefittedAssets = 0; + + // Ensure AssetMapByBaseModelId contains valid data + if (AssetMapByBaseModelId.Num() == 0) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("No base models found in AssetMapByBaseModelId")); + return; + } for ( auto BaseModel : AssetMapByBaseModelId) { for (auto Asset : BaseModel.Value) @@ -57,6 +64,15 @@ void FCacheGenerator::LoadAndStoreAssets() TotalRefittedAssets++; } } + + // Ensure there's at least one BaseModel + if (BaseModelAssets.Num() == 0) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("No base model assets found")); + OnCacheDataLoaded.ExecuteIfBound(false); + return; + } + int AssetIconRequestCount = 0; // load and store base model assets @@ -66,12 +82,20 @@ void FCacheGenerator::LoadAndStoreAssets() AssetIconRequestCount++; } - // load and store asset icon (only 1 set of icons is required) - for (auto Asset : AssetMapByBaseModelId[BaseModelAssets[0].Id]) + // Ensure AssetMap contains the BaseModelId + if (AssetMapByBaseModelId.Contains(BaseModelAssets[0].Id)) { - if(Asset.Type == FAssetApi::BaseModelType) continue; - LoadAndStoreAssetIcon(BaseModelAssets[0].Id, &Asset); - AssetIconRequestCount++; + for (auto& Asset : AssetMapByBaseModelId[BaseModelAssets[0].Id]) + { + if (Asset.Type == FAssetApi::BaseModelType) continue; + LoadAndStoreAssetIcon(BaseModelAssets[0].Id, &Asset); + AssetIconRequestCount++; + } + } + else + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("BaseModelId not found in AssetMapByBaseModelId")); + return; } RequiredAssetDownloadRequest = TotalRefittedAssets + AssetIconRequestCount; @@ -96,13 +120,24 @@ void FCacheGenerator::LoadAndStoreAssets() void FCacheGenerator::LoadAndStoreAssetGlb(const FString& BaseModelId, const FAsset* Asset) { + if (!Asset) // Ensure asset is valid + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("Invalid asset when loading GLB for BaseModelId: %s"), *BaseModelId); + return; + } + TSharedPtr AssetLoader = MakeShared(); - AssetLoader->OnGlbLoaded.BindRaw( this, &FCacheGenerator::OnAssetGlbSaved); + AssetLoader->OnGlbLoaded.BindRaw(this, &FCacheGenerator::OnAssetGlbSaved); AssetLoader->LoadGlb(*Asset, BaseModelId, true); } void FCacheGenerator::LoadAndStoreAssetIcon(const FString& BaseModelId, const FAsset* Asset) { + if (!Asset) // Ensure asset is valid + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("Invalid asset when loading Icon for BaseModelId: %s"), *BaseModelId); + return; + } TSharedPtr AssetLoader = MakeShared(); AssetLoader->OnIconLoaded.BindRaw( this, &FCacheGenerator::OnAssetIconSaved); AssetLoader->LoadIcon(*Asset, true); @@ -178,7 +213,7 @@ void FCacheGenerator::AddFolderToNonAssetDirectory() const void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful) { - if(bWasSuccessful && AssetListResponse.IsSuccess) + if(bWasSuccessful && AssetListResponse.IsSuccess && AssetListResponse.Data.Num() > 0) { if (AssetListResponse.Data[0].Type == FAssetApi::BaseModelType) { @@ -212,8 +247,7 @@ void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListRe return; } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to fetch assets")); - RefittedAssetRequestsCompleted++; - FetchNextRefittedAsset(); + OnCacheDataLoaded.ExecuteIfBound(false); } void FCacheGenerator::StartFetchingRefittedAssets() diff --git a/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp index 377de16..a24b04c 100644 --- a/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp @@ -201,6 +201,11 @@ FReply SCacheGeneratorWidget::OnDownloadRemoteCacheClicked() void SCacheGeneratorWidget::OnFetchCacheDataComplete(bool bWasSuccessful) { + if(!bWasSuccessful) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to fetch cache data")); + return; + } UE_LOG(LogReadyPlayerMe, Log, TEXT("Completed fetching assets")); CacheGenerator->LoadAndStoreAssets(); } From 3c6ee914530ffb461a72535699469bac99d9b484 Mon Sep 17 00:00:00 2001 From: Harrison Hough Date: Wed, 2 Oct 2024 07:48:08 +0300 Subject: [PATCH 47/54] RpmActor now supports setting/mapping of skeleton and animation blueprints (#13) ![image](https://github.com/user-attachments/assets/1aff3259-cb71-442a-bef0-c6501fa2074b) - RpmActor now has an `AnimationConfig` that is exposed to blueprints (and C++ functions). This can be used to set which skeleton and animation Blueprint to use. If you don't need to support multiple "styles" then this is all you need ![image](https://github.com/user-attachments/assets/666e94ee-0d25-4857-ac21-343f27ea4888) - to support multiple styles (multiple base modes) there is also a new property exposed to blueprints called `AnimationConfigsByBaseModelId`. This can be used to map different styles/base models to their appropriate skeletons and animations blueprints. If nothing is set then animations won't run for that style/basemodel. ![image](https://github.com/user-attachments/assets/ee95a39e-13d0-4eac-867e-6822e0484305) - Category icons and Asset panels are now created dynamically based on the categories fetched from RPM API's. As long as you are signed in and have authentication. - various things were refactored to make this work - Separation of LoadCharacter vs LoadAsset functions on RPMActor (to better handle loading a full character vs 1 single asset) --- .../Blueprints/BP_SampleRpmActor.uasset | Bin 32089 -> 38771 bytes .../Blueprints/WBP_RpmLoaderUI.uasset | Bin 116977 -> 54514 bytes .../Blueprints/WBP_RpmLoaderUI_NEW.uasset | Bin 0 -> 2552 bytes .../Samples/BasicLoader/RpmBasicLoader.umap | Bin 136933 -> 143362 bytes .../Blueprints/WBP_RpmAssetPanel.uasset | Bin 23992 -> 24076 bytes .../BasicUI/Blueprints/WBP_RpmBasicUI.uasset | Bin 132230 -> 69676 bytes .../Blueprints/WBP_RpmCategoryButton.uasset | Bin 28255 -> 27735 bytes .../Blueprints/WBP_RpmCategoryPanel.uasset | Bin 37655 -> 27721 bytes .../BasicUI/Blueprints/WBP_RpmUI.uasset | Bin 0 -> 2421 bytes Content/Samples/BasicUI/RpmBasicUISample.umap | Bin 78012 -> 78131 bytes .../T-rpm-Custom-bottom.uasset} | Bin 6444 -> 6464 bytes .../T-rpm-Custom-footwear.uasset} | Bin 6066 -> 6098 bytes .../T-rpm-Custom-top.uasset} | Bin 5583 -> 5603 bytes .../Icons/T-rpm-baseModel.uasset | Bin 6430 -> 6422 bytes .../{BasicUI => }/Icons/T-rpm-custom.uasset | Bin 5795 -> 5787 bytes .../Icons/T-rpm-facial-hair.uasset | Bin 6456 -> 6448 bytes .../{BasicUI => }/Icons/T-rpm-glasses.uasset | Bin 5983 -> 5975 bytes .../{BasicUI => }/Icons/T-rpm-hair.uasset | Bin 6826 -> 6818 bytes .../Private/Api/Assets/AssetApi.cpp | 26 +---- .../RpmNextGen/Private/Api/Files/FileApi.cpp | 33 +++++- .../Private/Api/Files/GlbLoader.cpp | 6 +- Source/RpmNextGen/Private/RpmActor.cpp | 102 +++++++++++++++--- .../RpmNextGen/Private/RpmLoaderComponent.cpp | 73 +++++++------ .../Private/Samples/RpmAssetPanelWidget.cpp | 17 +-- .../Samples/RpmCategoryButtonWidget.cpp | 14 ++- .../Samples/RpmCategoryPanelWidget.cpp | 89 ++++++++++++--- .../Private/Samples/RpmCreatorWidget.cpp | 89 +++++++++++++++ .../RpmNextGen/Public/Api/Assets/AssetApi.h | 12 ++- .../Public/Api/Assets/AssetGlbLoader.h | 9 +- .../Public/Api/Assets/AssetIconLoader.h | 12 +-- .../Public/Api/Assets/Models/Asset.h | 13 +++ .../Api/Assets/Models/AssetListRequest.h | 3 +- .../Api/Assets/Models/AssetListResponse.h | 3 + .../RpmNextGen/Public/Api/Auth/ApiRequest.h | 1 - Source/RpmNextGen/Public/Api/Auth/AuthApi.h | 4 +- .../Public/Api/Characters/CharacterApi.h | 14 +-- .../Public/Api/Common/Models/Pagination.h | 37 +++++++ Source/RpmNextGen/Public/Api/Common/WebApi.h | 11 +- Source/RpmNextGen/Public/Api/Files/FileApi.h | 16 +-- .../RpmNextGen/Public/Api/Files/GlbLoader.h | 9 +- .../Public/Api/Files/PakFileUtility.h | 4 +- .../Public/Cache/AssetCacheManager.h | 15 +-- .../RpmNextGen/Public/Cache/CacheGenerator.h | 45 ++++---- Source/RpmNextGen/Public/RpmActor.h | 52 +++++---- Source/RpmNextGen/Public/RpmCharacterTypes.h | 46 ++++++++ Source/RpmNextGen/Public/RpmLoaderComponent.h | 75 ++++++------- .../Public/Samples/RpmAssetButtonWidget.h | 10 +- .../Public/Samples/RpmAssetCardWidget.h | 22 ++-- .../Public/Samples/RpmAssetPanelWidget.h | 5 +- .../Public/Samples/RpmCategoryButtonWidget.h | 8 +- .../Public/Samples/RpmCategoryPanelWidget.h | 32 +++++- .../Public/Samples/RpmCreatorWidget.h | 47 ++++++++ .../Public/Settings/RpmDeveloperSettings.h | 9 +- .../Private/AssetNameGenerator.cpp | 6 +- .../Private/EditorAssetLoader.cpp | 5 +- .../Private/UI/SRpmDeveloperLoginWidget.cpp | 25 +++-- .../Public/AssetNameGenerator.h | 15 +-- .../Public/Auth/DevAuthTokenCache.h | 4 +- .../Public/Auth/DeveloperAuthApi.h | 4 +- .../Public/Auth/DeveloperTokenAuthStrategy.h | 4 +- .../DeveloperAccounts/DeveloperAccountApi.h | 14 +-- .../Public/EditorAssetLoader.h | 5 +- .../Public/RpmNextGenEditor.h | 3 +- .../Public/UI/SCacheGeneratorWidget.h | 9 +- .../Public/UI/SCharacterLoaderWidget.h | 21 ++-- .../Public/UI/SRpmDeveloperLoginWidget.h | 4 +- 66 files changed, 761 insertions(+), 321 deletions(-) create mode 100644 Content/Samples/BasicLoader/Blueprints/WBP_RpmLoaderUI_NEW.uasset create mode 100644 Content/Samples/BasicUI/Blueprints/WBP_RpmUI.uasset rename Content/Samples/{BasicUI/Icons/T-rpm-bottom.uasset => Icons/T-rpm-Custom-bottom.uasset} (86%) rename Content/Samples/{BasicUI/Icons/T-rpm-shoes.uasset => Icons/T-rpm-Custom-footwear.uasset} (85%) rename Content/Samples/{BasicUI/Icons/T-rpm-top.uasset => Icons/T-rpm-Custom-top.uasset} (84%) rename Content/Samples/{BasicUI => }/Icons/T-rpm-baseModel.uasset (86%) rename Content/Samples/{BasicUI => }/Icons/T-rpm-custom.uasset (85%) rename Content/Samples/{BasicUI => }/Icons/T-rpm-facial-hair.uasset (86%) rename Content/Samples/{BasicUI => }/Icons/T-rpm-glasses.uasset (85%) rename Content/Samples/{BasicUI => }/Icons/T-rpm-hair.uasset (82%) create mode 100644 Source/RpmNextGen/Private/Samples/RpmCreatorWidget.cpp create mode 100644 Source/RpmNextGen/Public/Api/Common/Models/Pagination.h create mode 100644 Source/RpmNextGen/Public/RpmCharacterTypes.h create mode 100644 Source/RpmNextGen/Public/Samples/RpmCreatorWidget.h diff --git a/Content/Samples/BasicLoader/Blueprints/BP_SampleRpmActor.uasset b/Content/Samples/BasicLoader/Blueprints/BP_SampleRpmActor.uasset index d784b8ebb942bc4f0d37f9424fc806941962d388..c4b2c179e633801d066050bc3c5c487c3b7f4784 100644 GIT binary patch literal 38771 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 diff --git a/Content/Samples/BasicLoader/Blueprints/WBP_RpmLoaderUI.uasset b/Content/Samples/BasicLoader/Blueprints/WBP_RpmLoaderUI.uasset index b9cbf13406a6c43779c359f13270896928737e1d..cbfed4b82f4a1562d47e5e9162a5a92971ae4840 100644 GIT binary patch literal 54514 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 TbPjqF{QxdndGNroV=n)H*63Lu diff --git a/Content/Samples/BasicLoader/Blueprints/WBP_RpmLoaderUI_NEW.uasset b/Content/Samples/BasicLoader/Blueprints/WBP_RpmLoaderUI_NEW.uasset new file mode 100644 index 0000000000000000000000000000000000000000..41e2705c26c53fea5f4053865b70ddceffec7c5b GIT binary patch literal 2552 zcmcImO-vI}5PpciApW3|7=r=_i$!Qv5D5nDZd)WN1tAR=p*-6su9mjxE|U!sYL9568+G#*I&i9`=ZPsWQ$H6g*Dm>xjUIxjZpMP7(gzCCkt;j~*C$mqk_@6IF#3%cFFT>PoV+@oNrdR<#G2#Vts_02ykkdXI<4d#ET9siX7E23VQd zA=>7VoK6jXCr_T1f9T)zHxs}IX4l^Z8}R_DnMBQ0q9oXT_v~sAL{Qc-w-5ldfM9$q zWLpXfCG|wC>|3|$_j=mF<*hdu$~BP4P|E&Ed^e38FnrGI?ZPZ+HJCxY|3rIMd>!m8 z-25d=jwQHTm^6K!q^9ZFg^jo?Rt^_|1*pW_ku0`9zqTH4HWF3{(iGgbw8%8(8P6^~ z#v-y%?eJt(!ket6cy_8R+E(4WQW5?De0U04!n=Wl8Ad|86_IJo6DtX6O@zsIs&7Rk zGuR%EUS}u^#A8zCSfwgqm%uR-Y5k)8PfaY-B$% zg=K{l9V<9bvCuD`%_O|^cNmz1Zwf~l>XRYF7PtJ9=p`rN62{$|sJQzRZAe`4mtaeW zk~74b1gyA_Fh5u;W+qC4SN?z~eOUmhwiEHXfB%*gC2rC>`J<{0hHY<_m=7T4*j_gm z#2Q38lK4Dk3Dol^(AcItkFcG*PHyX65HlV{w~<^hNb0Xjb%Zsh&XHrSMX#~uM&O_C E7g?rIPXGV_ literal 0 HcmV?d00001 diff --git a/Content/Samples/BasicLoader/RpmBasicLoader.umap b/Content/Samples/BasicLoader/RpmBasicLoader.umap index fb1012375876b200e996321756e625097dac6b04..76ad8dbfec5984f2c56e0bdfcfb5b8303ade0ab6 100644 GIT binary patch literal 143362 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

_`5g^*NIoeFUITaA0ZE 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 diff --git a/Content/Samples/BasicUI/Blueprints/WBP_RpmAssetPanel.uasset b/Content/Samples/BasicUI/Blueprints/WBP_RpmAssetPanel.uasset index 3132c5868de21645a15b1701f73f4a086d2c0719..64d0cb293f0f4b67ee30e5cce40e8588480f12fc 100644 GIT binary patch delta 3916 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 EKR~53`Tzg` literal 132230 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 Q;(WsK8(bTm-W;&^-_!SB>i_@% delta 7338 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 WIYHG88tA1oXq3*%-rtt^tN#Ln2G>6T diff --git a/Content/Samples/BasicUI/Blueprints/WBP_RpmCategoryPanel.uasset b/Content/Samples/BasicUI/Blueprints/WBP_RpmCategoryPanel.uasset index b4d76971382b6e18636a47d65a390fc08c83a293..913996e185f901e0de65fcd46fe0c31534527f36 100644 GIT binary patch delta 5273 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 diff --git a/Content/Samples/BasicUI/Icons/T-rpm-bottom.uasset b/Content/Samples/Icons/T-rpm-Custom-bottom.uasset similarity index 86% rename from Content/Samples/BasicUI/Icons/T-rpm-bottom.uasset rename to Content/Samples/Icons/T-rpm-Custom-bottom.uasset index cfde8a412ddcd6b993f7acac8e7abd557bbfa8dd..4d5db281edfb77c33320fa46f83c5cba20f31de1 100644 GIT binary patch delta 327 zcmZ2ubiimr1mmTNk&YfJYzz#{3=9l!fHW(R2JsCzfD9mT0Ma15o0oxM-gO7F3dJ=( zd)m{vH@0hM&HoiGz~u=C)co6i2_Y#K#}3@xDYW}i0vQazR7~@*^HMa*Rp3b2@6f0$SyzGfSrA_0LNZd gO$=RjKwoVVfVhqkNWm0rX5s5*XYAZOL39H%09?0IPXGV_ diff --git a/Content/Samples/BasicUI/Icons/T-rpm-shoes.uasset b/Content/Samples/Icons/T-rpm-Custom-footwear.uasset similarity index 85% rename from Content/Samples/BasicUI/Icons/T-rpm-shoes.uasset rename to Content/Samples/Icons/T-rpm-Custom-footwear.uasset index 5d7def300c145ca93e0c08ad63c062b7a2f8a34b..4dc0922b26395dd1b4c1228ae169e08d1f192d99 100644 GIT binary patch delta 335 zcmdm_e@TBr1mpaPk&Yf3Yzz#{3=9k(fHW(R2JuZefD9mT0n#Ac#LK{NQ?5Lzc71-G)8?d`DZ55pSk6mnX4|@pX nx6KdPm$N$KHp2tx+sOhDS2F@BoGLeO=WSSASA delta 303 zcmcblze#^W1Y`ZgNJkGDHUXKmCLM z%4FvxxA(gflq}x8=-j5@(&6yXeZKgP$rB%iD6U$=wg;qd4p6<1D2Rms|AC->b2#IB zW-~pYsD4mEu3u_JiF<0EesE%LK~8G1zEfgxW^$;fzGrfNUa@|NZc#z5ZgEC_YB7Tq zfBN^yb*xn)JV33;64NS-dnb#r{bQ;Zm^_hPd9o$DJLA;JjqKS>*90K~9Q>OVI7(Rc f(RJDY{Z%XgaUUa)LX+QoowuEx(PwkKXfYE2BmY$5 diff --git a/Content/Samples/BasicUI/Icons/T-rpm-top.uasset b/Content/Samples/Icons/T-rpm-Custom-top.uasset similarity index 84% rename from Content/Samples/BasicUI/Icons/T-rpm-top.uasset rename to Content/Samples/Icons/T-rpm-Custom-top.uasset index b6acb8e42fec151348601157c1538fea4e9fdc41..a57c9dd971253c15eb919b66a84b34c159849945 100644 GIT binary patch delta 311 zcmX@F{aAZK1f$)=NJkGjHUIBLKiRQ>g#| delta 303 zcmaE?eO`M)1f$l(NJkF|HUGO9DtrSn{w;#=kQS`#0IC^GJ4NCxTi1FHWd3}PX`e<0A>9M1Tj z*+d5@svlI4>z7(l;+~qPADoz5kds=h@03`anH=h=@0pyRSF9hRTU3y%TasVEa5HTC z#L1PcRYF`qjR;}B$!!{wdD#9jY4J}MWLKQ5z|J~3Q;27>9lHgSwZJ3}vB`bxAxt#_ oFv-na92P8UsAgCIeJ95caU~;=g2`?!<7r}N`Xs#hm+(tQ0BhAyDF6Tf diff --git a/Content/Samples/BasicUI/Icons/T-rpm-baseModel.uasset b/Content/Samples/Icons/T-rpm-baseModel.uasset similarity index 86% rename from Content/Samples/BasicUI/Icons/T-rpm-baseModel.uasset rename to Content/Samples/Icons/T-rpm-baseModel.uasset index 2536a4f7f033caea789d8b3f92e13640406d3506..ada5385530d00e8f1e2a1eefaf9c0aa229c8fa89 100644 GIT binary patch delta 219 zcmbPdG|gy21ml5;k&f;%Yzz#{3=9lUfHW(R2JtmGfD9n80Ma15j+cR9bNsohtsxKn zCoJKVKD{g4RpMQ=II9~<`(3n7VCQ^=jRpchv*g+d(LTkU|puC-wwYEc?{3@O?((D`2eVFnkYmojQkG-CpL#OzGs#<0*dJe z73BJ*R+PA>=II9~<`(3n7VA4D7H1}hdQOgI6=(T-+i%O}3f2jXOeX{;3$n{kR$ynJ tY{2fqbYF1tKX$RnJ?tTj3Y#CYe`aNM0Xl5Y=9|3B*%_y8ZWNu$3;=t9Kmh;% diff --git a/Content/Samples/BasicUI/Icons/T-rpm-custom.uasset b/Content/Samples/Icons/T-rpm-custom.uasset similarity index 85% rename from Content/Samples/BasicUI/Icons/T-rpm-custom.uasset rename to Content/Samples/Icons/T-rpm-custom.uasset index fd63df0b4395bf273646d677e49f7b40ab84a3a2..c1b140a89cf77dc3da2f246ffde75c9efd5c1196 100644 GIT binary patch delta 230 zcmZ3iJ6m@`1Y_I8NJkGbHU$AEH}SkgJfMVV{bI>B$PKqKS`UB-a4dMT$c7!pQ$X(6>39@jbJ-K2S_Q zs36xbwW7p5HBUb{F}ENmwOHRNu{bk1)KlLxIX|yhKSZ~vAXhiJw74Wcmtkw>gS(qc zS$i3o`UEC7vMWwjU}v4I#qPqmXmUP#Hq&E4xL_4K&t^W3Nvy1PKnFK%UdlUu}QY%W_Q}gtL6LSl4Qj7IHlk@Y6^+R-v3UYPR5|c9%b96HjGm99~yE`XsZepFp z$TVAE@vWY1>$BM1`M%*nBsmDL02s_C0I@~&oQEZQ6^dYKsj3T;1j diff --git a/Content/Samples/BasicUI/Icons/T-rpm-glasses.uasset b/Content/Samples/Icons/T-rpm-glasses.uasset similarity index 85% rename from Content/Samples/BasicUI/Icons/T-rpm-glasses.uasset rename to Content/Samples/Icons/T-rpm-glasses.uasset index 19b0ae4ffaebde308294985dd46a20e096ac272c..fc789d0b0ecd5c518f5ea2e53aba01c4876493ab 100644 GIT binary patch delta 219 zcmcbwcU^Bn1Y^m>NJkF|HU-h$Da&KJ?4Rj)bT_VgbhJwI^TbCnk`sXH97G{{VdQ@xDA^p&_@3E711P2+ zRFLbJT2bPjnx`L}m|KvOTCDGxoS#>$AEH}SkgJ=XlUQ7wTFmeu`_jG5b*z1iOeF%7 t1=*D+8?w7Ic22Hj&t^I$2ol)L$Z$LUoS#>$AEH}SkgJ=XlUQ7wTFmfkXR_hu zGS)surW%3CjqJ*kHQC)6Cr&P8&t|$L2od1m-z>mU!^-Laa_Q!YybIYGJvJMQsxScn Dhvh@j diff --git a/Content/Samples/BasicUI/Icons/T-rpm-hair.uasset b/Content/Samples/Icons/T-rpm-hair.uasset similarity index 82% rename from Content/Samples/BasicUI/Icons/T-rpm-hair.uasset rename to Content/Samples/Icons/T-rpm-hair.uasset index f2daba780da611684c84f9306a916e673091630a..0139833479677ce5109ca6b4977a66469faae86a 100644 GIT binary patch delta 229 zcmZ2wy2x}w1S9LjNJkGLHUWc9F>n>>Qi*IR3J-83CR7Q(&_p-*k4ybDK|!K4bv^ DAk9IQ delta 225 zcmZ2vy2^Ay1S9XnNJkGTHUe$@|gH2Mp7TB?wTk>FO2*T1iYKW8Q(LT>H@{| zg9>u}QY%W_Q}gtL6LSl4Qj7JS5{omFLp}99lk@Y6^+R-v3UYNb5;Kb!R=&>Lwz-(K zn~{lEa55vi>SR@RS4R2C+3eX&2|^%&&41a|*x0OqPGl6^tjRZ>o$=b{gQ5>v0OFBA Aa{vGU diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp index 16176ee..4a26748 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp @@ -1,4 +1,4 @@ -#include "Api/Assets/AssetApi.h" +#include "Api/Assets/AssetApi.h" #include "RpmNextGen.h" #include "Settings/RpmDeveloperSettings.h" @@ -68,9 +68,7 @@ void FAssetApi::ListAssetTypesAsync(const FAssetTypeListRequest& Request) void FAssetApi::HandleResponse(FString Response, bool bWasSuccessful) { if (bWasSuccessful) - { - #if ENGINE_MAJOR_VERSION < 5 || ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 1 // Manual parsing for Unreal Engine 5.0 and earlier - + { TSharedPtr JsonObject; TSharedRef> Reader = TJsonReaderFactory<>::Create(Response); @@ -110,26 +108,6 @@ void FAssetApi::HandleResponse(FString Response, bool bWasSuccessful) } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to parse JSON into known structs from response: %s"), *Response); - - #else - - // Use EStructJsonFlags::SkipMissingProperties for Unreal Engine 5.1 and later - FAssetListResponse AssetListResponse = FAssetListResponse(); - if (FJsonObjectConverter::JsonObjectStringToUStruct(Response, &AssetListResponse, 0, EStructJsonFlags::SkipMissingProperties)) - { - OnListAssetsResponse.ExecuteIfBound(AssetListResponse, true); - return; - } - FAssetTypeListResponse AssetTypeListResponse = FAssetTypeListResponse(); - if (FJsonObjectConverter::JsonObjectStringToUStruct(Response, &AssetTypeListResponse, 0, EStructJsonFlags::SkipMissingProperties)) - { - OnListAssetTypeResponse.ExecuteIfBound(AssetTypeListResponse, true); - return; - } - - UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to parse API from response %s"), *Response); - - #endif } else { diff --git a/Source/RpmNextGen/Private/Api/Files/FileApi.cpp b/Source/RpmNextGen/Private/Api/Files/FileApi.cpp index 3aa929f..08bcb24 100644 --- a/Source/RpmNextGen/Private/Api/Files/FileApi.cpp +++ b/Source/RpmNextGen/Private/Api/Files/FileApi.cpp @@ -2,6 +2,7 @@ #include "HttpModule.h" #include "RpmNextGen.h" +#include "Api/Assets/Models/Asset.h" #include "Interfaces/IHttpResponse.h" FFileApi::FFileApi() @@ -12,16 +13,25 @@ FFileApi::~FFileApi() { } -void FFileApi::LoadFileFromUrl(const FString& URL, const FString& AssetType) +void FFileApi::LoadFileFromUrl(const FString& URL) { TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); - HttpRequest->OnProcessRequestComplete().BindRaw(this, &FFileApi::FileRequestComplete, AssetType); + HttpRequest->OnProcessRequestComplete().BindRaw(this, &FFileApi::FileRequestComplete); HttpRequest->SetURL(URL); HttpRequest->SetVerb("GET"); HttpRequest->ProcessRequest(); } -void FFileApi::FileRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FString AssetType) +void FFileApi::LoadAssetFileFromUrl(const FString& URL, FAsset Asset) +{ + TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); + HttpRequest->OnProcessRequestComplete().BindRaw(this, &FFileApi::AssetFileRequestComplete, Asset); + HttpRequest->SetURL(URL); + HttpRequest->SetVerb("GET"); + HttpRequest->ProcessRequest(); +} + +void FFileApi::FileRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { FString URL = Request->GetURL(); FString FileName; @@ -33,11 +43,24 @@ void FFileApi::FileRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Res if (bWasSuccessful && Response.IsValid() && Response->GetContentLength() > 0) { TArray Content = Response->GetContent(); - OnFileRequestComplete.ExecuteIfBound(&Content, FileName, AssetType); + OnFileRequestComplete.ExecuteIfBound(&Content, FileName); + return; + } + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load file from URL")); + OnFileRequestComplete.ExecuteIfBound(nullptr, FileName); +} + +void FFileApi::AssetFileRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FAsset Asset) +{ + + if (bWasSuccessful && Response.IsValid() && Response->GetContentLength() > 0) + { + TArray Content = Response->GetContent(); + OnAssetFileRequestComplete.ExecuteIfBound(&Content, Asset); return; } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load file from URL")); - OnFileRequestComplete.ExecuteIfBound(nullptr, FileName, AssetType); + OnAssetFileRequestComplete.ExecuteIfBound(nullptr, Asset); } bool FFileApi::LoadFileFromPath(const FString& Path, TArray& OutContent) diff --git a/Source/RpmNextGen/Private/Api/Files/GlbLoader.cpp b/Source/RpmNextGen/Private/Api/Files/GlbLoader.cpp index c971c09..2e3a61f 100644 --- a/Source/RpmNextGen/Private/Api/Files/GlbLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Files/GlbLoader.cpp @@ -24,7 +24,7 @@ FGlbLoader::~FGlbLoader() delete FileWriter; } -void FGlbLoader::HandleFileRequestComplete(TArray* Data, const FString& FileName, const FString& AssetType) +void FGlbLoader::HandleFileRequestComplete(TArray* Data, const FString& FileName) { UglTFRuntimeAsset* GltfAsset = nullptr; if (Data) @@ -32,10 +32,10 @@ void FGlbLoader::HandleFileRequestComplete(TArray* Data, const FString& F if(OnGLtfAssetLoaded.IsBound()) { GltfAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(*Data, *GltfConfig); - OnGLtfAssetLoaded.ExecuteIfBound(GltfAsset, AssetType); + OnGLtfAssetLoaded.ExecuteIfBound(GltfAsset, TEXT("AssetType")); } return; } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load GLB from URL")); - OnGLtfAssetLoaded.ExecuteIfBound(GltfAsset, AssetType); + OnGLtfAssetLoaded.ExecuteIfBound(GltfAsset, TEXT("")); } diff --git a/Source/RpmNextGen/Private/RpmActor.cpp b/Source/RpmNextGen/Private/RpmActor.cpp index 0187871..209efac 100644 --- a/Source/RpmNextGen/Private/RpmActor.cpp +++ b/Source/RpmNextGen/Private/RpmActor.cpp @@ -6,55 +6,111 @@ #include "Components/SkeletalMeshComponent.h" #include "glTFRuntimeSkeletalMeshComponent.h" #include "RpmNextGen.h" +#include "RpmCharacterTypes.h" +#include "Api/Assets/AssetApi.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; + PrimaryActorTick.bCanEverTick = false; AssetRoot = CreateDefaultSubobject(TEXT("AssetRoot")); RootComponent = AssetRoot; + MasterPoseComponent = nullptr; } -// Called when the game starts or when spawned void ARpmActor::BeginPlay() { Super::BeginPlay(); } -void ARpmActor::LoadGltfAssets(TMap GltfAssetsByType) +void ARpmActor::LoadCharacter(const FRpmCharacterData& InCharacterData, UglTFRuntimeAsset* GltfAsset) { - for (const auto& Pairs : GltfAssetsByType) + CharacterData = InCharacterData; + if(AnimationConfigsByBaseModelId.Contains(CharacterData.BaseModelId)) { - LoadGltfAsset(Pairs.Value, Pairs.Key); + AnimationConfig = AnimationConfigsByBaseModelId[CharacterData.BaseModelId]; + SkeletalMeshConfig.Skeleton = AnimationConfig.Skeleton; + SkeletalMeshConfig.SkeletonConfig.CopyRotationsFrom = AnimationConfig.Skeleton; } + LoadAsset(FAsset(), GltfAsset); } -void ARpmActor::LoadGltfAsset(UglTFRuntimeAsset* GltfAsset, const FString& AssetType) +void ARpmActor::LoadAsset(const FAsset& Asset, UglTFRuntimeAsset* GltfAsset) { if (!GltfAsset) { UE_LOG(LogGLTFRuntime, Warning, TEXT("No asset to setup")); return; } - + if(Asset.Type == FAssetApi::BaseModelType) + { + CharacterData.BaseModelId = Asset.Id; + if(AnimationConfigsByBaseModelId.Contains(CharacterData.BaseModelId)) + { + AnimationConfig = AnimationConfigsByBaseModelId[CharacterData.BaseModelId]; + SkeletalMeshConfig.Skeleton = AnimationConfig.Skeleton; + SkeletalMeshConfig.SkeletonConfig.CopyRotationsFrom = AnimationConfig.Skeleton; + } + } + RemoveMeshComponentsOfType(Asset.Type); double LoadingStartTime = FPlatformTime::Seconds(); - RemoveMeshComponentsOfType(AssetType); - - const TArray NewMeshComponents = LoadMeshComponents(GltfAsset); + const TArray NewMeshComponents = LoadMeshComponents(GltfAsset, Asset.Type); if (NewMeshComponents.Num() > 0) { - LoadedMeshComponentsByAssetType.Add(AssetType, NewMeshComponents); + LoadedMeshComponentsByAssetType.Add(Asset.Type, NewMeshComponents); + if(AnimationConfigsByBaseModelId.Contains(CharacterData.BaseModelId)) + { + // Check if MasterPoseComponent is valid before using it + if (MasterPoseComponent == nullptr) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("MasterPoseComponent is null for base model %s"), *CharacterData.BaseModelId); + return; + } + + // Check if Animation Blueprint is valid + if (!AnimationConfig.AnimationBlueprint) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("AnimationBlueprint is null for base model %s"), *CharacterData.BaseModelId); + return; + } + + MasterPoseComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint); + MasterPoseComponent->SetAnimClass(AnimationConfig.AnimationBlueprint); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Set Animation Blueprint for %s"), *CharacterData.BaseModelId); + } + + UE_LOG(LogReadyPlayerMe, Log, TEXT("Asset loaded in %f seconds"), FPlatformTime::Seconds() - LoadingStartTime); + return; } + + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load mesh components")); +} - UE_LOG(LogReadyPlayerMe, Log, TEXT("Asset loaded in %f seconds"), FPlatformTime::Seconds() - LoadingStartTime); +void ARpmActor::LoadGltfAssetWithSkeleton(UglTFRuntimeAsset* GltfAsset, const FAsset& Asset, const FRpmAnimationConfig& InAnimationCharacter) +{ + AnimationConfig = InAnimationCharacter; + SkeletalMeshConfig.Skeleton = AnimationConfig.Skeleton; + SkeletalMeshConfig.SkeletonConfig.CopyRotationsFrom = AnimationConfig.Skeleton; + LoadAsset(Asset, GltfAsset); } void ARpmActor::RemoveMeshComponentsOfType(const FString& AssetType) { - if (LoadedMeshComponentsByAssetType.Contains(AssetType)) + if (LoadedMeshComponentsByAssetType.IsEmpty()) { + UE_LOG( LogReadyPlayerMe, Log, TEXT("No mesh components to remove")); + return; + } + + // Remove components by type, or remove all if AssetType is empty or it's a new base model + if (AssetType.IsEmpty() || AssetType == FAssetApi::BaseModelType) + { + UE_LOG(LogReadyPlayerMe, Log, TEXT("Removing all mesh components")); + RemoveAllMeshes(); + } + else if (LoadedMeshComponentsByAssetType.Contains(AssetType)) + { + UE_LOG(LogReadyPlayerMe, Log, TEXT("Removing mesh components of type %s"), *AssetType); TArray& ComponentsToRemove = LoadedMeshComponentsByAssetType[AssetType]; for (USceneComponent* ComponentToRemove : ComponentsToRemove) { @@ -84,10 +140,12 @@ void ARpmActor::RemoveAllMeshes() LoadedMeshComponentsByAssetType.Empty(); } -TArray ARpmActor::LoadMeshComponents(UglTFRuntimeAsset* GltfAsset) +TArray ARpmActor::LoadMeshComponents(UglTFRuntimeAsset* GltfAsset, const FString& AssetType) { TArray AllNodes = GltfAsset->GetNodes(); TArray NewMeshComponents; + //if baseModel or full character asset changes we need to update master pose component + bool bIsMasterPoseUpdateRequired = AssetType == FAssetApi::BaseModelType || AssetType.IsEmpty(); // Loop through all nodes to create mesh components for (const FglTFRuntimeNode& Node : AllNodes) @@ -100,7 +158,17 @@ TArray ARpmActor::LoadMeshComponents(UglTFRuntimeAsset* GltfAs if (Node.SkinIndex >= 0) { - NewMeshComponents.Add(CreateSkeletalMeshComponent(GltfAsset, Node)); + USkeletalMeshComponent* SkeletalMeshComponent = CreateSkeletalMeshComponent(GltfAsset, Node); + if(bIsMasterPoseUpdateRequired) + { + UE_LOG( LogReadyPlayerMe, Log, TEXT("Setting master pose component")); + MasterPoseComponent = SkeletalMeshComponent; + NewMeshComponents.Add(SkeletalMeshComponent); + bIsMasterPoseUpdateRequired = false; + continue; + } + SkeletalMeshComponent->SetMasterPoseComponent(MasterPoseComponent.Get()); + NewMeshComponents.Add(SkeletalMeshComponent); } else { diff --git a/Source/RpmNextGen/Private/RpmLoaderComponent.cpp b/Source/RpmNextGen/Private/RpmLoaderComponent.cpp index 8ed92f7..65a19db 100644 --- a/Source/RpmNextGen/Private/RpmLoaderComponent.cpp +++ b/Source/RpmNextGen/Private/RpmLoaderComponent.cpp @@ -20,26 +20,21 @@ URpmLoaderComponent::URpmLoaderComponent() PrimaryComponentTick.bCanEverTick = false; const URpmDeveloperSettings* RpmSettings = GetDefault(); AppId = RpmSettings->ApplicationId; - if(GltfConfig == nullptr) - { - GltfConfig = new FglTFRuntimeConfig(); - } - GlbLoader = MakeShared(GltfConfig); - GlbLoader->OnGLtfAssetLoaded.BindUObject(this, &URpmLoaderComponent::HandleGltfAssetLoaded); - + FileApi = MakeShared(); + FileApi->OnAssetFileRequestComplete.BindUObject(this, &URpmLoaderComponent::HandleAssetLoaded); + FileApi->OnFileRequestComplete.BindUObject(this, &URpmLoaderComponent::HandleCharacterAssetLoaded); CharacterApi = MakeShared(); CharacterApi->OnCharacterCreateResponse.BindUObject(this, &URpmLoaderComponent::HandleCharacterCreateResponse); CharacterApi->OnCharacterUpdateResponse.BindUObject(this, &URpmLoaderComponent::HandleCharacterUpdateResponse); CharacterApi->OnCharacterFindResponse.BindUObject(this, &URpmLoaderComponent::HandleCharacterFindResponse); CharacterData = FRpmCharacterData(); + GltfConfig = FglTFRuntimeConfig(); + GltfConfig.TransformBaseType = EglTFRuntimeTransformBaseType::YForward; } -void URpmLoaderComponent::SetGltfConfig(FglTFRuntimeConfig* Config) const +void URpmLoaderComponent::SetGltfConfig(const FglTFRuntimeConfig* Config) { - if(GlbLoader) - { - GlbLoader->SetConfig(Config); - } + GltfConfig = *Config; } void URpmLoaderComponent::BeginPlay() @@ -61,8 +56,12 @@ void URpmLoaderComponent::CreateCharacter(const FString& BaseModelId, bool bUseC TArray Data; if(FFileApi::LoadFileFromPath(CachedAssetData.GetGlbPathForBaseModelId(CharacterData.BaseModelId), Data)) { - UglTFRuntimeAsset* GltfRuntimeAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(Data, *GltfConfig); - HandleGltfAssetLoaded(GltfRuntimeAsset, FAssetApi::BaseModelType); + UglTFRuntimeAsset* GltfRuntimeAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(Data, GltfConfig); + if(!GltfRuntimeAsset) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load gltf asset")); + } + OnCharacterAssetLoaded.Broadcast(CharacterData, GltfRuntimeAsset); } return; } @@ -78,10 +77,10 @@ void URpmLoaderComponent::CreateCharacter(const FString& BaseModelId, bool bUseC void URpmLoaderComponent::LoadCharacterFromUrl(FString Url) { - GlbLoader->LoadFileFromUrl(Url); + FileApi->LoadFileFromUrl(Url); } -UglTFRuntimeAsset* URpmLoaderComponent::LoadGltfRuntimeAssetFromCache(const FAsset& Asset) +void URpmLoaderComponent::LoadGltfRuntimeAssetFromCache(const FAsset& Asset) { FCachedAssetData ExistingAsset; if(FAssetCacheManager::Get().GetCachedAsset(Asset.Id, ExistingAsset)) @@ -90,12 +89,16 @@ UglTFRuntimeAsset* URpmLoaderComponent::LoadGltfRuntimeAssetFromCache(const FAss TArray Data; if(FFileApi::LoadFileFromPath(ExistingAsset.GetGlbPathForBaseModelId(CharacterData.BaseModelId), Data)) { - UglTFRuntimeAsset* GltfRuntimeAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(Data, *GltfConfig); - return GltfRuntimeAsset; + UglTFRuntimeAsset* GltfRuntimeAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(Data, GltfConfig); + if(!GltfRuntimeAsset) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load gltf asset")); + } + OnNewAssetLoaded.Broadcast(Asset, GltfRuntimeAsset); + return; } } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load gltf asset from cache")); - return nullptr; } void URpmLoaderComponent::LoadCharacterFromAssetMapCache(TMap AssetMap) @@ -114,7 +117,7 @@ void URpmLoaderComponent::LoadCharacterFromAssetMapCache(TMap A LoadCharacterFromUrl(Url); } -void URpmLoaderComponent::LoadAssetsWithNewStyle() +void URpmLoaderComponent::LoadAssetsFromCacheWithNewStyle() { for (auto PreviewAssets : CharacterData.Assets) { @@ -122,8 +125,7 @@ void URpmLoaderComponent::LoadAssetsWithNewStyle() { continue; } - UglTFRuntimeAsset* GltfAsset = LoadGltfRuntimeAssetFromCache(PreviewAssets.Value); - HandleGltfAssetLoaded(GltfAsset, PreviewAssets.Value.Type); + LoadGltfRuntimeAssetFromCache(PreviewAssets.Value); } } @@ -144,13 +146,11 @@ void URpmLoaderComponent::LoadAssetPreview(FAsset AssetData, bool bUseCache) if(!FConnectionManager::Get().IsConnected() || bUseCache) { + LoadGltfRuntimeAssetFromCache(AssetData); if(bIsBaseModel && CharacterData.Assets.Num() > 1) { - LoadAssetsWithNewStyle(); + LoadAssetsFromCacheWithNewStyle(); } - - UglTFRuntimeAsset* GltfAsset = LoadGltfRuntimeAssetFromCache(AssetData); - HandleGltfAssetLoaded(GltfAsset, AssetData.Type); return; } TMap ParamAssets; @@ -163,20 +163,31 @@ void URpmLoaderComponent::LoadAssetPreview(FAsset AssetData, bool bUseCache) PreviewRequest.Params.Assets = ParamAssets; const FString& Url = CharacterApi->GeneratePreviewUrl(PreviewRequest); - LoadCharacterFromUrl(Url); + FileApi->LoadAssetFileFromUrl(Url, AssetData); +} + +void URpmLoaderComponent::HandleAssetLoaded(TArray* Data, const FAsset& Asset) +{ + UglTFRuntimeAsset* GltfRuntimeAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(*Data, GltfConfig); + if(!GltfRuntimeAsset) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load gltf asset")); + } + OnNewAssetLoaded.Broadcast(Asset, GltfRuntimeAsset); } -void URpmLoaderComponent::HandleGltfAssetLoaded(UglTFRuntimeAsset* gltfRuntimeAsset, const FString& AssetType) +void URpmLoaderComponent::HandleCharacterAssetLoaded(TArray* Data, const FString& FileName) { - if(!gltfRuntimeAsset) + UglTFRuntimeAsset* GltfRuntimeAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(*Data, GltfConfig); + if(!GltfRuntimeAsset) { UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load gltf asset")); } - OnGltfAssetLoaded.Broadcast(gltfRuntimeAsset, AssetType); + OnCharacterAssetLoaded.Broadcast(CharacterData, GltfRuntimeAsset); } void URpmLoaderComponent::HandleCharacterCreateResponse(FCharacterCreateResponse CharacterCreateResponse, - bool bWasSuccessful) + bool bWasSuccessful) { CharacterData.Id = CharacterCreateResponse.Data.Id; OnCharacterCreated.Broadcast(CharacterData); diff --git a/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp index cbb5ccb..40d85fd 100644 --- a/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp +++ b/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp @@ -27,8 +27,6 @@ void URpmAssetPanelWidget::NativeConstruct() } AssetApi->OnListAssetsResponse.BindUObject(this, &URpmAssetPanelWidget::OnAssetListResponse); - ButtonSize = FVector2D(200, 200); - ImageSize = FVector2D(200, 200); } void URpmAssetPanelWidget::OnAssetListResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful) @@ -52,11 +50,15 @@ void URpmAssetPanelWidget::LoadAssetsFromCache(const FString& AssetType) void URpmAssetPanelWidget::CreateButtonsFromAssets(TArray Assets) { + if(Assets.Num() < 1) + { + UE_LOG(LogReadyPlayerMe, Warning, TEXT("No assets found") ); + return; + } for (auto Asset : Assets) { CreateButton(Asset); } - UE_LOG(LogReadyPlayerMe, Warning, TEXT("No assets found") ); } void URpmAssetPanelWidget::ClearAllButtons() @@ -81,9 +83,7 @@ void URpmAssetPanelWidget::CreateButton(const FAsset& AssetData) { if (AssetButtonBlueprint) { - UWorld* World = GetWorld(); - - if (World) + if (UWorld* World = GetWorld()) { if (URpmAssetButtonWidget* AssetButtonInstance = CreateWidget(World, AssetButtonBlueprint)) { @@ -108,6 +108,11 @@ void URpmAssetPanelWidget::CreateButton(const FAsset& AssetData) } } +void URpmAssetPanelWidget::SynchronizeProperties() +{ + Super::SynchronizeProperties(); +} + void URpmAssetPanelWidget::OnAssetButtonClicked(const URpmAssetButtonWidget* AssetButton) { UpdateSelectedButton(const_cast(AssetButton)); diff --git a/Source/RpmNextGen/Private/Samples/RpmCategoryButtonWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmCategoryButtonWidget.cpp index 119bd8c..f0c5d00 100644 --- a/Source/RpmNextGen/Private/Samples/RpmCategoryButtonWidget.cpp +++ b/Source/RpmNextGen/Private/Samples/RpmCategoryButtonWidget.cpp @@ -19,14 +19,20 @@ void URpmCategoryButtonWidget::NativeConstruct() } } -void URpmCategoryButtonWidget::InitializeButton(FString Category, UTexture2D* Image) +void URpmCategoryButtonWidget::InitializeButton(FString Category, UTexture2D* Image, const FVector2D& InImageSize) { AssetCategoryName = Category; if (CategoryImage) - { - CategoryImageTexture = Image; - CategoryImage->SetBrushFromTexture(CategoryImageTexture); + { + CategoryImage->SetDesiredSizeOverride(InImageSize); + + if(Image) + { + CategoryImageTexture = Image; + CategoryImage->SetBrushFromTexture(CategoryImageTexture); + UE_LOG( LogTemp, Warning, TEXT("Setting image on button for Category: %s"), *Category ); + } } } diff --git a/Source/RpmNextGen/Private/Samples/RpmCategoryPanelWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmCategoryPanelWidget.cpp index b25abcd..70feb1e 100644 --- a/Source/RpmNextGen/Private/Samples/RpmCategoryPanelWidget.cpp +++ b/Source/RpmNextGen/Private/Samples/RpmCategoryPanelWidget.cpp @@ -1,28 +1,22 @@ // Fill out your copyright notice in the Description page of Project Settings. #include "Samples/RpmCategoryPanelWidget.h" + +#include "Api/Assets/AssetApi.h" +#include "Api/Assets/Models/AssetTypeListRequest.h" #include "Blueprint/WidgetTree.h" +#include "Components/SizeBox.h" #include "Samples/RpmCategoryButtonWidget.h" +#include "Settings/RpmDeveloperSettings.h" + +class URpmDeveloperSettings; 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); - } - } + AssetApi = MakeShared(); + AssetApi->OnListAssetTypeResponse.BindUObject(this, &URpmCategoryPanelWidget::AssetTypesLoaded); + AssetButtons = TArray>(); } void URpmCategoryPanelWidget::UpdateSelectedButton(URpmCategoryButtonWidget* CategoryButton) @@ -34,9 +28,72 @@ void URpmCategoryPanelWidget::UpdateSelectedButton(URpmCategoryButtonWidget* Cat SelectedCategoryButton = CategoryButton; } +void URpmCategoryPanelWidget::LoadAndCreateButtons() +{ + URpmDeveloperSettings* Settings = GetMutableDefault(); + FAssetTypeListRequest AssetListRequest; + FAssetTypeListQueryParams QueryParams = FAssetTypeListQueryParams(); + QueryParams.ApplicationId = Settings->ApplicationId; + AssetListRequest.Params = QueryParams; + AssetApi->ListAssetTypesAsync(AssetListRequest); +} + void URpmCategoryPanelWidget::OnCategoryButtonClicked(URpmCategoryButtonWidget* CategoryButton) { UpdateSelectedButton(CategoryButton); OnCategorySelected.Broadcast(CategoryButton->AssetCategoryName); } +void URpmCategoryPanelWidget::CreateButton(const FString& AssetType) +{ + if (UWorld* World = GetWorld()) + { + URpmCategoryButtonWidget* CategoryButton = CreateWidget(World, CategoryButtonBlueprint); + UTexture2D* ButtonTexture = nullptr; + const auto CleanAssetType = AssetType.Replace(TEXT(" "), TEXT("-")); + if(UObject* LoadedAsset = StaticLoadObject(UTexture2D::StaticClass(), nullptr, *FString::Printf(TEXT("/RpmNextGen/Samples/Icons/T-rpm-%s"), *CleanAssetType))) + { + ButtonTexture = Cast(LoadedAsset); + } + if (CategoryButton) + { + + USizeBox* ButtonSizeBox = NewObject(this); + if (ButtonSizeBox && ButtonContainer) + { + ButtonSizeBox->SetWidthOverride(ButtonSize.X); + ButtonSizeBox->SetHeightOverride(ButtonSize.Y); + ButtonSizeBox->AddChild(CategoryButton); + ButtonContainer->AddChild(ButtonSizeBox); + CategoryButton->InitializeButton(AssetType, ButtonTexture, ButtonSize); + CategoryButton->OnCategoryClicked.AddDynamic(this, &URpmCategoryPanelWidget::OnCategoryButtonClicked); + AssetButtons.Add(CategoryButton->GetClass()); + return; + } + UE_LOG(LogTemp, Error, TEXT("Failed to Load %s button"), *AssetType); + } + } +} + +void URpmCategoryPanelWidget::SynchronizeProperties() +{ + Super::SynchronizeProperties(); +} + +void URpmCategoryPanelWidget::AssetTypesLoaded(const FAssetTypeListResponse& AssetTypeListResponse, bool bWasSuccessful) +{ + if(bWasSuccessful && ButtonContainer) + { + ButtonContainer->ClearChildren(); + + for (const FString& AssetType : AssetTypeListResponse.Data) + { + CreateButton(AssetType); + } + OnCategoriesLoaded.Broadcast(AssetTypeListResponse.Data); + return; + } + UE_LOG(LogTemp, Error, TEXT("Failed to load asset types")); + OnCategoriesLoaded.Broadcast(TArray()); +} + diff --git a/Source/RpmNextGen/Private/Samples/RpmCreatorWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmCreatorWidget.cpp new file mode 100644 index 0000000..5e69eab --- /dev/null +++ b/Source/RpmNextGen/Private/Samples/RpmCreatorWidget.cpp @@ -0,0 +1,89 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "Samples/RpmCreatorWidget.h" +#include "Blueprint/WidgetTree.h" +#include "Components/VerticalBox.h" +#include "Components/WidgetSwitcher.h" +#include "Samples/RpmAssetPanelWidget.h" + +class UVerticalBox; + +void URpmCreatorWidget::NativeConstruct() +{ + Super::NativeConstruct(); + IndexMapByCategory = TMap(); +} + +void URpmCreatorWidget::CreateAssetPanelsFromCategories(const TArray& CategoryArray) +{ + if (!AssetPanelSwitcher || !AssetPanelBlueprint) + { + UE_LOG(LogTemp, Error, TEXT("WidgetSwitcher or WidgetBlueprintClass is not set!")); + return; + } + + AssetPanelSwitcher->ClearChildren(); + + IndexMapByCategory.Empty(); + for (int i = 0; i < CategoryArray.Num(); ++i) + { + CreateAssetPanel(CategoryArray[i]); + IndexMapByCategory.Add(CategoryArray[i], i ); + } + SwitchToPanel(CategoryArray[0]); +} + +void URpmCreatorWidget::SwitchToPanel(const FString& Category) +{ + if(AssetPanelSwitcher) + { + if(IndexMapByCategory[Category] == -1) + { + UE_LOG(LogTemp, Error, TEXT("Category %s not found!"), *Category); + return; + } + AssetPanelSwitcher->SetActiveWidgetIndex(IndexMapByCategory[Category]); + } +} + +void URpmCreatorWidget::SynchronizeProperties() +{ + Super::SynchronizeProperties(); +} + +void URpmCreatorWidget::HandleAssetSelectedFromPanel(const FAsset& AssetData) +{ + OnAssetSelected.Broadcast(AssetData); +} + +UUserWidget* URpmCreatorWidget::CreateAssetPanel(const FString& Category) +{ + if (!AssetPanelBlueprint) + { + UE_LOG(LogTemp, Error, TEXT("WidgetBlueprintClass is not set!")); + return nullptr; + } + + UWorld* World = GetWorld(); + if (!World) + { + UE_LOG(LogTemp, Error, TEXT("World is null!")); + return nullptr; + } + + URpmAssetPanelWidget* AssetPanelWidget = CreateWidget(World, AssetPanelBlueprint); + + if (!AssetPanelWidget) + { + UE_LOG(LogTemp, Error, TEXT("Failed to create widget from blueprint class!")); + return nullptr; + } + AssetPanelSwitcher->AddChild(AssetPanelWidget); + AssetPanelWidget->Rename(*Category); + AssetPanelWidget->SetCategoryName(Category); + AssetPanelWidget->ButtonSize = FVector2D(200, 200); + AssetPanelWidget->ImageSize = FVector2D(200, 200); + AssetPanelWidget->OnAssetSelected.AddDynamic(this, &URpmCreatorWidget::HandleAssetSelectedFromPanel); + AssetPanelWidget->LoadAssetsOfType(Category); + return AssetPanelWidget; +} diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetApi.h b/Source/RpmNextGen/Public/Api/Assets/AssetApi.h index 24eebc5..674e7df 100644 --- a/Source/RpmNextGen/Public/Api/Assets/AssetApi.h +++ b/Source/RpmNextGen/Public/Api/Assets/AssetApi.h @@ -12,13 +12,17 @@ DECLARE_DELEGATE_TwoParams(FOnListAssetTypeResponse, const FAssetTypeListRespons class RPMNEXTGEN_API FAssetApi : public FWebApiWithAuth { public: + static const FString BaseModelType; + + FOnListAssetsResponse OnListAssetsResponse; + FOnListAssetTypeResponse OnListAssetTypeResponse; + FAssetApi(); void ListAssetsAsync(const FAssetListRequest& Request); void ListAssetTypesAsync(const FAssetTypeListRequest& Request); - FOnListAssetsResponse OnListAssetsResponse; - FOnListAssetTypeResponse OnListAssetTypeResponse; - static const FString BaseModelType; + private: - void HandleResponse(FString Response, bool bWasSuccessful); FString ApiBaseUrl; + + void HandleResponse(FString Response, bool bWasSuccessful); }; diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetGlbLoader.h b/Source/RpmNextGen/Public/Api/Assets/AssetGlbLoader.h index c282fd3..8aea007 100644 --- a/Source/RpmNextGen/Public/Api/Assets/AssetGlbLoader.h +++ b/Source/RpmNextGen/Public/Api/Assets/AssetGlbLoader.h @@ -13,17 +13,16 @@ class RPMNEXTGEN_API FAssetGlbLoader : public TSharedFromThis { public: DECLARE_DELEGATE_TwoParams(FOnGlbLoaded, const FAsset&, const TArray&); + + FOnGlbLoaded OnGlbLoaded; FAssetGlbLoader(); virtual ~FAssetGlbLoader(); void LoadGlb(const FAsset& Asset, const FString& BaseModelId, bool bStoreInCache); - - FOnGlbLoaded OnGlbLoaded; private: - void LoadGlb(TSharedRef Context); - void GlbLoaded(TSharedPtr Response, bool bWasSuccessful, const TSharedRef& Context); - FHttpModule* Http; + void LoadGlb(TSharedRef Context); + void GlbLoaded(TSharedPtr Response, bool bWasSuccessful, const TSharedRef& Context); }; diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetIconLoader.h b/Source/RpmNextGen/Public/Api/Assets/AssetIconLoader.h index 3524be1..25c2cef 100644 --- a/Source/RpmNextGen/Public/Api/Assets/AssetIconLoader.h +++ b/Source/RpmNextGen/Public/Api/Assets/AssetIconLoader.h @@ -13,19 +13,19 @@ class RPMNEXTGEN_API FAssetIconLoader : public TSharedFromThis { public: DECLARE_DELEGATE_TwoParams(FOnIconLoaded, const FAsset&, const TArray&); - + + FOnIconLoaded OnIconLoaded; + FAssetIconLoader(); virtual ~FAssetIconLoader(); void LoadIcon(const FAsset& Asset, bool bStoreInCache); - FOnIconLoaded OnIconLoaded; - private: - void LoadIcon(TSharedRef Context); + FHttpModule* Http; + UFUNCTION() void IconLoaded(TSharedPtr Response, bool bWasSuccessful, const TSharedRef& Context); - FHttpModule* Http; - + void LoadIcon(TSharedRef Context); }; diff --git a/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h b/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h index b1735e7..9609248 100644 --- a/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h +++ b/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h @@ -28,4 +28,17 @@ struct RPMNEXTGEN_API FAsset UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "updatedAt")) FDateTime UpdatedAt; + +private: + UPROPERTY(meta = (JsonIgnore)) + FString organizationId = ""; + + UPROPERTY(meta = (JsonIgnore)) + FString cageMeshId = ""; + + UPROPERTY(meta = (JsonIgnore)) + bool active = false; + + UPROPERTY(meta = (JsonIgnore)) + FString _id = ""; }; \ 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 index 7cd963d..85d11bf 100644 --- a/Source/RpmNextGen/Public/Api/Assets/Models/AssetListRequest.h +++ b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListRequest.h @@ -54,7 +54,8 @@ inline FString FAssetListRequest::BuildQueryString() const } if (!Params.Type.IsEmpty()) { - QueryString += TEXT("type=") + Params.Type + TEXT("&"); + auto CleanType = Params.Type.Replace(TEXT(" "), TEXT("%20")); + QueryString += TEXT("type=") + CleanType + TEXT("&"); } if (!Params.ExcludeTypes.IsEmpty()) { diff --git a/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h index e6bfa81..3b429d4 100644 --- a/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h +++ b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListResponse.h @@ -3,6 +3,7 @@ #include "CoreMinimal.h" #include "Api/Assets/Models/Asset.h" #include "Api/Common/Models/ApiResponse.h" +#include "Api/Common/Models/Pagination.h" #include "AssetListResponse.generated.h" USTRUCT(BlueprintType) @@ -13,4 +14,6 @@ struct RPMNEXTGEN_API FAssetListResponse : public FApiResponse UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "data")) TArray Data; + UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "pagination") ) + FPagination Pagination; }; diff --git a/Source/RpmNextGen/Public/Api/Auth/ApiRequest.h b/Source/RpmNextGen/Public/Api/Auth/ApiRequest.h index 45ba306..7e8da68 100644 --- a/Source/RpmNextGen/Public/Api/Auth/ApiRequest.h +++ b/Source/RpmNextGen/Public/Api/Auth/ApiRequest.h @@ -13,7 +13,6 @@ enum ERequestMethod { PATCH }; - USTRUCT(BlueprintType) struct RPMNEXTGEN_API FApiRequest { diff --git a/Source/RpmNextGen/Public/Api/Auth/AuthApi.h b/Source/RpmNextGen/Public/Api/Auth/AuthApi.h index 8093000..228ec44 100644 --- a/Source/RpmNextGen/Public/Api/Auth/AuthApi.h +++ b/Source/RpmNextGen/Public/Api/Auth/AuthApi.h @@ -11,9 +11,11 @@ DECLARE_DELEGATE_TwoParams(FOnRefreshTokenResponse, const FRefreshTokenResponse& class RPMNEXTGEN_API FAuthApi : public FWebApi { public: + FOnRefreshTokenResponse OnRefreshTokenResponse; + FAuthApi(); void RefreshToken(const FRefreshTokenRequest& Request); - FOnRefreshTokenResponse OnRefreshTokenResponse; + virtual void OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) override; private: diff --git a/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h b/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h index d7d001a..cfca9a5 100644 --- a/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h +++ b/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h @@ -19,26 +19,26 @@ DECLARE_DELEGATE_TwoParams(FOnCharacterFindResponse, FCharacterFindByIdResponse, class RPMNEXTGEN_API FCharacterApi : public TSharedFromThis, public FWebApiWithAuth { public: + FOnWebApiResponse OnApiResponse; + FOnCharacterCreateResponse OnCharacterCreateResponse; + FOnCharacterUpdatResponse OnCharacterUpdateResponse; + FOnCharacterFindResponse OnCharacterFindResponse; + 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; + FHttpModule* Http; template FString ConvertToJsonString(const T& Data); - FHttpModule* Http; + virtual void OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) override; private: FString BaseUrl; diff --git a/Source/RpmNextGen/Public/Api/Common/Models/Pagination.h b/Source/RpmNextGen/Public/Api/Common/Models/Pagination.h new file mode 100644 index 0000000..c2e18ff --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Common/Models/Pagination.h @@ -0,0 +1,37 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Pagination.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FPagination +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "totalDocs")) + int TotalDocs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "limit")) + int Limit = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "totalPages")) + int TotalPages = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "page")) + int Page = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "pagingCounter")) + int PagingCounter = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "hasPrevPage")) + bool HasPrevPage = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "hasNextPage")) + bool HasNextPage = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "prevPage")) + int PrevPage = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "nextPage")) + int NextPage = 0; +}; diff --git a/Source/RpmNextGen/Public/Api/Common/WebApi.h b/Source/RpmNextGen/Public/Api/Common/WebApi.h index f611615..c84d7c0 100644 --- a/Source/RpmNextGen/Public/Api/Common/WebApi.h +++ b/Source/RpmNextGen/Public/Api/Common/WebApi.h @@ -12,24 +12,25 @@ DECLARE_DELEGATE_TwoParams(FOnWebApiResponse, FString, bool); class RPMNEXTGEN_API FWebApi { public: + FOnWebApiResponse OnApiResponse; + FWebApi(); virtual ~FWebApi(); - FOnWebApiResponse OnApiResponse; - protected: + FHttpModule* Http; + 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); + + virtual void OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); - FHttpModule* Http; }; template diff --git a/Source/RpmNextGen/Public/Api/Files/FileApi.h b/Source/RpmNextGen/Public/Api/Files/FileApi.h index d6b5d16..8df4b05 100644 --- a/Source/RpmNextGen/Public/Api/Files/FileApi.h +++ b/Source/RpmNextGen/Public/Api/Files/FileApi.h @@ -1,19 +1,23 @@ #pragma once #include "CoreMinimal.h" -#include "RpmNextGen.h" #include "Interfaces/IHttpRequest.h" -DECLARE_DELEGATE_ThreeParams(FOnFileRequestComplete, TArray*, const FString&, const FString&); +struct FAsset; +DECLARE_DELEGATE_TwoParams(FOnFileRequestComplete, TArray*, const FString&); +DECLARE_DELEGATE_TwoParams(FOnAssetFileRequestComplete, TArray*, const FAsset&); class RPMNEXTGEN_API FFileApi : public TSharedFromThis { public: + FOnAssetFileRequestComplete OnAssetFileRequestComplete; + FOnFileRequestComplete OnFileRequestComplete; + FFileApi(); virtual ~FFileApi(); - virtual void LoadFileFromUrl(const FString& URL, const FString& AssetType = TEXT("")); - virtual void FileRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FString AssetType); + virtual void LoadFileFromUrl(const FString& URL); + virtual void LoadAssetFileFromUrl(const FString& URL, FAsset Asset); + virtual void FileRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); + virtual void AssetFileRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FAsset Asset); static bool LoadFileFromPath(const FString& Path, TArray& OutContent); - - FOnFileRequestComplete OnFileRequestComplete; }; diff --git a/Source/RpmNextGen/Public/Api/Files/GlbLoader.h b/Source/RpmNextGen/Public/Api/Files/GlbLoader.h index 8ed1d20..68e9a45 100644 --- a/Source/RpmNextGen/Public/Api/Files/GlbLoader.h +++ b/Source/RpmNextGen/Public/Api/Files/GlbLoader.h @@ -13,6 +13,8 @@ DECLARE_DELEGATE_TwoParams(FOnGlbDownloaded, UglTFRuntimeAsset*, const FString&) class RPMNEXTGEN_API FGlbLoader : public FFileApi { public: + FOnGlbDownloaded OnGLtfAssetLoaded; + FGlbLoader(); FGlbLoader(FglTFRuntimeConfig* Config); @@ -23,12 +25,11 @@ class RPMNEXTGEN_API FGlbLoader : public FFileApi GltfConfig = Config; } - FOnGlbDownloaded OnGLtfAssetLoaded; - protected: FglTFRuntimeConfig* GltfConfig; - UFUNCTION() - virtual void HandleFileRequestComplete(TArray* Data, const FString& String, const FString& AssetType); FFileUtility* FileWriter; FString DownloadDirectory; + + UFUNCTION() + virtual void HandleFileRequestComplete(TArray* Data, const FString& String); }; diff --git a/Source/RpmNextGen/Public/Api/Files/PakFileUtility.h b/Source/RpmNextGen/Public/Api/Files/PakFileUtility.h index c2c3f39..e273921 100644 --- a/Source/RpmNextGen/Public/Api/Files/PakFileUtility.h +++ b/Source/RpmNextGen/Public/Api/Files/PakFileUtility.h @@ -5,10 +5,12 @@ class RPMNEXTGEN_API FPakFileUtility { public: + static const FString CachePakFilePath; + static void CreatePakFile(); static void ExtractPakFile(const FString& PakFilePath); static void ExtractFilesFromPak(const FString& PakFilePath); - static const FString CachePakFilePath; + private: static void CreatePakFile(const FString& PakFilePath); static void GeneratePakResponseFile(); diff --git a/Source/RpmNextGen/Public/Cache/AssetCacheManager.h b/Source/RpmNextGen/Public/Cache/AssetCacheManager.h index 4ec0683..348e7fc 100644 --- a/Source/RpmNextGen/Public/Cache/AssetCacheManager.h +++ b/Source/RpmNextGen/Public/Cache/AssetCacheManager.h @@ -237,6 +237,8 @@ class FAssetCacheManager } private: + TMap StoredAssets; + FAssetCacheManager() { LoadManifest(); @@ -244,15 +246,14 @@ class FAssetCacheManager template void MergeTMaps(TMap& DestinationMap, const TMap& SourceMap) + { + for (const TPair& Elem : SourceMap) { - for (const TPair& Elem : SourceMap) + // Add only if the key doesn't already exist in the destination map + if (!DestinationMap.Contains(Elem.Key)) { - // Add only if the key doesn't already exist in the destination map - if (!DestinationMap.Contains(Elem.Key)) - { - DestinationMap.Add(Elem.Key, Elem.Value); - } + DestinationMap.Add(Elem.Key, Elem.Value); } } - TMap StoredAssets; + } }; diff --git a/Source/RpmNextGen/Public/Cache/CacheGenerator.h b/Source/RpmNextGen/Public/Cache/CacheGenerator.h index 6b75398..b2aef82 100644 --- a/Source/RpmNextGen/Public/Cache/CacheGenerator.h +++ b/Source/RpmNextGen/Public/Cache/CacheGenerator.h @@ -19,47 +19,52 @@ DECLARE_DELEGATE_OneParam(FOnDownloadRemoteCache, bool); class RPMNEXTGEN_API FCacheGenerator : public TSharedFromThis { public: - virtual ~FCacheGenerator() = default; - FCacheGenerator(); - void DownloadRemoteCacheFromUrl(const FString& Url); - void GenerateLocalCache(int InItemsPerCategory); - void ExtractCache(); - FOnDownloadRemoteCache OnDownloadRemoteCacheDelegate; FOnCacheDataLoaded OnCacheDataLoaded; FOnLocalCacheGenerated OnLocalCacheGenerated; + FCacheGenerator(); + virtual ~FCacheGenerator() = default; + + void DownloadRemoteCacheFromUrl(const FString& Url); + void GenerateLocalCache(int InItemsPerCategory); + void ExtractCache(); void LoadAndStoreAssets(); void LoadAndStoreAssetGlb(const FString& BaseModelId, const FAsset* Asset); void LoadAndStoreAssetIcon(const FString& BaseModelId, const FAsset* Asset); - void Reset(); protected: - void FetchBaseModels() const; - void FetchAssetTypes() const; - - virtual void OnDownloadRemoteCacheComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful); + TUniquePtr AssetApi; + TArray AssetTypes; + TMap> AssetMapByBaseModelId; + TArray AssetListRequests; + int32 CurrentBaseModelIndex; + UFUNCTION() void OnAssetGlbSaved(const FAsset& Asset, const TArray& Data); UFUNCTION() void OnAssetIconSaved(const FAsset& Asset, const TArray& Data); + + void FetchBaseModels() const; + void FetchAssetTypes() const; void AddFolderToNonAssetDirectory() const; void FetchNextRefittedAsset(); - TUniquePtr AssetApi; - TArray AssetTypes; - TMap> AssetMapByBaseModelId; - TArray AssetListRequests; - int32 CurrentBaseModelIndex; + virtual void OnDownloadRemoteCacheComplete(TSharedPtr Request, TSharedPtr Response, bool bWasSuccessful); + private: - void OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful); - void StartFetchingRefittedAssets(); - void OnListAssetTypesResponse(const FAssetTypeListResponse& AssetTypeListResponse, bool bWasSuccessful); static const FString ZipFileName; + + FHttpModule* Http; + int MaxItemsPerCategory; int RefittedAssetRequestsCompleted = 0; int RequiredAssetDownloadRequest = 0; int NumberOfAssetsSaved = 0; - FHttpModule* Http; + + void StartFetchingRefittedAssets(); + void OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful); + void OnListAssetTypesResponse(const FAssetTypeListResponse& AssetTypeListResponse, bool bWasSuccessful); + }; diff --git a/Source/RpmNextGen/Public/RpmActor.h b/Source/RpmNextGen/Public/RpmActor.h index dc14e71..64abd6d 100644 --- a/Source/RpmNextGen/Public/RpmActor.h +++ b/Source/RpmNextGen/Public/RpmActor.h @@ -5,6 +5,8 @@ #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "glTFRuntimeAsset.h" +#include "RpmCharacterTypes.h" +#include "RpmLoaderComponent.h" #include "RpmActor.generated.h" UCLASS() @@ -14,42 +16,56 @@ class RPMNEXTGEN_API ARpmActor : public AActor public: ARpmActor(); - -protected: - virtual void BeginPlay() override; - - template - FName GetSafeNodeName(const FglTFRuntimeNode& Node) - { - return MakeUniqueObjectName(this, T::StaticClass(), *Node.Name); - } - -public: - virtual void Tick(float DeltaTime) override; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (ExposeOnSpawn = true), Category = "Ready Player Me") + FRpmAnimationConfig AnimationConfig; UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (ExposeOnSpawn = true), Category = "Ready Player Me") + TMap AnimationConfigsByBaseModelId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (ExposeOnSpawn = true), Category = "Ready Player Me|Glb Import Settings") FglTFRuntimeStaticMeshConfig StaticMeshConfig; - UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (ExposeOnSpawn = true), Category = "Ready Player Me") + UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (ExposeOnSpawn = true), Category = "Ready Player Me|Glb Import Settings") FglTFRuntimeSkeletalMeshConfig SkeletalMeshConfig; + FRpmCharacterData CharacterData; + + UFUNCTION(BlueprintCallable, Category = "Ready Player Me") + virtual void LoadCharacter(const FRpmCharacterData& InCharacterData, UglTFRuntimeAsset* GltfAsset); + UFUNCTION(BlueprintCallable, Category = "Ready Player Me") - virtual void LoadGltfAssets(TMap GltfAssetsByType); + virtual void LoadAsset(const FAsset& Asset, UglTFRuntimeAsset* GltfAsset ); UFUNCTION(BlueprintCallable, Category = "Ready Player Me") - virtual void LoadGltfAsset(UglTFRuntimeAsset* GltfAsset, const FString& AssetType = TEXT("")); + virtual void LoadGltfAssetWithSkeleton(UglTFRuntimeAsset* GltfAsset, const FAsset& Asset, const FRpmAnimationConfig& InAnimationCharacter); UFUNCTION(BlueprintCallable, Category = "Ready Player Me") void RemoveAllMeshes(); UFUNCTION(BlueprintCallable, Category = "Ready Player Me") void RemoveMeshComponentsOfType(const FString& AssetType); + + virtual void Tick(float DeltaTime) override; + +protected: + TWeakObjectPtr MasterPoseComponent; + + virtual void BeginPlay() override; + + template + FName GetSafeNodeName(const FglTFRuntimeNode& Node) + { + return MakeUniqueObjectName(this, T::StaticClass(), *Node.Name); + } private: - TArray LoadMeshComponents(UglTFRuntimeAsset* GltfAsset); - USkeletalMeshComponent* CreateSkeletalMeshComponent(UglTFRuntimeAsset* GltfAsset, const FglTFRuntimeNode& Node); - UStaticMeshComponent* CreateStaticMeshComponent(UglTFRuntimeAsset* GltfAsset, const FglTFRuntimeNode& Node); UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"), Category="Ready Player Me") USceneComponent* AssetRoot; + TMap> LoadedMeshComponentsByAssetType; + + TArray LoadMeshComponents(UglTFRuntimeAsset* GltfAsset, const FString& AssetType); + USkeletalMeshComponent* CreateSkeletalMeshComponent(UglTFRuntimeAsset* GltfAsset, const FglTFRuntimeNode& Node); + UStaticMeshComponent* CreateStaticMeshComponent(UglTFRuntimeAsset* GltfAsset, const FglTFRuntimeNode& Node); + }; diff --git a/Source/RpmNextGen/Public/RpmCharacterTypes.h b/Source/RpmNextGen/Public/RpmCharacterTypes.h new file mode 100644 index 0000000..fd4702f --- /dev/null +++ b/Source/RpmNextGen/Public/RpmCharacterTypes.h @@ -0,0 +1,46 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Api/Assets/Models/Asset.h" +#include "Components/ActorComponent.h" +#include "RpmCharacterTypes.generated.h" + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FRpmAnimationConfig +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me" ) + USkeleton* Skeleton; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + TSubclassOf AnimationBlueprint; + + FRpmAnimationConfig() + { + Skeleton = nullptr; + AnimationBlueprint = nullptr; + } +}; + +USTRUCT(BlueprintType) +struct RPMNEXTGEN_API FRpmCharacterData +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString Id; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + FString BaseModelId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "assets")) + TMap Assets; + + FRpmCharacterData() + { + Id = ""; + BaseModelId = ""; + Assets = TMap(); + } +}; diff --git a/Source/RpmNextGen/Public/RpmLoaderComponent.h b/Source/RpmNextGen/Public/RpmLoaderComponent.h index 8ea12c2..0080a96 100644 --- a/Source/RpmNextGen/Public/RpmLoaderComponent.h +++ b/Source/RpmNextGen/Public/RpmLoaderComponent.h @@ -4,10 +4,13 @@ #include "CoreMinimal.h" #include "glTFRuntimeAsset.h" +#include "Api/Assets/Models/Asset.h" #include "Api/Characters/Models/RpmCharacter.h" #include "Components/ActorComponent.h" +#include "RpmCharacterTypes.h" #include "RpmLoaderComponent.generated.h" +class FFileApi; class FGlbLoader; struct FCharacterCreateResponse; struct FCharacterUpdateResponse; @@ -15,32 +18,11 @@ struct FCharacterFindByIdResponse; class FCharacterApi; struct FAsset; -USTRUCT(BlueprintType) -struct RPMNEXTGEN_API FRpmCharacterData -{ - GENERATED_BODY() - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") - FString Id; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") - FString BaseModelId; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "assets")) - TMap Assets; - - FRpmCharacterData() - { - Id = ""; - BaseModelId = ""; - Assets = TMap(); - } -}; - DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCharacterCreated, FRpmCharacterData, CharacterData); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCharacterUpdated, FRpmCharacterData, CharacterData); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCharacterFound, FRpmCharacterData, CharacterData); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnAssetLoaded, UglTFRuntimeAsset*, Asset, const FString&, AssetType); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnCharacterAssetLoaded, const FRpmCharacterData&, CharacterData, UglTFRuntimeAsset*, GltfRuntimeAsset); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnNewAssetLoaded, const FAsset&, Asset, UglTFRuntimeAsset*, GltfRuntimeAsset ); UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) class RPMNEXTGEN_API URpmLoaderComponent : public UActorComponent @@ -48,18 +30,32 @@ class RPMNEXTGEN_API URpmLoaderComponent : public UActorComponent GENERATED_BODY() public: - URpmLoaderComponent(); - - void SetGltfConfig(FglTFRuntimeConfig* Config) const; - FglTFRuntimeConfig* GltfConfig = nullptr; - UPROPERTY(BlueprintAssignable, Category = "Ready Player Me" ) - FOnAssetLoaded OnGltfAssetLoaded; + FOnCharacterAssetLoaded OnCharacterAssetLoaded; + UPROPERTY(BlueprintAssignable, Category = "Ready Player Me" ) + FOnNewAssetLoaded OnNewAssetLoaded; + UPROPERTY(BlueprintAssignable, Category = "Ready Player Me" ) FOnCharacterCreated OnCharacterCreated; + UPROPERTY(BlueprintAssignable, Category = "Ready Player Me" ) FOnCharacterUpdated OnCharacterUpdated; + UPROPERTY(BlueprintAssignable, Category = "Ready Player Me" ) FOnCharacterFound OnCharacterFound; + + URpmLoaderComponent(); + void SetGltfConfig(const FglTFRuntimeConfig* Config); + + void HandleAssetLoaded(TArray* Data, const FAsset& Asset); + void HandleCharacterAssetLoaded(TArray* Array, const FString& FileName); + + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + protected: + FglTFRuntimeConfig GltfConfig; + FString AppId; + FRpmCharacter Character; + FRpmCharacterData CharacterData; + virtual void BeginPlay() override; UFUNCTION(BlueprintCallable, Category = "Ready Player Me") @@ -69,33 +65,24 @@ class RPMNEXTGEN_API URpmLoaderComponent : public UActorComponent virtual void LoadCharacterFromUrl(FString Url); UFUNCTION(BlueprintCallable, Category = "Ready Player Me") - UglTFRuntimeAsset* LoadGltfRuntimeAssetFromCache(const FAsset& Asset); + void LoadGltfRuntimeAssetFromCache(const FAsset& Asset); UFUNCTION(BlueprintCallable, Category = "Ready Player Me") virtual void LoadCharacterFromAssetMapCache(TMap AssetMap); - - void LoadAssetsWithNewStyle(); UFUNCTION(BlueprintCallable, Category = "Ready Player Me") virtual void LoadAssetPreview(FAsset AssetData, bool bUseCache); - - UFUNCTION() - virtual void HandleGltfAssetLoaded(UglTFRuntimeAsset* UglTFRuntimeAsset, const FString& AssetType); + UFUNCTION() virtual void HandleCharacterCreateResponse(FCharacterCreateResponse CharacterCreateResponse, bool bWasSuccessful); UFUNCTION() virtual void HandleCharacterUpdateResponse(FCharacterUpdateResponse CharacterUpdateResponse, bool bWasSuccessful); UFUNCTION() virtual void HandleCharacterFindResponse(FCharacterFindByIdResponse CharacterFindByIdResponse, bool bWasSuccessful); - -public: - virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; - -protected: - FString AppId; - FRpmCharacter Character; - FRpmCharacterData CharacterData; + private: TSharedPtr CharacterApi; - TSharedPtr GlbLoader; + TSharedPtr FileApi; + + void LoadAssetsFromCacheWithNewStyle(); }; diff --git a/Source/RpmNextGen/Public/Samples/RpmAssetButtonWidget.h b/Source/RpmNextGen/Public/Samples/RpmAssetButtonWidget.h index 4894698..6e98a3c 100644 --- a/Source/RpmNextGen/Public/Samples/RpmAssetButtonWidget.h +++ b/Source/RpmNextGen/Public/Samples/RpmAssetButtonWidget.h @@ -1,5 +1,4 @@ // Fill out your copyright notice in the Description page of Project Settings. - #pragma once #include "CoreMinimal.h" @@ -24,7 +23,6 @@ class RPMNEXTGEN_API URpmAssetButtonWidget : public UUserWidget GENERATED_BODY() public: - UPROPERTY(meta = (BindWidget)) UButton* AssetButton; @@ -44,17 +42,19 @@ class RPMNEXTGEN_API URpmAssetButtonWidget : public UUserWidget virtual void SetSelected(const bool bInIsSelected); FAsset GetAssetData() const { return AssetData; } + protected: UFUNCTION() void OnTextureLoaded(UTexture2D* Texture2D); + virtual void NativeConstruct() override; private: + TSharedPtr TextureLoader; FLinearColor DefaultColor; - FAsset AssetData; + bool bIsSelected; + UFUNCTION() virtual void HandleButtonClicked(); - TSharedPtr TextureLoader; - bool bIsSelected; }; diff --git a/Source/RpmNextGen/Public/Samples/RpmAssetCardWidget.h b/Source/RpmNextGen/Public/Samples/RpmAssetCardWidget.h index 7e2a2f2..29e026d 100644 --- a/Source/RpmNextGen/Public/Samples/RpmAssetCardWidget.h +++ b/Source/RpmNextGen/Public/Samples/RpmAssetCardWidget.h @@ -17,17 +17,6 @@ class RPMNEXTGEN_API URpmAssetCardWidget : public UUserWidget GENERATED_BODY() public: - UFUNCTION() - void OnTextureLoaded(UTexture2D* Texture2D); - - virtual void NativeConstruct() override; - - UFUNCTION(BlueprintCallable, Category = "Asset Card") - virtual void InitializeCard(const FAsset& Asset); - - UFUNCTION(BlueprintCallable, Category = "Asset Card") - void LoadImage(const FAsset& Asset); - UPROPERTY(meta = (BindWidget)) UTextBlock* AssetCategoryText; @@ -40,6 +29,17 @@ class RPMNEXTGEN_API URpmAssetCardWidget : public UUserWidget UPROPERTY(meta = (BindWidget)) UTextBlock* AssetIdText; + UFUNCTION() + void OnTextureLoaded(UTexture2D* Texture2D); + + UFUNCTION(BlueprintCallable, Category = "Ready Player Me") + virtual void InitializeCard(const FAsset& Asset); + + UFUNCTION(BlueprintCallable, Category = "Ready Player Me") + void LoadImage(const FAsset& Asset); + + virtual void NativeConstruct() override; + private: FAsset AssetData; TSharedPtr TextureLoader; diff --git a/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h b/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h index 3e89ee5..f5e5f26 100644 --- a/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h +++ b/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h @@ -21,7 +21,6 @@ class RPMNEXTGEN_API URpmAssetPanelWidget : public UUserWidget { GENERATED_BODY() public: - virtual void NativeConstruct() override; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Asset Panel" ) TSubclassOf AssetButtonBlueprint; @@ -63,6 +62,10 @@ class RPMNEXTGEN_API URpmAssetPanelWidget : public UUserWidget void LoadAssetsOfType(const FString& AssetType); void CreateButton(const FAsset& AssetData); + + virtual void SynchronizeProperties() override; + virtual void NativeConstruct() override; + private: TArray> AssetButtons; TSharedPtr AssetApi; diff --git a/Source/RpmNextGen/Public/Samples/RpmCategoryButtonWidget.h b/Source/RpmNextGen/Public/Samples/RpmCategoryButtonWidget.h index 3e22e0f..5d788ac 100644 --- a/Source/RpmNextGen/Public/Samples/RpmCategoryButtonWidget.h +++ b/Source/RpmNextGen/Public/Samples/RpmCategoryButtonWidget.h @@ -21,7 +21,6 @@ class RPMNEXTGEN_API URpmCategoryButtonWidget : public UUserWidget GENERATED_BODY() public: - virtual void NativeConstruct() override; UPROPERTY(meta = (BindWidget)) UImage* CategoryImage; @@ -39,7 +38,7 @@ class RPMNEXTGEN_API URpmCategoryButtonWidget : public UUserWidget UTexture2D* CategoryImageTexture; UFUNCTION(BlueprintCallable, Category = "Category Button") - virtual void InitializeButton(FString Category, UTexture2D* Image); + virtual void InitializeButton(FString Category, UTexture2D* Image, const FVector2D& InImageSize); UFUNCTION(BlueprintCallable, Category = "Category Button") virtual void SetSelected(bool bIsSelected); @@ -52,10 +51,11 @@ class RPMNEXTGEN_API URpmCategoryButtonWidget : public UUserWidget #endif virtual void SynchronizeProperties() override; + virtual void NativeConstruct() override; private: + FLinearColor DefaultColor; + UFUNCTION() virtual void HandleButtonClicked(); - - FLinearColor DefaultColor; }; diff --git a/Source/RpmNextGen/Public/Samples/RpmCategoryPanelWidget.h b/Source/RpmNextGen/Public/Samples/RpmCategoryPanelWidget.h index feae846..ba005cf 100644 --- a/Source/RpmNextGen/Public/Samples/RpmCategoryPanelWidget.h +++ b/Source/RpmNextGen/Public/Samples/RpmCategoryPanelWidget.h @@ -3,13 +3,16 @@ #pragma once #include "CoreMinimal.h" +#include "Api/Assets/Models/AssetTypeListResponse.h" #include "Blueprint/UserWidget.h" #include "RpmCategoryPanelWidget.generated.h" +class URpmAssetButtonWidget; +class FAssetApi; class URpmCategoryButtonWidget; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCategorySelected, const FString&, CategoryName); - +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCategoriesLoaded, const TArray, CategoryNames); /** * */ @@ -19,20 +22,39 @@ class RPMNEXTGEN_API URpmCategoryPanelWidget : public UUserWidget GENERATED_BODY() public: - virtual void NativeConstruct() override; - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Category Panel") + TSubclassOf CategoryButtonBlueprint; + + UPROPERTY(meta = (BindWidget)) + UPanelWidget* ButtonContainer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Category Panel", meta = (ExposeOnSpawn = "true") ) + FVector2D ButtonSize; + + UPROPERTY() URpmCategoryButtonWidget* SelectedCategoryButton; UPROPERTY(BlueprintAssignable, Category = "Events") FOnCategorySelected OnCategorySelected; + + UPROPERTY(BlueprintAssignable, Category = "Events" ) + FOnCategoriesLoaded OnCategoriesLoaded; UFUNCTION(BlueprintCallable, Category = "Category Panel") virtual void UpdateSelectedButton(URpmCategoryButtonWidget* CategoryButton); + UFUNCTION(BlueprintCallable, Category = "Category Panel") + void LoadAndCreateButtons(); + UFUNCTION() virtual void OnCategoryButtonClicked(URpmCategoryButtonWidget* CategoryButton); - + + virtual void CreateButton(const FString& AssetType); + virtual void SynchronizeProperties() override; + virtual void NativeConstruct() override; private: - void InitializeCategoryButtons(); + TArray> AssetButtons; + TSharedPtr AssetApi; + + void AssetTypesLoaded(const FAssetTypeListResponse& AssetTypeListResponse, bool bWasSuccessful); }; diff --git a/Source/RpmNextGen/Public/Samples/RpmCreatorWidget.h b/Source/RpmNextGen/Public/Samples/RpmCreatorWidget.h new file mode 100644 index 0000000..178891b --- /dev/null +++ b/Source/RpmNextGen/Public/Samples/RpmCreatorWidget.h @@ -0,0 +1,47 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "RpmAssetPanelWidget.h" +#include "Blueprint/UserWidget.h" +#include "RpmCreatorWidget.generated.h" + +class UWidgetSwitcher; + +/** + * + */ +UCLASS() +class RPMNEXTGEN_API URpmCreatorWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = "Ready Player Me") + void CreateAssetPanelsFromCategories(const TArray& CategoryArray); + + UFUNCTION(BlueprintCallable, Category = "Ready Player Me") + void SwitchToPanel(const FString& Category); + + virtual void NativeConstruct() override; + +protected: + UPROPERTY(meta = (BindWidget)) + UWidgetSwitcher* AssetPanelSwitcher; + + UPROPERTY(BlueprintAssignable, Category = "Events" ) + FOnAssetSelected OnAssetSelected; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") + TSubclassOf AssetPanelBlueprint; + TMap IndexMapByCategory; + + UFUNCTION() + void HandleAssetSelectedFromPanel(const FAsset& AssetData); + + virtual void SynchronizeProperties() override; + +private: + UUserWidget* CreateAssetPanel(const FString& Category); +}; diff --git a/Source/RpmNextGen/Public/Settings/RpmDeveloperSettings.h b/Source/RpmNextGen/Public/Settings/RpmDeveloperSettings.h index ecaeeaf..371fa99 100644 --- a/Source/RpmNextGen/Public/Settings/RpmDeveloperSettings.h +++ b/Source/RpmNextGen/Public/Settings/RpmDeveloperSettings.h @@ -18,7 +18,6 @@ class RPMNEXTGEN_API URpmDeveloperSettings : public UDeveloperSettings FString ApiBaseUrl; public: - URpmDeveloperSettings(); UPROPERTY(VisibleAnywhere, Config, Category = "Auth Settings", meta = (ReadOnly = "true", ToolTip = "Base URL for authentication requests.")) FString ApiBaseAuthUrl; @@ -31,9 +30,12 @@ class RPMNEXTGEN_API URpmDeveloperSettings : public UDeveloperSettings UPROPERTY(EditAnywhere, Config, Category = "Auth Settings", meta = (ToolTip = "Proxy URL for API requests. If empty, the base URL will be used.")) FString ApiProxyUrl; - + + URpmDeveloperSettings(); + void SetupDemoAccount(); void Reset(); + FString GetApiBaseUrl() const; bool IsValid() const @@ -46,7 +48,8 @@ class RPMNEXTGEN_API URpmDeveloperSettings : public UDeveloperSettings 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 +#endif }; diff --git a/Source/RpmNextGenEditor/Private/AssetNameGenerator.cpp b/Source/RpmNextGenEditor/Private/AssetNameGenerator.cpp index bbb8e00..350a704 100644 --- a/Source/RpmNextGenEditor/Private/AssetNameGenerator.cpp +++ b/Source/RpmNextGenEditor/Private/AssetNameGenerator.cpp @@ -10,15 +10,15 @@ UAssetNameGenerator::UAssetNameGenerator() void UAssetNameGenerator::SetPath(FString NewPath) { - this->path = NewPath; + this->Path = NewPath; } FString UAssetNameGenerator::GenerateMaterialName(UMaterialInterface* Material, const int32 MaterialIndex, const FString& SlotName) const { - return FString::Printf(TEXT("%sMaterial_%d"), *path, MaterialIndex); + 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); + return FString::Printf(TEXT("%s%s%s"), *Path, *Material->GetName(), *ParamName); } diff --git a/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp b/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp index d07ba9b..bc5bef1 100644 --- a/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp +++ b/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp @@ -46,8 +46,7 @@ USkeletalMesh* FEditorAssetLoader::SaveAsUAsset(UglTFRuntimeAsset* GltfAsset, co USkeletalMesh* skeletalMesh = GltfAsset->LoadSkeletalMeshRecursive(TEXT(""), {}, meshConfig); skeletalMesh->SetSkeleton(Skeleton); Skeleton->SetPreviewMesh(skeletalMesh); - - const FString CoreAssetPath = FString::Printf(TEXT("/Game/ReadyPlayerMe/%s/"), *LoadedAssetId); + const FString CoreAssetPath = FString::Printf(TEXT("/Game/ReadyPlayerMe/CharacterModels/%s/"), *LoadedAssetId); const FString SkeletonAssetPath = FString::Printf(TEXT("%s%s_Skeleton"), *CoreAssetPath, *LoadedAssetId); const FString SkeletalMeshAssetPath = FString::Printf(TEXT("%s%s_SkeletalMesh"), *CoreAssetPath, *LoadedAssetId); @@ -111,7 +110,7 @@ void FEditorAssetLoader::LoadAssetToWorld(const FString& AssetId, UglTFRuntimeAs GEditor->EditorUpdateComponents(); if (GltfAsset) { - NewActor->LoadGltfAsset(GltfAsset); + NewActor->LoadAsset(FAsset(), GltfAsset); } UE_LOG(LogReadyPlayerMe, Log, TEXT("Successfully loaded GLB asset into the editor world")); return; diff --git a/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp index 18f97d8..5b3fc9c 100644 --- a/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp @@ -153,7 +153,7 @@ void SRpmDeveloperLoginWidget::Construct(const FArguments& InArgs) .AutoHeight() [ SNew(STextBlock) - .Text(FText::FromString("Character Styles")) + .Text(FText::FromString("Character Models")) .Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Regular.ttf"), 16)) .Visibility(this, &SRpmDeveloperLoginWidget::GetLoggedInViewVisibility) ] @@ -162,7 +162,7 @@ void SRpmDeveloperLoginWidget::Construct(const FArguments& InArgs) .AutoHeight() [ SNew(STextBlock) - .Text(FText::FromString("Here you can import your character styles from Studio")) + .Text(FText::FromString("Here you can import your character models from Studio")) .Visibility(this, &SRpmDeveloperLoginWidget::GetLoggedInViewVisibility) ] + SVerticalBox::Slot() @@ -273,13 +273,16 @@ void SRpmDeveloperLoginWidget::AddCharacterStyle(const FAsset& StyleAsset) .WidthOverride(100.0f) [ SNew(SButton) - .Text(FText::FromString("Load Style")) - .OnClicked_Lambda([this, StyleAsset]() -> FReply - { - OnLoadStyleClicked(StyleAsset); - return FReply::Handled(); - }) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .Text(FText::FromString("Import")) + .OnClicked_Lambda([this, StyleAsset]() -> FReply + { + OnLoadBaseModelClicked(StyleAsset); + return FReply::Handled(); + }) ] + ] ] + SHorizontalBox::Slot() @@ -313,10 +316,10 @@ void SRpmDeveloperLoginWidget::OnTextureLoaded(UTexture2D* Texture2D, TSharedPtr ActiveLoaders.Remove(LoaderToRemove); } -void SRpmDeveloperLoginWidget::OnLoadStyleClicked(const FAsset& StyleAsset) +void SRpmDeveloperLoginWidget::OnLoadBaseModelClicked(const FAsset& StyleAsset) { - AssetLoader = FEditorAssetLoader(); - AssetLoader.LoadBaseModelAsset(StyleAsset); + AssetLoader = MakeShared(); + AssetLoader->LoadBaseModelAsset(StyleAsset); } EVisibility SRpmDeveloperLoginWidget::GetLoginViewVisibility() const diff --git a/Source/RpmNextGenEditor/Public/AssetNameGenerator.h b/Source/RpmNextGenEditor/Public/AssetNameGenerator.h index 346cd99..daf7f80 100644 --- a/Source/RpmNextGenEditor/Public/AssetNameGenerator.h +++ b/Source/RpmNextGenEditor/Public/AssetNameGenerator.h @@ -16,17 +16,18 @@ class RPMNEXTGENEDITOR_API UAssetNameGenerator : public UObject GENERATED_BODY() public: + FTransientObjectSaverMaterialNameGenerator MaterialNameGeneratorDelegate; + FTransientObjectSaverTextureNameGenerator TextureNameGeneratorDelegate; + 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; + FString GenerateTextureName(UTexture* Texture, UMaterialInterface* Material, const FString& MaterialPath, const FString& ParamName) const; + + void SetPath(FString Path); private: - FString path; + FString Path; }; diff --git a/Source/RpmNextGenEditor/Public/Auth/DevAuthTokenCache.h b/Source/RpmNextGenEditor/Public/Auth/DevAuthTokenCache.h index 316d9a6..b342d3c 100644 --- a/Source/RpmNextGenEditor/Public/Auth/DevAuthTokenCache.h +++ b/Source/RpmNextGenEditor/Public/Auth/DevAuthTokenCache.h @@ -11,12 +11,14 @@ class RPMNEXTGENEDITOR_API FDevAuthTokenCache static void SetAuthData(const FDeveloperAuth& DevAuthData); static void ClearAuthData(); private: + static FDeveloperAuth AuthData; + 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 index e8084b3..588493a 100644 --- a/Source/RpmNextGenEditor/Public/Auth/DeveloperAuthApi.h +++ b/Source/RpmNextGenEditor/Public/Auth/DeveloperAuthApi.h @@ -11,11 +11,13 @@ DECLARE_DELEGATE_TwoParams(FOnDeveloperLoginResponse, const FDeveloperLoginRespo class RPMNEXTGENEDITOR_API FDeveloperAuthApi : public FWebApi { public: + FOnDeveloperLoginResponse OnLoginResponse; + 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 index da3bf06..7b0cbd1 100644 --- a/Source/RpmNextGenEditor/Public/Auth/DeveloperTokenAuthStrategy.h +++ b/Source/RpmNextGenEditor/Public/Auth/DeveloperTokenAuthStrategy.h @@ -15,8 +15,10 @@ class RPMNEXTGENEDITOR_API DeveloperTokenAuthStrategy : public IAuthenticationSt 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; + + void RefreshTokenAsync(const FRefreshTokenRequest& Request); }; diff --git a/Source/RpmNextGenEditor/Public/DeveloperAccounts/DeveloperAccountApi.h b/Source/RpmNextGenEditor/Public/DeveloperAccounts/DeveloperAccountApi.h index 21565be..a28c880 100644 --- a/Source/RpmNextGenEditor/Public/DeveloperAccounts/DeveloperAccountApi.h +++ b/Source/RpmNextGenEditor/Public/DeveloperAccounts/DeveloperAccountApi.h @@ -14,16 +14,18 @@ DECLARE_DELEGATE_TwoParams(FOnOrganizationListResponse, const FOrganizationListR class RPMNEXTGENEDITOR_API FDeveloperAccountApi : public FWebApiWithAuth { public: + FOnApplicationListResponse OnApplicationListResponse; + FOnOrganizationListResponse OnOrganizationResponse; + FDeveloperAccountApi(IAuthenticationStrategy* InAuthenticationStrategy); void ListApplicationsAsync(const FApplicationListRequest& Request); void ListOrganizationsAsync(const FOrganizationListRequest& Request); - - FOnApplicationListResponse OnApplicationListResponse; - FOnOrganizationListResponse OnOrganizationResponse; + private: - static FString BuildQueryString(const TMap& Params); + FString ApiBaseUrl; + void HandleOrgListResponse(FString Data, bool bWasSuccessful); void HandleAppListResponse(FString Data, bool bWasSuccessful); - - FString ApiBaseUrl; + + static FString BuildQueryString(const TMap& Params); }; diff --git a/Source/RpmNextGenEditor/Public/EditorAssetLoader.h b/Source/RpmNextGenEditor/Public/EditorAssetLoader.h index 611b2c0..a650d47 100644 --- a/Source/RpmNextGenEditor/Public/EditorAssetLoader.h +++ b/Source/RpmNextGenEditor/Public/EditorAssetLoader.h @@ -10,6 +10,7 @@ class UglTFRuntimeAsset; class FEditorAssetLoader : public FAssetGlbLoader { public: + USkeleton* SkeletonToCopy; FEditorAssetLoader(); virtual ~FEditorAssetLoader() override; @@ -18,11 +19,11 @@ class FEditorAssetLoader : public FAssetGlbLoader void LoadBaseModelAsset(const FAsset& Asset); USkeletalMesh* SaveAsUAsset(UglTFRuntimeAsset* GltfAsset, const FString& LoadedAssetId) const; - USkeleton* SkeletonToCopy; private: + FglTFRuntimeConfig* GltfConfig; + void LoadAssetToWorld(const FString& AssetId, UglTFRuntimeAsset* GltfAsset); UFUNCTION() void HandleGlbLoaded(const FAsset& Asset, const TArray& Data); - FglTFRuntimeConfig* GltfConfig; }; diff --git a/Source/RpmNextGenEditor/Public/RpmNextGenEditor.h b/Source/RpmNextGenEditor/Public/RpmNextGenEditor.h index 6bd1790..c6cd461 100644 --- a/Source/RpmNextGenEditor/Public/RpmNextGenEditor.h +++ b/Source/RpmNextGenEditor/Public/RpmNextGenEditor.h @@ -17,14 +17,13 @@ class RPMNEXTGENEDITOR_API FRpmNextGenEditorModule : public IModuleInterface void PluginButtonClicked(); private: - void RegisterMenus(); void FillReadyPlayerMeMenu(UToolMenu* Menu); void OpenLoaderWindow(); void OpenCacheEditorWindow(); + TSharedRef OnSpawnLoaderWindow(const FSpawnTabArgs& SpawnTabArgs); TSharedRef OnSpawnCacheWindow(const FSpawnTabArgs& SpawnTabArgs); TSharedRef OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs); TSharedPtr PluginCommands; - }; diff --git a/Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h b/Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h index e3ebdd4..79769ac 100644 --- a/Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h +++ b/Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h @@ -14,8 +14,10 @@ class SCacheGeneratorWidget : public SCompoundWidget void Construct(const FArguments& InArgs); private: - + float ItemsPerCategory = 10.0f; + FString CacheUrl; TUniquePtr CacheGenerator; + // Callback functions for your buttons FReply OnGenerateOfflineCacheClicked(); FReply OnExtractCacheClicked(); @@ -40,10 +42,9 @@ class SCacheGeneratorWidget : public SCompoundWidget { CacheUrl = NewText.ToString(); } - - float ItemsPerCategory = 10.0f; + void OnItemsPerCategoryChanged(float NewValue); - FString CacheUrl; + void OnCacheUrlChanged(const FText& NewText); }; diff --git a/Source/RpmNextGenEditor/Public/UI/SCharacterLoaderWidget.h b/Source/RpmNextGenEditor/Public/UI/SCharacterLoaderWidget.h index 04a2038..dc3f2f0 100644 --- a/Source/RpmNextGenEditor/Public/UI/SCharacterLoaderWidget.h +++ b/Source/RpmNextGenEditor/Public/UI/SCharacterLoaderWidget.h @@ -20,21 +20,16 @@ class SCharacterLoaderWidget : public SCompoundWidget void OnSkeletonSelected(const FAssetData& AssetData); 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 = nullptr; + + FText PathText; + + FReply OnButtonClick(); + + void OnPathTextChanged(const FText& NewText); + void LoadAsset(const FString& Path); + FString GetCurrentSkeletonPath() const; }; diff --git a/Source/RpmNextGenEditor/Public/UI/SRpmDeveloperLoginWidget.h b/Source/RpmNextGenEditor/Public/UI/SRpmDeveloperLoginWidget.h index af72e36..33be258 100644 --- a/Source/RpmNextGenEditor/Public/UI/SRpmDeveloperLoginWidget.h +++ b/Source/RpmNextGenEditor/Public/UI/SRpmDeveloperLoginWidget.h @@ -46,7 +46,7 @@ class RPMNEXTGENEDITOR_API SRpmDeveloperLoginWidget : public SCompoundWidget EVisibility GetLoginViewVisibility() const; EVisibility GetLoggedInViewVisibility() const; TArray> ActiveLoaders; - FEditorAssetLoader AssetLoader; + TSharedPtr AssetLoader; TUniquePtr AssetApi; TUniquePtr DeveloperAccountApi; TUniquePtr DeveloperAuthApi; @@ -71,7 +71,7 @@ class RPMNEXTGENEDITOR_API SRpmDeveloperLoginWidget : public SCompoundWidget void HandleOrganizationListResponse(const FOrganizationListResponse& Response, bool bWasSuccessful); void HandleApplicationListResponse(const FApplicationListResponse& Response, bool bWasSuccessful); void HandleBaseModelListResponse(const FAssetListResponse& Response, bool bWasSuccessful); - void OnLoadStyleClicked(const FAsset& Asset); + void OnLoadBaseModelClicked(const FAsset& Asset); void SetLoggedInState(const bool IsLoggedIn); void PopulateComboBoxItems(const TArray& Items, const FString ActiveItem); void OnComboBoxSelectionChanged(TSharedPtr NewValue, ESelectInfo::Type SelectInfo); From f5df785cd8791db48b8d3acc0c2619197858ef98 Mon Sep 17 00:00:00 2001 From: Harrison Hough Date: Mon, 7 Oct 2024 14:03:09 +0300 Subject: [PATCH 48/54] Pagination + various fixes and improvements (#14) - In this PR pagination support has been added ![image](https://github.com/user-attachments/assets/613b8e8d-3a0b-41a4-a455-47f286b63dd3) (you will need to change the Pagination Limit on the RpmLoaderUI or RpmBasicUi widget to test it) ![image](https://github.com/user-attachments/assets/5d48de66-99f6-4776-a26a-a59991311ed8) - Big refactoring of how rpm web requests are handled and authenticated (WebApi, WebApiWithAuth, AssetAPi etc) - this should fix a number of issues, for example the delegates/callbacks not running if a request fails - Added a fallback to cache functionality for Asset Api and CharacterApi. Instead of manually checking connection, the SDK by default will try request from the API's and if they fail then it would fall back to cache if possible. Also a new request strategy enum. ![image](https://github.com/user-attachments/assets/b7e33d41-e94f-4721-9ae7-a33d79a8e3ed) Currently it is used only for AssetApi but it could be expanded upon and also added to CharacterApi in next iteration --- .../Samples/BasicLoader/BP_RpmActor.uasset | Bin 1459 -> 0 bytes .../Samples/BasicLoader/BP_RpmActor_C.uasset | Bin 1499 -> 0 bytes .../BasicLoader/BasicLoaderSample.umap | Bin 2477 -> 0 bytes .../Blueprints/BP_LoaderDemoUI.uasset | Bin 1493 -> 0 bytes .../Blueprints/BP_LoaderDemoUI_C.uasset | Bin 1527 -> 0 bytes .../Blueprints/BP_RpmActorSample.uasset | Bin 2507 -> 0 bytes .../BasicLoader/Blueprints/BP_RpmTest.uasset | Bin 2437 -> 0 bytes .../Default__BP_LoaderDemoUI_C.uasset | Bin 1641 -> 0 bytes .../Blueprints/WBP_LoaderDemoUI.uasset | Bin 2489 -> 0 bytes .../Blueprints/WBP_LoaderUI.uasset | Bin 2482 -> 0 bytes .../Blueprints/WBP_RpmLoaderUI.uasset | Bin 54514 -> 54555 bytes .../BasicLoader/Default__BP_RpmActor_C.uasset | Bin 1623 -> 0 bytes Content/Samples/BasicLoader/LoaderSample.umap | Bin 2394 -> 0 bytes .../Samples/BasicLoader/RpmBasicLoader.umap | Bin 143362 -> 138177 bytes .../Blueprints/WBP_BasicUISample.uasset | Bin 2501 -> 0 bytes .../Blueprints/WBP_RpmAssetPanel.uasset | Bin 24076 -> 55939 bytes .../BasicUI/Blueprints/WBP_RpmBasicUI.uasset | Bin 69676 -> 70421 bytes .../Blueprints/WBP_RpmPaginator.uasset | Bin 0 -> 36487 bytes Content/Samples/BasicUI/RpmBasicUISample.umap | Bin 78131 -> 73793 bytes .../Private/Api/Assets/AssetApi.cpp | 225 ++++++++++++------ .../Private/Api/Assets/AssetGlbLoader.cpp | 3 - .../Private/Api/Assets/AssetIconLoader.cpp | 2 - .../Private/Api/Auth/ApiKeyAuthStrategy.cpp | 19 +- .../RpmNextGen/Private/Api/Auth/AuthApi.cpp | 44 +++- .../Private/Api/Characters/CharacterApi.cpp | 111 ++++++--- .../RpmNextGen/Private/Api/Common/WebApi.cpp | 37 +-- .../Private/Api/Common/WebApiWithAuth.cpp | 57 +++-- .../RpmNextGen/Private/Api/Files/FileApi.cpp | 4 +- .../Private/Api/Files/GlbLoader.cpp | 4 +- .../Private/Api/Files/PakFileUtility.cpp | 44 ++-- Source/RpmNextGen/Private/RpmActor.cpp | 2 - .../RpmNextGen/Private/RpmFunctionLibrary.cpp | 97 ++++---- .../RpmNextGen/Private/RpmLoaderComponent.cpp | 84 +++---- .../Private/Samples/RpmAssetPanelWidget.cpp | 67 ++++-- .../Samples/RpmCategoryButtonWidget.cpp | 3 +- .../Samples/RpmCategoryPanelWidget.cpp | 20 +- .../Private/Samples/RpmCreatorWidget.cpp | 26 +- .../Private/Samples/RpmPaginatorWidget.cpp | 56 +++++ .../RpmNextGen/Public/Api/Assets/AssetApi.h | 20 +- .../Public/Api/Assets/Models/Asset.h | 13 +- .../Api/Assets/Models/AssetListRequest.h | 44 ++-- .../Public/Api/Auth/ApiKeyAuthStrategy.h | 6 +- Source/RpmNextGen/Public/Api/Auth/AuthApi.h | 6 +- .../Public/Api/Auth/IAuthenticationStrategy.h | 11 +- .../Public/Api/Characters/CharacterApi.h | 13 +- .../Models/CharacterCreateResponse.h | 14 ++ .../Api/Characters/Models/RpmCharacter.h | 2 +- .../Public/Api/Common/ApiRequestStrategy.h | 9 + .../Api/{Auth => Common/Models}/ApiRequest.h | 7 + .../Api/Common/Models/PaginationQueryParams.h | 2 +- Source/RpmNextGen/Public/Api/Common/WebApi.h | 20 +- .../Public/Api/Common/WebApiWithAuth.h | 17 +- Source/RpmNextGen/Public/Api/Files/FileApi.h | 6 +- .../RpmNextGen/Public/Api/Files/GlbLoader.h | 4 +- .../Public/Cache/AssetCacheManager.h | 17 ++ .../RpmNextGen/Public/Cache/CacheStrategy.h | 17 -- Source/RpmNextGen/Public/RpmFunctionLibrary.h | 10 +- Source/RpmNextGen/Public/RpmLoaderComponent.h | 10 +- .../Public/Samples/RpmAssetPanelWidget.h | 21 +- .../Public/Samples/RpmCategoryPanelWidget.h | 8 +- .../Public/Samples/RpmCreatorWidget.h | 5 +- .../Public/Samples/RpmPaginatorWidget.h | 55 +++++ .../Public/Utilities/ConnectionManager.cpp | 58 ----- .../Public/Utilities/ConnectionManager.h | 38 --- .../Private/Auth/DeveloperAuthApi.cpp | 31 ++- .../Auth/DeveloperTokenAuthStrategy.cpp | 28 +-- .../Private/Cache/CacheGenerator.cpp | 11 +- .../DeveloperAccounts/DeveloperAccountApi.cpp | 21 +- .../Private/UI/SRpmDeveloperLoginWidget.cpp | 20 +- .../Public/Auth/DeveloperAuthApi.h | 2 +- .../Public/Auth/DeveloperTokenAuthStrategy.h | 14 +- .../Auth/Models/DeveloperLoginResponse.h | 4 +- .../Public/Cache/CacheGenerator.h | 2 +- .../DeveloperAccounts/DeveloperAccountApi.h | 8 +- .../Models/ApplicationListRequest.h | 2 +- .../Models/OrganizationListRequest.h | 1 - .../Public/EditorAssetLoader.h | 2 +- .../Public/UI/SRpmDeveloperLoginWidget.h | 6 +- 78 files changed, 876 insertions(+), 614 deletions(-) delete mode 100644 Content/Samples/BasicLoader/BP_RpmActor.uasset delete mode 100644 Content/Samples/BasicLoader/BP_RpmActor_C.uasset delete mode 100644 Content/Samples/BasicLoader/BasicLoaderSample.umap delete mode 100644 Content/Samples/BasicLoader/Blueprints/BP_LoaderDemoUI.uasset delete mode 100644 Content/Samples/BasicLoader/Blueprints/BP_LoaderDemoUI_C.uasset delete mode 100644 Content/Samples/BasicLoader/Blueprints/BP_RpmActorSample.uasset delete mode 100644 Content/Samples/BasicLoader/Blueprints/BP_RpmTest.uasset delete mode 100644 Content/Samples/BasicLoader/Blueprints/Default__BP_LoaderDemoUI_C.uasset delete mode 100644 Content/Samples/BasicLoader/Blueprints/WBP_LoaderDemoUI.uasset delete mode 100644 Content/Samples/BasicLoader/Blueprints/WBP_LoaderUI.uasset delete mode 100644 Content/Samples/BasicLoader/Default__BP_RpmActor_C.uasset delete mode 100644 Content/Samples/BasicLoader/LoaderSample.umap delete mode 100644 Content/Samples/BasicUI/Blueprints/WBP_BasicUISample.uasset create mode 100644 Content/Samples/BasicUI/Blueprints/WBP_RpmPaginator.uasset create mode 100644 Source/RpmNextGen/Private/Samples/RpmPaginatorWidget.cpp create mode 100644 Source/RpmNextGen/Public/Api/Common/ApiRequestStrategy.h rename Source/RpmNextGen/Public/Api/{Auth => Common/Models}/ApiRequest.h (92%) delete mode 100644 Source/RpmNextGen/Public/Cache/CacheStrategy.h create mode 100644 Source/RpmNextGen/Public/Samples/RpmPaginatorWidget.h delete mode 100644 Source/RpmNextGen/Public/Utilities/ConnectionManager.cpp delete mode 100644 Source/RpmNextGen/Public/Utilities/ConnectionManager.h rename Source/{RpmNextGen => RpmNextGenEditor}/Private/Cache/CacheGenerator.cpp (96%) rename Source/{RpmNextGen => RpmNextGenEditor}/Public/Cache/CacheGenerator.h (96%) diff --git a/Content/Samples/BasicLoader/BP_RpmActor.uasset b/Content/Samples/BasicLoader/BP_RpmActor.uasset deleted file mode 100644 index b37b7dbca2eca927c1a900341befb06081c63cff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1459 zcmbtU&1(}u6rY-EwQ1DWdb3CmQZ;S9KgcCEpCyu}Atg7dj@z+rO*YH!XlqZ3Sn5#} zghDF>LGWS^dJ?o1^luRL;31bn^%SAvM|^L)8`E{gia#=$dGGz^y|-`P%-W5U%bSfx zH{v@$uqHZ9RUsZhz~o#-62J$3Zu-n-KVsTt#9q5wagf6xjVRdm`SL zFW`*_g1$s55={HU9$zpL3CF{!L?{u8BcD8nDDWfm5D7h@JkgF8LZ)HHF$d#9p`F;@ zzf3+_^sl}semF1)T*}VpL^A zL6=9f6Y{Bg;tzJ%q~TcUT?iKsA^u}@+5#w21HT?);u?VztrB(HD@Pv)c`96@Q!HB% zXi1!-T&zerT4t(D1x9-K)rBaK+y%LuW3zmON$zo4mP<@^$Ehj`*$SOxiaTDq%w$EB zIH>byft**^6_L%v1YS|dms=fUgFEq}ZQa^48tyLl;+}Cq5oPX9R1{VmJ3q|?PBt?` z(E}~Elr$y6+0GB&%yfdfnGETT_#IuSX>nYw@s&eO-L^bmC+`LJfGJU;s!I6G?CpIZ zjWSM?lv7f>_T)}CD7LaFut`yYY~cGg*6Z25V9-lR)RBe52Y<;;2TOwwm~liH2k{Ng z1oxT&$Eg7`Kzs!rew-OrJiD0$vcbc07{IQgmXU{5EcAtTD;w%98^GLS5>%|9Cxn~Kc1tHn#J3>H yR4z)|Mb!KK3}~B8J@8+5(e?ZF9MImHqY%Rim=z90Ka3qcm>L`Yu!w8$T>k{Gl{0Pt diff --git a/Content/Samples/BasicLoader/BP_RpmActor_C.uasset b/Content/Samples/BasicLoader/BP_RpmActor_C.uasset deleted file mode 100644 index 5de0f0e4d3a1f4ecfe905cf8a290bdcde6ce316c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1499 zcmbtU%WD&15dUh_*4Fwc76d7!r?#fqJZx-w3C(UERAUlSdyb!*kGh&AK*c~sAvyT51vF2^qPZ(=qX4fro_T6oU^TdqCZQ?t`x& zxa>!5!@l7owh@OgYV-T-POryh9~pJoc)yQFJb4FI;2{~PgqhG*wqt~li||Fg0s47k zIdCjI5L_-S#BV(vYI{39|0MT@=btZJdlf(FxQ+}l4-&vUCX(qHYw3cK**xiUnxibi zsLCu(&Zfr+`MNT=NgO8Wa`#a0OB}p_`X4Z9jX;x%`1e>7$0!KVJCQfN?(6|0LdA1* ziUnnnCZsv4NpdPglT1xhk&!O#!jc<6)@V8zVzXL+rK}S)nNBd(%2QPmgEEaX#mXn9 zSz3`&8n`1d07n#dUScyIQIi$&{c3BN+e#LFsy-(s5bN>mw*xq9LR6%*W))dO zWuit3;}-YcnwT#&CBf=0j$Ii)2<|E!;L8-MvAB?+s!9g$r7hU}630p*j!R~`tKM4X zz{2Ht|7j-#O9`}@*_*q-I>9vRr5YvK`Ny~SgQLVN%Hom&{NSH+>-k_C1j_9tYGA2* z-ygf0;Qr7! zvA=*%t=(BRFsG+H#{F0kJP)XU6G&T^7x5bnzSeRQybQ3%?-+hd3tm>vvKw)HD)g UiHgz(x}}R%N5cyny9(F(Z`8j>QUCw| diff --git a/Content/Samples/BasicLoader/BasicLoaderSample.umap b/Content/Samples/BasicLoader/BasicLoaderSample.umap deleted file mode 100644 index f9ca51c7a428cab5733b4d85a96ca1c1c489751e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2477 zcmcImO-NKx6uxR%Y4&58EwYjV^LKP~Y$ie+-rTtv{aO(3*$BWNI6YClfrydQg`Z<;aegZu8e=bU@~?!9Mb_+rt+ z*=RI+GL4XoR6@pKMx?-)IP-LLtnSuy=19e$`^}-UeZX_BBV;wq-mu7EN+nz1TLaqp zFmn#4gLiR!mB(4_vb!ARt_qK{((QIt+3lQ@uLBiAI>Ey#nET)kQueSEw!^F9{LF#)T!;)_m$L^DAVwp|obBN#+qd z1vuEtwU-*Ol1q&}nwpqFV@o7Hj$}-JYu}EtU^k0Mf`|z+i7G6>1*t5P!h4ZYJZNpD z^z>fGlJV>`?G7qFpOwkXx6h|`o0Y6G1lQvGzzsaF!%U$(RVW#~@Z#oXU}$-^uz(;z zzF~=sk4-nN1%ZB2M6Msm+4N_b}Q4r`E@e?i&hPN1ap%KR2`vH3EHCD{mN-rx$Lt7Yn8%#3&g_}6qLqdjG&)92L zH1-;?V;-4i1)VnX#Y==hJ*@I#%md%d!@G#UN9I;3t>x&X+LrL)Cjyv_v5We@7V+?e z5SWt@Tc1&Q;Gv(t`7&h5_`*hqF#EdtpL}I#u3~)MSs`C&8uOlmq;baFjfVIgXp|=d ztb^tAwC*~sa8{P;@mi5D-iCx7rwewL=+(j~tplDYIf0ka)Q-Gd2dX#p&297p;#gAPGyq%)mnsxK(Aq*FRNx;fE-WQT5+SKRC4l fo;?mkK8&RP3aQ%g!PjKaIOD4_ukmFK!ymQZv|2kL diff --git a/Content/Samples/BasicLoader/Blueprints/BP_LoaderDemoUI.uasset b/Content/Samples/BasicLoader/Blueprints/BP_LoaderDemoUI.uasset deleted file mode 100644 index 0cc5e936708dd8b8bda12c12643bab60c6d64614..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1493 zcmb7EO=uHQ5S|+U`imB&h*T*as>bH0wz0ulntuanT2exL(AsD6XjhYL$Ue1tkf2nm zpr97SfCa0^9t1rIo{D(Tn-q$8j)y|^AUPDO#+mJI3G0ex*xC2q%(vgn&YSlZZyudn zE0@dT7J#}f0IN8X)S!Nyd{JES&X(#wJLf%bg0{nG%Wng~2cmL@;{ecv+K1>i9FKRp z+?{T3SC_{T3^;>Mhs)dJ3%EOb0uI-SU@%B{SjH&$z#K**CR$(KN*;h4IFdd<{XQN} zE!-=3rVIVg23kHmYA8;YYM-7x{qn$Q)99hzS;~-jC;{TJK%uq2rWt2eYdp|xjqwy` z3h(H)IaEKuxA`M~@#9CTyl!zX)8sA2|45`2f<-Fl(MB<8PC^j15+%#K##USmvBX6- z!6R~lrKD?2NOC61(p<^11czpEb><|3?6GV*%BMt#XY9i)olSAY?q!OUh{!C-b9Qg) z63^zOj6n0yxecSAr{#DU&a7YG-&-BUh>_#YK5u9baSSJNQdZb~a*oFb&tKpP0c+uo zJ}RN^iw}eXNlC~#SQra`*@Ye>+o{`2cekV6mtu+n;>y(hT3iiq!Td}xDCS>1+>M3} z{uob6IgEpRt1G3*PIPD$10^)Gm-w~yCc%?q&p8qN@f)9(Y}53?h?Jb*VzXv2IQITA zZl5YUwDAY-6$LYj8IIC7ijJ$RaA*gm!UAB1(`}VNy|o%O<4?@5p%S3YsmChK_1c1Z z(4)<%Cv~ASLHaK-@)G`+Ar@^a3H>M4nPjnwSb832v51TO>xP)R-LzxOy6+ZNS7=EQ zp&7=MjKI|Bey#{9qaIZ42#lfC&{OSIoLFO@+Kh&F^)@<)QOtz$A|+`<{jV!kMdORg Ixri6~Z^X?)8vpJ;mgjx_hx3lnKy6V&RswB zaHUu*o-GHc+5}L*kx+*6<@|JhQJPqG&9_cU5{gX zdrKhP9trz>t+E^p$Zbbj{cY0GpcMA`T4dQz{*cEg_`nm4L`<~4v6(CYcW@+`MENRe zUwWqJrP+$lGc&G%n@gu_S4Ts`k;hLL8pmJXeoh$@4<$f6<#6A9qO1mIO-=D|pee@F zoawwd;At`J$T-#b2RnXbAQW=DY4Qo^M4kW4TNp9}!WW^$xO3F3okX#B?Ruqp}p&yi$6Y z=QJfNP~D$cQ+Dx;8t+cXpiDe_e{5qUr)(^6t4@mUA&$YMrsRZIRy7_!dF}#F3RvlG z?$};pi}yw$c;?#yDy)qMvw3&5@OBGYjiprZ12wB>ve($had6XsJCwcLl1;1Kkg%wk{aS-y=GWct}($= zYbjm&Fjx{HjU}=Cvu&066(sx>850(W*`5Pt1>wzz*uT zQk~0OP)Bx}bLvnFbVEq~1v@YKui9eKArokybWnta4aBnYaEe7-WdCl9Y3Ut1$hjXU zu)g=qw20E#%aPD>@8Y^pvc}aluS6D!{@P^?8@SQ-PWIRp3Aw@eU+t&uJAm>eA=IP% Q*H72*#ut@%4uAQ-08imjOaK4? diff --git a/Content/Samples/BasicLoader/Blueprints/BP_RpmActorSample.uasset b/Content/Samples/BasicLoader/Blueprints/BP_RpmActorSample.uasset deleted file mode 100644 index 850353175fe4907c7c53fdb025a455047fdf5d1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2507 zcmb_eUr1A76#vZrYG#>9FFr_t)pBb(n+p{8PczI+v4_Cc*Zk7iwma^pyw=ls6&edpXw zT|IPfF&d4Y&n6^i10f5*k+NWX@0ysKcZ^1IKbgnulXb=eAUm^-kWIjwxF7>Y$Zi-l zU|j;-Wvg*~#@A*3BR zjzJj5XM&SIM=KmJm-3sP;TIos$BPWFitg84d$a#TZDHgKB1At301;)A+r_n61u!!h z+T4|f7UpMEU}crYBQZT8KgJHG(8Hz}P4pb|<76KIn}(CB36d1W(?gO7&XMAoh-AOc zD+Wa~^g8_ zc^f&}!FZFyA7&xm7ZgF<>;YrDSLArfl6*EjJDs@`X|v*roU<4P`a?DxEvn>4UqzUOU*31L8P1QfRBl+pCtqc^FSc*5UM1K_Cb3gv#(b zlFu+O{jD9tCuuxR@?>P#DDO%Dy*m4-U*4tFz^-=_54!LTNs-0Ny*KeTTJ`vlKj43`8tE|;3N~Ni;9%YU3ogP3j|WKT3CmVhoo^lS(uMBZUu$>0*FvQ zv}fnvb=<+-!w(rg8ze-G^b4L6<~ETOJ1i3&7=k-Ph(9A+rU}`ZZ&?< zemOygcZY=dm9ySbwE&FsuULvMomh%nb*ynkD92I!!7_;yT{^My@cvr|*VZbykLTtK4rQbd%)Qzrrs-6-^wIQ+ol?EyG0q84zu=9kREkvmgNEZwy z1kGJc5PiYer$FwoXF(coqotXB4YVD~2#rlEDJ7|rkRix!HArn|UuVc3XDM0pA~HnJ zA^ODJK%oChw)%6T%{m?doAdw-J;Jwu1E~Z?T7Q&c*6;&`q^TsVE=7kND{Tt?#{B_v C1V;G) diff --git a/Content/Samples/BasicLoader/Blueprints/BP_RpmTest.uasset b/Content/Samples/BasicLoader/Blueprints/BP_RpmTest.uasset deleted file mode 100644 index ff64b87a6d8a9a5fc6807ba12217844362fc9879..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2437 zcmcImO-xfk5MC_)fgmCf6GIH)P!VZ`7NrLWrKJQ@`IUnvq^@Pfhovp;BO-}G<4=eQ zi3;)HZzP`77!G>TiyVyHyqK5}6EDOBMH3Yh{H^o7_SN>aAPRoTcJ|GDZ)bLPXZprZ zZM!}f3WW~n7)x8h*bGS^g~q$XV^h;LBeUs~rK8qI4#QT`WvyjwCCLV#SCC+ABaJFz z7mzeN$}RQ^i`i_du$NlO9k!Y>tI1-unXM+%t};gj_-r3}NF~`rKdU3p1*>3T%t{hN zqVe$N^V5@T#(8gd!4~W0%E!+N^TrF$=Qduw)IOq{_~1r_SO)V*{@-`AGgx(oSPRNeW^2lq7~Z5Y5D_?ny>I z8Jf8J2=5e5pPPH6elANsZv*cZK|gm3mMtF}tR#}L#oyf^`s6y{HMa6@zefa(H9RP} zoj%?n0>&CoukZ&XuT1JD7m=-Q*%t^i*-PVt=AHkt$X*eYS^5oKW)9wNbq6HBY_$0T zqOI{@mvGB$uD+ydeT=Qv+bPi@pRGPqpGN9vR4m70%GeaEp^bs&bnvr~FV(gfU%1LZtS|nbd}W4R#rf)4CSO<@`^KIjf0D#mLH?jY z(?tP!NLo~yny;mTNy7i^0r4FCWD diff --git a/Content/Samples/BasicLoader/Blueprints/Default__BP_LoaderDemoUI_C.uasset b/Content/Samples/BasicLoader/Blueprints/Default__BP_LoaderDemoUI_C.uasset deleted file mode 100644 index 64cf9ecc8d8ba3f8133bd77c2b9054941b5fc5e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1641 zcmb7FO-vI(6n=mrKY|E&K*h)j6lp1;P=p|C7aB}KgfyN^9orFCx7}uUQ1E0zV!)Uf zqh2%`LpbP76K|d%;igv)9tjr`G)j;l{?zxjTimQG$V+D5%=`P^ym>P_J8}H!dODrH zkOxqd3$TowC)i$BoT)FyYg`Q<=QV?dI7$sP_O0s=wNE&vQ>t`~4lw1S*QE<$wq)@~&| zeWz#gD@vEgCzPqL;gJ{f&!syL^H0A2GWVD=q&SoSWt0bxs=IQ^aaLa+>G9VOag{TJ zH~2k`rXJwa^s#^F(aEIN##eV~(un~%C1)cnQkr&8m^3FMT(c6ZdGjUJxEN%yt89da zwHQ;C8%!u#a)2eckzz3pi0fnb4&iExBp3Kp-C?_lGX>m8*@o|PA=>nF z!DJ>FEZ%&5e?JN`i45_$qGLju4$Dic;Q~}xRR$w8d7$jidNaV26EnO5$o!1Jawo#bX$P{xtTns*6k@s&Wl4jm2FHIr^QJX^sN}Qw GllcwiQEIvX diff --git a/Content/Samples/BasicLoader/Blueprints/WBP_LoaderDemoUI.uasset b/Content/Samples/BasicLoader/Blueprints/WBP_LoaderDemoUI.uasset deleted file mode 100644 index 2e1f21128aff44dfebf3d625c3eb8d3ba4937852..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2489 zcmcImTWkzb7(P|Ex~oexQPHQC)tzqHwh0lPz1h_kl~pgg#_nkid&|zCE{P_TMnnh_ zBt#mTNF+S)Af7yUAUyFPUhyDtPa`PS|IMX)=~OSNFFAA0`Tu{;|DFGw|DSnybotfq z>2!KWA!E}fF!r8$pn%$kU3cER6VLZef92?@f8e!Okj!1g*d*$k5^+u)WAmx4CF?Tk zYaF#s(d%-ra@4Hy){3q=N40llRb9PUUG4OEMKA1GCwZ7i{Y83AG3M(hL&MlE>e0?n z^Ka;VvcIi3aDMlSL-$Wt_+AT-1K+-E3!J=q`@~_-@wZ@zbwB`&3fbk-bp^#Vve|+j zr>&Vsxuoz)r@czkGxo7(*${eqNki+4{wfS?MC=~zG#eD8GVJ!4b44XZVe0;lvx{lw=55m6 zs2U38GY`4goS%-*i>z&ee$t;Rbp{ahMy^V3NtM`}&b#O4l0fIz%p-D&c#`k;@A?|1 zlEMf#Q+(YE=49pr3tU-oonwJ~GVJAZd)GV*1+mz?y6D-=92JGFf8nQ&*?B^;?X9?f z6MkFdNIO>t&W&wseSB%jU^yK)Pf03|=rP;(^jZmSFn~+|e$lO(rVn$TR(8{KEcA(K zhjTPxq_jEJdMi6qmThRhizouWiCH0Cxp|9U_wtWDJ1d1F$pK)+PFr#Odrg zP6R;q)oUZY78ygWUZElkhx~P6|0mBEZWe&On|tWW+BE$9%a^GeiLY63M9B7a;XnDp z1qQNx-54idSQlsWgErky4Kes>^pHV65`dd!$`4muqI3f-UxaW_{&x&zJw#DBM)q|^ zs6cTzHkM2f6Xb`RE2IF?}S0e!E2h*4ER4lks7EPDaUdD|qJC zx-k>Ya0&X$wR(_|=1g%(2zj#t&e^O1Wi%eqZ;`nWlE)mQ OM-pYvkxUQir|mDgdo&~f diff --git a/Content/Samples/BasicLoader/Blueprints/WBP_LoaderUI.uasset b/Content/Samples/BasicLoader/Blueprints/WBP_LoaderUI.uasset deleted file mode 100644 index 19ccc2e14a034c52fa299696b0cdf8910680c456..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2482 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 delta 5101 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

_`5g^*NIoeFUITaA0ZE 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 Duo*Yz diff --git a/Content/Samples/BasicUI/Blueprints/WBP_RpmAssetPanel.uasset b/Content/Samples/BasicUI/Blueprints/WBP_RpmAssetPanel.uasset index 64d0cb293f0f4b67ee30e5cce40e8588480f12fc..974abbcf352b7676ca5167bc379798f4a5cb9567 100644 GIT binary patch literal 55939 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 delta 5846 zcmd5=3s98T6~4mauDBc}c)Wg7^+t9ts##KnbErjG(N5;I1qS#-`48bZW;` zW65ouHbIkECy!|@#tj){(nMmSb@~{lGLuvj%@|tKV6paLrrN1J-~Zpc`C)%_r!(pF zFzmVa-0z%w?m6c^c6axL?t9zt{kLNmiwbv#xR!jdQcE(DcePQ^S(5oeVHwS4WC*tcm;Ua z+0H8rRY^}yWi0YZ>T2i?5$3Pv?;2WpRmdwmBeaem4o&03q04z@cq)HB+!i;9G>Y0i z9+$JK>6;EV|KmP4zY)HOZ<&zFU!9N?RqtqVG}L(&JRUaNI;AbO$zz-VJ`E}!b?wq)o|9j$Weqhp3UNd=Wx<3+s5B(r2 zhSV!IZgFh#_ze_Z^nJm*qVjllbQ*7qPUSy|Uc+-^8hLNb=D7{-x^0b>bq$U-W=lIV z$4KgHNZIAiS<0%jTZ~vpO;de?!}F-y;b6^K`)@tWz2@26We(>#<`w)Ma}VDan;1QT zfJ<8H+S-_B`%|6#PHeL&l0?PzrI>+*x!zpQyW_G`OC7GQ4v%{WOR6b)8jD??Q|_vF zY$)z)@m_vhYxZwK_Pt;H@(MeI8d#OoKmw)4g!Rfw zW+bO78Ic#i7V^Yb5M)CSazcer8F*14ST;CH4Jkn%95QfGg8p=p6U%6jNEMQv+h?11Ae584Ptb7HhxXUkD5`1lW=1@yB?c-3gh`;B^74U4|kMmP3z5(aY zLijcJps&(h8{&I^&n|-%TV>?B(*O@ZvSRbNd4GKU!>Ul30Zjw}l&3Ov#o8##h)STu zF*aQ^R|qZCznDZ%_2*h*9Z+oRNJ=vBKT}%~zQcTcuej1acm; z=u0m|mqL{!nkj-KJ)o&7D`hNMZ7CIkW}pq!3n*}8fjYp-_^j?m>q5~Bj0vrjpXoL& zmi_{W>H;C&T_5+Hy86S56`k`EZadP`U(%B;=ChAkjaEg6v0RE?d^mzssO+?32eY%F zRlB5hVYRtt<=)roe!BN$p-oS=Qn8ewv6N`Y9c88Q=C?X;_eB--T^a1yoHQi@R>sO6 zBrEZ}`;GgJ*jFI<1WWz)G;TWai1q8pe1bX^laU-h^NR#>mf-%%8X!Rut5?P2)W!d% zEDT-N6+?Nen-ancj+{CC;z!TSKPcA)?+|qmDy7SvHRnTvsqEzK^Gp74bJit2*?1L_ z;Lk0zX6&vSTs`x~^4Hr#KkwR86Qd`)YlVC*c`e66iRAkKi@d&M_}VnP@BA zcq=Xoh3DK{lkSLGU3%1ZreN3U>E zrq2+@U+#?)pYxPWe6Dv{>T*P%NG9MTP0r-Xmbx8G==d&u5N|L@h3K=V zLZmg{KhJfBG-2n#k3{+h!Mx%JHjC~6oYNg3x&D%X{suqr>zUqVz!1s(F7GILCo1|7 z%7MU(NE^tY%;8-m%z1S#HmyDJEb+e*uvCTc1fm~?6q3IB2XQF`WhDYh_nYCzd*X|< zdWbkEU;@0@V%0G=_QtKEI@n!iL|;Z$0ZWh+{#<~jU|U=7AEP33`Us6CXCg+*4^!%oSWCx%WoD4M zOmDFm%@o{URg8W6U0EGjVo)1lMMNuA_B}e%t}>u}U=$=5ArUiFF+TaP?9rP(+wl6? zi;a&LKDTk?MDN;92IRB=$geyBQ{(Fb4hxVptoCpUXiXr|v{s8Z@hJNjwdo6hLjGE! zo)(WnUZO$*_X2hzat@NGh+L=iQ5gVm^v$$IViILV!2v}OQ{c%^ZRp%ZKZsr gCtk*Q<%P6S_rSP%II{eDyy8N-NjqKt?&*jB1I!ae82|tP diff --git a/Content/Samples/BasicUI/Blueprints/WBP_RpmBasicUI.uasset b/Content/Samples/BasicUI/Blueprints/WBP_RpmBasicUI.uasset index b8ae41ed0a2793985192f6ae1eb549ddaae5b182..f962c0d50ab364ae299a12fde6417b3373549755 100644 GIT binary patch literal 70421 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 z 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 EKR~53`Tzg` diff --git a/Content/Samples/BasicUI/Blueprints/WBP_RpmPaginator.uasset b/Content/Samples/BasicUI/Blueprints/WBP_RpmPaginator.uasset new file mode 100644 index 0000000000000000000000000000000000000000..de4d000d3889b0e599f0623be69fe18306b14d72 GIT binary patch literal 36487 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

o8J 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 diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp index 4a26748..f7623f0 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp @@ -6,29 +6,48 @@ #include "Api/Assets/Models/AssetListResponse.h" #include "Api/Assets/Models/AssetTypeListRequest.h" #include "Api/Auth/ApiKeyAuthStrategy.h" +#include "Cache/AssetCacheManager.h" +#include "Interfaces/IHttpResponse.h" +struct FCachedAssetData; struct FAssetTypeListRequest; const FString FAssetApi::BaseModelType = TEXT("baseModel"); -FAssetApi::FAssetApi() +FAssetApi::FAssetApi() : ApiRequestStrategy(EApiRequestStrategy::FallbackToCache) { - OnApiResponse.BindRaw(this, &FAssetApi::HandleResponse); + Initialize(); +} - const URpmDeveloperSettings* Settings = GetDefault(); +FAssetApi::FAssetApi(EApiRequestStrategy InApiRequestStrategy) : ApiRequestStrategy(InApiRequestStrategy) +{ + Initialize(); +} - if(Settings->ApplicationId.IsEmpty()) +void FAssetApi::Initialize() +{ + if (bIsInitialized) return; + + const URpmDeveloperSettings* Settings = GetDefault(); + OnRequestComplete.BindRaw(this, &FAssetApi::HandleAssetResponse); + if (Settings->ApplicationId.IsEmpty()) { UE_LOG(LogReadyPlayerMe, Error, TEXT("Application ID is empty. Please set the Application ID in the Ready Player Me Developer Settings")); } - - if(!Settings->ApiKey.IsEmpty()) + + if (!Settings->ApiKey.IsEmpty() || Settings->ApiProxyUrl.IsEmpty()) { - SetAuthenticationStrategy(new FApiKeyAuthStrategy()); + SetAuthenticationStrategy(MakeShared()); } + bIsInitialized = true; } void FAssetApi::ListAssetsAsync(const FAssetListRequest& Request) { + if(ApiRequestStrategy == EApiRequestStrategy::CacheOnly) + { + LoadAssetsFromCache(Request.BuildQueryMap()); + return; + } const URpmDeveloperSettings* RpmSettings = GetDefault(); ApiBaseUrl = RpmSettings->GetApiBaseUrl(); if(RpmSettings->ApplicationId.IsEmpty()) @@ -37,17 +56,22 @@ void FAssetApi::ListAssetsAsync(const FAssetListRequest& Request) 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; + const FString Url = FString::Printf(TEXT("%s/v1/phoenix-assets"), *ApiBaseUrl); + TSharedPtr ApiRequest = MakeShared(); + ApiRequest->Url = Url; + ApiRequest->Method = GET; + ApiRequest->QueryParams = Request.BuildQueryMap(); DispatchRawWithAuth(ApiRequest); } void FAssetApi::ListAssetTypesAsync(const FAssetTypeListRequest& Request) { + if(ApiRequestStrategy == EApiRequestStrategy::CacheOnly) + { + LoadAssetTypesFromCache(); + return; + } URpmDeveloperSettings* Settings = GetMutableDefault(); ApiBaseUrl = Settings->GetApiBaseUrl(); if(Settings->ApplicationId.IsEmpty()) @@ -58,64 +82,131 @@ void FAssetApi::ListAssetTypesAsync(const FAssetTypeListRequest& Request) } FString QueryString = Request.BuildQueryString(); const FString Url = FString::Printf(TEXT("%s/v1/phoenix-assets/types%s"), *ApiBaseUrl, *QueryString); - FApiRequest ApiRequest = FApiRequest(); - ApiRequest.Url = Url; - ApiRequest.Method = GET; + TSharedPtr ApiRequest = MakeShared(); + ApiRequest->Url = Url; + ApiRequest->Method = GET; - DispatchRawWithAuth(ApiRequest); + DispatchRawWithAuth( ApiRequest); +} + +void FAssetApi::HandleAssetResponse(TSharedPtr ApiRequest, FHttpResponsePtr Response, bool bWasSuccessful) +{ + const bool bIsTypeRequest = ApiRequest->Url.Contains(TEXT("/types")); + if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) + { + FAssetTypeListResponse AssetTypeListResponse; + + if (bIsTypeRequest && FJsonObjectConverter::JsonObjectStringToUStruct(Response->GetContentAsString(), &AssetTypeListResponse, 0, 0)) + { + OnListAssetTypeResponse.ExecuteIfBound(AssetTypeListResponse, true); + return; + } + FAssetListResponse AssetListResponse; + if (!bIsTypeRequest && FJsonObjectConverter::JsonObjectStringToUStruct(Response->GetContentAsString(), &AssetListResponse, 0, 0)) + { + OnListAssetsResponse.ExecuteIfBound(AssetListResponse, true); + return; + } + } + + if(ApiRequestStrategy == EApiRequestStrategy::ApiOnly) + { + if(bIsTypeRequest) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("AssetApi:ListAssetTypesAsync request failed: %s"), *Response->GetContentAsString()); + OnListAssetTypeResponse.ExecuteIfBound(FAssetTypeListResponse(), false); + return; + } + UE_LOG(LogReadyPlayerMe, Error, TEXT("AssetApi:ListAssetsAsync request failed: %s"), *Response->GetContentAsString()); + OnListAssetsResponse.ExecuteIfBound(FAssetListResponse(), false); + return; + } + if(bIsTypeRequest) + { + UE_LOG(LogReadyPlayerMe, Warning, TEXT("FAssetApi::ListAssetTypesAsync request failed, falling back to cache.")); + LoadAssetTypesFromCache(); + return; + } + UE_LOG(LogReadyPlayerMe, Warning, TEXT("FAssetApi::ListAssetsAsync request failed, falling back to cache.")); + LoadAssetsFromCache(ApiRequest->QueryParams); } -void FAssetApi::HandleResponse(FString Response, bool bWasSuccessful) +void FAssetApi::LoadAssetsFromCache(TMap QueryParams) { - if (bWasSuccessful) - { - TSharedPtr JsonObject; - TSharedRef> Reader = TJsonReaderFactory<>::Create(Response); - - if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid()) - { - // Check if the "data" field is an array - const TArray>* DataArray; - if (JsonObject->TryGetArrayField(TEXT("data"), DataArray)) - { - if (DataArray->Num() > 0) - { - // Check the type of the first element to determine the response type - const TSharedPtr& FirstElement = (*DataArray)[0]; - - if (FirstElement->Type == EJson::Object) - { - // Assume this is an FAssetListResponse - FAssetListResponse AssetListResponse; - if (FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), &AssetListResponse, 0, 0)) - { - OnListAssetsResponse.ExecuteIfBound(AssetListResponse, true); - return; - } - } - else if (FirstElement->Type == EJson::String) - { - // Assume this is an FAssetTypeListResponse - FAssetTypeListResponse AssetTypeListResponse; - if (FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), &AssetTypeListResponse, 0, 0)) - { - OnListAssetTypeResponse.ExecuteIfBound(AssetTypeListResponse, true); - return; - } - } - } - } - } - - UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to parse JSON into known structs from response: %s"), *Response); - } - else - { - UE_LOG(LogReadyPlayerMe, Error, TEXT("API Response was unsuccessful")); - } - - // If all parsing attempts fail, execute with default/empty responses - OnListAssetsResponse.ExecuteIfBound(FAssetListResponse(), false); - OnListAssetTypeResponse.ExecuteIfBound(FAssetTypeListResponse(), false); + if(QueryParams.Num() < 1) + { + OnListAssetsResponse.ExecuteIfBound(FAssetListResponse(), false); + return; + } + const FString TypeKey = TEXT("type"); + const FString ExcludeTypeKey = TEXT("excludeType"); + FString Type = QueryParams.Contains(TypeKey) ? *QueryParams.Find(TypeKey) : FString(); + FString ExcludeType = QueryParams.Contains(ExcludeTypeKey) ? *QueryParams.Find(ExcludeTypeKey) : FString(); + TArray CachedAssets; + + if(ExcludeType.IsEmpty()) + { + CachedAssets = FAssetCacheManager::Get().GetAssetsOfType(Type); + } + else + { + auto ExtractQueryString = TEXT("excludeType=") + ExcludeType; + auto ExtractQueryArray = ExtractQueryValues(ExcludeType, ExcludeTypeKey); + CachedAssets = FAssetCacheManager::Get().GetAssetsExcludingTypes(ExtractQueryArray); + } + + if (CachedAssets.Num() > 0) + { + FAssetListResponse AssetListResponse; + for (const FCachedAssetData& CachedAsset : CachedAssets) + { + FAsset Asset = CachedAsset.ToAsset(); + AssetListResponse.Data.Add(Asset); + } + OnListAssetsResponse.ExecuteIfBound(AssetListResponse, true); + return; + } + UE_LOG(LogReadyPlayerMe, Warning, TEXT("No assets found in the cache.")); + OnListAssetsResponse.ExecuteIfBound(FAssetListResponse(), false); } +void FAssetApi::LoadAssetTypesFromCache() +{ + TArray AssetTypes = FAssetCacheManager::Get().LoadAssetTypes(); + if (AssetTypes.Num() > 0) + { + FAssetTypeListResponse AssetListResponse; + AssetListResponse.Data.Append(AssetTypes); + OnListAssetTypeResponse.ExecuteIfBound(AssetListResponse, true); + return; + } + UE_LOG(LogReadyPlayerMe, Warning, TEXT("No assets found in the cache.")); + OnListAssetTypeResponse.ExecuteIfBound(FAssetTypeListResponse(), false); +} + +TArray FAssetApi::ExtractQueryValues(const FString& QueryString, const FString& Key) +{ + TArray Values; + TArray Pairs; + + // Split the query string by '&' to separate each key-value pair + QueryString.ParseIntoArray(Pairs, TEXT("&"), true); + + // Iterate through all pairs and check if they match the key + for (const FString& Pair : Pairs) + { + FString KeyPart, ValuePart; + + // Split the pair by '=' to get the key and value + if (Pair.Split(TEXT("="), &KeyPart, &ValuePart)) + { + // If the key matches the one we're looking for, add the value to the array + if (KeyPart.Equals(Key, ESearchCase::IgnoreCase)) + { + Values.Add(ValuePart); + } + } + } + + return Values; +} diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetGlbLoader.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetGlbLoader.cpp index fb8bab6..dcba160 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetGlbLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetGlbLoader.cpp @@ -23,11 +23,8 @@ void FAssetGlbLoader::LoadGlb(const FAsset& Asset, const FString& BaseModelId, b if(FFileHelper::LoadFileToArray(GlbData, *StoredGlbPath)) { OnGlbLoaded.ExecuteIfBound(Asset, GlbData); - UE_LOG(LogReadyPlayerMe, Log, TEXT("Loading GLB from cache")); return; } - - UE_LOG(LogReadyPlayerMe, Log, TEXT("Unable to load GLB from cache")); } const TSharedRef Context = MakeShared(Asset, BaseModelId, bStoreInCache); LoadGlb(Context); diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetIconLoader.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetIconLoader.cpp index cd0e0f2..b9d8a1b 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetIconLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetIconLoader.cpp @@ -22,10 +22,8 @@ void FAssetIconLoader::LoadIcon(const FAsset& Asset, bool bStoreInCache) if(FFileHelper::LoadFileToArray(IconData, * FFileUtility::GetFullPersistentPath(StoredAsset.RelativeIconFilePath))) { OnIconLoaded.ExecuteIfBound(Asset, IconData); - UE_LOG(LogReadyPlayerMe, Log, TEXT("Loading icon from cache")); return; } - UE_LOG(LogReadyPlayerMe, Log, TEXT("Unable to load icon from cache")); } const TSharedRef Context = MakeShared(Asset, "", bStoreInCache); LoadIcon(Context); diff --git a/Source/RpmNextGen/Private/Api/Auth/ApiKeyAuthStrategy.cpp b/Source/RpmNextGen/Private/Api/Auth/ApiKeyAuthStrategy.cpp index f92a24f..80632d2 100644 --- a/Source/RpmNextGen/Private/Api/Auth/ApiKeyAuthStrategy.cpp +++ b/Source/RpmNextGen/Private/Api/Auth/ApiKeyAuthStrategy.cpp @@ -1,4 +1,6 @@ #include "Api/Auth/ApiKeyAuthStrategy.h" + +#include "RpmNextGen.h" #include "Settings/RpmDeveloperSettings.h" class URpmSettings; @@ -7,26 +9,25 @@ FApiKeyAuthStrategy::FApiKeyAuthStrategy() { } -void FApiKeyAuthStrategy::AddAuthToRequest(TSharedPtr Request) +void FApiKeyAuthStrategy::AddAuthToRequest(TSharedPtr ApiRequest) { const URpmDeveloperSettings* RpmSettings = GetDefault(); if(RpmSettings->ApiKey.IsEmpty()) { UE_LOG(LogReadyPlayerMe, Error, TEXT("API Key is empty")); - OnAuthComplete.ExecuteIfBound(false); + OnAuthComplete.ExecuteIfBound(ApiRequest, false); return; } - Request->Headers.Add(TEXT("X-API-KEY"), RpmSettings->ApiKey); - OnAuthComplete.ExecuteIfBound(true); + ApiRequest->Headers.Add(TEXT("X-API-KEY"), RpmSettings->ApiKey); + OnAuthComplete.ExecuteIfBound(ApiRequest, true); } -void FApiKeyAuthStrategy::OnRefreshTokenResponse(const FRefreshTokenResponse& Response, bool bWasSuccessful) +void FApiKeyAuthStrategy::OnRefreshTokenResponse(TSharedPtr ApiRequest, const FRefreshTokenResponse& Response, bool bWasSuccessful) { + OnTokenRefreshed.ExecuteIfBound(ApiRequest, Response.Data, bWasSuccessful); } -void FApiKeyAuthStrategy::TryRefresh(TSharedPtr Request) +void FApiKeyAuthStrategy::TryRefresh(TSharedPtr ApiRequest) { + OnAuthComplete.ExecuteIfBound(ApiRequest, false); } - - - diff --git a/Source/RpmNextGen/Private/Api/Auth/AuthApi.cpp b/Source/RpmNextGen/Private/Api/Auth/AuthApi.cpp index fa43fa2..9b7dca3 100644 --- a/Source/RpmNextGen/Private/Api/Auth/AuthApi.cpp +++ b/Source/RpmNextGen/Private/Api/Auth/AuthApi.cpp @@ -1,4 +1,6 @@ #include "Api/Auth/AuthApi.h" + +#include "RpmNextGen.h" #include "Interfaces/IHttpResponse.h" #include "Api/Auth/Models/RefreshTokenRequest.h" #include "Api/Auth/Models/RefreshTokenResponse.h" @@ -8,27 +10,45 @@ FAuthApi::FAuthApi() { const URpmDeveloperSettings* RpmSettings = GetDefault(); ApiUrl = FString::Printf(TEXT("%s/refresh"), *RpmSettings->ApiBaseAuthUrl); + OnRequestComplete.BindRaw(this, &FAuthApi::OnProcessComplete); } 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(); + TSharedPtr ApiRequest = MakeShared(); + 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) +void FAuthApi::OnProcessComplete(TSharedPtr ApiRequest, FHttpResponsePtr Response, bool bWasSuccessful) { - FString data = Response->GetContentAsString(); - - FRefreshTokenResponse TokenResponse; - if (bWasSuccessful && !data.IsEmpty() && FJsonObjectConverter::JsonObjectStringToUStruct(data, &TokenResponse, 0, 0)) + if (!ApiRequest.IsValid()) { - OnRefreshTokenResponse.ExecuteIfBound(TokenResponse, true); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Invalid ApiRequest in FAuthApi::OnProcessComplete.")); return; } - OnRefreshTokenResponse.ExecuteIfBound(FRefreshTokenResponse(), false); + + if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) + { + FString Data = Response->GetContentAsString(); + FRefreshTokenResponse TokenResponse; + + if (!Data.IsEmpty() && FJsonObjectConverter::JsonObjectStringToUStruct(Data, &TokenResponse, 0, 0)) + { + if (OnRefreshTokenResponse.IsBound()) + { + OnRefreshTokenResponse.ExecuteIfBound(ApiRequest, TokenResponse, true); + } + return; + } + } + + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to refresh token")); + if (OnRefreshTokenResponse.IsBound()) + { + OnRefreshTokenResponse.ExecuteIfBound(ApiRequest, FRefreshTokenResponse(), false); + } } diff --git a/Source/RpmNextGen/Private/Api/Characters/CharacterApi.cpp b/Source/RpmNextGen/Private/Api/Characters/CharacterApi.cpp index bbb60e6..8c83bf6 100644 --- a/Source/RpmNextGen/Private/Api/Characters/CharacterApi.cpp +++ b/Source/RpmNextGen/Private/Api/Characters/CharacterApi.cpp @@ -12,12 +12,11 @@ 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()); + SetAuthenticationStrategy(MakeShared()); } + OnRequestComplete.BindRaw(this, &FCharacterApi::HandleCharacterResponse); } FCharacterApi::~FCharacterApi() @@ -27,31 +26,31 @@ FCharacterApi::~FCharacterApi() void FCharacterApi::CreateAsync(const FCharacterCreateRequest& Request) { AssetByType.Append(Request.Data.Assets); - 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")); + TSharedPtr ApiRequest = MakeShared(); + 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) { AssetByType.Append(Request.Payload.Assets); - 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")); + TSharedPtr ApiRequest = MakeShared(); + 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")); + TSharedPtr ApiRequest = MakeShared(); + ApiRequest->Url = FString::Printf(TEXT("%s/%s"), *BaseUrl, *Request.Id); + ApiRequest->Method = GET; + ApiRequest->Headers.Add(TEXT("Content-Type"), TEXT("application/json")); DispatchRawWithAuth(ApiRequest); } @@ -62,41 +61,75 @@ FString FCharacterApi::GeneratePreviewUrl(const FCharacterPreviewRequest& Reques return url; } -void FCharacterApi::OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) +void FCharacterApi::HandleCharacterResponse(TSharedPtr ApiRequest, FHttpResponsePtr Response, bool bWasSuccessful) { - FWebApiWithAuth::OnProcessResponse(Request, Response, bWasSuccessful); + const FString Verb = ApiRequest->GetVerb(); bool bSuccess = bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode()); - if (Response->GetResponseCode() == 401) + if (Response.IsValid() && Response->GetResponseCode() == 401) { UE_LOG(LogReadyPlayerMe, 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.")); - return; + bSuccess = false; } - - const FString Verb = Request->GetVerb(); - if (Verb == "POST") + switch (ApiRequest->Method) + { + case POST: + HandleCharacterCreateResponse(Response, bSuccess); + break; + case PATCH: + HandleUpdateResponse( Response, bSuccess); + break; + case GET: + HandleFindResponse(Response, bSuccess); + break; + default: + break; + } +} + +void FCharacterApi::HandleCharacterCreateResponse(FHttpResponsePtr Response, bool bWasSuccessful) +{ + if(bWasSuccessful) { FCharacterCreateResponse CharacterCreateResponse; - bSuccess = bSuccess && FJsonObjectConverter::JsonObjectStringToUStruct( - Response->GetContentAsString(), &CharacterCreateResponse, 0, 0); - OnCharacterCreateResponse.ExecuteIfBound(CharacterCreateResponse, bSuccess); + if(FJsonObjectConverter::JsonObjectStringToUStruct( Response->GetContentAsString(), &CharacterCreateResponse, 0, 0)) + { + OnCharacterCreateResponse.ExecuteIfBound(CharacterCreateResponse, true); + return; + } } - else if (Verb == "PATCH") + UE_LOG(LogReadyPlayerMe, Warning, TEXT("Character CREATE request failed.")); + OnCharacterCreateResponse.ExecuteIfBound(FCharacterCreateResponse(), false); +} + +void FCharacterApi::HandleUpdateResponse(FHttpResponsePtr Response, bool bWasSuccessful) +{ + if(bWasSuccessful) { FCharacterUpdateResponse CharacterUpdateResponse; - bSuccess = bSuccess && FJsonObjectConverter::JsonObjectStringToUStruct( - Response->GetContentAsString(), &CharacterUpdateResponse, 0, 0); - OnCharacterUpdateResponse.ExecuteIfBound(CharacterUpdateResponse, bSuccess); + if(FJsonObjectConverter::JsonObjectStringToUStruct( Response->GetContentAsString(), &CharacterUpdateResponse, 0, 0)) + { + OnCharacterUpdateResponse.ExecuteIfBound(CharacterUpdateResponse, true); + return; + } } - else if (Verb == "GET") + UE_LOG(LogReadyPlayerMe, Warning, TEXT("Character UPDATE request failed.")); + OnCharacterUpdateResponse.ExecuteIfBound(FCharacterUpdateResponse(), false); +} + +void FCharacterApi::HandleFindResponse(FHttpResponsePtr Response, bool bWasSuccessful) +{ + if(bWasSuccessful) { FCharacterFindByIdResponse CharacterFindByIdResponse; - bSuccess = bSuccess && FJsonObjectConverter::JsonObjectStringToUStruct( - Response->GetContentAsString(), &CharacterFindByIdResponse, 0, 0); - OnCharacterFindResponse.ExecuteIfBound(CharacterFindByIdResponse, bSuccess); - } - else - { - UE_LOG(LogReadyPlayerMe, Warning, TEXT("Unhandled verb")); + if(FJsonObjectConverter::JsonObjectStringToUStruct( Response->GetContentAsString(), &CharacterFindByIdResponse, 0, 0)) + { + OnCharacterFindResponse.ExecuteIfBound(CharacterFindByIdResponse, true); + return; + } } + UE_LOG(LogReadyPlayerMe, Warning, TEXT("Character FIND request failed.")); + OnCharacterFindResponse.ExecuteIfBound(FCharacterFindByIdResponse(), false); } + + diff --git a/Source/RpmNextGen/Private/Api/Common/WebApi.cpp b/Source/RpmNextGen/Private/Api/Common/WebApi.cpp index 29b56f5..cd5a1ff 100644 --- a/Source/RpmNextGen/Private/Api/Common/WebApi.cpp +++ b/Source/RpmNextGen/Private/Api/Common/WebApi.cpp @@ -1,7 +1,6 @@ #include "Api/Common/WebApi.h" #include "HttpModule.h" #include "RpmNextGen.h" -#include "GenericPlatform/GenericPlatformHttp.h" #include "Interfaces/IHttpResponse.h" FWebApi::FWebApi() @@ -9,38 +8,43 @@ FWebApi::FWebApi() Http = &FHttpModule::Get(); } -FWebApi::~FWebApi() {} - -void FWebApi::DispatchRaw(const FApiRequest& Data) +FWebApi::~FWebApi() { - TSharedRef Request = Http->CreateRequest(); - Request->SetURL(Data.Url); - Request->SetVerb(Data.GetVerb()); + +} - for (const auto& Header : Data.Headers) +void FWebApi::DispatchRaw(TSharedPtr ApiRequest) +{ + TSharedPtr Request = Http->CreateRequest(); + FString Url = ApiRequest->Url + BuildQueryString(ApiRequest->QueryParams); + Request->SetURL(Url); + Request->SetVerb(ApiRequest->GetVerb()); + Request->SetTimeout(10); + FString Headers; + for (const auto& Header : ApiRequest->Headers) { Request->SetHeader(Header.Key, Header.Value); + Headers.Append(FString::Printf(TEXT("%s: %s\n"), *Header.Key, *Header.Value)); } - if (!Data.Payload.IsEmpty()) + if (!ApiRequest->Payload.IsEmpty() && ApiRequest->Method != ERequestMethod::GET) { - Request->SetContentAsString(Data.Payload); + Request->SetContentAsString(ApiRequest->Payload); } - - Request->OnProcessRequestComplete().BindRaw(this, &FWebApi::OnProcessResponse); + Request->OnProcessRequestComplete().BindRaw(this, &FWebApi::OnProcessResponse, ApiRequest); Request->ProcessRequest(); } -void FWebApi::OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) +void FWebApi::OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, TSharedPtr ApiRequest) { if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) { - OnApiResponse.ExecuteIfBound(Response->GetContentAsString(), true); + OnRequestComplete.ExecuteIfBound(ApiRequest, Response, true); return; } FString ErrorMessage = Response.IsValid() ? Response->GetContentAsString() : TEXT("Request failed"); UE_LOG(LogReadyPlayerMe, Warning, TEXT("WebApi from URL %s request failed: %s"), *Request->GetURL(), *ErrorMessage); - OnApiResponse.ExecuteIfBound(Response->GetContentAsString(), false); + OnRequestComplete.ExecuteIfBound(ApiRequest, Response, false); } FString FWebApi::BuildQueryString(const TMap& QueryParams) @@ -51,9 +55,10 @@ FString FWebApi::BuildQueryString(const TMap& QueryParams) QueryString.Append(TEXT("?")); for (const auto& Param : QueryParams) { - QueryString.Append(FString::Printf(TEXT("%s=%s&"), *FGenericPlatformHttp::UrlEncode(Param.Key), *FGenericPlatformHttp::UrlEncode(Param.Value))); + QueryString.Append(FString::Printf(TEXT("%s=%s&"), *Param.Key, *Param.Value)); } QueryString.RemoveFromEnd(TEXT("&")); } + QueryString = QueryString.Replace( TEXT(" "), TEXT("%20") ); return QueryString; } diff --git a/Source/RpmNextGen/Private/Api/Common/WebApiWithAuth.cpp b/Source/RpmNextGen/Private/Api/Common/WebApiWithAuth.cpp index 5877a38..95a6b8c 100644 --- a/Source/RpmNextGen/Private/Api/Common/WebApiWithAuth.cpp +++ b/Source/RpmNextGen/Private/Api/Common/WebApiWithAuth.cpp @@ -1,75 +1,82 @@ #include "Api/Common/WebApiWithAuth.h" +#include "RpmNextGen.h" #include "Interfaces/IHttpResponse.h" -FWebApiWithAuth::FWebApiWithAuth() : ApiRequestData(nullptr), AuthenticationStrategy(nullptr) +FWebApiWithAuth::FWebApiWithAuth() : AuthenticationStrategy(nullptr) { FWebApi(); + SetAuthenticationStrategy(nullptr); } -FWebApiWithAuth::FWebApiWithAuth(IAuthenticationStrategy* InAuthenticationStrategy) : AuthenticationStrategy(nullptr) +FWebApiWithAuth::FWebApiWithAuth(const TSharedPtr& InAuthenticationStrategy) : AuthenticationStrategy(nullptr) { SetAuthenticationStrategy(InAuthenticationStrategy); } -void FWebApiWithAuth::SetAuthenticationStrategy(IAuthenticationStrategy* InAuthenticationStrategy) +void FWebApiWithAuth::SetAuthenticationStrategy(const TSharedPtr& InAuthenticationStrategy) { AuthenticationStrategy = InAuthenticationStrategy; - - if (AuthenticationStrategy != nullptr) + if (AuthenticationStrategy.IsValid()) { + AuthenticationStrategy->OnAuthComplete.Unbind(); + AuthenticationStrategy->OnTokenRefreshed.Unbind(); + AuthenticationStrategy->OnAuthComplete.BindRaw(this, &FWebApiWithAuth::OnAuthComplete); AuthenticationStrategy->OnTokenRefreshed.BindRaw(this, &FWebApiWithAuth::OnAuthTokenRefreshed); } } -void FWebApiWithAuth::OnAuthComplete(bool bWasSuccessful) +void FWebApiWithAuth::OnAuthComplete(TSharedPtr ApiRequest, bool bWasSuccessful) { - if(bWasSuccessful && ApiRequestData != nullptr) + if(bWasSuccessful && ApiRequest.IsValid()) { - DispatchRaw(*ApiRequestData); + DispatchRaw(ApiRequest); return; } - OnApiResponse.ExecuteIfBound(TEXT("Auth failed"), false); + OnRequestComplete.ExecuteIfBound(ApiRequest, FHttpResponsePtr() , false); } -void FWebApiWithAuth::OnAuthTokenRefreshed(const FRefreshTokenResponseBody& Response, bool bWasSuccessful) +void FWebApiWithAuth::OnAuthTokenRefreshed(TSharedPtr ApiRequest, const FRefreshTokenResponseBody& Response, bool bWasSuccessful) { - if(bWasSuccessful) + if(bWasSuccessful && ApiRequest.IsValid()) { const FString Key = TEXT("Authorization"); - if (ApiRequestData->Headers.Contains(Key)) + if (ApiRequest->Headers.Contains(Key)) { - ApiRequestData->Headers.Remove(Key); + ApiRequest->Headers.Remove(Key); } - ApiRequestData->Headers.Add(Key, FString::Printf(TEXT("Bearer %s"), *Response.Token)); - DispatchRaw(*ApiRequestData); + ApiRequest->Headers.Add(Key, FString::Printf(TEXT("Bearer %s"), *Response.Token)); + DispatchRaw(ApiRequest); return; } - OnApiResponse.ExecuteIfBound(TEXT("Auth failed"), false); + OnRequestComplete.ExecuteIfBound(ApiRequest, FHttpResponsePtr() , false); } -void FWebApiWithAuth::DispatchRawWithAuth(FApiRequest& Data) +void FWebApiWithAuth::DispatchRawWithAuth(TSharedPtr ApiRequest) { - this->ApiRequestData = MakeShared(Data); if (AuthenticationStrategy == nullptr) { - DispatchRaw(Data); + DispatchRaw(ApiRequest); return; } - AuthenticationStrategy->AddAuthToRequest(this->ApiRequestData); + AuthenticationStrategy->AddAuthToRequest(ApiRequest); } -void FWebApiWithAuth::OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) +void FWebApiWithAuth::OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, TSharedPtr ApiRequest) { if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) { - OnApiResponse.ExecuteIfBound(Response->GetContentAsString(), true); - return; + OnRequestComplete.ExecuteIfBound(ApiRequest, Response, true); + } + else if(Response.IsValid() && Response->GetResponseCode() == EHttpResponseCodes::Denied && AuthenticationStrategy != nullptr) + { + AuthenticationStrategy->TryRefresh(ApiRequest); } - if(EHttpResponseCodes::Denied == Response->GetResponseCode() && AuthenticationStrategy != nullptr) + else { - AuthenticationStrategy->TryRefresh(ApiRequestData); + UE_LOG(LogReadyPlayerMe, Warning, TEXT("WebApi from URL %s request failed"), *Request->GetURL()); + OnRequestComplete.ExecuteIfBound(ApiRequest, Response, false); } } diff --git a/Source/RpmNextGen/Private/Api/Files/FileApi.cpp b/Source/RpmNextGen/Private/Api/Files/FileApi.cpp index 08bcb24..edd48a3 100644 --- a/Source/RpmNextGen/Private/Api/Files/FileApi.cpp +++ b/Source/RpmNextGen/Private/Api/Files/FileApi.cpp @@ -1,4 +1,4 @@ -#include "Api/Files/FileApi.h" +#include "Api/Files/FileApi.h" #include "HttpModule.h" #include "RpmNextGen.h" @@ -59,7 +59,7 @@ void FFileApi::AssetFileRequestComplete(FHttpRequestPtr Request, FHttpResponsePt OnAssetFileRequestComplete.ExecuteIfBound(&Content, Asset); return; } - UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load file from URL")); + UE_LOG(LogReadyPlayerMe, Warning, TEXT("Failed to load file from URL. Try loading from cache")); OnAssetFileRequestComplete.ExecuteIfBound(nullptr, Asset); } diff --git a/Source/RpmNextGen/Private/Api/Files/GlbLoader.cpp b/Source/RpmNextGen/Private/Api/Files/GlbLoader.cpp index 2e3a61f..aaf4d3f 100644 --- a/Source/RpmNextGen/Private/Api/Files/GlbLoader.cpp +++ b/Source/RpmNextGen/Private/Api/Files/GlbLoader.cpp @@ -1,4 +1,4 @@ -#include "Api/Files/GlbLoader.h" +#include "Api/Files/GlbLoader.h" #include "RpmNextGen.h" #include "glTFRuntime/Public/glTFRuntimeFunctionLibrary.h" #include "Api/Files//FileUtility.h" @@ -24,7 +24,7 @@ FGlbLoader::~FGlbLoader() delete FileWriter; } -void FGlbLoader::HandleFileRequestComplete(TArray* Data, const FString& FileName) +void FGlbLoader::HandleFileRequestComplete(const TArray* Data, const FString& FileName) { UglTFRuntimeAsset* GltfAsset = nullptr; if (Data) diff --git a/Source/RpmNextGen/Private/Api/Files/PakFileUtility.cpp b/Source/RpmNextGen/Private/Api/Files/PakFileUtility.cpp index 4bbbd20..ae4fff4 100644 --- a/Source/RpmNextGen/Private/Api/Files/PakFileUtility.cpp +++ b/Source/RpmNextGen/Private/Api/Files/PakFileUtility.cpp @@ -22,11 +22,11 @@ void FPakFileUtility::CreatePakFile(const FString& PakFilePath) FPlatformProcess::WaitForProc(ProcHandle); FPlatformProcess::CloseProc(ProcHandle); - UE_LOG(LogTemp, Log, TEXT("Pak file and I/O Store files created successfully: %s"), *PakFilePath); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Pak file and I/O Store files created successfully: %s"), *PakFilePath); } else { - UE_LOG(LogTemp, Error, TEXT("Failed to create Pak and I/O Store files: %s"), *PakFilePath); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to create Pak and I/O Store files: %s"), *PakFilePath); } } @@ -83,7 +83,7 @@ void FPakFileUtility::ExtractPakFile(const FString& PakFilePath) { if(!FPaths::FileExists(PakFilePath) ) { - UE_LOG(LogTemp, Error, TEXT("Pak file does not exist: %s"), *PakFilePath); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Pak file does not exist: %s"), *PakFilePath); return; } const FString UnrealPakPath = FPaths::ConvertRelativePathToFull(FPaths::EngineDir() / TEXT("Binaries/Win64/UnrealPak.exe")); @@ -97,11 +97,11 @@ void FPakFileUtility::ExtractPakFile(const FString& PakFilePath) FPlatformProcess::WaitForProc(ProcHandle); FPlatformProcess::CloseProc(ProcHandle); - UE_LOG(LogTemp, Log, TEXT("Pak file extracted successfully to: %s"), *DestinationPath); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Pak file extracted successfully to: %s"), *DestinationPath); } else { - UE_LOG(LogTemp, Error, TEXT("Failed to extract Pak file: %s"), *PakFilePath); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to extract Pak file: %s"), *PakFilePath); } } @@ -109,7 +109,7 @@ void FPakFileUtility::ExtractFilesFromPak(const FString& PakFilePath) { if(!FPaths::FileExists(PakFilePath) ) { - UE_LOG(LogTemp, Error, TEXT("Pak file does not exist: %s"), *PakFilePath); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Pak file does not exist: %s"), *PakFilePath); return; } @@ -124,11 +124,11 @@ void FPakFileUtility::ExtractFilesFromPak(const FString& PakFilePath) FPlatformFileManager::Get().SetPlatformFile(*PakPlatformFile); if (!PakPlatformFile->Initialize(&InnerPlatformFile, TEXT(""))) { - UE_LOG(LogTemp, Error, TEXT("Failed to initialize Pak Platform File")); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to initialize Pak Platform File")); delete PakPlatformFile; return; } - UE_LOG(LogTemp, Log, TEXT("Initializing new Pak Platform File")); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Initializing new Pak Platform File")); } // Step 2: Define mount point and destination paths @@ -138,11 +138,11 @@ void FPakFileUtility::ExtractFilesFromPak(const FString& PakFilePath) // Step 3: Mount the Pak file if (!PakPlatformFile->Mount(*PakFilePath, 0, *MountPoint)) { - UE_LOG(LogTemp, Error, TEXT("Failed to mount Pak file: %s"), *PakFilePath); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to mount Pak file: %s"), *PakFilePath); //return; } - UE_LOG(LogTemp, Log, TEXT("Successfully mounted Pak file: %s"), *PakFilePath); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Successfully mounted Pak file: %s"), *PakFilePath); // Step 4: List files in the mounted Pak file TArray Files; @@ -150,11 +150,11 @@ void FPakFileUtility::ExtractFilesFromPak(const FString& PakFilePath) if (Files.Num() == 0) { - UE_LOG(LogTemp, Warning, TEXT("No files found in Pak file: %s"), *PakFilePath); + UE_LOG(LogReadyPlayerMe, Warning, TEXT("No files found in Pak file: %s"), *PakFilePath); return; } - UE_LOG(LogTemp, Log, TEXT("Found %d files in Pak file: %s"), Files.Num(), *PakFilePath); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Found %d files in Pak file: %s"), Files.Num(), *PakFilePath); // Step 5: Extract each file from the Pak for (const FString& File : Files) @@ -167,7 +167,7 @@ void FPakFileUtility::ExtractFilesFromPak(const FString& PakFilePath) // Step 6: Handle JSON files separately if (File.EndsWith(TEXT(".json"))) { - UE_LOG(LogTemp, Log, TEXT("Processing JSON file: %s"), *File); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Processing JSON file: %s"), *File); FString JsonContent; if (FFileHelper::LoadFileToString(JsonContent, *File)) { @@ -180,26 +180,26 @@ void FPakFileUtility::ExtractFilesFromPak(const FString& PakFilePath) if (FJsonSerializer::Deserialize(Reader, JsonObject)) { - UE_LOG(LogTemp, Log, TEXT("Successfully parsed JSON content")); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Successfully parsed JSON content")); } else { - UE_LOG(LogTemp, Warning, TEXT("Invalid JSON content in file: %s"), *File); + UE_LOG(LogReadyPlayerMe, Warning, TEXT("Invalid JSON content in file: %s"), *File); } // Save JSON file to disk if (FFileHelper::SaveStringToFile(JsonContent, *DestinationFilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) { - UE_LOG(LogTemp, Log, TEXT("Successfully saved JSON file: %s"), *DestinationFilePath); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Successfully saved JSON file: %s"), *DestinationFilePath); } else { - UE_LOG(LogTemp, Error, TEXT("Failed to save JSON file: %s"), *DestinationFilePath); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to save JSON file: %s"), *DestinationFilePath); } } else { - UE_LOG(LogTemp, Error, TEXT("Failed to read JSON file from Pak: %s"), *File); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to read JSON file from Pak: %s"), *File); } } else @@ -210,20 +210,20 @@ void FPakFileUtility::ExtractFilesFromPak(const FString& PakFilePath) { if (FFileHelper::SaveArrayToFile(FileData, *DestinationFilePath)) { - UE_LOG(LogTemp, Log, TEXT("Successfully extracted file: %s"), *DestinationFilePath); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Successfully extracted file: %s"), *DestinationFilePath); } else { - UE_LOG(LogTemp, Error, TEXT("Failed to save file: %s"), *DestinationFilePath); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to save file: %s"), *DestinationFilePath); } } else { - UE_LOG(LogTemp, Error, TEXT("Failed to read file from Pak: %s"), *File); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to read file from Pak: %s"), *File); } } } - UE_LOG(LogTemp, Log, TEXT("Finished extracting files from Pak: %s"), *PakFilePath); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Finished extracting files from Pak: %s"), *PakFilePath); } diff --git a/Source/RpmNextGen/Private/RpmActor.cpp b/Source/RpmNextGen/Private/RpmActor.cpp index 209efac..4aee3eb 100644 --- a/Source/RpmNextGen/Private/RpmActor.cpp +++ b/Source/RpmNextGen/Private/RpmActor.cpp @@ -105,12 +105,10 @@ void ARpmActor::RemoveMeshComponentsOfType(const FString& AssetType) // Remove components by type, or remove all if AssetType is empty or it's a new base model if (AssetType.IsEmpty() || AssetType == FAssetApi::BaseModelType) { - UE_LOG(LogReadyPlayerMe, Log, TEXT("Removing all mesh components")); RemoveAllMeshes(); } else if (LoadedMeshComponentsByAssetType.Contains(AssetType)) { - UE_LOG(LogReadyPlayerMe, Log, TEXT("Removing mesh components of type %s"), *AssetType); TArray& ComponentsToRemove = LoadedMeshComponentsByAssetType[AssetType]; for (USceneComponent* ComponentToRemove : ComponentsToRemove) { diff --git a/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp b/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp index 798be50..2bddb27 100644 --- a/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp +++ b/Source/RpmNextGen/Private/RpmFunctionLibrary.cpp @@ -6,76 +6,69 @@ #include "Api/Assets/AssetApi.h" #include "Api/Assets/Models/AssetListRequest.h" #include "Api/Assets/Models/AssetListResponse.h" -#include "Api/Auth/ApiKeyAuthStrategy.h" #include "Api/Files/PakFileUtility.h" #include "Cache/AssetCacheManager.h" #include "Cache/CachedAssetData.h" #include "Settings/RpmDeveloperSettings.h" -#include "Utilities/ConnectionManager.h" void URpmFunctionLibrary::FetchFirstAssetId(UObject* WorldContextObject, const FString& AssetType, FOnAssetIdFetched OnAssetIdFetched) { - if(!IsInternetConnected()) - { - TArray CachedAssets = FAssetCacheManager::Get().GetAssetsOfType(AssetType); - if( CachedAssets.Num() > 0) - { - OnAssetIdFetched.ExecuteIfBound(CachedAssets[0].Id); - return; - } - UE_LOG(LogReadyPlayerMe, Warning, TEXT("Unable to fetch first asset from cache.")); - return; - } + if (!WorldContextObject) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("WorldContextObject is null")); + OnAssetIdFetched.ExecuteIfBound(FString()); + return; + } - 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); + TSharedPtr AssetApi = MakeShared(); + TSharedPtr SharedDelegate = MakeShared(OnAssetIdFetched); - if (!WorldContextObject) - { - UE_LOG(LogReadyPlayerMe, Error, TEXT("WorldContextObject is null")); - return; - } + const URpmDeveloperSettings* RpmSettings = GetDefault(); + FAssetListQueryParams QueryParams; + QueryParams.Type = AssetType; + QueryParams.ApplicationId = RpmSettings->ApplicationId; + QueryParams.Limit = 1; + FAssetListRequest AssetListRequest = FAssetListRequest(QueryParams); - 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); - }); + TWeakObjectPtr WeakContextObject(WorldContextObject); - AssetApi->ListAssetsAsync(AssetListRequest); -} + AssetApi->OnListAssetsResponse.BindLambda([WeakContextObject, SharedDelegate, AssetApi, AssetType](const FAssetListResponse& Response, bool bWasSuccessful) + { + if (!WeakContextObject.IsValid()) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("WorldContextObject is no longer valid.")); + SharedDelegate->ExecuteIfBound(FString()); + return; + } -bool URpmFunctionLibrary::IsInternetConnected() -{ - return FConnectionManager::Get().IsConnected(); -} + if (bWasSuccessful && Response.Data.Num() > 0) + { + FString FirstAssetId = Response.Data[0].Id; + UE_LOG(LogReadyPlayerMe, Warning, TEXT("FirstAssetId fetched: %s"), *FirstAssetId); + SharedDelegate->ExecuteIfBound(FirstAssetId); + return; + } -void URpmFunctionLibrary::CheckInternetConnection(const FOnConnectionStatusRefreshedDelegate& OnConnectionStatusRefreshed) -{ - FConnectionManager::Get().OnConnectionStatusRefreshed.BindLambda([OnConnectionStatusRefreshed](bool bIsConnected) - { - OnConnectionStatusRefreshed.ExecuteIfBound(bIsConnected); - }); + // Fallback to cache if online request failed or returned no data + TArray Assets = FAssetCacheManager::Get().GetAssetsOfType(AssetType); + if (Assets.Num() > 0) + { + FString FirstAssetId = Assets[0].Id; + UE_LOG(LogReadyPlayerMe, Warning, TEXT("FirstAssetId fetched from cache: %s"), *FirstAssetId); + SharedDelegate->ExecuteIfBound(FirstAssetId); + return; + } - FConnectionManager::Get().CheckInternetConnection(); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to fetch FirstAssetId")); + SharedDelegate->ExecuteIfBound(FString()); + }); + + AssetApi->ListAssetsAsync(AssetListRequest); } void URpmFunctionLibrary::ExtractCachePakFile() { - FString PakFilePath = FFileUtility::GetFullPersistentPath(FPakFileUtility::CachePakFilePath); + const FString PakFilePath = FFileUtility::GetFullPersistentPath(FPakFileUtility::CachePakFilePath); FPakFileUtility::ExtractFilesFromPak(PakFilePath); } diff --git a/Source/RpmNextGen/Private/RpmLoaderComponent.cpp b/Source/RpmNextGen/Private/RpmLoaderComponent.cpp index 65a19db..4d6ac76 100644 --- a/Source/RpmNextGen/Private/RpmLoaderComponent.cpp +++ b/Source/RpmNextGen/Private/RpmLoaderComponent.cpp @@ -13,6 +13,7 @@ #include "Api/Characters/Models/CharacterUpdateResponse.h" #include "Api/Files/GlbLoader.h" #include "Cache/AssetCacheManager.h" +#include "GenericPlatform/GenericPlatformCrashContext.h" #include "Settings/RpmDeveloperSettings.h" URpmLoaderComponent::URpmLoaderComponent() @@ -42,32 +43,13 @@ void URpmLoaderComponent::BeginPlay() Super::BeginPlay(); } -void URpmLoaderComponent::CreateCharacter(const FString& BaseModelId, bool bUseCache) +void URpmLoaderComponent::CreateCharacter(const FString& BaseModelId) { CharacterData.BaseModelId = BaseModelId; - if(!FConnectionManager::Get().IsConnected() || bUseCache) - { - FCachedAssetData CachedAssetData; - if(FAssetCacheManager::Get().GetCachedAsset(BaseModelId, CachedAssetData)) - { - const FAsset AssetFromCache = CachedAssetData.ToAsset(); - CharacterData.Assets.Add(FAssetApi::BaseModelType, AssetFromCache); - OnCharacterCreated.Broadcast(CharacterData); - TArray Data; - if(FFileApi::LoadFileFromPath(CachedAssetData.GetGlbPathForBaseModelId(CharacterData.BaseModelId), Data)) - { - UglTFRuntimeAsset* GltfRuntimeAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(Data, GltfConfig); - if(!GltfRuntimeAsset) - { - UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load gltf asset")); - } - OnCharacterAssetLoaded.Broadcast(CharacterData, GltfRuntimeAsset); - } - return; - } - UE_LOG(LogReadyPlayerMe, Warning, TEXT("Unable to create character from cache. Will try to create from Url."), *CachedAssetData.Id); - } - + FAsset BaseModelAsset = FAsset(); + BaseModelAsset.Id = BaseModelId; + BaseModelAsset.Type = FAssetApi::BaseModelType; + CharacterData.Assets.Add( FAssetApi::BaseModelType, BaseModelAsset); FCharacterCreateRequest CharacterCreateRequest = FCharacterCreateRequest(); CharacterCreateRequest.Data.Assets = TMap(); CharacterCreateRequest.Data.Assets.Add(FAssetApi::BaseModelType, BaseModelId); @@ -99,22 +81,15 @@ void URpmLoaderComponent::LoadGltfRuntimeAssetFromCache(const FAsset& Asset) } } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load gltf asset from cache")); + OnNewAssetLoaded.Broadcast(Asset, nullptr); } -void URpmLoaderComponent::LoadCharacterFromAssetMapCache(TMap AssetMap) +void URpmLoaderComponent::LoadCharacterAssetsFromCache(TMap AssetMap) { - TMap ParamAssets; - for (auto Pairs : AssetMap) + for (auto Element : AssetMap) { - ParamAssets.Add(Pairs.Key, Pairs.Value.Id); + LoadGltfRuntimeAssetFromCache(Element.Value); } - - CharacterData.Assets = AssetMap; - FCharacterPreviewRequest PreviewRequest; - PreviewRequest.Id = CharacterData.Id; - PreviewRequest.Params.Assets = ParamAssets; - const FString& Url = CharacterApi->GeneratePreviewUrl(PreviewRequest); - LoadCharacterFromUrl(Url); } void URpmLoaderComponent::LoadAssetsFromCacheWithNewStyle() @@ -140,11 +115,10 @@ void URpmLoaderComponent::LoadAssetPreview(FAsset AssetData, bool bUseCache) if(bIsBaseModel) { CharacterData.BaseModelId = AssetData.Id; - UE_LOG(LogReadyPlayerMe, Warning, TEXT("Asset is %s setting BaseModelId to AssetId."), *AssetData.Type); } CharacterData.Assets.Add(AssetData.Type, AssetData); - if(!FConnectionManager::Get().IsConnected() || bUseCache) + if(CharacterData.Id.IsEmpty()) { LoadGltfRuntimeAssetFromCache(AssetData); if(bIsBaseModel && CharacterData.Assets.Num() > 1) @@ -166,8 +140,13 @@ void URpmLoaderComponent::LoadAssetPreview(FAsset AssetData, bool bUseCache) FileApi->LoadAssetFileFromUrl(Url, AssetData); } -void URpmLoaderComponent::HandleAssetLoaded(TArray* Data, const FAsset& Asset) +void URpmLoaderComponent::HandleAssetLoaded(const TArray* Data, const FAsset& Asset) { + if(!Data) + { + LoadGltfRuntimeAssetFromCache(Asset); + return; + } UglTFRuntimeAsset* GltfRuntimeAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(*Data, GltfConfig); if(!GltfRuntimeAsset) { @@ -176,8 +155,13 @@ void URpmLoaderComponent::HandleAssetLoaded(TArray* Data, const F OnNewAssetLoaded.Broadcast(Asset, GltfRuntimeAsset); } -void URpmLoaderComponent::HandleCharacterAssetLoaded(TArray* Data, const FString& FileName) +void URpmLoaderComponent::HandleCharacterAssetLoaded(const TArray* Data, const FString& FileName) { + if(!Data) + { + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load character asset data")); + return; + } UglTFRuntimeAsset* GltfRuntimeAsset = UglTFRuntimeFunctionLibrary::glTFLoadAssetFromData(*Data, GltfConfig); if(!GltfRuntimeAsset) { @@ -186,22 +170,28 @@ void URpmLoaderComponent::HandleCharacterAssetLoaded(TArray* Data OnCharacterAssetLoaded.Broadcast(CharacterData, GltfRuntimeAsset); } -void URpmLoaderComponent::HandleCharacterCreateResponse(FCharacterCreateResponse CharacterCreateResponse, - bool bWasSuccessful) +void URpmLoaderComponent::HandleCharacterCreateResponse(FCharacterCreateResponse CharacterCreateResponse, bool bWasSuccessful) { - CharacterData.Id = CharacterCreateResponse.Data.Id; + if(bWasSuccessful && CharacterCreateResponse.IsValid()) + { + CharacterData.Id = CharacterCreateResponse.Data.Id; + OnCharacterCreated.Broadcast(CharacterData); + LoadCharacterFromUrl(CharacterCreateResponse.Data.GlbUrl); + return; + } + + OnCharacterCreated.Broadcast(CharacterData); - LoadCharacterFromUrl(CharacterCreateResponse.Data.GlbUrl); + UE_LOG( LogReadyPlayerMe, Error, TEXT("Failed to create character from web Api. Falling back to cache.")); + LoadCharacterAssetsFromCache(CharacterData.Assets); } -void URpmLoaderComponent::HandleCharacterUpdateResponse(FCharacterUpdateResponse CharacterUpdateResponse, - bool bWasSuccessful) +void URpmLoaderComponent::HandleCharacterUpdateResponse(FCharacterUpdateResponse CharacterUpdateResponse, bool bWasSuccessful) { OnCharacterUpdated.Broadcast(CharacterData); } -void URpmLoaderComponent::HandleCharacterFindResponse(FCharacterFindByIdResponse CharacterFindByIdResponse, - bool bWasSuccessful) +void URpmLoaderComponent::HandleCharacterFindResponse(FCharacterFindByIdResponse CharacterFindByIdResponse, bool bWasSuccessful) { OnCharacterFound.Broadcast(CharacterData); } diff --git a/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp index 40d85fd..1cc17ca 100644 --- a/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp +++ b/Source/RpmNextGen/Private/Samples/RpmAssetPanelWidget.cpp @@ -2,30 +2,20 @@ #include "Samples/RpmAssetPanelWidget.h" - #include "RpmNextGen.h" #include "Api/Assets/AssetApi.h" #include "Api/Assets/Models/AssetListRequest.h" -#include "Api/Auth/ApiKeyAuthStrategy.h" #include "Cache/AssetCacheManager.h" #include "Cache/CachedAssetData.h" #include "Components/PanelWidget.h" #include "Components/SizeBox.h" #include "Samples/RpmAssetButtonWidget.h" #include "Settings/RpmDeveloperSettings.h" -#include "Utilities/ConnectionManager.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); } @@ -33,6 +23,8 @@ void URpmAssetPanelWidget::OnAssetListResponse(const FAssetListResponse& AssetLi { if(bWasSuccessful && AssetListResponse.Data.Num() > 0) { + Pagination = AssetListResponse.Pagination; + OnPaginationUpdated.Broadcast(Pagination); CreateButtonsFromAssets(AssetListResponse.Data); return; } @@ -52,7 +44,7 @@ void URpmAssetPanelWidget::CreateButtonsFromAssets(TArray Assets) { if(Assets.Num() < 1) { - UE_LOG(LogReadyPlayerMe, Warning, TEXT("No assets found") ); + UE_LOG(LogReadyPlayerMe, Error, TEXT("No assets found")); return; } for (auto Asset : Assets) @@ -63,10 +55,21 @@ void URpmAssetPanelWidget::CreateButtonsFromAssets(TArray Assets) void URpmAssetPanelWidget::ClearAllButtons() { - if(!AssetButtons.IsEmpty()) + if (ButtonContainer) { - AssetButtons.Empty(); + ButtonContainer->ClearChildren(); + + for (auto& ButtonPair : AssetButtonMap) + { + if (URpmAssetButtonWidget* ButtonWidget = Cast(ButtonPair.Value->GetDefaultObject())) + { + ButtonWidget->RemoveFromParent(); + ButtonWidget->ConditionalBeginDestroy(); + } + } } + + AssetButtonMap.Empty(); SelectedAssetButton = nullptr; } @@ -97,7 +100,7 @@ void URpmAssetPanelWidget::CreateButton(const FAsset& AssetData) } AssetButtonInstance->InitializeButton(AssetData, ImageSize); - AssetButtons.Add(AssetButtonBlueprint); + AssetButtonMap.Add(AssetData.Id, AssetButtonBlueprint); AssetButtonInstance->OnAssetButtonClicked.AddDynamic(this, &URpmAssetPanelWidget::OnAssetButtonClicked); } } @@ -121,21 +124,43 @@ void URpmAssetPanelWidget::OnAssetButtonClicked(const URpmAssetButtonWidget* Ass void URpmAssetPanelWidget::LoadAssetsOfType(const FString& AssetType) { + CurrentAssetType = AssetType; if (!AssetApi.IsValid()) { UE_LOG(LogReadyPlayerMe, Error, TEXT("AssetApi is null or invalid")); return; } - if(!FConnectionManager::Get().IsConnected()) - { - UE_LOG(LogReadyPlayerMe, Warning, TEXT("No internet connection, loading assets from cache")); - LoadAssetsFromCache(AssetType); - return; - } + const URpmDeveloperSettings* RpmSettings = GetDefault(); FAssetListQueryParams QueryParams; QueryParams.Type = AssetType; QueryParams.ApplicationId = RpmSettings->ApplicationId; - FAssetListRequest AssetListRequest = FAssetListRequest(QueryParams); + QueryParams.Limit = PaginationLimit; + QueryParams.Page = Pagination.Page; + const FAssetListRequest AssetListRequest = FAssetListRequest(QueryParams); AssetApi->ListAssetsAsync(AssetListRequest); } + +void URpmAssetPanelWidget::LoadNextPage() +{ + if (!Pagination.HasNextPage) + { + UE_LOG(LogReadyPlayerMe, Warning, TEXT("Already on the last page")); + return; + } + ClearAllButtons(); + Pagination.Page++; + LoadAssetsOfType(CurrentAssetType); +} + +void URpmAssetPanelWidget::LoadPreviousPage() +{ + if (!Pagination.HasPrevPage) + { + UE_LOG(LogReadyPlayerMe, Warning, TEXT("Already on the first page")); + return; + } + ClearAllButtons(); + Pagination.Page--; + LoadAssetsOfType(CurrentAssetType); +} diff --git a/Source/RpmNextGen/Private/Samples/RpmCategoryButtonWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmCategoryButtonWidget.cpp index f0c5d00..7382dca 100644 --- a/Source/RpmNextGen/Private/Samples/RpmCategoryButtonWidget.cpp +++ b/Source/RpmNextGen/Private/Samples/RpmCategoryButtonWidget.cpp @@ -1,6 +1,8 @@ // Fill out your copyright notice in the Description page of Project Settings. #include "Samples/RpmCategoryButtonWidget.h" + +#include "RpmNextGen.h" #include "Components/Button.h" #include "Components/Image.h" @@ -31,7 +33,6 @@ void URpmCategoryButtonWidget::InitializeButton(FString Category, UTexture2D* Im { CategoryImageTexture = Image; CategoryImage->SetBrushFromTexture(CategoryImageTexture); - UE_LOG( LogTemp, Warning, TEXT("Setting image on button for Category: %s"), *Category ); } } } diff --git a/Source/RpmNextGen/Private/Samples/RpmCategoryPanelWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmCategoryPanelWidget.cpp index 70feb1e..b6cf8a5 100644 --- a/Source/RpmNextGen/Private/Samples/RpmCategoryPanelWidget.cpp +++ b/Source/RpmNextGen/Private/Samples/RpmCategoryPanelWidget.cpp @@ -1,7 +1,7 @@ // Fill out your copyright notice in the Description page of Project Settings. #include "Samples/RpmCategoryPanelWidget.h" - +#include "RpmNextGen.h" #include "Api/Assets/AssetApi.h" #include "Api/Assets/Models/AssetTypeListRequest.h" #include "Blueprint/WidgetTree.h" @@ -14,6 +14,12 @@ class URpmDeveloperSettings; void URpmCategoryPanelWidget::NativeConstruct() { Super::NativeConstruct(); + if(bIsInitialized) + { + UE_LOG(LogReadyPlayerMe, Warning, TEXT("URpmCategoryPanelWidget Already initialized")); + return; + } + bIsInitialized = true; AssetApi = MakeShared(); AssetApi->OnListAssetTypeResponse.BindUObject(this, &URpmCategoryPanelWidget::AssetTypesLoaded); AssetButtons = TArray>(); @@ -30,12 +36,12 @@ void URpmCategoryPanelWidget::UpdateSelectedButton(URpmCategoryButtonWidget* Cat void URpmCategoryPanelWidget::LoadAndCreateButtons() { - URpmDeveloperSettings* Settings = GetMutableDefault(); - FAssetTypeListRequest AssetListRequest; + const URpmDeveloperSettings* Settings = GetDefault(); + FAssetTypeListRequest AssetTypeListRequest; FAssetTypeListQueryParams QueryParams = FAssetTypeListQueryParams(); QueryParams.ApplicationId = Settings->ApplicationId; - AssetListRequest.Params = QueryParams; - AssetApi->ListAssetTypesAsync(AssetListRequest); + AssetTypeListRequest.Params = QueryParams; + AssetApi->ListAssetTypesAsync(AssetTypeListRequest); } void URpmCategoryPanelWidget::OnCategoryButtonClicked(URpmCategoryButtonWidget* CategoryButton) @@ -70,7 +76,7 @@ void URpmCategoryPanelWidget::CreateButton(const FString& AssetType) AssetButtons.Add(CategoryButton->GetClass()); return; } - UE_LOG(LogTemp, Error, TEXT("Failed to Load %s button"), *AssetType); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to Load %s button"), *AssetType); } } } @@ -93,7 +99,7 @@ void URpmCategoryPanelWidget::AssetTypesLoaded(const FAssetTypeListResponse& Ass OnCategoriesLoaded.Broadcast(AssetTypeListResponse.Data); return; } - UE_LOG(LogTemp, Error, TEXT("Failed to load asset types")); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to load asset types")); OnCategoriesLoaded.Broadcast(TArray()); } diff --git a/Source/RpmNextGen/Private/Samples/RpmCreatorWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmCreatorWidget.cpp index 5e69eab..83bb6ac 100644 --- a/Source/RpmNextGen/Private/Samples/RpmCreatorWidget.cpp +++ b/Source/RpmNextGen/Private/Samples/RpmCreatorWidget.cpp @@ -1,6 +1,8 @@ // Fill out your copyright notice in the Description page of Project Settings. #include "Samples/RpmCreatorWidget.h" + +#include "RpmNextGen.h" #include "Blueprint/WidgetTree.h" #include "Components/VerticalBox.h" #include "Components/WidgetSwitcher.h" @@ -11,14 +13,18 @@ class UVerticalBox; void URpmCreatorWidget::NativeConstruct() { Super::NativeConstruct(); - IndexMapByCategory = TMap(); } void URpmCreatorWidget::CreateAssetPanelsFromCategories(const TArray& CategoryArray) { + if (CategoryArray.Num() == 0) + { + UE_LOG(LogReadyPlayerMe, Warning, TEXT("No categories to create asset panels from!")); + return; + } if (!AssetPanelSwitcher || !AssetPanelBlueprint) { - UE_LOG(LogTemp, Error, TEXT("WidgetSwitcher or WidgetBlueprintClass is not set!")); + UE_LOG(LogReadyPlayerMe, Error, TEXT("WidgetSwitcher or WidgetBlueprintClass is not set!")); return; } @@ -30,16 +36,19 @@ void URpmCreatorWidget::CreateAssetPanelsFromCategories(const TArray& C CreateAssetPanel(CategoryArray[i]); IndexMapByCategory.Add(CategoryArray[i], i ); } - SwitchToPanel(CategoryArray[0]); + if(IndexMapByCategory.Num() > 0) + { + SwitchToPanel(CategoryArray[0]); + } } void URpmCreatorWidget::SwitchToPanel(const FString& Category) { if(AssetPanelSwitcher) { - if(IndexMapByCategory[Category] == -1) + if(IndexMapByCategory.Num() < 1 || IndexMapByCategory[Category] == -1) { - UE_LOG(LogTemp, Error, TEXT("Category %s not found!"), *Category); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Category %s not found! Category length %d"), *Category, IndexMapByCategory.Num()); return; } AssetPanelSwitcher->SetActiveWidgetIndex(IndexMapByCategory[Category]); @@ -60,14 +69,14 @@ UUserWidget* URpmCreatorWidget::CreateAssetPanel(const FString& Category) { if (!AssetPanelBlueprint) { - UE_LOG(LogTemp, Error, TEXT("WidgetBlueprintClass is not set!")); + UE_LOG(LogReadyPlayerMe, Error, TEXT("WidgetBlueprintClass is not set!")); return nullptr; } UWorld* World = GetWorld(); if (!World) { - UE_LOG(LogTemp, Error, TEXT("World is null!")); + UE_LOG(LogReadyPlayerMe, Error, TEXT("World is null!")); return nullptr; } @@ -75,10 +84,11 @@ UUserWidget* URpmCreatorWidget::CreateAssetPanel(const FString& Category) if (!AssetPanelWidget) { - UE_LOG(LogTemp, Error, TEXT("Failed to create widget from blueprint class!")); + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to create widget from blueprint class!")); return nullptr; } AssetPanelSwitcher->AddChild(AssetPanelWidget); + AssetPanelWidget->PaginationLimit = PaginationLimit; AssetPanelWidget->Rename(*Category); AssetPanelWidget->SetCategoryName(Category); AssetPanelWidget->ButtonSize = FVector2D(200, 200); diff --git a/Source/RpmNextGen/Private/Samples/RpmPaginatorWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmPaginatorWidget.cpp new file mode 100644 index 0000000..4ac4107 --- /dev/null +++ b/Source/RpmNextGen/Private/Samples/RpmPaginatorWidget.cpp @@ -0,0 +1,56 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "Samples/RpmPaginatorWidget.h" + +#include "Components/Button.h" +#include "Components/TextBlock.h" + +void URpmPaginatorWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + if (PreviousButton) + { + PreviousButton->OnClicked.AddDynamic(this, &URpmPaginatorWidget::OnPrevButtonClicked); + } + + if (NextButton) + { + NextButton->OnClicked.AddDynamic(this, &URpmPaginatorWidget::OnNextButtonClicked); + } + UpdateState(FPagination()); +} + +void URpmPaginatorWidget::UpdateState(const FPagination& Pagination) +{ + if (PageText) + { + PageText->SetText(GetPageCountText(Pagination)); + } + + if (PreviousButton) + { + PreviousButton->SetIsEnabled(Pagination.Page > 1); + } + + if (NextButton) + { + NextButton->SetIsEnabled(Pagination.Page < Pagination.TotalPages); + } +} + +FText URpmPaginatorWidget::GetPageCountText(const FPagination& Pagination) +{ + return FText::FromString(FString::Printf(TEXT("%d / %d"), Pagination.Page, Pagination.TotalPages)); +} + +void URpmPaginatorWidget::OnPrevButtonClicked() +{ + OnPreviousButtonEvent.Broadcast(); +} + +void URpmPaginatorWidget::OnNextButtonClicked() +{ + OnNextButtonEvent.Broadcast(); +} \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetApi.h b/Source/RpmNextGen/Public/Api/Assets/AssetApi.h index 674e7df..4935c24 100644 --- a/Source/RpmNextGen/Public/Api/Assets/AssetApi.h +++ b/Source/RpmNextGen/Public/Api/Assets/AssetApi.h @@ -1,7 +1,9 @@ -#pragma once +#pragma once +#include "Api/Common/ApiRequestStrategy.h" #include "Api/Common/WebApiWithAuth.h" #include "Models/AssetTypeListResponse.h" +struct FApiRequest; struct FAssetTypeListRequest; struct FAssetListRequest; struct FAssetListResponse; @@ -9,7 +11,7 @@ struct FAssetListResponse; DECLARE_DELEGATE_TwoParams(FOnListAssetsResponse, const FAssetListResponse&, bool); DECLARE_DELEGATE_TwoParams(FOnListAssetTypeResponse, const FAssetTypeListResponse&, bool); -class RPMNEXTGEN_API FAssetApi : public FWebApiWithAuth +class RPMNEXTGEN_API FAssetApi : public FWebApiWithAuth { public: static const FString BaseModelType; @@ -18,11 +20,21 @@ class RPMNEXTGEN_API FAssetApi : public FWebApiWithAuth FOnListAssetTypeResponse OnListAssetTypeResponse; FAssetApi(); + FAssetApi(EApiRequestStrategy InApiRequestStrategy); + + void Initialize(); void ListAssetsAsync(const FAssetListRequest& Request); void ListAssetTypesAsync(const FAssetTypeListRequest& Request); - +protected: + EApiRequestStrategy ApiRequestStrategy; + private: FString ApiBaseUrl; + bool bIsInitialized = false; + void HandleAssetResponse(TSharedPtr, FHttpResponsePtr Response, bool bWasSuccessful); - void HandleResponse(FString Response, bool bWasSuccessful); + void LoadAssetsFromCache(TMap QueryParams); + void LoadAssetTypesFromCache(); + + TArray ExtractQueryValues(const FString& QueryString, const FString& Key); }; diff --git a/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h b/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h index 9609248..a3301ce 100644 --- a/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h +++ b/Source/RpmNextGen/Public/Api/Assets/Models/Asset.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "CoreMinimal.h" #include "Asset.generated.h" @@ -28,6 +28,17 @@ struct RPMNEXTGEN_API FAsset UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "updatedAt")) FDateTime UpdatedAt; + + FAsset() + { + Id = ""; + Name = ""; + GlbUrl = ""; + IconUrl = ""; + Type = ""; + CreatedAt = FDateTime(); + UpdatedAt = FDateTime(); + } private: UPROPERTY(meta = (JsonIgnore)) diff --git a/Source/RpmNextGen/Public/Api/Assets/Models/AssetListRequest.h b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListRequest.h index 85d11bf..18427d5 100644 --- a/Source/RpmNextGen/Public/Api/Assets/Models/AssetListRequest.h +++ b/Source/RpmNextGen/Public/Api/Assets/Models/AssetListRequest.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "CoreMinimal.h" #include "Api/Common/Models/PaginationQueryParams.h" @@ -16,7 +16,7 @@ struct RPMNEXTGEN_API FAssetListQueryParams : public FPaginationQueryParams FString Type; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "excludeTypes")) - FString ExcludeTypes; + TArray ExcludeTypes; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "characterModelAssetId")) FString CharacterModelAssetId; @@ -30,53 +30,61 @@ struct RPMNEXTGEN_API FAssetListRequest 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; + TMap BuildQueryMap() const; }; -inline FString FAssetListRequest::BuildQueryString() const +inline TMap FAssetListRequest::BuildQueryMap() const { - if (Params.ApplicationId.IsEmpty() && Params.Type.IsEmpty() && Params.ExcludeTypes.IsEmpty()) return FString(); - FString QueryString = TEXT("?"); + TMap QueryMap; if (!Params.ApplicationId.IsEmpty()) { - QueryString += TEXT("applicationId=") + Params.ApplicationId + TEXT("&"); + QueryMap.Add(TEXT("applicationId"), Params.ApplicationId); } if (!Params.Type.IsEmpty()) { - auto CleanType = Params.Type.Replace(TEXT(" "), TEXT("%20")); - QueryString += TEXT("type=") + CleanType + TEXT("&"); + QueryMap.Add(TEXT("type"), Params.Type); } if (!Params.ExcludeTypes.IsEmpty()) { - QueryString += TEXT("excludeTypes=") + Params.ExcludeTypes + TEXT("&"); + if (Params.ExcludeTypes.Num() > 0) + { + FString ExcludeTypesString; + for (int32 i = 0; i < Params.ExcludeTypes.Num(); i++) + { + ExcludeTypesString += Params.ExcludeTypes[i]; + // Add '&excludeTypes=' only if it's not the last element + if (i < Params.ExcludeTypes.Num() - 1) + { + ExcludeTypesString += TEXT("&excludeTypes="); + } + } + QueryMap.Add(TEXT("excludeTypes"), ExcludeTypesString); + } } if (!Params.CharacterModelAssetId.IsEmpty()) { - QueryString += TEXT("characterModelAssetId=") + Params.CharacterModelAssetId + TEXT("&"); + QueryMap.Add(TEXT("characterModelAssetId"), Params.CharacterModelAssetId); } if( Params.Limit > 0 ) { - QueryString += TEXT("limit=") + FString::FromInt(Params.Limit) + TEXT("&"); + QueryMap.Add(TEXT("limit"), FString::FromInt(Params.Limit)); } if( Params.Page > 0 ) { - QueryString += TEXT("page=") + FString::FromInt(Params.Page) + TEXT("&"); + QueryMap.Add(TEXT("page"), FString::FromInt(Params.Page)); } if( !Params.Order.IsEmpty() ) { - QueryString += TEXT("order=") + Params.Order + TEXT("&"); + QueryMap.Add(TEXT("order"), Params.Order); } - QueryString.RemoveFromEnd(TEXT("&")); - return QueryString; + return QueryMap; } diff --git a/Source/RpmNextGen/Public/Api/Auth/ApiKeyAuthStrategy.h b/Source/RpmNextGen/Public/Api/Auth/ApiKeyAuthStrategy.h index 273693d..183414a 100644 --- a/Source/RpmNextGen/Public/Api/Auth/ApiKeyAuthStrategy.h +++ b/Source/RpmNextGen/Public/Api/Auth/ApiKeyAuthStrategy.h @@ -8,7 +8,7 @@ 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; + virtual void AddAuthToRequest(TSharedPtr ApiRequest) override; + virtual void TryRefresh(TSharedPtr ApiRequest) override; + virtual void OnRefreshTokenResponse(TSharedPtr ApiRequest, const FRefreshTokenResponse& Response, bool bWasSuccessful) override; }; diff --git a/Source/RpmNextGen/Public/Api/Auth/AuthApi.h b/Source/RpmNextGen/Public/Api/Auth/AuthApi.h index 228ec44..0399448 100644 --- a/Source/RpmNextGen/Public/Api/Auth/AuthApi.h +++ b/Source/RpmNextGen/Public/Api/Auth/AuthApi.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "CoreMinimal.h" #include "Api/Common/WebApi.h" @@ -6,7 +6,7 @@ struct FRefreshTokenResponse; struct FRefreshTokenRequest; -DECLARE_DELEGATE_TwoParams(FOnRefreshTokenResponse, const FRefreshTokenResponse&, bool); +DECLARE_DELEGATE_ThreeParams(FOnRefreshTokenResponse, TSharedPtr, const FRefreshTokenResponse&, bool); class RPMNEXTGEN_API FAuthApi : public FWebApi { @@ -16,7 +16,7 @@ class RPMNEXTGEN_API FAuthApi : public FWebApi FAuthApi(); void RefreshToken(const FRefreshTokenRequest& Request); - virtual void OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) override; + void OnProcessComplete(TSharedPtr ApiRequest, FHttpResponsePtr Response, bool bWasSuccessful); private: FString ApiUrl; diff --git a/Source/RpmNextGen/Public/Api/Auth/IAuthenticationStrategy.h b/Source/RpmNextGen/Public/Api/Auth/IAuthenticationStrategy.h index 651827b..dcd378f 100644 --- a/Source/RpmNextGen/Public/Api/Auth/IAuthenticationStrategy.h +++ b/Source/RpmNextGen/Public/Api/Auth/IAuthenticationStrategy.h @@ -1,11 +1,10 @@ #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); +DECLARE_DELEGATE_TwoParams(FOnAuthComplete, TSharedPtr, bool); +DECLARE_DELEGATE_ThreeParams(FOnTokenRefreshed, TSharedPtr, const FRefreshTokenResponseBody&, bool); class RPMNEXTGEN_API IAuthenticationStrategy { @@ -14,7 +13,7 @@ class RPMNEXTGEN_API IAuthenticationStrategy 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; + virtual void AddAuthToRequest(TSharedPtr ApiRequest) = 0; + virtual void TryRefresh(TSharedPtr ApiRequest) = 0; + virtual void OnRefreshTokenResponse(TSharedPtr ApiRequest, const FRefreshTokenResponse& Response, bool bWasSuccessful) = 0; }; \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h b/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h index cfca9a5..6caf48a 100644 --- a/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h +++ b/Source/RpmNextGen/Public/Api/Characters/CharacterApi.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "CoreMinimal.h" #include "JsonObjectConverter.h" @@ -16,10 +16,10 @@ DECLARE_DELEGATE_TwoParams(FOnCharacterCreateResponse, FCharacterCreateResponse, DECLARE_DELEGATE_TwoParams(FOnCharacterUpdatResponse, FCharacterUpdateResponse, bool); DECLARE_DELEGATE_TwoParams(FOnCharacterFindResponse, FCharacterFindByIdResponse, bool); -class RPMNEXTGEN_API FCharacterApi : public TSharedFromThis, public FWebApiWithAuth +class RPMNEXTGEN_API FCharacterApi : public FWebApiWithAuth { public: - FOnWebApiResponse OnApiResponse; + FOnRequestComplete OnApiResponse; FOnCharacterCreateResponse OnCharacterCreateResponse; FOnCharacterUpdatResponse OnCharacterUpdateResponse; FOnCharacterFindResponse OnCharacterFindResponse; @@ -33,13 +33,14 @@ class RPMNEXTGEN_API FCharacterApi : public TSharedFromThis FString ConvertToJsonString(const T& Data); - virtual void OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) override; + void HandleCharacterResponse(TSharedPtr ApiRequest, FHttpResponsePtr Response, bool bWasSuccessful); + void HandleCharacterCreateResponse(FHttpResponsePtr Response, bool bWasSuccessful); + void HandleUpdateResponse( FHttpResponsePtr Response, bool bWasSuccessful); + void HandleFindResponse(FHttpResponsePtr Response, bool bWasSuccessful); private: FString BaseUrl; TMap AssetByType = TMap(); diff --git a/Source/RpmNextGen/Public/Api/Characters/Models/CharacterCreateResponse.h b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterCreateResponse.h index 321ca68..dfa5f9f 100644 --- a/Source/RpmNextGen/Public/Api/Characters/Models/CharacterCreateResponse.h +++ b/Source/RpmNextGen/Public/Api/Characters/Models/CharacterCreateResponse.h @@ -12,4 +12,18 @@ struct RPMNEXTGEN_API FCharacterCreateResponse : public FApiResponse UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "data")) FRpmCharacter Data; + + FCharacterCreateResponse() + { + } + + FCharacterCreateResponse(FRpmCharacter Data) + { + this->Data = Data; + } + + bool IsValid() const + { + return !Data.Id.IsEmpty() && !Data.GlbUrl.IsEmpty(); + } }; diff --git a/Source/RpmNextGen/Public/Api/Characters/Models/RpmCharacter.h b/Source/RpmNextGen/Public/Api/Characters/Models/RpmCharacter.h index eec8bfd..8771bfc 100644 --- a/Source/RpmNextGen/Public/Api/Characters/Models/RpmCharacter.h +++ b/Source/RpmNextGen/Public/Api/Characters/Models/RpmCharacter.h @@ -7,7 +7,7 @@ USTRUCT(BlueprintType) struct RPMNEXTGEN_API FRpmCharacter { GENERATED_BODY() - + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "id")) FString Id; diff --git a/Source/RpmNextGen/Public/Api/Common/ApiRequestStrategy.h b/Source/RpmNextGen/Public/Api/Common/ApiRequestStrategy.h new file mode 100644 index 0000000..69a7460 --- /dev/null +++ b/Source/RpmNextGen/Public/Api/Common/ApiRequestStrategy.h @@ -0,0 +1,9 @@ +#pragma once + +UENUM(BlueprintType) +enum class EApiRequestStrategy : uint8 +{ + ApiOnly UMETA(DisplayName = "API only"), + FallbackToCache UMETA(DisplayName = "Fallback to Cache"), + CacheOnly UMETA(DisplayName = "Cache only") +}; \ No newline at end of file diff --git a/Source/RpmNextGen/Public/Api/Auth/ApiRequest.h b/Source/RpmNextGen/Public/Api/Common/Models/ApiRequest.h similarity index 92% rename from Source/RpmNextGen/Public/Api/Auth/ApiRequest.h rename to Source/RpmNextGen/Public/Api/Common/Models/ApiRequest.h index 7e8da68..9295742 100644 --- a/Source/RpmNextGen/Public/Api/Auth/ApiRequest.h +++ b/Source/RpmNextGen/Public/Api/Common/Models/ApiRequest.h @@ -26,6 +26,8 @@ struct RPMNEXTGEN_API FApiRequest UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me") TMap Headers; + TMap QueryParams; + FString Payload; FString GetVerb() const @@ -44,6 +46,11 @@ struct RPMNEXTGEN_API FApiRequest return TEXT("DELETE"); } } + + bool IsValid() const + { + return !Url.IsEmpty(); + } }; USTRUCT(BlueprintType) diff --git a/Source/RpmNextGen/Public/Api/Common/Models/PaginationQueryParams.h b/Source/RpmNextGen/Public/Api/Common/Models/PaginationQueryParams.h index 9b32785..1cf8c7b 100644 --- a/Source/RpmNextGen/Public/Api/Common/Models/PaginationQueryParams.h +++ b/Source/RpmNextGen/Public/Api/Common/Models/PaginationQueryParams.h @@ -9,7 +9,7 @@ struct RPMNEXTGEN_API FPaginationQueryParams GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "limit")) - int Limit = -1; + int Limit = 10; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me", meta = (JsonName = "page")) int Page = -1; diff --git a/Source/RpmNextGen/Public/Api/Common/WebApi.h b/Source/RpmNextGen/Public/Api/Common/WebApi.h index c84d7c0..152c622 100644 --- a/Source/RpmNextGen/Public/Api/Common/WebApi.h +++ b/Source/RpmNextGen/Public/Api/Common/WebApi.h @@ -1,36 +1,34 @@ -#pragma once +#pragma once #include "CoreMinimal.h" #include "JsonObjectConverter.h" -#include "Api/Auth/ApiRequest.h" #include "Interfaces/IHttpRequest.h" #include "Misc/ScopeExit.h" +#include "Models/ApiRequest.h" class FHttpModule; -DECLARE_DELEGATE_TwoParams(FOnWebApiResponse, FString, bool); + class RPMNEXTGEN_API FWebApi { public: - FOnWebApiResponse OnApiResponse; + DECLARE_DELEGATE_ThreeParams(FOnRequestComplete, TSharedPtr, FHttpResponsePtr, bool); + + FOnRequestComplete OnRequestComplete; FWebApi(); virtual ~FWebApi(); -protected: + void DispatchRaw(TSharedPtr ApiRequest); +protected: FHttpModule* Http; - void DispatchRaw( - const FApiRequest& Data - ); - FString BuildQueryString(const TMap& QueryParams); template FString ConvertToJsonString(const T& Data); - virtual void OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); - + virtual void OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, TSharedPtr ApiRequest); }; template diff --git a/Source/RpmNextGen/Public/Api/Common/WebApiWithAuth.h b/Source/RpmNextGen/Public/Api/Common/WebApiWithAuth.h index dd52dca..8add599 100644 --- a/Source/RpmNextGen/Public/Api/Common/WebApiWithAuth.h +++ b/Source/RpmNextGen/Public/Api/Common/WebApiWithAuth.h @@ -9,19 +9,18 @@ class RPMNEXTGEN_API FWebApiWithAuth : public FWebApi { public: FWebApiWithAuth(); - FWebApiWithAuth(IAuthenticationStrategy* InAuthenticationStrategy); + FWebApiWithAuth(const TSharedPtr& InAuthenticationStrategy); - void SetAuthenticationStrategy(IAuthenticationStrategy* InAuthenticationStrategy); + void SetAuthenticationStrategy(const TSharedPtr& InAuthenticationStrategy); - void OnAuthComplete(bool bWasSuccessful); - void OnAuthTokenRefreshed(const FRefreshTokenResponseBody& Response, bool bWasSuccessful); + void OnAuthComplete(TSharedPtr ApiRequest, bool bWasSuccessful); + void OnAuthTokenRefreshed(TSharedPtr ApiRequest, const FRefreshTokenResponseBody& Response, bool bWasSuccessful); - void DispatchRawWithAuth(FApiRequest& Data); + void DispatchRawWithAuth(TSharedPtr ApiRequest); + protected: - TSharedPtr ApiRequestData; - - virtual void OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) override; + virtual void OnProcessResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, TSharedPtr ApiRequest) override; private: - IAuthenticationStrategy* AuthenticationStrategy; + TSharedPtr AuthenticationStrategy; }; diff --git a/Source/RpmNextGen/Public/Api/Files/FileApi.h b/Source/RpmNextGen/Public/Api/Files/FileApi.h index 8df4b05..398f5d9 100644 --- a/Source/RpmNextGen/Public/Api/Files/FileApi.h +++ b/Source/RpmNextGen/Public/Api/Files/FileApi.h @@ -1,11 +1,11 @@ -#pragma once +#pragma once #include "CoreMinimal.h" #include "Interfaces/IHttpRequest.h" struct FAsset; -DECLARE_DELEGATE_TwoParams(FOnFileRequestComplete, TArray*, const FString&); -DECLARE_DELEGATE_TwoParams(FOnAssetFileRequestComplete, TArray*, const FAsset&); +DECLARE_DELEGATE_TwoParams(FOnFileRequestComplete, const TArray*, const FString&); +DECLARE_DELEGATE_TwoParams(FOnAssetFileRequestComplete, const TArray*, const FAsset&); class RPMNEXTGEN_API FFileApi : public TSharedFromThis { diff --git a/Source/RpmNextGen/Public/Api/Files/GlbLoader.h b/Source/RpmNextGen/Public/Api/Files/GlbLoader.h index 68e9a45..f850def 100644 --- a/Source/RpmNextGen/Public/Api/Files/GlbLoader.h +++ b/Source/RpmNextGen/Public/Api/Files/GlbLoader.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "CoreMinimal.h" #include "FileApi.h" @@ -31,5 +31,5 @@ class RPMNEXTGEN_API FGlbLoader : public FFileApi FString DownloadDirectory; UFUNCTION() - virtual void HandleFileRequestComplete(TArray* Data, const FString& String); + virtual void HandleFileRequestComplete(const TArray* Data, const FString& String); }; diff --git a/Source/RpmNextGen/Public/Cache/AssetCacheManager.h b/Source/RpmNextGen/Public/Cache/AssetCacheManager.h index 348e7fc..6f38ddc 100644 --- a/Source/RpmNextGen/Public/Cache/AssetCacheManager.h +++ b/Source/RpmNextGen/Public/Cache/AssetCacheManager.h @@ -69,6 +69,23 @@ class FAssetCacheManager return Assets; } + TArray GetAssetsExcludingTypes(const TArray& AssetExcludeTypes) const + { + TArray Assets; + for (const auto& Entry : StoredAssets) + { + const FCachedAssetData& CachedAsset = Entry.Value; + for (auto ExcludeTypes : AssetExcludeTypes) + { + if (CachedAsset.Type != ExcludeTypes) + { + Assets.Add(CachedAsset); + } + } + } + return Assets; + } + void StoreAndTrackIcon(const FAssetLoadingContext& Context, const bool bSaveManifest = true) { const FCachedAssetData& StoredAsset = FCachedAssetData(Context.Asset); diff --git a/Source/RpmNextGen/Public/Cache/CacheStrategy.h b/Source/RpmNextGen/Public/Cache/CacheStrategy.h deleted file mode 100644 index 7e9e688..0000000 --- a/Source/RpmNextGen/Public/Cache/CacheStrategy.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -UENUM(BlueprintType) -enum class ECacheStrategy : uint8 -{ - None UMETA(DisplayName = "No Caching"), - UseCache UMETA(DisplayName = "Cache on Disk"), - UseMemory UMETA(DisplayName = "Cache in Memory") -}; - -UENUM(BlueprintType) -enum class ELoadingStrategy : uint8 -{ - ApiOnly UMETA(DisplayName = "API only"), - CacheFirst UMETA(DisplayName = "Cache first"), - FallbackToCache UMETA(DisplayName = "Fallback to Cache") -}; \ No newline at end of file diff --git a/Source/RpmNextGen/Public/RpmFunctionLibrary.h b/Source/RpmNextGen/Public/RpmFunctionLibrary.h index ecda8ed..36508e3 100644 --- a/Source/RpmNextGen/Public/RpmFunctionLibrary.h +++ b/Source/RpmNextGen/Public/RpmFunctionLibrary.h @@ -4,11 +4,9 @@ #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" -#include "Utilities/ConnectionManager.h" #include "RpmFunctionLibrary.generated.h" DECLARE_DYNAMIC_DELEGATE_OneParam(FOnAssetIdFetched, FString, AssetId); -DECLARE_DYNAMIC_DELEGATE_OneParam(FOnConnectionStatusRefreshedDelegate, bool, bIsConnected); /** * @@ -21,13 +19,7 @@ class RPMNEXTGEN_API URpmFunctionLibrary : public UBlueprintFunctionLibrary public: UFUNCTION(BlueprintCallable, Category = "ReadyPlayerMe", meta = (WorldContext = "WorldContextObject")) static void FetchFirstAssetId(UObject* WorldContextObject, const FString& AssetType, FOnAssetIdFetched OnAssetIdFetched); - - UFUNCTION(BlueprintCallable, Category = "ReadyPlayerMe/Network") - static bool IsInternetConnected(); - - UFUNCTION(BlueprintCallable, Category = "ReadyPlayerMe/Network") - static void CheckInternetConnection(const FOnConnectionStatusRefreshedDelegate& OnConnectionStatusRefreshed); - + UFUNCTION(BlueprintCallable, Category = "ReadyPlayerMe/Cache") static void ExtractCachePakFile(); }; diff --git a/Source/RpmNextGen/Public/RpmLoaderComponent.h b/Source/RpmNextGen/Public/RpmLoaderComponent.h index 0080a96..7a3fac2 100644 --- a/Source/RpmNextGen/Public/RpmLoaderComponent.h +++ b/Source/RpmNextGen/Public/RpmLoaderComponent.h @@ -45,8 +45,8 @@ class RPMNEXTGEN_API URpmLoaderComponent : public UActorComponent void SetGltfConfig(const FglTFRuntimeConfig* Config); - void HandleAssetLoaded(TArray* Data, const FAsset& Asset); - void HandleCharacterAssetLoaded(TArray* Array, const FString& FileName); + void HandleAssetLoaded(const TArray* Data, const FAsset& Asset); + void HandleCharacterAssetLoaded(const TArray* Array, const FString& FileName); virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; @@ -59,7 +59,7 @@ class RPMNEXTGEN_API URpmLoaderComponent : public UActorComponent virtual void BeginPlay() override; UFUNCTION(BlueprintCallable, Category = "Ready Player Me") - virtual void CreateCharacter(const FString& BaseModelId, bool bUseCache); + virtual void CreateCharacter(const FString& BaseModelId); UFUNCTION(BlueprintCallable, Category = "Ready Player Me") virtual void LoadCharacterFromUrl(FString Url); @@ -68,8 +68,8 @@ class RPMNEXTGEN_API URpmLoaderComponent : public UActorComponent void LoadGltfRuntimeAssetFromCache(const FAsset& Asset); UFUNCTION(BlueprintCallable, Category = "Ready Player Me") - virtual void LoadCharacterFromAssetMapCache(TMap AssetMap); - + virtual void LoadCharacterAssetsFromCache(TMap AssetMap); + UFUNCTION(BlueprintCallable, Category = "Ready Player Me") virtual void LoadAssetPreview(FAsset AssetData, bool bUseCache); diff --git a/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h b/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h index f5e5f26..d5a45bb 100644 --- a/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h +++ b/Source/RpmNextGen/Public/Samples/RpmAssetPanelWidget.h @@ -12,6 +12,7 @@ struct FAsset; class URpmAssetButtonWidget; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAssetSelected, const FAsset&, AssetData); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FPaginationUpdated, const FPagination&, Pagination); /** * @@ -21,12 +22,11 @@ class RPMNEXTGEN_API URpmAssetPanelWidget : public UUserWidget { GENERATED_BODY() public: + UPROPERTY(meta = (BindWidget)) + UPanelWidget* ButtonContainer; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Asset Panel" ) TSubclassOf AssetButtonBlueprint; - - UPROPERTY(meta = (BindWidget)) - UPanelWidget* ButtonContainer; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Asset Panel") URpmAssetButtonWidget* SelectedAssetButton; @@ -37,9 +37,15 @@ class RPMNEXTGEN_API URpmAssetPanelWidget : public UUserWidget UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Asset Button" ) FVector2D ImageSize; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Asset Button" ) + int32 PaginationLimit = 50; + UPROPERTY(BlueprintAssignable, Category = "Events" ) FOnAssetSelected OnAssetSelected; + UPROPERTY(BlueprintAssignable, Category = "Events" ) + FPaginationUpdated OnPaginationUpdated; + UFUNCTION(BlueprintCallable, Category = "Asset Panel") void CreateButtonsFromAssets(TArray Assets); @@ -61,12 +67,19 @@ class RPMNEXTGEN_API URpmAssetPanelWidget : public UUserWidget UFUNCTION(BlueprintCallable, Category = "Asset Panel") void LoadAssetsOfType(const FString& AssetType); + UFUNCTION(BlueprintCallable, Category = "Asset Panel") + virtual void LoadNextPage(); + UFUNCTION(BlueprintCallable, Category = "Asset Panel") + virtual void LoadPreviousPage(); + void CreateButton(const FAsset& AssetData); virtual void SynchronizeProperties() override; virtual void NativeConstruct() override; private: - TArray> AssetButtons; + FPagination Pagination; + FString CurrentAssetType; + TMap> AssetButtonMap; TSharedPtr AssetApi; }; diff --git a/Source/RpmNextGen/Public/Samples/RpmCategoryPanelWidget.h b/Source/RpmNextGen/Public/Samples/RpmCategoryPanelWidget.h index ba005cf..4bbe563 100644 --- a/Source/RpmNextGen/Public/Samples/RpmCategoryPanelWidget.h +++ b/Source/RpmNextGen/Public/Samples/RpmCategoryPanelWidget.h @@ -4,9 +4,12 @@ #include "CoreMinimal.h" #include "Api/Assets/Models/AssetTypeListResponse.h" +#include "Api/Common/Models/ApiRequest.h" #include "Blueprint/UserWidget.h" #include "RpmCategoryPanelWidget.generated.h" +class IHttpRequest; +class IHttpResponse; class URpmAssetButtonWidget; class FAssetApi; class URpmCategoryButtonWidget; @@ -42,7 +45,7 @@ class RPMNEXTGEN_API URpmCategoryPanelWidget : public UUserWidget UFUNCTION(BlueprintCallable, Category = "Category Panel") virtual void UpdateSelectedButton(URpmCategoryButtonWidget* CategoryButton); - + UFUNCTION(BlueprintCallable, Category = "Category Panel") void LoadAndCreateButtons(); @@ -52,9 +55,10 @@ class RPMNEXTGEN_API URpmCategoryPanelWidget : public UUserWidget virtual void CreateButton(const FString& AssetType); virtual void SynchronizeProperties() override; virtual void NativeConstruct() override; + private: TArray> AssetButtons; TSharedPtr AssetApi; - + bool bIsInitialized = false; void AssetTypesLoaded(const FAssetTypeListResponse& AssetTypeListResponse, bool bWasSuccessful); }; diff --git a/Source/RpmNextGen/Public/Samples/RpmCreatorWidget.h b/Source/RpmNextGen/Public/Samples/RpmCreatorWidget.h index 178891b..4040a1c 100644 --- a/Source/RpmNextGen/Public/Samples/RpmCreatorWidget.h +++ b/Source/RpmNextGen/Public/Samples/RpmCreatorWidget.h @@ -23,7 +23,10 @@ class RPMNEXTGEN_API URpmCreatorWidget : public UUserWidget UFUNCTION(BlueprintCallable, Category = "Ready Player Me") void SwitchToPanel(const FString& Category); - + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ready Player Me" ) + int32 PaginationLimit = 50; + virtual void NativeConstruct() override; protected: diff --git a/Source/RpmNextGen/Public/Samples/RpmPaginatorWidget.h b/Source/RpmNextGen/Public/Samples/RpmPaginatorWidget.h new file mode 100644 index 0000000..55b2288 --- /dev/null +++ b/Source/RpmNextGen/Public/Samples/RpmPaginatorWidget.h @@ -0,0 +1,55 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Api/Common/Models/Pagination.h" +#include "Blueprint/UserWidget.h" +#include "RpmPaginatorWidget.generated.h" + +class UTextBlock; +class UButton; +struct FPagination; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnNextButtonClicked); +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPreviousButtonClicked); + +/** + * + */ +UCLASS() +class RPMNEXTGEN_API URpmPaginatorWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintAssignable, Category = "Events" ) + FOnNextButtonClicked OnNextButtonEvent; + + UPROPERTY( BlueprintAssignable, Category = "Events" ) + FOnPreviousButtonClicked OnPreviousButtonEvent; + + UFUNCTION(BlueprintCallable, Category = "Ready Player Me" ) + void UpdateState(const FPagination& Pagination); + + UFUNCTION() + void OnPrevButtonClicked(); + UFUNCTION() + void OnNextButtonClicked(); + +protected: + + virtual void NativeConstruct() override; + +private: + UPROPERTY(meta = (BindWidget)) + UButton* PreviousButton; + + UPROPERTY(meta = (BindWidget)) + UButton* NextButton; + + UPROPERTY(meta = (BindWidget)) + UTextBlock* PageText; + + FText GetPageCountText(const FPagination& Pagination); +}; diff --git a/Source/RpmNextGen/Public/Utilities/ConnectionManager.cpp b/Source/RpmNextGen/Public/Utilities/ConnectionManager.cpp deleted file mode 100644 index 3762b92..0000000 --- a/Source/RpmNextGen/Public/Utilities/ConnectionManager.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "ConnectionManager.h" -#include "HttpModule.h" -#include "RpmNextGen.h" -#include "Interfaces/IHttpResponse.h" -#include "Misc/ScopeLock.h" - -FConnectionManager& FConnectionManager::Get() -{ - // Singleton instance of the connection manager - static FConnectionManager Instance; - return Instance; -} - -FConnectionManager::FConnectionManager() -{ -} - -FConnectionManager::~FConnectionManager() -{ -} - -bool FConnectionManager::IsConnected() -{ - // Lock access to ensure thread safety - FScopeLock Lock(&ConnectionStatusCriticalSection); - return bIsConnected; -} - -void FConnectionManager::CheckInternetConnection() -{ - TSharedRef Request = FHttpModule::Get().CreateRequest(); - Request->SetURL(TEXT("http://www.google.com")); - Request->SetVerb(TEXT("GET")); - Request->OnProcessRequestComplete().BindRaw(this, &FConnectionManager::OnCheckResponse); - - Request->ProcessRequest(); -} - -void FConnectionManager::OnCheckResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) -{ - const bool bNewConnectionStatus = bWasSuccessful && Response.IsValid() && Response->GetResponseCode() == 200; - - UpdateConnectionStatus(bNewConnectionStatus); - - UE_LOG(LogReadyPlayerMe, Log, TEXT("Connection check result: %s"), bIsConnected ? TEXT("Connected") : TEXT("Disconnected")); -} - -void FConnectionManager::UpdateConnectionStatus(bool bNewStatus) -{ - FScopeLock Lock(&ConnectionStatusCriticalSection); - - if (bNewStatus != bIsConnected) - { - bIsConnected = bNewStatus; - OnConnectionStatusChanged.ExecuteIfBound(bIsConnected); - } - OnConnectionStatusRefreshed.ExecuteIfBound(bIsConnected); -} diff --git a/Source/RpmNextGen/Public/Utilities/ConnectionManager.h b/Source/RpmNextGen/Public/Utilities/ConnectionManager.h deleted file mode 100644 index d6c3b2e..0000000 --- a/Source/RpmNextGen/Public/Utilities/ConnectionManager.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "Delegates/Delegate.h" -#include "Interfaces/IHttpRequest.h" -#include "Misc/ScopeLock.h" - -DECLARE_DELEGATE_OneParam(FOnConnectionStatusChanged, bool); -DECLARE_DELEGATE_OneParam(FOnConnectionStatusRefreshed, bool); - -///

-/// A Singleton class that checks and tracks internet connection status. -/// -class RPMNEXTGEN_API FConnectionManager -{ -public: - static FConnectionManager& Get(); - - virtual ~FConnectionManager(); - - bool IsConnected(); - - void CheckInternetConnection(); - - FOnConnectionStatusChanged OnConnectionStatusChanged; - FOnConnectionStatusRefreshed OnConnectionStatusRefreshed; - -private: - FConnectionManager(); - - void OnCheckResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); - - void UpdateConnectionStatus(bool bNewStatus); - - bool bIsConnected = true; - - FCriticalSection ConnectionStatusCriticalSection; -}; diff --git a/Source/RpmNextGenEditor/Private/Auth/DeveloperAuthApi.cpp b/Source/RpmNextGenEditor/Private/Auth/DeveloperAuthApi.cpp index 37d4bc6..730255a 100644 --- a/Source/RpmNextGenEditor/Private/Auth/DeveloperAuthApi.cpp +++ b/Source/RpmNextGenEditor/Private/Auth/DeveloperAuthApi.cpp @@ -1,32 +1,39 @@ #include "Auth/DeveloperAuthApi.h" #include "Auth/Models/DeveloperLoginRequest.h" #include "Auth/Models/DeveloperLoginResponse.h" +#include "Interfaces/IHttpResponse.h" #include "Settings/RpmDeveloperSettings.h" FDeveloperAuthApi::FDeveloperAuthApi() { const URpmDeveloperSettings* RpmSettings = GetDefault(); ApiUrl = FString::Printf(TEXT("%s/login"), *RpmSettings->ApiBaseAuthUrl); - OnApiResponse.BindRaw(this, &FDeveloperAuthApi::HandleLoginResponse); + OnRequestComplete.BindRaw(this, &FDeveloperAuthApi::HandleLoginResponse); } -void FDeveloperAuthApi::HandleLoginResponse(FString JsonData, bool bIsSuccessful) const +void FDeveloperAuthApi::HandleLoginResponse(TSharedPtr ApiRequest, FHttpResponsePtr Response, bool bWasSuccessful) const { - FDeveloperLoginResponse Response; - if (bIsSuccessful && !JsonData.IsEmpty() && FJsonObjectConverter::JsonObjectStringToUStruct(JsonData, &Response, 0, 0)) + FDeveloperLoginResponse DevLoginResponse; + if(bWasSuccessful && Response.IsValid()) { - OnLoginResponse.ExecuteIfBound(Response, true); - return; + const FString Data = Response->GetContentAsString(); + if (!Data.IsEmpty() && FJsonObjectConverter::JsonObjectStringToUStruct(Data, &DevLoginResponse, 0, 0)) + { + OnLoginResponse.ExecuteIfBound(DevLoginResponse, true); + return; + } + UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to parse login response: %s"), *Data ); } - OnLoginResponse.ExecuteIfBound(Response, bIsSuccessful); + + OnLoginResponse.ExecuteIfBound(DevLoginResponse, bWasSuccessful); } 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(); + const TSharedPtr ApiRequest = MakeShared(); + 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 index abcbabc..7020d4f 100644 --- a/Source/RpmNextGenEditor/Private/Auth/DeveloperTokenAuthStrategy.cpp +++ b/Source/RpmNextGenEditor/Private/Auth/DeveloperTokenAuthStrategy.cpp @@ -2,46 +2,44 @@ #include "RpmNextGen.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); + AuthApi = MakeShared(); + AuthApi->OnRefreshTokenResponse.BindRaw(this, &DeveloperTokenAuthStrategy::OnRefreshTokenResponse); } -void DeveloperTokenAuthStrategy::AddAuthToRequest(TSharedPtr Request) +void DeveloperTokenAuthStrategy::AddAuthToRequest(TSharedPtr ApiRequest) { const FString Key = TEXT("Authorization"); const FString Token = FDevAuthTokenCache::GetAuthData().Token; if(Token.IsEmpty()) { UE_LOG(LogReadyPlayerMe, Error, TEXT("Token is empty")); - OnAuthComplete.ExecuteIfBound(false); + OnAuthComplete.ExecuteIfBound(ApiRequest, false); return; } - if (Request->Headers.Contains(Key)) + if (ApiRequest->Headers.Contains(Key)) { - Request->Headers.Remove(Key); + ApiRequest->Headers.Remove(Key); } - Request->Headers.Add(Key, FString::Printf(TEXT("Bearer %s"), *Token)); + ApiRequest->Headers.Add(Key, FString::Printf(TEXT("Bearer %s"), *Token)); - OnAuthComplete.ExecuteIfBound(true); + OnAuthComplete.ExecuteIfBound(ApiRequest, true); } -void DeveloperTokenAuthStrategy::TryRefresh(TSharedPtr Request) +void DeveloperTokenAuthStrategy::TryRefresh(TSharedPtr ApiRequest) { FRefreshTokenRequest RefreshRequest; RefreshRequest.Data.Token = FDevAuthTokenCache::GetAuthData().Token; RefreshRequest.Data.RefreshToken = FDevAuthTokenCache::GetAuthData().RefreshToken; - RefreshTokenAsync(RefreshRequest); } -void DeveloperTokenAuthStrategy::OnRefreshTokenResponse(const FRefreshTokenResponse& Response, bool bWasSuccessful) +void DeveloperTokenAuthStrategy::OnRefreshTokenResponse(TSharedPtr Request, const FRefreshTokenResponse& Response, bool bWasSuccessful) { if (bWasSuccessful && !Response.Data.Token.IsEmpty()) { @@ -49,15 +47,15 @@ void DeveloperTokenAuthStrategy::OnRefreshTokenResponse(const FRefreshTokenRespo DeveloperAuth.Token = Response.Data.Token; DeveloperAuth.RefreshToken = Response.Data.RefreshToken; FDevAuthTokenCache::SetAuthData(DeveloperAuth); - OnTokenRefreshed.ExecuteIfBound(Response.Data, true); + OnTokenRefreshed.ExecuteIfBound(Request, Response.Data, true); return; } UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to refresh token")); - OnTokenRefreshed.ExecuteIfBound(Response.Data, false); + OnTokenRefreshed.ExecuteIfBound(Request, Response.Data, false); } void DeveloperTokenAuthStrategy::RefreshTokenAsync(const FRefreshTokenRequest& Request) { - AuthApi.RefreshToken(Request); + AuthApi->RefreshToken(Request); } diff --git a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGenEditor/Private/Cache/CacheGenerator.cpp similarity index 96% rename from Source/RpmNextGen/Private/Cache/CacheGenerator.cpp rename to Source/RpmNextGenEditor/Private/Cache/CacheGenerator.cpp index a5f7fa3..abbe772 100644 --- a/Source/RpmNextGen/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGenEditor/Private/Cache/CacheGenerator.cpp @@ -6,7 +6,6 @@ #include "Api/Assets/AssetIconLoader.h" #include "Api/Assets/Models/AssetListRequest.h" #include "Api/Assets/Models/AssetTypeListRequest.h" -#include "Api/Files/PakFileUtility.h" #include "Cache/AssetCacheManager.h" #include "Interfaces/IHttpRequest.h" #include "Interfaces/IHttpResponse.h" @@ -21,7 +20,7 @@ const FString FCacheGenerator::ZipFileName = TEXT("CacheAssets.pak"); FCacheGenerator::FCacheGenerator() : CurrentBaseModelIndex(0), MaxItemsPerCategory(10) { Http = &FHttpModule::Get(); - AssetApi = MakeUnique(); + AssetApi = MakeUnique(EApiRequestStrategy::ApiOnly); AssetApi->OnListAssetsResponse.BindRaw(this, &FCacheGenerator::OnListAssetsResponse); AssetApi->OnListAssetTypeResponse.BindRaw(this, &FCacheGenerator::OnListAssetTypesResponse); } @@ -197,7 +196,7 @@ void FCacheGenerator::AddFolderToNonAssetDirectory() const if (CurrentValue.Contains(FolderToAdd)) { // Folder already exists, no need to add it - UE_LOG(LogTemp, Log, TEXT("Folder already added to Additional Non-Asset Directories: %s"), *FolderToAdd); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Folder already added to Additional Non-Asset Directories: %s"), *FolderToAdd); return; } } @@ -208,7 +207,7 @@ void FCacheGenerator::AddFolderToNonAssetDirectory() const // Force update the config file GConfig->Flush(false, ConfigFilePath); - UE_LOG(LogTemp, Log, TEXT("Added folder to Additional Non-Asset Directories: %s"), *FolderToAdd); + UE_LOG(LogReadyPlayerMe, Log, TEXT("Added folder to Additional Non-Asset Directories: %s"), *FolderToAdd); } void FCacheGenerator::OnListAssetsResponse(const FAssetListResponse& AssetListResponse, bool bWasSuccessful) @@ -341,7 +340,7 @@ void FCacheGenerator::ExtractCache() void FCacheGenerator::FetchBaseModels() const { - URpmDeveloperSettings* Settings = GetMutableDefault(); + const URpmDeveloperSettings* Settings = GetDefault(); FAssetListRequest AssetListRequest = FAssetListRequest(); FAssetListQueryParams QueryParams = FAssetListQueryParams(); QueryParams.ApplicationId = Settings->ApplicationId; @@ -353,7 +352,7 @@ void FCacheGenerator::FetchBaseModels() const void FCacheGenerator::FetchAssetTypes() const { - URpmDeveloperSettings* Settings = GetMutableDefault(); + const URpmDeveloperSettings* Settings = GetDefault(); FAssetTypeListRequest AssetListRequest; FAssetTypeListQueryParams QueryParams = FAssetTypeListQueryParams(); QueryParams.ApplicationId = Settings->ApplicationId; diff --git a/Source/RpmNextGenEditor/Private/DeveloperAccounts/DeveloperAccountApi.cpp b/Source/RpmNextGenEditor/Private/DeveloperAccounts/DeveloperAccountApi.cpp index 570dbf3..966a1da 100644 --- a/Source/RpmNextGenEditor/Private/DeveloperAccounts/DeveloperAccountApi.cpp +++ b/Source/RpmNextGenEditor/Private/DeveloperAccounts/DeveloperAccountApi.cpp @@ -4,9 +4,10 @@ #include "DeveloperAccounts/Models/ApplicationListResponse.h" #include "DeveloperAccounts/Models/OrganizationListRequest.h" #include "DeveloperAccounts/Models/OrganizationListResponse.h" +#include "Interfaces/IHttpResponse.h" #include "Settings/RpmDeveloperSettings.h" -FDeveloperAccountApi::FDeveloperAccountApi(IAuthenticationStrategy* InAuthenticationStrategy) : FWebApiWithAuth(InAuthenticationStrategy) +FDeveloperAccountApi::FDeveloperAccountApi(const TSharedPtr& InAuthenticationStrategy) : FWebApiWithAuth(InAuthenticationStrategy) { if (URpmDeveloperSettings* Settings = GetMutableDefault()) { @@ -21,9 +22,9 @@ void FDeveloperAccountApi::ListApplicationsAsync(const FApplicationListRequest& 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); + TSharedPtr ApiRequest = MakeShared(); + ApiRequest->Url = Url; + OnRequestComplete.BindRaw(this, &FDeveloperAccountApi::HandleAppListResponse); DispatchRawWithAuth(ApiRequest); } @@ -34,16 +35,17 @@ void FDeveloperAccountApi::ListOrganizationsAsync(const FOrganizationListRequest 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); + TSharedPtr ApiRequest = MakeShared(); + ApiRequest->Url = Url; + OnRequestComplete.BindRaw(this, &FDeveloperAccountApi::HandleOrgListResponse); DispatchRawWithAuth(ApiRequest); } -void FDeveloperAccountApi::HandleAppListResponse(FString Data, bool bWasSuccessful) +void FDeveloperAccountApi::HandleAppListResponse(TSharedPtr ApiRequest, FHttpResponsePtr Response, bool bWasSuccessful) { FApplicationListResponse ApplicationListResponse; + FString Data = Response->GetContentAsString(); if (bWasSuccessful && !Data.IsEmpty() && FJsonObjectConverter::JsonObjectStringToUStruct(Data, &ApplicationListResponse, 0, 0)) { OnApplicationListResponse.ExecuteIfBound(ApplicationListResponse, true); @@ -52,9 +54,10 @@ void FDeveloperAccountApi::HandleAppListResponse(FString Data, bool bWasSuccessf OnApplicationListResponse.ExecuteIfBound(ApplicationListResponse, false); } -void FDeveloperAccountApi::HandleOrgListResponse(FString Data, bool bWasSuccessful) +void FDeveloperAccountApi::HandleOrgListResponse(TSharedPtr ApiRequest, FHttpResponsePtr Response, bool bWasSuccessful) { FOrganizationListResponse OrganizationListResponse; + FString Data = Response->GetContentAsString(); if (bWasSuccessful && !Data.IsEmpty() && FJsonObjectConverter::JsonObjectStringToUStruct(Data, &OrganizationListResponse, 0, 0)) { OnOrganizationResponse.ExecuteIfBound(OrganizationListResponse, true); diff --git a/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp index 5b3fc9c..4df4186 100644 --- a/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp @@ -193,28 +193,27 @@ void SRpmDeveloperLoginWidget::Initialize() const FDeveloperAuth DevAuthData = FDevAuthTokenCache::GetAuthData(); if (!DeveloperAuthApi.IsValid()) { - DeveloperAuthApi = MakeUnique(); + DeveloperAuthApi = MakeShared(); DeveloperAuthApi->OnLoginResponse.BindRaw(this, &SRpmDeveloperLoginWidget::HandleLoginResponse); } if (!AssetApi.IsValid()) { - AssetApi = MakeUnique(); + AssetApi = MakeShared(); if (!DevAuthData.IsDemo) { - AssetApi->SetAuthenticationStrategy(new DeveloperTokenAuthStrategy()); + AssetApi->SetAuthenticationStrategy(MakeShared()); } AssetApi->OnListAssetsResponse.BindRaw(this, &SRpmDeveloperLoginWidget::HandleBaseModelListResponse); } if (!DeveloperAccountApi.IsValid()) { - DeveloperAccountApi = MakeUnique(nullptr); + DeveloperAccountApi = MakeShared(nullptr); if (!DevAuthData.IsDemo) { - DeveloperAccountApi->SetAuthenticationStrategy(new DeveloperTokenAuthStrategy()); + DeveloperAccountApi->SetAuthenticationStrategy(MakeShared()); } - DeveloperAccountApi->OnOrganizationResponse.BindRaw( this, &SRpmDeveloperLoginWidget::HandleOrganizationListResponse); DeveloperAccountApi->OnApplicationListResponse.BindRaw( @@ -346,8 +345,8 @@ FReply SRpmDeveloperLoginWidget::OnLoginClicked() FEditorCache::SetString(CacheKeyEmail, Email); Email = Email.TrimStartAndEnd(); Password = Password.TrimStartAndEnd(); - DeveloperAccountApi->SetAuthenticationStrategy(new DeveloperTokenAuthStrategy()); - AssetApi->SetAuthenticationStrategy(new DeveloperTokenAuthStrategy()); + DeveloperAccountApi->SetAuthenticationStrategy(MakeShared()); + AssetApi->SetAuthenticationStrategy(MakeShared()); FDeveloperLoginRequest LoginRequest = FDeveloperLoginRequest(Email, Password); DeveloperAuthApi->LoginWithEmail(LoginRequest); return FReply::Handled(); @@ -364,7 +363,7 @@ void SRpmDeveloperLoginWidget::HandleLoginResponse(const FDeveloperLoginResponse if (bWasSuccessful) { UserName = Response.Data.Name; - FDeveloperAuth AuthData = FDeveloperAuth(Response.Data, false); + const FDeveloperAuth AuthData = FDeveloperAuth(Response.Data, false); FDevAuthTokenCache::SetAuthData(AuthData); SetLoggedInState(true); GetOrgList(); @@ -374,8 +373,7 @@ void SRpmDeveloperLoginWidget::HandleLoginResponse(const FDeveloperLoginResponse FDevAuthTokenCache::ClearAuthData(); } -void SRpmDeveloperLoginWidget::HandleOrganizationListResponse(const FOrganizationListResponse& Response, - bool bWasSuccessful) +void SRpmDeveloperLoginWidget::HandleOrganizationListResponse(const FOrganizationListResponse& Response, bool bWasSuccessful) { if (bWasSuccessful) { diff --git a/Source/RpmNextGenEditor/Public/Auth/DeveloperAuthApi.h b/Source/RpmNextGenEditor/Public/Auth/DeveloperAuthApi.h index 588493a..05c3cfc 100644 --- a/Source/RpmNextGenEditor/Public/Auth/DeveloperAuthApi.h +++ b/Source/RpmNextGenEditor/Public/Auth/DeveloperAuthApi.h @@ -15,7 +15,7 @@ class RPMNEXTGENEDITOR_API FDeveloperAuthApi : public FWebApi FDeveloperAuthApi(); - void HandleLoginResponse(FString JsonData, bool bIsSuccessful) const; + void HandleLoginResponse(TSharedPtr ApiRequest, FHttpResponsePtr Response, bool bWasSuccessful) const; void LoginWithEmail(FDeveloperLoginRequest Request); private: diff --git a/Source/RpmNextGenEditor/Public/Auth/DeveloperTokenAuthStrategy.h b/Source/RpmNextGenEditor/Public/Auth/DeveloperTokenAuthStrategy.h index 7b0cbd1..0fe693d 100644 --- a/Source/RpmNextGenEditor/Public/Auth/DeveloperTokenAuthStrategy.h +++ b/Source/RpmNextGenEditor/Public/Auth/DeveloperTokenAuthStrategy.h @@ -1,24 +1,22 @@ -#pragma once +#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; + + virtual void AddAuthToRequest(TSharedPtr ApiRequest) override; + virtual void OnRefreshTokenResponse(TSharedPtr, const FRefreshTokenResponse& Response, bool bWasSuccessful) override; + virtual void TryRefresh(TSharedPtr ApiRequest) override; private: - FOnWebApiResponse OnWebApiResponse; - FAuthApi AuthApi; + TSharedPtr AuthApi; void RefreshTokenAsync(const FRefreshTokenRequest& Request); }; diff --git a/Source/RpmNextGenEditor/Public/Auth/Models/DeveloperLoginResponse.h b/Source/RpmNextGenEditor/Public/Auth/Models/DeveloperLoginResponse.h index 097e834..f6f4c1b 100644 --- a/Source/RpmNextGenEditor/Public/Auth/Models/DeveloperLoginResponse.h +++ b/Source/RpmNextGenEditor/Public/Auth/Models/DeveloperLoginResponse.h @@ -41,9 +41,9 @@ struct RPMNEXTGENEDITOR_API FDeveloperLoginResponse : public FApiResponse { if (JsonObject.IsValid()) { - return FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), StaticStruct(), &OutObject, 0, 0); + return FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), StaticStruct(), &OutObject, 0, 0); } - UE_LOG(LogReadyPlayerMe, Warning, TEXT("JsonObject Invalid")); + UE_LOG(LogReadyPlayerMe, Error, TEXT("JsonObject Invalid")); return false; } }; diff --git a/Source/RpmNextGen/Public/Cache/CacheGenerator.h b/Source/RpmNextGenEditor/Public/Cache/CacheGenerator.h similarity index 96% rename from Source/RpmNextGen/Public/Cache/CacheGenerator.h rename to Source/RpmNextGenEditor/Public/Cache/CacheGenerator.h index b2aef82..79dceab 100644 --- a/Source/RpmNextGen/Public/Cache/CacheGenerator.h +++ b/Source/RpmNextGenEditor/Public/Cache/CacheGenerator.h @@ -16,7 +16,7 @@ DECLARE_DELEGATE_OneParam(FOnCacheDataLoaded, bool); DECLARE_DELEGATE_OneParam(FOnLocalCacheGenerated, bool); DECLARE_DELEGATE_OneParam(FOnDownloadRemoteCache, bool); -class RPMNEXTGEN_API FCacheGenerator : public TSharedFromThis +class RPMNEXTGENEDITOR_API FCacheGenerator : public TSharedFromThis { public: FOnDownloadRemoteCache OnDownloadRemoteCacheDelegate; diff --git a/Source/RpmNextGenEditor/Public/DeveloperAccounts/DeveloperAccountApi.h b/Source/RpmNextGenEditor/Public/DeveloperAccounts/DeveloperAccountApi.h index a28c880..e55a9e7 100644 --- a/Source/RpmNextGenEditor/Public/DeveloperAccounts/DeveloperAccountApi.h +++ b/Source/RpmNextGenEditor/Public/DeveloperAccounts/DeveloperAccountApi.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "CoreMinimal.h" #include "Api/Common/WebApiWithAuth.h" @@ -17,15 +17,15 @@ class RPMNEXTGENEDITOR_API FDeveloperAccountApi : public FWebApiWithAuth FOnApplicationListResponse OnApplicationListResponse; FOnOrganizationListResponse OnOrganizationResponse; - FDeveloperAccountApi(IAuthenticationStrategy* InAuthenticationStrategy); + FDeveloperAccountApi(const TSharedPtr& InAuthenticationStrategy); void ListApplicationsAsync(const FApplicationListRequest& Request); void ListOrganizationsAsync(const FOrganizationListRequest& Request); private: FString ApiBaseUrl; - void HandleOrgListResponse(FString Data, bool bWasSuccessful); - void HandleAppListResponse(FString Data, bool bWasSuccessful); + void HandleOrgListResponse(TSharedPtr ApiRequest, FHttpResponsePtr Response, bool bWasSuccessful); + void HandleAppListResponse(TSharedPtrApiRequest, FHttpResponsePtr Response, bool bWasSuccessful); static FString BuildQueryString(const TMap& Params); }; diff --git a/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/ApplicationListRequest.h b/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/ApplicationListRequest.h index 12b49a8..4aec9d3 100644 --- a/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/ApplicationListRequest.h +++ b/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/ApplicationListRequest.h @@ -1,7 +1,7 @@ #pragma once #include "CoreMinimal.h" -#include "Api/Auth/ApiRequest.h" +#include "Api/Common/Models/ApiRequest.h" #include "ApplicationListRequest.generated.h" USTRUCT(BlueprintType) diff --git a/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/OrganizationListRequest.h b/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/OrganizationListRequest.h index 0a83cd0..3fed75e 100644 --- a/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/OrganizationListRequest.h +++ b/Source/RpmNextGenEditor/Public/DeveloperAccounts/Models/OrganizationListRequest.h @@ -1,7 +1,6 @@ #pragma once #include "CoreMinimal.h" -#include "Api/Auth/ApiRequest.h" #include "OrganizationListRequest.generated.h" USTRUCT(BlueprintType) diff --git a/Source/RpmNextGenEditor/Public/EditorAssetLoader.h b/Source/RpmNextGenEditor/Public/EditorAssetLoader.h index a650d47..188cd1d 100644 --- a/Source/RpmNextGenEditor/Public/EditorAssetLoader.h +++ b/Source/RpmNextGenEditor/Public/EditorAssetLoader.h @@ -7,7 +7,7 @@ struct FglTFRuntimeConfig; class UglTFRuntimeAsset; -class FEditorAssetLoader : public FAssetGlbLoader +class RPMNEXTGENEDITOR_API FEditorAssetLoader : public FAssetGlbLoader { public: USkeleton* SkeletonToCopy; diff --git a/Source/RpmNextGenEditor/Public/UI/SRpmDeveloperLoginWidget.h b/Source/RpmNextGenEditor/Public/UI/SRpmDeveloperLoginWidget.h index 33be258..6cf8dc9 100644 --- a/Source/RpmNextGenEditor/Public/UI/SRpmDeveloperLoginWidget.h +++ b/Source/RpmNextGenEditor/Public/UI/SRpmDeveloperLoginWidget.h @@ -47,9 +47,9 @@ class RPMNEXTGENEDITOR_API SRpmDeveloperLoginWidget : public SCompoundWidget EVisibility GetLoggedInViewVisibility() const; TArray> ActiveLoaders; TSharedPtr AssetLoader; - TUniquePtr AssetApi; - TUniquePtr DeveloperAccountApi; - TUniquePtr DeveloperAuthApi; + TSharedPtr AssetApi; + TSharedPtr DeveloperAccountApi; + TSharedPtr DeveloperAuthApi; static constexpr const TCHAR* CacheKeyEmail = TEXT("Email"); bool bIsLoggedIn = false; bool bIsInitialized = false; From 2998bb22d2de0155a5aaee47caee44779a139104 Mon Sep 17 00:00:00 2001 From: Harrison Hough Date: Tue, 8 Oct 2024 09:45:54 +0300 Subject: [PATCH 49/54] chore: update version --- CHANGELOG.md | 12 ++++++++++++ RpmNextGen.uplugin | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 601eccb..27edb30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.2.0] 2024-10-05 + +### Added + +- Pagination support added to UI samples +- WebApis updates to fallback to cache if available + +### Fixed + +- WebApi's refactored to fix a number of bugs + + ## [0.1.0] 2024-09-05 ### Added diff --git a/RpmNextGen.uplugin b/RpmNextGen.uplugin index 2091eb5..8f3560c 100644 --- a/RpmNextGen.uplugin +++ b/RpmNextGen.uplugin @@ -1,7 +1,7 @@ { "FileVersion": 3, "Version": 1, - "VersionName": "0.1", + "VersionName": "0.2", "FriendlyName": "RpmNextGen", "Description": "Ready Player Me Next Gen SDK", "Category": "Other", From 6edfb6ec99c24819055783302f644c8cbabb7cb4 Mon Sep 17 00:00:00 2001 From: Harrison Hough Date: Tue, 8 Oct 2024 10:01:09 +0300 Subject: [PATCH 50/54] chore: fix UI layout and padding --- .../Blueprints/WBP_RpmLoaderUI.uasset | Bin 54555 -> 56818 bytes .../Samples/BasicLoader/RpmBasicLoader.umap | Bin 138177 -> 138177 bytes .../Blueprints/WBP_RpmAssetPanel.uasset | Bin 55939 -> 55956 bytes .../BasicUI/Blueprints/WBP_RpmBasicUI.uasset | Bin 70421 -> 71804 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Content/Samples/BasicLoader/Blueprints/WBP_RpmLoaderUI.uasset b/Content/Samples/BasicLoader/Blueprints/WBP_RpmLoaderUI.uasset index 7752366e1332342e714cf12b182a9eadd3de2a71..b5a4e30d184c6790cffb87f2a576fc7912cd6e95 100644 GIT binary patch delta 2575 zcmcgteM}Q)7{7Z94m%u3iaRT%2OMkZ+R_5in+^&ZW}@iO=r+fUOB6CboSVWNPI53e zzz+m`#Id3CBccOmf_A$2RV8j#W|=UCd?+&uF1io40Z9jnbnji;qxi=t|Ljek%k%rV z=Y5~&lJ{!rB?A{E6&F!{gVw7lHyhE-u>MXH_jdW8(mg)wMnh2U~Re=vqqsM|>ugtI8BQcw%R zX_y*8kw!SYO9d67{&;pW$c&H$N%7wNVqK$sp-L>*9>Jh@_X$wPuqfdZSIWR8Muytl zkOD;{jcTezwJK7Ex}2~ll0{2D6O^7v8Zw`wki`khR5Y|>sIL)J9wwTsv( zAcPOd4{0rbQoI!Vg_q)^)KI1lM1^B;7{ghITq=vcIx3`mvzMY6q^t9QGKkj%q9Z2- zs!yYaIt;^4yC92_&$^W$3Eq?6KhKxnogmOTu};_X4BBiFVX~G%GaBJ10UL;(|0(=_q7u6(Omo(A*ycxJ1vQq&8PdLzg}f3iN&`?njYIj;4^R z-3>_%ysdv0$!`h7<7gI5>u{wsY&TG7wiEJ$_|?FmHDhq4kwHP7!ij5*iD-W}6oqP_ z)))f17zVv|M<~O;r;G82+87F*a#E{fX_y{MA!(1`SRKovtvt4z*{j@C4)X`qz(-d=UMpcUXR5#hI6f>}&q94#bg= zEK4Ol!nY=1=y7@f=u2l$%(yygT06p3o~#GsJjI?D{bA`m6-Q=>^tTNTqP){VDDP!w z6-w1dyo=g;&U)HGSWJ~C&MaPmE2KM!tz1On%(!LAgv=vWd6YkuW9qe=tiy7y`w-fp z_ActO4)*_X`LFVOS$)gfO}$;*UFG$bDNAI_5`8^lhP!n4muc?azZf?;&k=K;fX|OD zmZltD_eSBSTL+VgS04F(Ju>JZtYykK&pL?DxuO0OajCu=8q&C%rjhqs?N4tQzK>g$ zn{jq-2dBsfTPQK2|AyMfN2RCQ_kaG z`m06e{De94;hP&N_`=)`2AdpT+z@S>g`u2>khM>0ZYWfe@YwcW9%v}+gySDB^u~5= zn>RM|FCS%iwQEGlgypW6&V<0dK2TB;HPHciYg38N8(ToBSGJX@+E zJldG&OYxtH|F;J_zZb#(!g4p}rNc(+60!rXSVJTa7lc56^-LJDwxB!nq0#ytBsP^l zA^LqMim5lQkofYq9m6Lnme5K3w+(?zTNt$1@&zRyt~T@Sj8`l6`k1GE|IAbW0?wbd A9{>OV delta 1431 zcmcJNYe-Z<6vy|TwbZiOWX0Rsx>MfOZF5a`L$8@yminOwK`HdGz#dQ}L}k&3XceuE zveiyPVMc|PR>sP;7ilFF*~>?1R3P*@C^W<#U^REEca3#d zjMeQB@YdYhVxF93Fr<$(h@`N3>6v0r?atLRM=@2{Y1vPW=mEnY<0l;fuhK4r$4s&b z+zNQH^S&EPtd2+njHWQNq<5Cpi(x@&8A^3?z!$Kch_JYVz&Qq(6;M!Tdyo*aE zRx{q2CBf9AT30GYV9IO}inw%ewhY&TDZ%hK5!#OF#oOb|kw)6SI%MtE+N*SXAWnjj z6AWFF44r;0m9RrHLt(XsB3kVPVrz7_)^3Ik90%<(^qpkRpq=3Mco7yO!>R;=0~A}w zadCnOUxFyZx>H)ABNAwX>icb0JEh}1w=(BAu*zWqV=b#x@f*c+r!|xzt)8XoMRm-P zAaLO~G-IaI1olfz`sj3EGsVU$8p_ahJ?OAYQqM1)z>YUmw(9%5oh!ljn>ywuN^s^5 zwqA7N18W%8C(6+9n7Q?d61rSwXz9|BV6BTj#}gfeBoR8EGF*`)!^3A?ugGV!?=YK{HNI`!$Uc^jNp&-D`EeJUv#a>* zKEY?Ze~+pw8>< uJvGLmoFISr!+Y4Ld&=&YF#>w~7huc4A`I>4r+@E1plKzni}$Z1Lw^DM79U{% diff --git a/Content/Samples/BasicLoader/RpmBasicLoader.umap b/Content/Samples/BasicLoader/RpmBasicLoader.umap index b859cd10b20b450c26756e7f56455fadacd5be4e..31f765f6e28a285b48d0ef5a5eca8df4ccfcaf85 100644 GIT binary patch delta 130 zcmV-|0Db?#w+O+v2(X?35L?STQ&U!$M3qe&X2Pm7L4&RVx2^#JmM#i7Eig7NGcztR zGPh1J0jju{!RP@qm*VOH-Iu%<0tmPK=mD<*m%r-)!w`=!r*$$j{ZE?wl>fsc=EIkf ka{)M$pi~mK-0T5T7L%Y<7?+9$0)CfpT>%QW&I1DT7vZ}%Hvj+t delta 184 zcmV;p07w79w+O+v2(X?35U!t_H~^_NOt~m%(K>z7sDrKnx2^#JmM#i3EigDPG&wFY zFt<)H0jju{FX;iXx4r5C0s)siRRIW>#pnSqmn-Q3-Iu@X0mBf<+M2lL0X0FH_@WZ7 z0M1*Nk#hk!lb}=*x7_RjQWhByPlXJV4O+nV$g6rHvUWh2ALwt%npQw`mvIFG7nTJA m8JD0i0R#+R2>!qS|Ns9WJ5a!v%LW2Hm(F7W3b)P!0`nKdM@>io diff --git a/Content/Samples/BasicUI/Blueprints/WBP_RpmAssetPanel.uasset b/Content/Samples/BasicUI/Blueprints/WBP_RpmAssetPanel.uasset index 974abbcf352b7676ca5167bc379798f4a5cb9567..438ffd11638de79e2f78848886245cf5057790d6 100644 GIT binary patch delta 9213 zcmcgy2~<=^7X2US1PEq6vHh}%$kKM>LdzI6ZZV6Bn#DvXV-_PAi48_jBV@8-#))B( z^$BihP{xQFKy6W@I01t(CW@dEM8Pr3i6`T660=Os#50qrSHJ4>Km8j!Idf9yG*$K9 ztyk~8diCm89ah$Puc`Acs=s4Vwjh`UL3k)VEtyn4LThpJm}#TJ1z`l~7RZ8-87~MT zY0GyCLOA(-J5CT*m0SCSO}sLrcxgt>g!{zWlFQRip3Jmu2| z)qEiN+2umqs_d)IiMMU=it;6-nMn&4q@*nrMy6&hy^Z!4rlch;Pf1S`>Id8!==8q5 zuUCK%Va!fnBK$OOZJkq=7T%IoK`2;uDwS}FD72Tg9jwNxg0;1{{fu@q+yVEvEZxKh z-2-*FcRj=%x+yM8pcWs#DNqo)h-dbBh^zMycZAGOOK&dhBopuQ5VzX{+(ja9^y85fXhOGEgk^R6I`rH;H3~w$)xIUb z(q05|+*N{$<~X1W0EiRO;E|f|a%3ob5+q4wA_-}IxtFhv; z8yF3p^$u$r`RXX+VWxP|102fB5CkRodJ}ko0P73{ax-{DV06?N;J5>L$X+J6k5t^l ztVODxz`qmVDHZ67pBN6#?V9>q~xof<@Wzh{X;shg%HUhd5h?|Ab32 zrYIjK;W-pVieC#D^fNJ6`w%}0mfOr&I;QH@_ITiF3Beu%#YL` zF?Anm2QeZR6qr@!8P|{G$!8fnsDtWq+X#!wMpZfcHHD43+1WZtPLJF6w-LenzqB?buKwcn&Kuu7aw2+h zKx)>WYZ=W~x;9-Z&#bDd@XEFR#=orJZo=RF+`(3mRv1l^09VFY#Ym9GPmsPOx8px% zD-dMDrGB#W`SDj~h^wHWxxbhS&u7Q>aVP-#m(p)f2gc3?F~=Ib zi1%21FRLGi#_%BV6^LjKfsCAifm{Mh`a&)Q=2?TE=rLMi{k3FtI4sHY59Z@|{Q^Uz zg4AJ-kKsL5vok{wA_;*HLEdkw^e8?vuT`acs)@^@+5nAb%%TI1FS_aT(>C3bs5I9nOnJwUF3GI4jRDa z@)|)a7%~+C^Uc9L9r48)qjD$a&lhuHOa2(*hJeBtF}b85Ok7w}5ae~k3*rhx;n9LI zen||T4N&lVzxk|(!>sUQfkcM-7TTbxF4%OL@Q8>Y(+bj{Vh$=|wiIf`d{#K5hnR^) zk{YmNC@l^WlKmmG$U-JiT7ht^s2}NFESf-RePL+vaDUbATv$+SBaDLLQRMHj;y7_V z{7`(SmFBp2q#;ifdUft~d6F9db5UsXs>fslI^g8@R{1)<`gYVtt-p_9X*9q$+kM4EFjvHzsH>?ZEhLrGl`SGm+`Y1ao5m2)(>Kl4tsQD2Zca2< z)l-}2C>D6WnnQ!9GVOTmx@a4c$Pj91&S8WwxLy-%WJZLy){No`G~=TRS2LP+n4xsb z0|ur%xb>Htl6BKm7|RrQ*@+l=H_fy|+2}l0C$L(nL;*S~r>TpsO96MgO#!%P+=e{A zr8pq2%0_~QN@k2>Hp+5cxL+oWB*t@^AF5ijeAXT200}4&amr(4xQ0ih0eHEW$cW3JKQcJmWM}9@?DlBIeS*NuH<|sjA0Zt8TS??rS)) zcPup2`6$|U)g|@n?92Pn+1K|Q>T5!0D-U7l(hm=~bN1?kR&gFl7iKtdFdYs8s>%XqYs77c-on9$6@gPs8q` zHnGTg>6j#%h;G8u(9kfNkY_fICSM5l#!zQvV}Oc-3r9OCc_4u(K=Ri)rD?GWA)>zV zR^|TtM5&kfJQSWBs@w%b_<==VVZz9SZ!j;X!nKpbb&AqUqJF>t-GIkW4cA>5)!Pr0 zp9*vEnbeQeI5L>;Bb-!)V}}$K7)mtWi1nx4ncjKYz;s&=rQMp24{>fy$J0}5`YK3l=^xJhOs|m9Y<#z| zr7e164F*}rpuyzVJ;>BLJVrayp9vRBu2RvbGi?5aCf1d(wAHHKqg2~qXKR?uwNn>2 zgbZ^)XX{8m9dN7*ko-G<&e~t6L*tn+SJ4T-Zmb?w+-)#dx~it zaSwg#ApIalcfjRVyT%&BDRE)7Q{h^{~~%+^`(hXQ=#^3n0hYgA5VTd>kos@ zjhv^IuC62-uX;a%5Ih26xNCt0u)44Dc?KN&?|Dy%V|&)aeQqT|VX zO6LD_@4-1;EgXwb)n=H|J|&J@PID|SA#ic5R+l4y5FBv0eTe!LPy%1K50X{cZRGUf z;?{%Zd?Gnm-aMZ`-WFQU4_4pIP^^{wUd09}_+~bgJo`=Ph}I?OZhj`^SN7nmVFR5V kVPZBsd?7ksRfWh^`E(XdQpR%u(>oSAOwtZ(Tp7Fhf01=BEdT%j delta 9369 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 LTDHbzaU1>%)Rq=d diff --git a/Content/Samples/BasicUI/Blueprints/WBP_RpmBasicUI.uasset b/Content/Samples/BasicUI/Blueprints/WBP_RpmBasicUI.uasset index f962c0d50ab364ae299a12fde6417b3373549755..4cfdaac2992329aa9173035f35c1e5067a7ba688 100644 GIT binary patch delta 2793 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 bF Date: Wed, 9 Oct 2024 10:35:35 +0300 Subject: [PATCH 51/54] chore: remove set category --- Source/RpmNextGen/Private/Samples/RpmCreatorWidget.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/RpmNextGen/Private/Samples/RpmCreatorWidget.cpp b/Source/RpmNextGen/Private/Samples/RpmCreatorWidget.cpp index 83bb6ac..3b17b5c 100644 --- a/Source/RpmNextGen/Private/Samples/RpmCreatorWidget.cpp +++ b/Source/RpmNextGen/Private/Samples/RpmCreatorWidget.cpp @@ -90,7 +90,6 @@ UUserWidget* URpmCreatorWidget::CreateAssetPanel(const FString& Category) AssetPanelSwitcher->AddChild(AssetPanelWidget); AssetPanelWidget->PaginationLimit = PaginationLimit; AssetPanelWidget->Rename(*Category); - AssetPanelWidget->SetCategoryName(Category); AssetPanelWidget->ButtonSize = FVector2D(200, 200); AssetPanelWidget->ImageSize = FVector2D(200, 200); AssetPanelWidget->OnAssetSelected.AddDynamic(this, &URpmCreatorWidget::HandleAssetSelectedFromPanel); From 579aac1c1d693a0567a56852ec7858c8d5049ce5 Mon Sep 17 00:00:00 2001 From: Harrison Date: Wed, 9 Oct 2024 11:24:46 +0300 Subject: [PATCH 52/54] chore: fix 5.4 compile errors --- Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp | 4 ++++ Source/RpmNextGen/Public/Api/Assets/AssetApi.h | 1 + Source/RpmNextGen/Public/Api/Auth/IAuthenticationStrategy.h | 2 ++ Source/RpmNextGenEditor/Private/Cache/CacheGenerator.cpp | 4 ++++ Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp | 2 +- Source/RpmNextGenEditor/Public/Cache/CacheGenerator.h | 4 ++-- .../RpmNextGenEditor/Public/UI/Commands/CacheWindowCommands.h | 1 + Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h | 2 +- 8 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp index f7623f0..973ab13 100644 --- a/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp +++ b/Source/RpmNextGen/Private/Api/Assets/AssetApi.cpp @@ -23,6 +23,10 @@ FAssetApi::FAssetApi(EApiRequestStrategy InApiRequestStrategy) : ApiRequestStrat Initialize(); } +FAssetApi::~FAssetApi() +{ +} + void FAssetApi::Initialize() { if (bIsInitialized) return; diff --git a/Source/RpmNextGen/Public/Api/Assets/AssetApi.h b/Source/RpmNextGen/Public/Api/Assets/AssetApi.h index 4935c24..b89a713 100644 --- a/Source/RpmNextGen/Public/Api/Assets/AssetApi.h +++ b/Source/RpmNextGen/Public/Api/Assets/AssetApi.h @@ -21,6 +21,7 @@ class RPMNEXTGEN_API FAssetApi : public FWebApiWithAuth FAssetApi(); FAssetApi(EApiRequestStrategy InApiRequestStrategy); + virtual ~FAssetApi() override; void Initialize(); void ListAssetsAsync(const FAssetListRequest& Request); diff --git a/Source/RpmNextGen/Public/Api/Auth/IAuthenticationStrategy.h b/Source/RpmNextGen/Public/Api/Auth/IAuthenticationStrategy.h index dcd378f..b31505d 100644 --- a/Source/RpmNextGen/Public/Api/Auth/IAuthenticationStrategy.h +++ b/Source/RpmNextGen/Public/Api/Auth/IAuthenticationStrategy.h @@ -3,6 +3,8 @@ #include "CoreMinimal.h" #include "Models/RefreshTokenResponse.h" +struct FApiRequest; + DECLARE_DELEGATE_TwoParams(FOnAuthComplete, TSharedPtr, bool); DECLARE_DELEGATE_ThreeParams(FOnTokenRefreshed, TSharedPtr, const FRefreshTokenResponseBody&, bool); diff --git a/Source/RpmNextGenEditor/Private/Cache/CacheGenerator.cpp b/Source/RpmNextGenEditor/Private/Cache/CacheGenerator.cpp index abbe772..b682277 100644 --- a/Source/RpmNextGenEditor/Private/Cache/CacheGenerator.cpp +++ b/Source/RpmNextGenEditor/Private/Cache/CacheGenerator.cpp @@ -25,6 +25,10 @@ FCacheGenerator::FCacheGenerator() : CurrentBaseModelIndex(0), MaxItemsPerCatego AssetApi->OnListAssetTypeResponse.BindRaw(this, &FCacheGenerator::OnListAssetTypesResponse); } +FCacheGenerator::~FCacheGenerator() +{ +} + void FCacheGenerator::DownloadRemoteCacheFromUrl(const FString& Url) { TSharedRef Request = Http->CreateRequest(); diff --git a/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp index a24b04c..5158ae9 100644 --- a/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SCacheGeneratorWidget.cpp @@ -15,7 +15,7 @@ void SCacheGeneratorWidget::Construct(const FArguments& InArgs) { if(!CacheGenerator) { - CacheGenerator = MakeUnique(); + CacheGenerator = MakeShared(); CacheGenerator->OnCacheDataLoaded.BindRaw(this, &SCacheGeneratorWidget::OnFetchCacheDataComplete); CacheGenerator->OnDownloadRemoteCacheDelegate.BindRaw(this, &SCacheGeneratorWidget::OnDownloadRemoteCacheComplete); CacheGenerator->OnLocalCacheGenerated.BindRaw(this, &SCacheGeneratorWidget::OnGenerateLocalCacheCompleted); diff --git a/Source/RpmNextGenEditor/Public/Cache/CacheGenerator.h b/Source/RpmNextGenEditor/Public/Cache/CacheGenerator.h index 79dceab..b507d56 100644 --- a/Source/RpmNextGenEditor/Public/Cache/CacheGenerator.h +++ b/Source/RpmNextGenEditor/Public/Cache/CacheGenerator.h @@ -1,7 +1,7 @@ #pragma once #include "Api/Assets/Models/AssetListResponse.h" +#include "Api/Assets/Models/AssetListRequest.h" -struct FAssetListRequest; class FTaskManager; struct FCachedAssetData; class FAssetSaver; @@ -24,7 +24,7 @@ class RPMNEXTGENEDITOR_API FCacheGenerator : public TSharedFromThis diff --git a/Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h b/Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h index 79769ac..0d2663d 100644 --- a/Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h +++ b/Source/RpmNextGenEditor/Public/UI/SCacheGeneratorWidget.h @@ -16,7 +16,7 @@ class SCacheGeneratorWidget : public SCompoundWidget private: float ItemsPerCategory = 10.0f; FString CacheUrl; - TUniquePtr CacheGenerator; + TSharedPtr CacheGenerator; // Callback functions for your buttons FReply OnGenerateOfflineCacheClicked(); From cb1924c12faaad97528d4980b08bdf650fc152dc Mon Sep 17 00:00:00 2001 From: Harrison Hough Date: Thu, 10 Oct 2024 09:22:11 +0300 Subject: [PATCH 53/54] chore: small cleanup --- .../Private/EditorAssetLoader.cpp | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp b/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp index bc5bef1..941c2e8 100644 --- a/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp +++ b/Source/RpmNextGenEditor/Private/EditorAssetLoader.cpp @@ -39,24 +39,39 @@ USkeletalMesh* FEditorAssetLoader::SaveAsUAsset(UglTFRuntimeAsset* GltfAsset, co { const FglTFRuntimeSkeletonConfig SkeletonConfig = FglTFRuntimeSkeletonConfig(); USkeleton* Skeleton = GltfAsset->LoadSkeleton(0, SkeletonConfig); + if (!IsValid(Skeleton)) + { + UE_LOG(LogTemp, Error, TEXT("Failed to load skeleton for %s"), *LoadedAssetId); + return nullptr; + } + FglTFRuntimeSkeletalMeshConfig MeshConfig = FglTFRuntimeSkeletalMeshConfig(); + MeshConfig.Skeleton = Skeleton; - FglTFRuntimeSkeletalMeshConfig meshConfig = FglTFRuntimeSkeletalMeshConfig(); - meshConfig.Skeleton = Skeleton; + USkeletalMesh* SkeletalMesh = GltfAsset->LoadSkeletalMeshRecursive(TEXT(""), {}, MeshConfig); + + if (!IsValid(SkeletalMesh)) + { + UE_LOG(LogTemp, Error, TEXT("Failed to load skeletal mesh for %s. 1"), *LoadedAssetId); + return nullptr; + } + + // Ensure proper UObject flags to avoid garbage collection + SkeletalMesh->SetFlags(RF_Public | RF_Standalone); + SkeletalMesh->SetSkeleton(Skeleton); + Skeleton->SetPreviewMesh(SkeletalMesh); - USkeletalMesh* skeletalMesh = GltfAsset->LoadSkeletalMeshRecursive(TEXT(""), {}, meshConfig); - skeletalMesh->SetSkeleton(Skeleton); - Skeleton->SetPreviewMesh(skeletalMesh); const FString CoreAssetPath = FString::Printf(TEXT("/Game/ReadyPlayerMe/CharacterModels/%s/"), *LoadedAssetId); const FString SkeletonAssetPath = FString::Printf(TEXT("%s%s_Skeleton"), *CoreAssetPath, *LoadedAssetId); const FString SkeletalMeshAssetPath = FString::Printf(TEXT("%s%s_SkeletalMesh"), *CoreAssetPath, *LoadedAssetId); - + UE_LOG(LogTemp, Log, TEXT("Saving SkeletalMesh to path: %s"), *SkeletalMeshAssetPath); + UE_LOG(LogTemp, Log, TEXT("Saving Skeleton to path: %s"), *SkeletonAssetPath); const auto NameGenerator = NewObject(); NameGenerator->SetPath(CoreAssetPath); - UTransientObjectSaverLibrary::SaveTransientSkeletalMesh(skeletalMesh, SkeletalMeshAssetPath, SkeletonAssetPath, TEXT(""), NameGenerator->MaterialNameGeneratorDelegate, NameGenerator->TextureNameGeneratorDelegate); + UTransientObjectSaverLibrary::SaveTransientSkeletalMesh(SkeletalMesh, SkeletalMeshAssetPath, SkeletonAssetPath, TEXT(""), NameGenerator->MaterialNameGeneratorDelegate, NameGenerator->TextureNameGeneratorDelegate); UE_LOG(LogReadyPlayerMe, Log, TEXT("Character model saved: %s"), *LoadedAssetId); - return skeletalMesh; + return SkeletalMesh; } void FEditorAssetLoader::LoadBaseModelAsset(const FAsset& Asset) @@ -87,7 +102,7 @@ void FEditorAssetLoader::LoadAssetToWorld(const FString& AssetId, UglTFRuntimeAs if (GltfAsset) { - FTransform Transform = FTransform::Identity; + const FTransform Transform = FTransform::Identity; ARpmActor* NewActor = EditorWorld->SpawnActorDeferred(ARpmActor::StaticClass(), Transform); From be1dea73cc3f1fa23fdeed8e2b41833faaa4e145 Mon Sep 17 00:00:00 2001 From: Harrison Hough Date: Thu, 10 Oct 2024 14:40:39 +0300 Subject: [PATCH 54/54] chore: fix failed dev re-auth --- .../Private/Auth/DeveloperTokenAuthStrategy.cpp | 9 +++------ .../DeveloperAccounts/DeveloperAccountApi.cpp | 14 +++++++++----- .../Private/UI/SRpmDeveloperLoginWidget.cpp | 16 +++++++++++++--- .../Public/Auth/DeveloperTokenAuthStrategy.h | 4 ++-- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/Source/RpmNextGenEditor/Private/Auth/DeveloperTokenAuthStrategy.cpp b/Source/RpmNextGenEditor/Private/Auth/DeveloperTokenAuthStrategy.cpp index 7020d4f..7e00f8c 100644 --- a/Source/RpmNextGenEditor/Private/Auth/DeveloperTokenAuthStrategy.cpp +++ b/Source/RpmNextGenEditor/Private/Auth/DeveloperTokenAuthStrategy.cpp @@ -1,5 +1,4 @@ #include "Auth/DeveloperTokenAuthStrategy.h" - #include "RpmNextGen.h" #include "Auth/DevAuthTokenCache.h" #include "Api/Auth/Models/RefreshTokenRequest.h" @@ -27,12 +26,12 @@ void DeveloperTokenAuthStrategy::AddAuthToRequest(TSharedPtr ApiReq ApiRequest->Headers.Remove(Key); } ApiRequest->Headers.Add(Key, FString::Printf(TEXT("Bearer %s"), *Token)); - OnAuthComplete.ExecuteIfBound(ApiRequest, true); } void DeveloperTokenAuthStrategy::TryRefresh(TSharedPtr ApiRequest) { + ApiRequestToRetry = ApiRequest; FRefreshTokenRequest RefreshRequest; RefreshRequest.Data.Token = FDevAuthTokenCache::GetAuthData().Token; RefreshRequest.Data.RefreshToken = FDevAuthTokenCache::GetAuthData().RefreshToken; @@ -47,14 +46,12 @@ void DeveloperTokenAuthStrategy::OnRefreshTokenResponse(TSharedPtr DeveloperAuth.Token = Response.Data.Token; DeveloperAuth.RefreshToken = Response.Data.RefreshToken; FDevAuthTokenCache::SetAuthData(DeveloperAuth); - OnTokenRefreshed.ExecuteIfBound(Request, Response.Data, true); + OnTokenRefreshed.ExecuteIfBound(ApiRequestToRetry, Response.Data, true); return; } - UE_LOG(LogReadyPlayerMe, Error, TEXT("Failed to refresh token")); - OnTokenRefreshed.ExecuteIfBound(Request, Response.Data, false); + OnTokenRefreshed.ExecuteIfBound(ApiRequestToRetry, 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 index 966a1da..a77703d 100644 --- a/Source/RpmNextGenEditor/Private/DeveloperAccounts/DeveloperAccountApi.cpp +++ b/Source/RpmNextGenEditor/Private/DeveloperAccounts/DeveloperAccountApi.cpp @@ -1,4 +1,4 @@ -#include "DeveloperAccounts/DeveloperAccountApi.h" +#include "DeveloperAccounts/DeveloperAccountApi.h" #include "JsonObjectConverter.h" #include "DeveloperAccounts/Models/ApplicationListRequest.h" #include "DeveloperAccounts/Models/ApplicationListResponse.h" @@ -57,12 +57,16 @@ void FDeveloperAccountApi::HandleAppListResponse(TSharedPtr ApiRequ void FDeveloperAccountApi::HandleOrgListResponse(TSharedPtr ApiRequest, FHttpResponsePtr Response, bool bWasSuccessful) { FOrganizationListResponse OrganizationListResponse; - FString Data = Response->GetContentAsString(); - if (bWasSuccessful && !Data.IsEmpty() && FJsonObjectConverter::JsonObjectStringToUStruct(Data, &OrganizationListResponse, 0, 0)) + if(Response.IsValid()) { - OnOrganizationResponse.ExecuteIfBound(OrganizationListResponse, true); - return; + FString Data = Response->GetContentAsString(); + if (bWasSuccessful && !Data.IsEmpty() && FJsonObjectConverter::JsonObjectStringToUStruct(Data, &OrganizationListResponse, 0, 0)) + { + OnOrganizationResponse.ExecuteIfBound(OrganizationListResponse, true); + return; + } } + OnOrganizationResponse.ExecuteIfBound(OrganizationListResponse, false); } diff --git a/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp b/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp index 4df4186..1cf83b3 100644 --- a/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp +++ b/Source/RpmNextGenEditor/Private/UI/SRpmDeveloperLoginWidget.cpp @@ -27,9 +27,19 @@ BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SRpmDeveloperLoginWidget::Construct(const FArguments& InArgs) { FDeveloperAuth AuthData = FDevAuthTokenCache::GetAuthData(); - FDevAuthTokenCache::SetAuthData(AuthData); - bIsLoggedIn = AuthData.IsValid(); - UserName = AuthData.Name; + + bIsLoggedIn = false; + if(AuthData.IsValid()) + { + FDevAuthTokenCache::SetAuthData(AuthData); + bIsLoggedIn = AuthData.IsValid(); + UserName = AuthData.Name; + } + else + { + UserName = "User"; + FDevAuthTokenCache::ClearAuthData(); + } ChildSlot [ diff --git a/Source/RpmNextGenEditor/Public/Auth/DeveloperTokenAuthStrategy.h b/Source/RpmNextGenEditor/Public/Auth/DeveloperTokenAuthStrategy.h index 0fe693d..ee8d857 100644 --- a/Source/RpmNextGenEditor/Public/Auth/DeveloperTokenAuthStrategy.h +++ b/Source/RpmNextGenEditor/Public/Auth/DeveloperTokenAuthStrategy.h @@ -17,6 +17,6 @@ class RPMNEXTGENEDITOR_API DeveloperTokenAuthStrategy : public IAuthenticationSt private: TSharedPtr AuthApi; - - void RefreshTokenAsync(const FRefreshTokenRequest& Request); + TSharedPtr ApiRequestToRetry; + void RefreshTokenAsync(const FRefreshTokenRequest& RefreshRequest); };