diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp index 4823cc3..664a187 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp @@ -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 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()); +} + +void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath, const TArray& 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& 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 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; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h index 3983f9f..1a0e948 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h @@ -75,11 +75,15 @@ public: bool IsSimulating() const { return bSimulating; } /** Load a base level as sublevel for visual context. Unloads previous sublevel. - * @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 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& 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 CurrentSublevel; + /** Additional sublevels loaded alongside the base level. */ + UPROPERTY() + TArray> AdditionalSublevels; + /** True when a sublevel is loading and needs fade-in when ready. */ bool bWaitingForSublevelFadeIn = false; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableInterface.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableInterface.h index 6f01d7d..f2ba2f0 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableInterface.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableInterface.h @@ -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 GetCustomOptionsForProperty(const FString& PropertyPath) const; }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h index 493d2cf..7c0b338 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h @@ -68,6 +68,15 @@ struct FPS_Editor_BaseLevelEntry /** Thumbnail image shown in the BaseLevel selection popup. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor") TObjectPtr 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 AdditionalSublevels; }; /** diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp index 43d9fd3..9b0d58a 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp @@ -960,7 +960,12 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() { SceneMapPath = *Found; } - EditorPC->LoadBaseLevelAsSublevel(SceneLevelName, SceneMapPath); + TArray SceneExtras; + if (TArray* FoundExtras = BaseLevelExtraSublevels.Find(SceneLevelName)) + { + SceneExtras = *FoundExtras; + } + EditorPC->LoadBaseLevelAsSublevel(SceneLevelName, SceneMapPath, SceneExtras); CurrentLevelFilter = EditorPC->CurrentBaseLevel; for (const TSharedPtr& 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 ScnExtras; + if (TArray* 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 ValueWidget = SNullWidget::NullWidget; + // Check for custom dropdown options from the interface + TArray 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>>(); + for (const FString& Opt : CustomOptions) + { + Row.EnumOptions->Add(MakeShared(Opt)); + } + + ValueWidget = SAssignNew(Row.EnumCombo, SComboBox>) + .OptionsSource(Row.EnumOptions.Get()) + .Method(EPopupMethod::UseCurrentWindow) + .IsFocusable(true) + .OnSelectionChanged_Lambda([this, RowIndex](TSharedPtr 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 Item) -> TSharedRef + { + 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(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& 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 CardBorder; CurrentRow->AddSlot().AutoWidth().Padding(8.0f, 0.0f) [ SNew(SBox) .MinDesiredWidth(CardWidth + 16.0f) [ - SNew(SButton) - .OnClicked_Lambda([this, Name]() -> FReply - { - PendingBaseLevelSelection = Name; - return FReply::Handled(); - }) + SAssignNew(CardBorder, SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.1f, 0.1f, 0.1f, 0.0f)) + .Padding(FMargin(2.0f)) [ - Card + 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 Extras; + if (TArray* 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()) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h index 384e695..70f6440 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h @@ -82,6 +82,8 @@ private: FString CurrentLevelFilter; /** Maps DisplayName → full MapPath for base levels (from MasterCatalog). */ TMap BaseLevelPathMap; + /** Maps DisplayName → additional sublevels to load (from MasterCatalog). */ + TMap> BaseLevelExtraSublevels; /** Maps DisplayName → Thumbnail texture for base levels. */ TMap> BaseLevelThumbnails; TArray> CachedBrushes; @@ -90,7 +92,9 @@ private: TSharedPtr BaseLevelPopup; TSharedPtr BaseLevelGridContainer; FString PendingBaseLevelSelection; + TMap> BaseLevelCardBorders; void OpenBaseLevelPopup(); + void UpdateBaseLevelHighlight(); void ConfirmBaseLevelSelection(); void CancelBaseLevelPopup();