Additional sublevels, custom property options, BaseLevel highlight, lightmap fix
- Add AdditionalSublevels to FPS_Editor_BaseLevelEntry for always-loaded streaming levels that aren't auto-loaded when base level is a sublevel - Add LoadBaseLevelAsSublevel overload with ExtraSublevels parameter - Add GetCustomOptionsForProperty to IPS_Editor_EditableInterface for data-driven dropdowns (e.g. weapon lists from DataTables) - Add custom options combo in property panel (interface-driven, no CachedEnum) - Add BaseLevel selection highlight: selected card stays highlighted until validate or change - Fix deferred lightmap refresh with 0.5s timer instead of NextTick - Fix enum detection scope error in RebuildDynamicProperties Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -75,17 +75,19 @@ void APS_Editor_PlayerController::BeginPlay()
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Restoring from subsystem - BaseLevel='%s', Scenario='%s'"),
|
||||
*Sub->PendingBaseLevel, *Sub->PendingScenarioName);
|
||||
|
||||
// Resolve full map path from MasterCatalog
|
||||
// Resolve full map path and extra sublevels from MasterCatalog
|
||||
FString ResolvedMapPath;
|
||||
TArray<FString> ResolvedExtras;
|
||||
for (const FPS_Editor_BaseLevelEntry& Entry : Master->BaseLevels)
|
||||
{
|
||||
if (Entry.DisplayName == Sub->PendingBaseLevel)
|
||||
{
|
||||
ResolvedMapPath = Entry.MapPath;
|
||||
ResolvedExtras = Entry.AdditionalSublevels;
|
||||
break;
|
||||
}
|
||||
}
|
||||
LoadBaseLevelAsSublevel(Sub->PendingBaseLevel, ResolvedMapPath);
|
||||
LoadBaseLevelAsSublevel(Sub->PendingBaseLevel, ResolvedMapPath, ResolvedExtras);
|
||||
|
||||
if (!Sub->PendingScenarioName.IsEmpty() && SpawnManager && SceneSerializer)
|
||||
{
|
||||
@@ -381,6 +383,11 @@ void APS_Editor_PlayerController::ShowLoadingScreen(bool bShow)
|
||||
}
|
||||
|
||||
void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath)
|
||||
{
|
||||
LoadBaseLevelAsSublevel(LevelName, MapPath, TArray<FString>());
|
||||
}
|
||||
|
||||
void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath, const TArray<FString>& ExtraSublevels)
|
||||
{
|
||||
// Unload previous sublevel first
|
||||
if (CurrentSublevel)
|
||||
@@ -392,6 +399,18 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Unloaded previous sublevel"));
|
||||
}
|
||||
|
||||
// Unload additional sublevels from previous base level
|
||||
for (TObjectPtr<ULevelStreamingDynamic>& Extra : AdditionalSublevels)
|
||||
{
|
||||
if (Extra)
|
||||
{
|
||||
Extra->SetShouldBeLoaded(false);
|
||||
Extra->SetShouldBeVisible(false);
|
||||
Extra->SetIsRequestingUnloadAndRemoval(true);
|
||||
}
|
||||
}
|
||||
AdditionalSublevels.Empty();
|
||||
|
||||
// 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)
|
||||
@@ -456,6 +475,32 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
|
||||
CurrentSublevel->OnLevelShown.AddUniqueDynamic(this, &APS_Editor_PlayerController::DisableSublevelPostProcessMaterials);
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Loaded sublevel '%s' (path: '%s')"), *LevelName, *LevelToLoad);
|
||||
|
||||
// Load additional sublevels (always-loaded streaming levels)
|
||||
for (const FString& ExtraPath : ExtraSublevels)
|
||||
{
|
||||
if (ExtraPath.IsEmpty()) continue;
|
||||
|
||||
bool bExtraSuccess = false;
|
||||
ULevelStreamingDynamic* ExtraSublevel = ULevelStreamingDynamic::LoadLevelInstance(
|
||||
GetWorld(),
|
||||
ExtraPath,
|
||||
FVector::ZeroVector,
|
||||
FRotator::ZeroRotator,
|
||||
bExtraSuccess);
|
||||
|
||||
if (bExtraSuccess && ExtraSublevel)
|
||||
{
|
||||
ExtraSublevel->SetShouldBeLoaded(true);
|
||||
ExtraSublevel->SetShouldBeVisible(true);
|
||||
AdditionalSublevels.Add(ExtraSublevel);
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Loaded additional sublevel '%s'"), *ExtraPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to load additional sublevel '%s'"), *ExtraPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -470,8 +515,24 @@ void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials()
|
||||
ULevel* Level = CurrentSublevel->GetLoadedLevel();
|
||||
if (!Level) return;
|
||||
|
||||
// Force lighting/rendering resources to initialize properly for baked lightmaps
|
||||
Level->InitializeRenderingResources();
|
||||
// Deferred lightmap refresh: MapBuildData may be loaded async and not ready yet
|
||||
TWeakObjectPtr<ULevelStreamingDynamic> WeakSublevel = CurrentSublevel;
|
||||
FTimerHandle LightmapTimer;
|
||||
GetWorldTimerManager().SetTimer(LightmapTimer, [WeakSublevel]()
|
||||
{
|
||||
if (!WeakSublevel.IsValid()) return;
|
||||
ULevel* DelayedLevel = WeakSublevel->GetLoadedLevel();
|
||||
if (!DelayedLevel) return;
|
||||
|
||||
for (AActor* Actor : DelayedLevel->Actors)
|
||||
{
|
||||
if (Actor)
|
||||
{
|
||||
Actor->MarkComponentsRenderStateDirty();
|
||||
}
|
||||
}
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deferred lightmap refresh on sublevel"));
|
||||
}, 0.5f, false);
|
||||
|
||||
APS_Editor_CameraStart* CameraStart = nullptr;
|
||||
|
||||
|
||||
@@ -78,8 +78,12 @@ public:
|
||||
* @param LevelName Display name stored in CurrentBaseLevel (used for scene filtering).
|
||||
* @param MapPath Full package path for LoadLevelInstance (e.g. "/Game/Maps/Warehouse").
|
||||
* If empty, LevelName is used as-is (legacy behavior).
|
||||
* @param ExtraSublevels Additional sublevels to load alongside (e.g. always-loaded streaming levels).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
void LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath, const TArray<FString>& ExtraSublevels);
|
||||
|
||||
/** Overload without extra sublevels. */
|
||||
void LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath = TEXT(""));
|
||||
|
||||
// ---- Editor Mode (Normal / SplinePlacement) ----
|
||||
@@ -157,6 +161,10 @@ private:
|
||||
UPROPERTY()
|
||||
TObjectPtr<ULevelStreamingDynamic> CurrentSublevel;
|
||||
|
||||
/** Additional sublevels loaded alongside the base level. */
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<ULevelStreamingDynamic>> AdditionalSublevels;
|
||||
|
||||
/** True when a sublevel is loading and needs fade-in when ready. */
|
||||
bool bWaitingForSublevelFadeIn = false;
|
||||
|
||||
|
||||
@@ -67,4 +67,14 @@ public:
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor")
|
||||
void OnEditorTick(float DeltaTime);
|
||||
|
||||
/**
|
||||
* Return custom dropdown options for a given property.
|
||||
* If the returned array is non-empty, the editor shows a combo box with these options
|
||||
* instead of the default widget for that property type.
|
||||
* @param PropertyPath The property path (e.g. "WeaponName" or "MyComponent.WeaponType").
|
||||
* @return List of option strings. Empty = use default widget.
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor")
|
||||
TArray<FString> GetCustomOptionsForProperty(const FString& PropertyPath) const;
|
||||
};
|
||||
|
||||
@@ -68,6 +68,15 @@ struct FPS_Editor_BaseLevelEntry
|
||||
/** Thumbnail image shown in the BaseLevel selection popup. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
TObjectPtr<UTexture2D> Thumbnail;
|
||||
|
||||
/**
|
||||
* Additional sublevels to load alongside this base level in the editor.
|
||||
* Used for streaming levels that are "always loaded" in the persistent level
|
||||
* but not automatically loaded when the base level is a sublevel itself.
|
||||
* Full map paths, e.g. "/Game/Maps/Warehouse_Lighting"
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
TArray<FString> AdditionalSublevels;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -960,7 +960,12 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
{
|
||||
SceneMapPath = *Found;
|
||||
}
|
||||
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName, SceneMapPath);
|
||||
TArray<FString> SceneExtras;
|
||||
if (TArray<FString>* FoundExtras = BaseLevelExtraSublevels.Find(SceneLevelName))
|
||||
{
|
||||
SceneExtras = *FoundExtras;
|
||||
}
|
||||
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName, SceneMapPath, SceneExtras);
|
||||
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
||||
for (const TSharedPtr<FString>& Opt : BaseLevelOptions)
|
||||
{
|
||||
@@ -1092,6 +1097,10 @@ void UPS_Editor_MainWidget::NativeConstruct()
|
||||
{
|
||||
BaseLevelPathMap.Add(Entry.DisplayName, Entry.MapPath);
|
||||
}
|
||||
if (Entry.AdditionalSublevels.Num() > 0)
|
||||
{
|
||||
BaseLevelExtraSublevels.Add(Entry.DisplayName, Entry.AdditionalSublevels);
|
||||
}
|
||||
if (Entry.Thumbnail)
|
||||
{
|
||||
BaseLevelThumbnails.Add(Entry.DisplayName, Entry.Thumbnail);
|
||||
@@ -1184,7 +1193,12 @@ void UPS_Editor_MainWidget::PopulateSceneList()
|
||||
{
|
||||
SceneMapPath = *Found;
|
||||
}
|
||||
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName, SceneMapPath);
|
||||
TArray<FString> ScnExtras;
|
||||
if (TArray<FString>* FoundExtras = BaseLevelExtraSublevels.Find(SceneLevelName))
|
||||
{
|
||||
ScnExtras = *FoundExtras;
|
||||
}
|
||||
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName, SceneMapPath, ScnExtras);
|
||||
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
||||
|
||||
if (BaseLevelButtonText.IsValid())
|
||||
@@ -1890,7 +1904,50 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
||||
// Value widget depends on property type
|
||||
TSharedRef<SWidget> ValueWidget = SNullWidget::NullWidget;
|
||||
|
||||
// Check for custom dropdown options from the interface
|
||||
TArray<FString> CustomOptions;
|
||||
if (Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
|
||||
{
|
||||
CustomOptions = IPS_Editor_EditableInterface::Execute_GetCustomOptionsForProperty(Actor, Row.Key);
|
||||
}
|
||||
|
||||
if (CustomOptions.Num() > 0)
|
||||
{
|
||||
Row.EnumOptions = MakeShared<TArray<TSharedPtr<FString>>>();
|
||||
for (const FString& Opt : CustomOptions)
|
||||
{
|
||||
Row.EnumOptions->Add(MakeShared<FString>(Opt));
|
||||
}
|
||||
|
||||
ValueWidget = SAssignNew(Row.EnumCombo, SComboBox<TSharedPtr<FString>>)
|
||||
.OptionsSource(Row.EnumOptions.Get())
|
||||
.Method(EPopupMethod::UseCurrentWindow)
|
||||
.IsFocusable(true)
|
||||
.OnSelectionChanged_Lambda([this, RowIndex](TSharedPtr<FString> Selected, ESelectInfo::Type SelectInfo)
|
||||
{
|
||||
if (!Selected.IsValid() || SelectInfo == ESelectInfo::Direct) return;
|
||||
FPS_Editor_PropertyRowWidgets& R = DynamicPropertyRows[RowIndex];
|
||||
if (R.EnumComboText.IsValid())
|
||||
{
|
||||
R.EnumComboText->SetText(FText::FromString(*Selected));
|
||||
}
|
||||
ApplyPropertyFromUI(RowIndex);
|
||||
})
|
||||
.OnGenerateWidget_Lambda([](TSharedPtr<FString> Item) -> TSharedRef<SWidget>
|
||||
{
|
||||
return SNew(STextBlock)
|
||||
.Text(FText::FromString(Item.IsValid() ? *Item : TEXT("")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9));
|
||||
})
|
||||
[
|
||||
SAssignNew(Row.EnumComboText, STextBlock)
|
||||
.Text(FText::FromString(TEXT("Select...")))
|
||||
.Font(ValueFont)
|
||||
];
|
||||
}
|
||||
// Detect enum type (FEnumProperty for C++ enum class, FByteProperty with UEnum for legacy)
|
||||
else
|
||||
{
|
||||
UEnum* FoundEnum = nullptr;
|
||||
if (FEnumProperty* EnumProp = CastField<FEnumProperty>(Row.CachedProperty))
|
||||
{
|
||||
@@ -2024,6 +2081,8 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
||||
});
|
||||
}
|
||||
|
||||
} // end else (enum/struct/default type detection)
|
||||
|
||||
DynamicPropertiesContainer->AddSlot().AutoHeight().Padding(0.0f, 2.0f)
|
||||
[ ValueWidget ];
|
||||
|
||||
@@ -2050,7 +2109,31 @@ void UPS_Editor_MainWidget::RefreshDynamicPropertyValues()
|
||||
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
||||
if (!Target) continue;
|
||||
|
||||
if (Row.CachedEnum && Row.EnumCombo.IsValid() && !Row.EnumCombo->IsOpen())
|
||||
// Custom options combo (no CachedEnum, has EnumCombo)
|
||||
if (!Row.CachedEnum && Row.EnumCombo.IsValid() && !Row.EnumCombo->IsOpen())
|
||||
{
|
||||
// Read current property value as text
|
||||
FString CurrentVal;
|
||||
Row.CachedProperty->ExportText_InContainer(0, CurrentVal, Target, Target, nullptr, PPF_None);
|
||||
|
||||
if (Row.EnumComboText.IsValid() && Row.EnumComboText->GetText().ToString() != CurrentVal)
|
||||
{
|
||||
// Find matching option
|
||||
if (Row.EnumOptions.IsValid())
|
||||
{
|
||||
for (const TSharedPtr<FString>& Opt : *Row.EnumOptions)
|
||||
{
|
||||
if (*Opt == CurrentVal)
|
||||
{
|
||||
Row.EnumCombo->SetSelectedItem(Opt);
|
||||
Row.EnumComboText->SetText(FText::FromString(CurrentVal));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Row.CachedEnum && Row.EnumCombo.IsValid() && !Row.EnumCombo->IsOpen())
|
||||
{
|
||||
// Read current enum value index
|
||||
int64 EnumValue = 0;
|
||||
@@ -2187,7 +2270,12 @@ void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex)
|
||||
// Build new value text from UI and import
|
||||
FString NewValueText;
|
||||
|
||||
if (Row.CachedEnum && Row.EnumComboText.IsValid())
|
||||
if (!Row.CachedEnum && Row.EnumComboText.IsValid())
|
||||
{
|
||||
// Custom options combo: use selected text directly as property value
|
||||
NewValueText = Row.EnumComboText->GetText().ToString();
|
||||
}
|
||||
else if (Row.CachedEnum && Row.EnumComboText.IsValid())
|
||||
{
|
||||
FString DisplayName = Row.EnumComboText->GetText().ToString();
|
||||
for (int32 Idx = 0; Idx < Row.CachedEnum->NumEnums() - 1; ++Idx)
|
||||
@@ -2378,6 +2466,7 @@ void UPS_Editor_MainWidget::OpenBaseLevelPopup()
|
||||
if (!BaseLevelGridContainer.IsValid() || !BaseLevelPopup.IsValid()) return;
|
||||
|
||||
BaseLevelGridContainer->ClearChildren();
|
||||
BaseLevelCardBorders.Empty();
|
||||
PendingBaseLevelSelection.Empty();
|
||||
|
||||
const float CardWidth = 160.0f;
|
||||
@@ -2457,23 +2546,32 @@ void UPS_Editor_MainWidget::OpenBaseLevelPopup()
|
||||
.ColorAndOpacity(FLinearColor::White)
|
||||
];
|
||||
|
||||
// Add card to current row
|
||||
// Add card to current row with highlight border
|
||||
TSharedPtr<SBorder> CardBorder;
|
||||
CurrentRow->AddSlot().AutoWidth().Padding(8.0f, 0.0f)
|
||||
[
|
||||
SNew(SBox)
|
||||
.MinDesiredWidth(CardWidth + 16.0f)
|
||||
[
|
||||
SAssignNew(CardBorder, SBorder)
|
||||
.BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush"))
|
||||
.BorderBackgroundColor(FLinearColor(0.1f, 0.1f, 0.1f, 0.0f))
|
||||
.Padding(FMargin(2.0f))
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this, Name]() -> FReply
|
||||
{
|
||||
PendingBaseLevelSelection = Name;
|
||||
UpdateBaseLevelHighlight();
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
Card
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
BaseLevelCardBorders.Add(Name, CardBorder);
|
||||
|
||||
ColIndex++;
|
||||
}
|
||||
@@ -2519,7 +2617,12 @@ void UPS_Editor_MainWidget::ConfirmBaseLevelSelection()
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorPC->LoadBaseLevelAsSublevel(PendingBaseLevelSelection, SublevelPath);
|
||||
TArray<FString> Extras;
|
||||
if (TArray<FString>* Found = BaseLevelExtraSublevels.Find(PendingBaseLevelSelection))
|
||||
{
|
||||
Extras = *Found;
|
||||
}
|
||||
EditorPC->LoadBaseLevelAsSublevel(PendingBaseLevelSelection, SublevelPath, Extras);
|
||||
}
|
||||
|
||||
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
||||
@@ -2535,6 +2638,20 @@ void UPS_Editor_MainWidget::ConfirmBaseLevelSelection()
|
||||
PendingBaseLevelSelection.Empty();
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::UpdateBaseLevelHighlight()
|
||||
{
|
||||
for (auto& Pair : BaseLevelCardBorders)
|
||||
{
|
||||
if (Pair.Value.IsValid())
|
||||
{
|
||||
const bool bSelected = (Pair.Key == PendingBaseLevelSelection);
|
||||
Pair.Value->SetBorderBackgroundColor(bSelected
|
||||
? FLinearColor(0.2f, 0.6f, 1.0f, 0.8f)
|
||||
: FLinearColor(0.1f, 0.1f, 0.1f, 0.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::CancelBaseLevelPopup()
|
||||
{
|
||||
if (BaseLevelPopup.IsValid())
|
||||
|
||||
@@ -82,6 +82,8 @@ private:
|
||||
FString CurrentLevelFilter;
|
||||
/** Maps DisplayName → full MapPath for base levels (from MasterCatalog). */
|
||||
TMap<FString, FString> BaseLevelPathMap;
|
||||
/** Maps DisplayName → additional sublevels to load (from MasterCatalog). */
|
||||
TMap<FString, TArray<FString>> BaseLevelExtraSublevels;
|
||||
/** Maps DisplayName → Thumbnail texture for base levels. */
|
||||
TMap<FString, TObjectPtr<UTexture2D>> BaseLevelThumbnails;
|
||||
TArray<TSharedPtr<FSlateBrush>> CachedBrushes;
|
||||
@@ -90,7 +92,9 @@ private:
|
||||
TSharedPtr<SWidget> BaseLevelPopup;
|
||||
TSharedPtr<SVerticalBox> BaseLevelGridContainer;
|
||||
FString PendingBaseLevelSelection;
|
||||
TMap<FString, TSharedPtr<SBorder>> BaseLevelCardBorders;
|
||||
void OpenBaseLevelPopup();
|
||||
void UpdateBaseLevelHighlight();
|
||||
void ConfirmBaseLevelSelection();
|
||||
void CancelBaseLevelPopup();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user