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:
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:
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
@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
public void sendToPlayer(Player player, Packet packet) {
player.getConnection().send(packet);
}
Send to Multiple Players
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
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
// Send chat message to player
ChatMessage chatPacket = new ChatMessage(
"Hello, World!",
ChatMessage.Type.SYSTEM
);
player.getConnection().send(chatPacket);
Notification
// 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
// Play sound at position
PlaySoundPacket sound = new PlaySoundPacket(
"MyPlugin/Sounds/alert",
position,
1.0f, // Volume
1.0f // Pitch
);
player.getConnection().send(sound);
Entity Updates
// Send entity state update
EntityUpdates updates = new EntityUpdates(
entityId,
new HashMap<>() {{
put("health", health);
put("position", position);
}}
);
sendToNearby(position, 64, updates);
Set Block
// Update block on client
ServerSetBlock setBlock = new ServerSetBlock(
position,
blockTypeId,
blockState
);
sendToWorld(world, setBlock);
Creating Custom Packets
Define Packet Class
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
@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
// 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
// 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:
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:
// 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:
public void showCustomUI(Player player, String pageId, Map<String, Object> data) {
CustomPage page = new CustomPage(
pageId,
serializeData(data)
);
player.getConnection().send(page);
}
Window System
// 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
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
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
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:
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
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
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
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:
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
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
- Verify packet ID is registered
- Check handler is registered
- Ensure packet is properly serialized
- Check for exceptions in handler
Deserialization Errors
- Verify read order matches write order
- Check data type sizes
- Validate buffer has enough bytes
- Add bounds checking
Connection Drops
- Check for unhandled exceptions
- Verify packet size limits
- Monitor bandwidth usage
- Check ping/timeout settings
See references/packet-list.md for complete packet catalog.
See references/serialization.md for serialization patterns.