Admin Plugin Development
This guide covers creating custom admin plugins for NodeMod using the AMX Mod X-style plugin system.
Plugin Structure
Admin plugins extend BasePlugin and implement the Plugin interface. The plugin loader automatically instantiates plugins listed in plugins.ini.
Basic Plugin Template
import { BasePlugin } from './baseplugin';
import { Plugin, PluginMetadata } from './pluginloader';
import { ADMIN_CFG } from './constants';
import { adminSystem } from './admin';
class MyAdminPlugin extends BasePlugin implements Plugin {
// Required: Plugin metadata
readonly metadata: PluginMetadata = {
name: 'My Admin Plugin',
version: '1.0.0',
author: 'Your Name',
description: 'Custom admin functionality'
};
constructor(pluginName: string) {
// Pass plugin name for localization dictionary
super(pluginName);
// Register commands
this.registerCommand('amx_mycommand', ADMIN_CFG, '<args> - description',
(entity, args) => this.handleMyCommand(entity, args));
// Register CVARs (optional)
this.registerCvar('amx_myvar', '1', nodemod.FCVAR.SERVER, 'My variable');
}
private handleMyCommand(entity: nodemod.Entity | null, args: string[]) {
// Check access (shows error message if denied)
if (!adminSystem.cmdAccess(entity, ADMIN_CFG)) return;
// Your command logic here
this.sendConsole(entity, 'Command executed!');
}
}
// Export the class as default - plugin loader will instantiate it
export default MyAdminPlugin;
Plugin Lifecycle
- Constructor: Receives
pluginNamefrom loader, register commands/CVARs - onLoad(): Called after plugin is instantiated (optional)
- onUnload(): Called when plugin is deactivated (optional)
Using the Admin System
Access Checking
import { adminSystem } from './admin';
import { ADMIN_KICK, ADMIN_BAN, ADMIN_SLAY } from './constants';
// Silent check (for conditional logic)
if (adminSystem.hasAccess(entity, ADMIN_KICK)) {
// Player has kick permission
}
// Command check (shows error message if denied)
if (!adminSystem.cmdAccess(entity, ADMIN_KICK)) {
return; // Player notified of denial
}
Finding Target Players
Use cmdTarget to find players with proper validation:
import {
CMDTARGET_OBEY_IMMUNITY,
CMDTARGET_ALLOW_SELF,
CMDTARGET_ONLY_ALIVE,
CMDTARGET_NO_BOTS
} from './constants';
// Find player respecting immunity
const target = adminSystem.cmdTarget(admin, targetArg, CMDTARGET_OBEY_IMMUNITY);
if (!target) return; // Error message already sent
// Find player, allow self-targeting
const target = adminSystem.cmdTarget(admin, targetArg, CMDTARGET_ALLOW_SELF);
// Combine flags
const target = adminSystem.cmdTarget(admin, targetArg,
CMDTARGET_OBEY_IMMUNITY | CMDTARGET_ONLY_ALIVE);
Getting Player Information
// Get all connected players
const players = adminSystem.getPlayers();
// Filter options
const alivePlayers = adminSystem.getPlayers({ onlyAlive: true });
const humanPlayers = adminSystem.getPlayers({ excludeBots: true });
// Check player states
if (adminSystem.isBot(entity)) { /* is a bot */ }
if (adminSystem.isAlive(entity)) { /* is alive */ }
// Get user flags
const flags = adminSystem.getUserFlags(entity);
BasePlugin Helpers
The BasePlugin class provides many helpful methods:
Messaging
// Console message (to player or server log)
this.sendConsole(entity, 'Message here');
// Chat message (to player or all if null)
this.sendChat(entity, 'Chat message');
// Show admin activity (respects amx_show_activity)
this.showActivity(entity, 'kicked Player');
Localization
// Get localized string for player's language
const msg = this.getLang(entity, 'MY_KEY', arg1, arg2);
// Try plugin dictionary, fallback to common
const msg = this.getLangWithFallback(entity, 'KEY_NAME');
Utilities
// Get admin name or 'CONSOLE'
const name = this.getAdminName(entity);
// Get player name or 'Unknown'
const name = this.getPlayerName(entity);
// Get current game time
const time = this.getGameTime();
// Parse command arguments
const args = this.parseCommand(text);
// Log to AMXX log
this.logAmx('Action performed');
Registering Commands
Commands are registered with automatic help system integration:
// Full registration with all options
this.registerCommand(
'amx_mycommand', // Command name
ADMIN_CFG, // Required access flags
'<target> [option] - desc', // Usage (for amx_help)
(entity, args) => {
this.handleCommand(entity, args);
}
);
For client-only commands (not in help):
nodemodCore.cmd.registerClient('clientcmd', (entity, args) => {
// Handle client command
});
Registering CVARs
// Register with flags and description
this.registerCvar(
'amx_myvar', // CVAR name
'1', // Default value
nodemod.FCVAR.SERVER, // Flags
'Description here' // Description
);
// Use the CVAR
const wrapped = nodemodCore.cvar.wrap('amx_myvar');
const value = wrapped.value; // string
const intVal = wrapped.int; // number
const floatVal = wrapped.float; // number
Handling Events
// Player connected
nodemod.on('dllClientPutInServer', (entity: nodemod.Entity) => {
console.log(`Player joined: ${entity.netname}`);
});
// Player disconnected
nodemod.on('dllClientDisconnect', (entity: nodemod.Entity) => {
console.log(`Player left: ${entity.netname}`);
});
// Client command (say, etc.)
nodemod.on('dllClientCommand', (entity: nodemod.Entity, text: string) => {
if (text.startsWith('!myplugin')) {
// Handle custom command
// Block the command from processing further
nodemod.setMetaResult(nodemod.META_RES.SUPERCEDE);
}
});
Example: Simple Kick Plugin
import { BasePlugin } from './baseplugin';
import { Plugin, PluginMetadata } from './pluginloader';
import { ADMIN_KICK, CMDTARGET_OBEY_IMMUNITY } from './constants';
import { adminSystem } from './admin';
class SimpleKickPlugin extends BasePlugin implements Plugin {
readonly metadata: PluginMetadata = {
name: 'Simple Kick',
version: '1.0.0',
author: 'Example',
description: 'Simple kick command'
};
constructor(pluginName: string) {
super(pluginName);
this.registerCommand('amx_kick', ADMIN_KICK, '<player> [reason]',
(entity, args) => this.cmdKick(entity, args));
}
private cmdKick(admin: nodemod.Entity | null, args: string[]) {
if (!adminSystem.cmdAccess(admin, ADMIN_KICK)) return;
if (args.length < 1) {
this.sendConsole(admin, 'Usage: amx_kick <player> [reason]');
return;
}
const target = adminSystem.cmdTarget(admin, args[0], CMDTARGET_OBEY_IMMUNITY);
if (!target) return;
const reason = args.slice(1).join(' ') || 'Kicked by admin';
const userId = nodemod.eng.getPlayerUserId(target);
// Show activity to all players
this.showActivity(admin, `kicked ${target.netname}`);
// Execute kick
nodemod.eng.serverCommand(`kick #${userId} "${reason}"\n`);
// Log action
this.logAmx(`Kick: "${this.getAdminName(admin)}" kicked "${target.netname}" (reason "${reason}")`);
}
}
// Export the class - plugin loader instantiates it automatically
export default SimpleKickPlugin;
Plugin Configuration
plugins.ini
Add your plugin to configs/plugins.ini:
; My custom plugin
myplugin
The plugin loader will look for src/admin/myplugin.ts and instantiate its default export.
Adding CVARs to amxx.cfg
Add default values to configs/amxx.cfg:
// My Admin Plugin Settings
amx_myvar 1
amx_myother "default"
Localization
Create data/lang/myplugin.txt:
[en]
KICKED_PLAYER = %s was kicked by %s
REASON = Reason: %s
NO_ACCESS = You do not have access to this command
[de]
KICKED_PLAYER = %s wurde von %s gekickt
REASON = Grund: %s
NO_ACCESS = Sie haben keinen Zugriff auf diesen Befehl
Use in code:
const msg = this.getLang(entity, 'KICKED_PLAYER', targetName, adminName);
Best Practices
- Always check access using
cmdAccess()orhasAccess() - Use cmdTarget() for player targeting with immunity checks
- Log admin actions using
logAmx() - Show activity using
showActivity()for transparency - Support localization for multi-language servers
- Handle null entities (server console) gracefully
- Use constants instead of magic numbers
- Export default class - don't manually register with pluginLoader