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
┌─────────────────────────────────────────────────────────────────┐
│ Synchronization Pipeline │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ State Change │───►│ Visibility │───►│ Queue Updates │ │
│ │ (Component) │ │ Detection │ │ (Delta Encoding) │ │
│ └──────────────┘ └──────────────┘ └────────┬─────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ SendPackets │ │
│ │ (Batch Send) │ │
│ └─────────┬─────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ Client │ │
│ └───────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

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
}

25 component types can be synchronized:

TypeDescription
NameplateDisplay names
UIComponentsHealth bars, indicators
CombatTextDamage numbers
ModelEntity model
PlayerSkinPlayer skin data
ItemHeld/displayed item
BlockBlock-form entity
EquipmentEquipment loadout
EntityStatsStats and attributes
TransformPosition, rotation, scale
MovementStatesMovement flags
EntityEffectsActive effects/buffs
InteractionsInteraction options
DynamicLightLight emission
InteractableCan be interacted with
IntangibleNo collision
InvulnerableCannot be damaged
RespondToHitHit response behavior
HitboxCollisionCollision settings
RepulsionEntity repulsion
PredictionClient prediction ID
AudioAttached audio
MountedMount state
NewSpawnNewly spawned flag
ActiveAnimationsPlaying animations

Each ComponentUpdate can contain:

public class ComponentUpdate {
// Display
Nameplate nameplate;
EntityUIComponent[] uiComponents;
CombatText combatText;
// Visuals
Model model;
PlayerSkin playerSkin;
Item item;
Block block;
Equipment equipment;
// State
EntityStats entityStats;
Transform transform;
MovementStates movementStates;
EntityEffect[] entityEffects;
// Interactions
Interaction[] interactions;
boolean interactable;
boolean intangible;
boolean invulnerable;
// Physics
HitboxCollision hitboxCollision;
Repulsion repulsion;
int predictionId;
// Effects
DynamicLight dynamicLight;
Audio audio;
boolean mounted;
boolean newSpawn;
Animation[] activeAnimations;
}

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