Skip to main content
Sakuya AC includes a flexible command system that allows players to interact with the server and plugins through chat commands.

Overview

The chat command system intercepts messages starting with a configurable prefix and routes them to appropriate handlers, either built-in or plugin-provided.

Configuration

Configure the command prefix in config.py:
# Command Prefix for the server
# When commands are entered in chat, this will be treated
# as command when it starts with the prefix
# eg. if PREFIX = "/"
# /help will be treated as command

PREFIX = "/"
You can use any prefix character(s) you prefer: /, !, ., >>, etc.

How It Works

Command Detection

When a player sends a chat message, the proxy checks if it starts with the prefix:
elif packet_type == "FSNETCMD_TEXTMESSAGE":
    msg = FSNETCMD_TEXTMESSAGE(packet)
    
    finalMsg = (f"{player.username} : {msg.message}")
    if msg.message.startswith(PREFIX):
        data = None
        command = msg.message.split(" ")[0][1:]
        asyncio.create_task(triggerCommand.triggerCommand(
            command, msg.message, player, 
            message_to_client, message_to_server, plugin_manager
        ))
        info(f"Command {command} triggered by {player.username}")
Command processing:
  1. Message checked for prefix
  2. If found, packet blocked from server (data = None)
  3. Command word extracted (first word minus prefix)
  4. Command handler called asynchronously
  5. Command logged to console
Commands do not appear in server chat - they’re intercepted before reaching the server.

Command Handler

The command handler in lib/triggerCommand.py:
async def triggerCommand(command, full_message, player, message_to_client, message_to_server, plugin_manager):
    debug(f"{player.username} triggered command {command}")
    if command == "help":
        message_to_client.append(YSchat.message(plugin_manager.help_message))
        return 0
    
    h = plugin_manager.trigger_command(command, player, full_message, message_to_client, message_to_server)
    if not h:
        message_to_client.append(YSchat.message(f"Command not found, Type {PREFIX}help for all commands."))
Handler flow:
  1. Check for built-in commands (like help)
  2. Pass to plugin manager for plugin commands
  3. Return error if no handler found

Built-in Commands

/help

Displays available commands from all loaded plugins. Usage:
/help
Example output:
Available commands:
/help - Show this message
/tele - Teleport to coordinates (plugin)
/repair - Repair your aircraft (plugin)
The help message is automatically generated from all registered plugin commands.

Creating Plugin Commands

Plugins can register commands with the plugin manager:
class Plugin:
    def __init__(self):
        self.plugin_manager = None
    
    def register(self, plugin_manager):
        self.plugin_manager = plugin_manager
        # Register command
        self.plugin_manager.register_command(
            'mycommand',           # Command name
            self.handle_command,   # Handler function
            'Description of what it does'  # Help text
        )
    
    def handle_command(self, player, full_message, message_to_client, message_to_server):
        # Parse command arguments
        args = full_message.split()[1:]  # Skip command word
        
        # Send response to player
        message_to_client.append(YSchat.message(f"Command executed with args: {args}"))
        
        return True  # Command handled successfully

Command Handler Parameters

ParameterTypeDescription
playerPlayerPlayer object who sent command
full_messageStringComplete message including command and args
message_to_clientListQueue for messages to send to player
message_to_serverListQueue for packets to send to server

Return Values

  • True - Command was handled successfully
  • False or None - Command not found/not handled

Example Plugin Commands

Teleport Command

def register(self, plugin_manager):
    plugin_manager.register_command('tele', self.teleport, 'Teleport to X Y Z coordinates')

def teleport(self, player, full_message, message_to_client, message_to_server):
    args = full_message.split()[1:]
    
    if len(args) != 3:
        message_to_client.append(YSchat.message("Usage: /tele X Y Z"))
        return True
    
    try:
        x, y, z = float(args[0]), float(args[1]), float(args[2])
        # Create teleport packet
        teleport_packet = create_teleport_packet(player.aircraft.id, x, y, z)
        message_to_server.append(teleport_packet)
        message_to_client.append(YSchat.message(f"Teleporting to {x}, {y}, {z}"))
    except ValueError:
        message_to_client.append(YSchat.message("Invalid coordinates!"))
    
    return True

Status Command

def register(self, plugin_manager):
    plugin_manager.register_command('status', self.show_status, 'Show your aircraft status')

def show_status(self, player, full_message, message_to_client, message_to_server):
    life = player.aircraft.life
    pos = player.aircraft.last_packet.position
    
    status = f"Health: {life}% | Position: {pos[0]:.0f}, {pos[1]:.0f}, {pos[2]:.0f}"
    message_to_client.append(YSchat.message(status))
    
    return True

Admin Command with Permissions

ADMIN_IPS = ['192.168.1.100', '10.0.0.5']

def register(self, plugin_manager):
    plugin_manager.register_command('kick', self.kick_player, 'Kick a player (admin only)')

def kick_player(self, player, full_message, message_to_client, message_to_server):
    if player.ip not in ADMIN_IPS:
        message_to_client.append(YSchat.message("You don't have permission!"))
        return True
    
    args = full_message.split()[1:]
    if not args:
        message_to_client.append(YSchat.message("Usage: /kick <username>"))
        return True
    
    target_name = args[0]
    # Kick logic here
    message_to_server.append(YSchat.message(f"{target_name} has been kicked by admin."))
    
    return True
Command handlers run asynchronously. Avoid blocking operations or long-running tasks in command handlers.

Argument Parsing

Basic Parsing

args = full_message.split()[1:]  # Split by spaces, skip command word

Advanced Parsing

For commands with quoted arguments or complex syntax:
import shlex

try:
    args = shlex.split(full_message)[1:]
except ValueError:
    message_to_client.append(YSchat.message("Invalid command syntax!"))
    return True
This handles:
  • Quoted strings: /say "Hello World"
  • Escaped characters: /say Hello\\ World

Sending Messages

To Command Sender

message_to_client.append(YSchat.message("Your message here"))

To All Players

message_to_server.append(YSchat.message("Broadcast to everyone"))

To Specific Player

for p in CONNECTED_PLAYERS:
    if p.username == target_name:
        p.streamWriterObject.write(YSchat.message("Direct message"))
        await p.streamWriterObject.drain()
Use message_to_client and message_to_server queues when possible - they’re automatically processed by the proxy loop.

Command Examples

Player Usage

/help
/status
/tele 1000 500 2000
/repair
/info F-16

Admin Usage

/kick PlayerName
/ban 192.168.1.50
/weather rain
/settime 1200

Error Handling

Invalid Arguments

if len(args) < expected_count:
    message_to_client.append(YSchat.message(f"Usage: {PREFIX}command <arg1> <arg2>"))
    return True

Invalid Values

try:
    value = int(args[0])
    if value < 0 or value > 100:
        raise ValueError
except ValueError:
    message_to_client.append(YSchat.message("Invalid value! Must be 0-100."))
    return True

Player State Checks

if player.aircraft.id == -1:
    message_to_client.append(YSchat.message("You must be in an aircraft!"))
    return True

Debugging Commands

Enable debug logging to see command execution:
# config.py
LOGGING_LEVEL = DEBUG
Log output:
INFO [proxy.py:222]: Command tele triggered by PlayerName
DEBUG [triggerCommand.py:6]: PlayerName triggered command tele

Best Practices

Command Design Tips:
  1. Clear syntax - Use consistent argument patterns
  2. Help text - Provide usage examples in error messages
  3. Validation - Check arguments before processing
  4. Feedback - Always confirm command execution
  5. Permissions - Implement admin checks for dangerous commands
  6. Async-safe - Don’t block the event loop

Security Considerations

Important Security Notes:
  • Validate all user input
  • Implement permission systems for admin commands
  • Log command usage for audit trails
  • Rate-limit expensive commands
  • Sanitize arguments before using in packets
  • Never trust client-provided data
  • Commands work alongside all proxy features
  • Can control plugin behavior
  • Integrate with Discord bot for remote commands
  • Access all player and aircraft data

Build docs developers (and LLMs) love