From 8a4b9a3f06076734f836fa177d91062893bf259e Mon Sep 17 00:00:00 2001 From: jamestagg Date: Wed, 14 Jan 2026 16:35:00 -0600 Subject: [PATCH] Initial UE5 project with RalphaPlugin - UE5.7 project configured with LFS for binary assets - RalphaPlugin for MCP-based AI rendering control - Ready for content creation Co-Authored-By: Claude Opus 4.5 --- .gitattributes | 41 + .gitignore | 42 + Config/DefaultEditor.ini | 10 + Config/DefaultEngine.ini | 29 + Config/DefaultGame.ini | 8 + Config/DefaultInput.ini | 13 + Plugins/RalphaPlugin/Config/FilterPlugin.ini | 8 + Plugins/RalphaPlugin/RalphaPlugin.uplugin | 29 + Plugins/RalphaPlugin/Resources/Icon20.svg | 6 + .../Source/RalphaCore/Private/RalphaCore.cpp | 29 + .../RalphaCore/Private/RalphaMCPServer.cpp | 448 +++++++++ .../Private/RalphaParameterBridge.cpp | 881 ++++++++++++++++++ .../Private/RalphaScreenCapture.cpp | 166 ++++ .../Source/RalphaCore/Public/RalphaCore.h | 20 + .../RalphaCore/Public/RalphaMCPServer.h | 98 ++ .../RalphaCore/Public/RalphaParameterBridge.h | 80 ++ .../RalphaCore/Public/RalphaScreenCapture.h | 47 + .../Source/RalphaCore/RalphaCore.Build.cs | 59 ++ .../RalphaEditor/Private/RalphaEditor.cpp | 74 ++ .../Private/RalphaEditorCommands.cpp | 12 + .../Private/RalphaEditorStyle.cpp | 59 ++ .../Source/RalphaEditor/Public/RalphaEditor.h | 22 + .../Public/RalphaEditorCommands.h | 25 + .../RalphaEditor/Public/RalphaEditorStyle.h | 20 + .../Source/RalphaEditor/RalphaEditor.Build.cs | 52 ++ Ralpha.uproject | 16 + 26 files changed, 2294 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Config/DefaultEditor.ini create mode 100644 Config/DefaultEngine.ini create mode 100644 Config/DefaultGame.ini create mode 100644 Config/DefaultInput.ini create mode 100644 Plugins/RalphaPlugin/Config/FilterPlugin.ini create mode 100644 Plugins/RalphaPlugin/RalphaPlugin.uplugin create mode 100644 Plugins/RalphaPlugin/Resources/Icon20.svg create mode 100644 Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaCore.cpp create mode 100644 Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaMCPServer.cpp create mode 100644 Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaParameterBridge.cpp create mode 100644 Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaScreenCapture.cpp create mode 100644 Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaCore.h create mode 100644 Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaMCPServer.h create mode 100644 Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaParameterBridge.h create mode 100644 Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaScreenCapture.h create mode 100644 Plugins/RalphaPlugin/Source/RalphaCore/RalphaCore.Build.cs create mode 100644 Plugins/RalphaPlugin/Source/RalphaEditor/Private/RalphaEditor.cpp create mode 100644 Plugins/RalphaPlugin/Source/RalphaEditor/Private/RalphaEditorCommands.cpp create mode 100644 Plugins/RalphaPlugin/Source/RalphaEditor/Private/RalphaEditorStyle.cpp create mode 100644 Plugins/RalphaPlugin/Source/RalphaEditor/Public/RalphaEditor.h create mode 100644 Plugins/RalphaPlugin/Source/RalphaEditor/Public/RalphaEditorCommands.h create mode 100644 Plugins/RalphaPlugin/Source/RalphaEditor/Public/RalphaEditorStyle.h create mode 100644 Plugins/RalphaPlugin/Source/RalphaEditor/RalphaEditor.Build.cs create mode 100644 Ralpha.uproject diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..d2b070bc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,41 @@ +# UE5 LFS tracking + +# Unreal binary assets +*.uasset filter=lfs diff=lfs merge=lfs -text +*.umap filter=lfs diff=lfs merge=lfs -text + +# Textures +*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.tga filter=lfs diff=lfs merge=lfs -text +*.bmp filter=lfs diff=lfs merge=lfs -text +*.psd filter=lfs diff=lfs merge=lfs -text +*.exr filter=lfs diff=lfs merge=lfs -text +*.hdr filter=lfs diff=lfs merge=lfs -text + +# Audio +*.wav filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.ogg filter=lfs diff=lfs merge=lfs -text + +# Video +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.mov filter=lfs diff=lfs merge=lfs -text +*.avi filter=lfs diff=lfs merge=lfs -text + +# 3D formats +*.fbx filter=lfs diff=lfs merge=lfs -text +*.obj filter=lfs diff=lfs merge=lfs -text +*.gltf filter=lfs diff=lfs merge=lfs -text +*.glb filter=lfs diff=lfs merge=lfs -text +*.abc filter=lfs diff=lfs merge=lfs -text + +# Fonts +*.ttf filter=lfs diff=lfs merge=lfs -text +*.otf filter=lfs diff=lfs merge=lfs -text + +# Compiled/binary +*.dll filter=lfs diff=lfs merge=lfs -text +*.so filter=lfs diff=lfs merge=lfs -text +*.dylib filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a4371a9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# UE5 Project .gitignore + +# Build artifacts (rebuilt locally) +Binaries/ +Intermediate/ +DerivedDataCache/ +Build/ + +# Saved data (user-specific, logs, autosaves) +Saved/ + +# Plugin binaries (rebuilt per machine) +Plugins/**/Binaries/ +Plugins/**/Intermediate/ + +# Visual Studio +.vs/ +*.sln +*.suo +*.sdf +*.VC.db +*.VC.opendb + +# JetBrains Rider +.idea/ + +# VS Code +.vscode/ + +# macOS +.DS_Store +*.DS_Store + +# Windows +Thumbs.db +Desktop.ini + +# Cooked content (generated) +**/Cooked/ + +# Starter content (if using) +**/StarterContent/ diff --git a/Config/DefaultEditor.ini b/Config/DefaultEditor.ini new file mode 100644 index 00000000..77dd3321 --- /dev/null +++ b/Config/DefaultEditor.ini @@ -0,0 +1,10 @@ +[/Script/UnrealEd.EditorPerformanceSettings] +bMonitorEditorPerformance=True + +[/Script/UnrealEd.CrashReporterSettings] +bHideLogFilesOption=False +bHideRestartOption=False + +[ContentBrowser] +ShowEngineContent=False +ShowPluginContent=True diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini new file mode 100644 index 00000000..56cdf3e2 --- /dev/null +++ b/Config/DefaultEngine.ini @@ -0,0 +1,29 @@ +[/Script/EngineSettings.GameMapsSettings] +EditorStartupMap=/Game/Maps/Main +GameDefaultMap=/Game/Maps/Main + +[/Script/Engine.RendererSettings] +r.DefaultFeature.AutoExposure=True +r.DefaultFeature.MotionBlur=True +r.DefaultFeature.Bloom=True +r.DefaultFeature.AmbientOcclusion=True +r.DefaultFeature.AmbientOcclusionStaticFraction=True +r.Lumen.DiffuseIndirect.Allow=True +r.Lumen.Reflections.Allow=True +r.RayTracing=True + +[/Script/WindowsTargetPlatform.WindowsTargetSettings] +DefaultGraphicsRHI=DefaultGraphicsRHI_DX12 + +[/Script/Engine.Engine] ++ActiveGameNameRedirects=(OldGameName="TP_Blank",NewGameName="/Script/Ralpha") ++ActiveGameNameRedirects=(OldGameName="/Script/TP_Blank",NewGameName="/Script/Ralpha") + +[URL] +GameName=Ralpha + +[/Script/Engine.GarbageCollectionSettings] +gc.MaxObjectsNotConsideredByGC=1 + +[ConsoleVariables] +r.Lumen.ScreenProbeGather.FullResolutionJitterWidth=1 diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini new file mode 100644 index 00000000..105116c4 --- /dev/null +++ b/Config/DefaultGame.ini @@ -0,0 +1,8 @@ +[/Script/EngineSettings.GeneralProjectSettings] +ProjectID=Ralpha2026 +ProjectName=Ralpha +CompanyName=Ralpha +CopyrightNotice=Copyright Ralpha Team +Description=AI-driven style transfer for infinite world generation +bStartInVR=False +bStartInAR=False diff --git a/Config/DefaultInput.ini b/Config/DefaultInput.ini new file mode 100644 index 00000000..af79b5ba --- /dev/null +++ b/Config/DefaultInput.ini @@ -0,0 +1,13 @@ +[/Script/Engine.InputSettings] +-AxisConfig=(AxisKeyName="Gamepad_LeftX",AxisProperties=(DeadZone=0.25,Sensitivity=1.f,Exponent=1.f,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_LeftX",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +-AxisConfig=(AxisKeyName="Gamepad_LeftY",AxisProperties=(DeadZone=0.25,Sensitivity=1.f,Exponent=1.f,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_LeftY",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +-AxisConfig=(AxisKeyName="Gamepad_RightX",AxisProperties=(DeadZone=0.25,Sensitivity=1.f,Exponent=1.f,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightX",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +-AxisConfig=(AxisKeyName="Gamepad_RightY",AxisProperties=(DeadZone=0.25,Sensitivity=1.f,Exponent=1.f,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightY",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +bCaptureMouseOnLaunch=True +DefaultViewportMouseCaptureMode=CapturePermanently_IncludingInitialMouseDown +bDefaultViewportMouseLock=False +DefaultViewportMouseLockMode=DoNotLock diff --git a/Plugins/RalphaPlugin/Config/FilterPlugin.ini b/Plugins/RalphaPlugin/Config/FilterPlugin.ini new file mode 100644 index 00000000..ccebca2f --- /dev/null +++ b/Plugins/RalphaPlugin/Config/FilterPlugin.ini @@ -0,0 +1,8 @@ +[FilterPlugin] +; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and +; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. +; +; Examples: +; /README.txt +; /Extras/... +; /Binaries/ThirdParty/*.dll diff --git a/Plugins/RalphaPlugin/RalphaPlugin.uplugin b/Plugins/RalphaPlugin/RalphaPlugin.uplugin new file mode 100644 index 00000000..e43c477c --- /dev/null +++ b/Plugins/RalphaPlugin/RalphaPlugin.uplugin @@ -0,0 +1,29 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "Ralpha - AI Style Transfer", + "Description": "AI-driven style transfer system for Unreal Engine 5. Enables Claude to control rendering parameters via MCP.", + "Category": "Rendering", + "CreatedBy": "Ralpha Team", + "CreatedByURL": "", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "CanContainContent": true, + "IsBetaVersion": true, + "IsExperimentalVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "RalphaCore", + "Type": "Runtime", + "LoadingPhase": "Default" + }, + { + "Name": "RalphaEditor", + "Type": "Editor", + "LoadingPhase": "PostEngineInit" + } + ] +} diff --git a/Plugins/RalphaPlugin/Resources/Icon20.svg b/Plugins/RalphaPlugin/Resources/Icon20.svg new file mode 100644 index 00000000..820aba8f --- /dev/null +++ b/Plugins/RalphaPlugin/Resources/Icon20.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaCore.cpp b/Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaCore.cpp new file mode 100644 index 00000000..f484794e --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaCore.cpp @@ -0,0 +1,29 @@ +// Copyright Ralpha Team. All Rights Reserved. + +#include "RalphaCore.h" + +#define LOCTEXT_NAMESPACE "FRalphaCoreModule" + +void FRalphaCoreModule::StartupModule() +{ + UE_LOG(LogTemp, Log, TEXT("Ralpha Core Module Started")); +} + +void FRalphaCoreModule::ShutdownModule() +{ + UE_LOG(LogTemp, Log, TEXT("Ralpha Core Module Shutdown")); +} + +FRalphaCoreModule& FRalphaCoreModule::Get() +{ + return FModuleManager::LoadModuleChecked("RalphaCore"); +} + +bool FRalphaCoreModule::IsAvailable() +{ + return FModuleManager::Get().IsModuleLoaded("RalphaCore"); +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FRalphaCoreModule, RalphaCore) diff --git a/Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaMCPServer.cpp b/Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaMCPServer.cpp new file mode 100644 index 00000000..288e387b --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaMCPServer.cpp @@ -0,0 +1,448 @@ +// Copyright Ralpha Team. All Rights Reserved. + +#include "RalphaMCPServer.h" +#include "RalphaParameterBridge.h" +#include "RalphaScreenCapture.h" +#include "Dom/JsonObject.h" +#include "Dom/JsonValue.h" +#include "Serialization/JsonSerializer.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Misc/Base64.h" +#include "Engine/World.h" +#include "Engine/Engine.h" +#include "Kismet/GameplayStatics.h" + +DEFINE_LOG_CATEGORY(LogRalphaMCP); + +URalphaMCPServer* URalphaMCPServer::Instance = nullptr; + +URalphaMCPServer::URalphaMCPServer() + : ListenerSocket(nullptr) + , ServerPort(30010) + , bIsRunning(false) + , ParameterBridge(nullptr) + , ScreenCapture(nullptr) +{ +} + +URalphaMCPServer::~URalphaMCPServer() +{ + Stop(); +} + +URalphaMCPServer* URalphaMCPServer::Get() +{ + if (!Instance) + { + Instance = NewObject(); + Instance->AddToRoot(); // Prevent garbage collection + } + return Instance; +} + +bool URalphaMCPServer::Start(int32 Port) +{ + if (bIsRunning) + { + UE_LOG(LogRalphaMCP, Warning, TEXT("Server already running on port %d"), ServerPort); + return true; + } + + ServerPort = Port; + + // Create socket + ISocketSubsystem* SocketSubsystem = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM); + if (!SocketSubsystem) + { + UE_LOG(LogRalphaMCP, Error, TEXT("Failed to get socket subsystem")); + return false; + } + + ListenerSocket = SocketSubsystem->CreateSocket(NAME_Stream, TEXT("RalphaMCPServer"), false); + if (!ListenerSocket) + { + UE_LOG(LogRalphaMCP, Error, TEXT("Failed to create socket")); + return false; + } + + // Configure socket + ListenerSocket->SetReuseAddr(true); + ListenerSocket->SetNonBlocking(true); + + // Bind to port + TSharedRef Addr = SocketSubsystem->CreateInternetAddr(); + Addr->SetAnyAddress(); + Addr->SetPort(ServerPort); + + if (!ListenerSocket->Bind(*Addr)) + { + UE_LOG(LogRalphaMCP, Error, TEXT("Failed to bind to port %d"), ServerPort); + SocketSubsystem->DestroySocket(ListenerSocket); + ListenerSocket = nullptr; + return false; + } + + // Start listening + if (!ListenerSocket->Listen(8)) + { + UE_LOG(LogRalphaMCP, Error, TEXT("Failed to listen on port %d"), ServerPort); + SocketSubsystem->DestroySocket(ListenerSocket); + ListenerSocket = nullptr; + return false; + } + + // Create parameter bridge and screen capture + ParameterBridge = NewObject(this); + ScreenCapture = NewObject(this); + + // Register tick + TickDelegateHandle = FTSTicker::GetCoreTicker().AddTicker( + FTickerDelegate::CreateUObject(this, &URalphaMCPServer::Tick), 0.0f); + + bIsRunning = true; + UE_LOG(LogRalphaMCP, Log, TEXT("Ralpha MCP Server started on port %d"), ServerPort); + + return true; +} + +void URalphaMCPServer::Stop() +{ + if (!bIsRunning) + { + return; + } + + // Unregister tick + FTSTicker::GetCoreTicker().RemoveTicker(TickDelegateHandle); + + // Close all client connections + ClientConnections.Empty(); + + // Close listener socket + if (ListenerSocket) + { + ListenerSocket->Close(); + ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(ListenerSocket); + ListenerSocket = nullptr; + } + + bIsRunning = false; + UE_LOG(LogRalphaMCP, Log, TEXT("Ralpha MCP Server stopped")); +} + +bool URalphaMCPServer::IsRunning() const +{ + return bIsRunning; +} + +bool URalphaMCPServer::Tick(float DeltaTime) +{ + if (!bIsRunning || !ListenerSocket) + { + return true; + } + + // Check for new connections + bool bHasPendingConnection = false; + if (ListenerSocket->HasPendingConnection(bHasPendingConnection) && bHasPendingConnection) + { + TSharedRef RemoteAddress = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); + FSocket* ClientSocket = ListenerSocket->Accept(*RemoteAddress, TEXT("RalphaClient")); + + if (ClientSocket) + { + ClientSocket->SetNonBlocking(true); + + TSharedPtr NewConnection = MakeShared(ClientSocket, this); + ClientConnections.Add(NewConnection); + + UE_LOG(LogRalphaMCP, Log, TEXT("New client connected from %s"), *RemoteAddress->ToString(true)); + } + } + + // Process existing connections + for (int32 i = ClientConnections.Num() - 1; i >= 0; --i) + { + if (ClientConnections[i]->IsValid()) + { + ClientConnections[i]->Tick(); + } + else + { + ClientConnections.RemoveAt(i); + } + } + + return true; +} + +FString URalphaMCPServer::ProcessCommand(const FString& JsonCommand) +{ + TSharedPtr JsonObject; + TSharedRef> Reader = TJsonReaderFactory<>::Create(JsonCommand); + + if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid()) + { + return TEXT("{\"success\": false, \"error\": \"Invalid JSON\"}"); + } + + FString CommandType; + if (!JsonObject->TryGetStringField(TEXT("type"), CommandType)) + { + return TEXT("{\"success\": false, \"error\": \"Missing command type\"}"); + } + + TSharedPtr ResponseObject = MakeShared(); + + // Route commands + if (CommandType == TEXT("capture_screenshot")) + { + int32 Width = JsonObject->HasField(TEXT("width")) ? JsonObject->GetIntegerField(TEXT("width")) : 1920; + int32 Height = JsonObject->HasField(TEXT("height")) ? JsonObject->GetIntegerField(TEXT("height")) : 1080; + + FString Base64Image; + FString FilePath; + bool bSuccess = ScreenCapture->CaptureScreenshot(Width, Height, Base64Image, FilePath); + + ResponseObject->SetBoolField(TEXT("success"), bSuccess); + if (bSuccess) + { + ResponseObject->SetStringField(TEXT("base64"), Base64Image); + ResponseObject->SetStringField(TEXT("path"), FilePath); + } + } + else if (CommandType == TEXT("get_all_parameters")) + { + TSharedPtr ParamsObject = ParameterBridge->GetAllParameters(); + ResponseObject->SetBoolField(TEXT("success"), true); + ResponseObject->SetObjectField(TEXT("data"), ParamsObject); + } + else if (CommandType == TEXT("set_post_process")) + { + const TSharedPtr* Params; + if (JsonObject->TryGetObjectField(TEXT("parameters"), Params)) + { + bool bSuccess = ParameterBridge->SetPostProcessParameters(*Params); + ResponseObject->SetBoolField(TEXT("success"), bSuccess); + } + else + { + ResponseObject->SetBoolField(TEXT("success"), false); + ResponseObject->SetStringField(TEXT("error"), TEXT("Missing parameters")); + } + } + else if (CommandType == TEXT("set_directional_light")) + { + const TSharedPtr* Params; + if (JsonObject->TryGetObjectField(TEXT("parameters"), Params)) + { + bool bSuccess = ParameterBridge->SetDirectionalLightParameters(*Params); + ResponseObject->SetBoolField(TEXT("success"), bSuccess); + } + else + { + ResponseObject->SetBoolField(TEXT("success"), false); + ResponseObject->SetStringField(TEXT("error"), TEXT("Missing parameters")); + } + } + else if (CommandType == TEXT("set_sky_light")) + { + const TSharedPtr* Params; + if (JsonObject->TryGetObjectField(TEXT("parameters"), Params)) + { + bool bSuccess = ParameterBridge->SetSkyLightParameters(*Params); + ResponseObject->SetBoolField(TEXT("success"), bSuccess); + } + else + { + ResponseObject->SetBoolField(TEXT("success"), false); + ResponseObject->SetStringField(TEXT("error"), TEXT("Missing parameters")); + } + } + else if (CommandType == TEXT("set_exponential_height_fog")) + { + const TSharedPtr* Params; + if (JsonObject->TryGetObjectField(TEXT("parameters"), Params)) + { + bool bSuccess = ParameterBridge->SetFogParameters(*Params); + ResponseObject->SetBoolField(TEXT("success"), bSuccess); + } + else + { + ResponseObject->SetBoolField(TEXT("success"), false); + ResponseObject->SetStringField(TEXT("error"), TEXT("Missing parameters")); + } + } + else if (CommandType == TEXT("set_camera")) + { + const TSharedPtr* Params; + if (JsonObject->TryGetObjectField(TEXT("parameters"), Params)) + { + bool bSuccess = ParameterBridge->SetCameraParameters(*Params); + ResponseObject->SetBoolField(TEXT("success"), bSuccess); + } + else + { + ResponseObject->SetBoolField(TEXT("success"), false); + ResponseObject->SetStringField(TEXT("error"), TEXT("Missing parameters")); + } + } + else if (CommandType == TEXT("search_assets")) + { + FString Query = JsonObject->GetStringField(TEXT("query")); + FString AssetType = JsonObject->HasField(TEXT("asset_type")) ? JsonObject->GetStringField(TEXT("asset_type")) : TEXT("All"); + int32 MaxResults = JsonObject->HasField(TEXT("max_results")) ? JsonObject->GetIntegerField(TEXT("max_results")) : 20; + + TArray> Results = ParameterBridge->SearchAssets(Query, AssetType, MaxResults); + ResponseObject->SetBoolField(TEXT("success"), true); + ResponseObject->SetArrayField(TEXT("results"), Results); + } + else if (CommandType == TEXT("spawn_actor")) + { + FString AssetPath = JsonObject->GetStringField(TEXT("asset_path")); + + const TSharedPtr* LocationObj; + FVector Location = FVector::ZeroVector; + if (JsonObject->TryGetObjectField(TEXT("location"), LocationObj)) + { + Location.X = (*LocationObj)->GetNumberField(TEXT("x")); + Location.Y = (*LocationObj)->GetNumberField(TEXT("y")); + Location.Z = (*LocationObj)->GetNumberField(TEXT("z")); + } + + FRotator Rotation = FRotator::ZeroRotator; + const TSharedPtr* RotationObj; + if (JsonObject->TryGetObjectField(TEXT("rotation"), RotationObj)) + { + Rotation.Pitch = (*RotationObj)->HasField(TEXT("pitch")) ? (*RotationObj)->GetNumberField(TEXT("pitch")) : 0.0f; + Rotation.Yaw = (*RotationObj)->HasField(TEXT("yaw")) ? (*RotationObj)->GetNumberField(TEXT("yaw")) : 0.0f; + Rotation.Roll = (*RotationObj)->HasField(TEXT("roll")) ? (*RotationObj)->GetNumberField(TEXT("roll")) : 0.0f; + } + + float Scale = JsonObject->HasField(TEXT("scale")) ? JsonObject->GetNumberField(TEXT("scale")) : 1.0f; + FString ActorLabel = JsonObject->HasField(TEXT("actor_label")) ? JsonObject->GetStringField(TEXT("actor_label")) : TEXT(""); + + FString ActorId; + bool bSuccess = ParameterBridge->SpawnActor(AssetPath, Location, Rotation, Scale, ActorLabel, ActorId); + ResponseObject->SetBoolField(TEXT("success"), bSuccess); + if (bSuccess) + { + ResponseObject->SetStringField(TEXT("actor_id"), ActorId); + } + } + else if (CommandType == TEXT("delete_actor")) + { + FString ActorId = JsonObject->GetStringField(TEXT("actor_id")); + bool bSuccess = ParameterBridge->DeleteActor(ActorId); + ResponseObject->SetBoolField(TEXT("success"), bSuccess); + } + else if (CommandType == TEXT("list_actors")) + { + FString ClassFilter = JsonObject->HasField(TEXT("class_filter")) ? JsonObject->GetStringField(TEXT("class_filter")) : TEXT(""); + FString NameFilter = JsonObject->HasField(TEXT("name_filter")) ? JsonObject->GetStringField(TEXT("name_filter")) : TEXT(""); + bool bIncludeTransforms = JsonObject->HasField(TEXT("include_transforms")) ? JsonObject->GetBoolField(TEXT("include_transforms")) : true; + + TArray> Actors = ParameterBridge->ListActors(ClassFilter, NameFilter, bIncludeTransforms); + ResponseObject->SetBoolField(TEXT("success"), true); + ResponseObject->SetArrayField(TEXT("actors"), Actors); + } + else if (CommandType == TEXT("set_render_quality")) + { + const TSharedPtr* Settings; + if (JsonObject->TryGetObjectField(TEXT("settings"), Settings)) + { + bool bSuccess = ParameterBridge->SetRenderQuality(*Settings); + ResponseObject->SetBoolField(TEXT("success"), bSuccess); + } + else + { + ResponseObject->SetBoolField(TEXT("success"), false); + ResponseObject->SetStringField(TEXT("error"), TEXT("Missing settings")); + } + } + else + { + ResponseObject->SetBoolField(TEXT("success"), false); + ResponseObject->SetStringField(TEXT("error"), FString::Printf(TEXT("Unknown command type: %s"), *CommandType)); + } + + // Serialize response + FString ResponseString; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&ResponseString); + FJsonSerializer::Serialize(ResponseObject.ToSharedRef(), Writer); + + return ResponseString; +} + +// FRalphaClientConnection implementation + +FRalphaClientConnection::FRalphaClientConnection(FSocket* InSocket, URalphaMCPServer* InServer) + : Socket(InSocket) + , Server(InServer) +{ +} + +FRalphaClientConnection::~FRalphaClientConnection() +{ + if (Socket) + { + Socket->Close(); + ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(Socket); + Socket = nullptr; + } +} + +bool FRalphaClientConnection::IsValid() const +{ + return Socket != nullptr && Socket->GetConnectionState() == SCS_Connected; +} + +void FRalphaClientConnection::Tick() +{ + if (!IsValid()) + { + return; + } + + // Receive data + uint32 PendingDataSize = 0; + while (Socket->HasPendingData(PendingDataSize) && PendingDataSize > 0) + { + TArray ReceivedData; + ReceivedData.SetNumUninitialized(FMath::Min(PendingDataSize, 65536u)); + + int32 BytesRead = 0; + if (Socket->Recv(ReceivedData.GetData(), ReceivedData.Num(), BytesRead)) + { + ReceiveBuffer += FString(UTF8_TO_TCHAR(reinterpret_cast(ReceivedData.GetData()))); + } + } + + // Process complete messages (newline-delimited JSON) + int32 NewlineIndex; + while (ReceiveBuffer.FindChar(TEXT('\n'), NewlineIndex)) + { + FString Message = ReceiveBuffer.Left(NewlineIndex); + ReceiveBuffer = ReceiveBuffer.Mid(NewlineIndex + 1); + + if (!Message.IsEmpty()) + { + FString Response = Server->ProcessCommand(Message); + SendResponse(Response); + } + } +} + +void FRalphaClientConnection::SendResponse(const FString& Response) +{ + if (!IsValid()) + { + return; + } + + FString ResponseWithNewline = Response + TEXT("\n"); + FTCHARToUTF8 Converter(*ResponseWithNewline); + int32 BytesSent = 0; + Socket->Send(reinterpret_cast(Converter.Get()), Converter.Length(), BytesSent); +} diff --git a/Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaParameterBridge.cpp b/Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaParameterBridge.cpp new file mode 100644 index 00000000..0228cbee --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaParameterBridge.cpp @@ -0,0 +1,881 @@ +// Copyright Ralpha Team. All Rights Reserved. + +#include "RalphaParameterBridge.h" +#include "RalphaMCPServer.h" +#include "Engine/Engine.h" +#include "Engine/World.h" +#include "Engine/PostProcessVolume.h" +#include "Engine/DirectionalLight.h" +#include "Engine/SkyLight.h" +#include "Engine/ExponentialHeightFog.h" +#include "CineCameraActor.h" +#include "CineCameraComponent.h" +#include "Components/PostProcessComponent.h" +#include "Components/DirectionalLightComponent.h" +#include "Components/SkyLightComponent.h" +#include "Components/ExponentialHeightFogComponent.h" +#include "Kismet/GameplayStatics.h" +#include "EngineUtils.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Engine/StaticMeshActor.h" +#include "Engine/StaticMesh.h" +#include "Dom/JsonObject.h" +#include "Dom/JsonValue.h" +#include "Serialization/JsonSerializer.h" + +URalphaParameterBridge::URalphaParameterBridge() +{ +} + +UWorld* URalphaParameterBridge::GetCurrentWorld() +{ + if (GEngine && GEngine->GetWorldContexts().Num() > 0) + { + for (const FWorldContext& Context : GEngine->GetWorldContexts()) + { + if (Context.WorldType == EWorldType::Editor || Context.WorldType == EWorldType::PIE) + { + return Context.World(); + } + } + } + return nullptr; +} + +APostProcessVolume* URalphaParameterBridge::FindPostProcessVolume() +{ + UWorld* World = GetCurrentWorld(); + if (!World) return nullptr; + + for (TActorIterator It(World); It; ++It) + { + if (It->bUnbound) // Prefer unbound (global) volumes + { + return *It; + } + } + + // Return any post-process volume if no unbound one found + for (TActorIterator It(World); It; ++It) + { + return *It; + } + + return nullptr; +} + +ADirectionalLight* URalphaParameterBridge::FindDirectionalLight() +{ + UWorld* World = GetCurrentWorld(); + if (!World) return nullptr; + + for (TActorIterator It(World); It; ++It) + { + return *It; + } + return nullptr; +} + +ASkyLight* URalphaParameterBridge::FindSkyLight() +{ + UWorld* World = GetCurrentWorld(); + if (!World) return nullptr; + + for (TActorIterator It(World); It; ++It) + { + return *It; + } + return nullptr; +} + +AExponentialHeightFog* URalphaParameterBridge::FindExponentialHeightFog() +{ + UWorld* World = GetCurrentWorld(); + if (!World) return nullptr; + + for (TActorIterator It(World); It; ++It) + { + return *It; + } + return nullptr; +} + +ACineCameraActor* URalphaParameterBridge::FindCineCamera() +{ + UWorld* World = GetCurrentWorld(); + if (!World) return nullptr; + + for (TActorIterator It(World); It; ++It) + { + return *It; + } + return nullptr; +} + +FLinearColor URalphaParameterBridge::ParseHexColor(const FString& HexColor) +{ + FString CleanHex = HexColor; + CleanHex.RemoveFromStart(TEXT("#")); + + FColor Color = FColor::FromHex(CleanHex); + return FLinearColor(Color); +} + +FString URalphaParameterBridge::ColorToHex(const FLinearColor& Color) +{ + FColor SRGBColor = Color.ToFColor(true); + return FString::Printf(TEXT("#%02X%02X%02X"), SRGBColor.R, SRGBColor.G, SRGBColor.B); +} + +TSharedPtr URalphaParameterBridge::GetAllParameters() +{ + TSharedPtr Result = MakeShared(); + + Result->SetObjectField(TEXT("post_process"), GetPostProcessParameters()); + Result->SetObjectField(TEXT("directional_light"), GetDirectionalLightParameters()); + Result->SetObjectField(TEXT("sky_light"), GetSkyLightParameters()); + Result->SetObjectField(TEXT("fog"), GetFogParameters()); + Result->SetObjectField(TEXT("camera"), GetCameraParameters()); + + return Result; +} + +// ============================================================================ +// Post-Process Volume +// ============================================================================ + +bool URalphaParameterBridge::SetPostProcessParameters(const TSharedPtr& Params) +{ + APostProcessVolume* PPV = FindPostProcessVolume(); + if (!PPV) + { + UE_LOG(LogRalphaMCP, Warning, TEXT("No PostProcessVolume found in scene")); + return false; + } + + FPostProcessSettings& Settings = PPV->Settings; + + // Exposure + if (Params->HasField(TEXT("exposure_compensation"))) + { + Settings.bOverride_AutoExposureBias = true; + Settings.AutoExposureBias = Params->GetNumberField(TEXT("exposure_compensation")); + } + + // Color Grading + if (Params->HasField(TEXT("color_saturation"))) + { + Settings.bOverride_ColorSaturation = true; + float Sat = Params->GetNumberField(TEXT("color_saturation")); + Settings.ColorSaturation = FVector4(Sat, Sat, Sat, 1.0f); + } + + if (Params->HasField(TEXT("color_contrast"))) + { + Settings.bOverride_ColorContrast = true; + float Contrast = Params->GetNumberField(TEXT("color_contrast")); + Settings.ColorContrast = FVector4(Contrast, Contrast, Contrast, 1.0f); + } + + // White Balance + if (Params->HasField(TEXT("white_balance_temp"))) + { + Settings.bOverride_WhiteTemp = true; + Settings.WhiteTemp = Params->GetNumberField(TEXT("white_balance_temp")); + } + + if (Params->HasField(TEXT("white_balance_tint"))) + { + Settings.bOverride_WhiteTint = true; + Settings.WhiteTint = Params->GetNumberField(TEXT("white_balance_tint")); + } + + // Bloom + if (Params->HasField(TEXT("bloom_intensity"))) + { + Settings.bOverride_BloomIntensity = true; + Settings.BloomIntensity = Params->GetNumberField(TEXT("bloom_intensity")); + } + + if (Params->HasField(TEXT("bloom_threshold"))) + { + Settings.bOverride_BloomThreshold = true; + Settings.BloomThreshold = Params->GetNumberField(TEXT("bloom_threshold")); + } + + // Vignette + if (Params->HasField(TEXT("vignette_intensity"))) + { + Settings.bOverride_VignetteIntensity = true; + Settings.VignetteIntensity = Params->GetNumberField(TEXT("vignette_intensity")); + } + + // Film Grain + if (Params->HasField(TEXT("film_grain_intensity"))) + { + Settings.bOverride_FilmGrainIntensity = true; + Settings.FilmGrainIntensity = Params->GetNumberField(TEXT("film_grain_intensity")); + } + + // Chromatic Aberration + if (Params->HasField(TEXT("chromatic_aberration_intensity"))) + { + Settings.bOverride_SceneFringeIntensity = true; + Settings.SceneFringeIntensity = Params->GetNumberField(TEXT("chromatic_aberration_intensity")); + } + + PPV->MarkPackageDirty(); + return true; +} + +TSharedPtr URalphaParameterBridge::GetPostProcessParameters() +{ + TSharedPtr Result = MakeShared(); + + APostProcessVolume* PPV = FindPostProcessVolume(); + if (!PPV) + { + return Result; + } + + const FPostProcessSettings& Settings = PPV->Settings; + + Result->SetNumberField(TEXT("exposure_compensation"), Settings.AutoExposureBias); + Result->SetNumberField(TEXT("color_saturation"), Settings.ColorSaturation.X); + Result->SetNumberField(TEXT("color_contrast"), Settings.ColorContrast.X); + Result->SetNumberField(TEXT("white_balance_temp"), Settings.WhiteTemp); + Result->SetNumberField(TEXT("white_balance_tint"), Settings.WhiteTint); + Result->SetNumberField(TEXT("bloom_intensity"), Settings.BloomIntensity); + Result->SetNumberField(TEXT("bloom_threshold"), Settings.BloomThreshold); + Result->SetNumberField(TEXT("vignette_intensity"), Settings.VignetteIntensity); + Result->SetNumberField(TEXT("film_grain_intensity"), Settings.FilmGrainIntensity); + Result->SetNumberField(TEXT("chromatic_aberration_intensity"), Settings.SceneFringeIntensity); + + return Result; +} + +// ============================================================================ +// Directional Light +// ============================================================================ + +bool URalphaParameterBridge::SetDirectionalLightParameters(const TSharedPtr& Params) +{ + ADirectionalLight* Light = FindDirectionalLight(); + if (!Light) + { + UE_LOG(LogRalphaMCP, Warning, TEXT("No DirectionalLight found in scene")); + return false; + } + + UDirectionalLightComponent* LightComp = Cast(Light->GetLightComponent()); + if (!LightComp) return false; + + if (Params->HasField(TEXT("intensity"))) + { + LightComp->SetIntensity(Params->GetNumberField(TEXT("intensity"))); + } + + if (Params->HasField(TEXT("color"))) + { + FLinearColor Color = ParseHexColor(Params->GetStringField(TEXT("color"))); + LightComp->SetLightColor(Color); + } + + if (Params->HasField(TEXT("temperature"))) + { + LightComp->bUseTemperature = true; + LightComp->SetTemperature(Params->GetNumberField(TEXT("temperature"))); + } + + if (Params->HasField(TEXT("pitch")) || Params->HasField(TEXT("yaw"))) + { + FRotator CurrentRot = Light->GetActorRotation(); + if (Params->HasField(TEXT("pitch"))) + { + CurrentRot.Pitch = Params->GetNumberField(TEXT("pitch")); + } + if (Params->HasField(TEXT("yaw"))) + { + CurrentRot.Yaw = Params->GetNumberField(TEXT("yaw")); + } + Light->SetActorRotation(CurrentRot); + } + + if (Params->HasField(TEXT("source_angle"))) + { + LightComp->LightSourceAngle = Params->GetNumberField(TEXT("source_angle")); + } + + if (Params->HasField(TEXT("source_soft_angle"))) + { + LightComp->LightSourceSoftAngle = Params->GetNumberField(TEXT("source_soft_angle")); + } + + LightComp->MarkRenderStateDirty(); + return true; +} + +TSharedPtr URalphaParameterBridge::GetDirectionalLightParameters() +{ + TSharedPtr Result = MakeShared(); + + ADirectionalLight* Light = FindDirectionalLight(); + if (!Light) return Result; + + UDirectionalLightComponent* LightComp = Cast(Light->GetLightComponent()); + if (!LightComp) return Result; + + Result->SetNumberField(TEXT("intensity"), LightComp->Intensity); + Result->SetStringField(TEXT("color"), ColorToHex(LightComp->GetLightColor())); + Result->SetNumberField(TEXT("temperature"), LightComp->Temperature); + Result->SetNumberField(TEXT("pitch"), Light->GetActorRotation().Pitch); + Result->SetNumberField(TEXT("yaw"), Light->GetActorRotation().Yaw); + Result->SetNumberField(TEXT("source_angle"), LightComp->LightSourceAngle); + Result->SetNumberField(TEXT("source_soft_angle"), LightComp->LightSourceSoftAngle); + + return Result; +} + +// ============================================================================ +// Sky Light +// ============================================================================ + +bool URalphaParameterBridge::SetSkyLightParameters(const TSharedPtr& Params) +{ + ASkyLight* Light = FindSkyLight(); + if (!Light) + { + UE_LOG(LogRalphaMCP, Warning, TEXT("No SkyLight found in scene")); + return false; + } + + USkyLightComponent* LightComp = Light->GetLightComponent(); + if (!LightComp) return false; + + if (Params->HasField(TEXT("intensity"))) + { + LightComp->SetIntensity(Params->GetNumberField(TEXT("intensity"))); + } + + if (Params->HasField(TEXT("color"))) + { + FLinearColor Color = ParseHexColor(Params->GetStringField(TEXT("color"))); + LightComp->SetLightColor(Color); + } + + LightComp->MarkRenderStateDirty(); + return true; +} + +TSharedPtr URalphaParameterBridge::GetSkyLightParameters() +{ + TSharedPtr Result = MakeShared(); + + ASkyLight* Light = FindSkyLight(); + if (!Light) return Result; + + USkyLightComponent* LightComp = Light->GetLightComponent(); + if (!LightComp) return Result; + + Result->SetNumberField(TEXT("intensity"), LightComp->Intensity); + Result->SetStringField(TEXT("color"), ColorToHex(LightComp->GetLightColor())); + + return Result; +} + +// ============================================================================ +// Exponential Height Fog +// ============================================================================ + +bool URalphaParameterBridge::SetFogParameters(const TSharedPtr& Params) +{ + AExponentialHeightFog* Fog = FindExponentialHeightFog(); + if (!Fog) + { + UE_LOG(LogRalphaMCP, Warning, TEXT("No ExponentialHeightFog found in scene")); + return false; + } + + UExponentialHeightFogComponent* FogComp = Fog->GetComponent(); + if (!FogComp) return false; + + if (Params->HasField(TEXT("enabled"))) + { + FogComp->SetVisibility(Params->GetBoolField(TEXT("enabled"))); + } + + if (Params->HasField(TEXT("fog_density"))) + { + FogComp->SetFogDensity(Params->GetNumberField(TEXT("fog_density"))); + } + + if (Params->HasField(TEXT("fog_height_falloff"))) + { + FogComp->SetFogHeightFalloff(Params->GetNumberField(TEXT("fog_height_falloff"))); + } + + if (Params->HasField(TEXT("fog_inscattering_color"))) + { + FLinearColor Color = ParseHexColor(Params->GetStringField(TEXT("fog_inscattering_color"))); + FogComp->SetFogInscatteringColor(Color); + } + + if (Params->HasField(TEXT("fog_max_opacity"))) + { + FogComp->SetFogMaxOpacity(Params->GetNumberField(TEXT("fog_max_opacity"))); + } + + if (Params->HasField(TEXT("start_distance"))) + { + FogComp->SetStartDistance(Params->GetNumberField(TEXT("start_distance"))); + } + + if (Params->HasField(TEXT("volumetric_fog"))) + { + FogComp->SetVolumetricFog(Params->GetBoolField(TEXT("volumetric_fog"))); + } + + if (Params->HasField(TEXT("volumetric_fog_scattering_distribution"))) + { + FogComp->VolumetricFogScatteringDistribution = Params->GetNumberField(TEXT("volumetric_fog_scattering_distribution")); + } + + FogComp->MarkRenderStateDirty(); + return true; +} + +TSharedPtr URalphaParameterBridge::GetFogParameters() +{ + TSharedPtr Result = MakeShared(); + + AExponentialHeightFog* Fog = FindExponentialHeightFog(); + if (!Fog) return Result; + + UExponentialHeightFogComponent* FogComp = Fog->GetComponent(); + if (!FogComp) return Result; + + Result->SetBoolField(TEXT("enabled"), FogComp->IsVisible()); + Result->SetNumberField(TEXT("fog_density"), FogComp->FogDensity); + Result->SetNumberField(TEXT("fog_height_falloff"), FogComp->FogHeightFalloff); + Result->SetNumberField(TEXT("fog_max_opacity"), FogComp->FogMaxOpacity); + Result->SetNumberField(TEXT("start_distance"), FogComp->StartDistance); + Result->SetBoolField(TEXT("volumetric_fog"), FogComp->bEnableVolumetricFog); + Result->SetNumberField(TEXT("volumetric_fog_scattering_distribution"), FogComp->VolumetricFogScatteringDistribution); + + return Result; +} + +// ============================================================================ +// Camera +// ============================================================================ + +bool URalphaParameterBridge::SetCameraParameters(const TSharedPtr& Params) +{ + ACineCameraActor* Camera = FindCineCamera(); + if (!Camera) + { + UE_LOG(LogRalphaMCP, Warning, TEXT("No CineCameraActor found in scene")); + return false; + } + + UCineCameraComponent* CameraComp = Camera->GetCineCameraComponent(); + if (!CameraComp) return false; + + if (Params->HasField(TEXT("focal_length"))) + { + CameraComp->SetCurrentFocalLength(Params->GetNumberField(TEXT("focal_length"))); + } + + if (Params->HasField(TEXT("aperture"))) + { + CameraComp->CurrentAperture = Params->GetNumberField(TEXT("aperture")); + } + + if (Params->HasField(TEXT("focus_distance"))) + { + CameraComp->FocusSettings.ManualFocusDistance = Params->GetNumberField(TEXT("focus_distance")); + } + + if (Params->HasField(TEXT("dof_enabled"))) + { + CameraComp->PostProcessSettings.bOverride_DepthOfFieldFstop = Params->GetBoolField(TEXT("dof_enabled")); + } + + if (Params->HasField(TEXT("motion_blur_amount"))) + { + CameraComp->PostProcessSettings.bOverride_MotionBlurAmount = true; + CameraComp->PostProcessSettings.MotionBlurAmount = Params->GetNumberField(TEXT("motion_blur_amount")); + } + + CameraComp->MarkRenderStateDirty(); + return true; +} + +TSharedPtr URalphaParameterBridge::GetCameraParameters() +{ + TSharedPtr Result = MakeShared(); + + ACineCameraActor* Camera = FindCineCamera(); + if (!Camera) return Result; + + UCineCameraComponent* CameraComp = Camera->GetCineCameraComponent(); + if (!CameraComp) return Result; + + Result->SetNumberField(TEXT("focal_length"), CameraComp->CurrentFocalLength); + Result->SetNumberField(TEXT("aperture"), CameraComp->CurrentAperture); + Result->SetNumberField(TEXT("focus_distance"), CameraComp->FocusSettings.ManualFocusDistance); + Result->SetNumberField(TEXT("motion_blur_amount"), CameraComp->PostProcessSettings.MotionBlurAmount); + + return Result; +} + +// ============================================================================ +// Render Quality +// ============================================================================ + +bool URalphaParameterBridge::SetRenderQuality(const TSharedPtr& Settings) +{ + // This would typically use console commands or project settings + // For now, we'll log what we would set + + if (Settings->HasField(TEXT("lumen_quality"))) + { + float Quality = Settings->GetNumberField(TEXT("lumen_quality")); + UE_LOG(LogRalphaMCP, Log, TEXT("Would set Lumen quality to: %f"), Quality); + // r.Lumen.ScreenTracing.MaxIterations, etc. + } + + if (Settings->HasField(TEXT("shadow_quality"))) + { + FString Quality = Settings->GetStringField(TEXT("shadow_quality")); + UE_LOG(LogRalphaMCP, Log, TEXT("Would set shadow quality to: %s"), *Quality); + // sg.ShadowQuality + } + + if (Settings->HasField(TEXT("resolution_scale"))) + { + float Scale = Settings->GetNumberField(TEXT("resolution_scale")); + UE_LOG(LogRalphaMCP, Log, TEXT("Would set resolution scale to: %f"), Scale); + // r.ScreenPercentage + } + + return true; +} + +// ============================================================================ +// Asset Search +// ============================================================================ + +TArray> URalphaParameterBridge::SearchAssets(const FString& Query, const FString& AssetType, int32 MaxResults) +{ + TArray> Results; + + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); + + FARFilter Filter; + Filter.bRecursivePaths = true; + Filter.PackagePaths.Add(TEXT("/Game")); + + if (AssetType == TEXT("StaticMesh")) + { + Filter.ClassPaths.Add(UStaticMesh::StaticClass()->GetClassPathName()); + } + // Add more type filters as needed + + TArray AssetList; + AssetRegistry.GetAssets(Filter, AssetList); + + int32 Count = 0; + for (const FAssetData& Asset : AssetList) + { + if (Count >= MaxResults) break; + + FString AssetName = Asset.AssetName.ToString(); + if (AssetName.Contains(Query, ESearchCase::IgnoreCase)) + { + TSharedPtr AssetObj = MakeShared(); + AssetObj->SetStringField(TEXT("name"), AssetName); + AssetObj->SetStringField(TEXT("asset_path"), Asset.GetObjectPathString()); + AssetObj->SetStringField(TEXT("class"), Asset.AssetClassPath.GetAssetName().ToString()); + + Results.Add(MakeShared(AssetObj)); + Count++; + } + } + + return Results; +} + +TArray> URalphaParameterBridge::ListAssets(const FString& FolderPath, const FString& AssetType, bool bRecursive) +{ + TArray> Results; + + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); + + FARFilter Filter; + Filter.bRecursivePaths = bRecursive; + Filter.PackagePaths.Add(*FolderPath); + + TArray AssetList; + AssetRegistry.GetAssets(Filter, AssetList); + + for (const FAssetData& Asset : AssetList) + { + TSharedPtr AssetObj = MakeShared(); + AssetObj->SetStringField(TEXT("name"), Asset.AssetName.ToString()); + AssetObj->SetStringField(TEXT("asset_path"), Asset.GetObjectPathString()); + AssetObj->SetStringField(TEXT("class"), Asset.AssetClassPath.GetAssetName().ToString()); + + Results.Add(MakeShared(AssetObj)); + } + + return Results; +} + +// ============================================================================ +// Actor Management +// ============================================================================ + +bool URalphaParameterBridge::SpawnActor(const FString& AssetPath, const FVector& Location, const FRotator& Rotation, float Scale, const FString& ActorLabel, FString& OutActorId) +{ + UWorld* World = GetCurrentWorld(); + if (!World) return false; + + // Load the asset + UObject* LoadedAsset = StaticLoadObject(UStaticMesh::StaticClass(), nullptr, *AssetPath); + UStaticMesh* StaticMesh = Cast(LoadedAsset); + + if (!StaticMesh) + { + UE_LOG(LogRalphaMCP, Warning, TEXT("Failed to load static mesh: %s"), *AssetPath); + return false; + } + + // Spawn the actor + FActorSpawnParameters SpawnParams; + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + + AStaticMeshActor* NewActor = World->SpawnActor(Location, Rotation, SpawnParams); + if (!NewActor) + { + UE_LOG(LogRalphaMCP, Warning, TEXT("Failed to spawn actor")); + return false; + } + + NewActor->GetStaticMeshComponent()->SetStaticMesh(StaticMesh); + NewActor->SetActorScale3D(FVector(Scale)); + +#if WITH_EDITOR + if (!ActorLabel.IsEmpty()) + { + NewActor->SetActorLabel(ActorLabel); + } +#endif + + OutActorId = NewActor->GetName(); + return true; +} + +bool URalphaParameterBridge::DeleteActor(const FString& ActorId) +{ + UWorld* World = GetCurrentWorld(); + if (!World) return false; + + for (TActorIterator It(World); It; ++It) + { + if (It->GetName() == ActorId) + { + World->DestroyActor(*It); + return true; + } + } + + return false; +} + +TArray> URalphaParameterBridge::ListActors(const FString& ClassFilter, const FString& NameFilter, bool bIncludeTransforms) +{ + TArray> Results; + + UWorld* World = GetCurrentWorld(); + if (!World) return Results; + + for (TActorIterator It(World); It; ++It) + { + AActor* Actor = *It; + + // Apply filters + if (!ClassFilter.IsEmpty() && !Actor->GetClass()->GetName().Contains(ClassFilter)) + { + continue; + } + +#if WITH_EDITOR + if (!NameFilter.IsEmpty() && !Actor->GetActorLabel().Contains(NameFilter)) + { + continue; + } +#else + if (!NameFilter.IsEmpty() && !Actor->GetName().Contains(NameFilter)) + { + continue; + } +#endif + + TSharedPtr ActorObj = MakeShared(); + ActorObj->SetStringField(TEXT("actor_id"), Actor->GetName()); +#if WITH_EDITOR + ActorObj->SetStringField(TEXT("label"), Actor->GetActorLabel()); +#else + ActorObj->SetStringField(TEXT("label"), Actor->GetName()); +#endif + ActorObj->SetStringField(TEXT("class"), Actor->GetClass()->GetName()); + + if (bIncludeTransforms) + { + FVector Location = Actor->GetActorLocation(); + FRotator Rotation = Actor->GetActorRotation(); + FVector Scale = Actor->GetActorScale3D(); + + TSharedPtr TransformObj = MakeShared(); + + TSharedPtr LocObj = MakeShared(); + LocObj->SetNumberField(TEXT("x"), Location.X); + LocObj->SetNumberField(TEXT("y"), Location.Y); + LocObj->SetNumberField(TEXT("z"), Location.Z); + TransformObj->SetObjectField(TEXT("location"), LocObj); + + TSharedPtr RotObj = MakeShared(); + RotObj->SetNumberField(TEXT("pitch"), Rotation.Pitch); + RotObj->SetNumberField(TEXT("yaw"), Rotation.Yaw); + RotObj->SetNumberField(TEXT("roll"), Rotation.Roll); + TransformObj->SetObjectField(TEXT("rotation"), RotObj); + + TSharedPtr ScaleObj = MakeShared(); + ScaleObj->SetNumberField(TEXT("x"), Scale.X); + ScaleObj->SetNumberField(TEXT("y"), Scale.Y); + ScaleObj->SetNumberField(TEXT("z"), Scale.Z); + TransformObj->SetObjectField(TEXT("scale"), ScaleObj); + + ActorObj->SetObjectField(TEXT("transform"), TransformObj); + } + + Results.Add(MakeShared(ActorObj)); + } + + return Results; +} + +bool URalphaParameterBridge::SetActorTransform(const FString& ActorId, const FVector* Location, const FRotator* Rotation, const float* Scale, bool bRelative) +{ + UWorld* World = GetCurrentWorld(); + if (!World) return false; + + for (TActorIterator It(World); It; ++It) + { + if (It->GetName() == ActorId) + { + AActor* Actor = *It; + + if (Location) + { + if (bRelative) + { + Actor->SetActorLocation(Actor->GetActorLocation() + *Location); + } + else + { + Actor->SetActorLocation(*Location); + } + } + + if (Rotation) + { + if (bRelative) + { + Actor->SetActorRotation(Actor->GetActorRotation() + *Rotation); + } + else + { + Actor->SetActorRotation(*Rotation); + } + } + + if (Scale) + { + Actor->SetActorScale3D(FVector(*Scale)); + } + + return true; + } + } + + return false; +} + +TSharedPtr URalphaParameterBridge::GetActorDetails(const FString& ActorId, bool bIncludeComponents, bool bIncludeMaterials) +{ + TSharedPtr Result = MakeShared(); + + UWorld* World = GetCurrentWorld(); + if (!World) return Result; + + for (TActorIterator It(World); It; ++It) + { + if (It->GetName() == ActorId) + { + AActor* Actor = *It; + + Result->SetStringField(TEXT("actor_id"), Actor->GetName()); +#if WITH_EDITOR + Result->SetStringField(TEXT("label"), Actor->GetActorLabel()); +#else + Result->SetStringField(TEXT("label"), Actor->GetName()); +#endif + Result->SetStringField(TEXT("class"), Actor->GetClass()->GetName()); + + // Transform + FVector Location = Actor->GetActorLocation(); + FRotator Rotation = Actor->GetActorRotation(); + FVector Scale = Actor->GetActorScale3D(); + + TSharedPtr TransformObj = MakeShared(); + TSharedPtr LocObj = MakeShared(); + LocObj->SetNumberField(TEXT("x"), Location.X); + LocObj->SetNumberField(TEXT("y"), Location.Y); + LocObj->SetNumberField(TEXT("z"), Location.Z); + TransformObj->SetObjectField(TEXT("location"), LocObj); + + TSharedPtr RotObj = MakeShared(); + RotObj->SetNumberField(TEXT("pitch"), Rotation.Pitch); + RotObj->SetNumberField(TEXT("yaw"), Rotation.Yaw); + RotObj->SetNumberField(TEXT("roll"), Rotation.Roll); + TransformObj->SetObjectField(TEXT("rotation"), RotObj); + + Result->SetObjectField(TEXT("transform"), TransformObj); + + if (bIncludeComponents) + { + TArray> ComponentsArray; + TArray Components; + Actor->GetComponents(Components); + + for (UActorComponent* Component : Components) + { + TSharedPtr CompObj = MakeShared(); + CompObj->SetStringField(TEXT("name"), Component->GetName()); + CompObj->SetStringField(TEXT("class"), Component->GetClass()->GetName()); + ComponentsArray.Add(MakeShared(CompObj)); + } + + Result->SetArrayField(TEXT("components"), ComponentsArray); + } + + return Result; + } + } + + return Result; +} diff --git a/Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaScreenCapture.cpp b/Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaScreenCapture.cpp new file mode 100644 index 00000000..b1465022 --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaCore/Private/RalphaScreenCapture.cpp @@ -0,0 +1,166 @@ +// Copyright Ralpha Team. All Rights Reserved. + +#include "RalphaScreenCapture.h" +#include "RalphaMCPServer.h" +#include "Modules/ModuleManager.h" +#include "Engine/GameViewportClient.h" +#include "Engine/Engine.h" +#include "Slate/SceneViewport.h" +#include "ImageUtils.h" +#include "IImageWrapper.h" +#include "IImageWrapperModule.h" +#include "Misc/Base64.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "HAL/PlatformFileManager.h" +#include "Framework/Application/SlateApplication.h" +#include "Widgets/SWindow.h" +#include "UnrealClient.h" + +#if WITH_EDITOR +#include "Editor.h" +#include "LevelEditorViewport.h" +#endif + +URalphaScreenCapture::URalphaScreenCapture() +{ +} + +bool URalphaScreenCapture::CaptureScreenshot(int32 Width, int32 Height, FString& OutBase64, FString& OutFilePath) +{ + // Get the game viewport + UGameViewportClient* ViewportClient = GEngine ? GEngine->GameViewport : nullptr; + + // In editor, we might not have a game viewport, try to get the active viewport + FViewport* Viewport = nullptr; + + if (ViewportClient && ViewportClient->Viewport) + { + Viewport = ViewportClient->Viewport; + } +#if WITH_EDITOR + else + { + // Try to get the editor viewport + if (GEditor) + { + for (FLevelEditorViewportClient* LevelVC : GEditor->GetLevelViewportClients()) + { + if (LevelVC && LevelVC->Viewport) + { + Viewport = LevelVC->Viewport; + break; + } + } + } + } +#endif + + if (!Viewport) + { + UE_LOG(LogRalphaMCP, Warning, TEXT("No viewport available for screenshot")); + return false; + } + + // Read pixels from the viewport + TArray Bitmap; + int32 ViewportWidth = Viewport->GetSizeXY().X; + int32 ViewportHeight = Viewport->GetSizeXY().Y; + + if (ViewportWidth <= 0 || ViewportHeight <= 0) + { + UE_LOG(LogRalphaMCP, Warning, TEXT("Invalid viewport size: %dx%d"), ViewportWidth, ViewportHeight); + return false; + } + + // Read the viewport pixels + bool bSuccess = Viewport->ReadPixels(Bitmap); + + if (!bSuccess || Bitmap.Num() == 0) + { + UE_LOG(LogRalphaMCP, Warning, TEXT("Failed to read pixels from viewport")); + return false; + } + + // Convert to base64 PNG + OutBase64 = PixelsToBase64PNG(Bitmap, ViewportWidth, ViewportHeight); + + // Save to disk as well + FString ScreenshotDir = GetScreenshotDirectory(); + FString Filename = FString::Printf(TEXT("Ralpha_%s.png"), *FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S"))); + OutFilePath = ScreenshotDir / Filename; + + // Save the PNG + IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked(FName("ImageWrapper")); + TSharedPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG); + + if (ImageWrapper.IsValid() && ImageWrapper->SetRaw(Bitmap.GetData(), Bitmap.Num() * sizeof(FColor), ViewportWidth, ViewportHeight, ERGBFormat::BGRA, 8)) + { + TArray64 PNGData = ImageWrapper->GetCompressed(0); + if (PNGData.Num() > 0) + { + FFileHelper::SaveArrayToFile(PNGData, *OutFilePath); + } + } + + UE_LOG(LogRalphaMCP, Log, TEXT("Screenshot captured: %dx%d, saved to %s"), ViewportWidth, ViewportHeight, *OutFilePath); + + return true; +} + +bool URalphaScreenCapture::CaptureHighResScreenshot(int32 Width, int32 Height, FString& OutBase64) +{ + // For high-res screenshots, we would use FHighResScreenshotConfig + // This is a simplified implementation + + FString FilePath; + return CaptureScreenshot(Width, Height, OutBase64, FilePath); +} + +FString URalphaScreenCapture::PixelsToBase64PNG(const TArray& Pixels, int32 Width, int32 Height) +{ + // Load the image wrapper module + IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked(FName("ImageWrapper")); + TSharedPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG); + + if (!ImageWrapper.IsValid()) + { + UE_LOG(LogRalphaMCP, Warning, TEXT("Failed to create image wrapper")); + return FString(); + } + + // Set the raw pixel data + if (!ImageWrapper->SetRaw(Pixels.GetData(), Pixels.Num() * sizeof(FColor), Width, Height, ERGBFormat::BGRA, 8)) + { + UE_LOG(LogRalphaMCP, Warning, TEXT("Failed to set raw pixel data")); + return FString(); + } + + // Compress to PNG + TArray64 PNGData = ImageWrapper->GetCompressed(0); + if (PNGData.Num() == 0) + { + UE_LOG(LogRalphaMCP, Warning, TEXT("Failed to compress to PNG")); + return FString(); + } + + // Convert to base64 + TArray PNGData32; + PNGData32.Append(PNGData.GetData(), PNGData.Num()); + + return FBase64::Encode(PNGData32); +} + +FString URalphaScreenCapture::GetScreenshotDirectory() +{ + FString ScreenshotDir = FPaths::ProjectSavedDir() / TEXT("Screenshots") / TEXT("Ralpha"); + + // Create directory if it doesn't exist + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + if (!PlatformFile.DirectoryExists(*ScreenshotDir)) + { + PlatformFile.CreateDirectoryTree(*ScreenshotDir); + } + + return ScreenshotDir; +} diff --git a/Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaCore.h b/Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaCore.h new file mode 100644 index 00000000..1c761116 --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaCore.h @@ -0,0 +1,20 @@ +// Copyright Ralpha Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class RALPHACORE_API FRalphaCoreModule : public IModuleInterface +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + /** Get the module instance */ + static FRalphaCoreModule& Get(); + + /** Check if module is loaded */ + static bool IsAvailable(); +}; diff --git a/Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaMCPServer.h b/Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaMCPServer.h new file mode 100644 index 00000000..a0a76f2c --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaMCPServer.h @@ -0,0 +1,98 @@ +// Copyright Ralpha Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "Sockets.h" +#include "SocketSubsystem.h" +#include "Containers/Ticker.h" +#include "RalphaMCPServer.generated.h" + +class FRalphaClientConnection; +class URalphaParameterBridge; +class URalphaScreenCapture; + +DECLARE_LOG_CATEGORY_EXTERN(LogRalphaMCP, Log, All); + +/** + * TCP Server that receives JSON-RPC commands from the Python MCP server + * and executes them against UE5 systems. + */ +UCLASS(BlueprintType) +class RALPHACORE_API URalphaMCPServer : public UObject +{ + GENERATED_BODY() + +public: + URalphaMCPServer(); + virtual ~URalphaMCPServer(); + + /** Get the singleton instance */ + UFUNCTION(BlueprintCallable, Category = "Ralpha") + static URalphaMCPServer* Get(); + + /** Start the TCP server */ + UFUNCTION(BlueprintCallable, Category = "Ralpha") + bool Start(int32 Port = 30010); + + /** Stop the TCP server */ + UFUNCTION(BlueprintCallable, Category = "Ralpha") + void Stop(); + + /** Check if server is running */ + UFUNCTION(BlueprintCallable, Category = "Ralpha") + bool IsRunning() const; + + /** Get the port number */ + UFUNCTION(BlueprintCallable, Category = "Ralpha") + int32 GetPort() const { return ServerPort; } + + /** Process a JSON command and return response */ + FString ProcessCommand(const FString& JsonCommand); + +protected: + /** Tick function to accept new connections */ + bool Tick(float DeltaTime); + +private: + static URalphaMCPServer* Instance; + + FSocket* ListenerSocket; + int32 ServerPort; + bool bIsRunning; + + TArray> ClientConnections; + + UPROPERTY() + URalphaParameterBridge* ParameterBridge; + + UPROPERTY() + URalphaScreenCapture* ScreenCapture; + + FTSTicker::FDelegateHandle TickDelegateHandle; +}; + +/** + * Handles a single client connection + */ +class RALPHACORE_API FRalphaClientConnection : public TSharedFromThis +{ +public: + FRalphaClientConnection(FSocket* InSocket, URalphaMCPServer* InServer); + ~FRalphaClientConnection(); + + /** Process incoming data */ + void Tick(); + + /** Check if connection is valid */ + bool IsValid() const; + + /** Send response to client */ + void SendResponse(const FString& Response); + +private: + FSocket* Socket; + URalphaMCPServer* Server; + FString ReceiveBuffer; +}; diff --git a/Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaParameterBridge.h b/Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaParameterBridge.h new file mode 100644 index 00000000..5c85eafb --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaParameterBridge.h @@ -0,0 +1,80 @@ +// Copyright Ralpha Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "Dom/JsonObject.h" +#include "Dom/JsonValue.h" +#include "RalphaParameterBridge.generated.h" + +class APostProcessVolume; +class ADirectionalLight; +class ASkyLight; +class AExponentialHeightFog; +class ACineCameraActor; + +/** + * Bridge between MCP commands and UE5 rendering systems. + * Handles getting/setting parameters on post-process volumes, lights, fog, cameras, etc. + */ +UCLASS(BlueprintType) +class RALPHACORE_API URalphaParameterBridge : public UObject +{ + GENERATED_BODY() + +public: + URalphaParameterBridge(); + + // Get all current rendering parameters + TSharedPtr GetAllParameters(); + + // Post-Process Volume + bool SetPostProcessParameters(const TSharedPtr& Params); + TSharedPtr GetPostProcessParameters(); + + // Directional Light (Sun) + bool SetDirectionalLightParameters(const TSharedPtr& Params); + TSharedPtr GetDirectionalLightParameters(); + + // Sky Light + bool SetSkyLightParameters(const TSharedPtr& Params); + TSharedPtr GetSkyLightParameters(); + + // Exponential Height Fog + bool SetFogParameters(const TSharedPtr& Params); + TSharedPtr GetFogParameters(); + + // Camera + bool SetCameraParameters(const TSharedPtr& Params); + TSharedPtr GetCameraParameters(); + + // Render Quality + bool SetRenderQuality(const TSharedPtr& Settings); + + // Asset Search + TArray> SearchAssets(const FString& Query, const FString& AssetType, int32 MaxResults); + TArray> ListAssets(const FString& FolderPath, const FString& AssetType, bool bRecursive); + + // Actor Management + bool SpawnActor(const FString& AssetPath, const FVector& Location, const FRotator& Rotation, float Scale, const FString& ActorLabel, FString& OutActorId); + bool DeleteActor(const FString& ActorId); + TArray> ListActors(const FString& ClassFilter, const FString& NameFilter, bool bIncludeTransforms); + bool SetActorTransform(const FString& ActorId, const FVector* Location, const FRotator* Rotation, const float* Scale, bool bRelative); + TSharedPtr GetActorDetails(const FString& ActorId, bool bIncludeComponents, bool bIncludeMaterials); + +private: + // Find actors of specific types in the world + APostProcessVolume* FindPostProcessVolume(); + ADirectionalLight* FindDirectionalLight(); + ASkyLight* FindSkyLight(); + AExponentialHeightFog* FindExponentialHeightFog(); + ACineCameraActor* FindCineCamera(); + + // Get the current world + UWorld* GetCurrentWorld(); + + // Parse hex color string to FLinearColor + FLinearColor ParseHexColor(const FString& HexColor); + FString ColorToHex(const FLinearColor& Color); +}; diff --git a/Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaScreenCapture.h b/Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaScreenCapture.h new file mode 100644 index 00000000..ffa08a32 --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaCore/Public/RalphaScreenCapture.h @@ -0,0 +1,47 @@ +// Copyright Ralpha Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "RalphaScreenCapture.generated.h" + +/** + * Handles viewport screenshot capture for the style convergence loop. + */ +UCLASS(BlueprintType) +class RALPHACORE_API URalphaScreenCapture : public UObject +{ + GENERATED_BODY() + +public: + URalphaScreenCapture(); + + /** + * Capture the current viewport as a PNG image. + * @param Width Desired width (may be clamped to viewport size) + * @param Height Desired height (may be clamped to viewport size) + * @param OutBase64 The captured image as base64-encoded PNG + * @param OutFilePath Path where the image was saved (if saved to disk) + * @return True if capture succeeded + */ + UFUNCTION(BlueprintCallable, Category = "Ralpha") + bool CaptureScreenshot(int32 Width, int32 Height, FString& OutBase64, FString& OutFilePath); + + /** + * Capture a high-resolution screenshot. + * @param Width Desired width + * @param Height Desired height + * @param OutBase64 The captured image as base64-encoded PNG + * @return True if capture succeeded + */ + UFUNCTION(BlueprintCallable, Category = "Ralpha") + bool CaptureHighResScreenshot(int32 Width, int32 Height, FString& OutBase64); + +private: + /** Convert raw pixel data to base64 PNG */ + FString PixelsToBase64PNG(const TArray& Pixels, int32 Width, int32 Height); + + /** Get the screenshot save directory */ + FString GetScreenshotDirectory(); +}; diff --git a/Plugins/RalphaPlugin/Source/RalphaCore/RalphaCore.Build.cs b/Plugins/RalphaPlugin/Source/RalphaCore/RalphaCore.Build.cs new file mode 100644 index 00000000..91e17949 --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaCore/RalphaCore.Build.cs @@ -0,0 +1,59 @@ +// Copyright Ralpha Team. All Rights Reserved. + +using UnrealBuildTool; + +public class RalphaCore : ModuleRules +{ + public RalphaCore(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + } + ); + + PrivateIncludePaths.AddRange( + new string[] { + } + ); + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + "Sockets", + "Networking", + "Json", + "JsonUtilities", + "RenderCore", + "Renderer", + "RHI", + "ImageWrapper", + "AssetRegistry", + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "Slate", + "SlateCore", + "CinematicCamera", + } + ); + + if (Target.bBuildEditor) + { + PrivateDependencyModuleNames.Add("UnrealEd"); + } + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + } + ); + } +} diff --git a/Plugins/RalphaPlugin/Source/RalphaEditor/Private/RalphaEditor.cpp b/Plugins/RalphaPlugin/Source/RalphaEditor/Private/RalphaEditor.cpp new file mode 100644 index 00000000..4f6cf6ad --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaEditor/Private/RalphaEditor.cpp @@ -0,0 +1,74 @@ +// Copyright Ralpha Team. All Rights Reserved. + +#include "RalphaEditor.h" +#include "RalphaEditorCommands.h" +#include "RalphaEditorStyle.h" +#include "ToolMenus.h" +#include "RalphaMCPServer.h" + +#define LOCTEXT_NAMESPACE "FRalphaEditorModule" + +void FRalphaEditorModule::StartupModule() +{ + FRalphaEditorStyle::Initialize(); + FRalphaEditorStyle::ReloadTextures(); + + FRalphaEditorCommands::Register(); + + PluginCommands = MakeShareable(new FUICommandList); + + PluginCommands->MapAction( + FRalphaEditorCommands::Get().StartServer, + FExecuteAction::CreateLambda([]() + { + URalphaMCPServer* Server = URalphaMCPServer::Get(); + if (Server) + { + if (!Server->IsRunning()) + { + Server->Start(); + } + else + { + Server->Stop(); + } + } + }), + FCanExecuteAction() + ); + + UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FRalphaEditorModule::RegisterMenus)); + + UE_LOG(LogTemp, Log, TEXT("Ralpha Editor Module Started")); +} + +void FRalphaEditorModule::ShutdownModule() +{ + UToolMenus::UnRegisterStartupCallback(this); + UToolMenus::UnregisterOwner(this); + + FRalphaEditorStyle::Shutdown(); + FRalphaEditorCommands::Unregister(); + + UE_LOG(LogTemp, Log, TEXT("Ralpha Editor Module Shutdown")); +} + +void FRalphaEditorModule::RegisterMenus() +{ + FToolMenuOwnerScoped OwnerScoped(this); + + { + UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar"); + { + FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("Ralpha"); + { + FToolMenuEntry& Entry = Section.AddEntry(FToolMenuEntry::InitToolBarButton(FRalphaEditorCommands::Get().StartServer)); + Entry.SetCommandList(PluginCommands); + } + } + } +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FRalphaEditorModule, RalphaEditor) diff --git a/Plugins/RalphaPlugin/Source/RalphaEditor/Private/RalphaEditorCommands.cpp b/Plugins/RalphaPlugin/Source/RalphaEditor/Private/RalphaEditorCommands.cpp new file mode 100644 index 00000000..f3f84e15 --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaEditor/Private/RalphaEditorCommands.cpp @@ -0,0 +1,12 @@ +// Copyright Ralpha Team. All Rights Reserved. + +#include "RalphaEditorCommands.h" + +#define LOCTEXT_NAMESPACE "FRalphaEditorModule" + +void FRalphaEditorCommands::RegisterCommands() +{ + UI_COMMAND(StartServer, "Ralpha", "Start/Stop the Ralpha MCP Server", EUserInterfaceActionType::Button, FInputChord()); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/RalphaPlugin/Source/RalphaEditor/Private/RalphaEditorStyle.cpp b/Plugins/RalphaPlugin/Source/RalphaEditor/Private/RalphaEditorStyle.cpp new file mode 100644 index 00000000..18acfe12 --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaEditor/Private/RalphaEditorStyle.cpp @@ -0,0 +1,59 @@ +// Copyright Ralpha Team. All Rights Reserved. + +#include "RalphaEditorStyle.h" +#include "Styling/SlateStyleRegistry.h" +#include "Framework/Application/SlateApplication.h" +#include "Slate/SlateGameResources.h" +#include "Interfaces/IPluginManager.h" +#include "Styling/SlateStyleMacros.h" + +#define RootToContentDir Style->RootToContentDir + +TSharedPtr FRalphaEditorStyle::StyleInstance = nullptr; + +void FRalphaEditorStyle::Initialize() +{ + if (!StyleInstance.IsValid()) + { + StyleInstance = Create(); + FSlateStyleRegistry::RegisterSlateStyle(*StyleInstance); + } +} + +void FRalphaEditorStyle::Shutdown() +{ + FSlateStyleRegistry::UnRegisterSlateStyle(*StyleInstance); + ensure(StyleInstance.IsUnique()); + StyleInstance.Reset(); +} + +FName FRalphaEditorStyle::GetStyleSetName() +{ + static FName StyleSetName(TEXT("RalphaEditorStyle")); + return StyleSetName; +} + +const ISlateStyle& FRalphaEditorStyle::Get() +{ + return *StyleInstance; +} + +void FRalphaEditorStyle::ReloadTextures() +{ + if (FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); + } +} + +TSharedRef FRalphaEditorStyle::Create() +{ + TSharedRef Style = MakeShareable(new FSlateStyleSet("RalphaEditorStyle")); + Style->SetContentRoot(IPluginManager::Get().FindPlugin("RalphaPlugin")->GetBaseDir() / TEXT("Resources")); + + Style->Set("Ralpha.StartServer", new IMAGE_BRUSH_SVG(TEXT("Icon20"), FVector2D(20.0f, 20.0f))); + + return Style; +} + +#undef RootToContentDir diff --git a/Plugins/RalphaPlugin/Source/RalphaEditor/Public/RalphaEditor.h b/Plugins/RalphaPlugin/Source/RalphaEditor/Public/RalphaEditor.h new file mode 100644 index 00000000..242f0f31 --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaEditor/Public/RalphaEditor.h @@ -0,0 +1,22 @@ +// Copyright Ralpha Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class FToolBarBuilder; +class FMenuBuilder; + +class RALPHAEDITOR_API FRalphaEditorModule : public IModuleInterface +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + +private: + void RegisterMenus(); + + TSharedPtr PluginCommands; +}; diff --git a/Plugins/RalphaPlugin/Source/RalphaEditor/Public/RalphaEditorCommands.h b/Plugins/RalphaPlugin/Source/RalphaEditor/Public/RalphaEditorCommands.h new file mode 100644 index 00000000..4337f940 --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaEditor/Public/RalphaEditorCommands.h @@ -0,0 +1,25 @@ +// Copyright Ralpha Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Framework/Commands/Commands.h" +#include "RalphaEditorStyle.h" + +class FRalphaEditorCommands : public TCommands +{ +public: + FRalphaEditorCommands() + : TCommands( + TEXT("Ralpha"), + NSLOCTEXT("Contexts", "Ralpha", "Ralpha Plugin"), + NAME_None, + FRalphaEditorStyle::GetStyleSetName()) + { + } + + virtual void RegisterCommands() override; + +public: + TSharedPtr StartServer; +}; diff --git a/Plugins/RalphaPlugin/Source/RalphaEditor/Public/RalphaEditorStyle.h b/Plugins/RalphaPlugin/Source/RalphaEditor/Public/RalphaEditorStyle.h new file mode 100644 index 00000000..79cff4e0 --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaEditor/Public/RalphaEditorStyle.h @@ -0,0 +1,20 @@ +// Copyright Ralpha Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Styling/SlateStyle.h" + +class FRalphaEditorStyle +{ +public: + static void Initialize(); + static void Shutdown(); + static void ReloadTextures(); + static const ISlateStyle& Get(); + static FName GetStyleSetName(); + +private: + static TSharedRef Create(); + static TSharedPtr StyleInstance; +}; diff --git a/Plugins/RalphaPlugin/Source/RalphaEditor/RalphaEditor.Build.cs b/Plugins/RalphaPlugin/Source/RalphaEditor/RalphaEditor.Build.cs new file mode 100644 index 00000000..b9b0f7e1 --- /dev/null +++ b/Plugins/RalphaPlugin/Source/RalphaEditor/RalphaEditor.Build.cs @@ -0,0 +1,52 @@ +// Copyright Ralpha Team. All Rights Reserved. + +using UnrealBuildTool; + +public class RalphaEditor : ModuleRules +{ + public RalphaEditor(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + } + ); + + PrivateIncludePaths.AddRange( + new string[] { + } + ); + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "RalphaCore", + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "UnrealEd", + "EditorStyle", + "ToolMenus", + "LevelEditor", + "AssetRegistry", + "ContentBrowser", + "Projects", + } + ); + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + } + ); + } +} diff --git a/Ralpha.uproject b/Ralpha.uproject new file mode 100644 index 00000000..445dafda --- /dev/null +++ b/Ralpha.uproject @@ -0,0 +1,16 @@ +{ + "FileVersion": 3, + "EngineAssociation": "5.7", + "Category": "", + "Description": "AI-driven style transfer for infinite world generation", + "Modules": [], + "Plugins": [ + { + "Name": "RalphaPlugin", + "Enabled": true + } + ], + "TargetPlatforms": [ + "Windows" + ] +}