#include "Core/DescentSimSubsystem.h" #include "Core/DescentMath.h" #include "Debug/DescentStateRegistry.h" #include "TheDescent.h" #include "Engine/Level.h" #include "Engine/World.h" #include "Misc/App.h" #include "Templates/UnrealTemplate.h" namespace { const TCHAR* const SimStepNames[] = { TEXT("Intent"), TEXT("Carrier"), TEXT("Carousel"), TEXT("Tenders"), TEXT("TenderTool"), TEXT("Vitals"), TEXT("Terrain"), TEXT("Ore"), TEXT("Cargo"), TEXT("Fauna"), TEXT("Sonar"), TEXT("Horror"), }; static_assert(UE_ARRAY_COUNT(SimStepNames) == static_cast(EDescentSimStep::Count), "name every EDescentSimStep entry"); const TCHAR* const LateStepNames[] = { TEXT("InteractionFocus"), TEXT("Environment"), TEXT("LightPool"), TEXT("MarineSnow"), TEXT("Audio"), TEXT("HudModel"), TEXT("Dashboards"), }; static_assert(UE_ARRAY_COUNT(LateStepNames) == static_cast(EDescentLateStep::Count), "name every EDescentLateStep entry"); /** NaN-safe clamp to [0, Max]. */ double ClampDt(double Dt, double Max) { return (Dt > 0.0) ? FMath::Min(Dt, Max) : 0.0; } } const TCHAR* DescentSimStepName(EDescentSimStep Step) { const int32 I = static_cast(Step); return (I >= 0 && I < UE_ARRAY_COUNT(SimStepNames)) ? SimStepNames[I] : TEXT("?"); } const TCHAR* DescentLateStepName(EDescentLateStep Step) { const int32 I = static_cast(Step); return (I >= 0 && I < UE_ARRAY_COUNT(LateStepNames)) ? LateStepNames[I] : TEXT("?"); } // --------------------------------------------------------------------------------------------------------- // Tick functions // --------------------------------------------------------------------------------------------------------- void FDescentSimTickFunction::ExecuteTick(float DeltaTime, ELevelTick TickType, ENamedThreads::Type /*CurrentThread*/, const FGraphEventRef& /*MyCompletionGraphEvent*/) { // The editor's own viewport tick (LEVELTICK_ViewportsOnly) is not gameplay. if (!Target || TickType == LEVELTICK_ViewportsOnly) { return; } if (bLate) { Target->TickLate(DeltaTime); } else { Target->TickSim(DeltaTime); } } FString FDescentSimTickFunction::DiagnosticMessage() { return bLate ? TEXT("UDescentSimSubsystem::LateTick") : TEXT("UDescentSimSubsystem::SimTick"); } FName FDescentSimTickFunction::DiagnosticContext(bool /*bDetailed*/) { return FName(TEXT("DescentSim")); } // --------------------------------------------------------------------------------------------------------- // Subsystem // --------------------------------------------------------------------------------------------------------- UDescentSimSubsystem* UDescentSimSubsystem::Get(const UWorld* World) { return World ? World->GetSubsystem() : nullptr; } void UDescentSimSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); SimTickFunction.Target = this; SimTickFunction.bLate = false; SimTickFunction.TickGroup = TG_PrePhysics; SimTickFunction.bCanEverTick = true; SimTickFunction.bStartWithTickEnabled = true; SimTickFunction.bTickEvenWhenPaused = false; // an engine pause (SetPause) stops the sim, like it stops every actor SimTickFunction.bHighPriority = true; LateTickFunction.Target = this; LateTickFunction.bLate = true; LateTickFunction.TickGroup = TG_PostUpdateWork; // after the camera manager has updated LateTickFunction.bCanEverTick = true; LateTickFunction.bStartWithTickEnabled = true; LateTickFunction.bTickEvenWhenPaused = true; // the HUD and pause menu keep updating } void UDescentSimSubsystem::OnWorldBeginPlay(UWorld& InWorld) { Super::OnWorldBeginPlay(InWorld); // Registered with the persistent level once the world is playing: before that, nothing may step the game. RegisterTickFunctions(InWorld); } void UDescentSimSubsystem::Deinitialize() { UnregisterTickFunctions(); SimRegs.Reset(); LateRegs.Reset(); Super::Deinitialize(); } void UDescentSimSubsystem::RegisterTickFunctions(UWorld& InWorld) { if (ULevel* Level = InWorld.PersistentLevel) { if (!SimTickFunction.IsTickFunctionRegistered()) { SimTickFunction.RegisterTickFunction(Level); } if (!LateTickFunction.IsTickFunctionRegistered()) { LateTickFunction.RegisterTickFunction(Level); } } } void UDescentSimSubsystem::UnregisterTickFunctions() { if (SimTickFunction.IsTickFunctionRegistered()) { SimTickFunction.UnRegisterTickFunction(); } if (LateTickFunction.IsTickFunctionRegistered()) { LateTickFunction.UnRegisterTickFunction(); } } void UDescentSimSubsystem::TickSim(double DeltaSeconds) { if (!bPaused) { Step(DeltaSeconds); } } void UDescentSimSubsystem::TickLate(double DeltaSeconds) { LateStep(DeltaSeconds); } // --------------------------------------------------------------------------------------------------------- // Registration // --------------------------------------------------------------------------------------------------------- FDescentHandle UDescentSimSubsystem::AddReg(TArray& Regs, bool& bDirty, uint8 Entry, FDescentSimFn&& Fn, const FDescentSimRegisterOptions& Options) { FDescentHandle Handle; if (!Fn) { UE_LOG(LogDescent, Error, TEXT("Sim: registration by '%s' has an empty function"), *Options.Owner.ToString()); return Handle; } FReg R; R.Id = NextId++; R.Entry = Entry; R.SubOrder = Options.SubOrder; R.Owner = Options.Owner; R.RateHz = FMath::Max(Options.RateHz, 0.0); R.Fn = MakeShared(MoveTemp(Fn)); Regs.Add(MoveTemp(R)); bDirty = true; Handle.Id = Regs.Last().Id; return Handle; } FDescentHandle UDescentSimSubsystem::RegisterStep(EDescentSimStep Step, FDescentSimFn Fn) { return RegisterStep(Step, MoveTemp(Fn), FDescentSimRegisterOptions()); } FDescentHandle UDescentSimSubsystem::RegisterStep(EDescentSimStep Step, FDescentSimFn Fn, const FDescentSimRegisterOptions& Options) { if (Step >= EDescentSimStep::Count) { UE_LOG(LogDescent, Error, TEXT("Sim: invalid step entry")); return FDescentHandle(); } return AddReg(SimRegs, bSimDirty, static_cast(Step), MoveTemp(Fn), Options); } FDescentHandle UDescentSimSubsystem::RegisterLateStep(EDescentLateStep Step, FDescentSimFn Fn) { return RegisterLateStep(Step, MoveTemp(Fn), FDescentSimRegisterOptions()); } FDescentHandle UDescentSimSubsystem::RegisterLateStep(EDescentLateStep Step, FDescentSimFn Fn, const FDescentSimRegisterOptions& Options) { if (Step >= EDescentLateStep::Count) { UE_LOG(LogDescent, Error, TEXT("Sim: invalid late step entry")); return FDescentHandle(); } return AddReg(LateRegs, bLateDirty, static_cast(Step), MoveTemp(Fn), Options); } bool UDescentSimSubsystem::Unregister(FDescentHandle Handle) { if (!Handle.IsValid()) { return false; } // Marked dead rather than removed: the arrays may be mid-iteration. They are compacted at the next step. for (TArray* Regs : { &SimRegs, &LateRegs }) { for (FReg& R : *Regs) { if (R.Id == Handle.Id && R.bAlive) { R.bAlive = false; return true; } } } return false; } int32 UDescentSimSubsystem::NumRegistered(EDescentSimStep Step) const { int32 N = 0; for (const FReg& R : SimRegs) { N += (R.bAlive && R.Entry == static_cast(Step)) ? 1 : 0; } return N; } int32 UDescentSimSubsystem::NumRegistered(EDescentLateStep Step) const { int32 N = 0; for (const FReg& R : LateRegs) { N += (R.bAlive && R.Entry == static_cast(Step)) ? 1 : 0; } return N; } void UDescentSimSubsystem::PrepareRegs(TArray& Regs, bool& bDirty) { Regs.RemoveAll([](const FReg& R) { return !R.bAlive; }); if (bDirty) { // The order that matters: entry, then SubOrder, then owner name. The id (registration sequence) only // breaks ties between registrations that gave the same owner AND sub-order, which a caller can avoid. Regs.StableSort([](const FReg& A, const FReg& B) { if (A.Entry != B.Entry) { return A.Entry < B.Entry; } if (A.SubOrder != B.SubOrder) { return A.SubOrder < B.SubOrder; } const int32 C = A.Owner.Compare(B.Owner); if (C != 0) { return C < 0; } return A.Id < B.Id; }); bDirty = false; } } void UDescentSimSubsystem::RunRegs(TArray& Regs, const FDescentSimFrame& Frame, bool /*bLate*/) { // Index-based over the count at entry: a callback may register (appends, ignored this pass) or unregister // (marks dead, skipped) without invalidating the loop. Never hold a reference into Regs across a call. const int32 Count = Regs.Num(); for (int32 I = 0; I < Count; ++I) { if (!Regs[I].bAlive) { continue; } FDescentSimFrame CallFrame = Frame; if (Regs[I].RateHz > 0.0) { Regs[I].Accum += Frame.Dt; if (Regs[I].Accum + 1e-9 < 1.0 / Regs[I].RateHz) { continue; } CallFrame.Dt = Regs[I].Accum; Regs[I].Accum = 0.0; } TSharedPtr Fn = Regs[I].Fn; (*Fn)(CallFrame); } } // --------------------------------------------------------------------------------------------------------- // Stepping // --------------------------------------------------------------------------------------------------------- void UDescentSimSubsystem::Step(double Dt) { StepInternal(Dt, /*bFast*/ false); } void UDescentSimSubsystem::StepInternal(double RequestedDt, bool bFast) { if (bInStep) { UE_LOG(LogDescent, Error, TEXT("Sim: Step called from inside a step; ignored")); return; } TGuardValue InStep(bInStep, true); TGuardValue Fast(bFastForwarding, bFast); const double Dt = ClampDt(RequestedDt, Descent::Tune::Sim::MaxStepDt); PrepareRegs(SimRegs, bSimDirty); // Entry 1, built in: the clock and the intent latch run before any registered callback. LastStepDt = Dt; SimTimeSec += Dt; ++StepCounter; LatchIntent(Dt); FDescentSimFrame Frame; Frame.World = GetWorld(); Frame.Dt = Dt; Frame.SimTime = SimTimeSec; Frame.StepIndex = StepCounter; Frame.bFastForward = bFast; RunRegs(SimRegs, Frame, /*bLate*/ false); } void UDescentSimSubsystem::LateStep(double RequestedDt) { if (bInLateStep) { UE_LOG(LogDescent, Error, TEXT("Sim: LateStep called from inside a late step; ignored")); return; } TGuardValue InLate(bInLateStep, true); const double Dt = ClampDt(RequestedDt, Descent::Tune::Sim::MaxLateDt); PrepareRegs(LateRegs, bLateDirty); ++LateStepCounter; FDescentSimFrame Frame; Frame.World = GetWorld(); Frame.Dt = Dt; Frame.SimTime = SimTimeSec; Frame.StepIndex = LateStepCounter; RunRegs(LateRegs, Frame, /*bLate*/ true); } int64 UDescentSimSubsystem::FastForward(double Seconds, double RequestedDt) { const double Dt = ClampDt(RequestedDt, Descent::Tune::Sim::MaxStepDt); if (!(Seconds > 0.0) || !(Dt > 0.0)) { return 0; } // The tolerance keeps 10 s at 1/30 at exactly 300 full steps although 10 / (1/30) is 300.00000000000006 in doubles. const double FullD = FMath::FloorToDouble(Seconds / Dt + 1e-6); const double Remainder = Seconds - FullD * Dt; const bool bRemainder = Remainder > 1e-9; if (FullD + (bRemainder ? 1.0 : 0.0) > static_cast(Descent::Tune::Sim::MaxFastForwardSteps)) { UE_LOG(LogDescent, Error, TEXT("Sim: FastForward(%.1f s, dt %.4f) would run more than %lld steps; refused"), Seconds, Dt, Descent::Tune::Sim::MaxFastForwardSteps); return 0; } const int64 Full = static_cast(FullD); for (int64 I = 0; I < Full; ++I) { StepInternal(Dt, /*bFast*/ true); } int64 Steps = Full; if (bRemainder) { StepInternal(Remainder, /*bFast*/ true); ++Steps; } return Steps; } // --------------------------------------------------------------------------------------------------------- // Intent latch // --------------------------------------------------------------------------------------------------------- void UDescentSimSubsystem::SetDebugIntent(const FDescentIntent& Intent, double SimSeconds) { DebugIntent = Intent; DebugIntentSecondsLeft = FMath::Max(SimSeconds, 0.0); } void UDescentSimSubsystem::ClearDebugIntent() { DebugIntent = FDescentIntent(); DebugIntentSecondsLeft = 0.0; } void UDescentSimSubsystem::LatchIntent(double Dt) { if (DebugIntentSecondsLeft > Descent::Tune::Sim::OverrideEpsilonSec) { // The override wins for its sim duration. It is consumed by the dt of the steps it was latched for, so // "1 second" is 30 steps of 1/30 whether they were played or fast-forwarded. LatchedIntent = DebugIntent.Sanitized(); DebugIntentSecondsLeft -= Dt; if (DebugIntentSecondsLeft <= Descent::Tune::Sim::OverrideEpsilonSec) { ClearDebugIntent(); } } else { LatchedIntent = LiveIntent.Sanitized(); } } // --------------------------------------------------------------------------------------------------------- // State section "sim" // --------------------------------------------------------------------------------------------------------- namespace { TSharedRef IntentToJson(const FDescentIntent& I) { TSharedRef J = MakeShared(); J->SetField(TEXT("move"), Descent::Json::Vec3(I.Move)); TArray> Look; Look.Add(MakeShared(Descent::Json::Round(I.Look.X))); Look.Add(MakeShared(Descent::Json::Round(I.Look.Y))); J->SetArrayField(TEXT("look_dps"), Look); J->SetBoolField(TEXT("primary"), I.bPrimary); J->SetBoolField(TEXT("secondary"), I.bSecondary); J->SetBoolField(TEXT("sprint"), I.bSprint); J->SetBoolField(TEXT("jump"), I.bJump); J->SetBoolField(TEXT("down"), I.bDown); return J; } FDescentStateProviderRegistrar GSimSection(TEXT("sim"), [](UWorld* World, FJsonObject& Out) { const UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(World); Out.SetBoolField(TEXT("available"), Sim != nullptr); if (!Sim) { return; } Out.SetNumberField(TEXT("time_s"), Descent::Json::Round(Sim->SimTime())); Out.SetNumberField(TEXT("step_count"), static_cast(Sim->StepCount())); Out.SetNumberField(TEXT("late_step_count"), static_cast(Sim->LateStepCount())); // The fixed frame time of the run (-UseFixedTimeStep -FPS=n), 0 when frames use real time. Out.SetNumberField(TEXT("fixed_dt"), FApp::UseFixedTimeStep() ? Descent::Json::Round(FApp::GetFixedDeltaTime()) : 0.0); Out.SetBoolField(TEXT("paused"), Sim->IsPaused()); Out.SetNumberField(TEXT("last_dt"), Descent::Json::Round(Sim->LastDt())); Out.SetNumberField(TEXT("max_dt"), Descent::Tune::Sim::MaxStepDt); TSharedRef Intent = IntentToJson(Sim->GetIntent()); Intent->SetBoolField(TEXT("overridden"), Sim->IsIntentOverridden()); Intent->SetNumberField(TEXT("override_seconds_left"), Descent::Json::Round(Sim->DebugIntentRemaining())); Out.SetObjectField(TEXT("intent"), Intent); // Which entries have callbacks: shows at a glance that a system registered itself. TSharedRef Reg = MakeShared(); for (int32 I = 0; I < static_cast(EDescentSimStep::Count); ++I) { Reg->SetNumberField(DescentSimStepName(static_cast(I)), Sim->NumRegistered(static_cast(I))); } Out.SetObjectField(TEXT("registered_steps"), Reg); TSharedRef Late = MakeShared(); for (int32 I = 0; I < static_cast(EDescentLateStep::Count); ++I) { Late->SetNumberField(DescentLateStepName(static_cast(I)), Sim->NumRegistered(static_cast(I))); } Out.SetObjectField(TEXT("registered_late_steps"), Late); }); }