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()
UPROPERTY()
int32 Version = 2;
int32 Version = 3;
UPROPERTY()
FString SceneName;
/** The base level this scene was built on (e.g. "Warehouse", "Office_Floor2"). */
UPROPERTY()
FString LevelName;
UPROPERTY()
FString Timestamp;

View File

@@ -169,7 +169,7 @@ FString UPS_Editor_SceneLoader::GetSceneFilePath(const FString& SceneName)
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;
const FString Directory = GetScenesDirectory();
@@ -179,9 +179,54 @@ TArray<FString> UPS_Editor_SceneLoader::GetSavedSceneNames()
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();
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);
/**
* 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")
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:
/** 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.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();
for (const TWeakObjectPtr<AActor>& Weak : SpawnedActors)
{

View File

@@ -2,6 +2,7 @@
#include "PS_Editor_SelectionManager.h"
#include "PS_Editor_SpawnManager.h"
#include "PS_Editor_SceneSerializer.h"
#include "PS_Editor_SceneLoader.h"
#include "PS_Editor_UndoManager.h"
#include "PS_Editor_PlayerController.h"
#include "PS_Editor_EditableComponent.h"
@@ -569,8 +570,35 @@ void UPS_Editor_MainWidget::PopulateSceneList()
UPS_Editor_SceneSerializer* Ser = SceneSerializer.Get();
if (!Ser) return;
TArray<FString> Scenes = Ser->GetSavedSceneNames();
if (Scenes.Num() == 0)
// Get current level name for filtering
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()
.AutoHeight()
@@ -583,41 +611,98 @@ void UPS_Editor_MainWidget::PopulateSceneList()
return;
}
SceneListContainer->AddSlot()
.AutoHeight()
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Load scene:")))
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
.ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f))
];
for (const FString& SceneName : Scenes)
// Show scenes for current level first
if (LevelScenes.Num() > 0)
{
FString Name = SceneName;
SceneListContainer->AddSlot()
.AutoHeight()
.Padding(0.0f, 1.0f)
.Padding(0.0f, 2.0f, 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", 10))
]
SNew(STextBlock)
.Text(FText::FromString(TEXT("This level:")))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 9))
.ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f))
];
for (const FString& SceneName : LevelScenes)
{
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", 10))
]
];
}
}
// 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))
]
];
}
}
}