Files
Documentation/content/advanced/networking/sync.fr.md
2026-01-20 20:33:59 +01:00

12 KiB

title, type, weight
title type weight
Synchronization docs 2

Hytale uses an efficient synchronization system to keep game state consistent between the server and all connected clients. This page explains how entity and world data is synchronized and how to work effectively with the sync system.


Overview

The synchronization system handles:

  • Entity State: Position, rotation, velocity, health, equipment
  • World State: Block changes, chunk loading/unloading
  • Player Data: Inventory, effects, permissions
  • Game Events: Particles, sounds, animations

{{< callout type="info" >}} Synchronization is automatic for most operations. The server tracks what needs to be sent to each client and batches updates efficiently. {{< /callout >}}


The Network Outdated Pattern

Hytale uses a "dirty flag" pattern called isNetworkOutdated to track which components need synchronization:

// Simplified component sync interface
public interface NetworkSyncable {
    /**
     * Check if this component has changes that need to be sent to clients
     */
    boolean isNetworkOutdated();

    /**
     * Mark this component as having pending changes
     */
    void markNetworkOutdated();

    /**
     * Clear the outdated flag after changes have been sent
     */
    void clearNetworkOutdated();
}

How It Works

┌─────────────────────────────────────────────────────────────┐
│                    Update Cycle                              │
├─────────────────────────────────────────────────────────────┤
│  1. Game Logic          2. Mark Outdated    3. Sync Phase   │
│  ┌─────────────┐        ┌─────────────┐     ┌─────────────┐ │
│  │ entity.set  │   →    │ component.  │  →  │ Send to     │ │
│  │ Position()  │        │ markNetwork │     │ clients     │ │
│  │             │        │ Outdated()  │     │             │ │
│  └─────────────┘        └─────────────┘     └──────┬──────┘ │
│                                                     │        │
│  4. Clear Flag                                      │        │
│  ┌─────────────┐                                    │        │
│  │ clearNetwork│   ←────────────────────────────────┘        │
│  │ Outdated()  │                                             │
│  └─────────────┘                                             │
└─────────────────────────────────────────────────────────────┘

Entity Synchronization

Components That Sync

Component Sync Trigger Data Sent
TransformComponent Position/rotation change x, y, z, pitch, yaw, roll
Velocity Velocity change vx, vy, vz
Health Health change current, max
DisplayNameComponent Name change display name
EntityScaleComponent Scale change scale factor
ActiveAnimationComponent Animation change animation state

Automatic Sync

Most entity modifications automatically trigger synchronization:

// All of these automatically sync to clients
entity.setPosition(new Vector3d(100, 64, 200));
entity.setVelocity(new Vector3d(0, 1, 0));

Player player = (Player) entity;
player.setHealth(10.0f);

Manual Component Updates

When modifying components directly, the sync flag is managed automatically:

// Getting a component and modifying it
TransformComponent transform = entity.getComponent(TransformComponent.class);
if (transform != null) {
    // Internal methods handle markNetworkOutdated()
    transform.setPosition(newPosition);
}

// Setting a new component
entity.setComponent(DisplayNameComponent.class,
    new DisplayNameComponent(Message.raw("Custom Name")));

World Synchronization

Block Updates

Block changes are synchronized to players who have the relevant chunks loaded:

// Single block change
world.setBlock(position, blockType);

// The server:
// 1. Updates the block in world data
// 2. Marks the chunk as having pending updates
// 3. On next sync tick, sends BlockChange to relevant players

Chunk Loading

Chunks are sent to players based on view distance:

// When a player moves or joins
// 1. Server calculates which chunks should be visible
// 2. New chunks are queued for sending
// 3. Chunks out of range are marked for unloading

// Events you can listen to:
@Subscribe
public void onChunkLoad(ChunkPreLoadProcessEvent event) {
    // Chunk is about to be loaded for processing
}

@Subscribe
public void onChunkUnload(ChunkUnloadEvent event) {
    // Chunk is being unloaded
}

@Subscribe
public void onChunkSave(ChunkSaveEvent event) {
    // Chunk is being saved
}

View Distance

// Players receive updates based on their view distance
// This is configured server-side

// Particles have a fixed distance
public static final int DEFAULT_PARTICLE_DISTANCE = 75;

// Entity sync uses view distance (configurable)
// Block sync uses chunk loading range

Player Data Synchronization

Inventory Sync

Inventory changes sync automatically:

Player player = ...;
Inventory inventory = player.getInventory();

// These trigger sync automatically
inventory.addItem(itemStack);
inventory.removeItem(slot);
inventory.setItem(slot, itemStack);

Effects Sync

Status effects are synchronized when added or removed:

// Adding an effect syncs to the player
player.addEffect(effect, duration, amplifier);

// Removing syncs the removal
player.removeEffect(effectType);

// Clearing all effects
player.clearEffects();

Performance Optimization

Batching

The server automatically batches updates:

{{< tabs items="How Batching Works,Example" >}} {{< tab >}}

Updates are collected throughout each tick and sent together:

Tick Start
    │
    ├── Game Logic: entity1.setPosition(...)
    ├── Game Logic: entity2.setPosition(...)
    ├── Game Logic: entity3.setHealth(...)
    ├── Game Logic: world.setBlock(...)
    ├── Game Logic: world.setBlock(...)
    │
Tick End
    │
    └── Sync Phase: Send all updates in batched packets

Benefits:

  • Fewer packets sent
  • Lower network overhead
  • More efficient compression
  • Reduced client processing

{{< /tab >}} {{< tab >}}

// This code results in ONE batched update, not 100 individual packets
public void updateManyBlocks(World world) {
    for (int x = 0; x < 10; x++) {
        for (int z = 0; z < 10; z++) {
            world.setBlock(
                new Vector3i(baseX + x, 64, baseZ + z),
                blockType
            );
        }
    }
    // All 100 block changes are batched and sent together
}

{{< /tab >}} {{< /tabs >}}

Delta Compression

Only changed data is sent:

// If an entity only moves (position changes)
// Only position data is sent, not health, inventory, etc.

entity.setPosition(newPosition);
// Sends: position update only

entity.setHealth(newHealth);
// Sends: health update only

Relevancy Filtering

Updates are only sent to relevant clients:

// Entity updates go to players tracking that entity
// (players within view distance)

// Block updates go to players with that chunk loaded

// Sound effects respect distance
world.playSound(soundEvent, position, volume, pitch);
// Only players within hearing range receive this

Custom Synchronization

Creating Synced Data

If you need to sync custom data, use player metadata or entity components:

// Using player metadata (persisted per-player)
public class CustomSyncPlugin extends ServerPlugin {

    private static final String CUSTOM_DATA_KEY = "myplugin:custom_data";

    public void setCustomData(Player player, int value) {
        player.setMetadata(CUSTOM_DATA_KEY, value);
        // Send update to player
        sendCustomDataUpdate(player, value);
    }

    public int getCustomData(Player player) {
        Object data = player.getMetadata(CUSTOM_DATA_KEY);
        return data instanceof Integer ? (Integer) data : 0;
    }

    private void sendCustomDataUpdate(Player player, int value) {
        // Use messages to inform client of data changes
        // (custom client mod required to interpret this)
        player.sendMessage(Message.raw("")); // or use a custom channel
    }
}

Entity Visibility Control

Control which entities players can see:

// Hide an entity from a specific player
entity.setVisibleTo(player, false);

// Show a previously hidden entity
entity.setVisibleTo(player, true);

// Check visibility
boolean canSee = entity.isVisibleTo(player);

Debugging Sync Issues

{{< tabs items="Common Issues,Diagnostic Tools" >}} {{< tab >}}

Entity Not Appearing:

// Check if entity is in a loaded chunk
Chunk chunk = world.getChunk(entity.getChunkPosition());
if (chunk == null || !chunk.isLoaded()) {
    log.warn("Entity in unloaded chunk!");
}

// Check visibility
if (!entity.isVisibleTo(player)) {
    log.warn("Entity hidden from player!");
}

// Check distance
double distance = entity.getPosition().distanceTo(player.getPosition());
if (distance > viewDistance) {
    log.warn("Entity out of view distance: " + distance);
}

Block Changes Not Syncing:

// Verify chunk is loaded for player
Vector3i chunkPos = new Vector3i(
    blockPos.x() >> 4,
    0,
    blockPos.z() >> 4
);

// Check if player has chunk loaded
// (implementation depends on server API)

Delayed Updates:

// Updates are sent at end of tick
// If you need immediate sync, there's usually no way to force it
// Design your code to work with batched updates

{{< /tab >}} {{< tab >}}

Logging Sync Activity:

@Subscribe
public void onEntityMove(EntityMoveEvent event) {
    log.debug("Entity {} moved to {}",
        event.getEntity().getUuid(),
        event.getNewPosition());
}

@Subscribe
public void onBlockChange(BlockChangeEvent event) {
    log.debug("Block at {} changed to {}",
        event.getPosition(),
        event.getNewBlockType());
}

Monitoring Sync Timing:

// Track update frequency for an entity
private final Map<UUID, Long> lastUpdateTime = new HashMap<>();

public void onEntityUpdate(Entity entity) {
    long now = System.currentTimeMillis();
    Long last = lastUpdateTime.put(entity.getUuid(), now);

    if (last != null) {
        long delta = now - last;
        if (delta < 50) { // Less than 1 tick
            log.warn("Rapid updates on entity {}: {}ms apart",
                entity.getUuid(), delta);
        }
    }
}

{{< /tab >}} {{< /tabs >}}


Best Practices

{{< callout type="info" >}} Summary of best practices:

  1. Trust automatic sync - Don't try to manually trigger synchronization
  2. Batch your updates - Make multiple changes within a single tick when possible
  3. Use events - Listen for sync-related events to react to state changes
  4. Consider view distance - Don't waste resources updating entities far from players
  5. Handle disconnections - Clean up player-specific sync data when they leave {{< /callout >}}
// Example: Efficient entity spawning with proper sync
public class EfficientSpawnPlugin extends ServerPlugin {

    public void spawnManyEntities(World world, Vector3d center, int count) {
        // Spawn all entities in one tick - they'll be batched
        for (int i = 0; i < count; i++) {
            Vector3d offset = new Vector3d(
                Math.random() * 10 - 5,
                0,
                Math.random() * 10 - 5
            );

            Entity entity = world.spawnEntity(
                entityType,
                center.add(offset)
            );

            // Configure entity (still same tick, still batched)
            entity.setComponent(DisplayNameComponent.class,
                new DisplayNameComponent(Message.raw("Entity #" + i)));
        }
        // All spawns sent in one batched update at end of tick
    }
}