Skip to content

Client Synchronization

The client synchronization system replicates server state to connected clients. It uses delta encoding to send only changed data and visibility culling to reduce bandwidth.

  • Entity Tracker: com.hypixel.hytale.server.core.modules.entity.tracker
  • Chunk Systems: com.hypixel.hytale.server.core.universe.world.chunk.systems
  • Protocol: com.hypixel.hytale.protocol.packets
flowchart TB
    subgraph Pipeline["Synchronization Pipeline"]
        StateChange["State Change<br/>(Component)"]
        Visibility["Visibility<br/>Detection"]
        QueueUpdates["Queue Updates<br/>(Delta Encoding)"]
        SendPackets["Send Packets<br/>(Batch Send)"]
        Client["Client"]

        StateChange --> Visibility
        Visibility --> QueueUpdates
        QueueUpdates --> SendPackets
        SendPackets --> Client
    end

Each networked entity has a unique identifier:

package com.hypixel.hytale.server.core.modules.entity.tracker;
public class NetworkId {
private int id; // Unique network identifier
}

The system tracks which entities are visible to each player:

ComponentDescription
EntityViewerTracks visible entities for a player
VisibleTracks which players can see an entity

Entity synchronization runs in ordered system groups:

GroupPurpose
FIND_VISIBLE_ENTITIES_GROUPSpatial queries for visibility
QUEUE_UPDATE_GROUPQueue component changes
SEND_PACKET_GROUPSend packets to clients
  1. State Change: Entity component is modified
  2. Mark Dirty: EffectControllerComponent marks entity as “network outdated”
  3. Visibility Check: Systems determine which players can see the entity
  4. Queue Update: QueueComponentUpdates creates delta update objects
  5. Build Packet: Updates collected into EntityUpdates packet
  6. Send: Packet sent to player clients

The main packet for entity state synchronization:

// Packet ID: 161 (compressed)
public class EntityUpdates {
int[] removed; // Entity IDs to remove
EntityUpdate[] updates; // State updates
}
public class EntityUpdate {
int networkId; // Entity network ID
ComponentUpdateType[] removed; // Components to remove
ComponentUpdate[] updates; // Component updates
}

26 component types can be synchronized:

Type IDClassDescription
0NameplateUpdateDisplay names
1UIComponentsUpdateHealth bars, indicators
2CombatTextUpdateDamage numbers
3ModelUpdateEntity model
4PlayerSkinUpdatePlayer skin data
5ItemUpdateHeld/displayed item
6BlockUpdateBlock-form entity
7EquipmentUpdateEquipment loadout
8EntityStatsUpdateStats and attributes
9TransformUpdatePosition, rotation, scale
10MovementStatesUpdateMovement flags
11EntityEffectsUpdateActive effects/buffs
12InteractionsUpdateInteraction options
13DynamicLightUpdateLight emission
14InteractableUpdateCan be interacted with
15IntangibleUpdateNo collision (marker)
16InvulnerableUpdateCannot be damaged (marker)
17RespondToHitUpdateHit response behavior (marker)
18HitboxCollisionUpdateCollision settings
19RepulsionUpdateEntity repulsion
20PredictionUpdateClient prediction ID
21AudioUpdateAttached audio
22MountedUpdateMount state
23NewSpawnUpdateNewly spawned flag (marker)
24ActiveAnimationsUpdatePlaying animations
25PropUpdateProp marker (marker)

ComponentUpdate is now an abstract base class. Each subclass carries only its own fields and is dispatched via a VarInt type ID:

public abstract class ComponentUpdate {
public abstract void serialize(ByteBuf buf);
public abstract int computeSize();
public void serializeWithTypeId(ByteBuf buf);
public int computeSizeWithTypeId();
public static ComponentUpdate deserialize(ByteBuf buf, int offset);
}

Sends complete chunk data to clients:

// Packet ID: 131 (compressed, max 12MB)
public class SetChunk {
int x, y, z; // Chunk coordinates
byte[] localLight; // Local light data
byte[] globalLight; // Global light data
byte[] data; // Block data
}

Removes chunk from client:

// Packet ID: 135 (8 bytes)
public class UnloadChunk {
int chunkX;
int chunkZ;
}

Single block changes:

// Packet ID: 140
public class ServerSetBlock {
int x, y, z; // Block coordinates
int blockId; // Block type ID
byte filler; // Filler data
byte rotation; // Block rotation
}

Batch block changes:

// Packet ID: 141
public class ServerSetBlocks {
BlockChange[] changes; // Up to 1024 per packet
}
// Packet ID: 142
public class ServerSetFluid {
int x, y, z;
int fluidId;
byte level;
}
// Packet ID: 143
public class ServerSetFluids {
FluidChange[] changes;
}

The view radius controls how far clients can see:

// Packet ID: 32
public class ViewRadius {
int radius; // Chunk distance
}

Entities and chunks outside the view radius are not synchronized.

Only changed components are sent:

  1. Full update on first visibility
  2. Incremental updates thereafter
  3. Remove updates when component is removed

Entities outside view are not synchronized:

  1. Spatial queries determine visible entities
  2. Only visible entities queued for updates
  3. Removed entities send removal packet

Updates are batched for efficiency:

  1. Changes queued during tick
  2. Batch built at end of tick
  3. Single packet per player per frame

Large packets use Zstd compression:

  • SetChunk - Compressed (up to 12MB uncompressed)
  • EntityUpdates - Compressed
  • Reduces bandwidth significantly

When a player joins:

  1. SetClientId - Assign client identifier
  2. ViewRadius - Set chunk loading distance
  3. SetChunk - Send nearby chunks
  4. EntityUpdates - Send visible entities

Continuous synchronization:

  1. Entity state changes detected
  2. Chunk modifications tracked
  3. Updates batched per tick
  4. Packets sent to affected players

New entities:

  1. Visibility system detects new entity
  2. Full component update queued
  3. EntityUpdates with NewSpawn flag sent

Removed entities:

  1. Entity destroyed or leaves visibility
  2. Network ID added to removed array
  3. EntityUpdates with removal sent

Use existing packet types through PacketHandler:

// Send entity update
EntityUpdates packet = new EntityUpdates();
packet.updates = new EntityUpdate[] { update };
player.getPacketHandler().write(packet);
// Send block change
ServerSetBlock blockPacket = new ServerSetBlock();
blockPacket.x = x;
blockPacket.y = y;
blockPacket.z = z;
blockPacket.blockId = blockId;
player.getPacketHandler().write(blockPacket);

Modify components to trigger automatic sync:

// Component changes automatically queue network updates
TransformComponent transform = entity.getComponent(transformType);
transform.setPosition(newPosition);
// Entity tracker will detect change and sync
  1. Minimize state changes: Frequent changes increase bandwidth
  2. Use appropriate update types: Don’t send full updates when delta suffices
  3. Respect view radius: Don’t sync entities outside player view
  4. Batch operations: Group related changes when possible
  5. Consider compression: Large data benefits from compression