A VIP plugin is a good exercise because it touches every subsystem an admin plugin uses: access flags, spawn hooks, a menu, and persistent storage. This walks through a clean design — flag-gated benefits first, then an optional MySQL layer for VIP that expires. The goal is a plugin you can actually run, not a toy. It targets AMX Mod X 1.9+ and Ham Sandwich for the spawn hook.
1. Pick the VIP flag
Do not invent an auth system — reuse the AMXX flag model. VIP conventionally maps to a custom level flag, most commonly t (ADMIN_LEVEL_H). A user with t in users.ini is a VIP. Read the flag once at load:
#include <amxmodx>
#include <amxmisc>
#include <hamsandwich>
#include <fun>
new const VIP_FLAG = ADMIN_LEVEL_H
bool:is_vip(id) {
return (get_user_flags(id) & VIP_FLAG) ? true : false
}
2. Give the benefits on spawn
Hook Ham_Spawn and apply bonuses after the engine has finished spawning the player (post = 1):
public plugin_init() {
register_plugin("CSB VIP", "1.0", "CSB")
RegisterHam(Ham_Spawn, "player", "OnSpawnPost", 1)
}
public OnSpawnPost(id) {
if (!is_user_alive(id) || !is_vip(id))
return
set_user_health(id, 125)
set_user_armor(id, 100)
client_print_color(id, print_team_default, "^x04[VIP]^x01 Bonus health and armor applied")
}
client_print_color makes this 1.9+; see colored chat messages for the color codes.
3. Add a /vip menu
Register a say handler and build a menu with the AMXX menu API:
public plugin_init() {
// ...
register_clcmd("say /vip", "CmdVipMenu")
}
public CmdVipMenu(id) {
new menu = menu_create("\\rVIP Menu", "VipMenuHandler")
menu_additem(menu, "\\wExtra Health", "1")
menu_additem(menu, "\\wDrop Grenade", "2")
menu_display(id, menu)
return PLUGIN_HANDLED
}
public VipMenuHandler(id, menu, item) {
if (item == MENU_EXIT) {
menu_destroy(menu)
return PLUGIN_HANDLED
}
if (!is_vip(id)) {
client_print(id, print_chat, "[VIP] You are not a VIP.")
} else if (item == 0) {
set_user_health(id, get_user_health(id) + 25)
}
menu_destroy(menu)
return PLUGIN_HANDLED
}
For a fuller, ready-made version see CSB VIP Menu.
4. Persist expiring VIP in MySQL
Flag-based VIP is permanent and manual. For paid, time-limited VIP you want a database. Use the sqlx module (install it via the MySQL module guide) and always use threaded queries — a blocking query freezes the whole server on a slow database.
#include <sqlx>
new Handle:g_tuple
public plugin_cfg() {
g_tuple = SQL_MakeDbTuple("127.0.0.1", "csb", "secret", "csb_vip")
SQL_ThreadQuery(g_tuple, "IgnoreHandler",
"CREATE TABLE IF NOT EXISTS vips (steamid VARCHAR(32) PRIMARY KEY, expires INT)")
}
public client_authorized(id) {
new sid[32], q[128]
get_user_authid(id, sid, charsmax(sid))
formatex(q, charsmax(q), "SELECT expires FROM vips WHERE steamid='%s'", sid)
new data[1]
data[0] = id
SQL_ThreadQuery(g_tuple, "OnVipLookup", q, data, 1)
}
public OnVipLookup(failstate, Handle:query, error[], errnum, data[], size, Float:qtime) {
if (failstate != TQUERY_SUCCESS) {
log_amx("VIP query failed: %s", error)
return
}
new id = data[0]
if (SQL_NumResults(query) && SQL_ReadResult(query, 0) > get_systime())
set_user_flags(id, VIP_FLAG) // grant runtime VIP
}
Storing the SteamID as the key means VIP follows the player, not a slot. The expires column is a Unix timestamp compared against get_systime(), so lapsed VIP simply never gets the flag set.
Common errors
- Server hitches every time a VIP connects — you used
SQL_Execute(blocking) instead ofSQL_ThreadQuery. Always thread player-facing queries. - Benefits never apply — you hooked
Ham_Spawnwith post = 0. Health set pre-spawn is overwritten by the engine. Use post = 1. - Everyone is a VIP — your flag check used
|instead of&, or you compared toADMIN_ALL(which is 0). Useget_user_flags(id) & VIP_FLAG. - Module failed to load: sqlx — the MySQL module is not enabled in
modules.ini. See installing AMXX modules. - VIP granted at runtime is lost on map change — expected:
set_user_flagsis per-session. Re-run the lookup onclient_authorizedevery map, as above.
Verification
Give yourself flag t in users.ini, run amx_reloadadmins, and respawn — you should get the health/armor bonus and the [VIP] line. Type say /vip to open the menu. For the MySQL layer, insert a row with a future expires, reconnect, and confirm the flag is set with amx_who. A row with a past timestamp must leave you as a normal player. Once all three paths behave, compile cleanly per the compile guide and ship it.









