Skip to content

Data attachments and persistence internals

Status: Implemented (API 1.20). How attachments are stored, saved, synced and kept thread-safe, and what typed attachments add.

Reference: Persistence and data attachments.

Where the values live

Scope Stored in Saved as
Entity the entity’s extension record (created on the first value) clight:data in the entity NBT, synced keys listed in clight:data_sync
Player the player’s extension record clight:data in the player file
Chunk an immutable compound on the chunk (replaced on every change, so chunk snapshots for saving keep their own copy) clight:data in the chunk NBT
Saved data a cache per key on the server data/<namespace>/<path>.dat (every level of a key in one file), written at lifecycle.level_save

Vanilla ignores all of these keys and files. Celestial keeps the values of mods that are not installed exactly as they were, so removing a mod and adding it back loses nothing.

Threads

Values are read and written on the tick thread of the level that owns them (@context(TICK)): the server tick for entities, players, chunks and saved data. A client reads only the synced copies its connection received, on the client tick. There are no locks on the hot path; the per-key typed attachment table is read under a small mutex only while at least one key is registered (one relaxed atomic load otherwise).

Sync

A value set with CL_DATA_SYNC (or a key registered with CL_ATTACH_SYNC) goes on the clight:entities payload to every Celestial client with CLight that registered the channel and tracks the entity (the player itself included). It is sent again when a client starts tracking the entity. Vanilla clients get nothing. A removed synced key sends a removal. Synced values are at most 32 KiB.

Typed attachments

cl_attachment_register puts the key into a process-wide table (key, default SNBT, flags, owner mod):

  • Defaults. A get of a missing key with a default returns the default. On the server, the default is first stored (with the key’s sync rule), then read back in the format asked for, so the binary get and the save see the same value. A client has no SNBT parser: it returns the default text for its copy, and the binary get of an unset key stays CL_E_NOT_FOUND.
  • Sync. A set of a key with CL_ATTACH_SYNC is sent as if CL_DATA_SYNC were passed.
  • Death. Celestial keeps the same player object through a respawn (vanilla makes a new ServerPlayer and copies data with restoreFrom). When a player respawns after dying, the keys registered without CL_ATTACH_COPY_ON_DEATH are removed from the player (a synced one sends its removal); the next get gives the default again. Returning from the End keeps everything.

Cost

Without mods there is no extension record and no table: the save and load paths check one pointer. The death hook runs only for players with data.