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:
Hook-Based Plugins
Command Plugins
Content Filter Plugins
State-Tracking Plugins
These plugins intercept and modify game events: """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
These plugins add custom chat commands: """
This is an example test command!
"""
from lib import YSchat
from time import sleep
ENABLED = False
class Plugin :
def __init__ ( self ):
self .plugin_manager = None
def register ( self , plugin_manager ):
self .plugin_manager = plugin_manager
self .plugin_manager.register_command( 'test' , self .test)
self .plugin_manager.register_command( 'timer' , self .timer)
def test ( self , full_message , player , message_to_client , message_to_server ):
message_to_client.append(YSchat.message( "Test command received" ))
return True
def timer ( self , full_message , player , message_to_client , message_to_server ):
sleep( 5 )
message_to_client.append(YSchat.message( "Timer ended" ))
return True
Key Features:
Registers multiple commands with register_command()
Commands receive full message, player, and message queues
Can parse command arguments from full_message
Send responses via message_to_client
These plugins modify data passing through the server: """
Enables moderation of words in chat to make it safe for children
"""
from lib.PacketManager.packets import FSNETCMD_TEXTMESSAGE
import re
ENABLED = True
ABOVE_13 = False
class Plugin :
def __init__ ( self ):
self .plugin_manager = None
if ABOVE_13 :
# Moderate less for teens/adults
moderation_string = r ' \b ( inappropriate | words | here ) \b '
else :
# Stricter moderation for children
moderation_string = r ' \b ( more | strict | filtering ) \b '
self .filter_regex = re.compile(moderation_string, re. IGNORECASE )
def register ( self , plugin_manager ):
self .plugin_manager = plugin_manager
self .plugin_manager.register_hook( 'on_chat' , self .on_chat)
@ staticmethod
def censor_match_dynamic_length ( match_obj ):
"""Replace matched words with # characters"""
matched_word = match_obj.group( 0 )
return '#' * len (matched_word)
def on_chat ( self , data , player , message_to_client , message_to_server ):
decode = FSNETCMD_TEXTMESSAGE(data)
msg = decode.raw_message
# Check if the message contains filtered content
censored_text = self .filter_regex.sub(
self .censor_match_dynamic_length, msg
)
if censored_text != msg:
# Create a new message with censored content
message = FSNETCMD_TEXTMESSAGE .encode(censored_text, with_size = True )
message_to_server.append(message)
return False # Block original message
return True # Allow original message
Key Features:
Compiles regex patterns in __init__() for performance
Decodes packet to access message content
Censors inappropriate content
Re-encodes modified packet with with_size=True
Returns False to block original, sends modified version
These plugins maintain server-side state: plugins/crash_on_ground.py
# This plugin renders the dead plane until it crashes on ground
# Thus disabling mid air despawning when dead
from lib.PacketManager.packets import (
FSNETCMD_REMOVEAIRPLANE ,
FSNETCMD_READBACK ,
FSNETCMD_GETDAMAGE
)
ENABLED = True
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_remove_airplane_server' , self .on_death)
def on_death ( self , data , player , message_to_client , message_to_server ):
if player.aircraft.id != - 1 :
if player.aircraft.id == FSNETCMD_REMOVEAIRPLANE(data).object_id:
return True
else :
decode = FSNETCMD_REMOVEAIRPLANE(data)
# Send damage instead of removing
b = FSNETCMD_GETDAMAGE .encode(
decode.object_id, 1 , 1 ,
player.aircraft.id, 10000 ,
11 , 0 , True
)
message_to_client.append(b)
message_to_server.append(b)
a = FSNETCMD_READBACK .encode( 2 , player.aircraft.id, True )
message_to_server.append(a)
return False # Block removal packet
else :
return True
Key Features:
Intercepts aircraft removal packets
Replaces removal with damage packets
Allows aircraft to fall to ground realistically
Modifies both client and server message queues
Advanced Example: Radar System
Here’s a complete, production-ready plugin that implements radar visibility:
"""
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
Parameter Type Description 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
Parameter Type Description 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
Configuration at Module Level
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
Performance Considerations
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
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
Check Server Logs : Look for plugin load messages:
[INFO] Loaded plugin my_plugin
Test Edge Cases :
What happens when player disconnects during operation?
What if packet data is malformed?
How does it behave with multiple players?
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