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 │
└────────────────┴────────────────────────────────────────┘
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
| ID | Packet Type | Direction | Description |
|---|
| 1 | FSNETCMD_LOGON | C↔S | Player login/authentication |
| 2 | FSNETCMD_LOGOFF | C→S | Player disconnect |
| 4 | FSNETCMD_LOADFIELD | S→C | Load map/field |
| 5 | FSNETCMD_ADDOBJECT | S→C | Add aircraft/ground object |
| 8 | FSNETCMD_JOINREQUEST | C→S | Request to spawn aircraft |
| 9 | FSNETCMD_JOINAPPROVAL | S→C | Approve aircraft spawn |
| 11 | FSNETCMD_AIRPLANESTATE | C↔S | Aircraft position/state |
| 12 | FSNETCMD_UNJOIN | C→S | Leave aircraft |
| 16 | FSNETCMD_PREPARESIMULATION | S→C | Simulation ready |
| 30 | FSNETCMD_AIRCMD | S↔C | Aircraft command/config |
| 32 | FSNETCMD_TEXTMESSAGE | C↔S | Chat message |
| 36 | FSNETCMD_WEAPONCONFIG | C↔S | Weapon 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
Python’s struct module is used for packing/unpacking binary data:
| Format | Type | Size |
|---|
B | Unsigned byte | 1 byte |
b | Signed byte | 1 byte |
H | Unsigned short | 2 bytes |
h | Signed short | 2 bytes |
I | Unsigned int | 4 bytes |
i | Signed int | 4 bytes |
f | Float | 4 bytes |
d | Double | 8 bytes |
s | String | Variable |
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:
- Create a new file in
lib/PacketManager/packets/
- Implement
__init__, decode(), and encode() methods
- Import it in
lib/PacketManager/packets/__init__.py
- 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