Save and load menus

Pulse Save brings a save menu and a load menu for players (modules PulseSaveMVVM and PulseSaveUI): the slots newest first with a preview picture, name, date, play time and level, a new save, overwriting and deleting with a question first. What saving and loading do is a handler the game can replace; the default saves the actors with the save interface in place.

Try it on the example map: PulseSaveMenu Save opens the save menu, PulseSaveMenu Load the load menu.

Slot meta

The menus show what a slot’s meta says (FPulseSaveSlotMeta): DisplayName, SavedAt (UTC), PlayTime (seconds), LevelName, LevelPath (the package of the level, so a load can open it first; empty in metas of earlier builds), Thumbnail (JPEG bytes) and Fields for your own values (chapter, difficulty). It is stored in the meta save game of the slot (slot_meta.sav) as the static record PulseSaveSlotMeta, so other static models of your game can live in the same file.

Entry stamp: Entry (FPulseSaveEntryStamp: entry category and identifier, size and CRC32 of the entry bytes, read only in Blueprint) says which save of which entry the meta was written with. A save of the entry together with its meta stamps it; a meta written alone keeps the stamp of the meta it replaces only while that entry still has the stamped bytes (a rename), else it is written without one. Loads find the meta of the data they loaded by it. Metas of earlier builds have no stamp and are read as before.

After a load the meta follows the entry: LoadFromSlot may have loaded the backup of a damaged entry and put it back onto the file, or dropped a damaged staged version (GetLastSaveCheck().Files). Pulse::Save::SyncSlotMetaAfterLoad(Slot, Check) (Blueprint Sync Slot Meta After Load) then

  • returns the meta written with exactly the loaded bytes (staged, committed or backup meta, by the stamp), else the meta of the old rule (the backup’s meta after a backup load, else the current one; after an entry-only save, such as an auto save, that is still the meta the menu wrote);
  • puts slot_meta.sav.bak back onto slot_meta.sav when the entry’s backup went back and that meta backup provably belongs to it (its stamp matches the loaded bytes and the entry file is still the restored backup; the meta backup stays, like the entry’s);
  • drops a staged meta whose save is gone (a damaged staged entry was dropped, or the load went back to an older save).

It changes nothing when the load changed no file, when the loaded file changed since the load (size or time), after a save of the manager (which clears the record), for another slot than the loaded one (refused with a warning, bFilesInStep false), and never touches a meta file your game writes itself. The pure rule is Pulse::Save::PlanMetaAfterLoad(Facts) (FPulseSaveMetaLoadFacts -> FPulseSaveMetaLoadPlan: Source (EPulseSaveMetaSource), bRestoreBackup, bDiscardStaged). Call it once per load: it reads the loaded entry once more (plus a CRC32), the committed entry only after a restore, and up to three meta files.

Function (UPulseSaveSlotMetaLibrary) What it does
WriteSlotMeta(SlotName, Meta, RootDirectory) Writes the meta alone (a rename); other records of the meta save game stay. It replaces the committed meta without turning slot_meta.sav.bak over (only a save of the entry together with its meta does that, so both backups stay of the same save). Next to a staged entry (a save with flush mode None) it only stages the meta, so it is committed or discarded with that entry. The entry stamp of the replaced meta stays while its entry is unchanged. A meta file that is no Pulse save game (your game writes its own object there) is never overwritten: it returns false and logs a warning.
ReadSlotMeta(SlotName, Meta, RootDirectory) Reads it (a staged meta first, like every reader); false without one.
SyncSlotMetaAfterLoad(Manager, SlotName, Meta, RootDirectory) Sync Slot Meta After Load: after Load From Slot of that manager (on both pins), puts the meta files in step with what the load did to the entry and returns the meta of the loaded save game (see below); false without one or without a manager.
MakeSlotMeta(WorldContext, DisplayName) A meta for now: date, level of the world, play time.
ListSlotsWithMeta(RootDirectory, Prefix) The slots with their meta, newest first (the saved date of the meta, else the file date). A meta that is only staged is listed too.

A meta can be staged (.staging/slot_meta.sav): after a save with flush mode None, and when its commit failed (the next save commits it). Readers prefer it, as for every entry. Direct writes of the committed meta (FPulseSaveSlot::SaveMetaGameDirect, SaveMetaBytesDirect, Save Slot Save Game, Save Slot Meta Game) delete a staged meta afterwards, so readers see what they wrote.

C++ (Pulse::Save, PulseSaveSlotMeta.h): MakeMetaSaveGame(Base, Meta, OutGame) (the records of the base plus the meta record; the base is not changed), GetMetaFromSaveGame(Game, OutMeta) and ReadMetaGameForWrite(Slot, OutGame) (EPulseSaveMetaFile: Missing, Pulse, Foreign). Stamps (PulseSaveEntryStamp.h): MakeEntryStamp(EntryId, Bytes), HasEntryStamp, GetStampEntryId and CompareEntryStamps(Meta, Loaded) (EPulseSaveStampMatch: Unknown for no stamp or another entry, Match, Mismatch). | MakeNewSlotName(Prefix, RootDirectory) | The next free name: Save_0001, Save_0002, … | | FormatPlayTime(PlayTime) | 1:05:09 or 5:09. | | MakeThumbnailTexture(Thumbnail) | A transient texture of the picture for widgets. |

Preview picture: the async node Capture Slot Thumbnail (UPulseSaveCaptureThumbnail) takes the next frame of the game viewport without the UI, scales it to MaxWidth and compresses it as JPEG (about 7 KB at 320 pixels). Without a viewport (dedicated server) and after two seconds without a frame it ends with no picture. C++: Pulse::Save::CaptureThumbnail(World, MaxWidth, Callback).

Play time: the game instance subsystem UPulseSavePlayTimeSubsystem counts from the start of the game instance. SetPlayTime(Seconds) sets it, e.g. to the play time of a loaded slot (the default handler does that) or to 0 when a new game starts.

The menu

UPulseSaveUISubsystem (local player) opens it: Open Save Menu, Open Load Menu, Open Menu (mode), Close Menus. The default widget is /PulseSave/UI/W_PulseSaveMenu (look from the UI theme of Pulse Foundation); it is a UPulseSaveMenu with these parts, all optional, so own layouts keep only what they need:

Part Type What it shows
TitleText Text “Save Game” or “Load Game”
SlotList Dynamic Entry Box One UPulseSaveSlotEntry per slot; saving adds “New Save” first (until MaxSlots)
EmptyText Text Shown in the load menu without slots
NameInput Editable Text Box Name of the save (empty: the slot name)
ActionButton, DeleteButton, BackButton UPulseSaveUIButton Save, Overwrite or Load; Delete; Back
StatusText Text “Saving…”, “Saved.”, the reason why saving is not possible
ConfirmPanel, ConfirmText, ConfirmYesButton, ConfirmNoButton   The question before overwriting and deleting

The slot entry (W_PulseSaveSlotEntry) has ThumbnailImage, NoThumbnail, NameText, DateText, PlayTimeText, LevelText, NewSlotMark and SelectedFrame.

The actions are Blueprint functions too, for gamepad shortcuts or your own buttons: Save Or Load Selected, Delete Selected, Answer Confirm, Is Confirm Shown, Is Busy, Refresh, Set Mode. Escape and the right face button close the question, then the menu. bCloseAfterSave and bCloseAfterLoad (on by default) close the menu after a successful save or load; On Saved and On Loaded tell your UI.

View models (PulseSaveMVVM): UMVVMPulseSaveSlotList (Pulse Save Slots: Slots, SelectedSlot, bIsEmpty, Refresh, SelectSlot, SelectSlotByName; the selection stays on its slot after a refresh) and UMVVMPulseSaveSlot (SlotName, DisplayName, DateText, PlayTimeText, LevelText, Thumbnail, bIsNewSlot) for UIs of your own.

Where it shows: a presenter (UPulseUIPresenter of Pulse Foundation) adds the menu to the viewport. A game with its own UI layers names a presenter class in the settings that pushes it onto its stack.

The handler

The menus only call the handler of the local player (UPulseSaveUIHandler, Project Settings > Pulse > Save > UI > Handler Class). Derive in Blueprint or C++ (Add > Pulse > Save > Save UI Handler starts from the default handler):

Event What it does
Save To Slot (SlotName, DisplayName, Done) Saves and writes the meta (Save To Slot With Meta (Async)); call Done with the result
Load From Slot (SlotName, Done) Loads, e.g. travels to the level of the save first
Delete Slot (SlotName) Deletes the slot folder
Can Save / Can Load (Reason) Whether the player may save now (not in combat); the menu shows the reason

The default handler (UPulseSaveUIDefaultHandler) uses a save manager of its own: ManagerClass with the CollectorClasses of the settings (default: the actor collector, every actor with the save interface, and the byte collector, every actor and component that saves itself as bytes, such as the doors of Pulse Core), saves into the entry EntryId of the slot (SaveToSlotWithMeta, a new save game on the first save of a session) and loads it in place (LoadFromSlot). The meta gets the preview picture, the level and the play time and is written with the entry: both are staged together and committed together, so the slot never shows new data with an old meta. When the meta cannot be made or staged, nothing new is visible and the menu says “Saving failed.”; when only its commit fails, the meta stays staged, the menus show it and the next save commits it. After every load (also a failed one) it calls SyncSlotMetaAfterLoad, so a restored entry backup brings its meta back, and sets the play time from the meta of the loaded save.

Blueprint handlers save with Save To Slot With Meta (Async) (meta from Make Slot Meta plus the picture of Capture Slot Thumbnail) instead of Save To Slot (Async) followed by Write Slot Meta: two separate writes can leave new data with the old meta, and their backups belong to different saves. After Load From Slot (Async) they call Sync Slot Meta After Load on both pins and take the play time from its meta. Clients of a multiplayer game cannot save or load with it (only the host has the world).

Settings

Project Settings > Pulse > Save > UI (UPulseSaveUISettings, DefaultGame.ini):

Setting Default Description
HandlerClass none What saving, loading and deleting do; empty: the default handler
ManagerClass none Save manager of the default handler; empty: UPulseSaveManager
CollectorClasses actor collector, byte collector Collectors the default handler adds to its manager
EntryId Game Entry of the slot the default handler saves into
SlotPrefix Save Start of the names of new slots and the slots the menus list (empty: all)
RootDirectory empty Folder of the slots; empty: Saved/SaveGames
MaxSlots 0 Most slots a player can have; the save menu offers no new save once reached (0: no limit)
ThumbnailWidth 320 Width of the preview picture (0: none)
MenuClass W_PulseSaveMenu The save and load menu
PresenterClass none Shows the menu; empty: added to the viewport

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