Saving data
data keeps values between sessions: a player's coins, their inventory, the best time on a course. It is for server Scripts only.
A store
local store = data.store("PlayerData") -- a named store, one per game| Method | What it does |
|---|---|
store:get(key) | The value saved under key, or nil |
store:set(key, value) | Saves value under key (nil removes it) |
store:update(key, fn) | Reads the value, passes it to fn, saves what fn returns |
store:increment(key, delta) | Adds delta (1 if left out) to a number and returns the new total |
store:remove(key) | Removes the key |
Values can be nil, booleans, numbers, strings, value types (vec3, colours...) and tables of those - no parts or objects - up to 16 KB a key.
Each call waits for the answer without freezing the game, and raises an error when it fails (the network, the database). Always wrap it in pcall.
Saving a player's coins
local store = data.store("PlayerData")
players.joined:connect(function(player)
player.stats.Coins = 0 -- before anything was saved
if player.userId <= 0 then return end -- guests (userId -1) are never saved
local ok, saved = pcall(function()
return store:get("user_" .. player.userId)
end)
if ok and saved then
player.stats.Coins = saved
end
end)
players.left:connect(function(player)
if player.userId <= 0 then return end
pcall(function()
store:set("user_" .. player.userId, player.stats.Coins)
end)
end)Type savedata at the start of a line in the editor to get this snippet.
Key by userId, never by name
Players can change their name. Their userId stays the same for life. Guests have userId -1, so check userId > 0 before saving.
Saving a table
local profile = {
coins = 120,
level = 4,
inventory = {"Sword", "Shield"},
}
pcall(function() store:set("user_" .. player.userId, profile) end)Save one table per player rather than many keys: one request instead of several, and it can't be half-saved.
Safe changes: update and increment
update and increment are safe when several servers touch the same key at once (two servers of the same game, a global counter). update only writes over the value it read; if it changed meanwhile, it reads again and runs fn again - so keep fn free of side effects:
local stats = data.store("Global")
pcall(function() stats:increment("totalWins") end)
pcall(function()
stats:update("bestTime", function(old)
if old == nil or myTime < old then return myTime end
return old
end)
end)When the server shuts down
When a server closes - an update, a restart, the last player leaving - game.closing fires first, with every player still in the game, then every player leaves, so your players.left handlers run and save. The server waits for them up to 20 seconds.
game.closing:connect(function()
-- save anything the game keeps for everyone (a world record, a shared bank)
end)Read before you wait
After a task.wait inside players.left, the player is gone: read what you need (player.userId, player.stats.Coins) before waiting.
Where it is kept
Games started from KHIM save to KHIM, per game: another game can't read them.
Limits
- 16 KB a value.
- 64 requests in flight at once on one server.
updategives up with an error if the key keeps changing twelve times in a row.
JSON
json.encode(value, pretty?) turns a table into JSON text, json.decode(text) turns it back - for settings written as JSON, or a table saved as one string:
local text = json.encode({coins = 5, items = {"Sword"}}) -- '{"coins":5,"items":["Sword"]}'
local back = json.decode(text)
print(back.items[1]) -- Sword