├── Config └── FilterPlugin.ini ├── UE4Orchestrator.uplugin ├── LICENSE ├── .gitignore ├── Source └── UE4Orchestrator │ ├── Public │ └── UE4Orchestrator.h │ ├── UE4Orchestrator.build.cs │ └── Private │ ├── UE4OrchestratorPrivate.h │ └── UE4Orchestrator.cpp ├── Jenkinsfile └── README.md /Config/FilterPlugin.ini: -------------------------------------------------------------------------------- 1 | [FilterPlugin] 2 | /README.md 3 | -------------------------------------------------------------------------------- /UE4Orchestrator.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion" : 3, 3 | "Version" : 1, 4 | "VersionName": "0.0.1", 5 | "FriendlyName": "UE4 Orchestrator", 6 | "Description": "Embedded HTTP server to do your UE4 bidding.", 7 | "Category" : "Research", 8 | "CreatedBy": "sabhiram", 9 | "CreatedByURL": "https://github.com/recogni/ue4-orchestrator", 10 | "DocsURL" : "https://github.com/recogni/ue4-orchestrator", 11 | "EnabledByDefault" : true, 12 | "CanContainContent": true, 13 | "IsBetaVersion" : true, 14 | "Installed" : true, 15 | "Modules" : [ 16 | { 17 | "Name": "UE4Orchestrator", 18 | "Type": "Developer", 19 | "LoadingPhase": "Default" 20 | }, 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Recogni Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Plugins/ 3 | 4 | # Visual Studio 2015 user specific files 5 | .vs/ 6 | 7 | # Visual Studio 2015 database file 8 | *.VC.db 9 | 10 | # Compiled Object files 11 | *.slo 12 | *.lo 13 | *.o 14 | *.obj 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Compiled Dynamic libraries 21 | *.so 22 | *.dylib 23 | *.dll 24 | 25 | # Fortran module files 26 | *.mod 27 | 28 | # Compiled Static libraries 29 | *.lai 30 | *.la 31 | *.a 32 | *.lib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.ipa 39 | 40 | # These project files can be generated by the engine 41 | *.xcodeproj 42 | *.xcworkspace 43 | *.sln 44 | *.suo 45 | *.opensdf 46 | *.sdf 47 | *.VC.db 48 | *.VC.opendb 49 | 50 | # Precompiled Assets 51 | SourceArt/**/*.png 52 | SourceArt/**/*.tga 53 | 54 | # Binary Files 55 | Binaries/* 56 | 57 | # Builds 58 | Build/* 59 | 60 | # Whitelist PakBlacklist-.txt files 61 | !Build/*/ 62 | Build/*/** 63 | !Build/*/PakBlacklist*.txt 64 | 65 | # Don't ignore icon files in Build 66 | !Build/**/*.ico 67 | 68 | # Built data for maps 69 | *_BuiltData.uasset 70 | 71 | # Configuration files generated by the Editor 72 | Saved/* 73 | 74 | # Compiled source files for the engine to use 75 | Intermediate/* 76 | 77 | # Cache files for the editor to use 78 | DerivedDataCache/* 79 | -------------------------------------------------------------------------------- /Source/UE4Orchestrator/Public/UE4Orchestrator.h: -------------------------------------------------------------------------------- 1 | /* -*- mode: c; tab-width: 4; indent-tabs-mode: nil; -*- */ 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Modules/ModuleInterface.h" 7 | #include "Modules/ModuleManager.h" 8 | 9 | // Logging stuff ... 10 | DECLARE_LOG_CATEGORY_EXTERN(LogUE4Orc, Log, All); 11 | 12 | #include "mongoose.h" 13 | 14 | // UE4 15 | #include "IPlatformFilePak.h" 16 | #include "Runtime/Core/Public/HAL/FileManagerGeneric.h" 17 | #include "StreamingNetworkPlatformFile.h" 18 | #include "Runtime/AssetRegistry/Public/AssetRegistryModule.h" 19 | #include "Modules/ModuleInterface.h" 20 | 21 | #if WITH_EDITOR 22 | # include "LevelEditor.h" 23 | # include "Editor.h" 24 | # include "Editor/LevelEditor/Public/ILevelViewport.h" 25 | # include "Editor/LevelEditor/Public/LevelEditorActions.h" 26 | # include "Editor/UnrealEd/Public/LevelEditorViewport.h" 27 | #endif 28 | 29 | //////////////////////////////////////////////////////////////////////////////// 30 | 31 | typedef struct mg_str mg_str_t; 32 | typedef struct http_message http_message_t; 33 | typedef FModuleManager FManager; 34 | typedef FActorComponentTickFunction FTickFn; 35 | 36 | #if WITH_EDITOR 37 | typedef FLevelEditorModule FLvlEditor; 38 | #endif 39 | 40 | #define T TEXT 41 | #define LOG(fmt, ...) UE_LOG(LogUE4Orc, Log, TEXT(fmt), __VA_ARGS__) 42 | -------------------------------------------------------------------------------- /Source/UE4Orchestrator/UE4Orchestrator.build.cs: -------------------------------------------------------------------------------- 1 | using UnrealBuildTool; 2 | using System.IO; 3 | using System.Collections.Generic; 4 | 5 | public class UE4Orchestrator : ModuleRules 6 | { 7 | #if WITH_FORWARDED_MODULE_RULES_CTOR 8 | public UE4Orchestrator(ReadOnlyTargetRules Target) : base(Target) 9 | #else 10 | public UE4Orchestrator(TargetInfo Target) 11 | #endif // WITH_FORWARDED_MODULE_RULES_CTOR 12 | { 13 | PrivateDependencyModuleNames.AddRange( 14 | new string[] 15 | { 16 | "Sockets", 17 | "StreamingFile", 18 | "LevelEditor", 19 | "NetworkFile", 20 | "PakFile", 21 | "InputCore", 22 | "Json", 23 | "JsonUtilities", 24 | "AssetTools", 25 | "HTTP", 26 | // ... add private dependencies that you statically link with here ... 27 | } 28 | ); 29 | 30 | PublicDependencyModuleNames.AddRange( 31 | new string[] { 32 | "Core", 33 | "CoreUObject", // @todo Mac: for some reason it's needed to link in debug on Mac 34 | "Engine", 35 | "PakFile", 36 | "Sockets", 37 | "StreamingFile", 38 | "LevelEditor", 39 | "NetworkFile", 40 | "InputCore", 41 | "Json", 42 | "JsonUtilities", 43 | "AssetTools", 44 | } 45 | ); 46 | 47 | PrivateIncludePaths.AddRange( 48 | new string[] { 49 | "UE4Orchestrator/Private", 50 | // ... add other private include paths required here ... 51 | } 52 | ); 53 | 54 | PublicIncludePaths.AddRange( 55 | new string[] { 56 | "UE4Orchestrator/Public", 57 | // ... add other private include paths required here ... 58 | } 59 | ); 60 | 61 | if (Target.bBuildEditor) 62 | { 63 | PrivateDependencyModuleNames.Add("UnrealEd"); 64 | PublicDependencyModuleNames.Add("UnrealEd"); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | scmVars = [:] 2 | 3 | pipeline { 4 | agent any 5 | 6 | options { 7 | skipDefaultCheckout() 8 | } 9 | 10 | environment { 11 | GCP_PROJECT_ID = sh(script: "gcloud config get-value project", returnStdout: true).trim() 12 | SLACK_CHANNEL = "dev-automation" 13 | } 14 | 15 | stages { 16 | stage("Checkout") { 17 | steps { 18 | script { 19 | scmVars = checkout(scm) 20 | 21 | scmVars.GIT_URL = scmVars.GIT_URL.replaceFirst(/\.git$/, "") 22 | scmVars.GIT_REPOSITORY = scmVars.GIT_URL.replaceFirst(/^[a-z]+:\/\/[^\/]*\//, "") 23 | scmVars.GIT_AUTHOR = sh(script: "${GIT_EXEC_PATH}/git log -1 --pretty=%an ${scmVars.GIT_COMMIT}", returnStdout: true).trim() 24 | scmVars.GIT_MESSAGE = sh(script: "${GIT_EXEC_PATH}/git log -1 --pretty=%s ${scmVars.GIT_COMMIT}", returnStdout: true).trim() 25 | 26 | scmVars.each { k, v -> 27 | env."${k}" = "${v}" 28 | } 29 | } 30 | } 31 | } 32 | 33 | stage("Trigger") { 34 | steps { 35 | script { 36 | withCredentials([[$class: "UsernamePasswordMultiBinding", credentialsId: "jenkins-api", 37 | usernameVariable: "JENKINS_USERNAME", passwordVariable: "JENKINS_TOKEN"]]) { 38 | if (scmVars.GIT_BRANCH == "master") { 39 | sh('/opt/bitnami/common/bin/curl -k -X POST -u "${JENKINS_USERNAME}:${JENKINS_TOKEN}" "${JENKINS_URL}job/ue4-docker/job/master/build"') 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | post { 48 | success { 49 | sendSlackMessage("Success", "good") 50 | } 51 | 52 | failure { 53 | sendSlackMessage("Failure", "danger") 54 | } 55 | } 56 | } 57 | 58 | void sendSlackMessage(String result = "Success", String color = "good") { 59 | slackSend(channel: env.SLACK_CHANNEL, 60 | color: color, 61 | message: "${result}: ${scmVars.GIT_AUTHOR.split()[0]}'s build <${currentBuild.absoluteUrl}|${currentBuild.displayName}> in <${scmVars.GIT_URL}|${scmVars.GIT_REPOSITORY}> (<${scmVars.GIT_URL}/commit/${scmVars.GIT_COMMIT}|${scmVars.GIT_COMMIT.substring(0,8)}> on <${scmVars.GIT_URL}/tree/${scmVars.GIT_BRANCH}|${scmVars.GIT_BRANCH}>)\n• ${scmVars.GIT_MESSAGE}") 62 | } 63 | -------------------------------------------------------------------------------- /Source/UE4Orchestrator/Private/UE4OrchestratorPrivate.h: -------------------------------------------------------------------------------- 1 | /* -*- mode: c; tab-width: 4; indent-tabs-mode: nil; -*- */ 2 | 3 | #include "UE4Orchestrator.h" 4 | #include "UE4OrchestratorPrivate.generated.h" 5 | 6 | #pragma once 7 | 8 | //////////////////////////////////////////////////////////////////////////////// 9 | 10 | UCLASS() 11 | class UE4ORCHESTRATOR_API URCHTTP : public UObject, public FTickableGameObject 12 | { 13 | GENERATED_UCLASS_BODY() 14 | virtual ~URCHTTP(); 15 | 16 | public: 17 | 18 | void Init(); 19 | 20 | /* 21 | * FTickableObject interface. 22 | */ 23 | virtual void Tick(float dt) override; 24 | virtual TStatId GetStatId() const override; 25 | virtual bool IsTickable() const { return true; } 26 | virtual bool IsTickableWhenPaused() const { return true; } 27 | virtual bool IsTickableInEditor() const { return true; } 28 | 29 | /* 30 | * UObject interface. 31 | */ 32 | virtual void Serialize(FArchive& ar) override; 33 | virtual void PostLoad() override; 34 | virtual void PostInitProperties() override; 35 | #if WITH_EDITOR 36 | virtual void PostEditChangeProperty(FPropertyChangedEvent& evt) override; 37 | #endif 38 | 39 | void SetPollInterval(int v); 40 | 41 | private: 42 | 43 | struct mg_mgr mgr; 44 | struct mg_connection* conn; 45 | 46 | /* 47 | * The poll_interval dictates how often we wish to poll. If 48 | * this is set to 0 (default), it will invoke a poll of 49 | * `poll_ms` (default=1ms) each time this plugin gets a tick. 50 | * However, if this is set to N (N > 0), this will only call 51 | * poll once every N ticks. 52 | */ 53 | int poll_interval; 54 | int poll_ms; 55 | 56 | /* 57 | * Pak file. 58 | */ 59 | FPakPlatformFile *PakFileMgr; 60 | 61 | public: 62 | 63 | UFUNCTION() 64 | static URCHTTP* Get(); 65 | 66 | UFUNCTION() 67 | int MountPakFile(const FString& PakPath, bool bLoadContent); 68 | 69 | /* 70 | * TODO: LoadObject should probably be renamed to LoadObjectPak() or 71 | * something to that effect. 72 | */ 73 | UFUNCTION() 74 | UObject* LoadObject(const FString& ObjectPath); 75 | 76 | UFUNCTION() 77 | int UnloadObject(const FString& ObjectPath); 78 | 79 | UFUNCTION() 80 | void GarbageCollect(); 81 | 82 | UFUNCTION() 83 | void FinishAllShaderCompilation(); 84 | 85 | /* 86 | * Frame End 87 | */ 88 | UFUNCTION() 89 | virtual void GameRenderSync(); 90 | 91 | }; 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UE4-Orchestrator 2 | 3 | An unreal engine plugin with an embedded HTTP server to do your bidding. 4 | 5 | ## Install 6 | 7 | Install this plugin like any other UE4 plugin. Once this is done, the plugin will host a HTTP server at port `18820`. 8 | 9 | ## HTTP GET Endpoints 10 | 11 | All these endpoints will trigger the subsequent functionality in the engine. There is never a request body expected, and only the `200` status code indicates a success. 12 | 13 | | Endpoint | Description | 14 | |--------------|-----------------------------------------------------------------------| 15 | | /play | Trigger a play in the current level | 16 | | /stop | Stop playback | 17 | | /shutdown | Shutdown the UE4 editor | 18 | | /build | Trigger a build in the editor | 19 | | /is_building | Returns TRUE if the editor is currently building | 20 | | /list_assets | List all assets registered with the asset registry module | 21 | | /assets_idle | Returns OK if the asset importer is idle, returns TRY_AGAIN otherwise | 22 | | /debug | Calls the `debugFn()` used for experimentation | 23 | 24 | ## HTTP POST Endpoints 25 | 26 | | Endpoint | Description | 27 | |--------------|-----------------------------------------------------------------------| 28 | | /command | Execute a console command in the editor | 29 | | /loadpak | Load a pakfile | 30 | 31 | ### `POST /command` 32 | 33 | Post body is expected to be a `string`. The body is passed directly to the UE4 editor console where it is `Exec`'d. 34 | 35 | Example: Run a python script in the scripts dir called `foo.py` with arguments `a`, `b` and `42`: 36 | ``` 37 | echo "py.exec_args foo.py a b 42" | http POST localhost:18820/command 38 | ``` 39 | 40 | Example: Run a random UE4 engine console command: 41 | ``` 42 | echo "showfps" | http POST localhost:18820/command 43 | ``` 44 | 45 | ### `POST /loadpak` 46 | 47 | Post body is expected to be a comma-separated-`string`. The first element is the path in the local file-system to the `.pak` file we wish to mount, and the second argument is the mount path. 48 | 49 | Eample: Mount `/tmp/foo.pak` into `/Content/Import/02843684`: 50 | ``` 51 | echo /tmp/foo.pak,/Content/Import/02843684 | http POST localhost:18820/loadpak 52 | ``` 53 | 54 | The `/loadpak` endpoint will also accept HTTP POST payloads with JSON where the following keys are expected to live: 55 | ``` 56 | { 57 | pak_path: <...>, // Path to pak file in the local system 58 | mount_point: <...>, // Game mount point 59 | } 60 | ``` 61 | 62 | The JSON deserializer is attempted first, failing which the payload is checked against the csv scheme. 63 | 64 | ## Detailed usage example 65 | 66 | ### Import Shapenet class `00000001` from `/tmp/shapenet/` into `/Game/Import` and generate `/tmp/output.pak`: 67 | 68 | ``` 69 | echo "py.exec_args import_fbx.py import_shapenet /tmp/shapenet/ 00000001" | http POST localhost:18820/command 70 | echo "py.exec_args import_fbx.py make_pak /Game/Import/ /tmp/output.pak" | http POST localhost:18820/command 71 | echo "/tmp/output.pak,/Content/Import/02843684" | http POST localhost:18820/loadpak 72 | ``` 73 | -------------------------------------------------------------------------------- /Source/UE4Orchestrator/Private/UE4Orchestrator.cpp: -------------------------------------------------------------------------------- 1 | /* -*- mode: c; tab-width: 4; indent-tabs-mode: nil; -*- */ 2 | 3 | /* 4 | * UE4Orchestrator.h acts as the PCH for this project and must be the 5 | * very first file imported. 6 | */ 7 | #include "UE4Orchestrator.h" 8 | 9 | #include 10 | #include 11 | 12 | // UE4 13 | #include "CoreMinimal.h" 14 | #include "IPlatformFilePak.h" 15 | #include "Runtime/Core/Public/HAL/FileManagerGeneric.h" 16 | #include "StreamingNetworkPlatformFile.h" 17 | #include "Runtime/AssetRegistry/Public/AssetRegistryModule.h" 18 | #include "Runtime/Core/Public/Misc/WildcardString.h" 19 | #include "Runtime/Engine/Classes/Engine/StreamableManager.h" 20 | #include "Runtime/Engine/Classes/Engine/AssetManager.h" 21 | #include "Runtime/Engine/Public/ShaderCompiler.h" 22 | #include "Runtime/Engine/Public/UnrealEngine.h" 23 | 24 | #if WITH_EDITOR 25 | # include "LevelEditor.h" 26 | # include "Editor.h" 27 | # include "Editor/LevelEditor/Public/ILevelViewport.h" 28 | # include "Editor/LevelEditor/Public/LevelEditorActions.h" 29 | # include "Editor/UnrealEd/Public/LevelEditorViewport.h" 30 | #endif 31 | 32 | #include "UE4OrchestratorPrivate.h" 33 | 34 | // HTTP server 35 | #include "mongoose.h" 36 | 37 | //////////////////////////////////////////////////////////////////////////////// 38 | 39 | DEFINE_LOG_CATEGORY(LogUE4Orc); 40 | 41 | //////////////////////////////////////////////////////////////////////////////// 42 | 43 | static void 44 | debugFn(FString payload) 45 | { 46 | return; 47 | } 48 | 49 | //////////////////////////////////////////////////////////////////////////////// 50 | 51 | int 52 | URCHTTP::MountPakFile(const FString& pakPath, bool bLoadContent) 53 | { 54 | int ret = 0; 55 | IPlatformFile *originalPlatform = &FPlatformFileManager::Get().GetPlatformFile(); 56 | 57 | // Check to see if the file exists first 58 | if (!originalPlatform->FileExists(*pakPath)) 59 | { 60 | LOG("PakFile %s does not exist", *pakPath); 61 | return -1; 62 | } 63 | 64 | // The pak reader is now the current platform file 65 | FPlatformFileManager::Get().SetPlatformFile(*PakFileMgr); 66 | 67 | // Get the mount point from the Pak meta-data 68 | FPakFile PakFile(PakFileMgr, *pakPath, false); 69 | FString MountPoint = PakFile.GetMountPoint(); 70 | 71 | // Determine where the on-disk path is for the mountpoint and register it 72 | FString PathOnDisk = FPaths::ProjectDir() / MountPoint; 73 | FPackageName::RegisterMountPoint(MountPoint, PathOnDisk); 74 | 75 | FString MountPointFull = PathOnDisk; 76 | FPaths::MakeStandardFilename(MountPointFull); 77 | 78 | LOG("Mounting at %s and registering mount point %s at %s", *MountPointFull, *MountPoint, *PathOnDisk); 79 | if (PakFileMgr->Mount(*pakPath, 0, *MountPointFull)) 80 | { 81 | if (UAssetManager* Manager = UAssetManager::GetIfValid()) 82 | { 83 | Manager->GetAssetRegistry().SearchAllAssets(true); 84 | if (bLoadContent) 85 | { 86 | TArray FileList; 87 | PakFile.FindFilesAtPath(FileList, *PakFile.GetMountPoint(), true, false, true); 88 | 89 | // Iterate over the collected files from the pak 90 | for (auto asset : FileList) 91 | { 92 | FString Package, BaseName, Extension; 93 | FPaths::Split(asset, Package, BaseName, Extension); 94 | FString ModifiedAssetName = Package / BaseName + "." + BaseName; 95 | 96 | LOG("Trying to load %s as %s ", *asset, *ModifiedAssetName); 97 | Manager->GetStreamableManager().LoadSynchronous(ModifiedAssetName, true, nullptr); 98 | } 99 | } 100 | } 101 | else 102 | { 103 | LOG("Asset manager not valid!", NULL); 104 | ret = -1; goto exit; 105 | } 106 | } 107 | else 108 | { 109 | LOG("mount failed!", NULL); 110 | ret = -1; goto exit; 111 | } 112 | 113 | exit: 114 | // Restore the platform file 115 | FPlatformFileManager::Get().SetPlatformFile(*originalPlatform); 116 | 117 | return ret; 118 | } 119 | 120 | UObject* 121 | URCHTTP::LoadObject(const FString& assetPath) 122 | { 123 | UObject* ret = nullptr; 124 | IPlatformFile *originalPlatform = &FPlatformFileManager::Get().GetPlatformFile(); 125 | 126 | if (PakFileMgr == nullptr) 127 | { 128 | LOG("Failed to create platform file %s", T("PakFile")); 129 | return ret; 130 | } 131 | 132 | // The pak reader is now the current platform file. 133 | FPlatformFileManager::Get().SetPlatformFile(*PakFileMgr); 134 | UAssetManager* Manager = UAssetManager::GetIfValid(); 135 | 136 | ret = FindObject(ANY_PACKAGE, *assetPath); 137 | if (Manager && ret == nullptr) 138 | ret = Manager->GetStreamableManager().LoadSynchronous(assetPath, false, nullptr); 139 | 140 | // Reset the platform file. 141 | FPlatformFileManager::Get().SetPlatformFile(*originalPlatform); 142 | 143 | return ret; 144 | } 145 | 146 | /* 147 | * TODO: Deprecate this function. 148 | */ 149 | int 150 | URCHTTP::UnloadObject(const FString& assetPath) 151 | { 152 | GarbageCollect(); 153 | return 0; 154 | } 155 | 156 | void 157 | URCHTTP::GarbageCollect() 158 | { 159 | CollectGarbage(RF_NoFlags, true); 160 | } 161 | 162 | void 163 | URCHTTP::FinishAllShaderCompilation() 164 | { 165 | if (GShaderCompilingManager) 166 | GShaderCompilingManager->FinishAllCompilation(); 167 | } 168 | 169 | void 170 | URCHTTP::GameRenderSync() 171 | { 172 | static FFrameEndSync FrameEndSync; 173 | /* Do a full sync without allowing a frame of lag */ 174 | FrameEndSync.Sync(false); 175 | } 176 | 177 | //////////////////////////////////////////////////////////////////////////////// 178 | 179 | // HTTP responses. 180 | const mg_str_t STATUS_OK = mg_mk_str("OK\r\n"); 181 | const mg_str_t STATUS_ERROR = mg_mk_str("ERROR\r\n"); 182 | const mg_str_t STATUS_TRY_AGAIN = mg_mk_str("TRY AGAIN\r\n"); 183 | const mg_str_t STATUS_NOT_SUPPORTED = mg_mk_str("NOT SUPPORTED\r\n"); 184 | const mg_str_t STATUS_NOT_IMPLEMENTED = mg_mk_str("NOT IMPLEMENTED\r\n"); 185 | const mg_str_t STATUS_BAD_ACTION = mg_mk_str("BAD ACTION\r\n"); 186 | const mg_str_t STATUS_BAD_ENTITY = mg_mk_str("BAD ENTITY\r\n"); 187 | 188 | // HTTP query responses. 189 | const mg_str_t STATUS_TRUE = mg_mk_str("TRUE\r\n"); 190 | const mg_str_t STATUS_FALSE = mg_mk_str("FALSE\r\n"); 191 | 192 | // Helper to match a list of URIs. 193 | template bool 194 | matches_any(mg_str_t* s, Strings... args) 195 | { 196 | std::vector items = {args...}; 197 | for (int i = 0; i < items.size(); i++) 198 | if (mg_vcmp(s, items[i].c_str()) == 0) 199 | return true; 200 | return false; 201 | } 202 | 203 | //////////////////////////////////////////////////////////////////////////////// 204 | 205 | static void 206 | ev_handler(struct mg_connection* conn, int ev, void *ev_data) 207 | { 208 | if (ev != MG_EV_HTTP_REQUEST) 209 | return; 210 | 211 | http_message_t* msg = (http_message_t *)ev_data; 212 | mg_str_t rspMsg = STATUS_ERROR; 213 | int rspStatus = 404; 214 | URCHTTP* server = URCHTTP::Get(); 215 | 216 | #if WITH_EDITOR 217 | auto ar = "AssetRegistry"; 218 | FAssetRegistryModule& AssetRegistry = 219 | FModuleManager::LoadModuleChecked(ar); 220 | 221 | /* 222 | * HTTP GET commands 223 | */ 224 | if (mg_vcmp(&msg->method, "GET") == 0) 225 | { 226 | /* 227 | * HTTP GET / 228 | * 229 | * Return "OK" 230 | */ 231 | if (matches_any(&msg->uri, "/")) 232 | { 233 | goto OK; 234 | } 235 | 236 | /* 237 | * HTTP GET /play 238 | * 239 | * Trigger a play in the current level. 240 | */ 241 | else if (matches_any(&msg->uri, "/play", "/ue4/play")) 242 | { 243 | GEditor->PlayMap(NULL, NULL, -1, -1, false); 244 | goto OK; 245 | } 246 | 247 | /* 248 | * HTTP GET /play 249 | * 250 | * Trigger a play in the current level. 251 | */ 252 | else if (matches_any(&msg->uri, "/stop", "/ue4/stop")) 253 | { 254 | FLvlEditor &Editor = 255 | FManager::LoadModuleChecked("LevelEditor"); 256 | 257 | if (!Editor.GetFirstActiveViewport().IsValid()) 258 | { 259 | LOG("%s", "ERROR no valid viewport"); 260 | goto ERROR; 261 | } 262 | 263 | if (Editor.GetFirstActiveViewport()->HasPlayInEditorViewport()) 264 | { 265 | FString cmd = "Exit"; 266 | auto ew = GEditor->GetEditorWorldContext().World(); 267 | GEditor->Exec(ew, *cmd, *GLog); 268 | } 269 | goto OK; 270 | } 271 | 272 | /* 273 | * HTTP GET /shutdown-now 274 | * 275 | * Trigger an editor immediate (possibly unclean) shutdown. 276 | */ 277 | else if (matches_any(&msg->uri, "/shutdown-now", "/ue4/shutdown-now")) 278 | { 279 | FGenericPlatformMisc::RequestExit(true); 280 | goto OK; 281 | } 282 | 283 | /* 284 | * HTTP GET /shutdown 285 | * 286 | * Trigger an editor shutdown. 287 | */ 288 | else if (matches_any(&msg->uri, "/shutdown", "/ue4/shutdown")) 289 | { 290 | FGenericPlatformMisc::RequestExit(false); 291 | goto OK; 292 | } 293 | 294 | /* 295 | * HTTP GET /build 296 | * 297 | * Trigger a build all for the current level. 298 | */ 299 | else if (matches_any(&msg->uri, "/build", "/ue4/build")) 300 | { 301 | FLevelEditorActionCallbacks::Build_Execute(); 302 | goto OK; 303 | } 304 | 305 | /* 306 | * HTTP GET /is_building 307 | * 308 | * Returns TRUE if the editor is currently building, FALSE otherwise. 309 | */ 310 | else if (matches_any(&msg->uri, "/is_building", "/ue4/is_building")) 311 | { 312 | bool ok = FLevelEditorActionCallbacks::Build_CanExecute(); 313 | goto NOT_IMPLEMENTED; 314 | } 315 | 316 | /* 317 | * HTTP GET /list_assets 318 | * 319 | * Logs all the assets that are registered with the asset manager. 320 | */ 321 | else if (matches_any(&msg->uri, "/list_assets", "/ue4/list_assets")) 322 | { 323 | TArray AssetData; 324 | AssetRegistry.Get().GetAllAssets(AssetData); 325 | for (auto data : AssetData) 326 | { 327 | FString path = *(data.PackageName.ToString()); 328 | FPaths::MakePlatformFilename(path); 329 | LOG("%s %s", *(data.PackageName.ToString()), *path); 330 | } 331 | goto OK; 332 | } 333 | 334 | /* 335 | * HTTP GET /assets_idle 336 | * 337 | * Returns OK if the importer is idle. Returns a status code to try 338 | * again otherwise. 339 | */ 340 | else if (matches_any(&msg->uri, "/assets_idle", "/ue4/assets_idle")) 341 | { 342 | if (AssetRegistry.Get().IsLoadingAssets()) 343 | goto ONE_MO_TIME; 344 | goto OK; 345 | } 346 | 347 | /* 348 | * HTTP GET /debug 349 | * 350 | * Catch-all debug endpoint. 351 | */ 352 | else if (matches_any(&msg->uri, "/debug", "/ue4/debug")) 353 | { 354 | debugFn(""); 355 | goto OK; 356 | } 357 | 358 | else if (matches_any(&msg->uri, "/gc")) 359 | { 360 | URCHTTP::Get()->GarbageCollect(); 361 | goto OK; 362 | } 363 | 364 | goto BAD_ACTION; 365 | } 366 | 367 | /* 368 | * HTTP POST commands 369 | */ 370 | else if (mg_vcmp(&msg->method, "POST") == 0) 371 | { 372 | FString body; 373 | if (msg->body.len > 0) 374 | { 375 | body = FString::Printf(T("%.*s"), msg->body.len, 376 | UTF8_TO_TCHAR(msg->body.p)); 377 | } 378 | 379 | /* 380 | * HTTP POST /poll_interval 381 | * 382 | * POST body should contain an int which specifies the polling 383 | * interval. This value should be positive to delay the polling, 384 | * and can be set to 0 (or negative) to assume default behavior of 385 | * one poll() per tick(). 386 | */ 387 | if (matches_any(&msg->uri, "/poll_interval")) 388 | { 389 | int x = FCString::Atoi(*body); 390 | if (x < 0) 391 | x = 0; 392 | server->SetPollInterval(x); 393 | goto OK; 394 | } 395 | 396 | /* 397 | * HTTP POST /command 398 | * 399 | * POST body should contain the exact console command that is to be 400 | * run in the UE4 console. 401 | */ 402 | else if (matches_any(&msg->uri, "/command", "/ue4/command")) 403 | { 404 | if (body.Len() > 0) 405 | { 406 | auto ew = GEditor->GetEditorWorldContext().World(); 407 | GEditor->Exec(ew, *body, *GLog); 408 | goto OK; 409 | } 410 | goto BAD_ENTITY; 411 | } 412 | 413 | /* 414 | * HTTP POST /ue4/loadpak 415 | * 416 | * POST body should contain a comma separated list of the following two 417 | * arguments: 418 | * 1. Local .pak file path to mount into the engine. 419 | * 2. "all" or "none" to indicate if the pak's content should be loaded 420 | * 421 | */ 422 | else if (matches_any(&msg->uri, "/loadpak", "/ue4/loadpak")) 423 | { 424 | if (body.Len() > 0) 425 | { 426 | int32 num_params; 427 | TArray pak_options; 428 | 429 | FString pakPath; 430 | 431 | body.TrimEndInline(); 432 | 433 | num_params = body.ParseIntoArray(pak_options, T(","), true); 434 | pakPath = pak_options[0]; 435 | 436 | if (pakPath.Len() == 0) 437 | goto ERROR; 438 | 439 | if (num_params != 2) 440 | goto ERROR; 441 | 442 | LOG("Mounting pak file: %s", *pakPath); 443 | 444 | if (URCHTTP::Get()->MountPakFile(pakPath, pak_options[1] == T("all")) < 0) 445 | goto ERROR; 446 | 447 | goto OK; 448 | } 449 | goto BAD_ENTITY; 450 | } 451 | 452 | else if (matches_any(&msg->uri, "/loadobj", "/ue4/loadobj")) 453 | { 454 | if (body.Len() > 0) 455 | { 456 | body.TrimEndInline(); 457 | 458 | TArray objects; 459 | int32 num_params = body.ParseIntoArray(objects, T(","), true); 460 | 461 | if (num_params==1) 462 | { 463 | if (URCHTTP::Get()->LoadObject(objects[0]) != nullptr) 464 | goto OK; 465 | goto ERROR; 466 | } 467 | } 468 | goto BAD_ENTITY; 469 | } 470 | 471 | else if (matches_any(&msg->uri, "/unloadobj", "/ue4/unloadobj")) 472 | { 473 | if (body.Len() > 0) 474 | { 475 | body.TrimEndInline(); 476 | 477 | TArray objects; 478 | int32 num_params = body.ParseIntoArray(objects, T(","), true); 479 | 480 | if (num_params == 1) 481 | { 482 | if (URCHTTP::Get()->UnloadObject(objects[0]) < 0) 483 | goto ERROR; 484 | goto OK; 485 | } 486 | } 487 | goto BAD_ENTITY; 488 | } 489 | 490 | /* 491 | * HTTP POST /debug 492 | * 493 | * Catch-all debug endpoint. 494 | */ 495 | else if (matches_any(&msg->uri, "/debug", "/ue4/debug")) 496 | { 497 | debugFn(body); 498 | goto OK; 499 | } 500 | goto BAD_ACTION; 501 | } 502 | #endif // WITH_EDITOR 503 | 504 | #pragma GCC diagnostic push 505 | #pragma GCC diagnostic ignored "-Wunused-label" 506 | ERROR: 507 | rspMsg = STATUS_ERROR; 508 | rspStatus = 501; 509 | goto done; 510 | 511 | BAD_ACTION: 512 | rspMsg = STATUS_BAD_ACTION; 513 | rspStatus = 500; 514 | goto done; 515 | 516 | BAD_ENTITY: 517 | rspMsg = STATUS_BAD_ENTITY; 518 | rspStatus = 422; 519 | goto done; 520 | 521 | ONE_MO_TIME: 522 | rspMsg = STATUS_TRY_AGAIN; 523 | rspStatus = 416; 524 | goto done; 525 | 526 | NOT_IMPLEMENTED: 527 | rspMsg = STATUS_NOT_IMPLEMENTED; 528 | rspStatus = 500; 529 | goto done; 530 | 531 | OK: 532 | rspMsg = STATUS_OK; 533 | rspStatus = 200; 534 | #pragma GCC diagnostic pop 535 | 536 | done: 537 | mg_send_head(conn, rspStatus, rspMsg.len, "Content-Type: text/plain"); 538 | mg_printf(conn, "%s", rspMsg.p); 539 | } 540 | 541 | //////////////////////////////////////////////////////////////////////////////// 542 | 543 | URCHTTP* 544 | URCHTTP::Get() 545 | { 546 | UObject *URCHTTP_cdo = URCHTTP::StaticClass()->GetDefaultObject(true); 547 | return Cast(URCHTTP_cdo); 548 | } 549 | 550 | 551 | //////////////////////////////////////////////////////////////////////////////// 552 | 553 | URCHTTP::URCHTTP(const FObjectInitializer& oi) 554 | : Super(oi), poll_interval(0), poll_ms(1) 555 | { 556 | // Initialize .pak file reader 557 | if (PakFileMgr == nullptr) 558 | { 559 | PakFileMgr = new FPakPlatformFile; 560 | PakFileMgr->Initialize(&FPlatformFileManager::Get().GetPlatformFile(), T("")); 561 | PakFileMgr->InitializeNewAsyncIO(); 562 | } 563 | } 564 | 565 | URCHTTP::~URCHTTP() 566 | { 567 | mg_mgr_free(&mgr); 568 | } 569 | 570 | //////////////////////////////////////////////////////////////////////////////// 571 | 572 | void 573 | URCHTTP::Init() 574 | { 575 | // Initialize HTTPD server 576 | mg_mgr_init(&mgr, NULL); 577 | conn = mg_bind(&mgr, "18820", ev_handler); 578 | mg_set_protocol_http_websocket(conn); 579 | } 580 | 581 | void 582 | URCHTTP::SetPollInterval(int v) 583 | { 584 | poll_interval = v; 585 | } 586 | 587 | void 588 | URCHTTP::Tick(float dt) 589 | { 590 | static int tick_counter = 0; 591 | if (tick_counter == 0) 592 | Init(); 593 | 594 | if (poll_interval == 0 || (tick_counter++ % poll_interval) == 0) 595 | mg_mgr_poll(&mgr, poll_ms); 596 | 597 | if (tick_counter == 0) 598 | tick_counter++; 599 | } 600 | 601 | TStatId 602 | URCHTTP::GetStatId() const 603 | { 604 | RETURN_QUICK_DECLARE_CYCLE_STAT(URCHTTP, STATGROUP_Tickables); 605 | } 606 | 607 | void 608 | URCHTTP::Serialize(FArchive& ar) 609 | { 610 | Super::Serialize(ar); 611 | } 612 | 613 | void 614 | URCHTTP::PostLoad() 615 | { 616 | Super::PostLoad(); 617 | } 618 | 619 | void 620 | URCHTTP::PostInitProperties() 621 | { 622 | Super::PostInitProperties(); 623 | } 624 | 625 | #if WITH_EDITOR 626 | void 627 | URCHTTP::PostEditChangeProperty(FPropertyChangedEvent& evt) 628 | { 629 | Super::PostEditChangeProperty(evt); 630 | } 631 | #endif 632 | 633 | //////////////////////////////////////////////////////////////////////////////// 634 | --------------------------------------------------------------------------------