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:
- Message checked for prefix
- If found, packet blocked from server (
data = None)
- Command word extracted (first word minus prefix)
- Command handler called asynchronously
- 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:
- Check for built-in commands (like
help)
- Pass to plugin manager for plugin commands
- Return error if no handler found
Built-in Commands
/help
Displays available commands from all loaded plugins.
Usage:
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
| Parameter | Type | Description |
|---|
player | Player | Player object who sent command |
full_message | String | Complete message including command and args |
message_to_client | List | Queue for messages to send to player |
message_to_server | List | Queue 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:
- Clear syntax - Use consistent argument patterns
- Help text - Provide usage examples in error messages
- Validation - Check arguments before processing
- Feedback - Always confirm command execution
- Permissions - Implement admin checks for dangerous commands
- 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