From 13da8293c16211130a312e6b64639ca9e6b5c2ab Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Fri, 17 Apr 2026 12:40:39 +0200 Subject: [PATCH] Per-level catalogs, external scenarios dir, .pss extension, BP from mesh tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add per-base-level Catalogs to FPS_Editor_BaseLevelEntry, merged with global catalogs when the base level is loaded - Add SpawnManager::ClearCatalogs() and rebuild on LoadBaseLevelAsSublevel - Move scenarios storage to C:/Asterion_VR/SCENARIOS/{LevelName}/ for easy transfer between client PCs; avoids name conflicts via level subdirectories - Add GetSceneFilePathForLevel and refactor GetSavedSceneNames to scan subdirs - Change scenario file extension from .json to .pss with .json legacy fallback - Add content browser tool: right-click StaticMesh/SkeletalMesh → PS Editor section → Create Spawnable Blueprint (generates BP with mesh component, SpawnableComponent with pre-filled DisplayName, and EditableComponent) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../GameMode/PS_Editor_PlayerController.cpp | 22 ++ .../Serialization/PS_Editor_SceneLoader.cpp | 89 +++++--- .../Serialization/PS_Editor_SceneLoader.h | 4 + .../PS_Editor_SceneSerializer.cpp | 16 +- .../Serialization/PS_Editor_SceneSerializer.h | 7 +- .../PS_Editor/Spawn/PS_Editor_SpawnCatalog.h | 8 + .../Spawn/PS_Editor_SpawnManager.cpp | 5 + .../PS_Editor/Spawn/PS_Editor_SpawnManager.h | 3 + .../UI/Widgets/PS_Editor_MainWidget.cpp | 7 +- .../PS_EditorTools/PS_EditorTools.Build.cs | 11 +- .../Source/PS_EditorTools/PS_EditorTools.cpp | 11 + .../Source/PS_EditorTools/PS_EditorTools.h | 4 +- .../PS_Editor_BlueprintFromMesh.cpp | 192 ++++++++++++++++++ .../PS_Editor_BlueprintFromMesh.h | 26 +++ 14 files changed, 366 insertions(+), 39 deletions(-) create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_Editor_BlueprintFromMesh.cpp create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_Editor_BlueprintFromMesh.h diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp index 664a187..af54a82 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp @@ -399,6 +399,28 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa UE_LOG(LogTemp, Log, TEXT("PS_Editor: Unloaded previous sublevel")); } + // Rebuild spawn catalog: global + base-level-specific catalogs + if (SpawnManager && MasterCatalogAsset) + { + SpawnManager->ClearCatalogs(); + for (UPS_Editor_SpawnCatalog* Cat : MasterCatalogAsset->Catalogs) + { + if (Cat) SpawnManager->AddCatalog(Cat); + } + // Add catalogs specific to the new base level + for (const FPS_Editor_BaseLevelEntry& Entry : MasterCatalogAsset->BaseLevels) + { + if (Entry.DisplayName == LevelName) + { + for (UPS_Editor_SpawnCatalog* Cat : Entry.Catalogs) + { + if (Cat) SpawnManager->AddCatalog(Cat); + } + break; + } + } + } + // Unload additional sublevels from previous base level for (TObjectPtr& Extra : AdditionalSublevels) { diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp index d4d0d96..59868e3 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp @@ -231,37 +231,73 @@ bool UPS_Editor_SceneLoader::IsLoadingFromScene(const AActor* Actor) FString UPS_Editor_SceneLoader::GetScenesDirectory() { - return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("PS_Editor"), TEXT("Scenes")); + return TEXT("C:/Asterion_VR/SCENARIOS"); } FString UPS_Editor_SceneLoader::GetSceneFilePath(const FString& SceneName) { - return FPaths::Combine(GetScenesDirectory(), SceneName + TEXT(".json")); + // Legacy/fallback: search all subdirectories for the scene file (.pss then .json) + const FString Root = GetScenesDirectory(); + IFileManager& FM = IFileManager::Get(); + + TArray SubDirs; + FM.FindFiles(SubDirs, *FPaths::Combine(Root, TEXT("*")), false, true); + + for (const FString& SubDir : SubDirs) + { + const FString Pss = FPaths::Combine(Root, SubDir, SceneName + TEXT(".pss")); + if (FM.FileExists(*Pss)) return Pss; + const FString Json = FPaths::Combine(Root, SubDir, SceneName + TEXT(".json")); + if (FM.FileExists(*Json)) return Json; + } + // Not found: return default location under "Unfiled" subdir + return FPaths::Combine(Root, TEXT("Unfiled"), SceneName + TEXT(".pss")); +} + +FString UPS_Editor_SceneLoader::GetSceneFilePathForLevel(const FString& SceneName, const FString& LevelName) +{ + const FString LevelSubDir = LevelName.IsEmpty() ? TEXT("Unfiled") : LevelName; + // Prefer .pss but fall back to legacy .json if it exists + const FString Pss = FPaths::Combine(GetScenesDirectory(), LevelSubDir, SceneName + TEXT(".pss")); + const FString Json = FPaths::Combine(GetScenesDirectory(), LevelSubDir, SceneName + TEXT(".json")); + if (!IFileManager::Get().FileExists(*Pss) && IFileManager::Get().FileExists(*Json)) + { + return Json; + } + return Pss; } TArray UPS_Editor_SceneLoader::GetSavedSceneNames(const FString& LevelFilter) { TArray SceneNames; - const FString Directory = GetScenesDirectory(); + const FString Root = GetScenesDirectory(); + IFileManager& FM = IFileManager::Get(); - TArray Files; - IFileManager::Get().FindFiles(Files, *FPaths::Combine(Directory, TEXT("*.json")), true, false); - - for (const FString& File : Files) + if (!LevelFilter.IsEmpty()) { - const FString Name = FPaths::GetBaseFilename(File); - - if (LevelFilter.IsEmpty()) + // Scan only the subdirectory for this level (.pss + legacy .json) + const FString LevelDir = FPaths::Combine(Root, LevelFilter); + TArray Files; + FM.FindFiles(Files, *FPaths::Combine(LevelDir, TEXT("*.pss")), true, false); + FM.FindFiles(Files, *FPaths::Combine(LevelDir, TEXT("*.json")), true, false); + for (const FString& File : Files) { - SceneNames.Add(Name); + SceneNames.AddUnique(FPaths::GetBaseFilename(File)); } - else + } + else + { + // Scan all subdirectories + TArray SubDirs; + FM.FindFiles(SubDirs, *FPaths::Combine(Root, TEXT("*")), false, true); + for (const FString& SubDir : SubDirs) { - // Quick check: read the LevelName field from JSON without full parse - const FString LevelName = GetSceneLevelName(Name); - if (LevelName == LevelFilter) + TArray Files; + FM.FindFiles(Files, *FPaths::Combine(Root, SubDir, TEXT("*.pss")), true, false); + FM.FindFiles(Files, *FPaths::Combine(Root, SubDir, TEXT("*.json")), true, false); + for (const FString& File : Files) { - SceneNames.Add(Name); + SceneNames.AddUnique(FPaths::GetBaseFilename(File)); } } } @@ -272,18 +308,19 @@ TArray UPS_Editor_SceneLoader::GetSavedSceneNames(const FString& LevelF FString UPS_Editor_SceneLoader::GetSceneLevelName(const FString& SceneName) { - const FString FilePath = GetSceneFilePath(SceneName); - FString JsonString; - if (!FFileHelper::LoadFileToString(JsonString, *FilePath)) - { - return TEXT(""); - } + // The level name IS the subdirectory name containing the scene file + const FString Root = GetScenesDirectory(); + IFileManager& FM = IFileManager::Get(); - // Parse just the header (lightweight) - FPS_Editor_SceneData SceneData; - if (FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &SceneData, 0, 0)) + TArray SubDirs; + FM.FindFiles(SubDirs, *FPaths::Combine(Root, TEXT("*")), false, true); + + for (const FString& SubDir : SubDirs) { - return SceneData.LevelName; + const FString Pss = FPaths::Combine(Root, SubDir, SceneName + TEXT(".pss")); + if (FM.FileExists(*Pss)) return SubDir; + const FString Json = FPaths::Combine(Root, SubDir, SceneName + TEXT(".json")); + if (FM.FileExists(*Json)) return SubDir; } return TEXT(""); } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.h index e687440..3a96e26 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.h @@ -68,6 +68,10 @@ public: UFUNCTION(BlueprintCallable, Category = "PS Editor") static FString GetSceneFilePath(const FString& SceneName); + /** Get the full file path for a scene name and its base level. Uses level-specific subdirectory. */ + UFUNCTION(BlueprintCallable, Category = "PS Editor") + static FString GetSceneFilePathForLevel(const FString& SceneName, const FString& LevelName); + /** * Get all saved scene names. Optionally filter by level name. * @param LevelFilter If not empty, only return scenes associated with this level. diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp index 2ea775c..32abe47 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp @@ -119,8 +119,9 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_ return false; } - // Ensure directory exists - const FString Directory = GetScenesDirectory(); + // Use level-specific subdirectory (level name is used as folder name) + const FString FilePath = GetSceneFilePathForLevel(SceneName, SceneData.LevelName); + const FString Directory = FPaths::GetPath(FilePath); IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); if (!PlatformFile.DirectoryExists(*Directory)) { @@ -128,7 +129,6 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_ } // Write file - const FString FilePath = GetSceneFilePath(SceneName); if (!FFileHelper::SaveStringToFile(JsonString, *FilePath)) { UE_LOG(LogTemp, Error, TEXT("PS_Editor: Failed to write scene file: %s"), *FilePath); @@ -293,6 +293,7 @@ TArray UPS_Editor_SceneSerializer::GetSavedSceneNames() const const FString Directory = GetScenesDirectory(); TArray Files; + IFileManager::Get().FindFiles(Files, *FPaths::Combine(Directory, TEXT("*.pss")), true, false); IFileManager::Get().FindFiles(Files, *FPaths::Combine(Directory, TEXT("*.json")), true, false); for (const FString& File : Files) @@ -330,10 +331,15 @@ void UPS_Editor_SceneSerializer::ClearScene(UPS_Editor_SpawnManager* SpawnManage FString UPS_Editor_SceneSerializer::GetSceneFilePath(const FString& SceneName) const { - return FPaths::Combine(GetScenesDirectory(), SceneName + TEXT(".json")); + return UPS_Editor_SceneLoader::GetSceneFilePath(SceneName); +} + +FString UPS_Editor_SceneSerializer::GetSceneFilePathForLevel(const FString& SceneName, const FString& LevelName) const +{ + return UPS_Editor_SceneLoader::GetSceneFilePathForLevel(SceneName, LevelName); } FString UPS_Editor_SceneSerializer::GetScenesDirectory() const { - return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("PS_Editor"), TEXT("Scenes")); + return UPS_Editor_SceneLoader::GetScenesDirectory(); } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.h index 34a2d38..82ec935 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.h @@ -40,9 +40,12 @@ private: UPROPERTY() TWeakObjectPtr SelectionManagerRef; - /** Get the full file path for a scene name. */ + /** Get the full file path for a scene name (scans all level subdirs). */ FString GetSceneFilePath(const FString& SceneName) const; - /** Get the directory where scenes are stored. */ + /** Get the full file path using a specific base level subdirectory. */ + FString GetSceneFilePathForLevel(const FString& SceneName, const FString& LevelName) const; + + /** Get the directory where scenes are stored (root). */ FString GetScenesDirectory() const; }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h index 7c0b338..0cd811a 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h @@ -77,6 +77,14 @@ struct FPS_Editor_BaseLevelEntry */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor") TArray AdditionalSublevels; + + /** + * Sub-catalogs available only when this base level is loaded. + * Their entries are merged with the global catalogs (MasterCatalog::Catalogs). + * Empty = only global catalogs are shown. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor") + TArray> Catalogs; }; /** diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp index 1cf660a..78aceb9 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp @@ -14,6 +14,11 @@ void UPS_Editor_SpawnManager::Initialize(APlayerController* InOwnerPC) OwnerPC = InOwnerPC; } +void UPS_Editor_SpawnManager::ClearCatalogs() +{ + ResolvedEntries.Empty(); +} + void UPS_Editor_SpawnManager::AddCatalog(UPS_Editor_SpawnCatalog* InCatalog) { if (!InCatalog) return; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h index c05b6c5..29929eb 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h @@ -42,6 +42,9 @@ public: /** Load and add a catalog. Can be called multiple times for multiple catalogs. */ void AddCatalog(UPS_Editor_SpawnCatalog* InCatalog); + /** Clear all catalog entries (used before re-adding for a different base level). */ + void ClearCatalogs(); + /** Spawn the actor at the given catalog index, positioned in front of the camera. Returns the spawned actor. */ AActor* SpawnFromCatalog(int32 Index); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp index 9b0d58a..e8ad867 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp @@ -745,8 +745,8 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() ParentWindowHandle, TEXT("Save Scenario As"), DefaultPath, - SaveNameField.IsValid() ? SaveNameField->GetText().ToString() + TEXT(".json") : TEXT(""), - TEXT("Scenario Files (*.json)|*.json"), + SaveNameField.IsValid() ? SaveNameField->GetText().ToString() + TEXT(".pss") : TEXT(""), + TEXT("Scenario Files (*.pss)|*.pss"), 0, OutFiles); @@ -937,7 +937,7 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() TEXT("Load Scenario"), DefaultPath, TEXT(""), - TEXT("Scenario Files (*.json)|*.json"), + TEXT("Scenario Files (*.pss)|*.pss"), 0, OutFiles); @@ -2634,6 +2634,7 @@ void UPS_Editor_MainWidget::ConfirmBaseLevelSelection() } PopulateSceneList(); + PopulateSpawnCatalog(); LastOutlinerActorCount = -1; PendingBaseLevelSelection.Empty(); } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_EditorTools.Build.cs b/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_EditorTools.Build.cs index d7ef10b..973ec40 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_EditorTools.Build.cs +++ b/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_EditorTools.Build.cs @@ -12,7 +12,16 @@ public class PS_EditorTools : ModuleRules "CoreUObject", "Engine", "PS_Editor", - "UnrealEd" + "UnrealEd", + "ContentBrowser", + "AssetRegistry", + "AssetTools", + "Kismet", + "KismetCompiler", + "BlueprintGraph", + "ToolMenus", + "Slate", + "SlateCore" }); } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_EditorTools.cpp b/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_EditorTools.cpp index 9eb0897..73bbaca 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_EditorTools.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_EditorTools.cpp @@ -1,3 +1,14 @@ #include "PS_EditorTools.h" +#include "PS_Editor_BlueprintFromMesh.h" + +void FPS_EditorToolsModule::StartupModule() +{ + FPS_Editor_BlueprintFromMesh::Register(); +} + +void FPS_EditorToolsModule::ShutdownModule() +{ + FPS_Editor_BlueprintFromMesh::Unregister(); +} IMPLEMENT_MODULE(FPS_EditorToolsModule, PS_EditorTools) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_EditorTools.h b/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_EditorTools.h index 51ace27..dcbee7a 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_EditorTools.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_EditorTools.h @@ -5,6 +5,6 @@ class FPS_EditorToolsModule : public IModuleInterface { public: - virtual void StartupModule() override {} - virtual void ShutdownModule() override {} + virtual void StartupModule() override; + virtual void ShutdownModule() override; }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_Editor_BlueprintFromMesh.cpp b/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_Editor_BlueprintFromMesh.cpp new file mode 100644 index 0000000..e82cf1c --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_Editor_BlueprintFromMesh.cpp @@ -0,0 +1,192 @@ +#include "PS_Editor_BlueprintFromMesh.h" +#include "PS_Editor_SpawnableComponent.h" +#include "PS_Editor_EditableComponent.h" + +#include "ContentBrowserModule.h" +#include "IContentBrowserSingleton.h" +#include "Engine/StaticMesh.h" +#include "Engine/SkeletalMesh.h" +#include "Components/StaticMeshComponent.h" +#include "Components/SkeletalMeshComponent.h" +#include "AssetToolsModule.h" +#include "IAssetTools.h" +#include "Factories/BlueprintFactory.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Engine/SCS_Node.h" +#include "Engine/SimpleConstructionScript.h" +#include "Engine/Blueprint.h" +#include "Engine/BlueprintGeneratedClass.h" +#include "GameFramework/Actor.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "UObject/Package.h" +#include "Misc/Paths.h" +#include "Misc/MessageDialog.h" +#include "AssetRegistry/AssetRegistryModule.h" + +static FDelegateHandle ContentBrowserExtenderHandle; + +static TSharedRef OnExtendContentBrowserAssetSelectionMenu(const TArray& SelectedAssets) +{ + TSharedRef Extender(new FExtender()); + + // Only show if all selected assets are StaticMesh or SkeletalMesh + bool bAllAreMesh = SelectedAssets.Num() > 0; + for (const FAssetData& Asset : SelectedAssets) + { + if (Asset.AssetClassPath != UStaticMesh::StaticClass()->GetClassPathName() + && Asset.AssetClassPath != USkeletalMesh::StaticClass()->GetClassPathName()) + { + bAllAreMesh = false; + break; + } + } + + if (!bAllAreMesh) return Extender; + + TArray SelectedObjects; + for (const FAssetData& Asset : SelectedAssets) + { + if (UObject* Obj = Asset.GetAsset()) + { + SelectedObjects.Add(Obj); + } + } + + Extender->AddMenuExtension( + "GetAssetActions", + EExtensionHook::After, + nullptr, + FMenuExtensionDelegate::CreateLambda([SelectedObjects](FMenuBuilder& MenuBuilder) + { + MenuBuilder.BeginSection("PSEditor", NSLOCTEXT("PSEditor", "PSEditorMenu", "PS Editor")); + MenuBuilder.AddMenuEntry( + NSLOCTEXT("PSEditor", "CreateSpawnableBP", "Create Spawnable Blueprint"), + NSLOCTEXT("PSEditor", "CreateSpawnableBPTooltip", "Create a Blueprint with PS_Editor Spawnable + Editable components from this mesh"), + FSlateIcon(), + FUIAction(FExecuteAction::CreateLambda([SelectedObjects]() + { + FPS_Editor_BlueprintFromMesh::OnCreateBlueprintFromMesh(SelectedObjects); + })) + ); + MenuBuilder.EndSection(); + })); + + return Extender; +} + +void FPS_Editor_BlueprintFromMesh::Register() +{ + FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + TArray& Extenders = ContentBrowserModule.GetAllAssetViewContextMenuExtenders(); + + Extenders.Add(FContentBrowserMenuExtender_SelectedAssets::CreateStatic(&OnExtendContentBrowserAssetSelectionMenu)); + ContentBrowserExtenderHandle = Extenders.Last().GetHandle(); +} + +void FPS_Editor_BlueprintFromMesh::Unregister() +{ + if (FContentBrowserModule* ContentBrowserModule = FModuleManager::GetModulePtr("ContentBrowser")) + { + TArray& Extenders = ContentBrowserModule->GetAllAssetViewContextMenuExtenders(); + Extenders.RemoveAll([](const FContentBrowserMenuExtender_SelectedAssets& Delegate) + { + return Delegate.GetHandle() == ContentBrowserExtenderHandle; + }); + } +} + +void FPS_Editor_BlueprintFromMesh::OnCreateBlueprintFromMesh(TArray SelectedAssets) +{ + for (UObject* Asset : SelectedAssets) + { + CreateBlueprintForAsset(Asset); + } +} + +void FPS_Editor_BlueprintFromMesh::CreateBlueprintForAsset(UObject* MeshAsset) +{ + if (!MeshAsset) return; + + UStaticMesh* StaticMesh = Cast(MeshAsset); + USkeletalMesh* SkelMesh = Cast(MeshAsset); + if (!StaticMesh && !SkelMesh) return; + + // Build BP name and path alongside the mesh asset + const FString AssetPath = FPaths::GetPath(MeshAsset->GetPathName()); + const FString AssetName = MeshAsset->GetName(); + const FString BPName = FString::Printf(TEXT("BP_%s"), *AssetName); + const FString PackagePath = AssetPath; + const FString FullPath = PackagePath + TEXT("/") + BPName; + + // Check if it already exists + if (LoadObject(nullptr, *FullPath)) + { + FMessageDialog::Open(EAppMsgType::Ok, + FText::FromString(FString::Printf(TEXT("Blueprint '%s' already exists at %s"), *BPName, *PackagePath))); + return; + } + + // Create package for the blueprint + UPackage* Package = CreatePackage(*FullPath); + if (!Package) return; + + // Create blueprint from AActor parent class + UBlueprint* Blueprint = FKismetEditorUtilities::CreateBlueprint( + AActor::StaticClass(), + Package, + *BPName, + BPTYPE_Normal, + UBlueprint::StaticClass(), + UBlueprintGeneratedClass::StaticClass(), + FName("PS_Editor_CreateFromMesh")); + + if (!Blueprint) return; + + USimpleConstructionScript* SCS = Blueprint->SimpleConstructionScript; + if (!SCS) return; + + // Add mesh component as root (attached to the default root scene component) + USCS_Node* MeshNode = nullptr; + if (StaticMesh) + { + MeshNode = SCS->CreateNode(UStaticMeshComponent::StaticClass(), FName("Mesh")); + if (UStaticMeshComponent* MeshTemplate = Cast(MeshNode->ComponentTemplate)) + { + MeshTemplate->SetStaticMesh(StaticMesh); + } + } + else if (SkelMesh) + { + MeshNode = SCS->CreateNode(USkeletalMeshComponent::StaticClass(), FName("Mesh")); + if (USkeletalMeshComponent* MeshTemplate = Cast(MeshNode->ComponentTemplate)) + { + MeshTemplate->SetSkeletalMesh(SkelMesh); + } + } + if (MeshNode) + { + SCS->AddNode(MeshNode); + } + + // Add PS_Editor Spawnable component + USCS_Node* SpawnableNode = SCS->CreateNode(UPS_Editor_SpawnableComponent::StaticClass(), FName("PS_Editor_Spawnable")); + if (UPS_Editor_SpawnableComponent* SpawnableTemplate = Cast(SpawnableNode->ComponentTemplate)) + { + SpawnableTemplate->DisplayName = AssetName; + SpawnableTemplate->Category = TEXT("Default"); + } + SCS->AddNode(SpawnableNode); + + // Add PS_Editor Editable component + USCS_Node* EditableNode = SCS->CreateNode(UPS_Editor_EditableComponent::StaticClass(), FName("PS_Editor_Editable")); + SCS->AddNode(EditableNode); + + // Compile and save + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint); + FKismetEditorUtilities::CompileBlueprint(Blueprint); + Package->MarkPackageDirty(); + FAssetRegistryModule::AssetCreated(Blueprint); + + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Created Blueprint '%s' from mesh '%s'"), *BPName, *AssetName); +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_Editor_BlueprintFromMesh.h b/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_Editor_BlueprintFromMesh.h new file mode 100644 index 0000000..6f5d81a --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_EditorTools/PS_Editor_BlueprintFromMesh.h @@ -0,0 +1,26 @@ +#pragma once + +#include "CoreMinimal.h" + +class UObject; +class FMenuBuilder; + +/** + * Registers a content browser right-click menu to create a Blueprint + * from a StaticMesh or SkeletalMesh with PS_Editor Spawnable + Editable components. + */ +class FPS_Editor_BlueprintFromMesh +{ +public: + /** Register the context menu extension. */ + static void Register(); + + /** Unregister the context menu extension. */ + static void Unregister(); + + /** Called when the user clicks the menu item. */ + static void OnCreateBlueprintFromMesh(TArray SelectedAssets); + + /** Create a single Blueprint from the given mesh asset. */ + static void CreateBlueprintForAsset(UObject* MeshAsset); +};