Data attachments (mana on players, pollution on chunks)
Status: Implemented (API 1.20). Typed attachments: a key with a default, always synced if you want, and kept or dropped when a player dies. They work on entities, players, chunks and saved data. The untyped functions underneath are in Saved Data (API 1.12).
This guide gives every player mana that regenerates and shows on the HUD, a soul counter that survives death, and pollution on chunks that grows near furnaces.
Reference: Persistence and data attachments.
1. Register the keys
Register attachments once, at setup, on both sides (a client mod only needs the keys it reads):
cl.attachment("mana", { default = { value = 20, max = 20 }, sync = true })
cl.attachment("souls", { default = 0, copy_on_death = true })
cl.attachment("pollution", { default = 0 })
default: whatdata(key)returns while nothing was set. The server stores it on the first read, so the value is saved with the player from then on.sync = true: everyset_dataof this key also goes to Celestial clients with CLight (the player and everyone who sees it), as{ sync = true }would.copy_on_death = true: the value survives death. Without it, a player who dies and respawns starts again from the default (manais full again;soulskeeps counting).
The key "mana" becomes "<your mod>:mana"; other mods use the full name to read it.
2. Mana on players
cl.on("tick.server_post", function(e)
if e.tick_count % 20 ~= 0 then return end -- once a second
for _, p in ipairs(cl.entity.server_players()) do
local m = p:data("mana")
if m.value < m.max then
m.value = m.value + 1
p:set_data("mana", m) -- synced: the HUD sees it
end
end
end)
cl.on("server.command_register", function()
cl.cmd.register(cl.cmd.literal("fireball"):executes(function(ctx)
local p = ctx:player()
local m = p:data("mana")
if m.value < 5 then return ctx:failure("Not enough mana") end
m.value = m.value - 5
p:set_data("mana", m)
return 1
end))
end)
On the client, the synced copy reads the same way:
cl.hud.register_layer{ id = "mana", render = function(g)
local p = cl.entity.client_player()
if not p then return end
local m = p:data("mana") -- the default until the server sent one
g:text(("Mana %d / %d"):format(m.value, m.max), 4, 4, 0xFF55AAFF)
end }
3. Souls that survive death
cl.on("entity.death", function(e)
if e.attacker and e.attacker:is_player() then
local killer = e.attacker
killer:set_data("souls", killer:data("souls") + 1)
end
end)
When the player dies, mana is removed at the respawn (and reads as the default again); souls stays, because it was registered with copy_on_death.
4. Pollution on chunks
Chunk attachments use the same keys and defaults:
cl.on("block.place", function(e)
local ch = e.level:chunk_at(e.pos)
ch:set_data("pollution", ch:data("pollution") + 1)
end, { filter = "minecraft:furnace" })
5. C and C++
static void setup(void* payload, void* user) {
(void)payload; (void)user;
cl_attachment_desc d = CL_ATTACHMENT_DESC_INIT;
d.default_snbt = CL_STR("{value:20,max:20}");
d.flags = CL_ATTACH_SYNC;
cl_attachment_register(CL_STR("mana"), &d);
d.default_snbt = CL_STR("0");
d.flags = CL_ATTACH_COPY_ON_DEATH;
cl_attachment_register(CL_STR("souls"), &d);
}
cl_entity_data_get then returns the default as SNBT while nothing is set; cl_entity_data_get_nbt returns it as binary NBT on the server.
Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| Registering only on the server | The client reads nil before the first sync |
Register on both sides (side: both) |
| A large synced default | CL_E_LIMIT at registration |
Synced values are at most 32 KiB |
| Expecting copy-on-death for mobs | A mob’s data goes with the mob | Only players respawn; mobs keep data while they live |