Per-level catalogs, external scenarios dir, .pss extension, BP from mesh tool

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 12:40:39 +02:00
parent 82e76c01ff
commit 13da8293c1
14 changed files with 366 additions and 39 deletions

View File

@@ -399,6 +399,28 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Unloaded previous sublevel")); 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 // Unload additional sublevels from previous base level
for (TObjectPtr<ULevelStreamingDynamic>& Extra : AdditionalSublevels) for (TObjectPtr<ULevelStreamingDynamic>& Extra : AdditionalSublevels)
{ {

View File

@@ -231,37 +231,73 @@ bool UPS_Editor_SceneLoader::IsLoadingFromScene(const AActor* Actor)
FString UPS_Editor_SceneLoader::GetScenesDirectory() 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) 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<FString> 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<FString> UPS_Editor_SceneLoader::GetSavedSceneNames(const FString& LevelFilter) TArray<FString> UPS_Editor_SceneLoader::GetSavedSceneNames(const FString& LevelFilter)
{ {
TArray<FString> SceneNames; TArray<FString> SceneNames;
const FString Directory = GetScenesDirectory(); const FString Root = GetScenesDirectory();
IFileManager& FM = IFileManager::Get();
TArray<FString> Files; if (!LevelFilter.IsEmpty())
IFileManager::Get().FindFiles(Files, *FPaths::Combine(Directory, TEXT("*.json")), true, false);
for (const FString& File : Files)
{ {
const FString Name = FPaths::GetBaseFilename(File); // Scan only the subdirectory for this level (.pss + legacy .json)
const FString LevelDir = FPaths::Combine(Root, LevelFilter);
if (LevelFilter.IsEmpty()) TArray<FString> 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<FString> 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 TArray<FString> Files;
const FString LevelName = GetSceneLevelName(Name); FM.FindFiles(Files, *FPaths::Combine(Root, SubDir, TEXT("*.pss")), true, false);
if (LevelName == LevelFilter) 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<FString> UPS_Editor_SceneLoader::GetSavedSceneNames(const FString& LevelF
FString UPS_Editor_SceneLoader::GetSceneLevelName(const FString& SceneName) FString UPS_Editor_SceneLoader::GetSceneLevelName(const FString& SceneName)
{ {
const FString FilePath = GetSceneFilePath(SceneName); // The level name IS the subdirectory name containing the scene file
FString JsonString; const FString Root = GetScenesDirectory();
if (!FFileHelper::LoadFileToString(JsonString, *FilePath)) IFileManager& FM = IFileManager::Get();
{
return TEXT("");
}
// Parse just the header (lightweight) TArray<FString> SubDirs;
FPS_Editor_SceneData SceneData; FM.FindFiles(SubDirs, *FPaths::Combine(Root, TEXT("*")), false, true);
if (FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &SceneData, 0, 0))
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(""); return TEXT("");
} }

View File

@@ -68,6 +68,10 @@ public:
UFUNCTION(BlueprintCallable, Category = "PS Editor") UFUNCTION(BlueprintCallable, Category = "PS Editor")
static FString GetSceneFilePath(const FString& SceneName); 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. * Get all saved scene names. Optionally filter by level name.
* @param LevelFilter If not empty, only return scenes associated with this level. * @param LevelFilter If not empty, only return scenes associated with this level.

View File

@@ -119,8 +119,9 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
return false; return false;
} }
// Ensure directory exists // Use level-specific subdirectory (level name is used as folder name)
const FString Directory = GetScenesDirectory(); const FString FilePath = GetSceneFilePathForLevel(SceneName, SceneData.LevelName);
const FString Directory = FPaths::GetPath(FilePath);
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
if (!PlatformFile.DirectoryExists(*Directory)) if (!PlatformFile.DirectoryExists(*Directory))
{ {
@@ -128,7 +129,6 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
} }
// Write file // Write file
const FString FilePath = GetSceneFilePath(SceneName);
if (!FFileHelper::SaveStringToFile(JsonString, *FilePath)) if (!FFileHelper::SaveStringToFile(JsonString, *FilePath))
{ {
UE_LOG(LogTemp, Error, TEXT("PS_Editor: Failed to write scene file: %s"), *FilePath); UE_LOG(LogTemp, Error, TEXT("PS_Editor: Failed to write scene file: %s"), *FilePath);
@@ -293,6 +293,7 @@ TArray<FString> UPS_Editor_SceneSerializer::GetSavedSceneNames() const
const FString Directory = GetScenesDirectory(); const FString Directory = GetScenesDirectory();
TArray<FString> Files; TArray<FString> Files;
IFileManager::Get().FindFiles(Files, *FPaths::Combine(Directory, TEXT("*.pss")), true, false);
IFileManager::Get().FindFiles(Files, *FPaths::Combine(Directory, TEXT("*.json")), true, false); IFileManager::Get().FindFiles(Files, *FPaths::Combine(Directory, TEXT("*.json")), true, false);
for (const FString& File : Files) 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 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 FString UPS_Editor_SceneSerializer::GetScenesDirectory() const
{ {
return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("PS_Editor"), TEXT("Scenes")); return UPS_Editor_SceneLoader::GetScenesDirectory();
} }

View File

@@ -40,9 +40,12 @@ private:
UPROPERTY() UPROPERTY()
TWeakObjectPtr<UPS_Editor_SelectionManager> SelectionManagerRef; TWeakObjectPtr<UPS_Editor_SelectionManager> 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; 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; FString GetScenesDirectory() const;
}; };

View File

@@ -77,6 +77,14 @@ struct FPS_Editor_BaseLevelEntry
*/ */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
TArray<FString> AdditionalSublevels; TArray<FString> 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<TObjectPtr<UPS_Editor_SpawnCatalog>> Catalogs;
}; };
/** /**

View File

@@ -14,6 +14,11 @@ void UPS_Editor_SpawnManager::Initialize(APlayerController* InOwnerPC)
OwnerPC = InOwnerPC; OwnerPC = InOwnerPC;
} }
void UPS_Editor_SpawnManager::ClearCatalogs()
{
ResolvedEntries.Empty();
}
void UPS_Editor_SpawnManager::AddCatalog(UPS_Editor_SpawnCatalog* InCatalog) void UPS_Editor_SpawnManager::AddCatalog(UPS_Editor_SpawnCatalog* InCatalog)
{ {
if (!InCatalog) return; if (!InCatalog) return;

View File

@@ -42,6 +42,9 @@ public:
/** Load and add a catalog. Can be called multiple times for multiple catalogs. */ /** Load and add a catalog. Can be called multiple times for multiple catalogs. */
void AddCatalog(UPS_Editor_SpawnCatalog* InCatalog); 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. */ /** Spawn the actor at the given catalog index, positioned in front of the camera. Returns the spawned actor. */
AActor* SpawnFromCatalog(int32 Index); AActor* SpawnFromCatalog(int32 Index);

View File

@@ -745,8 +745,8 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
ParentWindowHandle, ParentWindowHandle,
TEXT("Save Scenario As"), TEXT("Save Scenario As"),
DefaultPath, DefaultPath,
SaveNameField.IsValid() ? SaveNameField->GetText().ToString() + TEXT(".json") : TEXT(""), SaveNameField.IsValid() ? SaveNameField->GetText().ToString() + TEXT(".pss") : TEXT(""),
TEXT("Scenario Files (*.json)|*.json"), TEXT("Scenario Files (*.pss)|*.pss"),
0, 0,
OutFiles); OutFiles);
@@ -937,7 +937,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
TEXT("Load Scenario"), TEXT("Load Scenario"),
DefaultPath, DefaultPath,
TEXT(""), TEXT(""),
TEXT("Scenario Files (*.json)|*.json"), TEXT("Scenario Files (*.pss)|*.pss"),
0, 0,
OutFiles); OutFiles);
@@ -2634,6 +2634,7 @@ void UPS_Editor_MainWidget::ConfirmBaseLevelSelection()
} }
PopulateSceneList(); PopulateSceneList();
PopulateSpawnCatalog();
LastOutlinerActorCount = -1; LastOutlinerActorCount = -1;
PendingBaseLevelSelection.Empty(); PendingBaseLevelSelection.Empty();
} }

View File

@@ -12,7 +12,16 @@ public class PS_EditorTools : ModuleRules
"CoreUObject", "CoreUObject",
"Engine", "Engine",
"PS_Editor", "PS_Editor",
"UnrealEd" "UnrealEd",
"ContentBrowser",
"AssetRegistry",
"AssetTools",
"Kismet",
"KismetCompiler",
"BlueprintGraph",
"ToolMenus",
"Slate",
"SlateCore"
}); });
} }
} }

View File

@@ -1,3 +1,14 @@
#include "PS_EditorTools.h" #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) IMPLEMENT_MODULE(FPS_EditorToolsModule, PS_EditorTools)

View File

@@ -5,6 +5,6 @@
class FPS_EditorToolsModule : public IModuleInterface class FPS_EditorToolsModule : public IModuleInterface
{ {
public: public:
virtual void StartupModule() override {} virtual void StartupModule() override;
virtual void ShutdownModule() override {} virtual void ShutdownModule() override;
}; };

View File

@@ -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<FExtender> OnExtendContentBrowserAssetSelectionMenu(const TArray<FAssetData>& SelectedAssets)
{
TSharedRef<FExtender> 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<UObject*> 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<FContentBrowserModule>("ContentBrowser");
TArray<FContentBrowserMenuExtender_SelectedAssets>& Extenders = ContentBrowserModule.GetAllAssetViewContextMenuExtenders();
Extenders.Add(FContentBrowserMenuExtender_SelectedAssets::CreateStatic(&OnExtendContentBrowserAssetSelectionMenu));
ContentBrowserExtenderHandle = Extenders.Last().GetHandle();
}
void FPS_Editor_BlueprintFromMesh::Unregister()
{
if (FContentBrowserModule* ContentBrowserModule = FModuleManager::GetModulePtr<FContentBrowserModule>("ContentBrowser"))
{
TArray<FContentBrowserMenuExtender_SelectedAssets>& Extenders = ContentBrowserModule->GetAllAssetViewContextMenuExtenders();
Extenders.RemoveAll([](const FContentBrowserMenuExtender_SelectedAssets& Delegate)
{
return Delegate.GetHandle() == ContentBrowserExtenderHandle;
});
}
}
void FPS_Editor_BlueprintFromMesh::OnCreateBlueprintFromMesh(TArray<UObject*> SelectedAssets)
{
for (UObject* Asset : SelectedAssets)
{
CreateBlueprintForAsset(Asset);
}
}
void FPS_Editor_BlueprintFromMesh::CreateBlueprintForAsset(UObject* MeshAsset)
{
if (!MeshAsset) return;
UStaticMesh* StaticMesh = Cast<UStaticMesh>(MeshAsset);
USkeletalMesh* SkelMesh = Cast<USkeletalMesh>(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<UObject>(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<UStaticMeshComponent>(MeshNode->ComponentTemplate))
{
MeshTemplate->SetStaticMesh(StaticMesh);
}
}
else if (SkelMesh)
{
MeshNode = SCS->CreateNode(USkeletalMeshComponent::StaticClass(), FName("Mesh"));
if (USkeletalMeshComponent* MeshTemplate = Cast<USkeletalMeshComponent>(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<UPS_Editor_SpawnableComponent>(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);
}

View File

@@ -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<UObject*> SelectedAssets);
/** Create a single Blueprint from the given mesh asset. */
static void CreateBlueprintForAsset(UObject* MeshAsset);
};