From e751315be12861e5bd5235fb8ec19f03cb547acd Mon Sep 17 00:00:00 2001 From: Oleg Petruny Date: Tue, 19 Nov 2024 22:35:28 +0100 Subject: [PATCH] Runtime content downloading --- .../BP_Test_RuntimeLoad.uasset | 4 +- ...kchunk100-Windows.pak => CustomBarrel.pak} | 0 .../Source/Lost_Edge/Lost_Edge.Build.cs | 2 +- .../Lost_Edge/Private/CommonFunctions.h | 41 ++++++ .../Lost_Edge/Private/ContentLoader.cpp | 137 +++++++++++++++++- .../Source/Lost_Edge/Private/ContentLoader.h | 25 +++- 6 files changed, 200 insertions(+), 9 deletions(-) rename UnrealProject/Lost_Edge/Content/Misc/ImportTest/Build/{pakchunk100-Windows.pak => CustomBarrel.pak} (100%) diff --git a/UnrealProject/Lost_Edge/Content/Levels/Test/Actors/RuntimeLoadTest/BP_Test_RuntimeLoad.uasset b/UnrealProject/Lost_Edge/Content/Levels/Test/Actors/RuntimeLoadTest/BP_Test_RuntimeLoad.uasset index b1a0cfc..19b84b6 100644 --- a/UnrealProject/Lost_Edge/Content/Levels/Test/Actors/RuntimeLoadTest/BP_Test_RuntimeLoad.uasset +++ b/UnrealProject/Lost_Edge/Content/Levels/Test/Actors/RuntimeLoadTest/BP_Test_RuntimeLoad.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23adf40e0f008b1a773ef6507f145a26888cfb6ddde6cdf6b4e8ed373718d709 -size 60103 +oid sha256:2c1d5e18df10568f346e078aee33a2bef68d4a7f8426a11598f17032076a54ce +size 68506 diff --git a/UnrealProject/Lost_Edge/Content/Misc/ImportTest/Build/pakchunk100-Windows.pak b/UnrealProject/Lost_Edge/Content/Misc/ImportTest/Build/CustomBarrel.pak similarity index 100% rename from UnrealProject/Lost_Edge/Content/Misc/ImportTest/Build/pakchunk100-Windows.pak rename to UnrealProject/Lost_Edge/Content/Misc/ImportTest/Build/CustomBarrel.pak diff --git a/UnrealProject/Lost_Edge/Source/Lost_Edge/Lost_Edge.Build.cs b/UnrealProject/Lost_Edge/Source/Lost_Edge/Lost_Edge.Build.cs index f0e199e..a64361a 100644 --- a/UnrealProject/Lost_Edge/Source/Lost_Edge/Lost_Edge.Build.cs +++ b/UnrealProject/Lost_Edge/Source/Lost_Edge/Lost_Edge.Build.cs @@ -9,7 +9,7 @@ public class Lost_Edge : ModuleRules { PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "OpenCV" }); PrivateDependencyModuleNames.AddRange(new string[] { "EnhancedInput", "UMG", "RHI", "RenderCore", "Lost_EdgeShaders", "PakFile", //"TextureCompressor", - "LevelSequence", "MovieScene" }); // "Slate", "SlateCore" + "LevelSequence", "MovieScene", "HTTP", "Json" }); // "Slate", "SlateCore" // UE_LOG(LogTemp, Log, TEXT("capture: %s"), (capture ? TEXT("true") : TEXT("false"))); // GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, TEXT("1")); diff --git a/UnrealProject/Lost_Edge/Source/Lost_Edge/Private/CommonFunctions.h b/UnrealProject/Lost_Edge/Source/Lost_Edge/Private/CommonFunctions.h index 40c5160..903cead 100644 --- a/UnrealProject/Lost_Edge/Source/Lost_Edge/Private/CommonFunctions.h +++ b/UnrealProject/Lost_Edge/Source/Lost_Edge/Private/CommonFunctions.h @@ -2,6 +2,7 @@ #pragma once +#include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "CommonFunctions.generated.h" @@ -37,6 +38,12 @@ public: static FWorldDilationChangedDelegate& GetWorldDilationChangedDelegate(); + + template + static TArray ArrayDiff(const TArray& a, const TArray& b); + template + static void ArrayRemoveFirstFromEnd(TArray& array, T element); + private: static void SlowMotionTick(); @@ -48,3 +55,37 @@ namespace SlowMotion constexpr float slowDilation = 0.2f; constexpr float interpolationSpeed = 0.1f; } + +template +inline TArray UCommonFunctions::ArrayDiff(const TArray& a, const TArray& b) +{ + TSet result; + + if(a.Num() == 0) + return b; + else if(b.Num() == 0) + return a; + + for(auto& i : a) + if(!b.Contains(i)) + result.Add(i); + + for(auto& i : b) + if(!a.Contains(i)) + result.Add(i); + + return result.Array(); +} + +template +inline void UCommonFunctions::ArrayRemoveFirstFromEnd(TArray& array, T element) +{ + for(int32 i = array.Num() - 1; i > -1; --i) + { + if(array[i] == element) + { + array.RemoveAt(i); + return; + } + } +} diff --git a/UnrealProject/Lost_Edge/Source/Lost_Edge/Private/ContentLoader.cpp b/UnrealProject/Lost_Edge/Source/Lost_Edge/Private/ContentLoader.cpp index 0a39ebc..c03765f 100644 --- a/UnrealProject/Lost_Edge/Source/Lost_Edge/Private/ContentLoader.cpp +++ b/UnrealProject/Lost_Edge/Source/Lost_Edge/Private/ContentLoader.cpp @@ -3,7 +3,26 @@ #include "ContentLoader.h" +#include "CommonFunctions.h" #include "IPlatformFilePak.h" +#include "Misc/FileHelper.h" + +#include +#include +#include + +namespace +{ + constexpr auto httpGet = TEXT("GET"); + + constexpr int32 httpResponseOK = 200; + + constexpr auto httpContentTypeHtml = TEXT("text/html"); + constexpr auto httpContentTypeJson = TEXT("application/json"); + + constexpr auto pakIndexUrl = TEXT("https://pixelyfier.com/lost_edge/content/"); + const auto pakFileSavePath = FPaths::ProjectDir(); +} void UContentLoader::BeginDestroy() { @@ -18,9 +37,121 @@ void UContentLoader::BeginDestroy() UObject::BeginDestroy(); } +void UContentLoader::HttpGet(const FString& url, FHttpRequestCompleteDelegate requestCompleteCallback) +{ + TSharedRef request = FHttpModule::Get().CreateRequest(); + request->SetURL(url); + request->SetVerb(httpGet); + request->OnProcessRequestComplete() = std::move(requestCompleteCallback); + request->ProcessRequest(); +} + +TArray UContentLoader::ParseDirectoryIndex(const TArray& content, const FString& contentType) +{ + TArray result; + + if(contentType == httpContentTypeHtml) + { + std::string contentHtml{ reinterpret_cast(content.GetData()), static_cast(content.Num()) }; + std::regex hrefRegex(R"( 1) + { + result.Add(UTF8_TO_TCHAR(match[1].str().c_str())); + } + it = match.suffix().first; + } + } + else if(contentType == httpContentTypeJson) + { + // TODO + } + + return result; +} + +FString UContentLoader::SelectContentByMethod(const TArray& content, const EContentDownloadMethod method) +{ + switch(method) + { + case EContentDownloadMethod::NonRepeatRandom: + { + auto filteredContent = UCommonFunctions::ArrayDiff(content, downloadedContent); + if(filteredContent.Num() == 0 && content.Num() != 0) + { + downloadedContent.Reset(); + filteredContent = UCommonFunctions::ArrayDiff(content, downloadedContent); + } + return filteredContent[FMath::RandRange(0, filteredContent.Num() - 1)]; + } + case EContentDownloadMethod::RatingOftenRandom: + // TODO + case EContentDownloadMethod::ClearRandom: + default: + return content[FMath::RandRange(0, content.Num() - 1)]; + } +} + +void UContentLoader::DownloadPak(FContentDownloadedCallback downloadedCallback, const EContentDownloadMethod method) +{ + UE_LOG(LogTemp, Log, TEXT("ContentLoader: Requesting pak http index at %s."), pakIndexUrl); + + FHttpRequestCompleteDelegate indexRequestCallback; + indexRequestCallback.BindLambda([=, this](FHttpRequestPtr request, FHttpResponsePtr response, bool successful) + { + if(!successful || response->GetResponseCode() != httpResponseOK) + { + int32 responseCode = response.IsValid() ? response->GetResponseCode() : -1; + UE_LOG(LogTemp, Log, TEXT("ContentLoader: Requesting pak http index failed with code: %d (-1 if response is null)."), responseCode); + return; + } + + TArray availablePaks = ParseDirectoryIndex(response->GetContent(), response->GetContentType()); + if(availablePaks.Num() == 0) + { + UE_LOG(LogTemp, Log, TEXT("ContentLoader: Pak http index is empty.")); + return; + } + + FString pakToDownload = SelectContentByMethod(availablePaks, method); + downloadedContent.Add(pakToDownload); + UE_LOG(LogTemp, Log, TEXT("ContentLoader: Starting to download pak %s from index."), *pakToDownload); + + FHttpRequestCompleteDelegate pakDownloadCallback; + pakDownloadCallback.BindLambda([=, this](FHttpRequestPtr request, FHttpResponsePtr response, bool successful) + { + if(!successful || response->GetResponseCode() != httpResponseOK) + { + UCommonFunctions::ArrayRemoveFirstFromEnd(downloadedContent, pakToDownload); + int32 responseCode = response.IsValid() ? response->GetResponseCode() : -1; + UE_LOG(LogTemp, Log, TEXT("ContentLoader: Pak downloading failed with code: %d (-1 if response is null)."), responseCode); + return; + } + + auto savePath = FString::Printf(TEXT("%s%s"), *pakFileSavePath, *pakToDownload); + if(!FFileHelper::SaveArrayToFile(response->GetContent(), *savePath)) + { + UE_LOG(LogTemp, Log, TEXT("ContentLoader: Saving pak file %s failed."), *savePath); + return; + } + UE_LOG(LogTemp, Log, TEXT("ContentLoader: Pak file saved as %s."), *savePath); + + downloadedCallback.ExecuteIfBound(savePath); + }); + + HttpGet(FString::Printf(TEXT("%s%s"), pakIndexUrl, *pakToDownload), pakDownloadCallback); + }); + + HttpGet(pakIndexUrl, indexRequestCallback); +} + UClass* UContentLoader::LoadPak(const FString& pakFilePath) { - UE_LOG(LogTemp, Log, TEXT("ContentLoader: Starting to mount pak %s"), *pakFilePath); + UE_LOG(LogTemp, Log, TEXT("ContentLoader: Starting to mount pak %s."), *pakFilePath); FPakPlatformFile* pakFileMgr = (FPakPlatformFile*)(FPlatformFileManager::Get().FindPlatformFile(TEXT("PakFile"))); if(!pakFileMgr) @@ -104,13 +235,13 @@ bool UContentLoader::UnloadPak(const FString& pakFilePath) FPakPlatformFile* pakFileMgr = (FPakPlatformFile*)(FPlatformFileManager::Get().FindPlatformFile(TEXT("PakFile"))); if(!pakFileMgr) { - UE_LOG(LogTemp, Warning, TEXT("Unable to get PakPlatformFile for pak file (Unmount): %s"), *pakFilePath); + UE_LOG(LogTemp, Warning, TEXT("ContentLoader: Unable to get PakPlatformFile for pak file (Unmount): %s."), *pakFilePath); return false; } if(!pakFileMgr->Unmount(*pakFilePath)) { - UE_LOG(LogTemp, Warning, TEXT("Unable to unmount pak: %s"), *pakFilePath); + UE_LOG(LogTemp, Warning, TEXT("ContentLoader: Unable to unmount pak: %s."), *pakFilePath); return false; } diff --git a/UnrealProject/Lost_Edge/Source/Lost_Edge/Private/ContentLoader.h b/UnrealProject/Lost_Edge/Source/Lost_Edge/Private/ContentLoader.h index b169842..2263206 100644 --- a/UnrealProject/Lost_Edge/Source/Lost_Edge/Private/ContentLoader.h +++ b/UnrealProject/Lost_Edge/Source/Lost_Edge/Private/ContentLoader.h @@ -2,10 +2,21 @@ #pragma once +#include "Http.h" #include "UObject/Object.h" #include "ContentLoader.generated.h" +UENUM(BlueprintType) +enum class EContentDownloadMethod : uint8 +{ + ClearRandom = 0, + NonRepeatRandom, + RatingOftenRandom +}; + +DECLARE_DYNAMIC_DELEGATE_OneParam(FContentDownloadedCallback, FString, pakFilePath); + UCLASS(BlueprintType) class UContentLoader : public UObject { @@ -13,15 +24,23 @@ class UContentLoader : public UObject public: UFUNCTION(BlueprintCallable, Category = "ContentLoader") - UClass* LoadPak(const FString& pakFilePath); + void DownloadPak(FContentDownloadedCallback downloadedCallback, const EContentDownloadMethod method = EContentDownloadMethod::NonRepeatRandom); -protected: - virtual void BeginDestroy() override; + UFUNCTION(BlueprintCallable, Category = "ContentLoader") + UClass* LoadPak(const FString& pakFilePath); UFUNCTION(BlueprintCallable, Category = "ContentLoader") bool UnloadPak(const FString& pakFilePath); protected: + virtual void BeginDestroy() override; + + void HttpGet(const FString& url, FHttpRequestCompleteDelegate requestCompleteCallback); + TArray ParseDirectoryIndex(const TArray& content, const FString& contentType); + FString SelectContentByMethod(const TArray& content, const EContentDownloadMethod method); + FString GetPakMountContent(const FString& pakFilePath, TArray* content = nullptr); UClass* GetPakClass(const FString& pakContentPath); + + TArray downloadedContent; };