Add level-based scene organization and filtering

SceneData now includes LevelName field (auto-detected from current map on save).
Version bumped to 3. Backward compatible (empty LevelName for old files).

SceneLoader:
- GetSavedSceneNames(LevelFilter) filters by associated level
- GetSceneLevelName() reads level association without full parse
- GetSavedSceneNamesForCurrentLevel() convenience for game code

Editor UI:
- Scene list grouped: "This level" (highlighted) + "Other levels" (dimmed)
- Shows current level name above the list

PROSERVE integration pattern:
  // List scenarios for current level
  auto Scenes = UPS_Editor_SceneLoader::GetSavedSceneNamesForCurrentLevel(this);
  // Load a specific scenario
  UPS_Editor_SceneLoader::LoadScene(this, "Scenario_A", OutActors);

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 11:43:00 +02:00
parent 77f2da027e
commit 4dc0f400b4
5 changed files with 194 additions and 37 deletions

View File

@@ -34,11 +34,15 @@ struct FPS_Editor_SceneData
GENERATED_BODY() GENERATED_BODY()
UPROPERTY() UPROPERTY()
int32 Version = 2; int32 Version = 3;
UPROPERTY() UPROPERTY()
FString SceneName; FString SceneName;
/** The base level this scene was built on (e.g. "Warehouse", "Office_Floor2"). */
UPROPERTY()
FString LevelName;
UPROPERTY() UPROPERTY()
FString Timestamp; FString Timestamp;

View File

@@ -169,7 +169,7 @@ FString UPS_Editor_SceneLoader::GetSceneFilePath(const FString& SceneName)
return FPaths::Combine(GetScenesDirectory(), SceneName + TEXT(".json")); return FPaths::Combine(GetScenesDirectory(), SceneName + TEXT(".json"));
} }
TArray<FString> UPS_Editor_SceneLoader::GetSavedSceneNames() TArray<FString> UPS_Editor_SceneLoader::GetSavedSceneNames(const FString& LevelFilter)
{ {
TArray<FString> SceneNames; TArray<FString> SceneNames;
const FString Directory = GetScenesDirectory(); const FString Directory = GetScenesDirectory();
@@ -179,9 +179,54 @@ TArray<FString> UPS_Editor_SceneLoader::GetSavedSceneNames()
for (const FString& File : Files) for (const FString& File : Files)
{ {
SceneNames.Add(FPaths::GetBaseFilename(File)); const FString Name = FPaths::GetBaseFilename(File);
if (LevelFilter.IsEmpty())
{
SceneNames.Add(Name);
}
else
{
// Quick check: read the LevelName field from JSON without full parse
const FString LevelName = GetSceneLevelName(Name);
if (LevelName == LevelFilter)
{
SceneNames.Add(Name);
}
}
} }
SceneNames.Sort(); SceneNames.Sort();
return SceneNames; return SceneNames;
} }
FString UPS_Editor_SceneLoader::GetSceneLevelName(const FString& SceneName)
{
const FString FilePath = GetSceneFilePath(SceneName);
FString JsonString;
if (!FFileHelper::LoadFileToString(JsonString, *FilePath))
{
return TEXT("");
}
// Parse just the header (lightweight)
FPS_Editor_SceneData SceneData;
if (FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &SceneData, 0, 0))
{
return SceneData.LevelName;
}
return TEXT("");
}
TArray<FString> UPS_Editor_SceneLoader::GetSavedSceneNamesForCurrentLevel(const UObject* WorldContextObject)
{
if (!WorldContextObject) return {};
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
if (!World) return {};
FString LevelName = World->GetMapName();
LevelName.RemoveFromStart(World->StreamingLevelsPrefix);
return GetSavedSceneNames(LevelName);
}

View File

@@ -57,10 +57,23 @@ public:
static FString GetSceneFilePath(const FString& SceneName); static FString GetSceneFilePath(const FString& SceneName);
/** /**
* Get all saved scene names. * Get all saved scene names. Optionally filter by level name.
* @param LevelFilter If not empty, only return scenes associated with this level.
*/ */
UFUNCTION(BlueprintCallable, Category = "PS Editor") UFUNCTION(BlueprintCallable, Category = "PS Editor")
static TArray<FString> GetSavedSceneNames(); static TArray<FString> GetSavedSceneNames(const FString& LevelFilter = TEXT(""));
/**
* Get the level name associated with a saved scene (reads the JSON header without spawning).
*/
UFUNCTION(BlueprintCallable, Category = "PS Editor")
static FString GetSceneLevelName(const FString& SceneName);
/**
* Get all saved scenes for the current level.
*/
UFUNCTION(BlueprintCallable, Category = "PS Editor", meta = (WorldContext = "WorldContextObject"))
static TArray<FString> GetSavedSceneNamesForCurrentLevel(const UObject* WorldContextObject);
private: private:
/** Spawn a single actor from saved data and apply all properties. */ /** Spawn a single actor from saved data and apply all properties. */

View File

@@ -22,6 +22,16 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
SceneData.SceneName = SceneName; SceneData.SceneName = SceneName;
SceneData.Timestamp = FDateTime::Now().ToString(); SceneData.Timestamp = FDateTime::Now().ToString();
// Auto-detect the current level name
if (APlayerController* PC = Cast<APlayerController>(GetOuter()))
{
if (UWorld* World = PC->GetWorld())
{
SceneData.LevelName = World->GetMapName();
SceneData.LevelName.RemoveFromStart(World->StreamingLevelsPrefix);
}
}
const TArray<TWeakObjectPtr<AActor>>& SpawnedActors = SpawnManager->GetSpawnedActors(); const TArray<TWeakObjectPtr<AActor>>& SpawnedActors = SpawnManager->GetSpawnedActors();
for (const TWeakObjectPtr<AActor>& Weak : SpawnedActors) for (const TWeakObjectPtr<AActor>& Weak : SpawnedActors)
{ {

View File

@@ -2,6 +2,7 @@
#include "PS_Editor_SelectionManager.h" #include "PS_Editor_SelectionManager.h"
#include "PS_Editor_SpawnManager.h" #include "PS_Editor_SpawnManager.h"
#include "PS_Editor_SceneSerializer.h" #include "PS_Editor_SceneSerializer.h"
#include "PS_Editor_SceneLoader.h"
#include "PS_Editor_UndoManager.h" #include "PS_Editor_UndoManager.h"
#include "PS_Editor_PlayerController.h" #include "PS_Editor_PlayerController.h"
#include "PS_Editor_EditableComponent.h" #include "PS_Editor_EditableComponent.h"
@@ -569,8 +570,35 @@ void UPS_Editor_MainWidget::PopulateSceneList()
UPS_Editor_SceneSerializer* Ser = SceneSerializer.Get(); UPS_Editor_SceneSerializer* Ser = SceneSerializer.Get();
if (!Ser) return; if (!Ser) return;
TArray<FString> Scenes = Ser->GetSavedSceneNames(); // Get current level name for filtering
if (Scenes.Num() == 0) FString CurrentLevel;
if (APlayerController* PC = GetOwningPlayer())
{
if (UWorld* World = PC->GetWorld())
{
CurrentLevel = World->GetMapName();
CurrentLevel.RemoveFromStart(World->StreamingLevelsPrefix);
}
}
// Show current level
if (!CurrentLevel.IsEmpty())
{
SceneListContainer->AddSlot()
.AutoHeight()
[
SNew(STextBlock)
.Text(FText::Format(NSLOCTEXT("PS_Editor", "LevelLabel", "Level: {0}"), FText::FromString(CurrentLevel)))
.Font(FCoreStyle::GetDefaultFontStyle("Italic", 8))
.ColorAndOpacity(FLinearColor(0.4f, 0.6f, 0.4f))
];
}
// Get scenes for current level + all scenes
TArray<FString> LevelScenes = UPS_Editor_SceneLoader::GetSavedSceneNames(CurrentLevel);
TArray<FString> AllScenes = UPS_Editor_SceneLoader::GetSavedSceneNames();
if (AllScenes.Num() == 0)
{ {
SceneListContainer->AddSlot() SceneListContainer->AddSlot()
.AutoHeight() .AutoHeight()
@@ -583,16 +611,20 @@ void UPS_Editor_MainWidget::PopulateSceneList()
return; return;
} }
// Show scenes for current level first
if (LevelScenes.Num() > 0)
{
SceneListContainer->AddSlot() SceneListContainer->AddSlot()
.AutoHeight() .AutoHeight()
.Padding(0.0f, 2.0f, 0.0f, 1.0f)
[ [
SNew(STextBlock) SNew(STextBlock)
.Text(FText::FromString(TEXT("Load scene:"))) .Text(FText::FromString(TEXT("This level:")))
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) .Font(FCoreStyle::GetDefaultFontStyle("Bold", 9))
.ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f)) .ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f))
]; ];
for (const FString& SceneName : Scenes) for (const FString& SceneName : LevelScenes)
{ {
FString Name = SceneName; FString Name = SceneName;
SceneListContainer->AddSlot() SceneListContainer->AddSlot()
@@ -621,6 +653,59 @@ void UPS_Editor_MainWidget::PopulateSceneList()
} }
} }
// Show other scenes (from different levels) in a collapsed section
TArray<FString> OtherScenes;
for (const FString& S : AllScenes)
{
if (!LevelScenes.Contains(S))
{
OtherScenes.Add(S);
}
}
if (OtherScenes.Num() > 0)
{
SceneListContainer->AddSlot()
.AutoHeight()
.Padding(0.0f, 6.0f, 0.0f, 1.0f)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Other levels:")))
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 8))
.ColorAndOpacity(FLinearColor(0.4f, 0.4f, 0.4f))
];
for (const FString& SceneName : OtherScenes)
{
FString Name = SceneName;
SceneListContainer->AddSlot()
.AutoHeight()
.Padding(0.0f, 1.0f)
[
SNew(SButton)
.OnClicked_Lambda([this, Name]() -> FReply
{
if (SceneSerializer.IsValid() && SpawnManager.IsValid())
{
SceneSerializer->LoadScene(Name, SpawnManager.Get());
if (SaveNameField.IsValid())
{
SaveNameField->SetText(FText::FromString(Name));
}
}
return FReply::Handled();
})
[
SNew(STextBlock)
.Text(FText::FromString(SceneName))
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
.ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f))
]
];
}
}
}
void UPS_Editor_MainWidget::PopulateSpawnCatalog() void UPS_Editor_MainWidget::PopulateSpawnCatalog()
{ {
if (!SpawnListContainer.IsValid()) if (!SpawnListContainer.IsValid())