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.2 · 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, confined to that side’s main 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 loads the class
EntityDataSerializersthe 43 registered serializers and the wire id of each, in registration ordera static block, once
ServerEntitywhether this entity sends anything this tick, and whatthe server main 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 client main 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 SheepEntityLivingEntityMobPathfinderMobAgeableMobAnimalSheep, 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

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 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 index the array with no bounds check at all.

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 that every call site inside Entity ignores in favour of a bare integer. 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.

The numbering belongs to the class, not to the concept. Avatar — the class 26.2 inserts between LivingEntity and Player — 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.

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 defends the container against a caller mutating a value it already handed over. EntityDataSerializer.ForValueType is the immutable case, where copying is identity, and EntityDataSerializer.forValueType builds one from a codec alone.

EntityDataSerializers registers 43 of them into a CrudeIncrementalIntIdentityHashBiMap, from a single static block, so registration order is the wire idEntityDataSerializers.BYTE is 0, EntityDataSerializers.POSE is 20, EntityDataSerializers.HUMANOID_ARM is 42 and last. Several of them carry Holders of data-pack registries — the per-species variants, painting variants, resolvable profiles — which is 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 43 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.

The trace: a sheep is sheared

sequenceDiagram
    participant MPGM as MultiPlayerGameMode
    participant SGPL as ServerGamePacketListenerImpl
    participant Sheep as Sheep
    participant SED as SynchedEntityData
    participant CM as ChunkMap
    participant SE as ServerEntity
    participant CPL as ClientPacketListener

    MPGM->>MPGM: predicts locally with Player.interactOn, unless spectator
    MPGM->>SGPL: ServerboundInteractPacket(entity id, hand, relative location, secondary)
    Note over SGPL: server tick, before MinecraftServer.tickServer runs
    SGPL->>SGPL: Entity.setShiftKeyDown from the packet flag, then range and border checks
    SGPL->>Sheep: Player.interactOn to Entity.interact to Mob.interact
    Sheep->>Sheep: checkAndHandleImportantInteractions, then Entity.interact, then Sheep.mobInteract
    Sheep->>Sheep: Sheep.shear — sound, then dropFromShearingLootTable, then setSheared
    Sheep->>SED: SynchedEntityData.set(Sheep.DATA_WOOL_ID, bit four set)
    SED->>Sheep: Entity.onSyncedDataUpdated, then DataItem.setDirty and isDirty
    Note over SGPL,SE: same tick, ServerLevel.tick chunkSource phase
    CM->>SE: ChunkMap.tick reaches this sheep, ServerEntity.sendChanges
    SE->>SED: isDirty opens the gate, then SynchedEntityData.packDirty
    SED-->>SE: one DataValue — id 18, serializer 0, one payload byte
    SE->>CPL: ClientboundSetEntityDataPacket, queued now and flushed at the end of the tick
    Note over SED: one container per side — the server's above, the client's below
    CPL->>SED: handleSetEntityData to assignValues, per item then the batch
    Note over CPL: next frame — SheepRenderer.extractRenderState reads Sheep.isSheared

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. 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. 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. Both the pack and the unpack side write and test the literal, incidentally, so the public ClientboundSetEntityDataPacket.EOF_MARKER is referenced by nothing, exactly like the private SynchedEntityData.MAX_ID_VALUE beside it.

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. Nothing tells the renderer. Next frame LevelExtractor walks the visible entities, EntityRenderDispatcher.extractEntity builds a SheepRenderState, and SheepRenderer.extractRenderState copies Sheep.isSheared and Sheep.getColor into it, 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

flowchart TD
    IN["ChunkMap.tick: section changed, or Entity.needsSync, or the chunk is in entity-ticking range"] --> SC["ServerEntity.sendChanges, opening with Entity.updateDataBeforeSync"]
    SC -->|"an ItemFrame, every tenth tick — the map bypass"| DATA["ServerEntity.sendDirtyEntityData"]
    SC --> GATE{"tickCount is a multiple of EntityType.updateInterval, or Entity.needsSync, or SynchedEntityData.isDirty"}
    GATE -->|"yes"| SEND["position, rotation and motion"] --> DATA
    GATE -->|"no"| HOLD["nothing goes out, and the dirty flags survive to the next tick"]

Two tests stand between a dirty byte and the wire. ChunkMap.tick decides whether ServerEntity.sendChanges is called at all; an entity that is tracked but outside entity-ticking range, and not moving between sections, simply is not asked, and keeps its dirty data until one of the three conditions becomes true. Inside ServerEntity.sendChanges, the interval gate covers the position block and the usual ServerEntity.sendDirtyEntityData call, which is why shearing a sheep also sends that sheep’s position delta this tick: synched data is, incidentally, a latency channel for movement.

The interval comes from EntityType.updateInterval, fixed when ChunkMap.TrackedEntity constructs the ServerEntity. EntityTypes.PLAYER sets 2 and EntityType.Builder defaults to 3. Thirty-seven types set the interval explicitly, most of them at 10 or 20 — and seven of those, item frames, paintings, leash knots and their kin, set Integer.MAX_VALUE.

Integer.MAX_VALUE — the update interval of EntityTypes.ITEM_FRAME, which is to say its interval branch never fires again after tick zero.

That is exactly why ServerEntity.sendChanges has an ItemFrame special case that calls ServerEntity.sendDirtyEntityData every tenth tick before the gate: it is the only path to the synched-data flush that skips the interval test, and without it a map in a frame would update only when something else set Entity.needsSync. (Two sends also sit outside the gate — the passengers packet and the Entity.hurtMarked 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: it realigns the tracker’s own counter so the very next evaluation lands on a multiple of the interval, which works even when that interval is Integer.MAX_VALUE.

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.

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. The client cannot write to the channel directly — though one of its packets does move two slots by proxy: ServerboundClientInformationPacket reaches ServerPlayer.updateOptions, which sets the skin-customisation byte and the main hand.

channelpacketsnote
synched dataClientboundSetEntityDataPacketto trackers and selfServerEntity.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.handleEntityEventEntityEvent declares 62 of them

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 any change made while the tracker sat outside its send gate is already folded into that cache.

Questions players ask

Why is a freshly loaded entity described by so little? Because defaults never travel. SynchedEntityData.getNonDefaultValues skips any item still equal to its SynchedEntityData.DataItem.initialValue, so pairing describes only what has changed. Both sides construct their own defaults independently; if they ever disagreed, no packet would correct it.

If a value changes twice in a tick, do I get two packets? No — and not always for the reason you would guess. SynchedEntityData.packDirty clears each flag as it packs, so one flush carries one value per slot. But the comparison in SynchedEntityData.set is against the current value, not the last one sent: 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, because SynchedEntityData.set has a three-argument force-dirty form that skips the comparison entirely. 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.

Why does my client-side change never reach the server? Nothing stops SynchedEntityData.set on the client — LocalPlayer prediction does it constantly — but ServerEntity.sendDirtyEntityData is the only caller of SynchedEntityData.packDirty in the whole tree. The client’s dirty flag is read by no one, and the next server value overwrites the slot (authority). The container is not useless to the client, though: ClientPacketListener.handleRespawn copies the old player’s non-default values straight into the new one, the single client-to-client use of the channel.

Can two mods both add a field to LivingEntity? Only by accident. Ids are ordinals, so inserting, removing or reordering a SynchedEntityData.defineId on a base class shifts every id below it in every subclass. Two mods claiming the same number either collide in SynchedEntityData.Builder.define, which rejects a duplicate, or land a value in the wrong slot and throw the serializer check on the client. SynchedEntityData.Builder.define also has an off-by-one in its bounds test — an id exactly equal to the array length slips past and dies on the array write instead.

Does any of this affect the physics the client simulates? One value does. Entity.DATA_POSE travels on its own EntityDataSerializers.POSE, and Entity.onSyncedDataUpdated turns an incoming pose into Entity.refreshDimensions — a synched value resizing a hitbox on both sides. Everything else on the channel is cosmetic to the client, or read back by gameplay code that already knew.

Two details that are only visible from the whole tree. The batch overload SyncedDataHolder.onSyncedDataUpdated, fired by SynchedEntityData.assignValues after every packet, is overridden by nothing in 7,055 classes — it is the only place a client could see a whole update atomically, and it is dead. And Display is the one class that treats accessor ids as values: Display.RENDER_STATE_IDS is an int set of the eight ids that force a render-state rebuild, tested against each incoming accessor, with Display.TextDisplay keeping a second set of its own. It is the sharpest demonstration in the codebase that these numbers really are ordinals.

Where to look

SynchedEntityData · SynchedEntityData.Builder · SynchedEntityData.DataItem · SynchedEntityData.DataValue · EntityDataAccessor · EntityDataSerializer · EntityDataSerializers · ClassTreeIdRegistry · Entity.defineSynchedData · Entity.onSyncedDataUpdated · Entity.updateDataBeforeSync · ServerEntity.sendChanges · ServerEntity.sendDirtyEntityData · ServerEntity.sendPairingData · ChunkMap.tick · ChunkMap.TrackedEntity · ClientboundSetEntityDataPacket · ClientPacketListener.handleSetEntityData · Sheep.mobInteract · Shearable


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