Skip to content
Tuxedo CatSTUDIO
Menu

Contributor Guide — frozen V1 contract

Review draft. This source page has not completed website editorial and release-specific verification.

Contributors add product-owned state without making core depend on that product. Public interfaces are in Contributors/SaveGameDirectorContributor.h, module SaveGameDirectorRuntime.

Register a module-lifetime ISaveGameDirectorContributorFactory using UE::SaveGameDirector::Contributors::RegisterFactory; unregister that exact object before its module unloads. Registry is non-owning. SaveGame Director creates and owns one contributor instance per factory per Game Instance. Contributor callbacks run on the game thread.

Factory ID and contributor ID must be stable, non-None, unique among active factories/contributors, and independent of slot, world, session, and process. GetProviderPluginName() is diagnostic metadata. GetSchemaVersion() returns a positive payload schema version. CapturePayload(OutPayload, OutError) creates the opaque bytes; empty is valid only if your payload contract defines it as valid.

Preflight matches records by ContributorId, checks Required/Optional semantics and exact schema version, then calls validation. V1 never coerces schema versions and never calls validation as migration.

Saved/current condition Behavior
Current registered Required contributor has no record Fail RequiredContributorMissing before Actor/world restoration.
Current registered Optional contributor has no record Allowed; no fabricated payload or warning solely for absence.
Saved unknown Optional record Warning, then skip.
Saved unknown Required record Fail restore.
Known contributor schema differs Fail before accepting payload.
Known matching schema Validate, then restore in configured phase.

General contributor payload migration is deferred to V1.1. Incrementing a schema makes old records fail under V1; plan product-level compatibility before changing it.

Each contributor selects one phase: BeforeActorRestore, AfterActorCreationBeforeProperties, AfterActorPropertyRestore, or FinalPostRestore. Its payload callback runs once, not in all four. GetRestorePriority() orders within phase; lower priority runs first, then deterministic identity order. Do not depend on factory registration order. Capture ordering is deterministic by contributor ID.

BeforeActorRestore runs before Actor restoration begins. AfterActorCreationBeforeProperties runs after eligible Actors have been discovered, reconciled, and any missing runtime Actors have been spawned and registered, but before their saved reflected state is applied. AfterActorPropertyRestore runs after supported Actor/component/subobject restoration, and FinalPostRestore runs last. Missing placed Actors can still be deferred under the Actor restore policy; this phase does not promise that unavailable placed Actors have been loaded into the current world.

The following uses actual interface signatures. The integer archive payload is illustrative product data; replace stable IDs and fields for your plugin.

#include "Contributors/SaveGameDirectorContributor.h"
#include "Contributors/SaveGameDirectorContributorRegistry.h"
#include "Serialization/MemoryReader.h"
#include "Serialization/MemoryWriter.h"
#include "Modules/ModuleManager.h"
class FCheckpointContributor final : public ISaveGameDirectorContributor
{
public:
FName GetContributorId() const override { return TEXT("ExampleGame.Checkpoint"); }
FName GetProviderPluginName() const override { return TEXT("ExampleGame"); }
int32 GetSchemaVersion() const override { return 1; }
ESaveGameDirectorContributorRequirement GetRequirement() const override
{
return ESaveGameDirectorContributorRequirement::Required;
}
ESaveGameDirectorContributorRestorePhase GetRestorePhase() const override
{
return ESaveGameDirectorContributorRestorePhase::BeforeActorRestore;
}
int32 GetRestorePriority() const override { return 0; }
bool CapturePayload(TArray<uint8>& OutPayload, FText& OutError) override
{
OutPayload.Reset();
FMemoryWriter Writer(OutPayload, true);
Writer << CheckpointIndex;
if (Writer.IsError())
{
OutError = FText::FromString(TEXT("Checkpoint serialization failed."));
return false;
}
return true;
}
bool ValidateRestorePayload(const TArray<uint8>& Payload, int32 SchemaVersion, FText& OutError) const override
{
if (SchemaVersion != GetSchemaVersion())
{
OutError = FText::FromString(TEXT("Unsupported checkpoint schema."));
return false;
}
FMemoryReader Reader(Payload, true);
int32 Candidate = 0;
Reader << Candidate;
if (Reader.IsError() || !Reader.AtEnd())
{
OutError = FText::FromString(TEXT("Checkpoint payload is malformed."));
return false;
}
return true;
}
bool RestorePayload(const TArray<uint8>& Payload, FText& OutError) override
{
FMemoryReader Reader(Payload, true);
int32 Candidate = 0;
Reader << Candidate;
if (Reader.IsError() || !Reader.AtEnd())
{
OutError = FText::FromString(TEXT("Checkpoint payload is malformed."));
return false;
}
CheckpointIndex = Candidate;
return true;
}
private:
int32 CheckpointIndex = 0;
};
class FCheckpointFactory final : public ISaveGameDirectorContributorFactory
{
public:
FName GetFactoryId() const override { return TEXT("ExampleGame.CheckpointFactory"); }
TUniquePtr<ISaveGameDirectorContributor> CreateContributor(UGameInstance& GameInstance) override
{
return MakeUnique<FCheckpointContributor>();
}
};
class FExampleGameModule final : public FDefaultGameModuleImpl
{
public:
void StartupModule() override
{
check(UE::SaveGameDirector::Contributors::RegisterFactory(Factory));
}
void ShutdownModule() override
{
UE::SaveGameDirector::Contributors::UnregisterFactory(Factory);
}
private:
FCheckpointFactory Factory;
};
IMPLEMENT_GAME_MODULE(FExampleGameModule, ExampleGame)

If the contributor needs the supplied Game Instance, store a weak reference in its instance and validate it before use. Core owns the contributor object; it does not own the factory. Use ESaveGameDirectorContributorPayloadExposure::Sensitive only when payload confidentiality requires the private container storage path; it is not encryption.