Standalone editor: scenario workflow, BaseLevel combo, ReturnToEditor
Editor workflow: - Removed auto-detection of BaseLevel (strip _Editor). Editor is standalone. - User selects BaseLevel from ComboBox (populated from MasterCatalog.BaseLevels) - "None" option for editing without a base level - Selecting BaseLevel: clears current scenario, hides persistent level actors, loads sublevel. Selecting "None" restores persistent level actors. UI terminology: Scene → Scenario (save field, buttons, notifications) GameInstanceSubsystem: - Renamed PendingSceneName → PendingScenarioName - Added LastEditedScenarioName/LastEditedBaseLevel (persist after Consume) - SetPendingScenario() stores in both Pending and LastEdited - ReturnToEditor() restores Pending from LastEdited, opens editor map - EditorMapName auto-saved on first Play, used by ReturnToEditor - OpenEditor() / ReturnToEditor() callable from gameplay BP Play button: resets input mode before OpenLevel (fixes cursor in FirstPerson GM) Test assets: FirstPerson template for testing Play/ReturnToEditor flow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,16 +1,46 @@
|
||||
#include "PS_Editor_GameInstanceSubsystem.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
void UPS_Editor_GameInstanceSubsystem::SetPendingScene(const FString& SceneName, const FString& BaseLevel)
|
||||
void UPS_Editor_GameInstanceSubsystem::SetPendingScenario(const FString& ScenarioName, const FString& BaseLevel)
|
||||
{
|
||||
PendingSceneName = SceneName;
|
||||
PendingScenarioName = ScenarioName;
|
||||
PendingBaseLevel = BaseLevel;
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Pending scene set: '%s' (level: '%s')"), *SceneName, *BaseLevel);
|
||||
LastEditedScenarioName = ScenarioName;
|
||||
LastEditedBaseLevel = BaseLevel;
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Pending scenario set: '%s' (base level: '%s')"), *ScenarioName, *BaseLevel);
|
||||
}
|
||||
|
||||
FString UPS_Editor_GameInstanceSubsystem::ConsumePendingScene()
|
||||
FString UPS_Editor_GameInstanceSubsystem::ConsumePendingScenario()
|
||||
{
|
||||
FString Result = PendingSceneName;
|
||||
PendingSceneName.Empty();
|
||||
FString Result = PendingScenarioName;
|
||||
PendingScenarioName.Empty();
|
||||
PendingBaseLevel.Empty();
|
||||
// LastEdited fields are NOT cleared — ReturnToEditor uses them
|
||||
return Result;
|
||||
}
|
||||
|
||||
void UPS_Editor_GameInstanceSubsystem::OpenEditor(const UObject* WorldContextObject)
|
||||
{
|
||||
if (EditorMapName.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: EditorMapName not set on GameInstanceSubsystem"));
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Opening editor '%s' (scenario: '%s', base level: '%s')"),
|
||||
*EditorMapName, *PendingScenarioName, *PendingBaseLevel);
|
||||
|
||||
UGameplayStatics::OpenLevel(WorldContextObject, FName(*EditorMapName), true, TEXT(""));
|
||||
}
|
||||
|
||||
void UPS_Editor_GameInstanceSubsystem::ReturnToEditor(const UObject* WorldContextObject)
|
||||
{
|
||||
// Restore pending from LastEdited so the editor picks them up
|
||||
PendingScenarioName = LastEditedScenarioName;
|
||||
PendingBaseLevel = LastEditedBaseLevel;
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Returning to editor (scenario: '%s', base level: '%s')"),
|
||||
*PendingScenarioName, *PendingBaseLevel);
|
||||
|
||||
OpenEditor(WorldContextObject);
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
/**
|
||||
* Lightweight subsystem that survives between level transitions (OpenLevel).
|
||||
* Used to pass the scene name from the editor to the gameplay level.
|
||||
* Used to pass scenario/base level info between PROSERVE and the editor.
|
||||
*
|
||||
* Usage from Blueprint:
|
||||
* Get Game Instance → Get Subsystem (PS_Editor_GameInstanceSubsystem) → Pending Scene Name
|
||||
* Get Game Instance → Get Subsystem (PS_Editor_GameInstanceSubsystem) → Pending Scenario Name
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_EDITOR_API UPS_Editor_GameInstanceSubsystem : public UGameInstanceSubsystem
|
||||
@@ -17,19 +17,38 @@ class PS_EDITOR_API UPS_Editor_GameInstanceSubsystem : public UGameInstanceSubsy
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Scene name to load on the next level. Set by PS_Editor Play button, read by gameplay GameMode. */
|
||||
/** Scenario name to load on the next level. Set by PROSERVE or Play button, consumed on load. */
|
||||
UPROPERTY(BlueprintReadWrite, Category = "PS Editor")
|
||||
FString PendingSceneName;
|
||||
FString PendingScenarioName;
|
||||
|
||||
/** Base level name associated with the pending scene. */
|
||||
/** Base level name associated with the pending scenario. */
|
||||
UPROPERTY(BlueprintReadWrite, Category = "PS Editor")
|
||||
FString PendingBaseLevel;
|
||||
|
||||
/** Helper: set both values at once. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
void SetPendingScene(const FString& SceneName, const FString& BaseLevel);
|
||||
/** Last scenario/base level edited. Persists after consume — used by ReturnToEditor. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "PS Editor")
|
||||
FString LastEditedScenarioName;
|
||||
|
||||
/** Helper: consume the pending scene (reads and clears). */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "PS Editor")
|
||||
FString LastEditedBaseLevel;
|
||||
|
||||
/** Name of the editor map to return to. Set this once at startup. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
FString EditorMapName;
|
||||
|
||||
/** Set pending scenario + base level (for opening the editor or gameplay). */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
FString ConsumePendingScene();
|
||||
void SetPendingScenario(const FString& ScenarioName, const FString& BaseLevel);
|
||||
|
||||
/** Consume the pending scenario (reads and clears). */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
FString ConsumePendingScenario();
|
||||
|
||||
/** Open the editor with the current pending scenario + base level. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor", meta = (WorldContext = "WorldContextObject"))
|
||||
void OpenEditor(const UObject* WorldContextObject);
|
||||
|
||||
/** Return to the editor, keeping the current base level and scenario. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor", meta = (WorldContext = "WorldContextObject"))
|
||||
void ReturnToEditor(const UObject* WorldContextObject);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
#include "PS_Editor_PointPlaceable.h"
|
||||
#include "Engine/LevelStreamingDynamic.h"
|
||||
#include "PS_Editor_GameInstanceSubsystem.h"
|
||||
#include "GameFramework/GameModeBase.h"
|
||||
#include "GameFramework/GameStateBase.h"
|
||||
#include "GameFramework/PlayerState.h"
|
||||
|
||||
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
||||
{
|
||||
@@ -43,24 +46,7 @@ void APS_Editor_PlayerController::BeginPlay()
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-detect base level: strip "_Editor"/"_editor" from map name, match against BaseLevels
|
||||
FString MapName = GetWorld()->GetMapName();
|
||||
MapName.RemoveFromStart(GetWorld()->StreamingLevelsPrefix);
|
||||
|
||||
FString Candidate = MapName;
|
||||
Candidate.RemoveFromEnd(TEXT("_Editor"));
|
||||
Candidate.RemoveFromEnd(TEXT("_editor"));
|
||||
|
||||
if (Master->BaseLevels.Contains(Candidate))
|
||||
{
|
||||
CurrentBaseLevel = Candidate;
|
||||
}
|
||||
else if (Master->BaseLevels.Num() > 0)
|
||||
{
|
||||
CurrentBaseLevel = Master->BaseLevels[0];
|
||||
}
|
||||
|
||||
// Check if PROSERVE set a pending base level via the subsystem
|
||||
// Check if PROSERVE set a pending base level + scenario via the subsystem
|
||||
if (UGameInstance* GI = GetGameInstance())
|
||||
{
|
||||
if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>())
|
||||
@@ -68,19 +54,18 @@ void APS_Editor_PlayerController::BeginPlay()
|
||||
if (!Sub->PendingBaseLevel.IsEmpty())
|
||||
{
|
||||
LoadBaseLevelAsSublevel(Sub->PendingBaseLevel);
|
||||
// Also load pending scene if set
|
||||
if (!Sub->PendingSceneName.IsEmpty() && SpawnManager)
|
||||
if (!Sub->PendingScenarioName.IsEmpty() && SpawnManager)
|
||||
{
|
||||
SceneSerializer->LoadScene(Sub->PendingSceneName, SpawnManager);
|
||||
SceneSerializer->LoadScene(Sub->PendingScenarioName, SpawnManager);
|
||||
}
|
||||
Sub->PendingSceneName.Empty();
|
||||
Sub->PendingScenarioName.Empty();
|
||||
Sub->PendingBaseLevel.Empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: MasterCatalog loaded (%d catalogs, %d entries). BaseLevel: '%s' (map: %s)"),
|
||||
Master->Catalogs.Num(), SpawnManager->GetResolvedEntries().Num(), *CurrentBaseLevel, *MapName);
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: MasterCatalog loaded (%d catalogs, %d entries). BaseLevel: '%s'"),
|
||||
Master->Catalogs.Num(), SpawnManager->GetResolvedEntries().Num(), *CurrentBaseLevel);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -206,9 +191,7 @@ void APS_Editor_PlayerController::CancelPointPlacement()
|
||||
|
||||
void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelName)
|
||||
{
|
||||
if (LevelName.IsEmpty()) return;
|
||||
|
||||
// Unload previous sublevel
|
||||
// Unload previous sublevel first
|
||||
if (CurrentSublevel)
|
||||
{
|
||||
CurrentSublevel->SetShouldBeLoaded(false);
|
||||
@@ -218,6 +201,35 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Unloaded previous sublevel"));
|
||||
}
|
||||
|
||||
// Hide/show persistent level scene actors (lights, meshes, post-process, etc.)
|
||||
// Exclude PS_Editor system actors (pawn, controller, gizmo, gamemode, spawned objects)
|
||||
if (ULevel* PersistentLevel = GetWorld()->PersistentLevel)
|
||||
{
|
||||
for (AActor* Actor : PersistentLevel->Actors)
|
||||
{
|
||||
if (!Actor) continue;
|
||||
// Skip system actors
|
||||
if (Actor == this || Actor == GetPawn()) continue;
|
||||
if (Cast<AGameModeBase>(Actor) || Cast<AGameStateBase>(Actor) || Cast<APlayerState>(Actor)) continue;
|
||||
if (Cast<APS_Editor_Gizmo>(Actor)) continue;
|
||||
// Skip PS_Editor spawned actors (tracked by SpawnManager)
|
||||
if (SpawnManager && SpawnManager->IsActorSpawned(Actor)) continue;
|
||||
|
||||
// Hide persistent level actors when loading a BaseLevel, show when clearing
|
||||
const bool bHide = !LevelName.IsEmpty() && LevelName != TEXT("None");
|
||||
Actor->SetActorHiddenInGame(bHide);
|
||||
Actor->SetActorEnableCollision(!bHide);
|
||||
}
|
||||
}
|
||||
|
||||
// "None" or empty = no sublevel, just clear
|
||||
if (LevelName.IsEmpty() || LevelName == TEXT("None"))
|
||||
{
|
||||
CurrentBaseLevel = TEXT("");
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: No BaseLevel selected, persistent level actors restored"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Load new sublevel
|
||||
bool bSuccess = false;
|
||||
CurrentSublevel = ULevelStreamingDynamic::LoadLevelInstance(
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "Widgets/Input/SButton.h"
|
||||
#include "Widgets/Images/SImage.h"
|
||||
#include "Widgets/Input/SCheckBox.h"
|
||||
#include "Widgets/Input/SComboBox.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "Widgets/Input/SEditableTextBox.h"
|
||||
#include "Widgets/SOverlay.h"
|
||||
@@ -453,7 +454,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
.Padding(0.0f, 0.0f, 4.0f, 0.0f)
|
||||
[
|
||||
SAssignNew(SaveNameField, SEditableTextBox)
|
||||
.HintText(FText::FromString(TEXT("Scene name...")))
|
||||
.HintText(FText::FromString(TEXT("Scenario name...")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
@@ -502,7 +503,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
})
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("New Scene")))
|
||||
.Text(FText::FromString(TEXT("New Scenario")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
]
|
||||
]
|
||||
@@ -540,10 +541,21 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
{
|
||||
if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>())
|
||||
{
|
||||
Sub->SetPendingScene(SceneName, BaseLevel);
|
||||
Sub->SetPendingScenario(SceneName, BaseLevel);
|
||||
// Remember editor map for ReturnToEditor
|
||||
if (Sub->EditorMapName.IsEmpty())
|
||||
{
|
||||
FString MapName = EditorPC->GetWorld()->GetMapName();
|
||||
MapName.RemoveFromStart(EditorPC->GetWorld()->StreamingLevelsPrefix);
|
||||
Sub->EditorMapName = MapName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset input mode before transition so the target GameMode starts clean
|
||||
EditorPC->bShowMouseCursor = false;
|
||||
EditorPC->SetInputMode(FInputModeGameOnly());
|
||||
|
||||
UGameplayStatics::OpenLevel(EditorPC, FName(*BaseLevel), true, TEXT(""));
|
||||
return FReply::Handled();
|
||||
})
|
||||
@@ -555,7 +567,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
]
|
||||
]
|
||||
]
|
||||
// BaseLevel selector row
|
||||
// BaseLevel combo box
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 4.0f, 0.0f, 2.0f)
|
||||
@@ -570,46 +582,46 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
]
|
||||
+ SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center)
|
||||
[
|
||||
SAssignNew(LevelFilterText, STextBlock)
|
||||
.Text(FText::FromString(TEXT("...")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 9))
|
||||
.ColorAndOpacity(FLinearColor(0.4f, 0.8f, 0.4f))
|
||||
]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(4.0f, 0.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this]() -> FReply
|
||||
SAssignNew(BaseLevelCombo, SComboBox<TSharedPtr<FString>>)
|
||||
.OptionsSource(&BaseLevelOptions)
|
||||
.OnSelectionChanged_Lambda([this](TSharedPtr<FString> Selected, ESelectInfo::Type)
|
||||
{
|
||||
if (!Selected.IsValid()) return;
|
||||
|
||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer());
|
||||
if (!EditorPC) return FReply::Handled();
|
||||
if (!EditorPC) return;
|
||||
|
||||
// Get BaseLevels from MasterCatalog
|
||||
TArray<FString> Levels;
|
||||
if (UPS_Editor_MasterCatalog* Master = EditorPC->MasterCatalogAsset.Get())
|
||||
// Clear current scenario before switching BaseLevel
|
||||
if (SceneSerializer.IsValid() && SpawnManager.IsValid())
|
||||
{
|
||||
Levels = Master->BaseLevels;
|
||||
SceneSerializer->ClearScene(SpawnManager.Get());
|
||||
}
|
||||
if (SaveNameField.IsValid())
|
||||
{
|
||||
SaveNameField->SetText(FText::GetEmpty());
|
||||
}
|
||||
if (Levels.Num() == 0) return FReply::Handled();
|
||||
|
||||
// Cycle to next
|
||||
int32 Idx = Levels.Find(EditorPC->CurrentBaseLevel);
|
||||
Idx = (Idx + 1) % Levels.Num();
|
||||
|
||||
// Load as sublevel + update filter
|
||||
EditorPC->LoadBaseLevelAsSublevel(Levels[Idx]);
|
||||
EditorPC->LoadBaseLevelAsSublevel(*Selected);
|
||||
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
||||
|
||||
if (LevelFilterText.IsValid())
|
||||
if (BaseLevelComboText.IsValid())
|
||||
{
|
||||
LevelFilterText->SetText(FText::FromString(CurrentLevelFilter));
|
||||
BaseLevelComboText->SetText(FText::FromString(*Selected));
|
||||
}
|
||||
PopulateSceneList();
|
||||
return FReply::Handled();
|
||||
LastOutlinerActorCount = -1; // Force outliner refresh
|
||||
})
|
||||
.OnGenerateWidget_Lambda([](TSharedPtr<FString> Item) -> TSharedRef<SWidget>
|
||||
{
|
||||
return SNew(STextBlock)
|
||||
.Text(FText::FromString(Item.IsValid() ? *Item : TEXT("")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9));
|
||||
})
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT(">")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
SAssignNew(BaseLevelComboText, STextBlock)
|
||||
.Text(FText::FromString(TEXT("Select...")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 9))
|
||||
.ColorAndOpacity(FLinearColor(0.4f, 0.8f, 0.4f))
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -668,13 +680,31 @@ void UPS_Editor_MainWidget::NativeConstruct()
|
||||
Super::NativeConstruct();
|
||||
PopulateSpawnCatalog();
|
||||
|
||||
// Init BaseLevel filter from PlayerController auto-detection
|
||||
// Populate BaseLevel combo from MasterCatalog
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||
{
|
||||
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
||||
if (LevelFilterText.IsValid() && !CurrentLevelFilter.IsEmpty())
|
||||
BaseLevelOptions.Add(MakeShared<FString>(TEXT("None")));
|
||||
if (UPS_Editor_MasterCatalog* Master = EditorPC->MasterCatalogAsset.Get())
|
||||
{
|
||||
LevelFilterText->SetText(FText::FromString(CurrentLevelFilter));
|
||||
for (const FString& Level : Master->BaseLevels)
|
||||
{
|
||||
BaseLevelOptions.Add(MakeShared<FString>(Level));
|
||||
}
|
||||
}
|
||||
|
||||
// If a BaseLevel was set (e.g. from subsystem), select it in the combo
|
||||
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
||||
if (!CurrentLevelFilter.IsEmpty())
|
||||
{
|
||||
for (const TSharedPtr<FString>& Opt : BaseLevelOptions)
|
||||
{
|
||||
if (*Opt == CurrentLevelFilter)
|
||||
{
|
||||
if (BaseLevelCombo.IsValid()) BaseLevelCombo->SetSelectedItem(Opt);
|
||||
if (BaseLevelComboText.IsValid()) BaseLevelComboText->SetText(FText::FromString(CurrentLevelFilter));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,7 +728,7 @@ void UPS_Editor_MainWidget::PopulateSceneList()
|
||||
.AutoHeight()
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(CurrentLevelFilter.IsEmpty() ? TEXT("No saved scenes") : FString::Printf(TEXT("No scenes for '%s'"), *CurrentLevelFilter)))
|
||||
.Text(FText::FromString(CurrentLevelFilter.IsEmpty() ? TEXT("No saved scenarios") : FString::Printf(TEXT("No scenarios for '%s'"), *CurrentLevelFilter)))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Italic", 9))
|
||||
.ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f))
|
||||
];
|
||||
|
||||
@@ -71,7 +71,9 @@ private:
|
||||
// Scene save/load
|
||||
TSharedPtr<SEditableTextBox> SaveNameField;
|
||||
TSharedPtr<SVerticalBox> SceneListContainer;
|
||||
TSharedPtr<STextBlock> LevelFilterText;
|
||||
TSharedPtr<SComboBox<TSharedPtr<FString>>> BaseLevelCombo;
|
||||
TArray<TSharedPtr<FString>> BaseLevelOptions;
|
||||
TSharedPtr<STextBlock> BaseLevelComboText;
|
||||
FString CurrentLevelFilter;
|
||||
TArray<TSharedPtr<FSlateBrush>> CachedBrushes;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user