// Scenarios of the simulation core: the real tick functions, the fixed time step, WaitSim, FastForward, the intent // override and the boot sequence, in a running game. (The same logic is covered headlessly by Tests/SimTests.cpp; // this one proves it also holds when the engine drives the tick.) #include "Core/DescentBoot.h" #include "Core/DescentGameMode.h" #include "Core/DescentSimSubsystem.h" #include "Debug/DescentScenario.h" #include "TheDescent.h" #include "Engine/World.h" #include "Misc/App.h" namespace { /** What the recording step on the Intent entry saw. Shared by the lambdas of one scenario build. */ struct FCoreSimCounters { FDescentHandle Handle; int64 StepsSeen = 0; double DtSum = 0.0; /** Steps (and their dt) on which the latched intent was "forward + sprint", i.e. the debug override. */ int64 OverrideSteps = 0; double OverrideDt = 0.0; double ClockAtMark = 0.0; int64 StepsAtMark = 0; }; } DESCENT_SCENARIO(core_sim, "The sim tick in a running game: fixed 30 FPS step, WaitSim, FastForward, the debug intent override and the boot report.") { S.TimeLimit(120.f); TSharedRef K = MakeShared(); S.Expect(TEXT("boot steps ran and passed"), [](FDescentScenarioContext& C) { const ADescentGameMode* GM = C.World->GetAuthGameMode(); return GM && GM->GetBootReport().bRan && GM->GetBootReport().bOk; }); S.Expect(TEXT("a sim subsystem exists"), [](FDescentScenarioContext& C) { return C.Sim() != nullptr; }); // One recording callback on the Intent entry for the whole scenario: it runs after the latch, so it sees // the intent the systems would see. S.Do(TEXT("register the recording step"), [K](FDescentScenarioContext& C) { UDescentSimSubsystem* Sim = C.Sim(); FDescentSimRegisterOptions Options; Options.Owner = TEXT("core_sim_scenario"); K->Handle = Sim->RegisterStep(EDescentSimStep::Intent, [K, Sim](const FDescentSimFrame& F) { ++K->StepsSeen; K->DtSum += F.Dt; const FDescentIntent& I = Sim->GetIntent(); if (I.Move.X == 1.0 && I.bSprint) { ++K->OverrideSteps; K->OverrideDt += F.Dt; } }, Options); if (!K->Handle.IsValid()) { C.Fail(TEXT("RegisterStep returned an invalid handle")); } }); S.Do(TEXT("mark the clock"), [K](FDescentScenarioContext& C) { K->ClockAtMark = C.Sim()->SimTime(); K->StepsAtMark = C.Sim()->StepCount(); K->StepsSeen = 0; K->DtSum = 0.0; }); // Real ticking: half a sim second is 15 steps of the fixed 1/30 s frame. S.WaitSim(0.5); S.ExpectNear(TEXT("WaitSim(0.5) moved the sim clock by 0.5 s"), [K](FDescentScenarioContext& C) { return C.Sim()->SimTime() - K->ClockAtMark; }, 0.5, 0.07); S.Expect(TEXT("each real step is 1/30 s under the fixed time step"), [K](FDescentScenarioContext&) { // Only a run with -UseFixedTimeStep -FPS=30 (run_scenario.ps1 passes both) can promise this. if (!FApp::UseFixedTimeStep()) { return true; } return K->StepsSeen >= 10 && FMath::IsNearlyEqual(K->DtSum / K->StepsSeen, 1.0 / 30.0, 0.0005); }); // FastForward: 60 s at 1/30 is exactly 1800 steps, all inside one frame, and the clock moves by exactly 60 s. S.Do(TEXT("mark the clock before fast-forward"), [K](FDescentScenarioContext& C) { K->ClockAtMark = C.Sim()->SimTime(); K->StepsAtMark = C.Sim()->StepCount(); }); S.Exec(TEXT("Descent.Sim.FastForward 60")); S.ExpectNear(TEXT("fast-forward moved the clock by 60 s"), [K](FDescentScenarioContext& C) { return C.Sim()->SimTime() - K->ClockAtMark; }, 60.0, 0.001); S.ExpectNear(TEXT("fast-forward ran 1800 steps"), [K](FDescentScenarioContext& C) { return static_cast(C.Sim()->StepCount() - K->StepsAtMark); }, 1800.0, 0.0); // Pause stops the tick function: the clock holds while frames go by, and runs again when unpaused. S.Exec(TEXT("Descent.Sim.Pause 1")); S.Do(TEXT("mark the clock before the pause"), [K](FDescentScenarioContext& C) { K->ClockAtMark = C.Sim()->SimTime(); }); S.WaitFrames(6); S.ExpectNear(TEXT("the clock held while paused"), [K](FDescentScenarioContext& C) { return C.Sim()->SimTime() - K->ClockAtMark; }, 0.0, 0.0); S.Expect(TEXT("the late steps kept running while paused"), [](FDescentScenarioContext& C) { return C.Sim()->IsPaused() && C.Sim()->LateStepCount() > 0; }); S.Exec(TEXT("Descent.Sim.Pause 0")); S.WaitSim(0.1); S.Expect(TEXT("the clock runs again after the pause"), [K](FDescentScenarioContext& C) { return !C.Sim()->IsPaused() && C.Sim()->SimTime() - K->ClockAtMark >= 0.09; }); // The intent override: hold forward + sprint for 1 s of sim time, see it latched, see it expire. S.Do(TEXT("clear the override counters"), [K](FDescentScenarioContext&) { K->OverrideSteps = 0; K->OverrideDt = 0.0; }); S.Exec(TEXT("Descent.Player.Intent 1 0 0 0 0 1 0 0 1")); S.Expect(TEXT("the override is active"), [](FDescentScenarioContext& C) { return C.Sim()->IsIntentOverridden(); }); S.WaitSim(0.4); S.Expect(TEXT("while held, the latched intent is forward + sprint and nothing else"), [](FDescentScenarioContext& C) { const FDescentIntent& I = C.Sim()->GetIntent(); return I.Move.X == 1.0 && I.Move.Y == 0.0 && I.bSprint && !I.bPrimary && !I.bSecondary; }); S.WaitSim(0.9); S.Expect(TEXT("after its second the override has expired and the live (neutral) intent is back"), [](FDescentScenarioContext& C) { return !C.Sim()->IsIntentOverridden() && C.Sim()->GetIntent().IsNeutral(); }); // The override was latched for exactly its duration of sim time (to within the last step). S.ExpectNear(TEXT("the override lasted 1 s of sim time"), [K](FDescentScenarioContext&) { return K->OverrideDt; }, 1.0, 0.04); S.Do(TEXT("remove the recording step"), [K](FDescentScenarioContext& C) { C.Sim()->Unregister(K->Handle); }); S.DumpState(TEXT("end")); S.Quit(); }