home / skills / mnkyarts / hytale-skills / hytale-networking

hytale-networking skill

/skills/hytale-networking

npx playbooks add skill mnkyarts/hytale-skills --skill hytale-networking

Review the files below or copy the command above to add this skill to your agents.

Files (3)
SKILL.md
15.5 KB
---
name: hytale-networking
description: Handle Hytale network packets, create custom packets, and implement client-server communication. Use when asked to "create custom packets", "handle network messages", "send data to client", "receive client data", or "implement networking".
metadata:
  author: Liam Robinson (MnkyArts)
  version: "1.0.0"
---

# Hytale Networking API

Complete guide for handling network packets and client-server communication.

## When to use this skill

Use this skill when:
- Sending data to clients
- Receiving data from clients
- Creating custom packet types
- Implementing custom UI synchronization
- Handling real-time player input
- Syncing custom game state

## Network Architecture Overview

Hytale uses a packet-based networking system:

```
Client <---> Server
   |           |
   Packet      Packet
   Encoder     Decoder
   |           |
   ByteBuf <-> ByteBuf
```

### Protocol Components

| Component | Description |
|-----------|-------------|
| `Packet` | Data structure with serialization |
| `PacketRegistry` | Maps packet IDs to classes |
| `PacketHandler` | Processes incoming packets |
| `PacketEncoder` | Serializes packets to bytes |
| `PacketDecoder` | Deserializes bytes to packets |

## Packet Structure

### Packet Interface

All packets implement the `Packet` interface:

```java
public interface Packet {
    int getId();
    void serialize(ByteBuf buffer);
    int computeSize();
}
```

### Built-in Packet Categories

| Category | Direction | Examples |
|----------|-----------|----------|
| `auth` | Bidirectional | AuthToken, ConnectAccept |
| `connection` | Bidirectional | Connect, Disconnect, Ping |
| `setup` | S→C | WorldSettings, AssetInitialize |
| `player` | C→S | ClientMovement, MouseInteraction |
| `entities` | S→C | EntityUpdates, PlayAnimation |
| `world` | S→C | SetChunk, ServerSetBlock |
| `inventory` | Bidirectional | MoveItemStack, SetActiveSlot |
| `window` | Bidirectional | OpenWindow, CloseWindow |
| `interface` | S→C | ChatMessage, Notification |
| `interaction` | Bidirectional | SyncInteractionChains |
| `camera` | S→C | CameraShakeEffect |

## Registering Packet Handlers

### SubPacketHandler Pattern

Create modular packet handlers:

```java
public class MyPacketHandler implements SubPacketHandler {
    private final MyPlugin plugin;
    
    public MyPacketHandler(MyPlugin plugin) {
        this.plugin = plugin;
    }
    
    @Override
    public void registerHandlers(IPacketHandler handler) {
        // Register by packet ID
        handler.registerHandler(108, this::handleClientMovement);
        handler.registerHandler(111, this::handleMouseInteraction);
        
        // Or by packet class
        handler.registerHandler(CustomPacket.ID, this::handleCustomPacket);
    }
    
    private void handleClientMovement(Packet packet) {
        ClientMovement movement = (ClientMovement) packet;
        Vector3d position = movement.getPosition();
        // Process movement
    }
    
    private void handleMouseInteraction(Packet packet) {
        MouseInteraction interaction = (MouseInteraction) packet;
        // Process mouse input
    }
}
```

### Registering in Plugin

```java
@Override
protected void setup() {
    // Register packet handler with server manager
    ServerManager serverManager = HytaleServer.get().getServerManager();
    serverManager.registerSubPacketHandler(new MyPacketHandler(this));
}
```

## Sending Packets

### Send to Single Player

```java
public void sendToPlayer(Player player, Packet packet) {
    player.getConnection().send(packet);
}
```

### Send to Multiple Players

```java
public void sendToAll(Packet packet) {
    for (Player player : HytaleServer.get().getOnlinePlayers()) {
        player.getConnection().send(packet);
    }
}

public void sendToWorld(World world, Packet packet) {
    for (Player player : world.getPlayers()) {
        player.getConnection().send(packet);
    }
}

public void sendToNearby(Vector3d position, double radius, Packet packet) {
    for (Player player : HytaleServer.get().getOnlinePlayers()) {
        if (player.getPosition().distanceTo(position) <= radius) {
            player.getConnection().send(packet);
        }
    }
}
```

### Send with Callback

```java
player.getConnection().send(packet).thenAccept(result -> {
    if (result.isSuccess()) {
        getLogger().atInfo().log("Packet sent successfully");
    } else {
        getLogger().atWarning().log("Packet failed to send: %s", result.getError());
    }
});
```

## Built-in Packets Reference

### Chat Message

```java
// Send chat message to player
ChatMessage chatPacket = new ChatMessage(
    "Hello, World!",
    ChatMessage.Type.SYSTEM
);
player.getConnection().send(chatPacket);
```

### Notification

```java
// Send notification popup
Notification notification = new Notification(
    "Achievement Unlocked!",
    "You found the secret area",
    Notification.Type.SUCCESS,
    5000  // Duration in ms
);
player.getConnection().send(notification);
```

### Play Sound

```java
// Play sound at position
PlaySoundPacket sound = new PlaySoundPacket(
    "MyPlugin/Sounds/alert",
    position,
    1.0f,  // Volume
    1.0f   // Pitch
);
player.getConnection().send(sound);
```

### Entity Updates

```java
// Send entity state update
EntityUpdates updates = new EntityUpdates(
    entityId,
    new HashMap<>() {{
        put("health", health);
        put("position", position);
    }}
);
sendToNearby(position, 64, updates);
```

### Set Block

```java
// Update block on client
ServerSetBlock setBlock = new ServerSetBlock(
    position,
    blockTypeId,
    blockState
);
sendToWorld(world, setBlock);
```

## Creating Custom Packets

### Define Packet Class

```java
public class MyCustomPacket implements Packet {
    public static final int ID = 5000; // Custom ID (use high numbers)
    
    private final String message;
    private final int value;
    private final Vector3d position;
    
    // Deserialize constructor
    public MyCustomPacket(ByteBuf buffer) {
        this.message = PacketIO.readString(buffer);
        this.value = buffer.readInt();
        this.position = new Vector3d(
            buffer.readDouble(),
            buffer.readDouble(),
            buffer.readDouble()
        );
    }
    
    // Create constructor
    public MyCustomPacket(String message, int value, Vector3d position) {
        this.message = message;
        this.value = value;
        this.position = position;
    }
    
    @Override
    public int getId() {
        return ID;
    }
    
    @Override
    public void serialize(ByteBuf buffer) {
        PacketIO.writeString(buffer, message);
        buffer.writeInt(value);
        buffer.writeDouble(position.x());
        buffer.writeDouble(position.y());
        buffer.writeDouble(position.z());
    }
    
    @Override
    public int computeSize() {
        return PacketIO.stringSize(message) + 4 + 24; // int + 3 doubles
    }
    
    // Getters
    public String getMessage() { return message; }
    public int getValue() { return value; }
    public Vector3d getPosition() { return position; }
}
```

### Register Custom Packet

```java
@Override
protected void setup() {
    PacketRegistry.register(
        MyCustomPacket.ID,
        MyCustomPacket.class,
        MyCustomPacket::new,  // Deserializer
        MyCustomPacket::validate  // Optional validator
    );
}

// Optional validation
public static boolean validate(ByteBuf buffer) {
    // Quick validation without full parse
    return buffer.readableBytes() >= 28; // Minimum size
}
```

## PacketIO Utilities

### Writing Data

```java
// Primitives
buffer.writeByte(value);
buffer.writeShort(value);
buffer.writeInt(value);
buffer.writeLong(value);
buffer.writeFloat(value);
buffer.writeDouble(value);
buffer.writeBoolean(value);

// Strings
PacketIO.writeString(buffer, string);

// Variable-length integers
VarInt.write(buffer, value);

// Collections
PacketIO.writeArray(buffer, items, PacketIO::writeString);

// UUIDs
PacketIO.writeUUID(buffer, uuid);

// Vectors
PacketIO.writeVector3d(buffer, vector);
PacketIO.writeVector3i(buffer, blockPos);

// Optional values (nullable)
PacketIO.writeOptional(buffer, value, PacketIO::writeString);
```

### Reading Data

```java
// Primitives
byte b = buffer.readByte();
short s = buffer.readShort();
int i = buffer.readInt();
long l = buffer.readLong();
float f = buffer.readFloat();
double d = buffer.readDouble();
boolean bool = buffer.readBoolean();

// Strings
String str = PacketIO.readString(buffer);

// Variable-length integers
int var = VarInt.read(buffer);

// Collections
List<String> items = PacketIO.readArray(buffer, PacketIO::readString);

// UUIDs
UUID uuid = PacketIO.readUUID(buffer);

// Vectors
Vector3d pos = PacketIO.readVector3d(buffer);
Vector3i blockPos = PacketIO.readVector3i(buffer);

// Optional values
String optional = PacketIO.readOptional(buffer, PacketIO::readString);
```

## Compression

Large packets are automatically compressed using Zstd:

```java
public class LargeDataPacket implements Packet {
    public static final boolean IS_COMPRESSED = true;
    
    private final byte[] data;
    
    @Override
    public void serialize(ByteBuf buffer) {
        // Data will be automatically compressed if IS_COMPRESSED = true
        buffer.writeBytes(data);
    }
}
```

Manual compression:

```java
// Compress
byte[] compressed = PacketIO.compress(data);

// Decompress
byte[] decompressed = PacketIO.decompress(compressed);
```

## Client-Bound Custom UI

### Custom Page Packet

Send custom UI pages to clients:

```java
public void showCustomUI(Player player, String pageId, Map<String, Object> data) {
    CustomPage page = new CustomPage(
        pageId,
        serializeData(data)
    );
    player.getConnection().send(page);
}
```

### Window System

```java
// Open custom window
OpenWindow openWindow = new OpenWindow(
    windowId,
    "MyPlugin:CustomWindow",
    windowData
);
player.getConnection().send(openWindow);

// Handle window actions
handler.registerHandler(SendWindowAction.ID, packet -> {
    SendWindowAction action = (SendWindowAction) packet;
    int windowId = action.getWindowId();
    String actionType = action.getActionType();
    
    processWindowAction(player, windowId, actionType, action.getData());
});

// Close window
CloseWindow closeWindow = new CloseWindow(windowId);
player.getConnection().send(closeWindow);
```

## Handling Client Input

### Mouse Interaction

```java
handler.registerHandler(MouseInteraction.ID, packet -> {
    MouseInteraction interaction = (MouseInteraction) packet;
    
    MouseButton button = interaction.getButton();
    Vector3d hitPos = interaction.getHitPosition();
    Vector3i blockPos = interaction.getBlockPosition();
    int entityId = interaction.getEntityId();
    
    if (button == MouseButton.RIGHT) {
        handleRightClick(player, hitPos, blockPos, entityId);
    } else if (button == MouseButton.LEFT) {
        handleLeftClick(player, hitPos, blockPos, entityId);
    }
});
```

### Movement

```java
handler.registerHandler(ClientMovement.ID, packet -> {
    ClientMovement movement = (ClientMovement) packet;
    
    Vector3d position = movement.getPosition();
    Vector3f rotation = movement.getRotation();
    Vector3d velocity = movement.getVelocity();
    boolean onGround = movement.isOnGround();
    
    validateMovement(player, position, velocity);
});
```

### Key Input

```java
handler.registerHandler(KeyInputPacket.ID, packet -> {
    KeyInputPacket input = (KeyInputPacket) packet;
    
    int keyCode = input.getKeyCode();
    boolean pressed = input.isPressed();
    
    handleKeyInput(player, keyCode, pressed);
});
```

## Rate Limiting

Protect against packet spam:

```java
public class RateLimitedHandler implements SubPacketHandler {
    private final Map<UUID, RateLimiter> limiters = new ConcurrentHashMap<>();
    
    @Override
    public void registerHandlers(IPacketHandler handler) {
        handler.registerHandler(MyPacket.ID, this::handleWithRateLimit);
    }
    
    private void handleWithRateLimit(Packet packet) {
        Player player = getPacketSender();
        RateLimiter limiter = limiters.computeIfAbsent(
            player.getUUID(), 
            k -> new RateLimiter(10, 1000) // 10 per second
        );
        
        if (!limiter.tryAcquire()) {
            getLogger().atWarning().log("Rate limit exceeded for %s", player.getName());
            return;
        }
        
        processPacket(packet);
    }
}
```

## Error Handling

```java
handler.registerHandler(MyPacket.ID, packet -> {
    try {
        processPacket(packet);
    } catch (Exception e) {
        getLogger().atSevere().withCause(e).log("Error processing packet");
        
        // Optionally disconnect on critical errors
        if (e instanceof CriticalPacketError) {
            player.disconnect("Protocol error");
        }
    }
});
```

## Packet Validation

```java
public class ValidatedPacket implements Packet {
    @Override
    public void serialize(ByteBuf buffer) {
        // Add checksum
        int checksum = computeChecksum();
        buffer.writeInt(checksum);
        // Write data
    }
    
    public static ValidatedPacket deserialize(ByteBuf buffer) {
        int checksum = buffer.readInt();
        // Read data
        ValidatedPacket packet = new ValidatedPacket(...);
        
        if (packet.computeChecksum() != checksum) {
            throw new PacketValidationException("Checksum mismatch");
        }
        
        return packet;
    }
}
```

## Performance Tips

### Packet Batching

```java
public class PacketBatcher {
    private final List<Packet> pending = new ArrayList<>();
    private final Player player;
    
    public void queue(Packet packet) {
        pending.add(packet);
    }
    
    public void flush() {
        if (pending.isEmpty()) return;
        
        // Send as batch if supported
        BatchPacket batch = new BatchPacket(pending);
        player.getConnection().send(batch);
        pending.clear();
    }
}
```

### Delta Compression

Only send changes:

```java
public class EntityStateSync {
    private final Map<Integer, EntityState> lastSent = new HashMap<>();
    
    public void sync(Player player, List<Entity> entities) {
        List<EntityDelta> deltas = new ArrayList<>();
        
        for (Entity entity : entities) {
            EntityState current = captureState(entity);
            EntityState last = lastSent.get(entity.getId());
            
            if (last == null || !current.equals(last)) {
                deltas.add(computeDelta(last, current));
                lastSent.put(entity.getId(), current);
            }
        }
        
        if (!deltas.isEmpty()) {
            player.getConnection().send(new EntityDeltaPacket(deltas));
        }
    }
}
```

### Lazy Serialization

```java
public class LazyPacket implements Packet {
    private ByteBuf cached;
    
    @Override
    public void serialize(ByteBuf buffer) {
        if (cached == null) {
            cached = Unpooled.buffer();
            doSerialize(cached);
        }
        buffer.writeBytes(cached.duplicate());
    }
}
```

## Troubleshooting

### Packet Not Received

1. Verify packet ID is registered
2. Check handler is registered
3. Ensure packet is properly serialized
4. Check for exceptions in handler

### Deserialization Errors

1. Verify read order matches write order
2. Check data type sizes
3. Validate buffer has enough bytes
4. Add bounds checking

### Connection Drops

1. Check for unhandled exceptions
2. Verify packet size limits
3. Monitor bandwidth usage
4. Check ping/timeout settings

See `references/packet-list.md` for complete packet catalog.
See `references/serialization.md` for serialization patterns.