#pragma once #include "CoreMinimal.h" #include "Dom/JsonObject.h" #include "Dom/JsonValue.h" class UWorld; /** * Registry of named snapshot providers. `Descent.DumpState ` calls every registered provider and * writes the result to Saved/State/.json: * * { "schema": 1, "name": "", "
": { ... }, "
": { ... } } * * A system adds its own section with one object at file scope: * * static FDescentStateProviderRegistrar GMySection(TEXT("fuel"), [](UWorld* World, FJsonObject& Out) * { * Out.SetNumberField(TEXT("litres"), 12.5); * }); * * Several systems can share ONE section by each owning a named part of it (the design's `ui` section is * {hud, widgets, window, cargo_display} written by different files). A part provider fills its own object, which * appears as a field named after the part: * * static FDescentStatePartRegistrar GHudPart(TEXT("ui"), TEXT("hud"), [](UWorld* World, FJsonObject& Out) { ... }); * * A section may have a whole-section provider, parts, or both (the provider writes first, then the parts). * * Positions are always written in metres (see DescentUnits.h) so a dump can be compared with the design * numbers directly. Providers run on the game thread, may be called with a null World (in the editor with * no game running) and must tolerate that. */ class THEDESCENT_API FDescentStateRegistry { public: using FProvider = TFunction; static FDescentStateRegistry& Get(); /** Registers (or replaces) the provider for a section. */ void Register(FName Section, FProvider Provider); bool Unregister(FName Section); /** Registers (or replaces) one named part of a section; see the class comment. */ void RegisterPart(FName Section, FName Part, FProvider Provider); bool UnregisterPart(FName Section, FName Part); TArray GetSectionNames() const; /** Builds the whole snapshot object. Sections appear in alphabetical order so dumps diff cleanly. */ TSharedRef BuildSnapshot(UWorld* World, const FString& SnapshotName) const; /** Saved/State and Saved/Shots as absolute paths. */ static FString GetStateDir(); static FString GetShotDir(); /** * Accepts a bare file stem like "boot" or "kazah_showcase_front" (letters, digits, '_', '-', '.'). * Rejects anything with a path separator or "..", so a console argument cannot write outside Saved/. */ static bool IsValidFileStem(const FString& Stem); /** Serialises the object (pretty-printed) to the given absolute path. */ static bool WriteJsonFile(const TSharedRef& Object, const FString& AbsPath, FString& OutError); /** meta, camera, pawn, world. Called by the module. */ static void RegisterBuiltInProviders(); static void UnregisterBuiltInProviders(); private: TMap Providers; TMap> Parts; }; /** RAII helper for registering a provider at file scope or as a member of a long-lived object. */ struct THEDESCENT_API FDescentStateProviderRegistrar { FDescentStateProviderRegistrar(FName InSection, FDescentStateRegistry::FProvider Provider) : Section(InSection) { FDescentStateRegistry::Get().Register(Section, MoveTemp(Provider)); } ~FDescentStateProviderRegistrar() { FDescentStateRegistry::Get().Unregister(Section); } FName Section; }; /** RAII helper for registering one part of a shared section at file scope. */ struct THEDESCENT_API FDescentStatePartRegistrar { FDescentStatePartRegistrar(FName InSection, FName InPart, FDescentStateRegistry::FProvider Provider) : Section(InSection), Part(InPart) { FDescentStateRegistry::Get().RegisterPart(Section, Part, MoveTemp(Provider)); } ~FDescentStatePartRegistrar() { FDescentStateRegistry::Get().UnregisterPart(Section, Part); } FName Section; FName Part; }; namespace Descent::Json { /** Rounds to 4 decimals so dumps stay readable and stable across runs. */ FORCEINLINE double Round(double V) { return FMath::RoundToDouble(V * 10000.0) / 10000.0; } THEDESCENT_API TSharedRef Vec3(const FVector& V); THEDESCENT_API TSharedRef Rot3(const FRotator& R); // [pitch, yaw, roll] in degrees }