HasSaveSlotsDataStore
The save slot system over a DataStore, with no Player attached.
This is the whole slot model -- the roster, the per-slot stores, selection, create/delete/reset, export/import, ephemeral slots and playtime -- expressed against an injected datastore rather than against a bound player. HasSaveSlots is the binder that owns one of these for a live player and supplies the player-shaped parts (replication, remotes, teleport data, summary providers).
Splitting it out is what makes a slot reachable for a player who is not in this server: admin
tooling borrows that player's datastore (see PlayerDataStoreHandle) and builds one of these over
it, getting the same slot semantics with no Player instance in sight.
The slot roster is still modelled as Folders with attributes, because that representation is what the client reads. Offline, those folders simply live in an unparented container and replicate to nobody.
INFO
Constructed with a promise of a datastore rather than a datastore, so every method can gate on the same load promise the player path always gated on. Callers do not have to wait before using it.
Types
HasSaveSlotsDataStoreOptions
interface HasSaveSlotsDataStoreOptions {SharedDataStoreService: any--
SaveSlotSharedDataStoreService
UserId: number?--
identity for generated share codes
UserName: string?--
identity for generated share codes
TrackPlaytime: boolean?--
accrue play sessions from selection changes, defaults to true
}The state and collaborators this class cannot derive from a datastore alone.
The four value objects are borrowed, never owned: the player path passes the ones backed by replicated player attributes, so writing them replicates, while offline tooling passes detached ones. Either way this class only reads and writes them, and never destroys them.
Functions
new
Builds the slot system over a datastore that may not have loaded yet.
GetSlotFolder
Returns the folder holding the slot's replicated metadata, or nil when there is no such slot. The folder is owned by this object; do not parent or destroy it.
GetActiveSlotId
Returns the active slot's id, or nil when nothing is selected.
GetLastActiveSlotId
Returns the slot the player is on, or would resume on: the active slot when one is selected, and the persisted "Continue" target otherwise. The synchronous twin of HasSaveSlotsDataStore.PromiseLastActiveSlotId.
This is the one to read when acting on "their current slot" without a live session. Freshly opened against a datastore, nothing is selected yet -- the stored pointer loads as the last-active slot, and HasSaveSlotsDataStore.GetActiveSlotId stays nil until something selects.
IsActiveSlotEphemeral
Returns whether the active slot is an ephemeral (session-only) one. False when nothing is active.
GetSlotMetadata
HasSaveSlotsDataStore.GetSlotMetadata(slotId: SlotId?) → SaveSlotMetadata?Returns the slot's metadata, or nil when there is no such slot.
GetActiveSlotMetadata
Returns the active slot's metadata, or nil when nothing is selected. Unlike HasSaveSlotsDataStore.GetSlotList this does see an ephemeral slot, which is the only way to read the metadata of a session in progress.
GetSlotList
Returns the persisted slots, ordered by index. Ephemeral slots are excluded, matching SaveSlotDataService.GetSlotList -- read the active slot directly with HasSaveSlotsDataStore.GetActiveSlotMetadata to see one.
Ordered because the underlying map is not: an unordered listing would shuffle between calls, and the tooling reading this prints it.
GetSlotIdFromIndex
Returns the id of the persisted slot at the given index, or nil when there is none. Ephemeral slots are never addressable by index.
GetSlotStore
Returns the DataStoreStage backing the slot's saved data. For the default slot this is the player's root store; for an ephemeral slot it is an in-memory store.
ObserveActiveSlotStoreBrio
HasSaveSlotsDataStore.ObserveActiveSlotStoreBrio(self: HasSaveSlotsDataStore) → Observable.Observable<Brio.Brio<DataStoreStage.DataStoreStage>>Observes the DataStoreStage for the active slot as a Brio
PromiseActiveSlotStore
HasSaveSlotsDataStore.PromiseActiveSlotStore(self: HasSaveSlotsDataStore) → Promise.Promise<DataStoreStage.DataStoreStage?>Returns the DataStoreStage for the active slot
PromiseSlotsLoaded
Promises that all slots have loaded
PromiseHasSlot
HasSaveSlotsDataStore.PromiseHasSlot(slotId: SaveSlotData.SlotId?) → Promise.Promise<boolean>Returns whether the slot with the given ID exists
PromiseSelectSlot
HasSaveSlotsDataStore.PromiseSelectSlot(slotId: SaveSlotData.SlotId) → Promise.Promise<any>Selects the slot with the given ID
PromiseDeselectSlot
Clears the active slot selection, returning the player to a no-slot state -- the counterpart to HasSaveSlotsDataStore.PromiseSelectSlot, backing a "back to menu" affordance. The active slot's progress is flushed first (mirroring the save PromiseSelectSlot runs when switching away), and the last-active slot is remembered, so HasSaveSlotsDataStore.PromiseSelectLastSaveSlot can resume it later. A no-op when no slot is active.
PromiseCreateSlot
HasSaveSlotsDataStore.PromiseCreateSlot() → Promise.Promise<SaveSlotData.SlotId>Creates a slot at the given index
PromiseExportSlot
Exports a slot's saved data into a plain, serializable SaveSlotExportUtils.SaveSlotExport. Rejects the main/default slot by default: its store is the player's shared root datastore, so exporting it would leak the SaveSlots system data and universe-scoped global data living alongside it. Only isolated non-main slot substores are exportable.
allowMainSlot opts out of that refusal for trusted admin tooling (see SaveSlotCmdrService).
The SaveSlots system data is always stripped from the result, so the export never carries the
slot roster or the other slots' saved data. Universe-scoped global data sharing the root store
is indistinguishable from the main slot's own data and is still carried, which is why this is
not exposed through SaveSlotService.
PromiseImportSlot
HasSaveSlotsDataStore.PromiseImportSlot(export: SaveSlotExportUtils.SaveSlotExport,targetSlotIndex: number?--
defaults to the lowest free non-main index
) → Promise<SlotId>
Imports an exported slot into a fresh slot, seeding the new slot's store with the exported data.
The destination defaults to the lowest free non-main index; passing targetSlotIndex puts the
import at that index instead, which must be free (delete or reset what is there first). Never uses
the main/default index either way -- importing onto the shared root store would wipe the player's
global data. Resolves to the new slot's id. Rejects a malformed export, a taken or main-slot
destination, or when no non-main index is free.
PromiseSaveSlotToSharedDataStore
HasSaveSlotsDataStore.PromiseSaveSlotToSharedDataStore(slotId: SlotId,key: string,) → Promise<boolean>Exports a non-main slot (see HasSaveSlotsDataStore.PromiseExportSlot) and writes it to the shared save slot store under the given key, tagged as a share code (see SaveSlotExportUtils.Kind) so the teleport arrival path will not load it.
PromiseImportSlotFromSharedDataStore
Reads a share code's export from the shared save slot store and imports it into a fresh non-main slot (see HasSaveSlotsDataStore.PromiseImportSlot). Rejects when no export is stored under the key, or when the entry there is a transfer snapshot rather than a share code -- a transfer key is replicated to every client (see the arrival path in HasSaveSlotsDataStore.PromiseSelectTransferableEphemeralSlot), so redeeming one into a kept slot would turn a public handle into a permanent copy of someone else's save.
PromiseSelectTransferableEphemeralSlot
Resumes a transfer: loads the snapshot stored under the given shared-store key into a fresh ephemeral slot and selects it, keeping the key so the next teleport carries the slot forward again (see the transferable-ephemeral teleport provider in SaveSlotService). Like every ephemeral slot it is never persisted, stays out of the slot list, and is torn down on deselect.
WARNING
The key comes from the arriving client's teleport band, so this loads whatever a client asks for. Only entries written as transfers are accepted -- a share code, which players hand to each other, is refused here and redeemed through HasSaveSlotsDataStore.PromiseImportEphemeralSaveSlotFromCode instead. Entries predating that tag are refused too; see SaveSlotExportUtils.canLoadAs.
PromiseImportEphemeralSaveSlotFromCode
Redeems a share code into a fresh transferable ephemeral slot and selects it (see HasSaveSlotsDataStore.PromiseSelectTransferableEphemeralSlot for what that slot is).
The slot transfers onward under a freshly minted key rather than the code it came from: the transfer key is handed to the client to carry across teleports, so reusing the code as that key would publish the code and hand every holder of it a slot the arrival path would load. The code entry is left untouched, so the same code still redeems for whoever else holds it.
PromiseBuildEphemeralTransferSlice
HasSaveSlotsDataStore.PromiseBuildEphemeralTransferSlice(self: HasSaveSlotsDataStore) → Promise<{[string]: any}?>Builds this player's teleport slice for a transferable ephemeral slot: re-saves the active slot's current live data to the shared store under its key and returns a slice carrying that key. Resolves nil when the active slot is not a transferable ephemeral slot. A failed re-save degrades to nil so a teleport is never blocked (the destination then re-loads the last saved state). Asynchronous -- it is consumed through TeleportDataService.PromiseBuildTeleportData.
SetCodeGenerator
Overrides the share-code generator for this player's exports (see SaveSlotCodeUtils.CodeGenerator). Games inject a custom format; the default is SaveSlotCodeUtils.generateDefaultCode. Usually set game-wide via SaveSlotService.SetCodeGenerator rather than per player.
PromiseExportSaveSlotToCode
HasSaveSlotsDataStore.PromiseExportSaveSlotToCode(slotId: SlotId?,--
defaults to the active slot
) → Promise<string>
Exports a slot to the shared store under a fresh generated code and resolves to that code. The code
is a shareable handle other sessions load with
HasSaveSlotsDataStore.PromiseImportEphemeralSaveSlotFromCode. Defaults to the active slot. Refuses
the main slot unless allowMainSlot is set (see HasSaveSlotsDataStore.PromiseExportSlot). The code
format comes from the configured generator (see HasSaveSlotsDataStore.SetCodeGenerator).
PromiseExportSaveSlotToJson
HasSaveSlotsDataStore.PromiseExportSaveSlotToJson(slotId: SlotId?,--
defaults to the active slot
) → Promise<string>
Exports a slot as a raw JSON string (no shared store), for direct inspection or attaching to a bug
report. Defaults to the active slot. Refuses the main slot unless allowMainSlot is set (see
HasSaveSlotsDataStore.PromiseExportSlot).
PromiseDuplicateSlot
HasSaveSlotsDataStore.PromiseDuplicateSlot(slotId: SaveSlotData.SlotId) → Promise.Promise<SaveSlotData.SlotId>Duplicates the slot with the given ID into a new slot at the lowest free index, copying its saved data and accrued playtime. Resolves to the new slot's id. The copy is not selected, its timestamps and session counters start fresh, and its name is suffixed with " (Copy)". Rejects when the source slot is missing or every index is in use.
An ephemeral slot may be duplicated: the copy is a real, persisted slot seeded with the ephemeral slot's live in-memory data, which is how a throwaway session is turned into a save. It keeps the source's name unsuffixed (the copy is the first real slot for that session, not a second copy of an existing save) and, like any duplicate, is not selected -- see HasSaveSlotsDataStore.PromisePersistEphemeralSlot for the version that continues play on the new slot.
PromisePersistEphemeralSlot
HasSaveSlotsDataStore.PromisePersistEphemeralSlot(slotId: SlotId?--
defaults to the active slot
) → Promise<SlotId>Turns an ephemeral slot into a real save: duplicates it into a fresh persisted slot at the lowest free index (see HasSaveSlotsDataStore.PromiseDuplicateSlot) and selects that slot, so play continues on data that is now being written. Defaults to the active slot, which in practice is the only ephemeral slot there is -- one stops existing the moment it stops being active. Resolves to the new slot's id.
The copy is taken from the ephemeral slot's live in-memory data at the moment this runs, and selecting the new slot retires the ephemeral one, so anything written to the old store between the copy and the selection is dropped. Rejects when no slot is active, the slot is missing, the slot is not ephemeral, or every index is in use.
PromiseDeleteSlot
HasSaveSlotsDataStore.PromiseDeleteSlot(slotId: SaveSlotData.SlotId) → Promise.Promise<any>Deletes the slot with the given ID. Deleting an ephemeral slot ends that session -- it is deselected and retired along with its in-memory store -- rather than being refused for being the active slot.
PromiseDeleteAllSlots
Deletes every slot for the player and clears the active/last-active selection, resetting the player to a fresh state. Resolves once all slots are gone.
PromiseResetSlot
HasSaveSlotsDataStore.PromiseResetSlot(slotId: SaveSlotData.SlotId) → Promise.Promise<SaveSlotData.SlotId>Resets the slot with the given id to a fresh empty one -- equivalent to deleting the slot and creating a new one at the same index. The slot keeps its index and name; its saved data and metadata (timestamps) start fresh. Resolves to the new slot id.
When the reset slot is the active slot, the selection clears and then reselects the fresh slot: everything bound to HasSaveSlotsDataStore.ObserveActiveSlotStoreBrio tears down as the selection clears and rebuilds against the empty store on reselect, so consumers reset reactively without wiping their own state. A non-active slot is left unselected, and its "Continue" pointer (when it was the last-active slot) is carried across to the fresh id so the reset slot stays resumable. Rejects when the slot is missing.
PromiseResetActiveSlot
HasSaveSlotsDataStore.PromiseResetActiveSlot(self: HasSaveSlotsDataStore) → Promise.Promise<SaveSlotData.SlotId?>Resets the active slot to a fresh empty one -- see HasSaveSlotsDataStore.PromiseResetSlot. The slot keeps its index and name; its saved data and metadata (timestamps) start fresh, and the fresh slot stays selected. Resolves to the new slot id, or nil when no slot is active.
PromiseSetSlotMetadata
HasSaveSlotsDataStore.PromiseSetSlotMetadata() → Promise.Promise<any>Sets the metadata for the slot with the given ID
PromiseGetSlotMetadata
HasSaveSlotsDataStore.PromiseGetSlotMetadata(slotId: SaveSlotData.SlotId) → Promise.Promise<SaveSlotData.SaveSlotMetadata?>Gets the metadata for the slot with the given ID
PromiseSlotIdFromIndex
HasSaveSlotsDataStore.PromiseSlotIdFromIndex(slotIndex: number) → Promise.Promise<SaveSlotData.SlotId?>Returns the slot ID from the given index
PromiseLastActiveSlotId
HasSaveSlotsDataStore.PromiseLastActiveSlotId(self: HasSaveSlotsDataStore) → Promise.Promise<SaveSlotData.SlotId?>Gets the last active slot ID
PromiseSelectLastSaveSlot
HasSaveSlotsDataStore.PromiseSelectLastSaveSlot(self: HasSaveSlotsDataStore) → Promise.Promise<SaveSlotData.SlotId?>Selects the player's last active slot if one still exists, resolving to the selected slot id -- or nil when there is no slot to continue on. Backs a "Continue" affordance that every save-slot consumer tends to need.
PromiseSelectNewSaveSlot
HasSaveSlotsDataStore.PromiseSelectNewSaveSlot(self: HasSaveSlotsDataStore) → Promise.Promise<SaveSlotData.SlotId?>Creates a new slot at the lowest free index and selects it, resolving to the new slot id -- or nil when every slot is already in use. Backs a "New Game" affordance.
PromiseSelectEphemeralSlot
HasSaveSlotsDataStore.PromiseSelectEphemeralSlot(metadata: SaveSlotCreateMetadata?--
optional SlotName/Summary for the in-memory slot
) → Promise<SlotId>Creates a fresh ephemeral slot and selects it, resolving to its id. An ephemeral slot is selectable and active exactly like a real one -- it drives HasSaveSlotsDataStore.ObserveActiveSlotStoreBrio, summaries, and playtime the same way, and its metadata replicates to the client like any other slot's, so UI can render the active session's name and summary -- but it is never persisted: no metadata is written, its data store is in-memory, and it is torn down the moment it stops being the active slot. It is also excluded from the save list (SaveSlotDataService.GetSlotList / SaveSlotDataService.ObserveSlotList), so it never shows up as something the player can return to. Selecting it never disturbs the persisted active-slot pointer or the "Continue" target, so the real slot the player came from resumes untouched afterward. Backs a throwaway session (e.g. exploring a lobby) that must leave no trace on save data.
_setupPlaytimeTracking
Accrues per-slot playtime automatically. A "session" spans the time a slot is the active slot: selecting a slot begins one (bumping PlayCount), and deselecting, switching, or unbinding ends it. Elapsed wall time is folded into the slot's TimePlayed from a datastore saving callback, so it persists on exactly the cadence the data is written -- always fresh at save time, with no separate timer -- and again at each session boundary.
Skipped entirely when the owner opts out (TrackPlaytime = false), which is what offline admin
tooling wants: editing a slot from a console is not someone playing it, and no session is ever
begun, so _flushPlaytime stays a no-op for the object's whole life.