Skip to content

Client-Server Synchronization

The interaction system uses a dual-path execution model where the client predicts interactions locally while the server remains authoritative. This page explains how synchronization works.

sequenceDiagram
    participant Client
    participant Server

    Client->>Client: simulateTick0()
    Note right of Client: Local prediction

    Client-->>Server: Input data

    Server->>Server: tick0()
    Note left of Server: Authoritative

    Server-->>Client: InteractionSyncData
    Note over Client,Server: state, progress, etc.

    Client->>Client: reconcile state
  • Responsiveness: Players see immediate feedback (animations, sounds)
  • Authority: Server validates all game-changing actions
  • Correction: Client state is corrected if prediction was wrong

Every interaction declares which side is authoritative:

package com.hypixel.hytale.protocol;
public enum WaitForDataFrom {
None, // No synchronization needed
Client, // Server waits for client data
Server // Client waits for server data
}
ValueUse CaseExample
NoneSelf-contained logicSerialInteraction, ConditionInteraction
ClientClient determines outcomeChargingInteraction (release timing), ChainingInteraction (combo index)
ServerServer determines outcomeDamageEntityInteraction (hit validation), PlaceBlockInteraction
public class ChargingInteraction extends Interaction {
@Override
public WaitForDataFrom getWaitForDataFrom() {
return WaitForDataFrom.Client;
// Server waits for client to report charge value
}
}
public class DamageEntityInteraction extends Interaction {
@Override
public WaitForDataFrom getWaitForDataFrom() {
return WaitForDataFrom.Server;
// Client waits for server to validate hit
}
}

Data synchronized between client and server:

package com.hypixel.hytale.protocol;
public class InteractionSyncData {
// Current interaction state
public InteractionState state;
// Progress through the interaction (0.0 to 1.0 or time in seconds)
public float progress;
// For charging interactions - how long charged
public float chargeValue;
// For chaining interactions - which combo step
public int chainingIndex;
// For parallel forks - count per interaction type
public Map<InteractionType, Integer> forkCounts;
// Which root interaction was entered (for nested chains)
public int enteredRootInteraction;
// Operation counter at this state
public int operationCounter;
// Root interaction ID
public int rootInteraction;
// Flag index for conditional branches
public int flagIndex;
}
// ChargingInteraction example
// Client sends charge value when player releases button
// In simulateTick0() (client)
if (playerReleasedButton) {
context.getState().chargeValue = currentChargeTime;
context.getState().state = InteractionState.Finished;
}
// In tick0() (server)
InteractionSyncData clientData = context.getClientState();
float chargeValue = clientData.chargeValue;
// Server uses client's charge value to determine tier
@Override
protected void tick0(..., InteractionContext context, ...) {
// Get this interaction's state (to write to)
InteractionSyncData state = context.getState();
// Get client-sent state (read only, may be null)
InteractionSyncData clientState = context.getClientState();
// Get server state (for reference)
InteractionSyncData serverState = context.getServerState();
}
@Override
protected void tick0(..., InteractionContext context, ...) {
InteractionSyncData clientData = context.getClientState();
// Always null-check client state
if (clientData == null) {
context.getState().state = InteractionState.NotFinished;
return; // Wait for client data
}
// Use client data
if (clientData.state == InteractionState.Finished) {
// Client reported completion
}
}

Sent to nearby players to show interaction effects:

// From Interaction.handle()
PlayInteractionFor packet = new PlayInteractionFor(
entityNetworkId, // Who is performing
chainId, // Chain identifier
forkedChainId, // Fork identifier (if forked)
operationIndex, // Which operation in chain
assetId, // Interaction asset ID
itemId, // Item being used
interactionType, // Primary/Secondary/etc
cancel // Whether to cancel
);
// Sent to all players within viewDistance
for (Ref<EntityStore> playerRef : nearbyPlayers) {
if (!chain.requiresClient() || !playerRef.equals(owningEntityRef)) {
playerPlayerRefComponent.getPacketHandler().writeNoCache(packet);
}
}

Synchronizes chain state:

// For fork synchronization
void syncFork(
Ref<EntityStore> ref,
InteractionManager manager,
SyncInteractionChain packet
) {
ForkedChainId baseId = packet.forkedId;
// Navigate to correct fork
while (baseId.forkedId != null) {
baseId = baseId.forkedId;
}
InteractionChain fork = this.findForkedChain(baseId, packet.data);
if (fork != null) {
manager.sync(ref, fork, packet);
}
}
// From InteractionChain.java
public void flagDesync() {
this.desynced = true;
// Also flag all forked chains
this.forkedChains.forEach((k, c) -> c.flagDesync());
}
public boolean isDesynced() {
return this.desynced;
}
  1. Network Latency - Client action before server response
  2. State Prediction Miss - Client predicted wrong outcome
  3. Invalid Client Data - Tampered or corrupted packets
  4. Race Conditions - Multiple interactions on same entity
// Clear sync data on desync
void clearInteractionSyncData(int operationIndex) {
// Remove temp sync data from this point forward
for (int end = tempSyncData.size() - 1; end >= tempIdx && end >= 0; end--) {
tempSyncData.remove(end);
}
// Clear client state from affected entries
for (int i = Math.max(idx, 0); i < interactions.size(); i++) {
interactions.get(i).setClientState(null);
}
}

Interactions declare if they need network sync:

public abstract boolean needsRemoteSync();
// Examples from source:
// SerialInteraction - No sync needed (just orchestration)
@Override
public boolean needsRemoteSync() {
return false;
}
// ChargingInteraction - Needs sync (charge timing)
@Override
public boolean needsRemoteSync() {
return true;
}
// ParallelInteraction - Needs sync (fork coordination)
@Override
public boolean needsRemoteSync() {
return true;
}

Interactions have a configurable view distance for network packets:

// From Interaction.java
protected double viewDistance = 96.0;
// Used when sending PlayInteractionFor
SpatialResource<Ref<EntityStore>, EntityStore> playerSpatialResource =
commandBuffer.getResource(EntityModule.get().getPlayerSpatialResourceType());
ObjectList<Ref<EntityStore>> results = SpatialResource.getThreadLocalReferenceList();
playerSpatialResource.getSpatialStructure().collect(position, this.viewDistance, results);

Configure in JSON:

{
"Type": "Simple",
"ViewDistance": 64.0,
"Effects": { ... }
}
InteractionSyncData clientData = context.getClientState();
if (clientData == null) {
// Wait for data
context.getState().state = InteractionState.NotFinished;
return;
}
// Don't trust client values blindly
float chargeValue = clientData.chargeValue;
chargeValue = Math.max(0, Math.min(chargeValue, maxChargeTime));
// Timing-sensitive → Client
// Result-sensitive → Server
// No data needed → None
// Allow small timing differences
if (Math.abs(serverTime - clientTime) < 0.1f) {
// Accept client timing
}
// Only sync when state changes
if (previousState != currentState) {
sendSync();
}