hytale-networking
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.
More from mnkyarts/hytale-skills
hytale-plugin-basics
Create and structure Hytale server plugins with proper lifecycle, manifest, dependencies, and registries. Use when asked to "create a Hytale plugin", "make a Hytale mod", "start a new Hytale plugin", "setup plugin structure", or "write plugin boilerplate".
21hytale-ui-windows
Create custom UI windows, containers, and interactive interfaces for Hytale plugins. Use when asked to "create inventory UI", "make custom window", "add container interface", "build crafting UI", "custom GUI", "create .ui file", or "design UI layout".
19hytale-crafting-recipes
Create custom crafting recipes for Hytale plugins including shaped, shapeless, processing, and blueprint recipes. Use when asked to "add crafting recipe", "create recipe", "make craftable item", "add smelting recipe", or "custom crafting".
18hytale-custom-entities
Create custom entities and NPCs for Hytale with AI behaviors, components, spawning, and animations. Use when asked to "create a custom entity", "add an NPC", "make a mob", "design AI behavior", or "configure entity spawning".
18hytale-commands
Create custom commands for Hytale server plugins with arguments, permissions, and execution handling. Use when asked to "create a command", "add slash command", "make admin command", "register commands", or "command with arguments".
15hytale-custom-assets
Create and manage custom assets for Hytale including models, textures, sounds, particles, and asset packs. Use when asked to "add custom assets", "create textures", "make models", "add sounds", "configure particles", or "build an asset pack".
15