#pragma once #include "CoreMinimal.h" #include "Engine/EngineBaseTypes.h" #include "Core/DescentHandle.h" #include "Core/DescentIntent.h" #include "Core/DescentWorldSubsystem.h" #include "Core/Tuning/DescentTune_Sim.h" #include "DescentSimSubsystem.generated.h" /** * The twelve steps of Step(dt), in execution order (DESIGN_UE.md section 1.4). The enum is declared up front with * every entry, so a system plugs into ITS entry from its own code and nobody edits this file: * * // in the Terrain subsystem's own Initialize(): * Collection.InitializeDependency(); * StepHandle = World->GetSubsystem()->RegisterStep(EDescentSimStep::Terrain, * [this](const FDescentSimFrame& F) { Tick(F.Dt); }); * * Entries nobody registered are skipped. Registration order never changes execution order: entries run in enum * order, and callbacks inside one entry run by (SubOrder, Owner name). */ enum class EDescentSimStep : uint8 { Intent = 0, // 1 sim clock; latch intents from input or debug commands (the clock and the latch are built in and run first) Carrier, // 2 helm intent -> CarrierPhysics -> move actor; sensors; hull consequences Carousel, // 3 carousel rotation; launch / dock / board animations Tenders, // 4 flight of the piloted Tender, drift of abandoned ones, collision, energy, crush; docked recharge TenderTool, // 5 drill (carve + cleanup), suction Vitals, // 6 nutrition, O2, health, honest void, death timer Terrain, // 7 start fill / mesh jobs, remesh of carved chunks, apply finished jobs Ore, // 8 loose ore chunks, crystals Cargo, // 9 unload jobs, pipes, packer, robot arms; refinery; fabricator timers Fauna, // 10 fauna, flora, biomass capture Sonar, // 11 sonar ray budget, contacts; map visits Horror, // 12 horror director, beacons, ending Count }; /** The LateStep(dt) entries, in execution order (section 1.4: after the camera manager has updated). */ enum class EDescentLateStep : uint8 { InteractionFocus = 0, // interaction focus search Environment, // fog, post-process, sun, parameter collection, dry/wet state LightPool, // light pool commit MarineSnow, // marine snow Audio, // audio parameters HudModel, // HUD model (10 Hz): systems write their fields into FDescentHudModel Dashboards, // world dashboards (8 Hz) Count }; THEDESCENT_API const TCHAR* DescentSimStepName(EDescentSimStep Step); THEDESCENT_API const TCHAR* DescentLateStepName(EDescentLateStep Step); /** What a step callback is told. */ struct FDescentSimFrame { UWorld* World = nullptr; /** Seconds this step integrates. Step: clamped to Tune::Sim::MaxStepDt. Late step: the frame time, clamped. For a rate-limited callback, the time since its last call. */ double Dt = 0.0; /** The sim clock AFTER this step's Dt was added (so "now" for state that was just integrated). */ double SimTime = 0.0; /** 1-based count of Steps (or of LateSteps for a late callback) including this one. */ int64 StepIndex = 0; /** True while FastForward is running: skip cosmetics (spawning effects, audio, widget updates). */ bool bFastForward = false; }; using FDescentSimFn = TFunction; struct FDescentSimRegisterOptions { /** Who registered; names the callback in logs and orders callbacks that share an entry. Give every registrant its own name. */ FName Owner = NAME_None; /** Orders callbacks inside one entry: lower runs first. Ties are broken by Owner name, never by registration order. */ int32 SubOrder = 0; /** > 0: call at most this many times per second of sim (Step) or frame (LateStep) time; Dt is then the time since the last call. */ double RateHz = 0.0; }; class UDescentSimSubsystem; /** The two tick functions of the sim (section 1.4). Public so a character can list the sim as a tick prerequisite. */ struct THEDESCENT_API FDescentSimTickFunction : public FTickFunction { UDescentSimSubsystem* Target = nullptr; bool bLate = false; virtual void ExecuteTick(float DeltaTime, ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) override; virtual FString DiagnosticMessage() override; virtual FName DiagnosticContext(bool bDetailed) override; }; /** * The one place game state advances (DESIGN_UE.md section 1.4): "anything that changes game state is stepped by * UDescentSimSubsystem, in one fixed order, with one dt". Actor and component Tick are for cosmetics only. * * - `SimTick` (TG_PrePhysics) calls Step(DeltaSeconds) unless the sim is paused; `LateTick` (TG_PostUpdateWork) * calls LateStep(DeltaSeconds) always, paused or not, so the HUD keeps updating under a pause menu. * - Step(dt) clamps dt to [0, MaxStepDt], advances the sim clock, latches the intent, then runs the registered * callbacks of every EDescentSimStep entry in enum order. Step itself ignores pause (pause only stops the tick * function), so tests and FastForward can always drive it. * - FastForward(seconds, dt) calls Step in a loop inside one frame: no rendering, no character movement. The * same (seconds, dt) always runs the same steps and ends on the same clock. * - Intent: the player controller writes SetLiveIntent every frame; a debug command writes SetDebugIntent, which * wins for its sim duration. Systems read GetIntent() (the latched value) from their step callbacks. */ UCLASS() class THEDESCENT_API UDescentSimSubsystem : public UDescentWorldSubsystem { GENERATED_BODY() public: static UDescentSimSubsystem* Get(const UWorld* World); virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override; virtual void OnWorldBeginPlay(UWorld& InWorld) override; // ---- registration ----------------------------------------------------------------------------------------- FDescentHandle RegisterStep(EDescentSimStep Step, FDescentSimFn Fn); FDescentHandle RegisterStep(EDescentSimStep Step, FDescentSimFn Fn, const FDescentSimRegisterOptions& Options); FDescentHandle RegisterLateStep(EDescentLateStep Step, FDescentSimFn Fn); FDescentHandle RegisterLateStep(EDescentLateStep Step, FDescentSimFn Fn, const FDescentSimRegisterOptions& Options); /** Removes a callback registered by either call. Safe from inside a step. Returns false if the handle was not (or no longer) registered. */ bool Unregister(FDescentHandle Handle); int32 NumRegistered(EDescentSimStep Step) const; int32 NumRegistered(EDescentLateStep Step) const; // ---- stepping --------------------------------------------------------------------------------------------- /** One simulation step of Dt seconds (clamped to [0, Tune::Sim::MaxStepDt]). Re-entrant calls are refused. */ void Step(double Dt); /** The late callbacks (environment, light pool, HUD model, ...). Dt is clamped to [0, Tune::Sim::MaxLateDt]. */ void LateStep(double Dt); /** * Runs Seconds of sim in steps of Dt (Dt clamped like Step's; a final shorter step makes the total exact). * Returns the number of steps run. Refuses more than Tune::Sim::MaxFastForwardSteps. The same arguments always * give the same step count and the same clock. */ int64 FastForward(double Seconds, double Dt = Descent::Tune::Sim::DefaultFastForwardDt); // ---- clock and pause -------------------------------------------------------------------------------------- double SimTime() const { return SimTimeSec; } int64 StepCount() const { return StepCounter; } int64 LateStepCount() const { return LateStepCounter; } double LastDt() const { return LastStepDt; } bool IsFastForwarding() const { return bFastForwarding; } void SetPaused(bool bInPaused) { bPaused = bInPaused; } bool IsPaused() const { return bPaused; } // ---- intent (section 5.7) --------------------------------------------------------------------------------- /** Written by the player controller every frame (the real input). */ void SetLiveIntent(const FDescentIntent& Intent) { LiveIntent = Intent; } const FDescentIntent& GetLiveIntent() const { return LiveIntent; } /** Debug override: wins over the live intent for SimSeconds of sim time (counted in Step dt, so FastForward consumes it). */ void SetDebugIntent(const FDescentIntent& Intent, double SimSeconds); void ClearDebugIntent(); bool IsIntentOverridden() const { return DebugIntentSecondsLeft > 0.0; } double DebugIntentRemaining() const { return DebugIntentSecondsLeft; } /** The intent latched at the start of the current (or most recent) Step. This is what systems read. */ const FDescentIntent& GetIntent() const { return LatchedIntent; } /** For FTickFunction::AddPrerequisite: the character's tick waits for the sim's. */ FTickFunction& GetSimTickFunction() { return SimTickFunction; } private: struct FReg { uint32 Id = 0; uint8 Entry = 0; int32 SubOrder = 0; FName Owner; double RateHz = 0.0; double Accum = 0.0; bool bAlive = true; TSharedPtr Fn; // shared so a callback that (un)registers while running never moves itself }; FDescentHandle AddReg(TArray& Regs, bool& bDirty, uint8 Entry, FDescentSimFn&& Fn, const FDescentSimRegisterOptions& Options); void PrepareRegs(TArray& Regs, bool& bDirty); void RunRegs(TArray& Regs, const FDescentSimFrame& Frame, bool bLate); void StepInternal(double Dt, bool bFast); void LatchIntent(double Dt); void TickSim(double DeltaSeconds); void TickLate(double DeltaSeconds); void RegisterTickFunctions(UWorld& InWorld); void UnregisterTickFunctions(); friend struct FDescentSimTickFunction; TArray SimRegs; TArray LateRegs; bool bSimDirty = false; bool bLateDirty = false; uint32 NextId = 1; double SimTimeSec = 0.0; int64 StepCounter = 0; int64 LateStepCounter = 0; double LastStepDt = 0.0; bool bPaused = false; bool bInStep = false; bool bInLateStep = false; bool bFastForwarding = false; FDescentIntent LiveIntent; FDescentIntent DebugIntent; double DebugIntentSecondsLeft = 0.0; FDescentIntent LatchedIntent; FDescentSimTickFunction SimTickFunction; FDescentSimTickFunction LateTickFunction; };