Data
Server only. Requiring this from a client fails at the require, naming the module.
local Data = require("@game/ServerScriptService/Twill").Data
Data.Configure({ Store = "PlayerData", Version = 1, Template = { Coins = 0, Stats = { Wins = 0 } },})
Lifecycle.SetPlayerGate(Data.Gate)Data puts a template, a version number, migrations, and a joining gate over
Store. Store holds the key; Data decides what the key
contains and who waits for it.
Configure and the gate
Section titled “Configure and the gate”Configure runs once, during Init. Calling it twice throws.
Data.Gate holds a joining player until their data exists. Wire it into
Lifecycle and no service sees a player before their
data is there.
A load that fails after its retries kicks the player rather than handing back the template. Data that could not be read is not data that should be overwritten.
Your saved data could not be loaded.
You have not lost anything. Please rejoin.A session claimed by another server kicks as well, with a different message. Nothing is lost either way: the other server holds the data.
In Studio, a place that cannot reach storage runs on stand-in stores instead, and says so once. Data then lives in memory and is forgotten when the session stops, rather than every player waiting for a store that will never answer.
When a player leaves
Section titled “When a player leaves”A player’s sessions end when their Scope.Player bag
closes. Under Lifecycle that is after the last
OnPlayerRemoving, so a service can still read and change Data.Get(player)
there, and the change is part of the last save.
Versions and migrations
Section titled “Versions and migrations”Version is the shape the running server expects. Migrations are keyed by the
version each one produces.
Data.Configure({ Store = "PlayerData", Version = 3, Template = { Coins = 0, Level = 1 }, Migrations = { [2] = function(data) data.Coins = data.Money or 0 end, [3] = function(data) data.Level = 1 end, },})Steps run in ascending order, from the version the data carries to the version the server wants. A missing step is skipped. The steps run on a copy, which replaces the live data only once every step has succeeded. A step that raises stops the load and the player is kicked, with their stored data left exactly as it was.
A player with no saved data starts from the template at the current version, and no step runs. A step only ever sees data that was saved at an older version.
Data already at a version past the server’s own is left exactly as it is, and reported once. This is what stops a rollback running every migration a second time on data that has already had them.
After migration, anything the template gained and the data lacks is filled in. Existing values are left alone.
Branches
Section titled “Branches”A branch is a separate store under the same user, with its own template, its own version, and its own migrations. Use one for data that is large, rarely read, or both.
Data.Configure({ Store = "PlayerData", Template = { Coins = 0 }, Branches = { pets = { Version = 1, Template = { Owned = {}, Slots = 3 } }, },})
local pets = Data.LoadBranch(player, "pets")Branches are not loaded with the player. LoadBranch opens one, and two callers
asking at once both wait on the same load rather than starting two.
UnloadBranch closes one early, for data only needed during part of a session.
Anything still open closes when the player leaves.
A branch cannot be named main.
Refusing data that should never be saved
Section titled “Refusing data that should never be saved”Validate is a check the data must pass before any save. A save it refuses is
skipped, the last good data stays stored, and the refusal is logged.
Data.Configure({ Store = "PlayerData", Template = { Coins = 0 }, Validate = function(data) return data.Coins >= 0, "coins cannot go below zero" end,})The check must return exactly true. A second return value is the reason that
is logged. A check that raises counts as a refusal. Each branch takes its own
Validate.
When a player leaves while their data fails the check, their session is still released so the next server does not wait, and the data they last saved is what that server loads.
Changing several things as one
Section titled “Changing several things as one”Transact runs a function that changes a player’s live data, and keeps either
every change or none. The changes are undone when the function raises, when it
yields, or when the result fails the store’s Validate check.
local bought, reason = Data.Transact(player, function(data) assert(data.Coins >= 250, "not enough coins") data.Coins -= 250 data.Owned.Sword = trueend)Raise to undo: assert(condition, reason) hands the reason back as the second
return value. A call that returns false and a reason, as every kit call does,
fits inside assert directly.
Undoing puts the data back in place. A table held before the transaction still points at live data afterwards.
A transaction opened inside another joins it. The outer one decides whether everything stays, so a failure inside undoes the outer changes too.
Defer holds an effect until the open transaction is kept. Use it for anything
that must not report a change that did not stay, such as a signal or an
analytics event.
Writing to anybody
Section titled “Writing to anybody”Edit and Reset work on any user, wherever they are. When this server holds
the data, the change lands here. When it does not, the change is written into the
key itself and applied by whichever server next holds it.
That difference is what Outcome reports. When the scope has a Validate
check, a change made here is tried on a copy first, and a change the check
refuses is never applied.
| Outcome | Meaning |
|---|---|
applied | Written here and saved. |
queued | Left on the key. It lands when a server holds them. |
blocked | Something along that path is not a table. |
refused | The scope’s Validate check rejected the result. Nothing changed. |
unknown | No scope by that name. |
unsupported | The value would not survive being saved. |
failed | The change could not be sent. |
queued is not applied. For a user who never returns, it never lands.
Data.Configure
Section titled “Data.Configure”[Server]
Prepares the store, the template, and the upgrade path.
function Data.Configure(config: Config)Config
| Field | Type | Description |
|---|---|---|
Store | string | The DataStore name. |
Template | { [string]: any } | What a player starts with. |
Version | number? | The shape this server expects. 1 when left out. |
Migrations | { [number]: (data) -> () }? | Keyed by the version each step produces. |
Branches | { [string]: Branch }? | Separate stores under the same user. |
Replicate | { string }? | Field names the player’s own client should see. |
Validate | ((data) -> (boolean, string?))? | A check every save must pass. See Refusing data that should never be saved. |
A Branch takes Template, Version, Migrations and Validate, with the
same meanings.
Throws on a second call, on a missing or empty Store, on a Template that is
not a table, and on a branch named main.
Data.IsConfigured
Section titled “Data.IsConfigured”[Server]
Reports whether a store has been configured.
function Data.IsConfigured(): booleanData.Check
Section titled “Data.Check”[Server]
Runs a store’s Validate check on data without saving it.
function Data.Check(data: { [string]: any }, branch: string?): (boolean, string?)Returns
boolean - true when the store would accept the data. true as well when the
store has no check.
string? - Why it would not.
Try a change on a copy first, and make it for real only when the copy passes. A change made across two players at once, such as a trade, is the usual reason.
Throws before Configure, on data that is not a table, and on a branch nobody
configured.
Data.Gate
Section titled “Data.Gate”[Server]
Holds a joining player until their data exists, then releases them with it.
function Data.Gate<T>(player: Player, ready: (data: T) -> ())Kicks the player when the session could not be opened. Yields. Throws when used
before Configure.
Data.Get
Section titled “Data.Get”[Server]
Returns a player’s live data.
function Data.Get<T>(player: Player): T?Write to it directly. There is no Set and no commit step.
Answers nil when no session is open. Throws when given anything but a Player.
Data.IsReady
Section titled “Data.IsReady”[Server]
Reports whether a player’s data is loaded and their session still holds.
function Data.IsReady(player: Player): booleanData.GetOffline
Section titled “Data.GetOffline”[Server]
Reads any user’s data, whether or not they are on this server.
function Data.GetOffline(userId: number, branch: string?): { [string]: any }?Answers the live table for a user this server holds, and a read-only copy for anyone else. Migrations and template filling are applied to the copy, so it has the shape this server expects.
Yields. Throws when used before Configure and when no branch answers to that
name.
Data.Save
Section titled “Data.Save”[Server]
Requests an early save for one player.
function Data.Save(player: Player)Returns before the write lands.
Data.SaveNow
Section titled “Data.SaveNow”[Server]
Saves a player and waits until the write is confirmed.
function Data.SaveNow(player: Player, verify: ((saved: { [string]: any }) -> boolean)?, timeout: number?): booleanParameters
| Name | Type | Description |
|---|---|---|
player | Player | Whose data to write. |
verify | ((saved) -> boolean)? | Given the data that landed. Answer true when it holds the change. |
timeout | number? | Seconds to wait. 30 when left out. |
Returns
boolean - true when the write landed and verify accepted it. Yields for
at most timeout seconds.
Only a save that read the data after the call counts, so a save that was already
under way cannot confirm a change it never saw. A save refused by Validate
answers false.
This is what a Developer Product grant waits on. Granting in memory and
answering PurchaseGranted before the write lands loses the purchase if the
server stops in between.
Data.Transact
Section titled “Data.Transact”[Server]
Makes several changes to a player’s data as one: all of them stay, or none does.
function Data.Transact<T>(player: Player, change: (data: T) -> ()): (boolean, string?)Parameters
| Name | Type | Description |
|---|---|---|
player | Player | Whose data to change. |
change | (data: T) -> () | Makes the changes on the live data. Raise to undo them all. |
Returns
boolean - true when every change was kept. string? - why nothing was
kept: what was raised, a transaction cannot yield, the refusal from
Validate, or data is not loaded.
Throws when player is not a Player or change is not a function. A
transaction copies the player’s data once, so keep it to changes made at the
pace of a player’s actions.
Data.Defer
Section titled “Data.Defer”[Server]
Runs an effect once the player’s open transaction is kept.
function Data.Defer(player: Player, effect: () -> ())Parameters
| Name | Type | Description |
|---|---|---|
player | Player | Whose transaction decides. |
effect | () -> () | What to do once the change is certain to stay. |
With no transaction open, the effect runs at once. An effect queued by a transaction that is undone never runs.
Data.SaveAll
Section titled “Data.SaveAll”[Server]
Requests an early save for every open session, branches included.
function Data.SaveAll()Returns before the writes land. Everything open is flushed on shutdown anyway,
so no save call belongs in BindToClose. Where the platform gives warning of a
scheduled restart, Configure asks for an early save on it.
Data.LoadBranch
Section titled “Data.LoadBranch”[Server]
Opens a branch for a player, or hands back the one already open.
function Data.LoadBranch<T>(player: Player, name: string): T?Two callers asking at once wait on the same load. Yields. Throws when no branch
answers to that name, and when given anything but a Player.
Data.GetBranch
Section titled “Data.GetBranch”[Server]
Returns an already open branch without loading anything.
function Data.GetBranch<T>(player: Player, name: string): T?Data.IsBranchLoaded
Section titled “Data.IsBranchLoaded”[Server]
Reports whether a branch is open and its session still holds.
function Data.IsBranchLoaded(player: Player, name: string): booleanData.UnloadBranch
Section titled “Data.UnloadBranch”[Server]
Closes a branch early.
function Data.UnloadBranch(player: Player, name: string)Data.ListBranches
Section titled “Data.ListBranches”[Server]
Lists the branch names this store was configured with.
function Data.ListBranches(): { string }A fresh list in a settled order, safe to keep or reorder.
Data.Edit
Section titled “Data.Edit”[Server]
Writes one field of a user’s data, wherever that user is.
function Data.Edit(userId: number, scope: string, path: string, value: any): OutcomeParameters
| Name | Type | Description |
|---|---|---|
userId | number | Whose data to edit. |
scope | string | "main", or a branch name. |
path | string | Dot separated, such as Stats.Coins. |
value | any | What to write. nil removes the field. |
A value storage would refuse is refused here instead, and answers unsupported.
Run it through Serialize.Encode first. A result the
scope’s Validate check rejects answers refused and changes nothing.
Yields. Throws when used before Configure, and on a missing user id or path.
Data.Reset
Section titled “Data.Reset”[Server]
Puts a field, or a whole scope, back to its template.
function Data.Reset(userId: number, scope: string, path: string?): OutcomeLeave path out to restore the whole scope.
There is no confirmation step and no undo. Restoring a scope rewrites the live
table in place, so anything holding it keeps working and sees the template.
Fields whose names begin with __twill survive a whole-scope reset, so a
purchase already paid for is still recognised as paid.
Data.Versions reads earlier versions if the old data is needed back.
Data.Inspect
Section titled “Data.Inspect”[Server]
Reports who holds a user’s scope and what the key records about it.
function Data.Inspect(userId: number, scope: string): { [string]: any }?Returns
{ [string]: any }? - Holder, Created, Updated, Loads and Users, or
nil when the key holds nothing or no scope answers to that name. Yields.
Holder is nil when no server holds the key. Created and Updated are
os.time readings, and Updated is 0 on a key that was never written.
Data.Versions
Section titled “Data.Versions”[Server]
Walks back through what a user’s scope held before.
function Data.Versions(userId: number, scope: string): anyReturns
any - A walk, or nil when no scope answers to that name. NextAsync on it
answers one version at a time, newest first, then nil.
local walk = Data.Versions(userId, "main")
while true do local older = walk:NextAsync() if not older then break end
print(older.Updated, older.Data.Coins)endReading a version changes nothing. Write a value back with Data.Edit.
Data.Migrate
Section titled “Data.Migrate”[Server]
Raises stored data to a version by applying each upgrade in turn.
function Data.Migrate(stored: { [string]: any }, version: number, steps: { [number]: (data) -> () })Changes stored in place. Data already current is left untouched, and data past
that version is left as it is rather than marked back down to it.
Throws when a step raises, naming the version it was reaching.
Outcome
Section titled “Outcome”export type Outcome = "applied" | "queued" | "blocked" | "refused" | "unknown" | "unsupported" | "failed"Replication
Section titled “Replication”Replicate names the fields a player’s own client should see. Those fields are
published to that player alone, under the key Data, and brought back in line
on an interval.
Data.Configure({ Store = "PlayerData", Template = { Coins = 0, Secret = "" }, Replicate = { "Coins" },})-- ClientReplication.Subscribe("Data.Coins", function(coins) label.Text = tostring(coins)end, bag)A field left out of Replicate never leaves the server.
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Time between replication refreshes | 0.1 seconds |
SaveNow default timeout | 30 seconds |
| Key size | 4 MB |
Autosave, set by Store | 180 seconds |