#pragma once #include "CoreMinimal.h" #include "Subsystems/GameInstanceSubsystem.h" #include "Debug/DescentImageStats.h" #include "Debug/DescentScenario.h" // complete FDescentScenarioRunner type: the UCLASS owns it through a TUniquePtr #include "DescentDebugSubsystem.generated.h" class ACameraActor; /** What a screenshot request reports back. */ struct FDescentShotResult { bool bOk = false; FString Path; FIntPoint Size = FIntPoint::ZeroValue; /** Mean of the sRGB-encoded luma, 0..1. A lit underwater scene is dark but never 0. (Copy of Stats.MeanLuma.) */ double MeanLuma = 0.0; /** Fraction of pixels with any channel above 8/255. Near 0 means a black or unrendered frame. (Copy of Stats.NonBlack.) */ double NonBlackFraction = 0.0; /** All the sidecar numbers: mean, centre, p99, non-black, mean rgb. */ FDescentLumaStats Stats; FString Error; }; /** * The agent-facing control surface of the game: JSON state dumps, screenshots to disk, and the scenario runner. * One per game instance, so it exists in a packaged game, `-game`, and Play In Editor alike. The `Descent.*` * console commands that drive it live in Debug/DescentCommands_Harness.cpp (one commands file per system). * * Every screenshot Saved/Shots/.png also gets a sidecar Saved/Shots/.json (DESIGN_UE.md section 9.2): * { "w", "h", "mean_luma", "centre_luma", "non_black", "p99_luma", "mean_rgb": [r, g, b] } * with sRGB-encoded values in 0..1. Descent.ShotRegion / Descent.ShotRing append a "regions" array to it. */ UCLASS() class THEDESCENT_API UDescentDebugSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() public: static UDescentDebugSubsystem* Get(const UWorld* World); virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override; /** Writes Saved/State/.json. Returns the absolute path in OutPathOrError on success, the reason otherwise. */ static bool DumpState(UWorld* World, const FString& Stem, FString& OutPathOrError); /** * Asks the engine for a screenshot of the game viewport and writes it to Saved/Shots/.png (and the sidecar * .json next to it). The result arrives on a later frame through Done (always called, also on failure). Only * one request at a time. */ bool RequestShot(const FString& Stem, TFunction Done); bool IsShotPending() const { return bShotPending; } /** Saved/Shots/.json, the sidecar of a shot. */ static FString GetShotSidecarPath(const FString& Stem); /** * Mean luma (sRGB-encoded, 0..1) of a rectangle given as fractions of the frame (origin top left), measured on * the PNG Saved/Shots/.png, and appended to that shot's sidecar under "regions". For a stem that has no * PNG the function fails with the reason in OutError. */ static bool MeasureShotRegion(const FString& Stem, double X0, double Y0, double X1, double Y1, double& OutMeanLuma, FString& OutError); /** Same for a ring: pixels whose centres are between R0 and R1 pixels from (Cx, Cy). */ static bool MeasureShotRing(const FString& Stem, double Cx, double Cy, double R0, double R1, double& OutMeanLuma, FString& OutError); /** Metres. Moves the possessed pawn, or the view target when there is no pawn. */ static bool Teleport(UWorld* World, const FVector& LocationM); /** Spawns the scenario camera on first use and makes it the player's view target. Metres, degrees. */ bool PlaceCamera(const FVector& LocationM, const FRotator& Rotation, float FovDeg); /** Ends the process. Non-zero codes use a forced exit so the code reaches the caller. */ static void QuitProcess(int32 ExitCode, const TCHAR* Why); bool StartScenario(const FString& Name); FDescentScenarioRunner* GetRunner() const { return Runner.Get(); } private: void OnScreenshotCaptured(int32 Width, int32 Height, const TArray& Colors); void FinishShot(const FDescentShotResult& Result); TUniquePtr Runner; UPROPERTY(Transient) TObjectPtr ScenarioCamera; bool bShotPending = false; FString PendingShotPath; FDelegateHandle ShotCapturedHandle; TFunction ShotDone; };