// m3_ui: the UI wiring of milestone M3. The controller opens the windows on the design's keys (section 5.7: Tab PDA, M map, Esc / P / H // pause, the title card at the start) and ignores move and look under them. The keys are injected into the player controller's own // input (APlayerController::InputKey), so the Enhanced Input bindings made in C++ are what is exercised, not a shortcut around them. #include "Core/DescentSimSubsystem.h" #include "Debug/DescentScenario.h" #include "Player/DescentCharacter.h" #include "Player/DescentPlayerController.h" #include "UI/DescentHudSubsystem.h" #include "UI/DescentUiSubsystem.h" #include "Vehicles/KazahTender.h" #include "Vehicles/TenderFlight.h" #include "DescentUnits.h" #include "TheDescent.h" #include "Engine/World.h" #include "InputKeyEventArgs.h" namespace { ADescentPlayerController* M3UiPC(const FDescentScenarioContext& C) { return C.World ? Cast(C.World->GetFirstPlayerController()) : nullptr; } AKazahTender* M3UiTender(const FDescentScenarioContext& C) { const UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(C.World); return Fleet ? Fleet->At(0) : nullptr; } void M3UiKey(FDescentScenarioBuilder& S, const FKey Key, bool bDown) { S.Do(FString::Printf(TEXT("key %s %s"), *Key.ToString(), bDown ? TEXT("down") : TEXT("up")), [Key, bDown](FDescentScenarioContext& C) { if (ADescentPlayerController* PC = M3UiPC(C)) { PC->InputKey(FInputKeyEventArgs::CreateSimulated(Key, bDown ? IE_Pressed : IE_Released, bDown ? 1.f : 0.f)); } }); S.WaitFrames(3); } void M3UiTap(FDescentScenarioBuilder& S, const FKey Key) { M3UiKey(S, Key, true); M3UiKey(S, Key, false); } void M3UiExpectWindow(FDescentScenarioBuilder& S, const FString& What, EDescentWindow Window, FName Tab = NAME_None) { S.WaitFrames(6); // the HUD model is written at 10 Hz S.Expect(What, [Window, Tab](FDescentScenarioContext& C) { const UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(C.World); const UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(C.World); const bool bOpen = Window != EDescentWindow::None; if (!Ui || !Hud || Ui->GetWindow() != Window || (!Tab.IsNone() && Ui->GetTab() != Tab) || Hud->Model().bUiWindowOpen != bOpen || M3UiPC(C)->IsUiOpen() != bOpen) { C.Fail(FString::Printf(TEXT("window is '%s' tab '%s', HUD bUiWindowOpen %d"), Ui ? *Descent::Ui::WindowName(Ui->GetWindow()).ToString() : TEXT("?"), Ui ? *Ui->GetTab().ToString() : TEXT("?"), Hud ? static_cast(Hud->Model().bUiWindowOpen) : -1)); return false; } return true; }); } void M3UiExpectMode(FDescentScenarioBuilder& S, EDescentMode M) { S.Expect(FString::Printf(TEXT("mode is %s"), DescentModeName(M)), [M](FDescentScenarioContext& C) { const ADescentPlayerController* PC = M3UiPC(C); if (!PC || PC->GetMode() != M) { C.Fail(FString::Printf(TEXT("mode is %s, expected %s"), PC ? DescentModeName(PC->GetMode()) : TEXT("(none)"), DescentModeName(M))); return false; } return true; }); } void M3UiWaitMode(FDescentScenarioBuilder& S, EDescentMode M, double MaxSimSeconds) { TSharedRef Start = MakeShared(-1.0); S.Until(FString::Printf(TEXT("mode becomes %s"), DescentModeName(M)), [M, MaxSimSeconds, Start](FDescentScenarioContext& C) { const ADescentPlayerController* PC = M3UiPC(C); if (*Start < 0.0) { *Start = C.Sim()->SimTime(); } if (PC->GetMode() == M) { return true; } if (C.Sim()->SimTime() - *Start > MaxSimSeconds) { C.Fail(FString::Printf(TEXT("mode is %s after %.1f s, expected %s"), DescentModeName(PC->GetMode()), MaxSimSeconds, DescentModeName(M))); return true; } return false; }); } void M3UiExpectLiveMove(FDescentScenarioBuilder& S, const FString& What, double ForwardWanted) { S.Expect(What, [ForwardWanted](FDescentScenarioContext& C) { const double X = C.Sim()->GetLiveIntent().Move.X; if (X != ForwardWanted) { C.Fail(FString::Printf(TEXT("live intent forward is %.1f, expected %.1f"), X, ForwardWanted)); return false; } return true; }); } void M3UiClose(FDescentScenarioBuilder& S) { S.Do(TEXT("close the window"), [](FDescentScenarioContext& C) { UDescentUiSubsystem::Get(C.World)->Close(); }); S.WaitFrames(3); } } DESCENT_SCENARIO(m3_ui, "The UI wiring: title card -> DIVE -> Foot; Tab opens the PDA, M the map, Esc / P / H the pause window (the sim stops); W held under an open window gives no movement; and the real keys E, Space, F, W, Shift drive board, launch, headlamp, flight, leave and re-enter.") { S.TimeLimit(240.f); // ---- the title card (a scenario starts in Foot, so the normal start is replayed here) ---- S.Do(TEXT("as at a normal start: mode Title, the title card open"), [](FDescentScenarioContext& C) { M3UiPC(C)->SetMode(EDescentMode::Title); UDescentUiSubsystem::Get(C.World)->Open(EDescentWindow::Title); }); M3UiExpectWindow(S, TEXT("the title card is open"), EDescentWindow::Title); M3UiKey(S, EKeys::W, true); M3UiExpectLiveMove(S, TEXT("W under the title card moves nothing"), 0.0); M3UiTap(S, EKeys::Tab); M3UiExpectWindow(S, TEXT("Tab does not open the PDA over the title card"), EDescentWindow::Title); S.Do(TEXT("press DIVE"), [](FDescentScenarioContext& C) { UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(C.World); if (!Ui->Models().Title.OnDive) { C.Fail(TEXT("the title card's DIVE has no action")); return; } Ui->Models().Title.OnDive(); }); M3UiExpectWindow(S, TEXT("DIVE closes the title card"), EDescentWindow::None); M3UiExpectMode(S, EDescentMode::Foot); M3UiExpectLiveMove(S, TEXT("with the card gone, the held W walks"), 1.0); // ---- PDA, map, pause over Foot ---- M3UiTap(S, EKeys::Tab); M3UiExpectWindow(S, TEXT("Tab opens the PDA"), EDescentWindow::Pda); M3UiExpectLiveMove(S, TEXT("W under the PDA moves nothing"), 0.0); M3UiExpectMode(S, EDescentMode::Foot); M3UiClose(S); M3UiExpectWindow(S, TEXT("the PDA is closed"), EDescentWindow::None); // Opening a window puts the engine in UI-only input, which lets go of every held key (no key can stick under a window): // after closing, W has to be pressed again. M3UiExpectLiveMove(S, TEXT("the W held before the PDA opened was let go by the window"), 0.0); M3UiKey(S, EKeys::W, false); M3UiKey(S, EKeys::W, true); M3UiExpectLiveMove(S, TEXT("W walks again"), 1.0); M3UiKey(S, EKeys::W, false); M3UiExpectLiveMove(S, TEXT("W released"), 0.0); M3UiTap(S, EKeys::M); M3UiExpectWindow(S, TEXT("M opens the PDA on the map tab"), EDescentWindow::Pda, TEXT("map")); M3UiClose(S); for (const FKey Key : { EKeys::Escape, EKeys::P, EKeys::H }) { M3UiTap(S, Key); M3UiExpectWindow(S, Key.ToString() + TEXT(" opens the pause window"), EDescentWindow::Pause); TSharedRef T0 = MakeShared(0.0); S.Do(TEXT("sim clock"), [T0](FDescentScenarioContext& C) { *T0 = C.Sim()->SimTime(); }); S.WaitFrames(10); S.Expect(TEXT("paused: the sim clock stands still"), [T0](FDescentScenarioContext& C) { return C.Sim()->IsPaused() && C.Sim()->SimTime() == *T0; }); M3UiClose(S); S.WaitFrames(5); S.Expect(TEXT("resumed: the sim clock runs"), [T0](FDescentScenarioContext& C) { return !C.Sim()->IsPaused() && C.Sim()->SimTime() > *T0; }); } // ---- the real keys through board, launch, flight, leave, re-enter ---- S.Exec(TEXT("Descent.Player.PlaceLocal -29 2.4 -7.5 90")); S.WaitSim(0.6); M3UiTap(S, EKeys::E); M3UiExpectMode(S, EDescentMode::Board); M3UiWaitMode(S, EDescentMode::Docked, 3.0); M3UiTap(S, EKeys::Tab); M3UiExpectWindow(S, TEXT("Tab opens the PDA in the cradle too"), EDescentWindow::Pda); M3UiTap(S, EKeys::SpaceBar); M3UiExpectMode(S, EDescentMode::Docked); // Space under the PDA does not launch M3UiClose(S); M3UiTap(S, EKeys::SpaceBar); M3UiExpectMode(S, EDescentMode::Launch); M3UiWaitMode(S, EDescentMode::Sub, 6.0); S.WaitSim(0.3); S.Expect(TEXT("the headlamp is on at sea"), [](FDescentScenarioContext& C) { return M3UiTender(C)->bHeadlightOn; }); M3UiTap(S, EKeys::F); S.Expect(TEXT("F switches the headlamp off"), [](FDescentScenarioContext& C) { return !M3UiTender(C)->bHeadlightOn; }); M3UiTap(S, EKeys::F); S.Expect(TEXT("F switches it on again"), [](FDescentScenarioContext& C) { return M3UiTender(C)->bHeadlightOn; }); M3UiKey(S, EKeys::W, true); M3UiKey(S, EKeys::LeftShift, true); S.WaitSim(2.0); S.Expect(TEXT("W + Shift fly the Tender at boost"), [](FDescentScenarioContext& C) { const AKazahTender* T = M3UiTender(C); UE_LOG(LogDescent, Display, TEXT("M3 ui: W + Shift for 2 s: speed %.2f m/s, thrust %.2f"), T->Flight->VelMps.Size(), T->Flight->Thrust); return T->Flight->VelMps.Size() > 12.0 && FMath::IsNearlyEqual(T->Flight->Thrust, 1.7, 0.01); }); M3UiTap(S, EKeys::Escape); M3UiExpectWindow(S, TEXT("Esc pauses in flight"), EDescentWindow::Pause); TSharedRef Where = MakeShared(FVector::ZeroVector); S.Do(TEXT("where the Tender is"), [Where](FDescentScenarioContext& C) { *Where = M3UiTender(C)->GetActorLocation(); }); S.WaitFrames(10); S.Expect(TEXT("paused: the Tender does not move although W is held"), [Where](FDescentScenarioContext& C) { return M3UiTender(C)->GetActorLocation().Equals(*Where, 0.001); }); M3UiClose(S); M3UiKey(S, EKeys::LeftShift, false); M3UiKey(S, EKeys::W, false); S.WaitSim(2.5); M3UiTap(S, EKeys::E); M3UiExpectMode(S, EDescentMode::Swim); S.Expect(TEXT("E away from the pool: the swimmer is beside the Tender, which stays where it is"), [](FDescentScenarioContext& C) { const AKazahTender* T = M3UiTender(C); const ADescentCharacter* P = M3UiPC(C)->GetDescentCharacter(); const double D = Descent::UUToMetres(FVector::Dist(T->GetActorLocation(), P->GetActorLocation())); UE_LOG(LogDescent, Display, TEXT("M3 ui: left the Tender, %.2f m from its origin, Tender state %s, hatch %.2f"), D, TenderStateName(T->Flight->State), T->GetHatchOpen01()); return T->Flight->State == ETenderState::Abandoned && D > 2.2 && D < 3.2 && P->IsSwimming(); }); S.WaitSim(1.0); M3UiTap(S, EKeys::E); M3UiExpectMode(S, EDescentMode::Sub); S.Expect(TEXT("E within 4.5 m: back in the Tender, piloting"), [](FDescentScenarioContext& C) { const AKazahTender* T = M3UiTender(C); return T->Flight->State == ETenderState::Piloted && M3UiPC(C)->GetDescentCharacter()->IsSeated() && UDescentTenderFleet::Get(C.World)->Piloted.Get() == T; }); S.DumpState(TEXT("end")); S.Quit(); }