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:
- Read the schema version.
- Load or open the target level.
- Restore runtime Data Layer states before any actor state.
- Resolve stable authored ids.
- Restore status data on each
URuneableObjectComponent. - 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.
- Restore player and camera.
- 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¶
- Play
LV_2ndPlayableor the assigned room. - Change at least two runeable objects into different statuses.
- Change at least one runtime Data Layer activation state.
- Trigger the checkpoint save.
- End Play In Editor.
- Start again and load the checkpoint.
- Confirm gameplay status, presentation, collision, Data Layer state, and player placement agree.
- Repeat with a save from an older schema version when migration code changes.
- 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.
- Quit while standing inside a volume, relaunch and load. The player must resume where they stood.
- 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."
RequestCheckpointSavereturns whether the write was dispatched. The outcome arrives onOnCheckpointSaveCompleted. 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(), notGetAssetName(). Map names are not unique in this project:Developers/alone holds fourShowcaseand fourLV_test. Comparing the full path and then travelling by short name can open a different map than the one the comparison just rejected. HandlePostLoadMapverifies 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.
OpenLevelonly logs when it cannot resolve a name, so the package is checked withFPackageName::DoesPackageExistfirst. Without that,bRestoreWaitingForMapstays 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::BeginPlayasks 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.
bRecordIsAuthoritativebecomes true on an explicitLoadCheckpointor 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.UGameplayStaticsbuilds the save archive without settingArIsSaveGame, so every non-TransientUPROPERTYon aUSaveGamepersists whether or not it is marked. Keep marking fieldsSaveGameto record intent, but useTransientwhen 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.
Related pages¶
- Rune system core
- Puzzle rules and behaviors
- Data layers
- Gameplay recording
- Memory Palace
- Development plan
- Technical Design Document
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.