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:
@@ -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<ULevelStreamingDynamic>& Extra : AdditionalSublevels)
|
||||
{
|
||||
|
||||
@@ -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<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> SceneNames;
|
||||
const FString Directory = GetScenesDirectory();
|
||||
const FString Root = GetScenesDirectory();
|
||||
IFileManager& FM = IFileManager::Get();
|
||||
|
||||
if (!LevelFilter.IsEmpty())
|
||||
{
|
||||
// Scan only the subdirectory for this level (.pss + legacy .json)
|
||||
const FString LevelDir = FPaths::Combine(Root, LevelFilter);
|
||||
TArray<FString> Files;
|
||||
IFileManager::Get().FindFiles(Files, *FPaths::Combine(Directory, TEXT("*.json")), true, false);
|
||||
|
||||
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)
|
||||
{
|
||||
const FString Name = FPaths::GetBaseFilename(File);
|
||||
|
||||
if (LevelFilter.IsEmpty())
|
||||
{
|
||||
SceneNames.Add(Name);
|
||||
SceneNames.AddUnique(FPaths::GetBaseFilename(File));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Quick check: read the LevelName field from JSON without full parse
|
||||
const FString LevelName = GetSceneLevelName(Name);
|
||||
if (LevelName == LevelFilter)
|
||||
// Scan all subdirectories
|
||||
TArray<FString> SubDirs;
|
||||
FM.FindFiles(SubDirs, *FPaths::Combine(Root, TEXT("*")), false, true);
|
||||
for (const FString& SubDir : SubDirs)
|
||||
{
|
||||
SceneNames.Add(Name);
|
||||
TArray<FString> 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.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)
|
||||
{
|
||||
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<FString> 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("");
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<FString> UPS_Editor_SceneSerializer::GetSavedSceneNames() const
|
||||
const FString Directory = GetScenesDirectory();
|
||||
|
||||
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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -40,9 +40,12 @@ private:
|
||||
UPROPERTY()
|
||||
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;
|
||||
|
||||
/** 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;
|
||||
};
|
||||
|
||||
@@ -77,6 +77,14 @@ struct FPS_Editor_BaseLevelEntry
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -745,8 +745,8 @@ TSharedRef<SWidget> 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<SWidget> 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();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,16 @@ public class PS_EditorTools : ModuleRules
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"PS_Editor",
|
||||
"UnrealEd"
|
||||
"UnrealEd",
|
||||
"ContentBrowser",
|
||||
"AssetRegistry",
|
||||
"AssetTools",
|
||||
"Kismet",
|
||||
"KismetCompiler",
|
||||
"BlueprintGraph",
|
||||
"ToolMenus",
|
||||
"Slate",
|
||||
"SlateCore"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user