Extending capture

Register a C++ component capturer so World State Reset Framework restores a component type it does not cover natively, with payload versioning and a stable API contract.

WSRF handles its documented native component types automatically. A project can register its own capturer in C++ for a component type WSRF does not cover — a spline whose control points are edited at runtime is a good example.

This is not the extension point for gameplay state

Health, AI state, quest progress, inventory, consumed decisions and Blueprint variables belong in Custom State. The Actor owns the meaning and the migration of that data. A component capturer is only appropriate for stable, reusable component configuration.

Runtime contract

  • A capturer receives only AActor&, UActorComponent&, and a capture or restore context. It cannot reach WSRF registration records, the subsystem, or another Actor’s state.
  • The capturer owns its payload schema. WSRF stores the payload as opaque bytes alongside the capturer ID, capturer version and stable component name.
  • Provider modules register through IModularFeatures during StartupModule. Registration after a world registry has been frozen is rejected.
  • Provider IDs sort lexically; capturers then sort by SortOrder and capturer ID. Execution order is deterministic.
  • Sort orders 0–999 are reserved by WSRF. Third-party capturers start at FWSRFComponentCapturerRegistry::FirstExternalSortOrder.
  • Capturer IDs must be globally unique. A collision with a native or already-discovered ID is rejected and reported.
  • WSRF never creates a missing component during restore.

The capturer

class FMySplineCapturer final : public IWSRFComponentStateCapturer
{
public:
    virtual FName GetCapturerId() const override
    {
        return TEXT("MyGame.Spline");
    }

    virtual int32 GetCapturerVersion() const override
    {
        return 1;
    }

    virtual UClass* GetSupportedComponentClass() const override
    {
        return USplineComponent::StaticClass();
    }

    virtual int32 GetSortOrder() const override
    {
        return FWSRFComponentCapturerRegistry::FirstExternalSortOrder;
    }

    virtual void CaptureComponent(
        AActor& Actor,
        UActorComponent& Component,
        FWSRFComponentCaptureContext& Context) const override;

    virtual bool RestoreComponent(
        AActor& Actor,
        UActorComponent& Component,
        const FWSRFComponentRestoreContext& Context,
        FString& OutError) const override;
};

The provider module

class FMyWSRFCapturerModule final
    : public IModuleInterface
    , public IWSRFComponentCapturerProvider
{
public:
    virtual void StartupModule() override
    {
        IModularFeatures::Get().RegisterModularFeature(
            GetModularFeatureName(), this);
    }

    virtual void ShutdownModule() override
    {
        if (IModularFeatures::Get().IsModularFeatureAvailable(GetModularFeatureName()))
        {
            IModularFeatures::Get().UnregisterModularFeature(
                GetModularFeatureName(), this);
        }
    }

    virtual FName GetProviderId() const override
    {
        return TEXT("MyGame.WSRFCapturers");
    }

    virtual void GetComponentCapturers(
        TArray<TSharedRef<IWSRFComponentStateCapturer>>& OutCapturers) const override
    {
        OutCapturers.Add(MakeShared<FMySplineCapturer>());
    }
};

IMPLEMENT_MODULE(FMyWSRFCapturerModule, MyWSRFCapturer)

Declare the module as "Type": "Runtime" with "LoadingPhase": "Default". It must stay loaded while worlds using its capturer exist, because a frozen world registry retains shared capturer instances.

The complete example ships with the plugin

Docs/ExtendingCapture.md contains the full four-file example — payload struct, capturer, provider module and Build.cs — in a form you can copy directly. It is the same design compiled and exercised by the plugin’s own automation suite.

Payload versioning

The capturer ID identifies the meaning of the bytes. Never reuse an ID for a different component or schema.

When the payload structure changes:

  1. increment GetCapturerVersion();
  2. override CanRestoreVersion() if the new capturer can still read older payloads;
  3. deserialise the correct legacy struct for each accepted version, then convert it;
  4. leave unsupported future versions rejected — WSRF reports and ignores them rather than guessing.

The Actor-state schema does not change when you change an opaque payload. That version belongs to the capturer. FWSRFActorState records whether the external section was captured, so an older v2 save that predates this API leaves third-party components at their construction or Blueprint values.

Removing an extension

If a save references a capturer that is no longer registered, WSRF reports the capturer ID and the component name, ignores that payload only, and continues restoring everything else it can read. It never reinterprets the bytes and never creates a replacement component. Reinstalling the provider makes the retained payload readable again.

API stability

For the WSRF 1.x line, these contracts are stable public API:

  • IWSRFComponentStateCapturer method meanings;
  • IWSRFComponentCapturerProvider discovery through its modular-feature name;
  • the capture and restore contexts’ opaque payload methods;
  • capturer ID plus positive capturer version routing;
  • deterministic order by provider ID, sort order and capturer ID;
  • the reservation of third-party sort orders beginning at FirstExternalSortOrder;
  • missing, unknown and unsupported payloads being isolated rather than failing the complete restore.

Additive context helpers, diagnostics or restore phases may appear in a compatible minor release. Private registry storage, native payload structures, cache implementation and subsystem record types are not extension points and may change. A future major version may revise the contract with migration notes; WSRF will not silently reinterpret an existing third-party payload ID.

Niagara adapter without the Demo

The core Runtime never loads Niagara. The included Demo loads WorldStateResetFrameworkNiagara because it uses Niagara content. If a project removes the Demo but wants Niagara component capture, keep the adapter module, enable Unreal’s Niagara plugin, and load WorldStateResetFrameworkNiagara before the first WSRF game-world registry is frozen. If the adapter is absent, Niagara payloads are reported and skipped while other state continues to restore.

Last updated: 2026-09-04

Type to search titles, sections and keywords.