diff --git a/README.md b/README.md index f394755e..eb2591a1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# Unreal.js-core +# Unreal.js-core kesselman dev fork - core component of [Unreal.js](https://github.com/ncsoft/Unreal.js) - Please visit [project repo](https://github.com/ncsoft/Unreal.js). + +Sample Blueprint: https://imgur.com/nexH05q + diff --git a/Source/JavascriptEditor/JavascriptEditorViewport.cpp b/Source/JavascriptEditor/JavascriptEditorViewport.cpp index c501ec7c..9cebe1fa 100644 --- a/Source/JavascriptEditor/JavascriptEditorViewport.cpp +++ b/Source/JavascriptEditor/JavascriptEditorViewport.cpp @@ -6,7 +6,7 @@ #include "Engine/Canvas.h" #include "Components/OverlaySlot.h" #include "AssetViewerSettings.h" -#include "Launch/Resources/Version.h" +#include "Runtime/Launch/Resources/Version.h" #include "Components/DirectionalLightComponent.h" #if ENGINE_MAJOR_VERSION > 4 #include "UnrealWidget.h" diff --git a/Source/JavascriptEditor/JavascriptUICommands.cpp b/Source/JavascriptEditor/JavascriptUICommands.cpp index aa89380d..bfa4d219 100644 --- a/Source/JavascriptEditor/JavascriptUICommands.cpp +++ b/Source/JavascriptEditor/JavascriptUICommands.cpp @@ -1,7 +1,7 @@ #include "JavascriptUICommands.h" #include "JavascriptMenuLibrary.h" #include "Framework/Commands/Commands.h" -#include "Launch/Resources/Version.h" +#include "Runtime/Launch/Resources/Version.h" //PRAGMA_DISABLE_OPTIMIZATION diff --git a/Source/JavascriptHttp/JavascriptHttpRequest.h b/Source/JavascriptHttp/JavascriptHttpRequest.h index e8182cc7..5dda2f96 100644 --- a/Source/JavascriptHttp/JavascriptHttpRequest.h +++ b/Source/JavascriptHttp/JavascriptHttpRequest.h @@ -7,7 +7,7 @@ #include "Interfaces/IHttpRequest.h" #include "Interfaces/IHttpResponse.h" #include "HttpModule.h" -#include "Launch/Resources/Version.h" +#include "Runtime/Launch/Resources/Version.h" #include "JavascriptHttpRequest.generated.h" diff --git a/Source/JavascriptUMG/JavascriptGameViewport.cpp b/Source/JavascriptUMG/JavascriptGameViewport.cpp index 08f80164..3e5f4e6b 100644 --- a/Source/JavascriptUMG/JavascriptGameViewport.cpp +++ b/Source/JavascriptUMG/JavascriptGameViewport.cpp @@ -8,7 +8,7 @@ #include "SceneView.h" #include "CanvasTypes.h" #include "Widgets/SViewport.h" -#include "Launch/Resources/Version.h" +#include "Runtime/Launch/Resources/Version.h" #define LOCTEXT_NAMESPACE "UMG" diff --git a/Source/JavascriptUMG/JavascriptWindow.cpp b/Source/JavascriptUMG/JavascriptWindow.cpp index 170f7511..8e704f90 100644 --- a/Source/JavascriptUMG/JavascriptWindow.cpp +++ b/Source/JavascriptUMG/JavascriptWindow.cpp @@ -1,6 +1,6 @@ #include "JavascriptWindow.h" #include "Widgets/SWindow.h" -#include "Launch/Resources/Version.h" +#include "Runtime/Launch/Resources/Version.h" UJavascriptWindow::UJavascriptWindow(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) diff --git a/Source/JavascriptWebSocket/JavascriptWebSocket.Build.cs b/Source/JavascriptWebSocket/JavascriptWebSocket.Build.cs index d8671e50..c434e417 100644 --- a/Source/JavascriptWebSocket/JavascriptWebSocket.Build.cs +++ b/Source/JavascriptWebSocket/JavascriptWebSocket.Build.cs @@ -48,7 +48,7 @@ private void HackWebSocketIncludeDir(String WebsocketPath, ReadOnlyTargetRules T } else if (Target.Platform == UnrealTargetPlatform.Linux) { - PlatformSubdir = Path.Combine(PlatformSubdir, Target.Architecture); + PlatformSubdir = Path.Combine(PlatformSubdir, Target.Architecture.ToString()); } PrivateDependencyModuleNames.Add("libWebSockets"); diff --git a/Source/V8/Private/JavaScriptModuleCompiler.cpp b/Source/V8/Private/JavaScriptModuleCompiler.cpp new file mode 100644 index 00000000..87591a80 --- /dev/null +++ b/Source/V8/Private/JavaScriptModuleCompiler.cpp @@ -0,0 +1,222 @@ +#include "JavaScriptModuleCompiler.h" + +#include "IV8.h" +#include "Misc/FileHelper.h" + +/******************************************************************** + * A static class that handles loading and execution EC6 module files + * to the V8 engine in the UE5 environment. + * Written by Professor Jeff Kesselman MS MFA + * Purdue University + * 11/23/2023 + * jpkessel@purdue.edu + * ***********************************/ +namespace module_compiler +{ + /***************************************************************************** + * char* readFile + * Reads file contents to a null-terminated string. + *****************************************************************************/ + char* readFile(const char filename[]) { + + // Opening file; ifstream::ate is use to determine file size + std::ifstream file; + file.open(filename, std::ifstream::ate); + char* contents; + if (!file) { + contents = new char[1]; + return contents; + } + + // Get file size + size_t file_size = file.tellg(); + + // Return file pointer from end of the file (set by ifstream::ate) to beginning + file.seekg(0); + + // Reading file to char array and returing it + std::filebuf* file_buf = file.rdbuf(); + contents = new char[file_size + 1](); + file_buf->sgetn(contents, file_size); + file.close(); + return contents; + } + + /***************************************************************************** + * void print + * Binding of simple console print function to the VM + *****************************************************************************/ + void print(const v8::FunctionCallbackInfo& args) { + + // Getting arguments; error handling + v8::Isolate* isolate = args.GetIsolate(); + v8::String::Utf8Value val(isolate, args[0]); + if (*val == nullptr) + isolate->ThrowException( + v8::String::NewFromUtf8(isolate, "First argument of function is empty") + .ToLocalChecked()); + + // Printing + printf("%s\n", *val); + } + + /***************************************************************************** + * v8::MaybeLocal loadModule + * Loads module from code[] without checking it + *****************************************************************************/ + v8::MaybeLocal JSModuleCompiler::loadModule(char code[], + char name[], + v8::Local cx) { + + // Convert char[] to VM's string type + v8::Local vcode = + v8::String::NewFromUtf8(cx->GetIsolate(), code).ToLocalChecked(); + + // Create script origin to determine if it is module or not. + // Only first and last argument matters; other ones are default values. + // First argument gives script name (useful in error messages), last + // informs that it is a module. + v8::ScriptOrigin origin( + v8::String::NewFromUtf8(cx->GetIsolate(), name).ToLocalChecked(), + v8::Integer::New(cx->GetIsolate(), 0), + v8::Integer::New(cx->GetIsolate(), 0), v8::False(cx->GetIsolate()), + v8::Local(), v8::Local(), + v8::False(cx->GetIsolate()), v8::False(cx->GetIsolate()), + v8::True(cx->GetIsolate())); + + // Compiling module from source (code + origin) + v8::Context::Scope context_scope(cx); + v8::ScriptCompiler::Source source(vcode, origin); + v8::MaybeLocal mod; + mod = v8::ScriptCompiler::CompileModule(cx->GetIsolate(), &source); + if (mod.IsEmpty()) + { + UE_LOG(LogTemp, Warning, TEXT("Script failed to load")); + } + // Returning non-checked module + return mod; + } + + /***************************************************************************** + * v8::Local checkModule + * Checks out module (if it isn't nullptr/empty) + *****************************************************************************/ + v8::Local JSModuleCompiler::checkModule(v8::MaybeLocal maybeModule, + v8::Local cx) { + + // Checking out + v8::Local mod; + if (!maybeModule.ToLocal(&mod)) { + printf("Error loading module!\n"); + exit(EXIT_FAILURE); + } + + // Instantianing (including checking out depedencies). It uses callResolve + // as callback: check # + v8::Maybe result = mod->InstantiateModule(cx, JSModuleCompiler::callResolve); + if (result.IsNothing()) { + printf("\nCan't instantiate module.\n"); + exit(EXIT_FAILURE); + } + + // Returning check-out module + return mod; + } + + /***************************************************************************** + * v8::Local execModule + * Executes module's code + *****************************************************************************/ + v8::Local JSModuleCompiler::execModule(v8::Local mod, + v8::Local cx, + bool nsObject) { + + cx->GetIsolate()->SetHostImportModuleDynamicallyCallback(callDynamic); + + // Executing module with return value + v8::Local retValue; + if (!mod->Evaluate(cx).ToLocal(&retValue)) { + printf("Error evaluating module!\n"); + exit(EXIT_FAILURE); + } + + // nsObject determins, if module namespace or return value has to be returned. + // Module namespace is required during import callback; see lines # and #. + if (nsObject) + return mod->GetModuleNamespace(); + else + return retValue; + } + + /***************************************************************************** + * v8::MaybeLocal callResolve + * Callback from static import. + *****************************************************************************/ + v8::MaybeLocal JSModuleCompiler::callResolve(v8::Local context, + v8::Local specifier, + v8::Local referrer) { + + v8::String::Utf8Value filename(context->GetIsolate(), specifier); + v8::String::Utf8Value fileroot(context->GetIsolate(), + context->GetEmbedderData(1)); + FString fqn(*fileroot); + fqn = fqn.Append(FString("/"))+FString(*filename); + + // Return unchecked module + FString Text; + if (!FFileHelper::LoadFileToString(Text, *fqn)) + { + std::string sfilename(*filename); + FString msg = TEXT("Failed to read script file '%s'"); + UE_LOG(LogJavascript, Warning, + TEXT("Failed to read script file '%s'"), + sfilename.c_str()); + } + + return loadModule(TCHAR_TO_ANSI(*Text), *filename, context); + } + + /***************************************************************************** + * v8::MaybeLocal callDynamic + * Callback from dynamic import. + *****************************************************************************/ + v8::MaybeLocal JSModuleCompiler::callDynamic(v8::Local context, + v8::Local referrer, + v8::Local specifier, + v8::Local import_assertions) { + + // Promise resolver: that way promise for dynamic import can be rejected + // or full-filed + v8::Local resolver = + v8::Promise::Resolver::New(context).ToLocalChecked(); + v8::MaybeLocal promise(resolver->GetPromise()); + + // Loading module (with checking) + v8::String::Utf8Value name(context->GetIsolate(), specifier); + v8::Local mod = + checkModule(loadModule(readFile(*name), *name, context), context); + v8::Local retValue = execModule(mod, context, true); + + // Resolving (fulfilling) promise with module global namespace + resolver->Resolve(context, retValue); + return promise; + } + + /***************************************************************************** + * void callMeta + * Callback for module metadata. + *****************************************************************************/ + void JSModuleCompiler::callMeta(v8::Local context, + v8::Local module, + v8::Local meta) { + + // In this example, this is throw-away function. But it shows that you can + // bind module's url. Here, placeholder is used. + meta->Set( + context, + v8::String::NewFromUtf8(context->GetIsolate(), "url").ToLocalChecked(), + v8::String::NewFromUtf8(context->GetIsolate(), "https://something.sh") + .ToLocalChecked()); + } + +} diff --git a/Source/V8/Private/JavaScriptModuleCompiler.h b/Source/V8/Private/JavaScriptModuleCompiler.h new file mode 100644 index 00000000..5a29a922 --- /dev/null +++ b/Source/V8/Private/JavaScriptModuleCompiler.h @@ -0,0 +1,39 @@ +#pragma once +/********************************************************** + * Javascript EC6 (modules) compiler + * by Jeffrey Kesselman (jpkessel@purdue.edu) + * + * Heavily cribbed from https://gist.github.com/surusek/4c05e4dcac6b82d18a1a28e6742fc23e + * + **********************************************************/ +#include +#include + +namespace module_compiler +{ + class JSModuleCompiler + { + private: + static v8::MaybeLocal callResolve(v8::Local context, v8::Local specifier, + v8::Local referrer); + static v8::MaybeLocal callDynamic(v8::Local context, v8::Local referrer, + v8::Local specifier, + v8::Local import_assertions); + static void callMeta(v8::Local context, v8::Local module, v8::Local meta); + + public: + static v8::MaybeLocal loadModule(char code[], + char name[], + v8::Local cx); + + // Check, if module isn't empty (or pointer to it); line #221 + static v8::Local checkModule(v8::MaybeLocal maybeModule, + v8::Local cx); + + // Executes module; line #247 + static v8::Local execModule(v8::Local mod, + v8::Local cx, + bool nsObject = false); + + }; +} diff --git a/Source/V8/Private/JavascriptContext_Private.cpp b/Source/V8/Private/JavascriptContext_Private.cpp index 6912005b..c4663229 100644 --- a/Source/V8/Private/JavascriptContext_Private.cpp +++ b/Source/V8/Private/JavascriptContext_Private.cpp @@ -30,8 +30,10 @@ #include "JavascriptStats.h" #include "../../Launch/Resources/Version.h" +#include "JavaScriptModuleCompiler.h" using namespace v8; +using namespace module_compiler; static const int kContextEmbedderDataIndex = 0; static const int32 MagicNumber = 0x2852abd3; @@ -1782,7 +1784,10 @@ class FJavascriptContextImplementation : public FJavascriptContext auto ScriptPath = GetScriptFileFullPath(Filename); FString Text; - if (Args.Num() > 0) + if (Filename.EndsWith(".mjs")){ // imports cannto be wrapped + // no argument support atm + Text = Script; + } else if (Args.Num() > 0) { FString strArgs = FString::Printf(TEXT("\'%s\'"), *Args[0]); for (int32 i = 1; i < Args.Num(); ++i) @@ -1797,8 +1802,14 @@ class FJavascriptContextImplementation : public FJavascriptContext { Text = FString::Printf(TEXT("(function (global,__filename,__dirname) { %s\n;}(this,'%s','%s'));"), *Script, *ScriptPath, *FPaths::GetPath(ScriptPath)); } - - auto ret = RunScript(ScriptPath, Text, 0); + Local ret; + if (Filename.EndsWith(".mjs")) + { + ret = RunModule(ScriptPath, Text, 0); + } else + { + ret = RunScript(ScriptPath, Text, 0); + } return ret.IsEmpty()? TEXT("(empty)") : StringFromV8(isolate(), ret); } @@ -1823,6 +1834,22 @@ class FJavascriptContextImplementation : public FJavascriptContext return str; } + FString Public_RunModule(const FString& Script, bool bOutput = true) + { + Isolate::Scope isolate_scope(isolate()); + HandleScope handle_scope(isolate()); + Context::Scope context_scope(context()); + + auto ret = RunModule(TEXT("(inline)"), Script); + auto str = ret.IsEmpty() ? TEXT("(empty)") : StringFromV8(isolate(), ret); + + if (bOutput && !ret.IsEmpty()) + { + UE_LOG(LogJavascript, Log, TEXT("%s"), *str); + } + return str; + } + void RequestV8GarbageCollection() { isolate()->LowMemoryNotification(); @@ -1870,6 +1897,57 @@ class FJavascriptContextImplementation : public FJavascriptContext } } + Local RunModule(const FString& Filename, const FString& Script, int line_offset = 0) + { + Isolate::Scope isolate_scope(isolate()); + Context::Scope context_scope(context()); + + TryCatch try_catch(isolate()); + try_catch.SetVerbose(true); + + auto Path = Filename; +#if PLATFORM_WINDOWS + // HACK for Visual Studio Code + if (Path.Len() && Path[1] == ':') + { + Path = Path.Mid(0, 1).ToLower() + Path.Mid(1); + } +#endif + + int lastDotIndex; + Filename.FindLastChar('/',lastDotIndex); + FString filename = Filename.Right(Filename.Len()-(lastDotIndex+1)); + FString filePath = Filename.Left(lastDotIndex); + auto ctx = context(); + ctx->SetEmbedderData(0, + String::NewFromUtf8( + ctx->GetIsolate(),TCHAR_TO_ANSI(*filename)).ToLocalChecked()); + ctx->SetEmbedderData(1, + String::NewFromUtf8( + ctx->GetIsolate(),TCHAR_TO_ANSI(*filePath)).ToLocalChecked()); + MaybeLocal MaybeMod = + JSModuleCompiler::loadModule( TCHAR_TO_ANSI(*Script), + TCHAR_TO_ANSI(*Filename),ctx); + if (MaybeMod.IsEmpty()) + { + UE_LOG(LogTemp, Warning, TEXT("Script load failed")); + return Local(); + } else + { + MaybeMod = JSModuleCompiler::checkModule(MaybeMod,ctx); + if (MaybeMod.IsEmpty()) + { + UE_LOG(LogTemp, Warning, TEXT("Script check failed")); + return Local(); + } else + { + return JSModuleCompiler::execModule(MaybeMod.ToLocalChecked(), ctx); + } + } + + + } + void FindPathFile(FString TargetRootPath, FString TargetFileName, TArray& OutFiles) { IFileManager::Get().FindFilesRecursive(OutFiles, TargetRootPath.GetCharArray().GetData(), TargetFileName.GetCharArray().GetData(), true, false); diff --git a/Source/V8/Private/JavascriptContext_Private.h b/Source/V8/Private/JavascriptContext_Private.h index dead2d5f..105a0ac7 100644 --- a/Source/V8/Private/JavascriptContext_Private.h +++ b/Source/V8/Private/JavascriptContext_Private.h @@ -47,6 +47,7 @@ struct FJavascriptContext : TSharedFromThis virtual FString GetScriptFileFullPath(const FString& Filename) = 0; virtual FString ReadScriptFile(const FString& Filename) = 0; virtual FString Public_RunScript(const FString& Script, bool bOutput = true) = 0; + virtual FString Public_RunModule(const FString& String, bool bOutput) = 0; virtual void RequestV8GarbageCollection() = 0; virtual FString Public_RunFile(const FString& Filename, const TArray& Args) = 0; virtual void FindPathFile(const FString TargetRootPath, const FString TargetFileName, TArray& OutFiles) = 0; @@ -65,7 +66,7 @@ struct FJavascriptContext : TSharedFromThis virtual v8::Local context() = 0; virtual v8::Local ExportObject(UObject* Object, bool bForce = false) = 0; virtual v8::Local GetProxyFunction(v8::Local Context, UObject* Object, const TCHAR* Name) = 0; - + static FJavascriptContext* FromV8(v8::Local Context); static FJavascriptContext* Create(TSharedPtr InEnvironment, TArray& InPaths); diff --git a/Source/V8/Private/JavascriptIsolate_Private.cpp b/Source/V8/Private/JavascriptIsolate_Private.cpp index 42b6a626..974f9174 100644 --- a/Source/V8/Private/JavascriptIsolate_Private.cpp +++ b/Source/V8/Private/JavascriptIsolate_Private.cpp @@ -1788,7 +1788,7 @@ class FJavascriptIsolateImplementation : public FJavascriptIsolate auto Function = reinterpret_cast((Local::Cast(info.Data()))->Value()); // Determine 'this' - auto Object = (Function->FunctionFlags & FUNC_Static) ? Function->GetOwnerClass()->ClassDefaultObject : UObjectFromV8(isolate->GetCurrentContext(), self); + TObjectPtr Object = (Function->FunctionFlags & FUNC_Static) ? Function->GetOwnerClass()->ClassDefaultObject : TObjectPtr(UObjectFromV8(isolate->GetCurrentContext(), self)); // Check 'this' is valid if (!IsValid(Object)) diff --git a/Source/V8/Private/JavascriptLibrary.cpp b/Source/V8/Private/JavascriptLibrary.cpp index d9813155..91677be3 100644 --- a/Source/V8/Private/JavascriptLibrary.cpp +++ b/Source/V8/Private/JavascriptLibrary.cpp @@ -6,7 +6,7 @@ #include "Sockets.h" #include "NavigationSystem.h" #include "HAL/PlatformApplicationMisc.h" -#include "Launch/Resources/Version.h" +#include "Runtime/Launch/Resources/Version.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "UObject/MetaData.h" diff --git a/Source/V8/Private/Translator.cpp b/Source/V8/Private/Translator.cpp index 704e31ee..c701d649 100644 --- a/Source/V8/Private/Translator.cpp +++ b/Source/V8/Private/Translator.cpp @@ -1,6 +1,6 @@ #include "Translator.h" #include "Engine/UserDefinedStruct.h" -#include "Launch/Resources/Version.h" +#include "Runtime/Launch/Resources/Version.h" namespace v8 { diff --git a/Source/V8/Private/V8Implementation.cpp b/Source/V8/Private/V8Implementation.cpp index 0b890c3b..85d7cac1 100644 --- a/Source/V8/Private/V8Implementation.cpp +++ b/Source/V8/Private/V8Implementation.cpp @@ -159,6 +159,15 @@ FString UJavascriptContext::RunScript(FString Script, bool bOutput) return TEXT(""); } +FString UJavascriptContext::RunModule(FString Script, bool bOutput) +{ + if (JavascriptContext.IsValid()) + { + return JavascriptContext->Public_RunModule(Script, bOutput); + } + return TEXT(""); +} + void UJavascriptContext::RegisterConsoleCommand(FString Command, FString Help, FJavascriptFunction Function) { #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) diff --git a/Source/V8/Public/JavascriptContext.h b/Source/V8/Public/JavascriptContext.h index 9642e4e4..a786faa5 100644 --- a/Source/V8/Public/JavascriptContext.h +++ b/Source/V8/Public/JavascriptContext.h @@ -73,6 +73,10 @@ class V8_API UJavascriptContext : public UObject UFUNCTION(BlueprintCallable, Category = "Scripting|Javascript") FString RunScript(FString Script, bool bOutput = true); + // Added by JPK on 11/24/2023 + UFUNCTION(BlueprintCallable, Category = "Scripting|Javascript") + FString RunModule(FString Script, bool bOutput = true); + UFUNCTION(BlueprintCallable, Category = "Scripting|Javascript") void RegisterConsoleCommand(FString Command, FString Help, FJavascriptFunction Function); diff --git a/Source/V8/Public/JavascriptIsolate.h b/Source/V8/Public/JavascriptIsolate.h index 509cc143..98db1935 100644 --- a/Source/V8/Public/JavascriptIsolate.h +++ b/Source/V8/Public/JavascriptIsolate.h @@ -103,7 +103,7 @@ struct V8_API FJavascriptHeapStatistics bool bDoesZapGarbage = false; }; -UCLASS() +UCLASS(BlueprintType) class V8_API UJavascriptIsolate : public UObject { GENERATED_UCLASS_BODY() @@ -113,6 +113,8 @@ class V8_API UJavascriptIsolate : public UObject TSharedPtr JavascriptIsolate; + + UFUNCTION(BlueprintCallable, Category = "Scripting|Javascript") void Init(bool bIsEditor); diff --git a/Source/V8/V8.Build.cs b/Source/V8/V8.Build.cs index d7396264..a0479755 100644 --- a/Source/V8/V8.Build.cs +++ b/Source/V8/V8.Build.cs @@ -85,7 +85,7 @@ private void HackWebSocketIncludeDir(String WebsocketPath, ReadOnlyTargetRules T } else if (Target.Platform == UnrealTargetPlatform.Linux) { - PlatformSubdir = Path.Combine(PlatformSubdir, Target.Architecture); + PlatformSubdir = Path.Combine(PlatformSubdir, Target.Architecture.ToString()); } PrivateDependencyModuleNames.Add("libWebSockets"); diff --git a/UnrealJS.uplugin b/UnrealJS.uplugin index eff3a885..895ceecb 100644 --- a/UnrealJS.uplugin +++ b/UnrealJS.uplugin @@ -1,14 +1,14 @@ { "FileVersion": 3, - "FriendlyName": "Unreal.js - Javascript Runtime", + "FriendlyName": "Unreal.js.jpk - Fork of Javascript Runtime", "Version": 2, "VersionName": "0.6.4", "FriendlyVersion": "0.6.4", - "EngineVersion": "5.1.0", + "EngineVersion": "5.3", "Description": "Javascript powered UnrealEngine", "Category": "Programming", - "CreatedBy": "NCSOFT Corporation", - "CreatedByURL": "http://github.com/ncsoft", + "CreatedBy": "Professor K", + "CreatedByURL": "http://github.com/profk", "EnabledByDefault": true, "MarketplaceURL" : "com.epicgames.launcher://ue/marketplace/content/be751eedc4a14cc09e39945bc5a531c4", "Modules": [