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 strukturypublic 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 extends Packet> 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 extends Packet> 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 extends Packet> 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 |