Rename LoadScene/LoadSceneAdditive to LoadScenario/UnloadScenario

The old naming suggested UE level loading; what these calls actually do
is spawn actors described by a .pss scenario file into the already-loaded
UE level. Renamed for clarity:

  SceneLoader::LoadScene         -> LoadScenario
  SceneLoader::LoadSceneFromData -> LoadScenarioFromData
  SceneLoader::LoadSceneAdditive -> dropped (merged into LoadScenario)
  SceneLoader::UnloadSceneAdditive -> UnloadScenario
  SceneSerializer::LoadScene     -> LoadScenario
  AdditiveSpawnedActors          -> TrackedSpawnedActors

LoadScenario now branches on the filter: when Filter == PreloadOnly the
timeline subsystem is skipped entirely (no SetData, no ActorId
registration, no auto-play) so a briefing scene can drop in player
placement markers without disturbing its own timeline. All other filter
values keep the full gameplay behaviour (timeline + AI-readiness poll).

Every load tracks its actors so a follow-up UnloadScenario() can destroy
them — supports the briefing flow where the player toggles between
scenarios at runtime.

Breaking change for external BP callers: any node binding the old
LoadScene / LoadSceneAdditive / UnloadSceneAdditive functions will show
up as missing on next BP compile; replace with LoadScenario /
UnloadScenario (identical or simpler signatures).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 15:42:57 +02:00
parent f6efc00c73
commit 4a98c6402b
8 changed files with 90 additions and 155 deletions

View File

@@ -138,8 +138,8 @@ void APS_Editor_PlayerController::BeginPlay()
if (!Sub->PendingScenarioName.IsEmpty() && SpawnManager && SceneSerializer) if (!Sub->PendingScenarioName.IsEmpty() && SpawnManager && SceneSerializer)
{ {
bool bLoaded = SceneSerializer->LoadScene(Sub->PendingScenarioName, SpawnManager); bool bLoaded = SceneSerializer->LoadScenario(Sub->PendingScenarioName, SpawnManager);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: LoadScene('%s') = %s"), UE_LOG(LogTemp, Log, TEXT("PS_Editor: LoadScenario('%s') = %s"),
*Sub->PendingScenarioName, bLoaded ? TEXT("OK") : TEXT("FAILED")); *Sub->PendingScenarioName, bLoaded ? TEXT("OK") : TEXT("FAILED"));
} }
Sub->PendingScenarioName.Empty(); Sub->PendingScenarioName.Empty();

View File

@@ -49,11 +49,13 @@ namespace
} }
} }
bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const FString& SceneName, TArray<AActor*>& OutActors, bool bStripEditorComponents, EPS_Editor_SceneLoadFilter Filter) TArray<TWeakObjectPtr<AActor>> UPS_Editor_SceneLoader::TrackedSpawnedActors;
bool UPS_Editor_SceneLoader::LoadScenario(const UObject* WorldContextObject, const FString& ScenarioName, TArray<AActor*>& OutActors, bool bStripEditorComponents, EPS_Editor_SceneLoadFilter Filter)
{ {
OutActors.Empty(); OutActors.Empty();
if (!WorldContextObject || SceneName.IsEmpty()) if (!WorldContextObject || ScenarioName.IsEmpty())
{ {
return false; return false;
} }
@@ -65,7 +67,7 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
} }
// Read JSON file // Read JSON file
const FString FilePath = GetSceneFilePath(SceneName); const FString FilePath = GetSceneFilePath(ScenarioName);
FString JsonString; FString JsonString;
if (!FFileHelper::LoadFileToString(JsonString, *FilePath)) if (!FFileHelper::LoadFileToString(JsonString, *FilePath))
{ {
@@ -81,11 +83,22 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
return false; return false;
} }
if (!LoadSceneFromData(World, SceneData, OutActors, Filter)) if (!LoadScenarioFromData(World, SceneData, OutActors, Filter))
{ {
return false; return false;
} }
// Track every spawned actor so UnloadScenario() can dispose of them later — this is the
// briefing-flow hook (load preload markers, then unload on scenario change). Gameplay
// callers that never unload don't pay a price beyond a small TWeakObjectPtr array.
for (AActor* Actor : OutActors)
{
if (Actor)
{
TrackedSpawnedActors.Add(Actor);
}
}
// Strip editor-only components for clean gameplay // Strip editor-only components for clean gameplay
if (bStripEditorComponents) if (bStripEditorComponents)
{ {
@@ -94,11 +107,6 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
if (!Actor) continue; if (!Actor) continue;
TArray<UActorComponent*> ToRemove; TArray<UActorComponent*> ToRemove;
for (UActorComponent* Comp : Actor->GetComponentsByInterface(UPS_Editor_PointPlaceable::StaticClass()))
{
// Don't remove — PointPlaceable is on the actor itself, not a component
}
if (UActorComponent* Spawnable = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>()) if (UActorComponent* Spawnable = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
{ {
ToRemove.Add(Spawnable); ToRemove.Add(Spawnable);
@@ -124,85 +132,10 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
return true; return true;
} }
TArray<TWeakObjectPtr<AActor>> UPS_Editor_SceneLoader::AdditiveSpawnedActors; int32 UPS_Editor_SceneLoader::UnloadScenario(const UObject* WorldContextObject)
bool UPS_Editor_SceneLoader::LoadSceneAdditive(const UObject* WorldContextObject, const FString& SceneName, EPS_Editor_SceneLoadFilter Filter, bool bStripEditorComponents)
{
if (!WorldContextObject || SceneName.IsEmpty())
{
return false;
}
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
if (!World)
{
return false;
}
// Read + parse the scenario JSON exactly like LoadScene
const FString FilePath = GetSceneFilePath(SceneName);
FString JsonString;
if (!FFileHelper::LoadFileToString(JsonString, *FilePath))
{
UE_LOG(LogTemp, Error, TEXT("PS_Editor SceneLoader (Additive): Failed to read file: %s"), *FilePath);
return false;
}
FPS_Editor_SceneData SceneData;
if (!FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &SceneData, 0, 0))
{
UE_LOG(LogTemp, Error, TEXT("PS_Editor SceneLoader (Additive): Failed to parse JSON from: %s"), *FilePath);
return false;
}
// Spawn matching actors WITHOUT touching the timeline subsystem, registering ActorIds,
// or starting any auto-play. The briefing scene drives its own timeline.
int32 SpawnedCount = 0;
bIsCurrentlyLoading = true;
for (const FPS_Editor_ActorData& ActorData : SceneData.Actors)
{
AActor* SpawnedActor = SpawnActorFromData(World, ActorData, Filter);
if (SpawnedActor)
{
AdditiveSpawnedActors.Add(SpawnedActor);
if (bStripEditorComponents)
{
TArray<UActorComponent*> ToRemove;
if (UActorComponent* Spawnable = SpawnedActor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
{
ToRemove.Add(Spawnable);
}
if (UActorComponent* Editable = SpawnedActor->FindComponentByClass<UPS_Editor_EditableComponent>())
{
ToRemove.Add(Editable);
}
for (UActorComponent* Comp : ToRemove)
{
Comp->DestroyComponent();
}
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(SpawnedActor))
{
Spline->SetVisualizationVisible(false);
}
}
SpawnedCount++;
}
}
bIsCurrentlyLoading = false;
UE_LOG(LogTemp, Log, TEXT("PS_Editor SceneLoader (Additive): Spawned %d actor(s) from '%s' [filter=%d]"),
SpawnedCount, *SceneName, (int32)Filter);
return SpawnedCount > 0;
}
int32 UPS_Editor_SceneLoader::UnloadSceneAdditive(const UObject* WorldContextObject)
{ {
int32 Destroyed = 0; int32 Destroyed = 0;
for (TWeakObjectPtr<AActor>& Weak : AdditiveSpawnedActors) for (TWeakObjectPtr<AActor>& Weak : TrackedSpawnedActors)
{ {
if (AActor* Actor = Weak.Get()) if (AActor* Actor = Weak.Get())
{ {
@@ -211,16 +144,16 @@ int32 UPS_Editor_SceneLoader::UnloadSceneAdditive(const UObject* WorldContextObj
} }
// else: already gone (GC, manual destroy, level reload) — skip silently // else: already gone (GC, manual destroy, level reload) — skip silently
} }
AdditiveSpawnedActors.Reset(); TrackedSpawnedActors.Reset();
if (Destroyed > 0) if (Destroyed > 0)
{ {
UE_LOG(LogTemp, Log, TEXT("PS_Editor SceneLoader (Additive): Unloaded %d actor(s)"), Destroyed); UE_LOG(LogTemp, Log, TEXT("PS_Editor SceneLoader: UnloadScenario destroyed %d actor(s)"), Destroyed);
} }
return Destroyed; return Destroyed;
} }
bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors, EPS_Editor_SceneLoadFilter Filter) bool UPS_Editor_SceneLoader::LoadScenarioFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors, EPS_Editor_SceneLoadFilter Filter)
{ {
if (!World) if (!World)
{ {
@@ -240,7 +173,13 @@ bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_S
// Static nav is preserved on disk for scenarios that don't need this. // Static nav is preserved on disk for scenarios that don't need this.
PS_Editor_NavUtils::SetDynamicNavigationEnabled(World, SceneData.bUseDynamicNavigation); PS_Editor_NavUtils::SetDynamicNavigationEnabled(World, SceneData.bUseDynamicNavigation);
UPS_Editor_TimelineSubsystem* TimelineSS = World->GetSubsystem<UPS_Editor_TimelineSubsystem>(); // PreloadOnly is the briefing path: only player-placement markers / briefing props
// should spawn, and the briefing scene runs its OWN timeline (don't push the scenario's
// timeline data or auto-play it). Skip the timeline subsystem entirely in that case.
const bool bSkipTimeline = (Filter == EPS_Editor_SceneLoadFilter::PreloadOnly);
UPS_Editor_TimelineSubsystem* TimelineSS = bSkipTimeline
? nullptr
: World->GetSubsystem<UPS_Editor_TimelineSubsystem>();
int32 SpawnedCount = 0; int32 SpawnedCount = 0;
@@ -250,7 +189,8 @@ bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_S
AActor* SpawnedActor = SpawnActorFromData(World, ActorData, Filter); AActor* SpawnedActor = SpawnActorFromData(World, ActorData, Filter);
if (SpawnedActor) if (SpawnedActor)
{ {
// Register the stable ID so timeline tracks can find this actor // Register the stable ID so timeline tracks can find this actor.
// Only when timeline is active for this load (PreloadOnly skips the subsystem).
if (TimelineSS && ActorData.ActorId.IsValid()) if (TimelineSS && ActorData.ActorId.IsValid())
{ {
TimelineSS->RegisterActorId(SpawnedActor, ActorData.ActorId); TimelineSS->RegisterActorId(SpawnedActor, ActorData.ActorId);
@@ -316,8 +256,8 @@ bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_S
} }
} }
UE_LOG(LogTemp, Log, TEXT("PS_Editor SceneLoader: Loaded %d actors from scene '%s'"), UE_LOG(LogTemp, Log, TEXT("PS_Editor SceneLoader: Loaded %d actor(s) from scenario '%s' [filter=%d]"),
SpawnedCount, *SceneData.SceneName); SpawnedCount, *SceneData.SceneName, (int32)Filter);
return SpawnedCount > 0; return SpawnedCount > 0;
} }

View File

@@ -5,7 +5,7 @@
#include "Kismet/BlueprintFunctionLibrary.h" #include "Kismet/BlueprintFunctionLibrary.h"
#include "PS_Editor_SceneLoader.generated.h" #include "PS_Editor_SceneLoader.generated.h"
/** Filter for selective scene loading. */ /** Filter for selective scenario loading. */
UENUM(BlueprintType) UENUM(BlueprintType)
enum class EPS_Editor_SceneLoadFilter : uint8 enum class EPS_Editor_SceneLoadFilter : uint8
{ {
@@ -15,20 +15,27 @@ enum class EPS_Editor_SceneLoadFilter : uint8
}; };
/** /**
* Standalone scene loader for PS_Editor. * Standalone scenario loader for PS_Editor.
* Loads a JSON scene file and spawns all actors with their properties and spline data. * A "scenario" is a .pss file describing actors + properties + timeline saved by the runtime
* editor. This loader spawns those actors into an already-loaded UE level — it does NOT
* change the current UE level. Spawning is additive: existing world content is preserved.
* *
* This is the entry point for GAME MODE usage — it does NOT require * This is the entry point for GAME MODE usage — it does NOT require
* PS_Editor's PlayerController, SelectionManager, Gizmo, or UI. * PS_Editor's PlayerController, SelectionManager, Gizmo, or UI.
* Just call LoadScene() from your GameMode's BeginPlay. * Just call LoadScenario() from your GameMode's BeginPlay.
* *
* Usage: * Usage:
* void AMyGameMode::BeginPlay() * void AMyGameMode::BeginPlay()
* { * {
* Super::BeginPlay(); * Super::BeginPlay();
* TArray<AActor*> SpawnedActors; * TArray<AActor*> SpawnedActors;
* UPS_Editor_SceneLoader::LoadScene(GetWorld(), TEXT("MyScene"), SpawnedActors); * UPS_Editor_SceneLoader::LoadScenario(GetWorld(), TEXT("MyScenario"), SpawnedActors);
* } * }
*
* Briefing usage (player placement markers only, no timeline animation):
* UPS_Editor_SceneLoader::UnloadScenario(this);
* UPS_Editor_SceneLoader::LoadScenario(this, TEXT("MyScenario"), Out, true,
* EPS_Editor_SceneLoadFilter::PreloadOnly);
*/ */
UCLASS() UCLASS()
class PS_EDITOR_API UPS_Editor_SceneLoader : public UBlueprintFunctionLibrary class PS_EDITOR_API UPS_Editor_SceneLoader : public UBlueprintFunctionLibrary
@@ -37,83 +44,71 @@ class PS_EDITOR_API UPS_Editor_SceneLoader : public UBlueprintFunctionLibrary
public: public:
/** /**
* Load a scene from a JSON file and spawn all actors. * Load a scenario from a .pss file and spawn its actors into the current world.
* *
* @param World The world to spawn actors into. * The call is additive — existing world content is preserved. Spawned actors are tracked
* @param SceneName Scene name (without .json extension). * internally so UnloadScenario() can clean them up; this lets you switch scenarios at
* @param OutActors Filled with all spawned actors. * runtime (typical briefing flow). When Filter == PreloadOnly, the timeline subsystem is
* NOT touched (no SetData, no auto-play, no ActorId registration) so a briefing scene's
* own timeline keeps running untouched.
*
* @param ScenarioName Scenario name (without .pss extension).
* @param OutActors Filled with all spawned actors (also tracked internally).
* @param bStripEditorComponents If true, removes SpawnableComponent and EditableComponent after spawn (cleaner for gameplay). * @param bStripEditorComponents If true, removes SpawnableComponent and EditableComponent after spawn (cleaner for gameplay).
* @return True if the scene was loaded successfully. * @param Filter Which actors to spawn. PreloadOnly = briefing-only, skips timeline setup.
* @return True if the scenario was loaded successfully.
*/ */
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject")) UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
static bool LoadScene(const UObject* WorldContextObject, const FString& SceneName, TArray<AActor*>& OutActors, static bool LoadScenario(const UObject* WorldContextObject, const FString& ScenarioName, TArray<AActor*>& OutActors,
bool bStripEditorComponents = true, EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All); bool bStripEditorComponents = true, EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All);
/** /**
* Load scene from an already-parsed SceneData struct. * Destroy every actor previously spawned via LoadScenario(). No-op if nothing was loaded
* Useful if you want to parse the JSON yourself or modify data before spawning. * or all tracked actors have already been GC'd / manually destroyed.
*/ * Call this before LoadScenario() when switching scenarios.
static bool LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors,
EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All);
/**
* Additive load: spawns actors from a scenario WITHOUT clearing the current world and
* WITHOUT touching the timeline subsystem. Designed for briefing scenes where you only
* want to materialize preload-tagged actors (player placement markers, briefing props)
* configured in the main scenario.
*
* Spawned actors are tracked internally — call UnloadSceneAdditive() to destroy them all
* before loading the next scenario.
*
* @param SceneName Scenario name (without extension)
* @param Filter Which actors to spawn — defaults to PreloadOnly
* @param bStripEditorComponents If true, removes SpawnableComponent / EditableComponent after spawn
* @return True if at least one actor was spawned
*/
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
static bool LoadSceneAdditive(const UObject* WorldContextObject, const FString& SceneName,
EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::PreloadOnly,
bool bStripEditorComponents = true);
/**
* Destroy every actor previously spawned via LoadSceneAdditive. No-op if nothing was loaded.
* Call this before LoadSceneAdditive when switching briefing scenarios.
* @return Number of actors destroyed. * @return Number of actors destroyed.
*/ */
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject")) UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
static int32 UnloadSceneAdditive(const UObject* WorldContextObject); static int32 UnloadScenario(const UObject* WorldContextObject);
/** /**
* Get the directory where scenes are stored. * Load scenario actors from an already-parsed SceneData struct.
* Useful if you want to parse the .pss JSON yourself or modify data before spawning.
*/
static bool LoadScenarioFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors,
EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All);
/**
* Get the directory where scenarios are stored.
*/ */
UFUNCTION(BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintCallable, Category = "PS_Editor")
static FString GetScenesDirectory(); static FString GetScenesDirectory();
/** /**
* Get the full file path for a scene name. * Get the full file path for a scenario name.
*/ */
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. */ /** Get the full file path for a scenario name and its base level. Uses level-specific subdirectory. */
UFUNCTION(BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintCallable, Category = "PS_Editor")
static FString GetSceneFilePathForLevel(const FString& SceneName, const FString& LevelName); static FString GetSceneFilePathForLevel(const FString& SceneName, const FString& LevelName);
/** /**
* Get all saved scene names. Optionally filter by level name. * Get all saved scenario 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 scenarios associated with this level.
*/ */
UFUNCTION(BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintCallable, Category = "PS_Editor")
static TArray<FString> GetSavedSceneNames(const FString& LevelFilter = TEXT("")); static TArray<FString> GetSavedSceneNames(const FString& LevelFilter = TEXT(""));
/** /**
* Get the level name associated with a saved scene (reads the JSON header without spawning). * Get the level name associated with a saved scenario (reads the JSON header without spawning).
*/ */
UFUNCTION(BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintCallable, Category = "PS_Editor")
static FString GetSceneLevelName(const FString& SceneName); static FString GetSceneLevelName(const FString& SceneName);
/** /**
* Returns true while a scene is being loaded (actors are being spawned from JSON). * Returns true while a scenario is being loaded (actors are being spawned from JSON).
* Use in BeginPlay to skip default initialization — wait for OnPropertiesLoaded instead. * Use in BeginPlay to skip default initialization — wait for OnPropertiesLoaded instead.
*/ */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS_Editor") UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS_Editor")
@@ -130,7 +125,7 @@ public:
static bool IsInEditorMode() { return bIsEditorModeActive; } static bool IsInEditorMode() { return bIsEditorModeActive; }
/** /**
* Get all saved scenes for the current level. * Get all saved scenarios for the current level.
*/ */
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject")) UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
static TArray<FString> GetSavedSceneNamesForCurrentLevel(const UObject* WorldContextObject); static TArray<FString> GetSavedSceneNamesForCurrentLevel(const UObject* WorldContextObject);
@@ -142,7 +137,7 @@ private:
/** Apply custom editable properties to a spawned actor. */ /** Apply custom editable properties to a spawned actor. */
static void ApplyCustomProperties(AActor* Actor, const TMap<FString, FString>& CustomProperties); static void ApplyCustomProperties(AActor* Actor, const TMap<FString, FString>& CustomProperties);
/** Actors spawned via LoadSceneAdditive, tracked so UnloadSceneAdditive can find them. /** Actors spawned via LoadScenario(), tracked so UnloadScenario() can find them.
* TWeakObjectPtr handles GC / manual destruction gracefully (skipped at unload time). */ * TWeakObjectPtr handles GC / manual destruction gracefully (skipped at unload time). */
static TArray<TWeakObjectPtr<AActor>> AdditiveSpawnedActors; static TArray<TWeakObjectPtr<AActor>> TrackedSpawnedActors;
}; };

View File

@@ -202,7 +202,7 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
return true; return true;
} }
bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager) bool UPS_Editor_SceneSerializer::LoadScenario(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager)
{ {
if (!SpawnManager || SceneName.IsEmpty()) if (!SpawnManager || SceneName.IsEmpty())
{ {
@@ -523,9 +523,9 @@ void UPS_Editor_SceneSerializer::ClearScene(UPS_Editor_SpawnManager* SpawnManage
// NOTE: Mandatory-actor auto-spawn is intentionally NOT done here. ClearScene is called // NOTE: Mandatory-actor auto-spawn is intentionally NOT done here. ClearScene is called
// from three contexts: (1) "New Scenario" button (we want mandatory spawn — handled in the // from three contexts: (1) "New Scenario" button (we want mandatory spawn — handled in the
// widget after ClearScene returns), (2) LoadScene before re-spawning saved actors (we // widget after ClearScene returns), (2) LoadScenario before re-spawning saved actors (we
// don't want it — the JSON itself contains the mandatory actor, and there's a final // don't want it — the JSON itself contains the mandatory actor, and there's a final
// EnsureMandatoryActorsPresent at the end of LoadScene that handles legacy saves), and // EnsureMandatoryActorsPresent at the end of LoadScenario that handles legacy saves), and
// (3) BaseLevel switch (we don't want it — the new baseLevel isn't loaded yet, the // (3) BaseLevel switch (we don't want it — the new baseLevel isn't loaded yet, the
// MandatoryAnchor isn't in the world; the post-OnLevelShown hook handles that). // MandatoryAnchor isn't in the world; the post-OnLevelShown hook handles that).
} }

View File

@@ -21,8 +21,8 @@ public:
/** Save the current spawned actors to a JSON file. */ /** Save the current spawned actors to a JSON file. */
bool SaveScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager); 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. */ /** Load a scenario from a .pss file. Destroys current spawned actors and re-spawns from file. */
bool LoadScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager); bool LoadScenario(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager);
/** Get a list of all saved scene names (without .json extension). */ /** Get a list of all saved scene names (without .json extension). */
TArray<FString> GetSavedSceneNames() const; TArray<FString> GetSavedSceneNames() const;

View File

@@ -1352,7 +1352,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
} }
} }
SceneSerializer->LoadScene(FileName, SpawnManager.Get()); SceneSerializer->LoadScenario(FileName, SpawnManager.Get());
bSceneDirty = false; bSceneDirty = false;
if (SaveNameField.IsValid()) if (SaveNameField.IsValid())
{ {
@@ -1653,7 +1653,7 @@ void UPS_Editor_MainWidget::PopulateSceneList()
// current sublevel as-is — don't unload it (avoids empty scenes). // current sublevel as-is — don't unload it (avoids empty scenes).
} }
SceneSerializer->LoadScene(Name, SpawnManager.Get()); SceneSerializer->LoadScenario(Name, SpawnManager.Get());
bSceneDirty = false; bSceneDirty = false;
if (SaveNameField.IsValid()) if (SaveNameField.IsValid())
{ {

View File

@@ -1192,7 +1192,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::BuildSceneButtons()
} }
} }
SceneSerializer->LoadScene(FileName, SpawnManager.Get()); SceneSerializer->LoadScenario(FileName, SpawnManager.Get());
bSceneDirty = false; bSceneDirty = false;
if (SaveNameField.IsValid()) if (SaveNameField.IsValid())
{ {
@@ -1487,7 +1487,7 @@ void UPS_Editor_MainWidget_Legacy::PopulateSceneList()
// current sublevel as-is — don't unload it (avoids empty scenes). // current sublevel as-is — don't unload it (avoids empty scenes).
} }
SceneSerializer->LoadScene(Name, SpawnManager.Get()); SceneSerializer->LoadScenario(Name, SpawnManager.Get());
bSceneDirty = false; bSceneDirty = false;
if (SaveNameField.IsValid()) if (SaveNameField.IsValid())
{ {

View File

@@ -130,7 +130,7 @@ bool UPS_Editor_MainWidget_UMG::LoadScenario(const FString& Name)
} }
if (!SM) SM = SpawnManager.Get(); if (!SM) SM = SpawnManager.Get();
if (!Ser || !SM || Name.IsEmpty()) return false; if (!Ser || !SM || Name.IsEmpty()) return false;
Ser->LoadScene(Name, SM); Ser->LoadScenario(Name, SM);
OnSceneDirty.Broadcast(); OnSceneDirty.Broadcast();
return true; return true;
} }