Skip to main content
YSFlight uses a custom binary protocol for client-server communication. Sakuya AC’s packet system provides tools to decode, encode, and manipulate these packets.

Packet structure

Every YSFlight packet follows this format:
┌────────────────┬────────────────────────────────────────┐
│  Length (4B)   │           Payload (variable)           │
├────────────────┼────────────────────────────────────────┤
│ Unsigned Int   │  Packet Type (4B) + Packet Data        │
└────────────────┴────────────────────────────────────────┘

Header (4 bytes)

The first 4 bytes indicate the payload length as an unsigned 32-bit integer (little-endian):
header = await reader.readexactly(4)
length = unpack("I", header)[0]  # "I" = unsigned int
From proxy.py:125-140

Payload

The payload starts with a 4-byte packet type ID, followed by packet-specific data:
packet = await reader.read(length)
packet_type_id = unpack("<I", packet[:4])[0]  # Little-endian unsigned int
packet_type_name = MESSAGE_TYPES[packet_type_id]

Packet types

YSFlight defines 80+ packet types. Sakuya AC implements decoders for the most important ones.

Common packet types

IDPacket TypeDirectionDescription
1FSNETCMD_LOGONC↔SPlayer login/authentication
2FSNETCMD_LOGOFFC→SPlayer disconnect
4FSNETCMD_LOADFIELDS→CLoad map/field
5FSNETCMD_ADDOBJECTS→CAdd aircraft/ground object
8FSNETCMD_JOINREQUESTC→SRequest to spawn aircraft
9FSNETCMD_JOINAPPROVALS→CApprove aircraft spawn
11FSNETCMD_AIRPLANESTATEC↔SAircraft position/state
12FSNETCMD_UNJOINC→SLeave aircraft
16FSNETCMD_PREPARESIMULATIONS→CSimulation ready
30FSNETCMD_AIRCMDS↔CAircraft command/config
32FSNETCMD_TEXTMESSAGEC↔SChat message
36FSNETCMD_WEAPONCONFIGC↔SWeapon loadout
C→S means client-to-server, S→C means server-to-client, and C↔S means bidirectional.
The full list is defined in lib/PacketManager/packets/constants.py:
MESSAGE_TYPES = [
    "FSNETCMD_NULL",                   #   0
    "FSNETCMD_LOGON",                  #   1
    "FSNETCMD_LOGOFF",                 #   2
    "FSNETCMD_ERROR",                  #   3
    "FSNETCMD_LOADFIELD",              #   4
    # ... 80+ more types
]
From lib/PacketManager/packets/constants.py:3-86

PacketManager class

The PacketManager identifies packet types:
class PacketManager:
    def get_packet_type(self, data: bytes):
        if len(data) < 4:
            return None
        return MESSAGE_TYPES[unpack("<I", data[:4])[0]]
From lib/PacketManager/PacketManager.py:5-14 Usage:
packet_type = PacketManager().get_packet_type(packet)
if packet_type == "FSNETCMD_LOGON":
    # Handle login packet

Packet classes

Each major packet type has a dedicated class in lib/PacketManager/packets/.

FSNETCMD_LOGON

Handles player authentication when connecting to the server. Structure:
  • Packet type (4 bytes): 1
  • Username (16 bytes): Null-terminated string
  • Version (4 bytes): YSFlight version number
  • Alias (optional, 200+ bytes): Extended username
Decoding:
class FSNETCMD_LOGON:
    def __init__(self, buffer: bytes, should_decode: bool = True):
        self.buffer = buffer
        self.version = None
        self.username = None
        self.alias = None
        if len(self.buffer) > 5 and should_decode:
            self.decode()
    
    def decode(self):
        self.username, self.version = unpack("16sI", self.buffer[4:24])
        if len(self.buffer) > 24:
            self.alias = self.buffer[24:].decode().strip('\x00')
        else:
            self.alias = self.username
        self.username = self.username.decode().strip('\x00')
From lib/PacketManager/packets/FSNETCMD_LOGON.py:8-29 Encoding:
@staticmethod
def encode(username, version, with_size: bool = False):
    if len(username) > 15:
        shortform = username[:15]
        alias = username
    else:
        shortform = username
        alias = None
    
    buffer = pack("I16sI", 1, shortform.encode(), version)
    if alias:
        buffer += alias.encode().ljust(200, b'\x00') + b'\x00\x00\x00\x00'
    
    if with_size:
        return pack("I", len(buffer)) + buffer
    return buffer
From lib/PacketManager/packets/FSNETCMD_LOGON.py:32-54

FSNETCMD_AIRPLANESTATE

The most frequently sent packet - updates aircraft position and state. Structure:
  • Remote time (4 bytes): Server timestamp
  • Player ID (4 bytes): Unique aircraft ID
  • Packet version (2 bytes): State packet format version
  • Position (12 bytes): X, Y, Z as floats
  • Attitude (6 bytes): Heading, pitch, bank as shorts
  • Velocity (6 bytes): VX, VY, VZ as shorts
  • Many more fields: fuel, weapons, controls, flags, etc.
Decoding example:
class FSNETCMD_AIRPLANESTATE:
    def __init__(self, buffer: bytes, should_decode: bool = True):
        self.buffer = buffer
        self.remote_time = None
        self.player_id = None
        self.packet_version = None
        self.position = [0, 0, 0]
        self.atti = [0, 0, 0]
        self.velocity = [0, 0, 0]
        # ... many more fields
        if should_decode:
            self.decode()
    
    def decode(self):
        self.remote_time, self.player_id = unpack("fI", self.buffer[4:12])
        self.packet_version = unpack("h", self.buffer[12:14])[0]
        
        if self.packet_version == 4 or self.packet_version == 5:
            self.position = list(unpack("fff", self.buffer[14:26]))
            self.atti = list(map(
                lambda x: x * (pi / 32768.0),
                unpack("HHH", self.buffer[26:32])
            ))
            # ... decode remaining fields
From lib/PacketManager/packets/FSNETCMD_AIRPLANESTATE.py:6-76
The attitude values use a custom encoding where angles are represented as 16-bit integers and converted to radians by multiplying by π/32768.
Usage in proxy:
if packet_type == "FSNETCMD_AIRPLANESTATE":
    decode = FSNETCMD_AIRPLANESTATE(packet)
    player.aircraft.add_state(decode)
    
    # Anti-cheat: check for health hacks
    if player.aircraft.prev_life < player.aircraft.life:
        cheatingMsg = YSchat.message(f"Health hack detected: {player.username}")
        message_to_server.append(cheatingMsg)
From proxy.py:184-196

FSNETCMD_TEXTMESSAGE

Handles in-game chat messages. Structure:
  • Packet type (4 bytes): 32
  • Reserved (8 bytes): Two unused integers
  • Message (variable): UTF-8 encoded text, null-terminated
Decoding:
class FSNETCMD_TEXTMESSAGE:
    def __init__(self, buffer: bytes, should_decode: bool = True):
        self.buffer = buffer
        self.raw_message = None
        self.user = None
        self.message = ""
        if should_decode:
            self.decode()
    
    def decode(self):
        self.raw_message = self.buffer[12:].decode("utf-8").strip("\x00")
        # Format: "(username)message text"
        match = re.match(r"^\(([^)]+)\)(.+)", self.raw_message)
        if match:
            self.user, self.message = match.groups()
From lib/PacketManager/packets/FSNETCMD_TEXTMESSAGE.py:4-17 Encoding:
@staticmethod
def encode(message: str, with_size: bool = False):
    buffer = pack("III", 32, 0, 0) + message.encode("utf-8") + b"\x00"
    if with_size:
        return pack("I", len(buffer)) + buffer
    return buffer
From lib/PacketManager/packets/FSNETCMD_TEXTMESSAGE.py:19-24

FSNETCMD_ADDOBJECT

Sent by server when adding an aircraft or ground object to the simulation. Structure:
  • Packet type (4 bytes): 5
  • Object type (2 bytes): 0 = aircraft, 1 = ground
  • Network type (2 bytes): Network identifier
  • Object ID (4 bytes): Unique object ID
  • IFF (2 bytes): Team/faction
  • Position (12 bytes): X, Y, Z as floats
  • Attitude (12 bytes): H, P, B as floats
  • Identifier (32 bytes): Aircraft name
  • Substitute name (32 bytes): Alternative aircraft
  • YSF ID (4 bytes): YSFlight internal ID
  • Flags (8 bytes): Object flags
  • Outside radius (4 bytes): Collision radius
  • Aircraft class/category (4 bytes, optional)
  • Pilot name (32 bytes, optional)
Decoding:
class FSNETCMD_ADDOBJECT:
    def decode(self):
        self.object_type, self.net_type = unpack("HH", self.buffer[4:8])
        self.object_id = unpack("I", self.buffer[8:12])[0]
        self.iff, _ = unpack("hh", self.buffer[12:16])
        self.pos = list(unpack("fff", self.buffer[16:28]))
        self.atti = list(unpack("fff", self.buffer[28:40]))
        self.identifier = unpack("32s", self.buffer[40:72])[0].decode().strip('\x00')
        # ... more fields
        if len(self.buffer) >= 176:
            self.pilot = unpack("32s", self.buffer[124:156])[0].decode().strip('\x00')
From lib/PacketManager/packets/FSNETCMD_ADDOBJECT.py:28-48 Usage:
if packet_type == "FSNETCMD_ADDOBJECT":
    if player.check_add_object(FSNETCMD_ADDOBJECT(packet)):
        info(f"{player.username} has spawned an aircraft")
        # Inject custom config
        addSmoke = FSNETCMD_WEAPONCONFIG.addSmoke(player.aircraft.id)
        message_to_server.append(addSmoke)
From proxy.py:252-256

Creating custom packets

All packet classes provide static encode() methods:
# Create a login packet
login_packet = FSNETCMD_LOGON.encode(
    username="TestPilot",
    version=20181124,
    with_size=True  # Include 4-byte length header
)

# Create a text message
chat_packet = FSNETCMD_TEXTMESSAGE.encode(
    message="(Server)Welcome to the server!",
    with_size=True
)

# Inject into message queue
message_to_client.append(chat_packet)
Set with_size=True when encoding packets to automatically prepend the 4-byte length header.

Packet versioning

Some packets (like FSNETCMD_AIRPLANESTATE) have multiple format versions:
self.packet_version = unpack("h", self.buffer[12:14])[0]

if self.packet_version == 4 or self.packet_version == 5:
    # Use compact format
    self.position = list(unpack("fff", self.buffer[14:26]))
else:
    # Use legacy format
    self.position = list(unpack("fff", self.buffer[16:28]))
From lib/PacketManager/packets/FSNETCMD_AIRPLANESTATE.py:69-76 This allows YSFlight to maintain backwards compatibility while adding new features.

Binary encoding tips

Struct format characters

Python’s struct module is used for packing/unpacking binary data:
FormatTypeSize
BUnsigned byte1 byte
bSigned byte1 byte
HUnsigned short2 bytes
hSigned short2 bytes
IUnsigned int4 bytes
iSigned int4 bytes
fFloat4 bytes
dDouble8 bytes
sStringVariable

Endianness

  • < prefix: little-endian (most common)
  • > prefix: big-endian
  • No prefix: native byte order
unpack("<I", data[:4])  # Little-endian unsigned int
unpack(">H", data[:2])  # Big-endian unsigned short

Bit manipulation

Many fields pack multiple values into a single byte:
# Pack two 4-bit values into one byte
c = unpack("B", self.buffer[54:55])[0]
spoiler = (c >> 4 & 15) / 15.0    # Upper 4 bits
landing_gear = (c & 15) / 15.0    # Lower 4 bits
From lib/PacketManager/packets/FSNETCMD_AIRPLANESTATE.py:92-94
>> 4 shifts bits right by 4 positions, & 15 masks the lower 4 bits (binary 1111).

Extending the packet system

To add support for a new packet type:
  1. Create a new file in lib/PacketManager/packets/
  2. Implement __init__, decode(), and encode() methods
  3. Import it in lib/PacketManager/packets/__init__.py
  4. Add handling logic in proxy.py
Example:
# lib/PacketManager/packets/FSNETCMD_MYPACKET.py
from struct import pack, unpack

class FSNETCMD_MYPACKET:
    def __init__(self, buffer: bytes, should_decode: bool = True):
        self.buffer = buffer
        self.my_field = None
        if should_decode:
            self.decode()
    
    def decode(self):
        self.my_field = unpack("I", self.buffer[4:8])[0]
    
    @staticmethod
    def encode(my_field, with_size: bool = False):
        buffer = pack("II", 99, my_field)  # 99 = packet type ID
        if with_size:
            return pack("I", len(buffer)) + buffer
        return buffer

Build docs developers (and LLMs) love