Skip to content

Save / Load

Rephrased uses checkpoint saves built on Unreal USaveGame. The root save object carries an explicit schema version, and the save captures durable progression plus enough puzzle state to reconstruct the current area.

What a checkpoint stores

A checkpoint save stores:

  • level and checkpoint identity;
  • player transform when that checkpoint needs it;
  • learned runes and learned drawing patterns;
  • runeable object status state keyed by a stable authored id;
  • for a puzzle, the settled pose of each component a Move or Rotate behavior moved, and any mesh a Swap Mesh behavior swapped in;
  • runtime Data Layer activation state;
  • Mind Palace state: rooms unlocked and pedestal assignments;
  • rune use recordings that feed the pedestal plaques.

Each persisted runeable object uses a stable authored id rather than a runtime object name. The runtime Data Layer state is saved because a state swap implemented as a runtime Data Layer destroys its actors when unloaded; the save must re-apply that state after the layer loads.

Restore order

Restore follows this order:

  1. Read the schema version.
  2. Load or open the target level.
  3. Restore runtime Data Layer states before any actor state.
  4. Resolve stable authored ids.
  5. Restore status data on each URuneableObjectComponent.
  6. Ask each receiver to rebuild presentation from that state: a puzzle takes back the saved component transforms its performances left behind, applies the state's State Looks, then re-applies the saved swapped meshes.
  7. Restore player and camera.
  8. Resume input.

Gameplay state remains authoritative. Presentation derives from it.

Data Layer restore requirement

A state swap implemented as a runtime Data Layer destroys its actors when the layer is unloaded. The checkpoint must therefore save the Data Layer activation state and re-apply it after the layer loads, before resolving stable ids or restoring status data. See Data layers.

Local verification

  1. Play LV_2ndPlayable or the assigned room.
  2. Change at least two runeable objects into different statuses.
  3. Change at least one runtime Data Layer activation state.
  4. Trigger the checkpoint save.
  5. End Play In Editor.
  6. Start again and load the checkpoint.
  7. Confirm gameplay status, presentation, collision, Data Layer state, and player placement agree.
  8. Repeat with a save from an older schema version when migration code changes.
  9. Cross a volume while an orb is in flight or a puzzle is mid-slide, keep walking, then load. The player must return to the crossing, not to where the retry caught up with them.
  10. Quit while standing inside a volume, relaunch and load. The player must resume where they stood.
  11. Quit outside any volume. The last crossed checkpoint must be what loads.

Editor save files live under Saved/SaveGames/ on Windows.

Engineering rules

  • Mark persisted fields with UPROPERTY(SaveGame).
  • Version the schema from its first implementation.
  • Give added fields deterministic defaults.
  • Keep migration code covered by focused automation.
  • Save stable gameplay state and rebuild transient actors, widgets, effects, and camera blends.
  • Keep each checkpoint write atomic from the player's perspective.

Implementation

Implemented under SAVE-001 in Source/Rephrased/Save/:

Class Role
URephrasedSaveGame Root save. Carries SchemaVersion, checkpoint and level identity, the player transform, runtime Data Layer records, receiver records keyed by authored id, and the knowledge dictionary.
URephrasedSaveSubsystem Game-instance subsystem owning the one resident save. Captures and restores, and exposes the knowledge accessors.
URuneSaveIdComponent Authored SaveId on a placed actor, seeded on creation and validated by IsDataValid.
ACheckpointVolume Invisible trigger that writes a checkpoint when the player crosses it.
USaveIndicatorWidget Autosave indicator base. The subsystem creates and drives it; the Blueprint subclass owns every visual.

Writes are asynchronous (SAVE-001.1)

Checkpoints go through UGameplayStatics::AsyncSaveGameToSlot. That serializes on the game thread and hands only the file write to a background task, so the recorded bytes are a snapshot of the moment the checkpoint was requested and later gameplay cannot tear them — the wiki's "atomic from the player's perspective" rule holds without a blocking write.

Two consequences when extending this:

  • The return value does not mean "it is on disk." RequestCheckpointSave returns whether the write was dispatched. The outcome arrives on OnCheckpointSaveCompleted. A serialization failure runs that delegate inline, before the request call returns, rather than on a later frame.
  • Writes in flight are counted, not flagged. Two volumes firing a frame apart must raise one started and one completed, or the indicator flickers off while a write is still running.

The teardown save is the one deliberate exception and writes synchronously: a background task and its game-thread callback do not outlive the world.

Where the player transform comes from

ACheckpointVolume latches the pawn transform at the overlap and passes it to RequestCheckpointSaveAtTransform. This matters because TrySave re-arms a 0.25s retry while the world is unsettled (an orb in flight, a puzzle sliding or playing its repair cinematic), and the player keeps walking across those retries. Capturing at write time would record wherever they ended up, which could be the next room.

The exception is quitting. FWorldDelegates::OnWorldBeginTearDown fires while actors are still alive; if the pawn is then standing inside a checkpoint volume, the checkpoint is rewritten with its current transform so a player who quits deep inside a large volume resumes where they stood. Outside a volume nothing is written — the player gets the last checkpoint they crossed.

Containment is asked with Bounds->IsOverlappingActor rather than tracked through begin and end overlap. A pawn raises those once per primitive (capsule, mesh, interaction trigger), so one component leaving would clear a tracked flag while the pawn is still inside. This is the same multi-primitive behaviour the volume's LastSaveFrame guard exists for.

The teardown hook returns early while bRestoreWaitingForMap is set, because LoadCheckpoint's own OpenLevel tears the outgoing world down and would otherwise write a half-restored world over the record being restored from.

Level identity is a package path, never a map name

LevelPath holds a full object path and every step must use it as one.

  • Travel by GetLongPackageName(), not GetAssetName(). Map names are not unique in this project: Developers/ alone holds four Showcase and four LV_test. Comparing the full path and then travelling by short name can open a different map than the one the comparison just rejected.
  • HandlePostLoadMap verifies which map actually arrived. Otherwise any map finishing loading satisfies a pending restore. That combination fails silently in the worst way: Data Layers warn and skip, receiver ids simply miss, and the pawn is still teleported to coordinates that mean nothing in the level that loaded.
  • A travel that cannot happen is abandoned, not left pending. OpenLevel only logs when it cannot resolve a name, so the package is checked with FPackageName::DoesPackageExist first. Without that, bRestoreWaitingForMap stays set for the rest of the session and the next unrelated map load trips a restore that should have been dropped — and it also permanently suppresses the teardown save, which early-returns on that flag.

Both abandon paths clear bRecordIsAuthoritative as well. A record for a level that was never entered must not be handed to receivers in whatever level the player is actually standing in, whose authored ids can collide with it.

Hooking up the indicator

ARephraseCharacter::SaveIndicatorWidgetClass holds the Blueprint class, next to HUDWidgetClass, and registers it with the subsystem at BeginPlay. The class lives on the character for the reason FMindPalaceOpenParams gives for OverlayWidgetClass — designers tune it per pawn, and a subsystem has no editor surface of its own. The registration is guarded, so a pawn that leaves it empty (the Mind Palace pawn) cannot clear what the main character registered.

The Blueprint subclass implements OnSaveStarted and OnSaveCompleted. Both fire even when the write is too fast to see, so any minimum on-screen time belongs in the Blueprint; holding it in C++ would mean the subsystem lying about when the write finished.

A receiver's noun is not saved. Grammar is two words for good, so nothing at runtime mutates URuneableObjectComponent::NounRune — it is authored design data that belongs in the map, by the same rule Data layers applies to initial runtime states. A restored status is therefore filtered against the authored noun's states, which means a designer can change an object's noun or its rules and existing saves keep working: tags no state uses are dropped rather than forced back, and a partial status gains the noun's Default Status tags it is missing.

Three behaviours worth knowing before extending this:

  • Receivers pull, they are not pushed to. Streaming destroys and respawns actors, so URuneableObjectComponent::BeginPlay asks the subsystem for its own record. Receivers already alive when a restore lands are pushed to instead, which covers the reload-in-place case.
  • The record is only handed out once it is authoritative for this session. The slot is read into memory at startup, but that data describes a previous session; applying it without being asked would leave a half-restored world, with receivers holding saved status while Data Layer state and the player transform stayed authored. bRecordIsAuthoritative becomes true on an explicit LoadCheckpoint or on the first checkpoint write, after which the record is this session's own state. The write case is not optional: an actor destroyed by a Data Layer unload has nowhere else to recover from when it streams back in, and gating that on an explicit load silently reverts it to authored state — and the next checkpoint then captures the reverted state over the good record. The first write of a session that did not load resets the receiver records first, so a previous session cannot leak in through the merge.
  • UPROPERTY(SaveGame) does not filter anything. UGameplayStatics builds the save archive without setting ArIsSaveGame, so every non-Transient UPROPERTY on a USaveGame persists whether or not it is marked. Keep marking fields SaveGame to record intent, but use Transient when a field must not persist.

Dev builds clear saves at BeginPlay so everyday work starts clean. Rephrased.Save.WipeOnPlay 0 keeps them. F6 and F7 are bound to Rephrased.Save.Checkpoint and Rephrased.Save.Load through DebugExecBindings, which is stripped from shipping builds. Pick debug keys against the engine's own BaseInput.ini rather than by eye: it already claims F1-F5, F9 and F11 (F5 is shader complexity, F9 is screenshot) and F8 ejects from PIE. Rephrased.Save.Wipe stays console-only, so no stray keypress can delete every save.

Open questions

  • Checkpoint granularity: the exact pacing and number of checkpoints per area is not locked. The volume is deliberately opinion-free so design can place them per room, per beat, or both.
  • Narrative state: how much of the "previous Sol remains" narrative state the save must carry is not yet defined.
  • Recent-use clip ids and palace room unlock state are not in the schema yet; they belong to the ticket that builds the pedestals. Property serialization is tagged, so adding them later loads an existing save with the new field at its default.

Sources

  • Mind Palace Doc (Sept 10): Mind Palace rooms, pedestal assignments, and the rune use recordings that feed the pedestal plaques.
  • Rephrased GDD: high-level save and progression requirements.
  • Data layers: runtime Data Layer actor lifecycle and state re-application.

Change history

  • 2026-10-02 — Gabriel Li — Listed what a puzzle's record saves (settled component poses and swapped meshes) and matched the restore step to the order the puzzle rebuilds in (CORE-011).
  • 2026-09-29 — Gabriel Li — Restore rebuilds puzzles from State Looks plus saved component transforms and meshes (CORE-009).
  • 2026-09-19 — Ricky Lu — SAVE-001: documented when the resident record becomes authoritative, after a PIE bug where gating it on an explicit load reverted actors that streamed back in and then captured the reverted state over the good record.
  • 2026-09-17 — Ricky Lu — SAVE-001: dropped the receiver noun from the checkpoint contract. Grammar is two words for good, so no gameplay mutates a receiver's noun and it is authored map data, not save data. Status is now filtered against the authored noun on restore.
  • 2026-09-17 — Ricky Lu — SAVE-001: recorded the implemented schema and classes, the pull-not-push restore rule, the UPROPERTY(SaveGame) serialization caveat, and the dev wipe and debug keys. Closed the automatic-versus-manual open question: checkpoints are automatic and invisible.
  • 2026-09-17 — Gabriel Li — Expanded the checkpoint save contract with runtime Data Layer state, Mind Palace state, rune use recordings, restore order, and open questions.
  • 2026-09-17 — Gabriel Li — Defined the checkpoint save contract around stable receiver IDs, runeable-object status, presentation rebuild, and schema versioning.
  • 2026-05-23 — Gabriel Li — Added PIE save location and wipe procedure for local testing.
  • 2026-05-08 — Gabriel Li — Added baseline change history section for weekly wiki maintenance.