#include "UI/DescentUiSubsystem.h" #include "Core/DescentBoot.h" #include "Core/DescentSimSubsystem.h" #include "Core/Tuning/DescentTune_UI.h" #include "Debug/DescentStateRegistry.h" #include "TheDescent.h" #include "UI/DescentHUD.h" #include "UI/DescentHudSubsystem.h" #include "UI/DescentUiDemo.h" #include "UI/DescentUiLayer.h" #include "UI/DescentWindows.h" #include "UI/NeuIcons.h" #include "UI/NeuStyle.h" #include "Engine/Engine.h" #include "Engine/GameViewportClient.h" #include "Engine/World.h" #include "Framework/Application/SlateApplication.h" #include "GameFramework/PlayerController.h" #include "HAL/IConsoleManager.h" namespace UiTune = Descent::Tune::UI; UDescentUiSubsystem* UDescentUiSubsystem::Get(const UWorld* World) { return World ? World->GetSubsystem() : nullptr; } void UDescentUiSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Collection.InitializeDependency(); Collection.InitializeDependency(); Super::Initialize(Collection); BloomVariable = UiTune::BloomCVarName(); } void UDescentUiSubsystem::Deinitialize() { if (Window != EDescentWindow::None) { Close(); } DestroyWidgets(); Super::Deinitialize(); } // --------------------------------------------------------------------------------------------------------- // Widgets // --------------------------------------------------------------------------------------------------------- void UDescentUiSubsystem::CreateWidgets() { UWorld* W = GetWorld(); UGameViewportClient* VC = W ? W->GetGameViewport() : nullptr; if (!VC || !FSlateApplication::IsInitialized() || HudWidget.IsValid()) { return; } FNeuStyle::Get(); // generate the shadow textures now, not in the middle of the first paint HudWidget = SNew(SDescentHUD) .World(W) .Model([this]() -> const FDescentHudModel* { return GetDisplayedHudModel(); }); HudLayer = SNew(SDescentUiLayer)[ HudWidget.ToSharedRef() ]; HostWidget = SNew(SDescentWindowHost).Ui(this); HostLayer = SNew(SDescentUiLayer)[ HostWidget.ToSharedRef() ]; VC->AddViewportWidgetContent(HudLayer.ToSharedRef(), 10); VC->AddViewportWidgetContent(HostLayer.ToSharedRef(), 20); UE_LOG(LogDescent, Display, TEXT("UI: HUD and window host added to the viewport")); } void UDescentUiSubsystem::DestroyWidgets() { UWorld* W = GetWorld(); UGameViewportClient* VC = W ? W->GetGameViewport() : nullptr; if (VC) { if (HudLayer.IsValid()) { VC->RemoveViewportWidgetContent(HudLayer.ToSharedRef()); } if (HostLayer.IsValid()) { VC->RemoveViewportWidgetContent(HostLayer.ToSharedRef()); } } HudWidget.Reset(); HudLayer.Reset(); HostWidget.Reset(); HostLayer.Reset(); } // --------------------------------------------------------------------------------------------------------- // Windows // --------------------------------------------------------------------------------------------------------- TArray UDescentUiSubsystem::GetTabs() const { TArray Ids; TArray Labels; Descent::Ui::GetWindowTabs(*this, Window, Ids, Labels); return Ids; } FString UDescentUiSubsystem::GetTabLabel(FName TabId) const { TArray Ids; TArray Labels; Descent::Ui::GetWindowTabs(*this, Window, Ids, Labels); const int32 Index = Ids.IndexOfByKey(TabId); return Index != INDEX_NONE ? Labels[Index] : FString(); } void UDescentUiSubsystem::ApplyInputMode(bool bWindowOpen) { UWorld* W = GetWorld(); APlayerController* PC = W ? W->GetFirstPlayerController() : nullptr; if (!PC || !FSlateApplication::IsInitialized()) { return; } if (bWindowOpen) { PC->bShowMouseCursor = true; FInputModeUIOnly Mode; if (HostWidget.IsValid()) { Mode.SetWidgetToFocus(HostWidget); } Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock); PC->SetInputMode(Mode); } else { PC->bShowMouseCursor = false; PC->SetInputMode(FInputModeGameOnly()); } } bool UDescentUiSubsystem::Open(EDescentWindow NewWindow, FName NewTab) { if (NewWindow == EDescentWindow::None) { return false; } const bool bWasOpen = Window != EDescentWindow::None; Window = NewWindow; Tab = NAME_None; const TArray Tabs = GetTabs(); if (Tabs.Num() > 0) { Tab = Tabs.Contains(NewTab) ? NewTab : Tabs[0]; } ++Revision; UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld()); if (Window == EDescentWindow::Pause) { if (Sim && !Sim->IsPaused()) { Sim->SetPaused(true); bWePausedSim = true; } } else if (bWePausedSim && Sim) { Sim->SetPaused(false); bWePausedSim = false; } if (HostWidget.IsValid()) { HostWidget->Rebuild(); if (FSlateApplication::IsInitialized()) { FSlateApplication::Get().SetKeyboardFocus(HostWidget, EFocusCause::SetDirectly); } } if (!bWasOpen) { ApplyInputMode(true); } OnWindowChanged.Broadcast(Window, Tab); return true; } bool UDescentUiSubsystem::OpenByName(const FString& Name) { EDescentWindow W; FName T; if (!Descent::Ui::ParseWindowName(Name, W, T)) { return false; } return Open(W, T); } void UDescentUiSubsystem::Close() { if (Window == EDescentWindow::None) { return; } Window = EDescentWindow::None; Tab = NAME_None; ++Revision; if (bWePausedSim) { if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { Sim->SetPaused(false); } bWePausedSim = false; } if (HostWidget.IsValid()) { HostWidget->Rebuild(); } ApplyInputMode(false); OnWindowChanged.Broadcast(Window, Tab); } bool UDescentUiSubsystem::SetTab(FName NewTab) { if (Window == EDescentWindow::None || !GetTabs().Contains(NewTab)) { return false; } Tab = NewTab; ++Revision; if (HostWidget.IsValid()) { HostWidget->Rebuild(); } OnWindowChanged.Broadcast(Window, Tab); return true; } void UDescentUiSubsystem::NotifyChanged() { ++Revision; } // --------------------------------------------------------------------------------------------------------- // HUD model, demo // --------------------------------------------------------------------------------------------------------- const FDescentHudModel* UDescentUiSubsystem::GetDisplayedHudModel() const { if (bDemo) { return &DemoHud; } const UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld()); return Hud ? &Hud->Model() : nullptr; } void UDescentUiSubsystem::SetDemo(bool bOn) { bDemo = bOn; UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld()); if (bOn) { Descent::Ui::FillDemo(UiModels, DemoHud); if (Hud) { Hud->Model() = DemoHud; // so the `ui.hud` state shows it too; the screen draws the frozen copy } } else { UiModels = FDescentUiModels(); DemoHud.Reset(); if (Hud) { Hud->Model().Reset(); } } ++Revision; if (HostWidget.IsValid() && Window != EDescentWindow::None) { HostWidget->Rebuild(); } } // --------------------------------------------------------------------------------------------------------- // Bloom slider // --------------------------------------------------------------------------------------------------------- bool UDescentUiSubsystem::IsBloomAvailable() const { return IConsoleManager::Get().FindConsoleVariable(*BloomVariable) != nullptr; } float UDescentUiSubsystem::GetBloomValue() const { const IConsoleVariable* Var = IConsoleManager::Get().FindConsoleVariable(*BloomVariable); return Var ? Var->GetFloat() : 0.f; } float UDescentUiSubsystem::GetBloomMax() const { return FMath::Max(UiTune::BloomSliderMax, GetBloomValue()); } void UDescentUiSubsystem::SetBloomValue(float Value) { if (IConsoleVariable* Var = IConsoleManager::Get().FindConsoleVariable(*BloomVariable)) { Var->Set(FMath::Clamp(Value, 0.f, GetBloomMax()), ECVF_SetByGameSetting); } } // --------------------------------------------------------------------------------------------------------- // Sizes // --------------------------------------------------------------------------------------------------------- float UDescentUiSubsystem::GetUiScale() const { FVector2D VS(1280.0, 720.0); if (GEngine && GEngine->GameViewport) { GEngine->GameViewport->GetViewportSize(VS); } return UiTune::UiScaleForViewport(static_cast(VS.X), static_cast(VS.Y)); } FVector2f UDescentUiSubsystem::GetLayoutSize() const { FVector2D VS(1280.0, 720.0); if (GEngine && GEngine->GameViewport) { GEngine->GameViewport->GetViewportSize(VS); } const float S = GetUiScale(); return FVector2f(static_cast(VS.X) / S, static_cast(VS.Y) / S); } // --------------------------------------------------------------------------------------------------------- // Boot: add the widgets after the world exists, before the player is placed (Boot::Order::CreateUi) // --------------------------------------------------------------------------------------------------------- DESCENT_BOOT_STEP(UiWidgets, Descent::Boot::Order::CreateUi) { UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(Ctx.World); if (!Ui) { Ctx.Fail(TEXT("UDescentUiSubsystem is missing from the world")); return; } Ui->CreateWidgets(); } // --------------------------------------------------------------------------------------------------------- // State section "ui", parts "widgets", "window", "cargo_display" (DESIGN_UE.md 9.4) // --------------------------------------------------------------------------------------------------------- namespace { TSharedRef UiRectJson(const TCHAR* Name, bool bVisible, float X, float Y, float W, float H, float Scale) { TSharedRef O = MakeShared(); O->SetStringField(TEXT("name"), Name); O->SetBoolField(TEXT("visible"), bVisible); O->SetNumberField(TEXT("x"), Descent::Json::Round(X * Scale)); O->SetNumberField(TEXT("y"), Descent::Json::Round(Y * Scale)); O->SetNumberField(TEXT("w"), Descent::Json::Round(W * Scale)); O->SetNumberField(TEXT("h"), Descent::Json::Round(H * Scale)); return O; } FDescentStatePartRegistrar GUiWidgetsPart(TEXT("ui"), TEXT("widgets"), [](UWorld* World, FJsonObject& Out) { // Every top-level widget's rectangle in viewport pixels: the HUD regions (from the same layout function the test uses) and the window frame. const UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(World); Out.SetBoolField(TEXT("available"), Ui != nullptr); TArray> List; if (Ui) { const FDescentHudModel* M = Ui->GetDisplayedHudModel(); const FVector2f Layout = Ui->GetLayoutSize(); const float S = Ui->GetUiScale(); Out.SetNumberField(TEXT("ui_scale"), Descent::Json::Round(S)); Out.SetArrayField(TEXT("viewport_px"), { MakeShared(Layout.X * S), MakeShared(Layout.Y * S) }); if (M) { const FDescentHudLayout L = FDescentHudLayout::Compute(Layout, *M); for (const FDescentHudRect* R : L.All()) { List.Add(MakeShared(UiRectJson(*R->Name.ToString(), R->bVisible, R->X, R->Y, R->W, R->H, S))); } } if (Ui->IsWindowOpen()) { const Descent::Ui::FWindowFit Fit = Descent::Ui::FitWindow(Descent::Ui::WindowDesignSize(Ui->GetWindow()), Layout); List.Add(MakeShared(UiRectJson(TEXT("window"), true, Fit.Pos.X, Fit.Pos.Y, Fit.Size.X, Fit.Size.Y, S))); } for (const TPair& E : Ui->GetExtraWidgets()) { List.Add(MakeShared(UiRectJson(*E.Key.ToString(), E.Value.bVisible, E.Value.X, E.Value.Y, E.Value.W, E.Value.H, 1.f))); } } Out.SetArrayField(TEXT("list"), List); }); FDescentStatePartRegistrar GUiWindowPart(TEXT("ui"), TEXT("window"), [](UWorld* World, FJsonObject& Out) { const UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(World); Out.SetBoolField(TEXT("available"), Ui != nullptr); if (!Ui) { return; } const EDescentWindow W = Ui->GetWindow(); Out.SetBoolField(TEXT("open"), W != EDescentWindow::None); Out.SetStringField(TEXT("name"), W == EDescentWindow::None ? FString() : Descent::Ui::WindowName(W).ToString()); Out.SetStringField(TEXT("tab"), Ui->GetTab().IsNone() ? FString() : Ui->GetTab().ToString()); Out.SetBoolField(TEXT("paused"), Ui->IsPaused()); Out.SetBoolField(TEXT("demo"), Ui->IsDemo()); TArray> Tabs; for (const FName& T : Ui->GetTabs()) { Tabs.Add(MakeShared(T.ToString())); } Out.SetArrayField(TEXT("tabs"), Tabs); Out.SetBoolField(TEXT("bloom_available"), Ui->IsBloomAvailable()); Out.SetNumberField(TEXT("bloom_value"), Descent::Json::Round(Ui->GetBloomValue())); if (W != EDescentWindow::None) { const FVector2f Layout = Ui->GetLayoutSize(); const float S = Ui->GetUiScale(); const Descent::Ui::FWindowFit Fit = Descent::Ui::FitWindow(Descent::Ui::WindowDesignSize(W), Layout); Out.SetNumberField(TEXT("scale"), Descent::Json::Round(Fit.Scale)); Out.SetBoolField(TEXT("fits_viewport"), Fit.bFits); Out.SetObjectField(TEXT("rect"), UiRectJson(TEXT("window"), true, Fit.Pos.X, Fit.Pos.Y, Fit.Size.X, Fit.Size.Y, S)); } }); FDescentStatePartRegistrar GUiCargoPart(TEXT("ui"), TEXT("cargo_display"), [](UWorld* World, FJsonObject& Out) { const UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(World); Out.SetBoolField(TEXT("available"), Ui != nullptr); if (!Ui) { return; } const FCargoDisplayModel& C = Ui->Models().Cargo; Out.SetNumberField(TEXT("cap"), C.Cap); Out.SetNumberField(TEXT("used"), C.Used()); Out.SetBoolField(TEXT("full"), C.IsFull()); Out.SetBoolField(TEXT("unloading"), C.bUnloading); Out.SetNumberField(TEXT("bio_fu"), Descent::Json::Round(C.BioFu)); Out.SetNumberField(TEXT("bio_cap_fu"), Descent::Json::Round(C.BioCapFu)); TSharedRef Counts = MakeShared(); Counts->SetNumberField(TEXT("ferrite"), C.CountOf(TEXT("ferrite"))); Counts->SetNumberField(TEXT("cobalt"), C.CountOf(TEXT("cobalt"))); Counts->SetNumberField(TEXT("pearl"), C.CountOf(TEXT("pearl"))); Out.SetObjectField(TEXT("counts"), Counts); // Cells bottom-up in arrival order, Descent::Tune::UI::CargoColumns per row (row 0 is the bottom row). TArray> Cells; for (int32 I = 0; I < C.Cap; ++I) { TSharedRef Cell = MakeShared(); Cell->SetNumberField(TEXT("i"), I); Cell->SetNumberField(TEXT("row"), I / UiTune::CargoColumns); Cell->SetNumberField(TEXT("col"), I % UiTune::CargoColumns); if (C.Order.IsValidIndex(I)) { Cell->SetStringField(TEXT("ore"), C.Order[I].ToString()); } else { Cell->SetField(TEXT("ore"), MakeShared()); } Cells.Add(MakeShared(Cell)); } Out.SetArrayField(TEXT("cells"), Cells); }); }