Skip to main content

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

  1. Constructor: Receives pluginName from loader, register commands/CVARs
  2. onLoad(): Called after plugin is instantiated (optional)
  3. 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

  1. Always check access using cmdAccess() or hasAccess()
  2. Use cmdTarget() for player targeting with immunity checks
  3. Log admin actions using logAmx()
  4. Show activity using showActivity() for transparency
  5. Support localization for multi-language servers
  6. Handle null entities (server console) gracefully
  7. Use constants instead of magic numbers
  8. Export default class - don't manually register with pluginLoader