Saved data (attachments on entities, players and chunks, world data)
Status: Implemented (API 1.12). Data attachments on any entity or player (saved with it, optionally synced to Celestial clients), on chunks (saved in the chunk), and saved data per level (saved with the world). Values are NBT: Lua tables in Lua, SNBT text or binary NBT in C and C++.
This guide gives players a quest log, marks claimed chunks and keeps a world-wide counter of finished quests. Everything is saved with the world, survives restarts, and stays in the save untouched if the mod is removed.
Reference: Persistence and data attachments, function tables entity, level, events. Small synced values for rendering: Guide: Entity Gameplay (set_synced).
1. Keys and values
Every value lives under a key, a namespaced id like "my_mod:quests". A bare key ("quests") gets your mod’s namespace. Use one key per feature, so other mods never collide with yours.
A value is one NBT value. In Lua you just use Lua values:
| Lua | NBT | back in Lua |
|---|---|---|
true / false |
byte 1b / 0b |
true / false |
| integer | int (long outside the int range) | integer |
| float | double | number |
| string | string | string |
| table with keys 1…n | list | sequence |
other table (or {}) |
compound | table |
Bytes other than 0 and 1, shorts, floats and the typed arrays written by C mods come back as numbers and sequences. Functions, userdata and nil inside tables can’t be stored (setting a key to nil removes it).
In C and C++ a value is either SNBT text ({level:3,done:[1b,0b]}, what /data get shows) or binary NBT in a buf (network framing: the type byte, then the payload; the format NbtIo.writeAnyTag writes).
2. Entities and players
cl.on("player.join", function(ev)
local p = ev.player
local log = p:data("quests") or { done = {}, active = "find_the_cave" }
p:set_data("quests", log)
end)
function finish(p, quest)
local log = p:data("quests")
log.done[#log.done + 1] = quest
log.active = nil
p:set_data("quests", log, { sync = true }) -- also send it to the player's Celestial client
end
e:data(key)reads (nilwhen unset),e:set_data(key, value, opts)writes,e:data_keys()lists the keys,e:set_data(key, nil)removes.- Works on every entity: mobs, items, minecarts, and players. Mobs save it in their entity NBT under
clight:data; players in their player file. { sync = true }sends the value to every Celestial client with CLight that sees the entity (and to the player itself). Vanilla clients get nothing. On the client,e:data(key)reads the synced copy andentity.data_changedfires when it changes. A synced value is at most 32 KiB; use synced data only for what clients really need.- Setting the same value again does nothing (no save, no packet).
C and C++:
cl_entity_data_set(player, CL_STR("my_mod:quests"), CL_STR("{active:\"find_the_cave\",done:[]}"), CL_DATA_SYNC);
char text[512];
size_t len = 0;
if (cl_entity_data_get(player, CL_STR("my_mod:quests"), text, sizeof text, &len) == CL_OK) { /* parse it */ }
auto quests = clight::Entity(player).data_get("my_mod:quests"); // Expected<std::string> (SNBT)
clight::Buf raw = clight::Entity(player).data_get_nbt("my_mod:quests"); // binary NBT, released by the Buf
3. Chunks
Chunk data belongs to a loaded chunk (chunk coordinates are block coordinates >> 4) and is saved inside the chunk under clight:data:
local chunk = world:chunk_at(p.pos)
if not chunk:data("claim") then
chunk:set_data("claim", { owner = p.name, since = world:game_time() })
end
-- also: world:chunk(cx, cz), chunk:keys(), world:chunk_data(cx, cz, key), world:set_chunk_data(cx, cz, key, value)
A chunk that is not loaded gives CL_E_NOT_LOADED (Lua: an error); read it when players are near, or keep a world-wide index in saved data.
4. Saved data (per level)
Saved data is one value per key and level, saved in the world’s data/<namespace>/<path>.dat (every level of a key in one file). Use the overworld for world-wide data.
local stats = cl.level.server_overworld():saved_data("stats") -- a live table
stats.quests_done = (stats.quests_done or 0) + 1 -- no call needed: written at every world save
level:saved_data(key) gives the same table every time; CLight writes it at every save (autosave, /save-all, stopping the server). level:set_saved_data(key, value) replaces it at once (and nil removes it).
C and C++ read and write saved data like the other kinds:
cl_level_saved_data_set(overworld, CL_STR("my_mod:stats"), CL_STR("{quests_done:12}"));
A saved value is at most 16 MiB. The file is written when the world saves, never on every change.
5. What happens without your mod
- Vanilla never reads
clight:dataor thedata/<your namespace>/files. A world opened in vanilla keeps the mobs and chunks; vanilla drops the extra keys only when it rewrites them. - Celestial keeps the data of mods that are not installed untouched: remove your mod, play, add it again and the data is still there.
6. Limits and performance
- At most 256 keys per entity or chunk, 1 MiB per value, 16 MiB per saved value, 32 KiB per synced value.
- Reading and writing converts between NBT and your language: fine for game events, but don’t rebuild big tables every tick. Keep a working copy in your mod and write it back when it changes.
- Nothing runs or costs anything for entities, chunks and levels without data.
cl.has("data.entity"),"data.player","data.chunk","data.saved","data.sync"and"data.nbt"tell a mod these exist.