Skip to main content

Overview

The PluginManager class is responsible for discovering, loading, and managing plugins in Sakuya AC. It provides a system for registering hooks, commands, and managing plugin lifecycle.

Constructor

PluginManager(connected_players)
Creates a new PluginManager instance and automatically loads all enabled plugins.
connected_players
dict | list
required
Reference to the connected players collection for plugin access
Behavior:
  • Initializes empty plugin registry
  • Initializes hook system
  • Registers default ‘help’ command
  • Automatically calls load_plugins() to discover and load plugins

Properties

plugins
dict
default:"{}"
Dictionary mapping plugin names to plugin instances
hooks
dict
default:"{}"
Dictionary mapping hook names to lists of callback functions
commands
dict
default:"{'help': None}"
Dictionary mapping command names to callback functions
help_message
str
default:"'List of Available Commands:\\n'"
Auto-generated help text containing all registered commands and descriptions
connected_players
dict | list
Reference to connected players collection

Methods

load_plugins

load_plugins() -> None
Scans the plugins directory and loads all enabled plugins. Behavior:
  • Scans ../plugins directory relative to library location
  • Supports both single-file plugins (.py) and package plugins (directories with __init__.py)
  • Only loads plugins with ENABLED = True attribute
  • Requires plugins to have a Plugin class
  • Calls register_plugin() for each loaded plugin
  • Logs successful plugin loads
Plugin Requirements:
  • Must have ENABLED = True module-level attribute
  • Must have a Plugin class
  • Plugin class must have a register(plugin_manager) method
Example plugin structure:
# plugins/my_plugin.py
ENABLED = True

class Plugin:
    def register(self, plugin_manager):
        plugin_manager.register_command("mycommand", self.my_command)
    
    def my_command(self, full_message, player, msg_to_client, msg_to_server):
        msg_to_client.append("Command executed!")

register_plugin

register_plugin(plugin) -> None
Registers a plugin instance with the plugin manager.
plugin
object
required
Plugin instance with a register(plugin_manager) method
Behavior:
  • Calls the plugin’s register() method, passing self as argument
  • Plugin’s register method should call register_hook() and/or register_command() to integrate with the system
Example:
class MyPlugin:
    def register(self, pm):
        pm.register_hook("on_damage", self.on_damage_hook)
        pm.register_command("heal", self.heal_command, "Heals your aircraft")
    
    def on_damage_hook(self, data, *args, **kwargs):
        print(f"Damage detected: {data}")
        return True
    
    def heal_command(self, msg, player, to_client, to_server):
        to_client.append("Aircraft healed!")

plugin = MyPlugin()
plugin_manager.register_plugin(plugin)

register_hook

register_hook(hook_name: str, callback) -> None
Registers a callback function for a specific hook.
hook_name
str
required
Name of the hook to register (e.g., “on_packet_received”, “on_player_join”)
callback
callable
required
Callback function to execute when hook is triggered. Should accept (data, *args, **kwargs) and return bool
Callback Signature:
def callback(data, *args, **kwargs) -> bool:
    # Return True to keep original data
    # Return False to block/cancel the event
    pass
Example:
def on_message_hook(data, player, *args, **kwargs):
    print(f"Message from {player.username}: {data}")
    if "blocked_word" in data:
        return False  # Block message
    return True  # Allow message

plugin_manager.register_hook("on_message", on_message_hook)

register_command

register_command(command_name: str, callback, help_text: str = "No Description", alias: str = None) -> None
Registers a chat command with optional alias.
command_name
str
required
Primary command name (without leading slash)
callback
callable
required
Callback function to execute when command is triggered
help_text
str
default:"'No Description'"
Description text shown in help command
alias
str
Optional alias for the command (e.g., “tp” for “teleport”)
Callback Signature:
def callback(full_message: str, player: Player, message_to_client: list, message_to_server: list) -> None:
    pass
Behavior:
  • Logs warning if command name already registered (ignores duplicate)
  • Logs warning if alias already registered (ignores duplicate)
  • Automatically updates help_message with command info
  • Format in help: /command_name [alias] : help_text
Example:
def teleport_cmd(full_msg, player, to_client, to_server):
    # Parse coordinates from full_msg
    parts = full_msg.split()
    if len(parts) == 4:
        x, y, z = float(parts[1]), float(parts[2]), float(parts[3])
        player.aircraft.set_position([x, y, z])
        to_client.append(f"Teleported to {x}, {y}, {z}")
    else:
        to_client.append("Usage: /teleport <x> <y> <z>")

plugin_manager.register_command(
    "teleport",
    teleport_cmd,
    "Teleport to coordinates: /teleport <x> <y> <z>",
    alias="tp"
)

# Now both /teleport and /tp work
# help_message will show: "/teleport [tp] : Teleport to coordinates: /teleport <x> <y> <z>"

triggar_hook

triggar_hook(hook_name: str, data, *args, **kwargs) -> bool
Note: Method name has typo “triggar” instead of “trigger” in source code
Triggers all callbacks registered for a specific hook.
hook_name
str
required
Name of the hook to trigger
data
any
required
Data to pass to hook callbacks
*args
any
Additional positional arguments to pass to callbacks
**kwargs
any
Additional keyword arguments to pass to callbacks
return
bool
True if all callbacks returned True or non-boolean, False if any callback returned False
Behavior:
  • Executes all callbacks for the hook in registration order
  • If any callback returns False, sets return value to False
  • Logs warning if callback returns non-boolean value
  • Useful for packet filtering: return False to block packet forwarding
Example:
# Register hooks
def validate_packet(packet, player):
    if packet.is_valid():
        return True
    print("Invalid packet blocked")
    return False

def log_packet(packet, player):
    print(f"Packet from {player.username}")
    return True

plugin_manager.register_hook("on_packet", validate_packet)
plugin_manager.register_hook("on_packet", log_packet)

# Trigger hook
packet = get_packet()
should_forward = plugin_manager.triggar_hook("on_packet", packet, player=current_player)

if should_forward:
    forward_packet(packet)
else:
    print("Packet blocked by plugin")

trigger_command

trigger_command(command: str, player: Player, full_message: str, message_to_client: list, message_to_server: list) -> bool
Executes a registered command if it exists.
command
str
required
Command name to execute (without leading slash)
player
Player
required
Player instance who issued the command
full_message
str
required
Complete command message including command name and arguments
message_to_client
list
required
List to append response messages for the client
message_to_server
list
required
List to append messages to forward to server
return
bool
True if command was found and executed, False if command not registered
Example:
# Setup
def heal_command(full_msg, player, to_client, to_server):
    player.aircraft.life = 1000
    to_client.append("Aircraft fully repaired!")

plugin_manager.register_command("heal", heal_command, "Repairs your aircraft")

# Usage
message = "/heal"
command_name = message[1:]  # Remove leading slash

to_client = []
to_server = []

if plugin_manager.trigger_command(command_name, player, message, to_client, to_server):
    # Send messages in to_client back to player
    for msg in to_client:
        send_message(player, msg)
else:
    send_message(player, "Unknown command. Type /help for commands.")

Usage Example

from lib.plugin_manager import PluginManager

# Initialize with connected players reference
connected_players = {}
plugin_manager = PluginManager(connected_players)

# Plugins are auto-loaded from plugins/ directory
print(f"Loaded plugins: {list(plugin_manager.plugins.keys())}")

# Manually register a quick plugin
class QuickPlugin:
    def register(self, pm):
        pm.register_hook("on_damage", self.damage_hook)
        pm.register_command("god", self.god_mode, "Toggle god mode", alias="g")
    
    def damage_hook(self, data, player):
        if hasattr(player, 'god_mode') and player.god_mode:
            return False  # Block damage
        return True
    
    def god_mode(self, msg, player, to_client, to_server):
        player.god_mode = not getattr(player, 'god_mode', False)
        status = "enabled" if player.god_mode else "disabled"
        to_client.append(f"God mode {status}")

quick_plugin = QuickPlugin()
plugin_manager.register_plugin(quick_plugin)

# Trigger hooks
keep_packet = plugin_manager.triggar_hook("on_damage", damage_data, player=player_obj)

# Execute commands
msg = "/god"
to_client = []
to_server = []
if plugin_manager.trigger_command("god", player_obj, msg, to_client, to_server):
    for response in to_client:
        print(response)

# Display help
print(plugin_manager.help_message)

Hook System

The hook system allows plugins to intercept and modify server behavior: Common Hook Patterns:
# Packet filtering hook
def filter_hook(packet, player):
    if should_block(packet):
        return False  # Block packet
    return True  # Allow packet

# Logging hook
def log_hook(data, *args, **kwargs):
    log_to_file(data)
    return True  # Always allow, just observe

# Modification hook
def modify_hook(data, player):
    # Modify data in place
    data.value = transform(data.value)
    return True  # Allow modified data

Command System

Command Best Practices:
def my_command(full_message, player, message_to_client, message_to_server):
    # Parse arguments
    args = full_message.split()[1:]  # Skip command name
    
    # Validate arguments
    if len(args) < 2:
        message_to_client.append("Usage: /mycommand <arg1> <arg2>")
        return
    
    # Execute command logic
    result = do_something(args[0], args[1])
    
    # Send response to client
    message_to_client.append(f"Command executed: {result}")
    
    # Optionally send data to server
    # message_to_server.append(some_packet)

Build docs developers (and LLMs) love