From 4dc0f400b43bed38808a43e628cb31156da4070e Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Sun, 12 Apr 2026 11:43:00 +0200 Subject: [PATCH] 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) --- .../Serialization/PS_Editor_SceneData.h | 6 +- .../Serialization/PS_Editor_SceneLoader.cpp | 49 +++++- .../Serialization/PS_Editor_SceneLoader.h | 17 +- .../PS_Editor_SceneSerializer.cpp | 10 ++ .../UI/Widgets/PS_Editor_MainWidget.cpp | 149 ++++++++++++++---- 5 files changed, 194 insertions(+), 37 deletions(-) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h index 6667745..c31dbf1 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h @@ -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; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp index e37e75a..ef458b6 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp @@ -169,7 +169,7 @@ FString UPS_Editor_SceneLoader::GetSceneFilePath(const FString& SceneName) return FPaths::Combine(GetScenesDirectory(), SceneName + TEXT(".json")); } -TArray UPS_Editor_SceneLoader::GetSavedSceneNames() +TArray UPS_Editor_SceneLoader::GetSavedSceneNames(const FString& LevelFilter) { TArray SceneNames; const FString Directory = GetScenesDirectory(); @@ -179,9 +179,54 @@ TArray 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 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); +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.h index 9a654bf..36d420d 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.h @@ -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 GetSavedSceneNames(); + static TArray 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 GetSavedSceneNamesForCurrentLevel(const UObject* WorldContextObject); private: /** Spawn a single actor from saved data and apply all properties. */ diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp index 1a71fef..40be6c3 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp @@ -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(GetOuter())) + { + if (UWorld* World = PC->GetWorld()) + { + SceneData.LevelName = World->GetMapName(); + SceneData.LevelName.RemoveFromStart(World->StreamingLevelsPrefix); + } + } + const TArray>& SpawnedActors = SpawnManager->GetSpawnedActors(); for (const TWeakObjectPtr& Weak : SpawnedActors) { diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp index c9301be..24fec77 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp @@ -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 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 LevelScenes = UPS_Editor_SceneLoader::GetSavedSceneNames(CurrentLevel); + TArray 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 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)) + ] + ]; + } } }