Build a VIP System Plugin (Flags, Menu, MySQL)

August 13, 2025 Daemon666 10 min read 19 просмотров

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 of SQL_ThreadQuery. Always thread player-facing queries.
  • Benefits never apply — you hooked Ham_Spawn with 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 to ADMIN_ALL (which is 0). Use get_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_flags is per-session. Re-run the lookup on client_authorized every 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.

Участники: Daemon666 ✦
Поделиться: