#pragma once #include "CoreMinimal.h" #include "Containers/Ticker.h" #include "Dom/JsonObject.h" #include "Debug/DescentImageStats.h" class AActor; class UWorld; class UDescentDebugSubsystem; class UDescentSimSubsystem; /** * Scenarios: scripted, unattended runs of the game. `UnrealEditor.exe ... -game -DescentScenario=` * (see Tools/run_scenario.ps1) finds the scenario registered under , walks its steps one frame at a * time, and exits the process when it is done. * * Adding one takes a few lines, in any .cpp of the module: * * DESCENT_SCENARIO(my_scenario, "What this checks, in a sentence.") * { * S.TimeLimit(60.f); * S.Do(TEXT("spawn"), [](FDescentScenarioContext& C) { C.World->SpawnActor(); }); * S.LookAt(FVector(8, 6, 3), FVector(0, 0, 0.5f)); // camera position and target, in metres * S.Shot(TEXT("overview")); // Saved/Shots/my_scenario_overview.png * S.DumpState(TEXT("end")); // Saved/State/my_scenario_end.json * S.Quit(); * } * * Time: WaitSeconds is wall-clock; WaitSim waits on the SIM clock (UDescentSimSubsystem::SimTime), which * run_scenario.ps1 makes independent of the machine by running the game at a fixed 30 FPS step. FastForward runs * the sim without rendering. Expect / ExpectNear assert, and name the label and the numbers in the log when they fail. * * A scenario that runs past its time limit, hits a failing step (Ctx.Fail), or takes a black frame logs an * error and ends the process with a non-zero exit code. A scenario that reaches Quit() ends with 0. * Either way the log gets one line starting with DESCENT_SCENARIO_RESULT, which the scripts look for. */ /** What a step sees. Steps are called once per frame until they return true. */ class THEDESCENT_API FDescentScenarioContext { public: UWorld* World = nullptr; UDescentDebugSubsystem* Debug = nullptr; FString ScenarioName; FIntPoint RequestedResolution = FIntPoint(1280, 720); /** Frames and seconds since the current step was first called. */ int64 FramesInStep() const { return static_cast(GFrameCounter) - StepEnterFrame; } double SecondsInStep() const { return FPlatformTime::Seconds() - StepEnterTime; } double SecondsInScenario() const { return FPlatformTime::Seconds() - ScenarioStartTime; } /** Actors a scenario wants to find again in a later step. */ void SetNamed(FName Key, AActor* Actor); AActor* GetNamed(FName Key) const; /** Fails the scenario: logs the reason as an error and ends the run with a non-zero exit code. */ void Fail(const FString& Reason); bool HasFailed() const { return bFailed; } /** Statistics of the most recent Shot() of this scenario (all zeros before the first), for Expect on brightness. */ FDescentLumaStats LastShotStats; /** The world's sim (null before the world exists). */ UDescentSimSubsystem* Sim() const; /** The state dump as an object right now (what Descent.DumpState would write), for asserting on `Snapshot()->GetObjectField("sim")`. */ TSharedRef Snapshot() const; /** Asks the runner to end the process after this step. 0 is a pass. */ void RequestQuit(int32 ExitCode) { bQuitRequested = true; QuitCode = ExitCode; } // Bookkeeping owned by the runner. int64 StepEnterFrame = 0; double StepEnterTime = 0.0; double ScenarioStartTime = 0.0; private: friend class FDescentScenarioRunner; TMap> Named; bool bFailed = false; FString FailReason; bool bQuitRequested = false; int32 QuitCode = 0; }; /** Returns true when the step is finished. Called every frame until it does. */ using FDescentStepFn = TFunction; struct FDescentScenarioStep { FString Label; FDescentStepFn Run; }; /** Collects steps. The methods chain. All positions are metres, all angles degrees. */ class THEDESCENT_API FDescentScenarioBuilder { public: /** Wall-clock limit for the whole scenario, counted from its first step. Default 180 s. */ FDescentScenarioBuilder& TimeLimit(float Seconds); /** Runs a function once, then moves on to the next step. */ FDescentScenarioBuilder& Do(const FString& Label, TFunction Fn); /** Runs a function every frame until it returns true. */ FDescentScenarioBuilder& Until(const FString& Label, FDescentStepFn Fn); FDescentScenarioBuilder& WaitFrames(int32 Frames); /** Wall-clock seconds. */ FDescentScenarioBuilder& WaitSeconds(float Seconds); /** Sim seconds: waits until UDescentSimSubsystem::SimTime() has advanced by Seconds since this step began. */ FDescentScenarioBuilder& WaitSim(double Seconds); /** Runs the sim for Seconds in one frame (UDescentSimSubsystem::FastForward): no rendering, no character movement. */ FDescentScenarioBuilder& FastForward(double Seconds, double Dt = 1.0 / 30.0); /** Fails the scenario, naming Label, when the function returns false. */ FDescentScenarioBuilder& Expect(const FString& Label, TFunction Fn); /** Fails the scenario, naming Label and the numbers, unless |value - expected| <= tolerance. */ FDescentScenarioBuilder& ExpectNear(const FString& Label, TFunction ValueFn, double Expected, double Tolerance); FDescentScenarioBuilder& ExpectNear(const FString& Label, double Value, double Expected, double Tolerance); /** Runs a console command (anything typed into the ~ console). */ FDescentScenarioBuilder& Exec(const FString& Command); /** Puts the scenario camera at a position/rotation and makes it the view. */ FDescentScenarioBuilder& Camera(const FVector& LocationM, const FRotator& Rotation, float FovDeg = 60.f); /** Same, but aimed at a target point. */ FDescentScenarioBuilder& LookAt(const FVector& LocationM, const FVector& TargetM, float FovDeg = 60.f); /** * Waits until shaders and async assets have finished compiling, then at least WarmupFrames frames and * WarmupSeconds seconds, then captures the viewport to Saved/Shots/_