Skip to main content
This guide covers the complete structure of Sakuya AC plugins with real-world examples from the codebase.

Required Components

Every plugin must have these three components:

1. ENABLED Flag

ENABLED = True  # Must be True for plugin to load
The PluginManager checks this flag during discovery. Set to False to disable a plugin without deleting it.

2. Plugin Class

class Plugin:
    def __init__(self):
        self.plugin_manager = None
        # Initialize plugin-specific data here
The class must be named exactly Plugin (case-sensitive).

3. Register Method

def register(self, plugin_manager):
    """Called by PluginManager during plugin loading"""
    self.plugin_manager = plugin_manager
    # Register hooks and commands here
This method is called once during server startup. Use it to register all hooks and commands.

Plugin Types

Plugins typically fall into these categories:
These plugins intercept and modify game events:
plugins/over_g_damage.py
"""This plugin will cause damage to the aircraft if
it exceeds the g-force limit set in the config file."""
from lib.PacketManager.packets import FSNETCMD_GETDAMAGE, FSNETCMD_TEXTMESSAGE
from config import G_LIM
import time

# CONFIG
ENABLED = True  # Enable or disable the plugin
INTERVAL = 0.3  # Interval of second before G Limit check is enforced

class Plugin:
    def __init__(self):
        self.plugin_manager = None

    def register(self, plugin_manager):
        self.plugin_manager = plugin_manager
        self.plugin_manager.register_hook('on_flight_data', self.on_receive)

    def on_receive(self, data, player, messages_to_client, *args):
        if ENABLED:
            if abs(player.aircraft.last_packet.g_value) > G_LIM:
                if time.time() - player.aircraft.last_over_g_message > INTERVAL:
                    player.aircraft.last_over_g_message = time.time()
                    damage_packet = FSNETCMD_GETDAMAGE.encode(
                        player.aircraft.id,
                        1, 1,
                        player.aircraft.id,
                        1, 11, 0, True
                    )
                    warning_message = FSNETCMD_TEXTMESSAGE.encode(
                        f"You are exceeding the G Limit for the aircraft!, gValue = {player.aircraft.last_packet.g_value}!",
                        True
                    )
                    messages_to_client.append(damage_packet)
                    messages_to_client.append(warning_message)
        return True
Key Features:
  • Hooks into on_flight_data to monitor aircraft state
  • Checks G-force limits from config
  • Injects damage packets and warning messages
  • Uses time-based throttling to prevent spam

Advanced Example: Radar System

Here’s a complete, production-ready plugin that implements radar visibility:
plugins/radar.py
"""
This plugin allows you to have Radar features similar to that of
original WW3 server.
"""
import math
from lib.PacketManager.packets import FSNETCMD_AIRPLANESTATE
from lib.YSchat import send
import struct
import traceback

ENABLED = True
RADIUS = 9000  # in meters, within this range planes with different IFF can see each other

flying_players = {}

class Plugin:
    def __init__(self):
        self.plugin_manager = None

    def register(self, plugin_manager):
        self.plugin_manager = plugin_manager
        self.plugin_manager.register_hook('on_flight_data_server', self.on_flight_data_server)
        self.plugin_manager.register_hook('on_flight_data', self.on_flight_data)
        self.plugin_manager.register_hook('on_unjoin', self.on_unjoin)

    def on_flight_data(self, data, player, message_to_client, message_to_server):
        try:
            flying_players[player.aircraft.id][1] = FSNETCMD_AIRPLANESTATE(data).position
        except KeyError:
            return True
        return True

    def on_flight_data_server(self, data, player, message_to_client, message_to_server):
        try:
            # Track new players
            if player.aircraft.id not in flying_players and player.aircraft.id != -1:
                flying_players[player.aircraft.id] = [player, []]
                return True
            elif player.aircraft.id == -1:
                return True

            decode = FSNETCMD_AIRPLANESTATE(data)

            if len(flying_players[player.aircraft.id][1]) == 0:
                return True
            elif decode.player_id == player.aircraft.id:
                return True
            elif flying_players[decode.player_id][0].iff == player.iff:
                return True  # Same team, always visible
            elif self.in_range(flying_players[player.aircraft.id][1], decode.position):
                return True  # In radar range
            else:
                # Out of range, send fake position
                position_data = struct.pack("3f", 10e8, 200, 10e8)
                if decode.packet_version == 4 or 5:
                    updated_data = data[:14] + position_data + data[26:]
                else:
                    updated_data = data[:16] + position_data + data[28:]

                player.streamWriterObject.write(send(updated_data))
                return False  # Block original packet
        except Exception as e:
            if isinstance(e, KeyError):
                return True
            traceback.print_exc()

    def on_unjoin(self, data, player, message_to_client, message_to_server):
        if player.aircraft.id in flying_players:
            flying_players.pop(player.aircraft.id)
        return True

    @staticmethod
    def in_range(pos1, pos2):
        """Calculate 3D distance between two positions"""
        dist = math.sqrt(
            (pos1[0] - pos2[0])**2 + 
            (pos1[1] - pos2[1])**2 + 
            (pos1[2] - pos2[2])**2
        )
        return dist <= RADIUS
Advanced Techniques Used:
  • Global state dictionary to track all players
  • Multiple hook registrations for different events
  • Direct manipulation of binary packet data with struct
  • Distance calculations for game logic
  • Cleanup on player disconnect

Hook Parameters

All hook callbacks receive these parameters:
def hook_callback(self, data, player, message_to_client, message_to_server, *args, **kwargs):
    pass
ParameterTypeDescription
databytesRaw packet data
playerPlayerPlayer object with .username, .aircraft, .iff
message_to_clientlistQueue of packets to send to client
message_to_serverlistQueue of packets to send to server

Command Parameters

All command callbacks receive these parameters:
def command_callback(self, full_message, player, message_to_client, message_to_server):
    pass
ParameterTypeDescription
full_messagestrComplete message including command
playerPlayerPlayer who sent the command
message_to_clientlistQueue of packets to send to client
message_to_serverlistQueue of packets to send to server

Best Practices

Put configuration constants at the top of the file:
ENABLED = True
INTERVAL = 0.3
RADIUS = 9000
DEBUG_MODE = False
This makes it easy for server admins to adjust settings without diving into code.
Always clean up state when players disconnect:
def register(self, plugin_manager):
    self.plugin_manager.register_hook('on_unjoin', self.on_unjoin)

def on_unjoin(self, data, player, message_to_client, message_to_server):
    # Clean up player-specific data
    if player.id in self.player_data:
        del self.player_data[player.id]
    return True
Document your plugin’s purpose and each method:
"""
Over-G Damage Plugin

Applies damage to aircraft that exceed configured G-force limits.
Useful for realistic flight servers.
"""

def on_receive(self, data, player, messages_to_client, *args):
    """Check G-forces and apply damage if needed"""
    pass
Wrap risky operations in try-except blocks:
def on_flight_data(self, data, player, message_to_client, message_to_server):
    try:
        # Process data
        decode = FSNETCMD_AIRPLANESTATE(data)
        # ... your logic
    except KeyError:
        return True  # Continue normally
    except Exception as e:
        logging.error(f"Plugin error: {e}")
        traceback.print_exc()
        return True
  • Compile regexes in __init__(), not in hook callbacks
  • Use time-based throttling for frequent events
  • Avoid expensive operations in on_flight_data hooks
  • Cache calculated values when possible
def __init__(self):
    self.plugin_manager = None
    self.regex = re.compile(pattern)  # Compile once
    self.last_check = {}

Plugin Registration Methods

register_hook(hook_name, callback)

Register a callback for an event hook:
self.plugin_manager.register_hook('on_chat', self.on_chat)

register_command(command_name, callback, help_text, alias)

Register a chat command:
self.plugin_manager.register_command(
    'refuel',
    self.refuel_command,
    help_text="Refuel your aircraft",
    alias='rf'  # Optional: /rf works too
)
The help_text automatically adds your command to /help output

Testing Your Plugin

  1. Enable Debug Logging: Add print statements to track execution:
    def on_chat(self, data, player, message_to_client, message_to_server):
        print(f"[DEBUG] Chat hook triggered by {player.username}")
        return True
    
  2. Check Server Logs: Look for plugin load messages:
    [INFO] Loaded plugin my_plugin
    
  3. Test Edge Cases:
    • What happens when player disconnects during operation?
    • What if packet data is malformed?
    • How does it behave with multiple players?
  4. Use ENABLED Flag: Quickly disable problematic plugins:
    ENABLED = False  # Disable without deleting
    

Next Steps

Network Protocol

Learn about YSFlight packet structures

Packet Manager

Reference for encoding and decoding packets

Build docs developers (and LLMs) love