Packet Schema (.packet) Format

From JFTSE Wiki
Revision as of 15:41, 18 December 2025 by XxharCs (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Packet Schema (.packet) Format

This page explains how to define Fantasy Tennis packets using the schema based .packet format consumed by FTPacketGen. The generator turns these schema files into Java classes (packets and nested structures), including parsing for client packets and auto writing for server packets.

Where .packet files live

FTPacketGen scans a directory tree for files ending in .packet (server-core/src/main/packets/). The folder structure becomes the Java package:

com.jftse.server.core.shared.packets.<relative folders...>

Example:

server-core/src/main/packets/
  auth/
    CMSG_Login.packet

Generates classes in:

com.jftse.server.core.shared.packets.auth

Message blocks

A schema file can contain one or multiple message blocks.

Packet message (has packet id)

Use this to define an actual network packet (generates a class that implements IPacket):

message CMSG_Login (0xFA1) {
    string username = 1;
    string password = 2 [encoding = utf8];
    int32 version = 3;
    byte unk0 = 4;
    string hwid = 5 [encoding = utf8];
}
  • The (0x....) makes it a packet class and sets PACKET_ID.
  • If the message name starts with CMSG (case-insensitive), it is treated as a client packet (read/parse from bytes).
  • Otherwise it is treated as a server packet (write/build to bytes).

Struct / nested message (no packet id)

Use this to define a reusable structure you can reference as a field type in other messages:

message Account {
    int32 id = 1;
    int32 id2 = 2;
    byte tutorialCount = 3;
    int32 lastPlayedPlayerId = 4;
    boolean gameMaster = 5;
}

Then use it inside a packet:

message SMSG_PlayerList (0x1005) {
    Account account = 1;
    repeated Player players = 2;
}

Field syntax

Each field line follows this shape:

[repeated] <type> <name> = <number> [<options>];

Examples:

int32 gold = 1;
repeated int32 itemIds = 2;
string nickname = 3 [len = 16];

Field numbering rules (IMPORTANT)

Field numbers are validated strictly:

  • Must start at 1
  • Must be sequential with no gaps (1,2,3,...)
  • Must not contain duplicates

If you skip a number (or duplicate one), generation fails.

Reserved field names

Do not use these field names:

  • data
  • metaData

They are reserved internally by the generated packet classes.

Supported base types

These are recognized and mapped by the generator. Sizes refer to how values are written to / read from the packet payload.

Schema type Java type Payload representation
int / int32 / uint32 int 4 bytes (little-endian)
long / int64 / uint64 long 8 bytes (little-endian)
short / int16 / uint16 short 2 bytes (little-endian)
char char 2 bytes (UTF-16 / Java char)
byte / int8 / uint8 byte 1 byte
bool / boolean boolean 1 byte (0 = false, 1 = true)
float float 4 bytes (IEEE-754, little-endian)
double double 8 bytes (IEEE-754, little-endian)
string String Variable length, null-terminated (UTF-16LE by default; UTF-8 if specified)
date java.util.Date 8 bytes (Windows FILETIME)
bytes byte[] Raw byte sequence (length defined by schema)

Anything else is treated as a custom/nested message type (must be defined as message <TypeName> { ... } somewhere in the scanned schema set).

Notes:

  • All numeric values are written using little-endian byte order.
  • char corresponds to Java char and is always 2 bytes wide.
  • String encoding can be overridden using [encoding=utf8] or [len=...].

repeated fields

Use repeated for lists/arrays (except bytes, which is already a raw byte array).

repeated int32 values = 1;
repeated Player players = 2;

How repeated is encoded

By default, repeated fields are encoded as:

  1. a count prefix (1 byte by default), then
  2. that many elements

On the read side, the count prefix is always read as a single byte unless you force fixed length via [len=...].

On the write side, you can control the numeric type used for the count prefix via [type=...].

Options

Options go in square brackets:

string name = 1 [len = 16];
repeated Player players = 2 [type = short];

Options are parsed as:

key = value

separated by commas.

len (strings, bytes, repeated)

len changes how a field is read/written.

string + len

Reads a fixed-length string (UTF-8) of exactly len bytes:

string nickname = 1 [len = 16];

bytes + len

Reads exactly len bytes:

bytes payload = 1 [len = 64];

If bytes has no len, it reads “the rest of the packet payload”.

repeated + len

Reads a fixed number of elements (no count prefix is consumed on read):

repeated int32 fixedSet = 1 [len = 10];

Dynamic length references (len = msg.<field>)

In addition to constant values, the len option may reference a previously read field using the generated packet instance via msg.<fieldName>.

This allows defining dynamic-length fields whose size depends on earlier values in the packet payload.

Rules

  • The referenced field must appear earlier in the schema.
  • The referenced field must already be fully read when the dynamic field is processed.
  • This is most commonly used with repeated fields.
  • The reference uses the generated packet instance (msg), not raw schema names.

Example

message CMSG_GuildCreate (0x200B) {
    string name = 1;
    string introduction = 2;
    bool isPublic = 3;
    byte levelRestriction = 4;
    byte allowedCharacterTypeCount = 5;
    repeated byte allowedCharacterType = 6 [len = msg.allowedCharacterTypeCount];
}

In this example:

  • allowedCharacterTypeCount is read first
  • allowedCharacterType reads exactly that many elements
  • No count prefix is read for the repeated field

Notes

  • Dynamic references are evaluated at runtime during packet parsing.
  • If the referenced value is invalid or out of bounds, parsing behavior is undefined.
  • Only simple field references are supported (no expressions or calculations).

type (size/count prefix control)

type controls the integer type used when writing sizes/counts (and can also force a cast for some primitives).

repeated + type

Controls the numeric type used to write the list size:

repeated Player players = 1 [type = short];

This writes (short) players.size() before the elements.

primitive field + type

If you set type=... on a supported primitive field, the generator will cast when writing. This is useful when the schema expresses a value as int32 but the protocol stores it smaller:

int32 flags = 1 [type = byte];

encoding=utf8 (strings)

For writing strings:

  • Default writing uses UTF-16LE + 0x0000 terminator.
  • With [encoding = utf8] it writes UTF-8 + 0x00 terminator.
string password = 1 [encoding = utf8];

Reading strings:

  • Variable-length string is auto-detected by the generated reader (UTF-16LE vs ASCII/UTF-8 style).
  • Fixed-length strings ([len = ...]) are read as UTF-8 bytes.

Client vs Server packet behavior

The generator treats packets differently depending on the message name.

Client packets (CMSG*)

If the message name starts with CMSG / cmsg:

  • The generated class registers itself with PacketRegistry in a static block.
  • fromBytes(...) parses all defined fields in schema order.
  • A generic read(Class<T>) API exists plus type-specific read helpers.

These are also included in the generated PacketAutoRegister list.

Server packets (everything else)

For non-CMSG packet messages:

  • The builder’s build() writes fields into the internal byte buffer automatically (in schema order).
  • toBytes() returns the full header + payload.

Nested/composite messages

Any message without a packet id becomes a plain Java class with fields, getters/setters, and a builder.

When you reference that message name as a field type inside a packet, the generator writes/reads its subfields in order.

Full example: Player list packet

This shows nested messages and a repeated composite type.

message Account {
    int32 id = 1;
    int32 id2 = 2;
    byte tutorialCount = 3;
    int32 lastPlayedPlayerId = 4;
    boolean gameMaster = 5;
}

message ClothEquipment {
    int32 hair = 1;
    int32 face = 2;
    int32 dress = 3;
    int32 pants = 4;
    int32 socks = 5;
    int32 shoes = 6;
    int32 gloves = 7;
    int32 racket = 8;
    int32 glasses = 9;
    int32 bag = 10;
    int32 hat = 11;
    int32 dye = 12;
}

message Player {
    int32 id = 1;
    string name = 2;
    byte level = 3;
    boolean created = 4;
    boolean canDelete = 5;
    int32 gold = 6;
    byte playerType = 7;
    byte str = 8;
    byte sta = 9;
    byte dex = 10;
    byte wil = 11;
    byte statPoints = 12;
    boolean oldRenameAllowed = 13;
    boolean renameAllowed = 14;
    ClothEquipment clothEquipment = 15;
}

message SMSG_PlayerList (0x1005) {
    Account account = 1;
    repeated Player players = 2;
}

Practical tips / gotchas

  • Keep schema field order exactly matching the on wire protocol order. The generator reads/writes strictly in schema order.
  • Use [type = short] (or another type) on repeated if the protocol’s list length prefix is not 1 byte.
  • Use bytes for raw data blocks; don’t try repeated byte unless you specifically want a length-prefixed list of bytes.
  • Avoid data and metaData as field names.
  • Don’t skip field numbers, generation will fail.