Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Synched entity data

Verified against Minecraft 26.3 · Part VI · A player shears a sheep: one byte flips on the server, and the wool disappears on every screen in tracking range.

You right-click a sheep with shears. Somewhere on the server a single byte changes — bit four of one entry in a nineteen-slot array the sheep carries — and before that tick ends, every player watching has been told, by a packet that spends four bytes after the entity id. That array is SynchedEntityData, the channel the server uses to describe an entity to the clients that see it: the health bar over another player, a sneaking crouch, an armour stand’s pose, an item frame’s item, this sheep’s wool. It is a numbered array, and the numbers are the surprising part. The slot the wool lives in is not written anywhere in Sheep: it is 18 because eighteen slots were handed out above Sheep in its superclass chain, and one new field on Entity would renumber every entity in the game. Ids are ordinals handed out down the class tree by a single shared ClassTreeIdRegistry, which walks only the superclass chain: Entity takes 0 to 7, LivingEntity 8 to 14, Mob 15, AgeableMob 16 and 17, and Sheep, last in the chain, gets 18. Nothing names these numbers, nothing writes them down, and they stop at 254 — because on the wire, 255 means end of packet.

The cast

classwhat it decidesthread
SynchedEntityDataone entity’s numbered array, and whether anything in it has changed since the last flushone container per side, a live one confined to the Server thread or the Render thread
SynchedEntityData.DataItemone slot: its value, the default it was built with, and its own dirty flagwith its container
EntityDataAccessorthe key — an int id and a serializer, equal to another accessor on the id aloneimmutable, shared by every instance of the class
ClassTreeIdRegistrywhich id a SynchedEntityData.defineId call gets, from the last id already taken by an ancestor classwhichever thread first initialises the class
EntityDataSerializersthe 44 registered serializers and the wire id of each, in registration ordera static block, once
ServerEntitywhether this entity sends anything this tick, and whatthe Server thread
ClientboundSetEntityDataPacketthe wire form: an entity id, then id/serializer/value triples, then 255encoded on the Netty pipeline, built on the Server thread
ClientPacketListenerapplying an incoming batch to the client’s own containerthe Render thread

Nineteen slots, and where the numbers come from

SynchedEntityData.defineId is called from the static initialiser of an entity class and asks ClassTreeIdRegistry.define for a number. ClassTreeIdRegistry keeps one map from class to last id issued, and ClassTreeIdRegistry.getLastIdFor walks up the superclass chain until it finds an entry — so a subclass continues its parent’s numbering rather than starting over. Java’s guarantee that a superclass initialises before its subclass is the entire ordering mechanism. ClassTreeIdRegistry.getCount is the same walk plus one, and it is what sizes the array.

A Sheep — Entity → LivingEntity → Mob → PathfinderMob → AgeableMob → Animal → Sheep, of which PathfinderMob and Animal define nothing — therefore has exactly nineteen slots:

idfieldserializerdefault
0Entity.DATA_SHARED_FLAGS_IDEntityDataSerializers.BYTE0
1Entity.DATA_AIR_SUPPLY_IDEntityDataSerializers.INTEntity.getMaxAirSupply
2Entity.DATA_CUSTOM_NAMEEntityDataSerializers.OPTIONAL_COMPONENTempty
3Entity.DATA_CUSTOM_NAME_VISIBLEEntityDataSerializers.BOOLEANfalse
4Entity.DATA_SILENTEntityDataSerializers.BOOLEANfalse
5Entity.DATA_NO_GRAVITYEntityDataSerializers.BOOLEANfalse
6Entity.DATA_POSEEntityDataSerializers.POSEPose.STANDING
7Entity.DATA_TICKS_FROZENEntityDataSerializers.INT0
8LivingEntity.DATA_LIVING_ENTITY_FLAGSEntityDataSerializers.BYTE0
9LivingEntity.DATA_HEALTH_IDEntityDataSerializers.FLOAT1.0
10LivingEntity.DATA_EFFECT_PARTICLESEntityDataSerializers.PARTICLESempty
11LivingEntity.DATA_EFFECT_AMBIENCE_IDEntityDataSerializers.BOOLEANfalse
12LivingEntity.DATA_ARROW_COUNT_IDEntityDataSerializers.INT0
13LivingEntity.DATA_STINGER_COUNT_IDEntityDataSerializers.INT0
14LivingEntity.SLEEPING_POS_IDEntityDataSerializers.OPTIONAL_BLOCK_POSempty
15Mob.DATA_MOB_FLAGS_IDEntityDataSerializers.BYTE0
16AgeableMob.DATA_BABY_IDEntityDataSerializers.BOOLEANfalse
17AgeableMob.AGE_LOCKEDEntityDataSerializers.BOOLEANfalse
18Sheep.DATA_WOOL_IDEntityDataSerializers.BYTE0

There is one of those arrays per side, not one per entity: the server’s Sheep has its own container and your client’s Sheep has another, built independently from the same class chain, and the packet below is the only thing that ever reconciles them.

Declaration order is not definition order

The declaration order and the definition order are two different lists. Ids come from where SynchedEntityData.defineId sits in the class body; the values come from the Entity constructor, which defines its own eight and then calls the abstract Entity.defineSynchedData that every subclass with data of its own overrides and chains up through. LivingEntity defines its seven in a different order from the one that numbered them, and it does not matter, because SynchedEntityData.Builder.define writes each item at EntityDataAccessor.id. SynchedEntityData.Builder.build then refuses to hand over a container with any slot still null, naming the id it is missing — which is why SynchedEntityData.get can read a slot with no null check at all.

Four slots that are bitfields

Four of the nineteen are bitfields, and they are the dense part of the channel. Slot 0 is Entity.FLAG_ONFIRE 0, Entity.FLAG_SHIFT_KEY_DOWN 1, Entity.FLAG_SPRINTING 3, Entity.FLAG_SWIMMING 4, Entity.FLAG_INVISIBLE 5, Entity.FLAG_GLOWING 6 and Entity.FLAG_FALL_FLYING 7 — bit 2 is unnamed and unused — behind Entity.getSharedFlag and Entity.setSharedFlag, whose parameter carries a purpose-built Entity.Flags type-use annotation. Slot 8 carries using an item, off hand and spin attack (LivingEntity.LIVING_ENTITY_FLAG_IS_USING, LivingEntity.LIVING_ENTITY_FLAG_OFF_HAND, LivingEntity.LIVING_ENTITY_FLAG_SPIN_ATTACK), slot 15 no-AI, left-handed and aggressive behind Mob.setNoAi, Mob.setLeftHanded and Mob.setAggressive, and slot 18 packs a DyeColor id into the low nibble and sheared into bit four — Sheep.getColor, Sheep.setColor, Sheep.isSheared, Sheep.setSheared, and the same storage read out as DataComponents.SHEEP_COLOR by Sheep.get and written back through Sheep.applyImplicitComponent.

Nothing may be inserted above you

The numbering belongs to the class, not to the concept. Avatar — the class between LivingEntity and Player (entity anatomy) — owns Avatar.DATA_PLAYER_MAIN_HAND and Avatar.DATA_PLAYER_MODE_CUSTOMISATION, so the skin-part toggles belong to every avatar, while Player.DATA_PLAYER_ABSORPTION_ID, Player.DATA_SCORE_ID and the two shoulder parrots (Player.DATA_SHOULDER_PARROT_LEFT, Player.DATA_SHOULDER_PARROT_RIGHT, optional ints rather than NBT) sit one level further down.

That is the whole answer to can two mods both add a field to LivingEntity: only by accident. Inserting, removing or reordering a SynchedEntityData.defineId on a base class shifts every id below it in every subclass, so two mods claiming the same number either collide in SynchedEntityData.Builder.define, which rejects a duplicate, or land a value in the wrong slot, which throws the serializer check on the client when the two serializers differ and passes silently when they match. And one class treats these numbers as what they are: Display.RENDER_STATE_IDS is an int set of the eight accessor ids that force a render-state rebuild, tested against each incoming accessor, with Display.TextDisplay keeping a second set of its own. Nothing in the codebase demonstrates more plainly that a slot number is an ordinal and not a name.

The serializer is the other half of the key

An EntityDataAccessor is an id and an EntityDataSerializer, and the serializer is what turns the value into bytes. The interface is deliberately thin: a StreamCodec returned by EntityDataSerializer.codec — returned, not extended — paired with an EntityDataSerializer.copy that copies a value as it is packed, so a later change to the container’s live value cannot reach a packet already built or the tracker’s snapshot. EntityDataSerializer.ForValueType is the immutable case, where copying is identity, and EntityDataSerializer.forValueType builds one from a codec alone.

EntityDataSerializers registers 44 of them into a CrudeIncrementalIntIdentityHashBiMap, from a single static block, so registration order is the wire id — EntityDataSerializers.BYTE is 0, EntityDataSerializers.POSE is 20, EntityDataSerializers.DYE_COLOR is 43 and last. Several of them carry Holders of data-pack registries — the per-species variants and the painting variants — and those, the item stacks, the text components and the particles are why the buffer on both sides is a RegistryFriendlyByteBuf rather than a plain one (codecs). The full list with wire ids and value types is the serializer table. EntityDataSerializers.registerSerializer is public and is the only thing that fills the bimap: vanilla calls it 44 times from that one block, and nothing else in the tree ever calls it again. It is a mod extension point shipped with no caller outside its own file.

Shears to wool gone, inside one tick

sequenceDiagram
    participant MPGM as MultiPlayer<br/>GameMode
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant Sheep as Sheep
    participant SED as SynchedEntityData

    MPGM->>SGPL: ServerboundInteractPacket, from MultiPlayerGameMode.interact
    MPGM->>MPGM: then the same interaction locally, unless spectator
    rect rgba(0, 0, 0, 0.04)
    Note over SGPL,SED: the server tick, before MinecraftServer.tickServer
    SGPL->>SGPL: Entity.setShiftKeyDown, then the range and border checks
    SGPL->>Sheep: Mob.interact, down from Player.interactOn
    Sheep->>Sheep: Mob.checkAndHandle<br/>ImportantInteractions
    Sheep->>Sheep: Sheep.mobInteract
    Sheep->>Sheep: Sheep.shear: the sound, the loot table, then Sheep.setSheared
    Sheep->>SED: SynchedEntityData.set, slot 18 with bit four
    SED->>Sheep: Entity.onSyncedDataUpdated, before anything is dirty
    SED->>SED: SynchedEntityData.<br/>DataItem.setDirty
    end

The server’s container, and the one moment worth slowing down for: the callback to Sheep runs first and the item is marked dirty after it, not the other way round. The whole band is one tick, and it is over before MinecraftServer.tickServer has begun.

Later in that same tick the container is drained, and what the client writes it into is a second container — one per side, not one per entity:

sequenceDiagram
    box transparent the server
    participant SED as SynchedEntityData
    participant SE as ServerEntity
    end
    box transparent the client
    participant CPL as ClientPacketListener
    participant CSED as SynchedEntityData
    end

    rect rgba(0, 0, 0, 0.04)
    Note over SED,SE: the same tick, the ServerLevel.chunkSource phase
    SE->>SED: SynchedEntityData.packDirty, once the gate opens
    SED-->>SE: one SynchedEntityData.DataValue: id 18, serializer 0, one byte
    SE->>CPL: ClientboundSetEntityDataPacket, flushed at the end of the tick
    end
    CPL->>CSED: SynchedEntityData.assignValues, per item then the batch
    Note over CPL,CSED: next frame: SheepRenderer.extractRenderState reads Sheep.isSheared

Two containers, one per side, and the packet is the only thing that ever reconciles them. The server’s half runs later in the same tick as the shear, in the chunk-source phase; the client’s container was built with the client’s own Sheep long before, and changes only when the packet lands.

The click. MultiPlayerGameMode.interact sends a ServerboundInteractPacket — a flat record of entity id, hand, an entity-relative hit location and the secondary-action flag, attacks having left for ServerboundAttackPacket — and then, on the next line, runs the interaction locally as a prediction, unless the local game mode is spectator. The packet goes first.

The server checks the geometry, not the outcome. ServerGamePacketListenerImpl.handleInteract confirms the thread with PacketUtils.ensureRunningOnSameThread and that the client has loaded, resolves the entity with ServerLevel.getEntityOrPart, tests the world border and Player.isWithinEntityInteractionRange, and checks the held item against the level’s feature flags. Before any of the geometry, though, it writes the packet’s secondary-action flag straight into Entity.setShiftKeyDown — which is itself a synched-data write on the player, so every interaction packet is also a potential update on slot 0.

Dispatch down the hierarchy. Player.interactOn calls Entity.interact, which dispatches virtually to the most derived override, Mob.interact. That runs three things in a fixed order: Mob.checkAndHandleImportantInteractions (name tags, spawn eggs), then the superclass hook Entity.interact with its leashing branch, and only if that passes, Sheep.mobInteract — the base hook runs between the two mob hooks, not before them. The shears test is item identity against Items.SHEARS, not a tag and not a component, plus Sheep.readyForShearing from the shared Shearable interface, whose other implementors are MushroomCow, SnowGolem, Bogged, CopperGolem and SulfurCube. A sheep that is not ready returns InteractionResult.CONSUME rather than falling through, which is why shears on an already-sheared sheep do nothing visible at all.

The effect, and one byte. Sheep.shear plays SoundEvents.SHEEP_SHEAR, drops wool through LivingEntity.dropFromShearingLootTable with BuiltInLootTables.SHEAR_SHEEP — the tool is passed in, so the loot table can see it — and calls Sheep.setSheared, which ors bit four into slot 18. SynchedEntityData.set compares the new value against the current one, stores it, calls Entity.onSyncedDataUpdated for that accessor, and then marks the item and the container dirty. Sheep does not override the hook, and the base implementation reacts to exactly one accessor, Entity.DATA_POSE, by calling Entity.refreshDimensions — which is also the first answer to whether any of this channel touches the physics the client simulates. A few slots do. Entity.DATA_POSE travels on its own EntityDataSerializers.POSE, and the hook resizes the hitbox on whichever side just received it (entity anatomy has what the resize costs); a dozen subclasses resize the same way on a value of their own, this sheep’s AgeableMob.DATA_BABY_ID among them; Entity.DATA_NO_GRAVITY switches gravity off; and a handful more steer what the client simulates, a pig’s boost, a sleeper’s bed, an item in use slowing your own input. The rest of the channel is cosmetic to the client, or read back by gameplay code that already knew. Back in Sheep.mobInteract: Entity.gameEvent with GameEvent.SHEAR for the sculk listeners (game events), ItemStack.hurtAndBreak on the shears, and InteractionResult.SUCCESS_SERVER.

The send, in the same tick. MinecraftServer.processPacketsAndTick drains the queue with PacketProcessor.processQueuedPackets and only then calls MinecraftServer.tickServer, so the shear happened before the tick proper began. ServerLevel.tick reaches its chunkSource phase after block and fluid ticks and before block events and entity ticking, and that phase runs ChunkMap.tick, whose loop over ChunkMap.TrackedEntity is the only caller of ServerEntity.sendChanges in the tree. The dirty flag opens the gate, ServerEntity.sendDirtyEntityData calls SynchedEntityData.packDirty, and one ClientboundSetEntityDataPacket goes to every tracking player, and to the entity itself when it is a player. After the entity id, the wire carries an unsigned byte 18, a var-int 0 for EntityDataSerializers.BYTE, one payload byte and then the terminator 255 — and that terminator is the whole reason ids stop at 254: the 255 ClientboundSetEntityDataPacket.EOF_MARKER names, and the 254 SynchedEntityData.MAX_ID_VALUE names.

The packet is not on the wire yet. MinecraftServer.tickChildren calls ServerCommonPacketListenerImpl.suspendFlushing on every player before it ticks any level and ServerCommonPacketListenerImpl.resumeFlushing at the end of the tick, so a whole tick’s packets leave together.

The apply, and the frame. ClientPacketListener.handleSetEntityData looks the entity up in ClientLevel and silently drops the entire packet if the id is unknown, then calls SynchedEntityData.assignValues, which checks that the incoming serializer is the one the accessor was defined with — a mismatch throws, loudly, on the client — stores each value, fires Entity.onSyncedDataUpdated per item and then the batch overload once. That batch overload, SyncedDataHolder.onSyncedDataUpdated, is the only place a client could see a whole update atomically, and nothing in the game overrides Entity’s empty body of it. Nothing tells the renderer. It finds out next frame, by reading the sheep again from scratch: SheepRenderer.extractRenderState copies Sheep.isSheared and Sheep.getColor into a fresh SheepRenderState, after which SheepWoolLayer draws nothing. The wool vanishing is a layer skipped, not a model swap — and only one layer, because SheepWoolUndercoatLayer tests colour, baby and invisibility but never the sheared flag, so a sheared coloured sheep still draws its undercoat.

The gate that holds a packet back

Two tests stand between a dirty byte and the wire, and only the first can hold it back:

flowchart TD
    IN{"ChunkMap.tick: a section changed, Entity.needsSync, or in entity-ticking range?"}
    SC["ServerEntity.sendChanges, opening with Entity.updateDataBeforeSync"]
    GATE{"the tracker's UpdateInterval, Entity.needsSync, or SynchedEntityData.isDirty?"}
    SEND["position, rotation and motion"]
    DATA["ServerEntity.sendDirtyEntityData"]
    HOLD["nothing goes out, and the dirty flags survive the tick"]
    QUIET["no position this tick, and no data was waiting"]

    IN -- "no" --> HOLD
    IN -- "yes" --> SC
    SC -- "an ItemFrame, every tenth tick: the map bypass" --> DATA
    SC --> GATE
    GATE -- "yes" --> SEND
    SEND --> DATA
    GATE -- "no" --> QUIET

Two tests, and the one thing that gets past the second without answering it. A dirty container is one of the second test’s own terms, so only the first can hold a byte back, and what it holds is kept for a later tick, not lost.

The first is not this page’s: ChunkMap.tick decides whether ServerEntity.sendChanges is called at all, on three conditions what the client is told sets out. An entity that fails them keeps its dirty data until one of the three becomes true. Entity.needsSync is the entity’s own way of forcing the issue: a flag that a push, a load and twenty-one other classes set to say ask about me this tick whatever the schedule says, read by both tests. The second test lets a dirty byte through, since SynchedEntityData.isDirty is one of its terms. Inside ServerEntity.sendChanges, that gate covers the position block and the usual ServerEntity.sendDirtyEntityData call, which is why shearing a sheep can also send that sheep’s position and rotation this tick: synched data is, incidentally, a latency channel for movement.

The interval comes from EntityType.updateInterval, fixed on the type at registration and turned into an UpdateInterval when ChunkMap starts tracking the entity — 2 for a player, 3 by default, and Integer.MAX_VALUE for eight types including EntityTypes.ITEM_FRAME, which becomes UpdateInterval.NEVER, so their interval branch never fires at all (entity anatomy has the eight and the tracking range beside it).

ServerEntity.sendChanges also has an ItemFrame special case that runs every tenth tick before the gate: it sends a framed map’s own update packets, which a player not carrying that map gets no other way, and then calls ServerEntity.sendDirtyEntityData, the one call to the synched-data flush outside the gate. (Two sends also sit outside the gate — the passengers packet and the Entity.syncVelocity motion packet — but neither touches the data channel.) ServerEntity.handleMinecartPosRot calls it too, but from inside the gate, not around it. Entity.syncPosition is the other lever. A living entity’s SteppedInterpolationTracker (every living type’s but a shulker’s) takes the flag first and records the position as a step; for the rest it realigns the tracker’s own counter so the very next evaluation lands on a multiple of the interval, which does nothing for a type whose interval is UpdateInterval.NEVER.

Entity.updateDataBeforeSync opens ServerEntity.sendChanges, ahead of the gate, and it is the hook LivingEntity overrides to reconcile its effects: LivingEntity.updateInvisibilityStatus and the glowing status write slot 0, and the swirl list goes into slot 10. A mob effect that expired this tick can therefore dirty the container and open its own gate, in the same call that goes on to read the flag.

One flush carries at most one value per slot, because SynchedEntityData.packDirty clears each flag as it packs — but the comparison in SynchedEntityData.set is against the current value, not the last one sent, so setting A then B then A within a tick dirties the item twice and then sends A, a value the client already had. The only thing that never dirties is setting a slot to what it already holds, and even that can be overridden: SynchedEntityData.set has a three-argument force-dirty form that skips the comparison. Display uses it to restart an interpolation whose delay was re-set to the same number; CopperGolem’s two uses of it are redundant, because the value it writes is always the previous weather state and so always different.

Five more channels, all keyed by the same entity id

Synched data is one of six clientbound descriptions of an entity, and knowing which one a fact travels on answers most why does the client not know that questions. There is no serverbound counterpart to any of them. Nothing stops SynchedEntityData.set on the client — LocalPlayer prediction does it constantly — but the write goes nowhere: ServerEntity.sendDirtyEntityData is the only caller of SynchedEntityData.packDirty in the whole tree, so the client’s dirty flag is read by nobody and the next server value overwrites the slot (authority). One serverbound packet moves two slots by proxy all the same: ServerboundClientInformationPacket reaches ServerPlayer.updateOptions, which sets the skin-customisation byte and the main hand. And the client’s container is not useless to it — ClientPacketListener.handleRespawn copies the old player’s non-default values straight into the new one when the respawn keeps entity data, the single client-to-client use of the channel.

channelpacketsnote
synched dataClientboundSetEntityDataPacketto trackers and self, when the entity is a player — ServerEntity.Synchronizer.sendToTrackingPlayersAndSelf
attributesClientboundUpdateAttributesPacketflushed by the same ServerEntity.sendDirtyEntityData (attributes)
equipmentClientboundSetEquipmentPacketincremental updates bypass ServerEntity entirely — LivingEntity.handleEquipmentChanges sends them, to trackers only, not the wearer
mob effectsClientboundUpdateMobEffectPacket, ClientboundRemoveMobEffectPacketthe authoritative list, unlike the swirl in LivingEntity.DATA_EFFECT_PARTICLES
position and motionClientboundMoveEntityPacket, ClientboundEntityPositionSyncPacket, ClientboundRotateHeadPacket, ClientboundSetEntityMotionPacket, ClientboundMoveMinecartPacketthe block the synched-data gate shares; ClientboundTeleportEntityPacket is the exception, sent from Entity itself
one-shot eventsClientboundEntityEventPacketa single byte from ServerLevel.broadcastEntityEvent, dispatched by Entity.handleEntityEvent — EntityEvent declares 64 of them

Pairing a viewer who has never seen it

Pairing a new viewer runs the same machinery once. ServerEntity.addPairing calls ServerEntity.sendPairingData, which bundles ClientboundAddEntityPacket with a ClientboundSetEntityDataPacket, attributes, equipment, passengers and a leash link into one ClientboundBundlePacket. The data packet is built not from a fresh pack but from ServerEntity.trackedDataValues, a snapshot taken in the ServerEntity constructor from SynchedEntityData.getNonDefaultValues and refreshed only when SynchedEntityData.packDirty later returns something. Two consequences: an entity still entirely at its defaults sends no data packet on pairing at all, and a change still waiting behind a closed gate is not in that cache, so a new viewer is paired with the older snapshot and gets the change with everyone else on the next flush.

Defaults never travel on pairing, in other words, because SynchedEntityData.getNonDefaultValues skips any item still equal to its SynchedEntityData.DataItem.initialValue — which is why a freshly loaded entity is described by so little. Both sides construct their own defaults independently from the same class chain, and if they ever disagreed no packet would correct it.

Where to look

ClassTreeIdRegistry is one short class and it is where the numbering scheme lives; read it before anything else, then SynchedEntityData itself and its SynchedEntityData.Builder for how the array is filled and checked. EntityDataAccessor and EntityDataSerializers are the key and its other half. On the sending side the whole gate is ServerEntity.sendChanges, with ServerEntity.sendDirtyEntityData as its exit and ServerEntity.sendPairingData, reached from ServerEntity.addPairing, as the pairing path; on the receiving side, ClientPacketListener.handleSetEntityData. Sheep.mobInteract is the scenario’s own method, three lines of which are this whole page. One door worth opening: Display, the class that reads accessor ids as values.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.