Guides

Client preferences

Per-player settings that survive reconnects and map changes — registering a cookie, reading and writing it, knowing when a player's values are loaded.

ISource2ModCookies is SourceMod's clientprefs. A cookie is a named string setting (myplugin_hud, quake_sounds); every player has their own value of it, kept in a database under their SteamID and loaded into memory when the player is authorized.

plugin.cpp
#include "source2mod/ISource2ModCookies.h"

In SourcePawn everything is on the Source2ModCookies methodmap from #include <source2mod/api>.

Register once, when the plugin loads, and keep the id. Registering a name that is already registered — by this plugin or any other — returns the same cookie: cookies are shared by name.

plugin.cpp
Source2ModCookieId g_HudCookie = 0;

// Load()
g_HudCookie = g_pSource2ModCookies->RegisterCookie("myplugin_hud", "Speed HUD: 1 on, 0 off", SOURCE2MOD_COOKIE_PUBLIC);

The id is 0 for an empty name or one longer than 30 bytes. FindCookie / Source2ModCookies.Find look a cookie up by name without registering it.

The access level says who may see and change the cookie from the cookie commands — what a plugin may do with it is not limited:

C++SourcePawn
SOURCE2MOD_COOKIE_PUBLICCookieAccess_PublicPlayers can read and change it.
SOURCE2MOD_COOKIE_PROTECTEDCookieAccess_ProtectedPlayers can read it, only plugins change it.
SOURCE2MOD_COOKIE_PRIVATECookieAccess_PrivateHidden from players.
Limit
Name30 bytes (SOURCE2MOD_COOKIE_MAX_NAME_LENGTH)
Value100 bytes (SOURCE2MOD_COOKIE_MAX_VALUE_LENGTH)

Reading and writing

Values are strings. GetCookie returns false and "" when the player never had a value — that is where your default applies. SetCookie stores the value and writes it to the database right away, asynchronously; nothing is lost on a crash or a map change.

plugin.cpp
// Load()
g_pSource2ModCommands->RegisterConCommand("hud", [](const Source2ModCommandContext& ctx, const Source2ModCommandArgs& args, bool post)
{
    CPlayerSlot slot = ctx.GetPlayerSlot();
    if (!slot.IsValid())
        return;

    if (!g_pSource2ModCookies->AreCookiesCached(slot))
    {
        REPLY_TO_COMMAND(ctx, "Your settings are still loading, try again in a moment.");
        return;
    }

    char value[SOURCE2MOD_COOKIE_MAX_VALUE_LENGTH + 1];
    g_pSource2ModCookies->GetCookie(slot, g_HudCookie, value, sizeof(value));
    const bool on = strcmp(value, "0") != 0; // no value yet = the default, on

    g_pSource2ModCookies->SetCookie(slot, g_HudCookie, on ? "0" : "1");
    REPLY_TO_COMMAND(ctx, "Speed HUD %s.", on ? "off" : "on");
});

SetCookie returns false for an empty slot, a bot, a bad id or a value over 100 bytes. GetCookieTime / Source2ModCookies.GetTime is when the value was last set (Unix time, 0 for never).

When values are ready

A player's values are loaded once per connection, after OnClientAuthorized — do not count on them in OnClientConnect or OnClientPutInServer. Until then:

  • AreCookiesCached(slot) is false;
  • reads return false and "", the same as "no value";
  • writes are kept and saved once the player's values arrived.

React to the load instead of polling: a cached listener in C++, the Source2Mod_OnClientCookiesCached forward in SourcePawn. Both run on the main thread. A plugin loaded while players are already on the server walks them once itself — their listener call has already happened.

plugin.cpp
bool g_bHud[64];

static void ApplyHud(CPlayerSlot slot)
{
    char value[SOURCE2MOD_COOKIE_MAX_VALUE_LENGTH + 1];
    g_bHud[slot.Get()] = !g_pSource2ModCookies->GetCookie(slot, g_HudCookie, value, sizeof(value))
                      || strcmp(value, "0") != 0;
}

// Load()
g_pSource2ModCookies->AddCookiesCachedListener([](CPlayerSlot slot) { ApplyHud(slot); });

if (late)
{
    for (int i = 0; i < 64; i++)
        if (g_pSource2ModCookies->AreCookiesCached(CPlayerSlot(i)))
            ApplyHud(CPlayerSlot(i));
}

The listener belongs to the plugin and goes when it unloads; RemoveCookiesCachedListener(id) takes it down earlier.

Players who are not connected

SetCookieBySteamID writes the value of a player who need not be on the server — an admin command, a web shop handing out a setting.

plugin.cpp
g_pSource2ModCookies->SetCookieBySteamID(76561198012345678ull, g_HudCookie, "0");

The database

Values live in an SQLite file, addons/source2mod/data/clientprefs.sq3, unless ClientPrefsDatabase in configs/core.json names another:

configs/core.json
{
  "ClientPrefsDatabase": "data/clientprefs.sq3"
}

The tables are SourceMod's (sm_cookies, sm_cookie_cache) and players are keyed by STEAM_1:Y:Z, so a SourceMod clientprefs database can be carried over as it is.

IsConnected() says whether the database is open. Without it everything still works, only nothing is loaded or saved: values last as long as the player stays. The feature check is the clientprefs capability (see Features).

SourceMod's own clientprefs.inc names (Cookie, RegClientCookie, OnClientCookiesCached, ...) work on the same cookies, so a ported script needs no rewrite.

Pitfalls

  • "No value" and "not loaded" look the same. Before AreCookiesCached is true, GetCookie returns false and "". A plugin that reads in OnClientPutInServer and saves the default back overwrites the player's real value once it arrives. Read in the cached callback.
  • Names are global. Two plugins registering hud share one value. Prefix cookies with your plugin's name.
  • The first registration wins. Registering an existing name returns the existing cookie; the description and access level passed the second time are not applied.
  • Bots have no cookies. SetCookie returns false for them.
  • Values are short strings. 100 bytes at most — store a number or a few flags, not a JSON document. Larger per-player data belongs in your own table (SQL).

Reference

ISource2ModCookies — every call with its parameters.

On this page