Compare commits
8 Commits
8f7476d2db
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6eca4fc94b | |||
| 4a98c6402b | |||
| f6efc00c73 | |||
| 34280de44a | |||
| ac91e4e8f9 | |||
| c7ec3e9653 | |||
| d9ed016297 | |||
| 67ac3ffab7 |
@@ -1,4 +1,4 @@
|
|||||||
- [PS_Editor plugin project](project_ps_editor.md) — Runtime editor plugin for UE 5.5, Phases 1-4 done, outline material pending
|
- [PS_Editor plugin project](project_ps_editor.md) — Runtime editor plugin for UE 5.7 (migrated from 5.5 in April 2026), Phases 1-4 done, outline material pending
|
||||||
- [Underscore naming](feedback_naming.md) — Always use PS_Editor_ prefix with underscores in file/class names
|
- [Underscore naming](feedback_naming.md) — Always use PS_Editor_ prefix with underscores in file/class names
|
||||||
- [UE5 build process](reference_build.md) — CLI build command for the project (Build.bat path, target name, flags)
|
- [UE5 build process](reference_build.md) — CLI build command for PROSERVE_UE_5_7 (5.5 host deprecated), Build.bat path, target name, junction check
|
||||||
- [Build workflow](feedback_build_workflow.md) — Prefer Live Coding (Ctrl+Alt+F11) when UE is open, CLI build only when UE is closed
|
- [Build workflow](feedback_build_workflow.md) — Prefer Live Coding (Ctrl+Alt+F11) when UE is open, CLI build only when UE is closed
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: PS_Editor plugin project
|
name: PS_Editor plugin project
|
||||||
description: Runtime editor plugin for UE 5.5 - object placement, scene editing, JSON save/load in packaged builds
|
description: Runtime editor plugin for UE 5.7 - object placement, scene editing, JSON save/load in packaged builds
|
||||||
type: project
|
type: project
|
||||||
---
|
---
|
||||||
|
|
||||||
PS_Editor is a runtime editor plugin for Unreal Engine 5.5 for the PROSERVE project.
|
PS_Editor is a runtime editor plugin for Unreal Engine 5.7 for the PROSERVE project. The plugin was started on UE 5.5; the host project migrated to UE 5.7 in April 2026 and the plugin follows. Build details in reference_build.md.
|
||||||
|
|
||||||
Key decisions:
|
Key decisions:
|
||||||
- UI: UMG driven by C++ (Slate)
|
- UI: UMG driven by C++ (Slate)
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
---
|
---
|
||||||
name: UE5 build process
|
name: UE5 build process
|
||||||
description: How to build the PS_ProserveEditor UE 5.5 project from command line on Windows
|
description: How to build the PROSERVE host project (currently UE 5.7) from command line on Windows
|
||||||
type: reference
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
UE 5.5 is installed at `C:\Program Files\Epic Games\UE_5.5` (found via registry key `HKLM\SOFTWARE\EpicGames\Unreal Engine\5.5`).
|
The PS_Editor plugin is developed in `E:\ASTERION\GIT\PS_Editor\Unreal\Plugins\PS_Editor` and consumed by the PROSERVE host project via an NTFS junction. Always build the host project, not the plugin in isolation.
|
||||||
|
|
||||||
|
**Current host project (April 2026):** `E:\ASTERION\SVN\DEV\PROSERVE_UE_5_7\PROSERVE_UE_5_7.uproject` (Unreal Engine 5.7)
|
||||||
|
|
||||||
|
The previous 5.5 host (`PROSERVE_UE_5_5`) is deprecated — do not use unless explicitly asked.
|
||||||
|
|
||||||
|
UE 5.7 is installed at `C:\Program Files\Epic Games\UE_5.7` (found via registry key `HKLM\SOFTWARE\EpicGames\Unreal Engine\5.7`).
|
||||||
|
|
||||||
Build command (Editor target, Development):
|
Build command (Editor target, Development):
|
||||||
```
|
```
|
||||||
"C:/Program Files/Epic Games/UE_5.5/Engine/Build/BatchFiles/Build.bat" PS_ProserveEditorEditor Win64 Development -Project="C:/ASTERION/GIT/PS_ProserveEditor/Unreal/PS_ProserveEditor.uproject" -WaitMutex -FromMsBuild
|
"C:/Program Files/Epic Games/UE_5.7/Engine/Build/BatchFiles/Build.bat" PROSERVE_UE_5_7Editor Win64 Development -Project="E:/ASTERION/SVN/DEV/PROSERVE_UE_5_7/PROSERVE_UE_5_7.uproject" -WaitMutex -FromMsBuild
|
||||||
```
|
```
|
||||||
|
|
||||||
**How to apply:** Use this command to compile after code changes. Timeout should be set to 600000ms (10 min) for safety. The target name is `PS_ProserveEditorEditor` (Editor suffix) for editor builds.
|
**How to apply:** Use this command to compile after code changes. Timeout should be set to 600000ms (10 min) for safety. The target name is `PROSERVE_UE_5_7Editor` (Editor suffix) for editor builds.
|
||||||
|
|
||||||
**When to use CLI vs Live Coding:**
|
**When to use CLI vs Live Coding:**
|
||||||
- Live Coding (Ctrl+Alt+F11): .cpp only changes (no new UCLASS, USTRUCT, UENUM, UPROPERTY in headers)
|
- Live Coding (Ctrl+Alt+F11): .cpp only changes (no new UCLASS, USTRUCT, UENUM, UPROPERTY in headers)
|
||||||
- Full CLI build (UE must be closed): new files, header changes with new reflected types, Build.cs changes
|
- Full CLI build (UE must be closed): new files, header changes with new reflected types, Build.cs changes
|
||||||
|
|
||||||
|
**Plugin junction verification:**
|
||||||
|
```
|
||||||
|
ls -la E:/ASTERION/SVN/DEV/PROSERVE_UE_5_7/Plugins/ | grep PS_Editor
|
||||||
|
```
|
||||||
|
Should show a symlink pointing to `/e/ASTERION/GIT/PS_Editor/Unreal/Plugins/PS_Editor`. If broken, the host project will build against a stale or missing plugin copy.
|
||||||
|
|||||||
14
CLAUDE.md
14
CLAUDE.md
@@ -1,7 +1,7 @@
|
|||||||
# PS_ProserveEditor - Project Guidelines
|
# PS_ProserveEditor - Project Guidelines
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
Unreal Engine 5.5 project with runtime editor plugin (PS_Editor). The plugin enables runtime scene editing in packaged builds: object placement, property editing, scenario definition, splines, lighting, and JSON scene save/load.
|
Unreal Engine 5.7 project with runtime editor plugin (PS_Editor). The plugin enables runtime scene editing in packaged builds: object placement, property editing, scenario definition, splines, lighting, and JSON scene save/load. The plugin was started on UE 5.5; the host project migrated to UE 5.7 in April 2026.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
- **Plugin**: PS_Editor (runtime plugin, must work in packaged builds)
|
- **Plugin**: PS_Editor (runtime plugin, must work in packaged builds)
|
||||||
@@ -118,14 +118,16 @@ Symptom that the wrong widget was edited: logs you added don't appear in the Out
|
|||||||
and wait. Don't pre-emptively write + apply the code change in the same turn.
|
and wait. Don't pre-emptively write + apply the code change in the same turn.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
- Engine: Unreal Engine 5.5
|
- Engine: Unreal Engine 5.7 (host project migrated from 5.5 in April 2026)
|
||||||
- Plugin location: `Unreal/Plugins/PS_Editor/`
|
- Plugin location: `Unreal/Plugins/PS_Editor/` (consumed via NTFS junction from the PROSERVE host project's `Plugins/` directory)
|
||||||
- Game module: `PS_ProserveEditor` (in `Unreal/Source/PS_ProserveEditor/`)
|
- Host project: `E:\ASTERION\SVN\DEV\PROSERVE_UE_5_7\PROSERVE_UE_5_7.uproject` — always build this, not the plugin in isolation
|
||||||
|
- Game module: `PROSERVE_UE_5_7` (in the host project's `Source/`)
|
||||||
- CLI build command (Editor, Development):
|
- CLI build command (Editor, Development):
|
||||||
```
|
```
|
||||||
"<UE_INSTALL>/Engine/Build/BatchFiles/Build.bat" PS_ProserveEditorEditor Win64 Development -Project="<REPO>/Unreal/PS_ProserveEditor.uproject" -WaitMutex -FromMsBuild
|
"C:/Program Files/Epic Games/UE_5.7/Engine/Build/BatchFiles/Build.bat" PROSERVE_UE_5_7Editor Win64 Development -Project="E:/ASTERION/SVN/DEV/PROSERVE_UE_5_7/PROSERVE_UE_5_7.uproject" -WaitMutex -FromMsBuild
|
||||||
```
|
```
|
||||||
UE install path can be found via registry: `HKLM\SOFTWARE\EpicGames\Unreal Engine\5.5` > `InstalledDirectory`
|
UE install path can be found via registry: `HKLM\SOFTWARE\EpicGames\Unreal Engine\5.7` > `InstalledDirectory`
|
||||||
|
- The 5.5 host (`PROSERVE_UE_5_5`) is deprecated — do not build against it unless explicitly asked.
|
||||||
|
|
||||||
## Project Memory
|
## Project Memory
|
||||||
Shared project memory is stored in `.claude/memory/` (versioned in the repository). When starting a new session on any machine, read these files to restore context:
|
Shared project memory is stored in `.claude/memory/` (versioned in the repository). When starting a new session on any machine, read these files to restore context:
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
#include "PS_Editor_GameInstanceSubsystem.h"
|
#include "PS_Editor_GameInstanceSubsystem.h"
|
||||||
|
#include "PS_Editor_SceneLoader.h"
|
||||||
|
#include "PS_Editor_SceneData.h"
|
||||||
#include "Kismet/GameplayStatics.h"
|
#include "Kismet/GameplayStatics.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "JsonObjectConverter.h"
|
||||||
|
|
||||||
void UPS_Editor_GameInstanceSubsystem::SetPendingScenario(const FString& ScenarioName, const FString& BaseLevel)
|
void UPS_Editor_GameInstanceSubsystem::SetPendingScenario(const FString& ScenarioName, const FString& BaseLevel, uint8 ScenarioType)
|
||||||
{
|
{
|
||||||
PendingScenarioName = ScenarioName;
|
PendingScenarioName = ScenarioName;
|
||||||
PendingBaseLevel = BaseLevel;
|
PendingBaseLevel = BaseLevel;
|
||||||
|
PendingScenarioType = ScenarioType;
|
||||||
LastEditedScenarioName = ScenarioName;
|
LastEditedScenarioName = ScenarioName;
|
||||||
LastEditedBaseLevel = BaseLevel;
|
LastEditedBaseLevel = BaseLevel;
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Pending scenario set: '%s' (base level: '%s')"), *ScenarioName, *BaseLevel);
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Pending scenario set: '%s' (base level: '%s', type: %u)"),
|
||||||
|
*ScenarioName, *BaseLevel, ScenarioType);
|
||||||
}
|
}
|
||||||
|
|
||||||
FString UPS_Editor_GameInstanceSubsystem::ConsumePendingScenario()
|
FString UPS_Editor_GameInstanceSubsystem::ConsumePendingScenario()
|
||||||
@@ -45,3 +51,44 @@ void UPS_Editor_GameInstanceSubsystem::ReturnToEditor(const UObject* WorldContex
|
|||||||
|
|
||||||
OpenEditor(WorldContextObject);
|
OpenEditor(WorldContextObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UPS_Editor_GameInstanceSubsystem::ReadScenarioMetadata(const FString& ScenarioName, uint8& OutScenarioType, FString& OutBaseLevel) const
|
||||||
|
{
|
||||||
|
OutScenarioType = 0;
|
||||||
|
OutBaseLevel.Empty();
|
||||||
|
|
||||||
|
if (ScenarioName.IsEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString FilePath = UPS_Editor_SceneLoader::GetSceneFilePath(ScenarioName);
|
||||||
|
FString JsonString;
|
||||||
|
if (!FFileHelper::LoadFileToString(JsonString, *FilePath))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: ReadScenarioMetadata — file not found: %s"), *FilePath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the full SceneData. We could parse just the metadata fields with a custom
|
||||||
|
// reader, but FJsonObjectConverter is fast enough for the scenario sizes we deal with
|
||||||
|
// (tens of actors max), and reusing the same struct keeps version handling consistent.
|
||||||
|
FPS_Editor_SceneData SceneData;
|
||||||
|
if (!FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &SceneData, 0, 0))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: ReadScenarioMetadata — failed to parse JSON for '%s'"), *ScenarioName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutScenarioType = SceneData.ScenarioType;
|
||||||
|
OutBaseLevel = SceneData.LevelName;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8 UPS_Editor_GameInstanceSubsystem::GetScenarioTypeFromFile(const FString& ScenarioName) const
|
||||||
|
{
|
||||||
|
uint8 Type = 0;
|
||||||
|
FString DummyLevel;
|
||||||
|
ReadScenarioMetadata(ScenarioName, Type, DummyLevel);
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,9 +40,36 @@ public:
|
|||||||
UPROPERTY(BlueprintReadWrite, Category = "PS_Editor")
|
UPROPERTY(BlueprintReadWrite, Category = "PS_Editor")
|
||||||
bool bIsPlayMode = false;
|
bool bIsPlayMode = false;
|
||||||
|
|
||||||
/** Set pending scenario + base level (for opening the editor or gameplay). */
|
/** Pending scenario type as uint8 — index into UPS_Editor_MasterCatalog::ScenarioTypeEnum.
|
||||||
|
* Set by the Play button alongside PendingScenarioName. The gameplay GameMode reads it
|
||||||
|
* (Byte to Enum) and routes its scenario-type-specific logic. Default 0 = first enum entry. */
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category = "PS_Editor")
|
||||||
|
uint8 PendingScenarioType = 0;
|
||||||
|
|
||||||
|
/** Set pending scenario + base level + type (for opening the editor or gameplay). */
|
||||||
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
void SetPendingScenario(const FString& ScenarioName, const FString& BaseLevel);
|
void SetPendingScenario(const FString& ScenarioName, const FString& BaseLevel, uint8 ScenarioType = 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-parse a scenario file from disk (without spawning anything) to extract its
|
||||||
|
* persisted metadata: scenario type and base level. Use this from PROSERVE GameMode
|
||||||
|
* BeginPlay (or earlier) to know what type a scenario is BEFORE deciding to load it,
|
||||||
|
* when the standard Play-button flow hasn't populated PendingScenarioType.
|
||||||
|
*
|
||||||
|
* @param ScenarioName Name of the scenario file (without extension).
|
||||||
|
* @param OutScenarioType Set to the scenario's saved type (uint8 index into MasterCatalog::ScenarioTypeEnum). 0 on failure.
|
||||||
|
* @param OutBaseLevel Set to the scenario's saved base level name. Empty on failure.
|
||||||
|
* @return True if the file was found and parsed successfully.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
|
bool ReadScenarioMetadata(const FString& ScenarioName, uint8& OutScenarioType, FString& OutBaseLevel) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience: read just the scenario type from a file. Returns 0 if the file isn't
|
||||||
|
* found / can't be parsed (which is also the default for a saved-but-untyped scenario).
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS_Editor")
|
||||||
|
uint8 GetScenarioTypeFromFile(const FString& ScenarioName) const;
|
||||||
|
|
||||||
/** Consume the pending scenario (reads and clears). */
|
/** Consume the pending scenario (reads and clears). */
|
||||||
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
|
|||||||
@@ -812,7 +812,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
InsertSM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
InsertSM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Inserted spline point at index %d"), InsertIdx);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Inserted spline point at index %d"), InsertIdx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -868,7 +868,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Added spline point at end (index %d)"), NewIdx);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Added spline point at end (index %d)"), NewIdx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1059,7 +1059,7 @@ void APS_Editor_Pawn::HandleTranslateMode(const FInputActionValue& Value)
|
|||||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
{
|
{
|
||||||
SM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
SM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Translate mode"));
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Translate mode"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1072,7 +1072,7 @@ void APS_Editor_Pawn::HandleRotateMode(const FInputActionValue& Value)
|
|||||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
{
|
{
|
||||||
SM->SetTransformMode(EPS_Editor_TransformMode::Rotate);
|
SM->SetTransformMode(EPS_Editor_TransformMode::Rotate);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Rotate mode"));
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Rotate mode"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1085,7 +1085,7 @@ void APS_Editor_Pawn::HandleScaleMode(const FInputActionValue& Value)
|
|||||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
{
|
{
|
||||||
SM->SetTransformMode(EPS_Editor_TransformMode::Scale);
|
SM->SetTransformMode(EPS_Editor_TransformMode::Scale);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scale mode"));
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Scale mode"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1155,7 +1155,7 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deleted spline point, %d remaining"), Spline->GetNumPoints());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Deleted spline point, %d remaining"), Spline->GetNumPoints());
|
||||||
}
|
}
|
||||||
else if (Spline)
|
else if (Spline)
|
||||||
{
|
{
|
||||||
@@ -1288,11 +1288,6 @@ void APS_Editor_Pawn::HandleSnapToGround(const FInputActionValue& Value)
|
|||||||
|
|
||||||
if (GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Camera, Params))
|
if (GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Camera, Params))
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SnapToGround - PointLoc=%s, HitZ=%.2f, HitActor=%s, HitComp=%s"),
|
|
||||||
*PointLoc.ToString(), Hit.ImpactPoint.Z,
|
|
||||||
Hit.GetActor() ? *Hit.GetActor()->GetName() : TEXT("null"),
|
|
||||||
Hit.GetComponent() ? *Hit.GetComponent()->GetName() : TEXT("null"));
|
|
||||||
|
|
||||||
// Store state for undo
|
// Store state for undo
|
||||||
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -210,6 +210,20 @@ void APS_Editor_PlayerController::BeginPlay()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APS_Editor_PlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||||
|
{
|
||||||
|
// Reset the runtime-editor flag whenever this controller's world tears down — covers
|
||||||
|
// OpenLevel from any caller (Play button, scenario loader BP, gameplay code that calls
|
||||||
|
// OpenLevel directly), end of PIE, and seamless world transitions. Without this, the
|
||||||
|
// static would stay 'true' from the previous editor session and IsInEditorMode() would
|
||||||
|
// lie in the next world (which has no PS_Editor PlayerController). The 3 existing eager
|
||||||
|
// resets (CloseEditor + Play buttons) remain as defense-in-depth so any check between
|
||||||
|
// the click and the actual world tear-down also returns false.
|
||||||
|
UPS_Editor_SceneLoader::bIsEditorModeActive = false;
|
||||||
|
|
||||||
|
Super::EndPlay(EndPlayReason);
|
||||||
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::Tick(float DeltaTime)
|
void APS_Editor_PlayerController::Tick(float DeltaTime)
|
||||||
{
|
{
|
||||||
Super::Tick(DeltaTime);
|
Super::Tick(DeltaTime);
|
||||||
@@ -655,7 +669,7 @@ void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf<AActor> InActo
|
|||||||
bAppendingToActor = false;
|
bAppendingToActor = false;
|
||||||
AppendInitialPoints.Empty();
|
AppendInitialPoints.Empty();
|
||||||
EditorMode = EPS_Editor_EditorMode::SplinePlacement;
|
EditorMode = EPS_Editor_EditorMode::SplinePlacement;
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Point placement mode started (class: %s)"), *PlacementActorClass->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Point placement mode started (class: %s)"), *PlacementActorClass->GetName());
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActor)
|
void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActor)
|
||||||
@@ -682,7 +696,7 @@ void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActo
|
|||||||
IPS_Editor_ChildMovable::Execute_SetChildHandlesVisible(ExistingActor, true);
|
IPS_Editor_ChildMovable::Execute_SetChildHandlesVisible(ExistingActor, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Editing sub-elements of %s"), *ExistingActor->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Editing sub-elements of %s"), *ExistingActor->GetName());
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::FinishPointPlacement()
|
void APS_Editor_PlayerController::FinishPointPlacement()
|
||||||
@@ -718,7 +732,7 @@ void APS_Editor_PlayerController::FinishPointPlacement()
|
|||||||
SelectionManager->SelectActor(PlacementActor);
|
SelectionManager->SelectActor(PlacementActor);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exited placement mode"));
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Exited placement mode"));
|
||||||
bAppendingToActor = false;
|
bAppendingToActor = false;
|
||||||
AppendInitialPoints.Empty();
|
AppendInitialPoints.Empty();
|
||||||
EditorMode = EPS_Editor_EditorMode::Normal;
|
EditorMode = EPS_Editor_EditorMode::Normal;
|
||||||
@@ -759,7 +773,7 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
|
|||||||
CurrentSublevel->SetShouldBeVisible(false);
|
CurrentSublevel->SetShouldBeVisible(false);
|
||||||
CurrentSublevel->SetIsRequestingUnloadAndRemoval(true);
|
CurrentSublevel->SetIsRequestingUnloadAndRemoval(true);
|
||||||
CurrentSublevel = nullptr;
|
CurrentSublevel = nullptr;
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Unloaded previous sublevel"));
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Unloaded previous sublevel"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rebuild spawn catalog: global + base-level-specific catalogs
|
// Rebuild spawn catalog: global + base-level-specific catalogs
|
||||||
@@ -879,7 +893,7 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
|
|||||||
ExtraSublevel->SetShouldBeLoaded(true);
|
ExtraSublevel->SetShouldBeLoaded(true);
|
||||||
ExtraSublevel->SetShouldBeVisible(true);
|
ExtraSublevel->SetShouldBeVisible(true);
|
||||||
AdditionalSublevels.Add(ExtraSublevel);
|
AdditionalSublevels.Add(ExtraSublevel);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Loaded additional sublevel '%s'"), *ExtraPath);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Loaded additional sublevel '%s'"), *ExtraPath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -927,7 +941,7 @@ void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials()
|
|||||||
Actor->MarkComponentsRenderStateDirty();
|
Actor->MarkComponentsRenderStateDirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deferred lightmap refresh on sublevel"));
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Deferred lightmap refresh on sublevel"));
|
||||||
}, 0.5f, false);
|
}, 0.5f, false);
|
||||||
|
|
||||||
APS_Editor_CameraStart* CameraStart = nullptr;
|
APS_Editor_CameraStart* CameraStart = nullptr;
|
||||||
@@ -940,7 +954,7 @@ void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials()
|
|||||||
if (APostProcessVolume* PPVolume = Cast<APostProcessVolume>(Actor))
|
if (APostProcessVolume* PPVolume = Cast<APostProcessVolume>(Actor))
|
||||||
{
|
{
|
||||||
PPVolume->Settings.WeightedBlendables.Array.Empty();
|
PPVolume->Settings.WeightedBlendables.Array.Empty();
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Disabled PP materials on '%s'"), *PPVolume->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Disabled PP materials on '%s'"), *PPVolume->GetName());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find camera start marker
|
// Find camera start marker
|
||||||
@@ -958,7 +972,7 @@ void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials()
|
|||||||
const FRotator YawOnly(CameraStart->GetActorRotation().Pitch, CameraStart->GetActorRotation().Yaw, 0.0f);
|
const FRotator YawOnly(CameraStart->GetActorRotation().Pitch, CameraStart->GetActorRotation().Yaw, 0.0f);
|
||||||
MyPawn->SetActorLocationAndRotation(CameraStart->GetActorLocation(), YawOnly);
|
MyPawn->SetActorLocationAndRotation(CameraStart->GetActorLocation(), YawOnly);
|
||||||
SetControlRotation(YawOnly);
|
SetControlRotation(YawOnly);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Camera moved to CameraStart at %s"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Camera moved to CameraStart at %s"),
|
||||||
*CameraStart->GetActorLocation().ToString());
|
*CameraStart->GetActorLocation().ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1077,7 +1091,7 @@ void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bA
|
|||||||
{
|
{
|
||||||
const int32 InsertIdx = Spline->FindInsertIndex(TubeHit.ImpactPoint);
|
const int32 InsertIdx = Spline->FindInsertIndex(TubeHit.ImpactPoint);
|
||||||
Spline->InsertPoint(InsertIdx, TubeHit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f));
|
Spline->InsertPoint(InsertIdx, TubeHit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f));
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Inserted point at index %d during placement"), InsertIdx);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Inserted point at index %d during placement"), InsertIdx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ public:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
|
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||||
virtual void Tick(float DeltaTime) override;
|
virtual void Tick(float DeltaTime) override;
|
||||||
virtual void OnPossess(APawn* InPawn) override;
|
virtual void OnPossess(APawn* InPawn) override;
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ void UPS_Editor_SelectionManager::SelectActor(AActor* Actor)
|
|||||||
Spline->SetSelected(true);
|
Spline->SetSelected(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selected %s"), *Actor->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Selected %s"), *Actor->GetName());
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
|
void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
|
||||||
@@ -235,7 +235,7 @@ void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deselected %s"), *Actor->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Deselected %s"), *Actor->GetName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +287,7 @@ void UPS_Editor_SelectionManager::ClearSelection()
|
|||||||
|
|
||||||
if (Count > 0)
|
if (Count > 0)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Cleared selection (%d actors)"), Count);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Cleared selection (%d actors)"), Count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ void UPS_Editor_UndoManager::RecordAction(TSharedPtr<FPS_Editor_Action> Action)
|
|||||||
|
|
||||||
PruneHistory();
|
PruneHistory();
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Recorded action: %s (stack size: %d)"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Recorded action: %s (stack size: %d)"),
|
||||||
*Action->GetDescription(), ActionStack.Num());
|
*Action->GetDescription(), ActionStack.Num());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,4 +68,10 @@ struct FPS_Editor_SceneData
|
|||||||
* cooked Static nav, which has zero runtime cost. Default: false. */
|
* cooked Static nav, which has zero runtime cost. Default: false. */
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
bool bUseDynamicNavigation = false;
|
bool bUseDynamicNavigation = false;
|
||||||
|
|
||||||
|
/** Project-specific scenario type as a uint8 (index into UPS_Editor_MasterCatalog::ScenarioTypeEnum).
|
||||||
|
* Selected by the user via the Settings popup dropdown. Defaults to 0 (= first enum entry) for
|
||||||
|
* legacy scenarios that were saved before this field existed. */
|
||||||
|
UPROPERTY()
|
||||||
|
uint8 ScenarioType = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,20 +132,54 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors, EPS_Editor_SceneLoadFilter Filter)
|
int32 UPS_Editor_SceneLoader::UnloadScenario(const UObject* WorldContextObject)
|
||||||
|
{
|
||||||
|
int32 Destroyed = 0;
|
||||||
|
for (TWeakObjectPtr<AActor>& Weak : TrackedSpawnedActors)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Weak.Get())
|
||||||
|
{
|
||||||
|
Actor->Destroy();
|
||||||
|
++Destroyed;
|
||||||
|
}
|
||||||
|
// else: already gone (GC, manual destroy, level reload) — skip silently
|
||||||
|
}
|
||||||
|
TrackedSpawnedActors.Reset();
|
||||||
|
|
||||||
|
if (Destroyed > 0)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor SceneLoader: UnloadScenario destroyed %d actor(s)"), Destroyed);
|
||||||
|
}
|
||||||
|
return Destroyed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UPS_Editor_SceneLoader::LoadScenarioFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors, EPS_Editor_SceneLoadFilter Filter)
|
||||||
{
|
{
|
||||||
if (!World)
|
if (!World)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defensive: reaching the gameplay-side SceneLoader means we are NOT in the runtime
|
||||||
|
// editor (the editor uses UPS_Editor_SceneSerializer instead). Force the static flag
|
||||||
|
// false now so subsequent IsInEditorMode() checks reflect reality even if a previous
|
||||||
|
// PIE session left the static at 'true' — PIE keeps C++ statics across stop/start, and
|
||||||
|
// a forced PIE-stop or BP-driven OpenLevel can skip the EndPlay reset on the editor PC.
|
||||||
|
bIsEditorModeActive = false;
|
||||||
|
|
||||||
// Apply the scenario's dynamic-navigation flag BEFORE spawning actors. If the scenario
|
// Apply the scenario's dynamic-navigation flag BEFORE spawning actors. If the scenario
|
||||||
// needs runtime nav generation, switching the level's RecastNavMesh to Dynamic now
|
// needs runtime nav generation, switching the level's RecastNavMesh to Dynamic now
|
||||||
// means the actors we're about to spawn will dirty + rebuild tiles around them. Cooked
|
// means the actors we're about to spawn will dirty + rebuild tiles around them. Cooked
|
||||||
// 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;
|
||||||
|
|
||||||
@@ -147,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);
|
||||||
@@ -213,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,56 +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.
|
||||||
|
* @return Number of actors destroyed.
|
||||||
*/
|
*/
|
||||||
static bool LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors,
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
|
||||||
|
static int32 UnloadScenario(const UObject* WorldContextObject);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the directory where scenes are stored.
|
* 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")
|
||||||
@@ -103,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);
|
||||||
@@ -114,4 +136,8 @@ 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 LoadScenario(), tracked so UnloadScenario() can find them.
|
||||||
|
* TWeakObjectPtr handles GC / manual destruction gracefully (skipped at unload time). */
|
||||||
|
static TArray<TWeakObjectPtr<AActor>> TrackedSpawnedActors;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
|
|||||||
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(Actor))
|
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(Actor))
|
||||||
{
|
{
|
||||||
Spline->ExportToCustomProperties(ActorData.CustomProperties);
|
Spline->ExportToCustomProperties(ActorData.CustomProperties);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exported spline with %d points, class=%s"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Exported spline with %d points, class=%s"),
|
||||||
Spline->GetNumPoints(), *ActorData.ClassPath);
|
Spline->GetNumPoints(), *ActorData.ClassPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +171,7 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
|
|||||||
if (SpawnManager)
|
if (SpawnManager)
|
||||||
{
|
{
|
||||||
SceneData.bUseDynamicNavigation = SpawnManager->bUseDynamicNavigation;
|
SceneData.bUseDynamicNavigation = SpawnManager->bUseDynamicNavigation;
|
||||||
|
SceneData.ScenarioType = SpawnManager->ScenarioType;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to JSON
|
// Convert to JSON
|
||||||
@@ -201,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())
|
||||||
{
|
{
|
||||||
@@ -450,6 +451,7 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
|
|||||||
if (SpawnManager)
|
if (SpawnManager)
|
||||||
{
|
{
|
||||||
SpawnManager->bUseDynamicNavigation = SceneData.bUseDynamicNavigation;
|
SpawnManager->bUseDynamicNavigation = SceneData.bUseDynamicNavigation;
|
||||||
|
SpawnManager->ScenarioType = SceneData.ScenarioType;
|
||||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOuter()))
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOuter()))
|
||||||
{
|
{
|
||||||
PS_Editor_NavUtils::SetDynamicNavigationEnabled(EditorPC->GetWorld(), SpawnManager->bUseDynamicNavigation);
|
PS_Editor_NavUtils::SetDynamicNavigationEnabled(EditorPC->GetWorld(), SpawnManager->bUseDynamicNavigation);
|
||||||
@@ -521,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).
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ void UPS_Editor_EditableComponent::PostEditChangeProperty(FPropertyChangedEvent&
|
|||||||
FPS_Editor_EditablePropertyEntry NewEntry;
|
FPS_Editor_EditablePropertyEntry NewEntry;
|
||||||
NewEntry.Path = FullPath;
|
NewEntry.Path = FullPath;
|
||||||
EditableProperties.Add(NewEntry);
|
EditableProperties.Add(NewEntry);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Added editable property: %s"), *FullPath.ToString());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Added editable property: %s"), *FullPath.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
AddProperty = NAME_None; // Reset for next pick
|
AddProperty = NAME_None; // Reset for next pick
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
#include "PS_Editor_FloorCutManager.h"
|
||||||
|
#include "PS_Editor_MandatoryAnchor.h"
|
||||||
|
#include "PS_Editor_SceneLoader.h"
|
||||||
|
#include "Components/PrimitiveComponent.h"
|
||||||
|
#include "EngineUtils.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
|
||||||
|
bool UPS_Editor_FloorCutManager::ShouldCreateSubsystem(UObject* Outer) const
|
||||||
|
{
|
||||||
|
// Only create on real game worlds — skip preview / inactive worlds.
|
||||||
|
if (const UWorld* World = Cast<UWorld>(Outer))
|
||||||
|
{
|
||||||
|
const EWorldType::Type WT = World->WorldType;
|
||||||
|
return WT == EWorldType::Game || WT == EWorldType::PIE || WT == EWorldType::Editor;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::Initialize(FSubsystemCollectionBase& Collection)
|
||||||
|
{
|
||||||
|
Super::Initialize(Collection);
|
||||||
|
bEnabled = false;
|
||||||
|
HeightAboveAnchor = 200.f;
|
||||||
|
bAnchorZCached = false;
|
||||||
|
CachedAnchorZ = 0.f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::Deinitialize()
|
||||||
|
{
|
||||||
|
// Restore everything we touched, so a world tear-down leaves a clean state.
|
||||||
|
RestoreAll();
|
||||||
|
Snapshots.Empty();
|
||||||
|
Super::Deinitialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::SetEnabled(bool bInEnabled)
|
||||||
|
{
|
||||||
|
// Editor-only feature: silently ignore in gameplay mode. The widgets gate calls already,
|
||||||
|
// but this is a belt-and-braces safety so nothing leaks into a SceneLoader-driven runtime.
|
||||||
|
if (!UPS_Editor_SceneLoader::IsInEditorMode())
|
||||||
|
{
|
||||||
|
if (bEnabled)
|
||||||
|
{
|
||||||
|
RestoreAll();
|
||||||
|
bEnabled = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bEnabled == bInEnabled) return;
|
||||||
|
|
||||||
|
bEnabled = bInEnabled;
|
||||||
|
if (bEnabled)
|
||||||
|
{
|
||||||
|
ApplyCutToWorld();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RestoreAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::SetHeightAboveAnchor(float HeightCm)
|
||||||
|
{
|
||||||
|
// Clamp to a sane range matching the UI slider (0–1000 cm).
|
||||||
|
HeightCm = FMath::Clamp(HeightCm, 0.f, 10000.f);
|
||||||
|
if (FMath::IsNearlyEqual(HeightAboveAnchor, HeightCm)) return;
|
||||||
|
|
||||||
|
HeightAboveAnchor = HeightCm;
|
||||||
|
if (bEnabled)
|
||||||
|
{
|
||||||
|
ApplyCutToWorld();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float UPS_Editor_FloorCutManager::GetCutWorldZ() const
|
||||||
|
{
|
||||||
|
if (!bAnchorZCached)
|
||||||
|
{
|
||||||
|
// Const-cast acceptable: lazy cache only, no observable mutation of "logical" state.
|
||||||
|
const_cast<UPS_Editor_FloorCutManager*>(this)->ResolveAnchorZ();
|
||||||
|
}
|
||||||
|
return CachedAnchorZ + HeightAboveAnchor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::ReapplyCut()
|
||||||
|
{
|
||||||
|
if (!bEnabled) return;
|
||||||
|
ApplyCutToWorld();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::OnActorSpawned(AActor* Actor)
|
||||||
|
{
|
||||||
|
if (!bEnabled || !Actor) return;
|
||||||
|
|
||||||
|
ResolveAnchorZ();
|
||||||
|
const float CutZ = CachedAnchorZ + HeightAboveAnchor;
|
||||||
|
|
||||||
|
TArray<UPrimitiveComponent*> Prims;
|
||||||
|
Actor->GetComponents<UPrimitiveComponent>(Prims);
|
||||||
|
for (UPrimitiveComponent* P : Prims)
|
||||||
|
{
|
||||||
|
ApplyCutToComponent(P, CutZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::InvalidateAnchorCache()
|
||||||
|
{
|
||||||
|
bAnchorZCached = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float UPS_Editor_FloorCutManager::ResolveAnchorZ()
|
||||||
|
{
|
||||||
|
bAnchorZCached = true;
|
||||||
|
CachedAnchorZ = 0.f;
|
||||||
|
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
if (!World) return CachedAnchorZ;
|
||||||
|
|
||||||
|
for (TActorIterator<APS_Editor_MandatoryAnchor> It(World); It; ++It)
|
||||||
|
{
|
||||||
|
if (APS_Editor_MandatoryAnchor* Anchor = *It)
|
||||||
|
{
|
||||||
|
CachedAnchorZ = Anchor->GetActorLocation().Z;
|
||||||
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: FloorCut anchor Z = %f"), CachedAnchorZ);
|
||||||
|
return CachedAnchorZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: FloorCut — no APS_Editor_MandatoryAnchor found, using world Z=0 as reference"));
|
||||||
|
return CachedAnchorZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::ApplyCutToWorld()
|
||||||
|
{
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
if (!World) return;
|
||||||
|
|
||||||
|
ResolveAnchorZ();
|
||||||
|
const float CutZ = CachedAnchorZ + HeightAboveAnchor;
|
||||||
|
|
||||||
|
int32 Hidden = 0;
|
||||||
|
int32 Visible = 0;
|
||||||
|
|
||||||
|
for (TActorIterator<AActor> ActorIt(World); ActorIt; ++ActorIt)
|
||||||
|
{
|
||||||
|
AActor* Actor = *ActorIt;
|
||||||
|
if (!Actor) continue;
|
||||||
|
|
||||||
|
// Never hide the anchor itself (it's hidden in game anyway, but defensive).
|
||||||
|
if (Actor->IsA<APS_Editor_MandatoryAnchor>()) continue;
|
||||||
|
|
||||||
|
TArray<UPrimitiveComponent*> Prims;
|
||||||
|
Actor->GetComponents<UPrimitiveComponent>(Prims);
|
||||||
|
for (UPrimitiveComponent* P : Prims)
|
||||||
|
{
|
||||||
|
if (!P) continue;
|
||||||
|
|
||||||
|
const FBoxSphereBounds B = P->Bounds;
|
||||||
|
const float MinZ = B.Origin.Z - B.BoxExtent.Z;
|
||||||
|
if (MinZ > CutZ)
|
||||||
|
{
|
||||||
|
ApplyCutToComponent(P, CutZ);
|
||||||
|
++Hidden;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Component sits at or straddles the plane → must be in its restored state.
|
||||||
|
RestoreComponent(P);
|
||||||
|
++Visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: FloorCut applied — CutZ=%.1f, hidden=%d, kept=%d, anchor=%.1f, height=%.1f"),
|
||||||
|
CutZ, Hidden, Visible, CachedAnchorZ, HeightAboveAnchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::ApplyCutToComponent(UPrimitiveComponent* Comp, float CutZ)
|
||||||
|
{
|
||||||
|
if (!Comp) return;
|
||||||
|
|
||||||
|
const FBoxSphereBounds B = Comp->Bounds;
|
||||||
|
const float MinZ = B.Origin.Z - B.BoxExtent.Z;
|
||||||
|
if (MinZ <= CutZ)
|
||||||
|
{
|
||||||
|
// Below the plane — make sure it is in its restored state (caller may not have).
|
||||||
|
RestoreComponent(Comp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot before first mutation, so we can restore the exact original state.
|
||||||
|
TWeakObjectPtr<UPrimitiveComponent> Key(Comp);
|
||||||
|
if (!Snapshots.Contains(Key))
|
||||||
|
{
|
||||||
|
FCutSnapshot Snap;
|
||||||
|
Snap.bWasVisible = Comp->IsVisible();
|
||||||
|
Snap.CollisionEnabled = Comp->GetCollisionEnabled();
|
||||||
|
Snapshots.Add(Key, Snap);
|
||||||
|
}
|
||||||
|
|
||||||
|
Comp->SetVisibility(false, /*bPropagateToChildren=*/false);
|
||||||
|
Comp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::RestoreComponent(UPrimitiveComponent* Comp)
|
||||||
|
{
|
||||||
|
if (!Comp) return;
|
||||||
|
TWeakObjectPtr<UPrimitiveComponent> Key(Comp);
|
||||||
|
const FCutSnapshot* Snap = Snapshots.Find(Key);
|
||||||
|
if (!Snap) return;
|
||||||
|
|
||||||
|
Comp->SetVisibility(Snap->bWasVisible, /*bPropagateToChildren=*/false);
|
||||||
|
Comp->SetCollisionEnabled(Snap->CollisionEnabled);
|
||||||
|
Snapshots.Remove(Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_FloorCutManager::RestoreAll()
|
||||||
|
{
|
||||||
|
for (auto It = Snapshots.CreateIterator(); It; ++It)
|
||||||
|
{
|
||||||
|
UPrimitiveComponent* Comp = It.Key().Get();
|
||||||
|
if (Comp)
|
||||||
|
{
|
||||||
|
Comp->SetVisibility(It.Value().bWasVisible, /*bPropagateToChildren=*/false);
|
||||||
|
Comp->SetCollisionEnabled(It.Value().CollisionEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Snapshots.Empty();
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Subsystems/WorldSubsystem.h"
|
||||||
|
#include "Engine/EngineTypes.h"
|
||||||
|
#include "UObject/WeakObjectPtr.h"
|
||||||
|
#include "PS_Editor_FloorCutManager.generated.h"
|
||||||
|
|
||||||
|
class UPrimitiveComponent;
|
||||||
|
class APS_Editor_MandatoryAnchor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor-only horizontal "floor cut" plane.
|
||||||
|
*
|
||||||
|
* Hides every UPrimitiveComponent whose world-space bounds-min Z is strictly above
|
||||||
|
* AnchorZ + HeightAboveAnchor, by toggling per-component visibility and collision.
|
||||||
|
* The original state is snapshotted before any modification and restored on disable
|
||||||
|
* (or on plugin exit), so the feature is non-destructive.
|
||||||
|
*
|
||||||
|
* Editor-only by design — the gameplay runtime loaded via SceneLoader::LoadScene never
|
||||||
|
* touches this subsystem. UI in the editor widgets is the only consumer.
|
||||||
|
*
|
||||||
|
* Limitations:
|
||||||
|
* - Components straddling the cut plane (minZ <= cut <= maxZ) stay fully visible.
|
||||||
|
* Per-pixel cuts would require modifying master materials, which we explicitly
|
||||||
|
* don't want.
|
||||||
|
* - Anchor Z is sampled once and cached. If the user moves the APS_Editor_MandatoryAnchor
|
||||||
|
* at runtime, call InvalidateAnchorCache() to re-sample.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - Get via UWorld::GetSubsystem<UPS_Editor_FloorCutManager>(World)
|
||||||
|
* - SetEnabled(true), SetHeightAboveAnchor(200.f)
|
||||||
|
* - OnActorSpawned(NewActor) when SpawnManager spawns a new actor while active
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class PS_EDITOR_API UPS_Editor_FloorCutManager : public UWorldSubsystem
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
// UWorldSubsystem
|
||||||
|
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||||
|
virtual void Deinitialize() override;
|
||||||
|
virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
|
||||||
|
|
||||||
|
/** Enable / disable the cut. On disable, all snapshotted components are restored. */
|
||||||
|
void SetEnabled(bool bInEnabled);
|
||||||
|
bool IsEnabled() const { return bEnabled; }
|
||||||
|
|
||||||
|
/** Set the cut height in centimeters above the MandatoryAnchor. Re-applies immediately
|
||||||
|
* if the cut is enabled. */
|
||||||
|
void SetHeightAboveAnchor(float HeightCm);
|
||||||
|
float GetHeightAboveAnchor() const { return HeightAboveAnchor; }
|
||||||
|
|
||||||
|
/** Computed world Z of the cut plane (AnchorZ + HeightAboveAnchor). Returns
|
||||||
|
* TNumericLimits<float>::Max() if no anchor is found. */
|
||||||
|
float GetCutWorldZ() const;
|
||||||
|
|
||||||
|
/** Force a re-application of the current cut on all current components.
|
||||||
|
* Cheap (no allocation if components haven't moved). */
|
||||||
|
void ReapplyCut();
|
||||||
|
|
||||||
|
/** Called by the SpawnManager whenever it spawns a new actor while the cut is
|
||||||
|
* active, so the new actor's primitives are evaluated against the cut. */
|
||||||
|
void OnActorSpawned(AActor* Actor);
|
||||||
|
|
||||||
|
/** Drop the cached anchor Z (re-sampled on next ReapplyCut). */
|
||||||
|
void InvalidateAnchorCache();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/** Per-component snapshot of the state we tweak, so we can restore exactly. */
|
||||||
|
struct FCutSnapshot
|
||||||
|
{
|
||||||
|
bool bWasVisible = true;
|
||||||
|
TEnumAsByte<ECollisionEnabled::Type> CollisionEnabled = ECollisionEnabled::QueryAndPhysics;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Apply the cut to a single primitive (snapshot if needed, then hide). */
|
||||||
|
void ApplyCutToComponent(UPrimitiveComponent* Comp, float CutZ);
|
||||||
|
/** Restore a single primitive from its snapshot, if any. */
|
||||||
|
void RestoreComponent(UPrimitiveComponent* Comp);
|
||||||
|
|
||||||
|
/** Walk the world and apply the cut to every primitive. */
|
||||||
|
void ApplyCutToWorld();
|
||||||
|
/** Restore every snapshotted primitive. */
|
||||||
|
void RestoreAll();
|
||||||
|
|
||||||
|
/** Find the world Z of the first MandatoryAnchor in the world, or fall back to 0.
|
||||||
|
* Result is cached in CachedAnchorZ. */
|
||||||
|
float ResolveAnchorZ();
|
||||||
|
|
||||||
|
bool bEnabled = false;
|
||||||
|
float HeightAboveAnchor = 200.f; // cm — default 2m as agreed with user
|
||||||
|
|
||||||
|
bool bAnchorZCached = false;
|
||||||
|
float CachedAnchorZ = 0.f;
|
||||||
|
|
||||||
|
/** Snapshots of components we have modified, keyed by weak pointer so destroyed
|
||||||
|
* primitives are simply ignored on restore. */
|
||||||
|
TMap<TWeakObjectPtr<UPrimitiveComponent>, FCutSnapshot> Snapshots;
|
||||||
|
};
|
||||||
@@ -18,8 +18,11 @@ APS_Editor_MandatoryAnchor::APS_Editor_MandatoryAnchor()
|
|||||||
SpriteComponent->bIsScreenSizeScaled = true;
|
SpriteComponent->bIsScreenSizeScaled = true;
|
||||||
SpriteComponent->ScreenSize = 0.0015f;
|
SpriteComponent->ScreenSize = 0.0015f;
|
||||||
|
|
||||||
// Pivot icon — clearly distinct from APS_Editor_CameraStart's note icon.
|
// Target-point icon — clearly distinct from APS_Editor_CameraStart's note icon.
|
||||||
static ConstructorHelpers::FObjectFinder<UTexture2D> IconFinder(TEXT("/Engine/EditorResources/S_Pivot"));
|
// (Engine/EditorResources/S_Pivot does not exist in UE 5.5 — using S_TargetPoint instead,
|
||||||
|
// which fits the "positional anchor marker" semantic and avoids the cook-time
|
||||||
|
// "Failed to find /Engine/EditorResources/S_Pivot" CDO error.)
|
||||||
|
static ConstructorHelpers::FObjectFinder<UTexture2D> IconFinder(TEXT("/Engine/EditorResources/S_TargetPoint"));
|
||||||
if (IconFinder.Succeeded())
|
if (IconFinder.Succeeded())
|
||||||
{
|
{
|
||||||
SpriteComponent->SetSprite(IconFinder.Object);
|
SpriteComponent->SetSprite(IconFinder.Object);
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ namespace PS_Editor_NavUtils
|
|||||||
if (Mat)
|
if (Mat)
|
||||||
{
|
{
|
||||||
GSurfaceMaterial = Mat;
|
GSurfaceMaterial = Mat;
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: NavMesh surface material loaded from %s"), GNavMeshSurfaceMaterialPath);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: NavMesh surface material loaded from %s"), GNavMeshSurfaceMaterialPath);
|
||||||
GSurfaceMaterialMissingWarned = false;
|
GSurfaceMaterialMissingWarned = false;
|
||||||
return Mat;
|
return Mat;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ void UPS_Editor_SpawnCatalog::CaptureAllThumbnails()
|
|||||||
// Skip if already has a custom thumbnail
|
// Skip if already has a custom thumbnail
|
||||||
if (SpawnComp->Thumbnail)
|
if (SpawnComp->Thumbnail)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: %s already has a thumbnail, skipping"), *LoadedClass->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: %s already has a thumbnail, skipping"), *LoadedClass->GetName());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ void UPS_Editor_SpawnCatalog::CaptureAllThumbnails()
|
|||||||
Blueprint->MarkPackageDirty();
|
Blueprint->MarkPackageDirty();
|
||||||
|
|
||||||
Captured++;
|
Captured++;
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Captured thumbnail for %s -> %s"), *Blueprint->GetName(), *FullPath);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Captured thumbnail for %s -> %s"), *Blueprint->GetName(), *FullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Captured %d thumbnails"), Captured);
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Captured %d thumbnails"), Captured);
|
||||||
|
|||||||
@@ -118,4 +118,16 @@ public:
|
|||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS_Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS_Editor")
|
||||||
TArray<FPS_Editor_BaseLevelEntry> BaseLevels;
|
TArray<FPS_Editor_BaseLevelEntry> BaseLevels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional project-specific enum used to categorize scenarios. The runtime editor's
|
||||||
|
* "Scenario Type" dropdown (Settings popup) reads this enum to populate its choices.
|
||||||
|
* The picked value is saved per-scenario as a uint8 (index into the enum) and exposed
|
||||||
|
* at runtime via UPS_Editor_GameInstanceSubsystem::PendingScenarioType — the gameplay
|
||||||
|
* GameMode reads it (Byte to Enum) and dispatches its scenario-type-specific logic.
|
||||||
|
*
|
||||||
|
* Leave null if your project doesn't use scenario typing — the dropdown is hidden.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS_Editor")
|
||||||
|
TObjectPtr<UEnum> ScenarioTypeEnum;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "PS_Editor_GroundSnap.h"
|
#include "PS_Editor_GroundSnap.h"
|
||||||
#include "PS_Editor_NavUtils.h"
|
#include "PS_Editor_NavUtils.h"
|
||||||
#include "PS_Editor_MandatoryAnchor.h"
|
#include "PS_Editor_MandatoryAnchor.h"
|
||||||
|
#include "PS_Editor_FloorCutManager.h"
|
||||||
#include "AIController.h"
|
#include "AIController.h"
|
||||||
#include "BrainComponent.h"
|
#include "BrainComponent.h"
|
||||||
#include "EngineUtils.h"
|
#include "EngineUtils.h"
|
||||||
@@ -96,11 +97,6 @@ void UPS_Editor_SpawnManager::AddCatalog(UPS_Editor_SpawnCatalog* InCatalog)
|
|||||||
Resolved.Thumbnail = SpawnComp ? SpawnComp->Thumbnail : nullptr;
|
Resolved.Thumbnail = SpawnComp ? SpawnComp->Thumbnail : nullptr;
|
||||||
Resolved.bIsMandatory = SpawnComp ? SpawnComp->bIsMandatory : false;
|
Resolved.bIsMandatory = SpawnComp ? SpawnComp->bIsMandatory : false;
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolved '%s' - SpawnComp=%s, Thumbnail=%s"),
|
|
||||||
*Resolved.DisplayName,
|
|
||||||
SpawnComp ? TEXT("found") : TEXT("null"),
|
|
||||||
Resolved.Thumbnail ? *Resolved.Thumbnail->GetPathName() : TEXT("null"));
|
|
||||||
|
|
||||||
ResolvedEntries.Add(Resolved);
|
ResolvedEntries.Add(Resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,6 +311,19 @@ AActor* UPS_Editor_SpawnManager::SpawnActorDirect(UClass* ActorClass, FVector Lo
|
|||||||
SpawnedActors.Add(SpawnedActor);
|
SpawnedActors.Add(SpawnedActor);
|
||||||
RegisterSpawnedActorId(PC->GetWorld(), SpawnedActor);
|
RegisterSpawnedActorId(PC->GetWorld(), SpawnedActor);
|
||||||
|
|
||||||
|
// If the editor's floor-cut is active, evaluate the new actor against it so it doesn't
|
||||||
|
// appear floating above a "hidden" floor.
|
||||||
|
if (UWorld* W = PC->GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
if (FC->IsEnabled())
|
||||||
|
{
|
||||||
|
FC->OnActorSpawned(SpawnedActor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return SpawnedActor;
|
return SpawnedActor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,6 +349,17 @@ void UPS_Editor_SpawnManager::TrackSpawnedActor(AActor* Actor)
|
|||||||
if (APlayerController* PC = OwnerPC.Get())
|
if (APlayerController* PC = OwnerPC.Get())
|
||||||
{
|
{
|
||||||
RegisterSpawnedActorId(PC->GetWorld(), Actor);
|
RegisterSpawnedActorId(PC->GetWorld(), Actor);
|
||||||
|
// Re-evaluate against the floor cut (duplicate / paste paths land here).
|
||||||
|
if (UWorld* W = PC->GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
if (FC->IsEnabled())
|
||||||
|
{
|
||||||
|
FC->OnActorSpawned(Actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,7 +447,6 @@ AActor* UPS_Editor_SpawnManager::RespawnActorWithProperties(AActor* OldActor, co
|
|||||||
if (Prop)
|
if (Prop)
|
||||||
{
|
{
|
||||||
Prop->ImportText_InContainer(*Pair.Value, Target, Target, PPF_None);
|
Prop->ImportText_InContainer(*Pair.Value, Target, Target, PPF_None);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Respawn apply [%s] = '%s' on %s"), *Pair.Key, *Pair.Value, *Target->GetName());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -494,7 +513,7 @@ void UPS_Editor_SpawnManager::EnsureMandatoryActorsPresent()
|
|||||||
{
|
{
|
||||||
if (E.bIsMandatory) ++MandatoryCount;
|
if (E.bIsMandatory) ++MandatoryCount;
|
||||||
}
|
}
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: EnsureMandatoryActorsPresent — scanning %d catalog entries (%d marked mandatory)"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: EnsureMandatoryActorsPresent — scanning %d catalog entries (%d marked mandatory)"),
|
||||||
ResolvedEntries.Num(), MandatoryCount);
|
ResolvedEntries.Num(), MandatoryCount);
|
||||||
if (MandatoryCount == 0)
|
if (MandatoryCount == 0)
|
||||||
{
|
{
|
||||||
@@ -513,7 +532,7 @@ void UPS_Editor_SpawnManager::EnsureMandatoryActorsPresent()
|
|||||||
{
|
{
|
||||||
AnchorXf = Anchor->GetActorTransform();
|
AnchorXf = Anchor->GetActorTransform();
|
||||||
bAnchorFound = true;
|
bAnchorFound = true;
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: MandatoryAnchor found at %s"), *AnchorXf.GetLocation().ToString());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: MandatoryAnchor found at %s"), *AnchorXf.GetLocation().ToString());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -546,7 +565,6 @@ void UPS_Editor_SpawnManager::EnsureMandatoryActorsPresent()
|
|||||||
if (bAlreadyPresent)
|
if (bAlreadyPresent)
|
||||||
{
|
{
|
||||||
++SkippedExisting;
|
++SkippedExisting;
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Mandatory '%s' already present, skipping."), *Entry.DisplayName);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,12 @@ public:
|
|||||||
* Persisted in the scenario JSON; default false. */
|
* Persisted in the scenario JSON; default false. */
|
||||||
bool bUseDynamicNavigation = false;
|
bool bUseDynamicNavigation = false;
|
||||||
|
|
||||||
|
/** Project-specific scenario type as uint8 (index into MasterCatalog::ScenarioTypeEnum).
|
||||||
|
* Set via the Settings popup dropdown, persisted in the scenario JSON, pushed into
|
||||||
|
* PS_Editor_GameInstanceSubsystem::PendingScenarioType at Play click for gameplay
|
||||||
|
* GameMode to consume. Default 0 (first enum entry). */
|
||||||
|
uint8 ScenarioType = 0;
|
||||||
|
|
||||||
// ---- Session-only UI preferences (NOT persisted) ----
|
// ---- Session-only UI preferences (NOT persisted) ----
|
||||||
|
|
||||||
/** If true, the navmesh is rendered at runtime (mimics the editor's P key). Personal
|
/** If true, the navmesh is rendered at runtime (mimics the editor's P key). Personal
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ bool UPS_Editor_TimelineSubsystem::AddOrUpdateKeyAtCurrentTime(AActor* Actor, co
|
|||||||
const FGuid ActorId = GetOrCreateActorId(Actor);
|
const FGuid ActorId = GetOrCreateActorId(Actor);
|
||||||
if (!ActorId.IsValid()) return false;
|
if (!ActorId.IsValid()) return false;
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor Timeline: AddOrUpdateKey — Actor=%s ActorId=%s Prop=%s CurrentTime=%.3f"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor Timeline: AddOrUpdateKey — Actor=%s ActorId=%s Prop=%s CurrentTime=%.3f"),
|
||||||
*Actor->GetName(), *ActorId.ToString(), *PropertyPath.ToString(), CurrentTime);
|
*Actor->GetName(), *ActorId.ToString(), *PropertyPath.ToString(), CurrentTime);
|
||||||
|
|
||||||
// Resolve property to read current value
|
// Resolve property to read current value
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
#include "DesktopPlatformModule.h"
|
#include "DesktopPlatformModule.h"
|
||||||
#include "PS_Editor_GameInstanceSubsystem.h"
|
#include "PS_Editor_GameInstanceSubsystem.h"
|
||||||
#include "PS_Editor_TimelineSubsystem.h"
|
#include "PS_Editor_TimelineSubsystem.h"
|
||||||
|
#include "PS_Editor_FloorCutManager.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
#include "PS_Editor_UIStyleAsset.h"
|
#include "PS_Editor_UIStyleAsset.h"
|
||||||
#include "PS_Editor_UIStyle.h"
|
#include "PS_Editor_UIStyle.h"
|
||||||
@@ -591,6 +592,69 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
// Scenario Type dropdown — visible only when MasterCatalog::ScenarioTypeEnum is set.
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(0.0f, 8.0f, 0.0f, 4.0f)
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
.Visibility_Lambda([this]() -> EVisibility
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (PC->MasterCatalogAsset && PC->MasterCatalogAsset->ScenarioTypeEnum)
|
||||||
|
{
|
||||||
|
return EVisibility::Visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EVisibility::Collapsed;
|
||||||
|
})
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(0.45f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Scenario Type")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Saved per-scenario; pushed to PendingScenarioType at Play.")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
|
||||||
|
.AutoWrapText(true)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(0.55f).VAlign(VAlign_Center).Padding(8.0f, 0.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SAssignNew(ScenarioTypeCombo, SComboBox<TSharedPtr<FString>>)
|
||||||
|
.OptionsSource(&ScenarioTypeOptions)
|
||||||
|
.OnComboBoxOpening_Lambda([this]() { RefreshScenarioTypeOptions(); })
|
||||||
|
.OnGenerateWidget_Lambda([](TSharedPtr<FString> Item) -> TSharedRef<SWidget>
|
||||||
|
{
|
||||||
|
return SNew(STextBlock).Text(FText::FromString(Item.IsValid() ? *Item : FString()));
|
||||||
|
})
|
||||||
|
.OnSelectionChanged_Lambda([this](TSharedPtr<FString> NewItem, ESelectInfo::Type SelectType)
|
||||||
|
{
|
||||||
|
if (!NewItem.IsValid()) return;
|
||||||
|
const int32 Idx = ScenarioTypeOptions.IndexOfByKey(NewItem);
|
||||||
|
if (Idx == INDEX_NONE) return;
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
SM->ScenarioType = (uint8)Idx;
|
||||||
|
bSceneDirty = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text_Lambda([this]() -> FText
|
||||||
|
{
|
||||||
|
return FText::FromString(GetCurrentScenarioTypeDisplayName());
|
||||||
|
})
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 16.0f, 0.0f, 0.0f)
|
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 16.0f, 0.0f, 0.0f)
|
||||||
[
|
[
|
||||||
SNew(SButton)
|
SNew(SButton)
|
||||||
@@ -829,6 +893,70 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildToolbar()
|
|||||||
})
|
})
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth() [ MakeSeparator() ]
|
||||||
|
// ---- Floor cut: editor-only horizontal plane to hide upper floors ----
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth()
|
||||||
|
[
|
||||||
|
MakeToggleButton(EPS_Editor_Icon::Grid, EPS_Editor_Icon::Grid,
|
||||||
|
TAttribute<FString>::CreateLambda([this]() -> FString
|
||||||
|
{
|
||||||
|
UWorld* W = GetWorld();
|
||||||
|
UPS_Editor_FloorCutManager* FC = W ? W->GetSubsystem<UPS_Editor_FloorCutManager>() : nullptr;
|
||||||
|
return (FC && FC->IsEnabled()) ? TEXT("Floor cut On") : TEXT("Floor cut");
|
||||||
|
}),
|
||||||
|
[this]() -> bool
|
||||||
|
{
|
||||||
|
UWorld* W = GetWorld();
|
||||||
|
UPS_Editor_FloorCutManager* FC = W ? W->GetSubsystem<UPS_Editor_FloorCutManager>() : nullptr;
|
||||||
|
return FC && FC->IsEnabled();
|
||||||
|
},
|
||||||
|
[this]()
|
||||||
|
{
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
FC->SetEnabled(!FC->IsEnabled());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SBox).MinDesiredWidth(120.0f)
|
||||||
|
[
|
||||||
|
SAssignNew(FloorCutSlider, SSlider)
|
||||||
|
.MinValue(0.0f)
|
||||||
|
.MaxValue(1000.0f)
|
||||||
|
.StepSize(10.0f)
|
||||||
|
.MouseUsesStep(true)
|
||||||
|
.Value(200.0f)
|
||||||
|
.ToolTipText(FText::FromString(TEXT("Cut height above MandatoryAnchor (cm) — range 0..1000, step 10")))
|
||||||
|
.OnValueChanged_Lambda([this](float NewValue)
|
||||||
|
{
|
||||||
|
const float Snapped = FMath::RoundToFloat(NewValue / 10.f) * 10.f;
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
FC->SetHeightAboveAnchor(Snapped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (FloorCutValueText.IsValid())
|
||||||
|
{
|
||||||
|
FloorCutValueText->SetText(FText::FromString(FString::Printf(TEXT("%.0f cm"), Snapped)));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(2.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SAssignNew(FloorCutValueText, STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("200 cm")))
|
||||||
|
.Font(FPS_Editor_UIStyle::SmallFont(S))
|
||||||
|
.ColorAndOpacity(FPS_Editor_UIStyle::TextMuted(S))
|
||||||
|
.MinDesiredWidth(48.0f)
|
||||||
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1170,7 +1298,8 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
{
|
{
|
||||||
if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>())
|
if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>())
|
||||||
{
|
{
|
||||||
Sub->SetPendingScenario(SceneName, BaseLevel);
|
const uint8 TypeToSend = SpawnManager.IsValid() ? SpawnManager->ScenarioType : 0;
|
||||||
|
Sub->SetPendingScenario(SceneName, BaseLevel, TypeToSend);
|
||||||
Sub->bIsPlayMode = true;
|
Sub->bIsPlayMode = true;
|
||||||
if (Sub->EditorMapName.IsEmpty())
|
if (Sub->EditorMapName.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -1288,7 +1417,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())
|
||||||
{
|
{
|
||||||
@@ -1547,18 +1676,14 @@ void UPS_Editor_MainWidget::PopulateSceneList()
|
|||||||
// No base level in the scenario file — try to resolve the default from the catalog.
|
// No base level in the scenario file — try to resolve the default from the catalog.
|
||||||
if (!bHasValidBaseLevel && EditorPC->MasterCatalogAsset)
|
if (!bHasValidBaseLevel && EditorPC->MasterCatalogAsset)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scenario '%s' has no base level, searching for default in MasterCatalog (%d entries)"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Scenario '%s' has no base level, searching for default in MasterCatalog (%d entries)"),
|
||||||
*Name, EditorPC->MasterCatalogAsset->BaseLevels.Num());
|
*Name, EditorPC->MasterCatalogAsset->BaseLevels.Num());
|
||||||
for (const FPS_Editor_BaseLevelEntry& Entry : EditorPC->MasterCatalogAsset->BaseLevels)
|
for (const FPS_Editor_BaseLevelEntry& Entry : EditorPC->MasterCatalogAsset->BaseLevels)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT(" BaseLevel entry: DisplayName='%s', MapPath='%s', bIsDefault=%d"),
|
|
||||||
*Entry.DisplayName, *Entry.MapPath, Entry.bIsDefault ? 1 : 0);
|
|
||||||
if (Entry.bIsDefault && !Entry.DisplayName.IsEmpty())
|
if (Entry.bIsDefault && !Entry.DisplayName.IsEmpty())
|
||||||
{
|
{
|
||||||
SceneLevelName = Entry.DisplayName;
|
SceneLevelName = Entry.DisplayName;
|
||||||
bHasValidBaseLevel = BaseLevelPathMap.Contains(SceneLevelName);
|
bHasValidBaseLevel = BaseLevelPathMap.Contains(SceneLevelName);
|
||||||
UE_LOG(LogTemp, Log, TEXT(" -> Picked default '%s' (in PathMap=%d)"),
|
|
||||||
*SceneLevelName, bHasValidBaseLevel ? 1 : 0);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1593,7 +1718,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())
|
||||||
{
|
{
|
||||||
@@ -2401,7 +2526,7 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
|||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: EditableComponent has 0 properties on %s"), *Actor->GetName());
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: EditableComponent has 0 properties on %s"), *Actor->GetName());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Building %d dynamic property rows for %s"), EditComp->EditableProperties.Num(), *Actor->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Building %d dynamic property rows for %s"), EditComp->EditableProperties.Num(), *Actor->GetName());
|
||||||
|
|
||||||
const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||||
const FSlateFontInfo ValueFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
const FSlateFontInfo ValueFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||||
@@ -2429,8 +2554,6 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
|||||||
Row.DisplayName = EditComp->GetDisplayName(Entry.Path);
|
Row.DisplayName = EditComp->GetDisplayName(Entry.Path);
|
||||||
Row.Key = Entry.Path.ToString();
|
Row.Key = Entry.Path.ToString();
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolving [%s] comp='%s' prop='%s'"), *Row.Key, *Row.ComponentName, *Row.PropertyName);
|
|
||||||
|
|
||||||
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
||||||
if (!Target)
|
if (!Target)
|
||||||
{
|
{
|
||||||
@@ -2451,8 +2574,6 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
|||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Property '%s' not found on %s (%s)"), *Row.PropertyName, *Target->GetName(), *Target->GetClass()->GetName());
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Property '%s' not found on %s (%s)"), *Row.PropertyName, *Target->GetName(), *Target->GetClass()->GetName());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolved OK → %s on %s"), *Row.CachedProperty->GetName(), *Target->GetClass()->GetName());
|
|
||||||
|
|
||||||
const int32 RowIndex = DynamicPropertyRows.Num();
|
const int32 RowIndex = DynamicPropertyRows.Num();
|
||||||
|
|
||||||
// Label
|
// Label
|
||||||
@@ -2703,7 +2824,7 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
|||||||
DynamicPropertyRows.Add(Row);
|
DynamicPropertyRows.Add(Row);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Built %d dynamic property row(s)"), DynamicPropertyRows.Num());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Built %d dynamic property row(s)"), DynamicPropertyRows.Num());
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_Editor_MainWidget::RefreshDynamicPropertyValues()
|
void UPS_Editor_MainWidget::RefreshDynamicPropertyValues()
|
||||||
@@ -3033,7 +3154,7 @@ void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex)
|
|||||||
if (UFunction* RepFunc = Target->FindFunction(Row.CachedProperty->RepNotifyFunc))
|
if (UFunction* RepFunc = Target->FindFunction(Row.CachedProperty->RepNotifyFunc))
|
||||||
{
|
{
|
||||||
Target->ProcessEvent(RepFunc, nullptr);
|
Target->ProcessEvent(RepFunc, nullptr);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Called %s on %s"), *Row.CachedProperty->RepNotifyFunc.ToString(), *Target->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Called %s on %s"), *Row.CachedProperty->RepNotifyFunc.ToString(), *Target->GetName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3060,7 +3181,7 @@ void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Property %s changed: %s → %s"), *Row.Key, *OldValue, *NewValue);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Property %s changed: %s → %s"), *Row.Key, *OldValue, *NewValue);
|
||||||
|
|
||||||
// Always notify property changed
|
// Always notify property changed
|
||||||
const bool bImplementsInterface = Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass());
|
const bool bImplementsInterface = Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass());
|
||||||
@@ -3344,10 +3465,57 @@ void UPS_Editor_MainWidget::OpenSettingsPopup()
|
|||||||
{
|
{
|
||||||
if (SettingsPopup.IsValid())
|
if (SettingsPopup.IsValid())
|
||||||
{
|
{
|
||||||
|
// Refresh enum-driven options each time the popup opens — covers the case where the
|
||||||
|
// MasterCatalogAsset (or its ScenarioTypeEnum) wasn't set at widget Construct time.
|
||||||
|
RefreshScenarioTypeOptions();
|
||||||
SettingsPopup->SetVisibility(EVisibility::Visible);
|
SettingsPopup->SetVisibility(EVisibility::Visible);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::RefreshScenarioTypeOptions()
|
||||||
|
{
|
||||||
|
ScenarioTypeOptions.Empty();
|
||||||
|
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (PC->MasterCatalogAsset && PC->MasterCatalogAsset->ScenarioTypeEnum)
|
||||||
|
{
|
||||||
|
UEnum* E = PC->MasterCatalogAsset->ScenarioTypeEnum;
|
||||||
|
// NumEnums()-1 to skip the auto-generated _MAX entry on regular UENUMs.
|
||||||
|
// User-defined enums (UUserDefinedEnum) don't have _MAX but the last "MAX" entry
|
||||||
|
// is hidden behind the `bIsHidden` meta — we drop the last one defensively.
|
||||||
|
const int32 Count = FMath::Max(0, E->NumEnums() - 1);
|
||||||
|
for (int32 i = 0; i < Count; ++i)
|
||||||
|
{
|
||||||
|
ScenarioTypeOptions.Add(MakeShared<FString>(E->GetDisplayNameTextByIndex(i).ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ScenarioTypeCombo.IsValid())
|
||||||
|
{
|
||||||
|
ScenarioTypeCombo->RefreshOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UPS_Editor_MainWidget::GetCurrentScenarioTypeDisplayName() const
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (PC->MasterCatalogAsset && PC->MasterCatalogAsset->ScenarioTypeEnum)
|
||||||
|
{
|
||||||
|
UEnum* E = PC->MasterCatalogAsset->ScenarioTypeEnum;
|
||||||
|
const int32 Count = FMath::Max(0, E->NumEnums() - 1);
|
||||||
|
if (SM->ScenarioType < Count)
|
||||||
|
{
|
||||||
|
return E->GetDisplayNameTextByIndex(SM->ScenarioType).ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|
||||||
void UPS_Editor_MainWidget::CloseSettingsPopup()
|
void UPS_Editor_MainWidget::CloseSettingsPopup()
|
||||||
{
|
{
|
||||||
if (SettingsPopup.IsValid())
|
if (SettingsPopup.IsValid())
|
||||||
@@ -4039,7 +4207,7 @@ void UPS_Editor_MainWidget::RebuildTimelineTracksList()
|
|||||||
// previously-selected actor (e.g. a spline still in overlay-visible state)
|
// previously-selected actor (e.g. a spline still in overlay-visible state)
|
||||||
// is properly released and its handles/highlight go away.
|
// is properly released and its handles/highlight go away.
|
||||||
AActor* TrackActor = TS2->FindActorById(Tr.ActorId);
|
AActor* TrackActor = TS2->FindActorById(Tr.ActorId);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Key click — track=%d key=%d ActorId=%s resolved=%s"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Key click — track=%d key=%d ActorId=%s resolved=%s"),
|
||||||
CapturedTrack, CapturedKey, *Tr.ActorId.ToString(), TrackActor ? *TrackActor->GetName() : TEXT("null"));
|
CapturedTrack, CapturedKey, *Tr.ActorId.ToString(), TrackActor ? *TrackActor->GetName() : TEXT("null"));
|
||||||
if (TrackActor)
|
if (TrackActor)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ private:
|
|||||||
TSharedPtr<STextBlock> SnapText;
|
TSharedPtr<STextBlock> SnapText;
|
||||||
TSharedPtr<SEditableTextBox> SnapSizeField;
|
TSharedPtr<SEditableTextBox> SnapSizeField;
|
||||||
|
|
||||||
|
// Floor-cut UI (editor-only horizontal plane that hides primitives above AnchorZ+H)
|
||||||
|
TSharedPtr<STextBlock> FloorCutValueText;
|
||||||
|
TSharedPtr<class SSlider> FloorCutSlider;
|
||||||
|
|
||||||
// Spawn catalog
|
// Spawn catalog
|
||||||
TSharedPtr<SVerticalBox> SpawnListContainer;
|
TSharedPtr<SVerticalBox> SpawnListContainer;
|
||||||
|
|
||||||
@@ -134,13 +138,19 @@ private:
|
|||||||
void ConfirmBaseLevelSelection();
|
void ConfirmBaseLevelSelection();
|
||||||
void CancelBaseLevelPopup();
|
void CancelBaseLevelPopup();
|
||||||
|
|
||||||
// Global Settings popup (per-scenario nav option + session-only navmesh visu).
|
// Global Settings popup (per-scenario nav option + session-only navmesh visu + scenario type).
|
||||||
TSharedPtr<SWidget> SettingsPopup;
|
TSharedPtr<SWidget> SettingsPopup;
|
||||||
void OpenSettingsPopup();
|
void OpenSettingsPopup();
|
||||||
void CloseSettingsPopup();
|
void CloseSettingsPopup();
|
||||||
void OnUseDynamicNavigationToggled(ECheckBoxState State);
|
void OnUseDynamicNavigationToggled(ECheckBoxState State);
|
||||||
void OnShowNavMeshToggled(ECheckBoxState State);
|
void OnShowNavMeshToggled(ECheckBoxState State);
|
||||||
|
|
||||||
|
// Scenario type dropdown — populated from MasterCatalog::ScenarioTypeEnum, hidden when null.
|
||||||
|
TArray<TSharedPtr<FString>> ScenarioTypeOptions;
|
||||||
|
TSharedPtr<SComboBox<TSharedPtr<FString>>> ScenarioTypeCombo;
|
||||||
|
void RefreshScenarioTypeOptions();
|
||||||
|
FString GetCurrentScenarioTypeDisplayName() const;
|
||||||
|
|
||||||
// Transform fields
|
// Transform fields
|
||||||
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
||||||
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
#include "DesktopPlatformModule.h"
|
#include "DesktopPlatformModule.h"
|
||||||
#include "PS_Editor_GameInstanceSubsystem.h"
|
#include "PS_Editor_GameInstanceSubsystem.h"
|
||||||
#include "PS_Editor_TimelineSubsystem.h"
|
#include "PS_Editor_TimelineSubsystem.h"
|
||||||
|
#include "PS_Editor_FloorCutManager.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
|
|
||||||
// Forward declaration — the real definition lives further down with the timeline UI block,
|
// Forward declaration — the real definition lives further down with the timeline UI block,
|
||||||
@@ -550,6 +551,69 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::RebuildWidget()
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
// Scenario Type dropdown — visible only when MasterCatalog::ScenarioTypeEnum is set.
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(0.0f, 8.0f, 0.0f, 4.0f)
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
.Visibility_Lambda([this]() -> EVisibility
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (PC->MasterCatalogAsset && PC->MasterCatalogAsset->ScenarioTypeEnum)
|
||||||
|
{
|
||||||
|
return EVisibility::Visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EVisibility::Collapsed;
|
||||||
|
})
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(0.45f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Scenario Type")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Saved per-scenario; pushed to PendingScenarioType at Play.")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
|
||||||
|
.AutoWrapText(true)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(0.55f).VAlign(VAlign_Center).Padding(8.0f, 0.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SAssignNew(ScenarioTypeCombo, SComboBox<TSharedPtr<FString>>)
|
||||||
|
.OptionsSource(&ScenarioTypeOptions)
|
||||||
|
.OnComboBoxOpening_Lambda([this]() { RefreshScenarioTypeOptions(); })
|
||||||
|
.OnGenerateWidget_Lambda([](TSharedPtr<FString> Item) -> TSharedRef<SWidget>
|
||||||
|
{
|
||||||
|
return SNew(STextBlock).Text(FText::FromString(Item.IsValid() ? *Item : FString()));
|
||||||
|
})
|
||||||
|
.OnSelectionChanged_Lambda([this](TSharedPtr<FString> NewItem, ESelectInfo::Type SelectType)
|
||||||
|
{
|
||||||
|
if (!NewItem.IsValid()) return;
|
||||||
|
const int32 Idx = ScenarioTypeOptions.IndexOfByKey(NewItem);
|
||||||
|
if (Idx == INDEX_NONE) return;
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
SM->ScenarioType = (uint8)Idx;
|
||||||
|
bSceneDirty = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text_Lambda([this]() -> FText
|
||||||
|
{
|
||||||
|
return FText::FromString(GetCurrentScenarioTypeDisplayName());
|
||||||
|
})
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
// Close button
|
// Close button
|
||||||
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 16.0f, 0.0f, 0.0f)
|
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 16.0f, 0.0f, 0.0f)
|
||||||
[
|
[
|
||||||
@@ -677,6 +741,69 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::BuildToolbar()
|
|||||||
})
|
})
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
// ---- Floor cut: editor-only horizontal plane to hide upper floors ----
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(16.0f, 0.0f, 2.0f, 0.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.ToolTipText(FText::FromString(TEXT("Toggle floor cut. Hides primitives whose bounds-min Z is above (Anchor.Z + Height)")))
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
FC->SetEnabled(!FC->IsEnabled());
|
||||||
|
if (FloorCutToggleText.IsValid())
|
||||||
|
{
|
||||||
|
FloorCutToggleText->SetText(FText::FromString(FC->IsEnabled() ? TEXT("Floor cut: On") : TEXT("Floor cut: Off")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SAssignNew(FloorCutToggleText, STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Floor cut: Off")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(4.0f, 0.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SBox).MinDesiredWidth(120.0f)
|
||||||
|
[
|
||||||
|
SAssignNew(FloorCutSlider, SSlider)
|
||||||
|
.MinValue(0.0f)
|
||||||
|
.MaxValue(1000.0f)
|
||||||
|
.StepSize(10.0f)
|
||||||
|
.MouseUsesStep(true)
|
||||||
|
.Value(200.0f)
|
||||||
|
.ToolTipText(FText::FromString(TEXT("Cut height above MandatoryAnchor (cm) — range 0..1000, step 10")))
|
||||||
|
.OnValueChanged_Lambda([this](float NewValue)
|
||||||
|
{
|
||||||
|
// Snap to 10 cm — SSlider's MouseUsesStep already does this for clicks,
|
||||||
|
// but a drag still emits raw floats, so we re-snap here.
|
||||||
|
const float Snapped = FMath::RoundToFloat(NewValue / 10.f) * 10.f;
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
FC->SetHeightAboveAnchor(Snapped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (FloorCutValueText.IsValid())
|
||||||
|
{
|
||||||
|
FloorCutValueText->SetText(FText::FromString(FString::Printf(TEXT("%.0f cm"), Snapped)));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SAssignNew(FloorCutValueText, STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("200 cm")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.MinDesiredWidth(48.0f)
|
||||||
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1011,7 +1138,8 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::BuildSceneButtons()
|
|||||||
{
|
{
|
||||||
if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>())
|
if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>())
|
||||||
{
|
{
|
||||||
Sub->SetPendingScenario(SceneName, BaseLevel);
|
const uint8 TypeToSend = SpawnManager.IsValid() ? SpawnManager->ScenarioType : 0;
|
||||||
|
Sub->SetPendingScenario(SceneName, BaseLevel, TypeToSend);
|
||||||
Sub->bIsPlayMode = true;
|
Sub->bIsPlayMode = true;
|
||||||
if (Sub->EditorMapName.IsEmpty())
|
if (Sub->EditorMapName.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -1128,7 +1256,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())
|
||||||
{
|
{
|
||||||
@@ -1381,18 +1509,14 @@ void UPS_Editor_MainWidget_Legacy::PopulateSceneList()
|
|||||||
// No base level in the scenario file — try to resolve the default from the catalog.
|
// No base level in the scenario file — try to resolve the default from the catalog.
|
||||||
if (!bHasValidBaseLevel && EditorPC->MasterCatalogAsset)
|
if (!bHasValidBaseLevel && EditorPC->MasterCatalogAsset)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scenario '%s' has no base level, searching for default in MasterCatalog (%d entries)"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Scenario '%s' has no base level, searching for default in MasterCatalog (%d entries)"),
|
||||||
*Name, EditorPC->MasterCatalogAsset->BaseLevels.Num());
|
*Name, EditorPC->MasterCatalogAsset->BaseLevels.Num());
|
||||||
for (const FPS_Editor_BaseLevelEntry& Entry : EditorPC->MasterCatalogAsset->BaseLevels)
|
for (const FPS_Editor_BaseLevelEntry& Entry : EditorPC->MasterCatalogAsset->BaseLevels)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT(" BaseLevel entry: DisplayName='%s', MapPath='%s', bIsDefault=%d"),
|
|
||||||
*Entry.DisplayName, *Entry.MapPath, Entry.bIsDefault ? 1 : 0);
|
|
||||||
if (Entry.bIsDefault && !Entry.DisplayName.IsEmpty())
|
if (Entry.bIsDefault && !Entry.DisplayName.IsEmpty())
|
||||||
{
|
{
|
||||||
SceneLevelName = Entry.DisplayName;
|
SceneLevelName = Entry.DisplayName;
|
||||||
bHasValidBaseLevel = BaseLevelPathMap.Contains(SceneLevelName);
|
bHasValidBaseLevel = BaseLevelPathMap.Contains(SceneLevelName);
|
||||||
UE_LOG(LogTemp, Log, TEXT(" -> Picked default '%s' (in PathMap=%d)"),
|
|
||||||
*SceneLevelName, bHasValidBaseLevel ? 1 : 0);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1427,7 +1551,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())
|
||||||
{
|
{
|
||||||
@@ -2076,7 +2200,7 @@ void UPS_Editor_MainWidget_Legacy::RebuildDynamicProperties()
|
|||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: EditableComponent has 0 properties on %s"), *Actor->GetName());
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: EditableComponent has 0 properties on %s"), *Actor->GetName());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Building %d dynamic property rows for %s"), EditComp->EditableProperties.Num(), *Actor->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Building %d dynamic property rows for %s"), EditComp->EditableProperties.Num(), *Actor->GetName());
|
||||||
|
|
||||||
const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||||
const FSlateFontInfo ValueFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
const FSlateFontInfo ValueFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||||
@@ -2104,8 +2228,6 @@ void UPS_Editor_MainWidget_Legacy::RebuildDynamicProperties()
|
|||||||
Row.DisplayName = EditComp->GetDisplayName(Entry.Path);
|
Row.DisplayName = EditComp->GetDisplayName(Entry.Path);
|
||||||
Row.Key = Entry.Path.ToString();
|
Row.Key = Entry.Path.ToString();
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolving [%s] comp='%s' prop='%s'"), *Row.Key, *Row.ComponentName, *Row.PropertyName);
|
|
||||||
|
|
||||||
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
||||||
if (!Target)
|
if (!Target)
|
||||||
{
|
{
|
||||||
@@ -2126,8 +2248,6 @@ void UPS_Editor_MainWidget_Legacy::RebuildDynamicProperties()
|
|||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Property '%s' not found on %s (%s)"), *Row.PropertyName, *Target->GetName(), *Target->GetClass()->GetName());
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Property '%s' not found on %s (%s)"), *Row.PropertyName, *Target->GetName(), *Target->GetClass()->GetName());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolved OK → %s on %s"), *Row.CachedProperty->GetName(), *Target->GetClass()->GetName());
|
|
||||||
|
|
||||||
const int32 RowIndex = DynamicPropertyRows.Num();
|
const int32 RowIndex = DynamicPropertyRows.Num();
|
||||||
|
|
||||||
// Label
|
// Label
|
||||||
@@ -2374,7 +2494,7 @@ void UPS_Editor_MainWidget_Legacy::RebuildDynamicProperties()
|
|||||||
DynamicPropertyRows.Add(Row);
|
DynamicPropertyRows.Add(Row);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Built %d dynamic property row(s)"), DynamicPropertyRows.Num());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Built %d dynamic property row(s)"), DynamicPropertyRows.Num());
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_Editor_MainWidget_Legacy::RefreshDynamicPropertyValues()
|
void UPS_Editor_MainWidget_Legacy::RefreshDynamicPropertyValues()
|
||||||
@@ -2707,7 +2827,7 @@ void UPS_Editor_MainWidget_Legacy::ApplyPropertyFromUI(int32 RowIndex)
|
|||||||
if (UFunction* RepFunc = Target->FindFunction(Row.CachedProperty->RepNotifyFunc))
|
if (UFunction* RepFunc = Target->FindFunction(Row.CachedProperty->RepNotifyFunc))
|
||||||
{
|
{
|
||||||
Target->ProcessEvent(RepFunc, nullptr);
|
Target->ProcessEvent(RepFunc, nullptr);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Called %s on %s"), *Row.CachedProperty->RepNotifyFunc.ToString(), *Target->GetName());
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Called %s on %s"), *Row.CachedProperty->RepNotifyFunc.ToString(), *Target->GetName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2734,7 +2854,7 @@ void UPS_Editor_MainWidget_Legacy::ApplyPropertyFromUI(int32 RowIndex)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Property %s changed: %s → %s"), *Row.Key, *OldValue, *NewValue);
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Property %s changed: %s → %s"), *Row.Key, *OldValue, *NewValue);
|
||||||
|
|
||||||
// Always notify property changed
|
// Always notify property changed
|
||||||
const bool bImplementsInterface = Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass());
|
const bool bImplementsInterface = Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass());
|
||||||
@@ -3018,10 +3138,54 @@ void UPS_Editor_MainWidget_Legacy::OpenSettingsPopup()
|
|||||||
{
|
{
|
||||||
if (SettingsPopup.IsValid())
|
if (SettingsPopup.IsValid())
|
||||||
{
|
{
|
||||||
|
// Refresh enum-driven options each time the popup opens — covers the case where the
|
||||||
|
// MasterCatalogAsset (or its ScenarioTypeEnum) wasn't set at widget Construct time.
|
||||||
|
RefreshScenarioTypeOptions();
|
||||||
SettingsPopup->SetVisibility(EVisibility::Visible);
|
SettingsPopup->SetVisibility(EVisibility::Visible);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_Legacy::RefreshScenarioTypeOptions()
|
||||||
|
{
|
||||||
|
ScenarioTypeOptions.Empty();
|
||||||
|
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (PC->MasterCatalogAsset && PC->MasterCatalogAsset->ScenarioTypeEnum)
|
||||||
|
{
|
||||||
|
UEnum* E = PC->MasterCatalogAsset->ScenarioTypeEnum;
|
||||||
|
const int32 Count = FMath::Max(0, E->NumEnums() - 1);
|
||||||
|
for (int32 i = 0; i < Count; ++i)
|
||||||
|
{
|
||||||
|
ScenarioTypeOptions.Add(MakeShared<FString>(E->GetDisplayNameTextByIndex(i).ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ScenarioTypeCombo.IsValid())
|
||||||
|
{
|
||||||
|
ScenarioTypeCombo->RefreshOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UPS_Editor_MainWidget_Legacy::GetCurrentScenarioTypeDisplayName() const
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (PC->MasterCatalogAsset && PC->MasterCatalogAsset->ScenarioTypeEnum)
|
||||||
|
{
|
||||||
|
UEnum* E = PC->MasterCatalogAsset->ScenarioTypeEnum;
|
||||||
|
const int32 Count = FMath::Max(0, E->NumEnums() - 1);
|
||||||
|
if (SM->ScenarioType < Count)
|
||||||
|
{
|
||||||
|
return E->GetDisplayNameTextByIndex(SM->ScenarioType).ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|
||||||
void UPS_Editor_MainWidget_Legacy::CloseSettingsPopup()
|
void UPS_Editor_MainWidget_Legacy::CloseSettingsPopup()
|
||||||
{
|
{
|
||||||
if (SettingsPopup.IsValid())
|
if (SettingsPopup.IsValid())
|
||||||
@@ -3689,7 +3853,7 @@ void UPS_Editor_MainWidget_Legacy::RebuildTimelineTracksList()
|
|||||||
// previously-selected actor (e.g. a spline still in overlay-visible state)
|
// previously-selected actor (e.g. a spline still in overlay-visible state)
|
||||||
// is properly released and its handles/highlight go away.
|
// is properly released and its handles/highlight go away.
|
||||||
AActor* TrackActor = TS2->FindActorById(Tr.ActorId);
|
AActor* TrackActor = TS2->FindActorById(Tr.ActorId);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Key click — track=%d key=%d ActorId=%s resolved=%s"),
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: Key click — track=%d key=%d ActorId=%s resolved=%s"),
|
||||||
CapturedTrack, CapturedKey, *Tr.ActorId.ToString(), TrackActor ? *TrackActor->GetName() : TEXT("null"));
|
CapturedTrack, CapturedKey, *Tr.ActorId.ToString(), TrackActor ? *TrackActor->GetName() : TEXT("null"));
|
||||||
if (TrackActor)
|
if (TrackActor)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ private:
|
|||||||
TSharedPtr<STextBlock> SnapText;
|
TSharedPtr<STextBlock> SnapText;
|
||||||
TSharedPtr<SEditableTextBox> SnapSizeField;
|
TSharedPtr<SEditableTextBox> SnapSizeField;
|
||||||
|
|
||||||
|
// Floor-cut UI (editor-only horizontal plane that hides primitives above AnchorZ+H)
|
||||||
|
TSharedPtr<STextBlock> FloorCutToggleText;
|
||||||
|
TSharedPtr<class SSlider> FloorCutSlider;
|
||||||
|
TSharedPtr<STextBlock> FloorCutValueText;
|
||||||
|
|
||||||
// Spawn catalog
|
// Spawn catalog
|
||||||
TSharedPtr<SVerticalBox> SpawnListContainer;
|
TSharedPtr<SVerticalBox> SpawnListContainer;
|
||||||
|
|
||||||
@@ -107,7 +112,7 @@ private:
|
|||||||
void ConfirmBaseLevelSelection();
|
void ConfirmBaseLevelSelection();
|
||||||
void CancelBaseLevelPopup();
|
void CancelBaseLevelPopup();
|
||||||
|
|
||||||
// Global Settings popup (per-scenario nav option + session-only navmesh visu).
|
// Global Settings popup (per-scenario nav option + session-only navmesh visu + scenario type).
|
||||||
TSharedPtr<SWidget> SettingsPopup;
|
TSharedPtr<SWidget> SettingsPopup;
|
||||||
void OpenSettingsPopup();
|
void OpenSettingsPopup();
|
||||||
void CloseSettingsPopup();
|
void CloseSettingsPopup();
|
||||||
@@ -116,6 +121,12 @@ private:
|
|||||||
/** Toggle the navmesh visualization (P key handler). */
|
/** Toggle the navmesh visualization (P key handler). */
|
||||||
void ToggleShowNavMesh();
|
void ToggleShowNavMesh();
|
||||||
|
|
||||||
|
// Scenario type dropdown — populated from MasterCatalog::ScenarioTypeEnum, hidden when null.
|
||||||
|
TArray<TSharedPtr<FString>> ScenarioTypeOptions;
|
||||||
|
TSharedPtr<SComboBox<TSharedPtr<FString>>> ScenarioTypeCombo;
|
||||||
|
void RefreshScenarioTypeOptions();
|
||||||
|
FString GetCurrentScenarioTypeDisplayName() const;
|
||||||
|
|
||||||
// Transform fields
|
// Transform fields
|
||||||
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
||||||
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_SceneSerializer.h"
|
#include "PS_Editor_SceneSerializer.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
|
#include "PS_Editor_FloorCutManager.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
|
||||||
void UPS_Editor_MainWidget_UMG::SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager)
|
void UPS_Editor_MainWidget_UMG::SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager)
|
||||||
{
|
{
|
||||||
@@ -130,7 +132,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;
|
||||||
}
|
}
|
||||||
@@ -161,6 +163,54 @@ void UPS_Editor_MainWidget_UMG::ClearSelection()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------- Floor cut --------------------
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_UMG::SetFloorCutEnabled(bool bEnabled)
|
||||||
|
{
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
FC->SetEnabled(bEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_UMG::SetFloorCutHeightCm(float HeightCm)
|
||||||
|
{
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
FC->SetHeightAboveAnchor(HeightCm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UPS_Editor_MainWidget_UMG::IsFloorCutEnabled() const
|
||||||
|
{
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
return FC->IsEnabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float UPS_Editor_MainWidget_UMG::GetFloorCutHeightCm() const
|
||||||
|
{
|
||||||
|
if (UWorld* W = GetWorld())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
|
||||||
|
{
|
||||||
|
return FC->GetHeightAboveAnchor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 200.f;
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------- Getters --------------------
|
// -------------------- Getters --------------------
|
||||||
|
|
||||||
EPS_Editor_TransformMode UPS_Editor_MainWidget_UMG::GetCurrentTransformMode() const
|
EPS_Editor_TransformMode UPS_Editor_MainWidget_UMG::GetCurrentTransformMode() const
|
||||||
|
|||||||
@@ -70,6 +70,22 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "PS Editor|Selection")
|
UFUNCTION(BlueprintCallable, Category = "PS Editor|Selection")
|
||||||
void ClearSelection();
|
void ClearSelection();
|
||||||
|
|
||||||
|
// ---- Floor cut (editor-only horizontal plane that hides upper floors) ----
|
||||||
|
|
||||||
|
/** Enable/disable the floor cut. Slider value drives the cut height in cm above the
|
||||||
|
* MandatoryAnchor. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS Editor|FloorCut")
|
||||||
|
void SetFloorCutEnabled(bool bEnabled);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS Editor|FloorCut")
|
||||||
|
void SetFloorCutHeightCm(float HeightCm);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "PS Editor|FloorCut")
|
||||||
|
bool IsFloorCutEnabled() const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "PS Editor|FloorCut")
|
||||||
|
float GetFloorCutHeightCm() const;
|
||||||
|
|
||||||
// ==================== BP-PURE GETTERS ====================
|
// ==================== BP-PURE GETTERS ====================
|
||||||
UFUNCTION(BlueprintPure, Category = "PS Editor|Tools")
|
UFUNCTION(BlueprintPure, Category = "PS Editor|Tools")
|
||||||
EPS_Editor_TransformMode GetCurrentTransformMode() const;
|
EPS_Editor_TransformMode GetCurrentTransformMode() const;
|
||||||
|
|||||||
@@ -185,6 +185,19 @@ void FPS_Editor_BlueprintFromMesh::CreateBlueprintForAsset(UObject* MeshAsset)
|
|||||||
USCS_Node* EditableNode = SCS->CreateNode(UPS_Editor_EditableComponent::StaticClass(), FName("PS_Editor_Editable"));
|
USCS_Node* EditableNode = SCS->CreateNode(UPS_Editor_EditableComponent::StaticClass(), FName("PS_Editor_Editable"));
|
||||||
SCS->AddNode(EditableNode);
|
SCS->AddNode(EditableNode);
|
||||||
|
|
||||||
|
// Enable replication on the generated BP — without it the actor is server-local only and
|
||||||
|
// clients never see it spawn. SetReplicates() is the public path (the bReplicates field
|
||||||
|
// itself is protected). SetReplicateMovement is paired so any server-side transform change
|
||||||
|
// (timeline keyframe, scripted move) propagates to clients.
|
||||||
|
if (UClass* GeneratedClass = Blueprint->GeneratedClass)
|
||||||
|
{
|
||||||
|
if (AActor* CDO = Cast<AActor>(GeneratedClass->GetDefaultObject()))
|
||||||
|
{
|
||||||
|
CDO->SetReplicates(true);
|
||||||
|
CDO->SetReplicateMovement(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Compile and save
|
// Compile and save
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
|
||||||
FKismetEditorUtilities::CompileBlueprint(Blueprint);
|
FKismetEditorUtilities::CompileBlueprint(Blueprint);
|
||||||
|
|||||||
Reference in New Issue
Block a user