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"));
|
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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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("");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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