Serialization

Pulse plugins do not depend on PulseSave. They offer SaveToBytes / LoadFromBytes instead, and the save bridges put the bytes into save models. Pulse::Serialization writes those bytes the same way in every plugin.

Struct to bytes

FMySaveData data;
data.Version = FMySaveData::CurrentVersion;
...
TArray<uint8> bytes;
Pulse::Serialization::StructToBytes(data, bytes);

FMySaveData loaded;
switch (Pulse::Serialization::ReadVersioned(bytes, FMySaveData::CurrentVersion, loaded))
{
case EPulseReadResult::Success: ...
case EPulseReadResult::TooNew:  // written by a newer build: do not overwrite it
case EPulseReadResult::Empty:
case EPulseReadResult::Corrupt: ...
}
  • Properties are written as tagged properties: added and removed properties are fine; renamed properties need a PropertyRedirects entry.
  • Object references and names (gameplay tags too) are written as strings, so the data survives asset moves with redirects.
  • Transient properties are skipped.
  • Versioning: give the struct an int32 Version property. ReadVersioned rejects data with a higher version, so an older build never misreads newer data. Migrate older versions after reading.
  • The format is the one PulseSkill, PulseAchievement and PulseCore wrote before, so their existing save data reads unchanged (those helpers also wrote transient properties; reading such data is fine).

InstancedStructToBytes / BytesToInstancedStruct also store the struct type, for payloads whose type is only known at runtime.

Import reports

Applying loaded data can partly fail: a skill that no longer exists, an item asset that was deleted, a value outside its range. FPulseImportReport collects what happened, so the game, the save bridges and the Pulse Debugger can show it.

Field / function Meaning
Messages FPulseImportMessage: Severity (Info, Warning, Error), Text, Context (tag, asset path, entry id), Code (machine readable reason)
NumImported, NumSkipped Counters (CountImported, CountSkipped, AddSkipped)
DataVersion, MigratedToVersion Set by NoteMigration(From, To)
AddInfo / AddWarning / AddError Add a message
Append(Other, Prefix) Merges the report of a part (e.g. "Inventory/")
HasErrors, HasWarnings, IsClean, GetMaxSeverity, Count, CountCode Evaluation
GetSummary() “12 imported, 1 skipped, 0 error(s), 1 warning(s), 2 info(s) (migrated 1 -> 2)”
LogTo(LogCategory, Title) Logs the summary and each message with the verbosity of its severity

Severity guide: Info = adjusted on purpose (migrated, default filled in); Warning = data lost or changed (entry skipped, value clamped); Error = the import failed or is incomplete.

Suggested codes: UnknownTag, MissingAsset, Clamped, Migrated, Duplicate, NotAllowed, Corrupt.

FPulseReportBuilder::AddImportReport turns an import report into a section of a debug report (see Debugging).

Saveable as bytes

Components that save themselves implement IPulseByteSaveable (Serialization/PulseByteSaveable.h, C++ only). A save system needs nothing else: PulseSave keeps every object with the interface in one general model, so no save bridge per plugin is needed, and components of your game work the same way.

Function What it does
SaveToBytes(Bytes) Writes the state (usually Pulse::Serialization::StructToBytes of a struct with a Version)
LoadFromBytes(Bytes, Report) Reads a state of this or an older version and reports what was read or skipped
GetSaveId() The id of what is saved, the same in every session; default MakeSaveKey (below)
GetSaveVersion() The version SaveToBytes writes now (default 1), for reports and checks
ShouldSaveBytes() False leaves the object out of this save
GetLoadPriority() Higher loads first (default 0)
GetSaveTiming() Synchronous (default) or Asynchronous
BeginSaveToBytes(Done), BeginLoadFromBytes(Bytes, Done) The asynchronous way; by default they call the two functions above at once
class UMyStashComponent : public UActorComponent, public IPulseByteSaveable
{
	virtual bool SaveToBytes(TArray<uint8>& OutBytes) const override
	{
		return Pulse::Serialization::StructToBytes(Data, OutBytes);
	}

	virtual bool LoadFromBytes(const TArray<uint8>& InBytes, FPulseImportReport& OutReport) override
	{
		return Pulse::Serialization::BytesToStruct(InBytes, Data) == EPulseReadResult::Success;
	}
};

Save identity

The id should not depend on object names: the names of components added at runtime, of subsystems and of spawned actors differ between sessions and builds, and renaming a component in a Blueprint would lose its saves. So an object that saves itself says what it saves with an FPulseSaveIdentity and returns it from GetSaveIdentity(); the default GetSaveId() builds the key from it.

Field Meaning
Tag What is saved: Pulse.<Plugin>.Save.<Thing>, or a tag of your game
Key Which one, when an owner has several with the same tag (a second stash), or a fixed key of a placed object (“North”)
Scope Owner (default): the player id or the actor comes first. World: once per world or game (a component of the game state, a subsystem)

Keys made from an identity:

Object Key
Component of a player actor Player.<player id>.<Tag> (.<Key> appended when set)
Component of an actor in the persistent level or a World Partition cell <Actor>.<Tag>
Component of an actor in a sublevel or level instance <level package>.<Actor>.<Tag> (the long package name without PIE prefix, unique per instance)
Scope World, or an object without actor World.<Tag>.<Key>

Scope World is ignored on player actors (every player would write the same record): the key stays per player and a warning is logged once per class. With scope Owner on a placed actor the key contains the actor’s name, so renaming that actor loses its saves; objects that must survive a rename use scope World with a Key. A key never starts with Player unless it belongs to a player, also for a game tag such as Player.Stats (World.Player.Stats).

UMyStashComponent::UMyStashComponent()
{
	SaveIdentity.Tag = TAG_MyGame_Save_Stash;
}

FPulseSaveIdentity UMyStashComponent::GetSaveIdentity() const
{
	return SaveIdentity;   // UPROPERTY(EditAnywhere) FPulseSaveIdentity SaveIdentity;
}

Without an identity the key is made from names as before: Pulse::Serialization::MakeSaveKey(Object) gives Player.<player id>.<component> for player actors (player id from player resolution), <actor>.<component> for components of other actors and the name of other objects. DescribeSaveId(Object) tells how far a default id can be trusted (Stable, NameBased, SpawnOrder for the name of an actor spawned while playing, None); PulseSave’s Pulse Check warns about name-based ids. A default id made from the name of a spawned actor is also logged once per class (Log level).

Automatic components are named Pulse_<Class>_0 in every build (see Workflow), so their name-based keys are stable too; they should still get an identity, because renaming the class would change the name.


PulseFoundation plugin documentation.

This site uses Just the Docs, a documentation theme for Jekyll.