Skip to content

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: what data(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: every set_data of 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 (mana is full again; souls keeps 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