From 36b04bee61efadef08d3a44d963ab94e7e077a6b Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Fri, 10 Apr 2026 20:31:15 +0200 Subject: [PATCH] Add Phase 4: JSON scene save/load system Serialization: - SceneData USTRUCT with Version, SceneName, Timestamp, Actors array - ActorData: ClassPath, Location, Rotation, Scale per spawned actor - SceneSerializer: SaveScene/LoadScene/ClearScene/GetSavedSceneNames - Files stored in Saved/PS_Editor/Scenes/{name}.json - Uses FJsonObjectConverter for clean USTRUCT-to-JSON conversion SpawnManager: - Tracks all spawned actors in SpawnedActors array - SpawnActorDirect: spawn from class/transform (used by scene loading) - DestroyAllSpawnedActors: clean scene clear UI: - Save field + button in toolbar area - New Scene button to clear all spawned actors - Saved scenes list with click-to-load - Auto-refreshes scene list after save - Clears selection on load/new Co-Authored-By: Claude Opus 4.6 (1M context) --- .../PS_Editor/Content/Cubeezrzea.uasset | Bin 29846 -> 30063 bytes .../GameMode/PS_Editor_PlayerController.cpp | 4 + .../GameMode/PS_Editor_PlayerController.h | 7 + .../Source/PS_Editor/PS_Editor.Build.cs | 5 +- .../Serialization/PS_Editor_SceneData.h | 43 +++++ .../PS_Editor_SceneSerializer.cpp | 165 ++++++++++++++++++ .../Serialization/PS_Editor_SceneSerializer.h | 48 +++++ .../Spawn/PS_Editor_SpawnManager.cpp | 45 +++++ .../PS_Editor/Spawn/PS_Editor_SpawnManager.h | 13 ++ .../Source/PS_Editor/UI/PS_Editor_HUD.cpp | 1 + .../UI/Widgets/PS_Editor_MainWidget.cpp | 156 +++++++++++++++++ .../UI/Widgets/PS_Editor_MainWidget.h | 11 ++ 12 files changed, 497 insertions(+), 1 deletion(-) create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.h diff --git a/Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset b/Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset index 5b3fc780d2db6b69101bdda25cc5523fc2904e53..6a3523d4d7d45096c56ca80b4662355b7eece662 100644 GIT binary patch delta 4534 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 IV;kl2zvF#882|tP delta 4344 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 diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp index d3196e6..859987c 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp @@ -2,6 +2,7 @@ #include "PS_Editor_SelectionManager.h" #include "PS_Editor_UndoManager.h" #include "PS_Editor_SpawnManager.h" +#include "PS_Editor_SceneSerializer.h" #include "PS_Editor_Gizmo.h" #include "PS_Editor_Pawn.h" @@ -28,6 +29,9 @@ void APS_Editor_PlayerController::BeginPlay() UPS_Editor_SpawnCatalog* LoadedCatalog = SpawnCatalogAsset.LoadSynchronous(); SpawnManager->Initialize(this, LoadedCatalog); + SceneSerializer = NewObject(this); + SceneSerializer->SetSelectionManager(SelectionManager); + // After undo/redo, deselect hidden actors and refresh gizmo UndoManager->OnUndoRedo.AddWeakLambda(this, [this]() { diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h index 37385e7..1be1722 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h @@ -8,6 +8,7 @@ class UPS_Editor_SelectionManager; class UPS_Editor_UndoManager; class UPS_Editor_SpawnManager; class UPS_Editor_SpawnCatalog; +class UPS_Editor_SceneSerializer; /** * Player controller for PS_Editor runtime editing. @@ -30,6 +31,9 @@ public: UFUNCTION(BlueprintCallable, Category = "PS Editor") UPS_Editor_SpawnManager* GetSpawnManager() const { return SpawnManager; } + UFUNCTION(BlueprintCallable, Category = "PS Editor") + UPS_Editor_SceneSerializer* GetSceneSerializer() const { return SceneSerializer; } + /** The spawn catalog DataAsset to load. Set this in the Blueprint or World Settings. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor") TSoftObjectPtr SpawnCatalogAsset; @@ -48,5 +52,8 @@ private: UPROPERTY() TObjectPtr SpawnManager; + UPROPERTY() + TObjectPtr SceneSerializer; + void HandleEditorClick(FVector2D ScreenPos, bool bAdditive); }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs index 4b34e74..cb2a12f 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs @@ -16,6 +16,7 @@ public class PS_Editor : ModuleRules PublicIncludePaths.Add(Path.Combine(SourceDir, "Selection")); PublicIncludePaths.Add(Path.Combine(SourceDir, "Gizmo")); PublicIncludePaths.Add(Path.Combine(SourceDir, "Spawn")); + PublicIncludePaths.Add(Path.Combine(SourceDir, "Serialization")); PublicDependencyModuleNames.AddRange(new string[] { @@ -24,7 +25,9 @@ public class PS_Editor : ModuleRules "Engine", "InputCore", "EnhancedInput", - "UMG" + "UMG", + "Json", + "JsonUtilities" }); PrivateDependencyModuleNames.AddRange(new string[] 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 new file mode 100644 index 0000000..4627fc4 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h @@ -0,0 +1,43 @@ +#pragma once + +#include "CoreMinimal.h" +#include "PS_Editor_SceneData.generated.h" + +/** Serialized data for a single spawned actor. */ +USTRUCT() +struct FPS_Editor_ActorData +{ + GENERATED_BODY() + + /** Class path (e.g. "/Game/BP_Cube.BP_Cube_C"). */ + UPROPERTY() + FString ClassPath; + + UPROPERTY() + FVector Location = FVector::ZeroVector; + + UPROPERTY() + FRotator Rotation = FRotator::ZeroRotator; + + UPROPERTY() + FVector Scale = FVector::OneVector; +}; + +/** Serialized data for a complete scene. */ +USTRUCT() +struct FPS_Editor_SceneData +{ + GENERATED_BODY() + + UPROPERTY() + int32 Version = 1; + + UPROPERTY() + FString SceneName; + + UPROPERTY() + FString Timestamp; + + UPROPERTY() + TArray Actors; +}; 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 new file mode 100644 index 0000000..3f9eeaf --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp @@ -0,0 +1,165 @@ +#include "PS_Editor_SceneSerializer.h" +#include "PS_Editor_SpawnManager.h" +#include "PS_Editor_SelectionManager.h" +#include "JsonObjectConverter.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "HAL/PlatformFileManager.h" + +bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager) +{ + if (!SpawnManager || SceneName.IsEmpty()) + { + return false; + } + + // Build scene data from currently spawned actors + FPS_Editor_SceneData SceneData; + SceneData.SceneName = SceneName; + SceneData.Timestamp = FDateTime::Now().ToString(); + + const TArray>& SpawnedActors = SpawnManager->GetSpawnedActors(); + for (const TWeakObjectPtr& Weak : SpawnedActors) + { + AActor* Actor = Weak.Get(); + if (!Actor || Actor->IsHidden()) + { + continue; // Skip destroyed or soft-deleted actors + } + + FPS_Editor_ActorData ActorData; + ActorData.ClassPath = Actor->GetClass()->GetPathName(); + ActorData.Location = Actor->GetActorLocation(); + ActorData.Rotation = Actor->GetActorRotation(); + ActorData.Scale = Actor->GetActorScale3D(); + + SceneData.Actors.Add(ActorData); + } + + // Convert to JSON + FString JsonString; + if (!FJsonObjectConverter::UStructToJsonObjectString(SceneData, JsonString, 0, 0, 0, nullptr, true)) + { + UE_LOG(LogTemp, Error, TEXT("PS_Editor: Failed to serialize scene to JSON")); + return false; + } + + // Ensure directory exists + const FString Directory = GetScenesDirectory(); + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + if (!PlatformFile.DirectoryExists(*Directory)) + { + PlatformFile.CreateDirectoryTree(*Directory); + } + + // 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); + return false; + } + + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scene saved: %s (%d actors) -> %s"), *SceneName, SceneData.Actors.Num(), *FilePath); + return true; +} + +bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager) +{ + if (!SpawnManager || SceneName.IsEmpty()) + { + return false; + } + + // Read file + const FString FilePath = GetSceneFilePath(SceneName); + FString JsonString; + if (!FFileHelper::LoadFileToString(JsonString, *FilePath)) + { + UE_LOG(LogTemp, Error, TEXT("PS_Editor: Failed to read scene file: %s"), *FilePath); + return false; + } + + // Parse JSON + FPS_Editor_SceneData SceneData; + if (!FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &SceneData, 0, 0)) + { + UE_LOG(LogTemp, Error, TEXT("PS_Editor: Failed to parse scene JSON")); + return false; + } + + // Clear existing spawned actors + ClearScene(SpawnManager); + + // Re-spawn actors from saved data + int32 SpawnedCount = 0; + for (const FPS_Editor_ActorData& ActorData : SceneData.Actors) + { + UClass* LoadedClass = LoadObject(nullptr, *ActorData.ClassPath); + if (!LoadedClass) + { + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to load class: %s"), *ActorData.ClassPath); + continue; + } + + AActor* SpawnedActor = SpawnManager->SpawnActorDirect(LoadedClass, ActorData.Location, ActorData.Rotation, ActorData.Scale); + if (SpawnedActor) + { + SpawnedCount++; + } + } + + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scene loaded: %s (%d actors)"), *SceneName, SpawnedCount); + return true; +} + +TArray UPS_Editor_SceneSerializer::GetSavedSceneNames() const +{ + TArray SceneNames; + const FString Directory = GetScenesDirectory(); + + TArray Files; + IFileManager::Get().FindFiles(Files, *FPaths::Combine(Directory, TEXT("*.json")), true, false); + + for (const FString& File : Files) + { + SceneNames.Add(FPaths::GetBaseFilename(File)); + } + + SceneNames.Sort(); + return SceneNames; +} + +bool UPS_Editor_SceneSerializer::DeleteScene(const FString& SceneName) +{ + const FString FilePath = GetSceneFilePath(SceneName); + if (IFileManager::Get().Delete(*FilePath)) + { + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scene deleted: %s"), *SceneName); + return true; + } + return false; +} + +void UPS_Editor_SceneSerializer::ClearScene(UPS_Editor_SpawnManager* SpawnManager) +{ + if (UPS_Editor_SelectionManager* SM = SelectionManagerRef.Get()) + { + SM->ClearSelection(); + } + + if (SpawnManager) + { + SpawnManager->DestroyAllSpawnedActors(); + } +} + +FString UPS_Editor_SceneSerializer::GetSceneFilePath(const FString& SceneName) const +{ + return FPaths::Combine(GetScenesDirectory(), SceneName + TEXT(".json")); +} + +FString UPS_Editor_SceneSerializer::GetScenesDirectory() const +{ + return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("PS_Editor"), TEXT("Scenes")); +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.h new file mode 100644 index 0000000..34a2d38 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.h @@ -0,0 +1,48 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "PS_Editor_SceneData.h" +#include "PS_Editor_SceneSerializer.generated.h" + +class UPS_Editor_SpawnManager; +class UPS_Editor_SelectionManager; + +/** + * Handles saving and loading PS_Editor scenes as JSON files. + * Scenes are stored in {ProjectDir}/Saved/PS_Editor/Scenes/{SceneName}.json + */ +UCLASS() +class PS_EDITOR_API UPS_Editor_SceneSerializer : public UObject +{ + GENERATED_BODY() + +public: + /** Save the current spawned actors to a JSON file. */ + bool SaveScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager); + + /** Load a scene from a JSON file. Destroys current spawned actors and re-spawns from file. */ + bool LoadScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager); + + /** Get a list of all saved scene names (without .json extension). */ + TArray GetSavedSceneNames() const; + + /** Delete a saved scene file. */ + bool DeleteScene(const FString& SceneName); + + /** Clear all spawned actors (new scene). Also clears selection and undo history. */ + void ClearScene(UPS_Editor_SpawnManager* SpawnManager); + + /** Set optional selection manager to clear on load/new. */ + void SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager) { SelectionManagerRef = InSelectionManager; } + +private: + UPROPERTY() + TWeakObjectPtr SelectionManagerRef; + + /** Get the full file path for a scene name. */ + FString GetSceneFilePath(const FString& SceneName) const; + + /** Get the directory where scenes are stored. */ + FString GetScenesDirectory() const; +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp index 67c6673..84ccec2 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp @@ -138,6 +138,9 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector } } + // Track spawned actor + SpawnedActors.Add(SpawnedActor); + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spawned %s at %s"), *Entry.DisplayName, *WorldLocation.ToString()); return SpawnedActor; } @@ -158,6 +161,48 @@ FVector UPS_Editor_SpawnManager::ComputeSpawnLocation() const return CameraLocation + CameraRotation.Vector() * 500.0f; } +AActor* UPS_Editor_SpawnManager::SpawnActorDirect(UClass* ActorClass, FVector Location, FRotator Rotation, FVector Scale) +{ + APlayerController* PC = OwnerPC.Get(); + if (!PC || !PC->GetWorld() || !ActorClass) + { + return nullptr; + } + + FActorSpawnParameters SpawnParams; + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn; + + AActor* SpawnedActor = PC->GetWorld()->SpawnActor(ActorClass, Location, Rotation, SpawnParams); + if (!SpawnedActor) + { + return nullptr; + } + + if (USceneComponent* Root = SpawnedActor->GetRootComponent()) + { + Root->SetMobility(EComponentMobility::Movable); + } + + SpawnedActor->SetActorScale3D(Scale); + SpawnedActors.Add(SpawnedActor); + + return SpawnedActor; +} + +void UPS_Editor_SpawnManager::DestroyAllSpawnedActors() +{ + for (const TWeakObjectPtr& Weak : SpawnedActors) + { + if (AActor* Actor = Weak.Get()) + { + Actor->Destroy(); + } + } + SpawnedActors.Empty(); + + UE_LOG(LogTemp, Log, TEXT("PS_Editor: All spawned actors destroyed")); +} + TArray UPS_Editor_SpawnManager::GetCategories() const { TArray Categories; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h index 2dc9ed8..b45321e 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h @@ -45,6 +45,15 @@ public: /** Spawn the actor at a specific world location. */ AActor* SpawnFromCatalogAtLocation(int32 Index, FVector WorldLocation); + /** Spawn an actor directly from a class (used by scene loading). No undo recorded. */ + AActor* SpawnActorDirect(UClass* ActorClass, FVector Location, FRotator Rotation, FVector Scale); + + /** Destroy all spawned actors (for scene clear/load). */ + void DestroyAllSpawnedActors(); + + /** Get all currently spawned actors. */ + const TArray>& GetSpawnedActors() const { return SpawnedActors; } + /** Get the resolved catalog entries for UI display. */ const TArray& GetResolvedEntries() const { return ResolvedEntries; } @@ -61,6 +70,10 @@ private: UPROPERTY() TArray ResolvedEntries; + /** All actors spawned by this manager (tracked for save/load). */ + UPROPERTY() + TArray> SpawnedActors; + /** Resolve all catalog entries (load classes, read SpawnableComponent metadata). */ void ResolveCatalog(); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp index 5bc7129..70dccf3 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp @@ -18,6 +18,7 @@ void APS_Editor_HUD::BeginPlay() { MainWidget->SetSelectionManager(EditorPC->GetSelectionManager()); MainWidget->SetSpawnManager(EditorPC->GetSpawnManager()); + MainWidget->SetSceneSerializer(EditorPC->GetSceneSerializer()); } MainWidget->AddToViewport(0); 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 5222842..0c09984 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 @@ -1,6 +1,7 @@ #include "PS_Editor_MainWidget.h" #include "PS_Editor_SelectionManager.h" #include "PS_Editor_SpawnManager.h" +#include "PS_Editor_SceneSerializer.h" #include "PS_Editor_UndoManager.h" #include "PS_Editor_PlayerController.h" #include "Widgets/SBoxPanel.h" @@ -23,6 +24,11 @@ void UPS_Editor_MainWidget::SetSpawnManager(UPS_Editor_SpawnManager* InSpawnMana SpawnManager = InSpawnManager; } +void UPS_Editor_MainWidget::SetSceneSerializer(UPS_Editor_SceneSerializer* InSerializer) +{ + SceneSerializer = InSerializer; +} + TSharedRef UPS_Editor_MainWidget::RebuildWidget() { return SNew(SOverlay) @@ -70,6 +76,13 @@ TSharedRef UPS_Editor_MainWidget::RebuildWidget() [ BuildToolbar() ] + // Scene save/load + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 4.0f, 0.0f, 0.0f) + [ + BuildSceneButtons() + ] ] // Left panel: Spawn Catalogue + SOverlay::Slot() @@ -258,6 +271,88 @@ TSharedRef UPS_Editor_MainWidget::BuildPropertiesPanel() ]; } +TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() +{ + return SNew(SBorder) + .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.85f)) + .Padding(FMargin(8.0f, 4.0f)) + [ + SNew(SVerticalBox) + // Save row: text field + save button + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 2.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .Padding(0.0f, 0.0f, 4.0f, 0.0f) + [ + SAssignNew(SaveNameField, SEditableTextBox) + .HintText(FText::FromString(TEXT("Scene name..."))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + if (SaveNameField.IsValid() && SceneSerializer.IsValid() && SpawnManager.IsValid()) + { + FString Name = SaveNameField->GetText().ToString(); + if (!Name.IsEmpty()) + { + SceneSerializer->SaveScene(Name, SpawnManager.Get()); + // Refresh scene list + PopulateSceneList(); + } + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Save"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + ] + ] + ] + // Action buttons: New + Load list + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 2.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0.0f, 0.0f, 4.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + if (SceneSerializer.IsValid() && SpawnManager.IsValid()) + { + SceneSerializer->ClearScene(SpawnManager.Get()); + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("New Scene"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + ] + ] + ] + // Saved scenes list + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 4.0f, 0.0f, 0.0f) + [ + SAssignNew(SceneListContainer, SVerticalBox) + ] + ]; +} + TSharedRef UPS_Editor_MainWidget::BuildSpawnCatalog() { TSharedRef CatalogList = SNew(SVerticalBox); @@ -302,6 +397,67 @@ void UPS_Editor_MainWidget::NativeConstruct() { Super::NativeConstruct(); PopulateSpawnCatalog(); + PopulateSceneList(); +} + +void UPS_Editor_MainWidget::PopulateSceneList() +{ + if (!SceneListContainer.IsValid()) return; + SceneListContainer->ClearChildren(); + + UPS_Editor_SceneSerializer* Ser = SceneSerializer.Get(); + if (!Ser) return; + + TArray Scenes = Ser->GetSavedSceneNames(); + if (Scenes.Num() == 0) + { + SceneListContainer->AddSlot() + .AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("No saved scenes"))) + .Font(FCoreStyle::GetDefaultFontStyle("Italic", 9)) + .ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f)) + ]; + 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) + { + 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)) + ] + ]; + } } void UPS_Editor_MainWidget::PopulateSpawnCatalog() diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h index b737501..3a0e396 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h @@ -7,6 +7,7 @@ class UPS_Editor_SelectionManager; class UPS_Editor_SpawnManager; +class UPS_Editor_SceneSerializer; /** * Main editor widget for PS_Editor. @@ -22,6 +23,7 @@ public: /** Set manager references. Must be called after construction. */ void SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager); void SetSpawnManager(UPS_Editor_SpawnManager* InSpawnManager); + void SetSceneSerializer(UPS_Editor_SceneSerializer* InSerializer); protected: virtual TSharedRef RebuildWidget() override; @@ -35,12 +37,19 @@ private: UPROPERTY() TWeakObjectPtr SpawnManager; + UPROPERTY() + TWeakObjectPtr SceneSerializer; + // ---- Slate widget refs for dynamic updates ---- TSharedPtr ModeText; TSharedPtr SelectionInfoText; // Spawn catalog TSharedPtr SpawnListContainer; + + // Scene save/load + TSharedPtr SaveNameField; + TSharedPtr SceneListContainer; TArray> CachedBrushes; // Transform fields @@ -52,6 +61,8 @@ private: TSharedRef BuildToolbar(); TSharedRef BuildPropertiesPanel(); TSharedRef BuildSpawnCatalog(); + TSharedRef BuildSceneButtons(); + void PopulateSceneList(); void PopulateSpawnCatalog(); void RefreshPropertiesFromSelection(); void ApplyTransformFromUI();