HyCodeYourTale

Packets

Packets

Detailní dokumentace k packet systému v Hytale.

---

Přehled Architektury

PacketRegistry (statická registrace všech paketů)

Packet (interface)
├── CachedPacket (optimalizovaný pro opakované odesílání)

PacketHandler (abstraktní base třída)
├── GenericPacketHandler (přidává handler registraci)
│ └── GamePacketHandler (herní logika)
└── SetupPacketHandler (setup fáze)

---

Packet Interface

Základní rozhraní pro všechny pakety:

public interface Packet {
// ID paketu z PacketRegistry
int getId();

// Serializace do ByteBuf
void serialize(@Nonnull ByteBuf buf);

// Výpočet velikosti
int computeSize();
}

Příklad Paketu

// Z protokolu - každý paket má:
// - Unikátní ID
// - Serializaci
// - Deserializaci (statická metoda)
// - Validaci struktury

public class ChatMessage implements Packet {
public String message;

@Override
public int getId() {
return 211; // ID z PacketRegistry
}

@Override
public void serialize(@Nonnull ByteBuf buf) {
// Zápis stringu
PacketIO.writeString(buf, this.message);
}

public static ChatMessage deserialize(ByteBuf buf, int protocolVersion) {
ChatMessage packet = new ChatMessage();
packet.message = PacketIO.readString(buf);
return packet;
}
}

---

PacketRegistry

Centrální registrace všech paketů:

public final class PacketRegistry {
private static final Map BY_ID = new HashMap<>();
private static final Map, Integer> BY_TYPE = new HashMap<>();

// Registrace paketu
private static void register(
int id,
String name,
Class type,
int fixedBlockSize,
int maxSize,
boolean compressed,
BiFunction validate,
BiFunction deserialize
);

// Získání info podle ID
@Nullable
public static PacketInfo getById(int id);

// Získání ID podle typu
@Nullable
public static Integer getId(Class type);

// Všechny pakety
@Nonnull
public static Map all();
}

PacketInfo Record

public static record PacketInfo(
int id, // Unikátní ID
@Nonnull String name, // Název paketu
@Nonnull Class type, // Třída paketu
int fixedBlockSize, // Fixní velikost bloku
int maxSize, // Maximální velikost
boolean compressed, // Zda se komprimuje
BiFunction validate, // Validace
BiFunction deserialize // Deserializace
);

Registrované Pakety (výběr)

| ID | Název | Popis |
|----|-------|-------|
| 0 | Connect | Připojení klienta |
| 1 | Disconnect | Odpojení |
| 2 | Ping | Ping request |
| 3 | Pong | Pong response |
| 108 | ClientMovement | Pohyb hráče |
| 161 | EntityUpdates | Aktualizace entit |
| 210 | ServerMessage | Zpráva od serveru |
| 211 | ChatMessage | Chat zpráva |
| 216 | SetPage | Nastavení UI stránky |
| 218 | CustomPage | Custom UI stránka |
| 219 | CustomPageEvent | UI event |

---

CachedPacket

Optimalizovaný paket pro opakované odesílání:

public final class CachedPacket implements Packet, AutoCloseable {
private final Class packetType;
private final int packetId;
private final ByteBuf cachedBytes;

// Factory metoda pro vytvoření cache
public static CachedPacket cache(@Nonnull T packet) {
if (packet instanceof CachedPacket) {
throw new IllegalArgumentException("Cannot cache a CachedPacket");
}

ByteBuf buf = Unpooled.buffer();
packet.serialize(buf);
return new CachedPacket<>((Class)packet.getClass(), packet.getId(), buf);
}

@Override
public void serialize(@Nonnull ByteBuf buf) {
// Zapisuje předem serializovaná data
buf.writeBytes(this.cachedBytes, this.cachedBytes.readerIndex(), this.cachedBytes.readableBytes());
}

@Override
public void close() {
if (this.cachedBytes.refCnt() > 0) {
this.cachedBytes.release();
}
}
}

Použití CachedPacket

// Pro broadcast paketu mnoha hráčům
Packet originalPacket = new ServerMessage(Message.raw("Hello everyone!"));
CachedPacket cached = CachedPacket.cache(originalPacket);

try {
for (PlayerRef player : Universe.get().getPlayers()) {
player.getPacketHandler().writeNoCache(cached);
}
} finally {
cached.close(); // Uvolnění buffer
}

---

PacketHandler

Abstraktní třída pro zpracování paketů:

public abstract class PacketHandler implements IPacketReceiver {
public static final int MAX_PACKET_ID = 512;

@Nonnull
protected final Channel channel;
@Nonnull
protected final ProtocolVersion protocolVersion;
@Nullable
protected PlayerAuthentication auth;

// Odesílání paketů
@Override
public void write(@Nonnull Packet packet);

@Override
public void writeNoCache(@Nonnull Packet packet);

// Batch odesílání
public void write(@Nonnull Packet... packets);

// Odpojení hráče
public void disconnect(@Nonnull String message);

// Ping/Pong
public void sendPing();
public void handlePong(@Nonnull Pong packet);

// Abstrakt - musí implementovat
public abstract void accept(@Nonnull Packet packet);

@Nonnull
public abstract String getIdentifier();
}

Write vs WriteNoCache

// write() - automaticky cachuje paket
// Použij pro jednorázové odeslání
player.getPacketHandler().write(packet);

// writeNoCache() - nepoužívá cache
// Použij když už máš CachedPacket nebo nechceš cache
player.getPacketHandler().writeNoCache(cachedPacket);

---

GamePacketHandler

Hlavní handler pro herní fázi:

public class GamePacketHandler extends GenericPacketHandler implements IPacketHandler {
private PlayerRef playerRef;
@Nonnull
private final Deque interactionPacketQueue = new ConcurrentLinkedDeque<>();

// Registrace handlerů
protected void registerHandlers() {
this.registerHandler(1, p -> this.handle((Disconnect)p));
this.registerHandler(3, p -> this.handlePong((Pong)p));
this.registerHandler(108, p -> this.handle((ClientMovement)p));
this.registerHandler(211, p -> this.handle((ChatMessage)p));
this.registerHandler(111, p -> this.handle((MouseInteraction)p));
// ... další handlery
}

// Příklad handleru
public void handle(@Nonnull ChatMessage packet) {
if (packet.message != null && !packet.message.isEmpty()) {
String message = packet.message;
char firstChar = message.charAt(0);

if (firstChar == '/') {
// Příkaz
CommandManager.get().handleCommand(this.playerComponent, message.substring(1));
} else {
// Chat zpráva
// ... zpracování chatu
}
} else {
this.disconnect("Invalid chat message packet! Message was empty.");
}
}
}

Thread Safety v Handlerech

public void handle(@Nonnull MouseInteraction packet) {
Ref ref = this.playerRef.getReference();
if (ref != null && ref.isValid()) {
Store store = ref.getStore();
World world = store.getExternalData().getWorld();

// DŮLEŽITÉ: Přepni na world thread pro přístup ke komponentám
world.execute(() -> {
Player playerComponent = store.getComponent(ref, Player.getComponentType());
// Bezpečný přístup ke komponentám
InteractionModule.get().doMouseInteraction(ref, store, packet, playerComponent, this.playerRef);
});
}
}

---

IPacketHandler Interface

Rozhraní pro packet handlery:

public interface IPacketHandler {
// Registrace handleru pro packet ID
void registerHandler(int packetId, @Nonnull Consumer handler);

// Registrace no-op handlerů (pakety které ignorujeme)
void registerNoOpHandlers(int... packetIds);

@Nonnull
PlayerRef getPlayerRef();

@Nonnull
String getIdentifier();
}

---

SubPacketHandler

Pro modulární registraci handlerů:

public interface SubPacketHandler {
void registerHandlers();
}

Příklad: InventoryPacketHandler

public class InventoryPacketHandler implements SubPacketHandler {
private final IPacketHandler parent;

public InventoryPacketHandler(IPacketHandler parent) {
this.parent = parent;
}

@Override
public void registerHandlers() {
this.parent.registerHandler(171, p -> this.handle((SetCreativeItem)p));
this.parent.registerHandler(172, p -> this.handle((DropCreativeItem)p));
this.parent.registerHandler(174, p -> this.handle((DropItemStack)p));
this.parent.registerHandler(175, p -> this.handle((MoveItemStack)p));
this.parent.registerHandler(177, p -> this.handle((SetActiveSlot)p));
}

private void handle(SetCreativeItem packet) {
// Implementace
}
}

Registrace SubPacketHandlerů

// V ServerManager nebo podobném
public void populateSubPacketHandlers(IPacketHandler handler) {
List handlers = new ArrayList<>();

handlers.add(new InventoryPacketHandler(handler));
handlers.add(new BuilderToolsPacketHandler(handler));
// ... další handlery

// Uložení pro pozdější registerHandlers()
}

---

PingInfo

Sledování latence:

public static class PingInfo {
public static final TimeUnit TIME_UNIT = TimeUnit.MICROSECONDS;

protected final PongType pingType;
@Nonnull
protected final HistoricMetric pingMetricSet; // Historie ping hodnot
protected final Metric packetQueueMetric; // Fronta paketů

public PingInfo(PongType pingType) {
this.pingType = pingType;
this.pingMetricSet = HistoricMetric.builder(1000L, TimeUnit.MILLISECONDS)
.addPeriod(1L, TimeUnit.SECONDS)
.addPeriod(1L, TimeUnit.MINUTES)
.addPeriod(5L, TimeUnit.MINUTES)
.build();
}

// Zpracování Pong paketu
protected void handlePacket(@Nonnull Pong packet) {
long nanoTime = System.nanoTime();
long pingValue = nanoTime - sentTimestamp;

this.pingMetricSet.add(nanoTime, TIME_UNIT.convert(pingValue, TimeUnit.NANOSECONDS));
this.packetQueueMetric.add((long)packet.packetQueueSize);
}
}

Typy Pingu

public enum PongType {
Raw, // Surový ping (network only)
Direct, // Přímý ping
Tick // Ping včetně tick processing
}

---

Validace Paketů

DisconnectReason

public static class DisconnectReason {
@Nullable
private String serverDisconnectReason; // Server odpojil hráče
@Nullable
private DisconnectType clientDisconnectType; // Klient se odpojil

public void setServerDisconnectReason(String reason) {
this.serverDisconnectReason = reason;
this.clientDisconnectType = null;
}

public void setClientDisconnectType(DisconnectType type) {
this.clientDisconnectType = type;
this.serverDisconnectReason = null;
}
}

Validace Pozice

public void handle(@Nonnull ClientMovement packet) {
// Validace bezpečné pozice
if (packet.absolutePosition != null && !ValidateUtil.isSafePosition(packet.absolutePosition)) {
this.disconnect("Sent impossible position data!");
return;
}

// Validace směru
if ((packet.bodyOrientation == null || ValidateUtil.isSafeDirection(packet.bodyOrientation))
&& (packet.lookOrientation == null || ValidateUtil.isSafeDirection(packet.lookOrientation))) {
// OK - zpracuj
} else {
this.disconnect("Sent impossible orientation data!");
}
}

---

Packet Flow Diagram

Client                          Server
│ │
│ Connect (ID: 0) │
├─────────────────────────────>│
│ │ AuthenticationPacketHandler
│ │
│ ConnectAccept (ID: 14) │
│<─────────────────────────────┤
│ │
│ WorldSettings (ID: 20) │
│<─────────────────────────────┤
│ │
│ Asset packets │
│<─────────────────────────────┤
│ │
│ JoinWorld (ID: 104) │
│<─────────────────────────────┤
│ │ SetupPacketHandler → GamePacketHandler
│ │
│ ClientReady (ID: 105) │
├─────────────────────────────>│
│ │
│ Ping/Pong (every 1s) │
│<────────────────────────────>│
│ │
│ Game packets... │
│<────────────────────────────>│
│ │

---

Best Practices

1. Používej CachedPacket pro Broadcast

// Pro odesílání stejného paketu více hráčům
ServerMessage message = new ServerMessage(buildMessage());
CachedPacket cached = CachedPacket.cache(message);

try {
for (PlayerRef player : getTargetPlayers()) {
player.getPacketHandler().writeNoCache(cached);
}
} finally {
cached.close();
}

2. Validuj Vstupní Data

public void handle(@Nonnull CustomPacket packet) {
// Kontrola null
if (packet.data == null) {
return;
}

// Kontrola rozsahu
if (packet.index < 0 || packet.index >= MAX_INDEX) {
this.disconnect("Invalid packet data!");
return;
}

// Zpracování
}

3. World Thread pro Komponenty

public void handle(@Nonnull SomePacket packet) {
Ref ref = this.playerRef.getReference();
if (ref != null && ref.isValid()) {
Store store = ref.getStore();
World world = store.getExternalData().getWorld();

// VŽDY přepni na world thread
world.execute(() -> {
// Přístup ke komponentám zde
});
}
}

---

Shrnutí

| Třída | Účel |
|-------|------|
| Packet | Interface pro všechny pakety |
| PacketRegistry | Centrální registrace paketů |
| CachedPacket | Optimalizovaný paket pro broadcast |
| PacketHandler | Abstraktní base handler |
| GamePacketHandler | Handler pro herní fázi |
| SubPacketHandler | Modulární registrace handlerů |

| Metoda | Kdy použít |
|--------|------------|
| write(packet) | Jednorázové odeslání |
| writeNoCache(packet) | CachedPacket nebo vlastní cache |
| write(packets...) | Batch odeslání více paketů |
| disconnect(message) | Odpojení s chybovou zprávou |

| ID Range | Kategorie |
|----------|-----------|
| 0-19 | Connection, Auth |
| 20-39 | Setup, World |
| 40-99 | Assets |
| 100-130 | Player |
| 131-169 | World, Entities |
| 170-209 | Inventory, Window |
| 210-239 | Interface, UI |
| 240-259 | WorldMap |
| 260-299 | Camera, Interaction |
| 300-399 | AssetEditor |
| 400-499 | BuilderTools |

Last updated: 20. ledna 2026