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.
#include "source2mod/ISource2ModCookies.h"In SourcePawn everything is on the Source2ModCookies methodmap from
#include <source2mod/api>.
Registering a cookie
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.
Source2ModCookieId g_HudCookie = 0;
// Load()
g_HudCookie = g_pSource2ModCookies->RegisterCookie("myplugin_hud", "Speed HUD: 1 on, 0 off", SOURCE2MOD_COOKIE_PUBLIC);#include <source2mod/api>
int g_HudCookie;
public void OnPluginStart()
{
g_HudCookie = Source2ModCookies.Register("myplugin_hud", "Speed HUD: 1 on, 0 off", CookieAccess_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_PUBLIC | CookieAccess_Public | Players can read and change it. |
SOURCE2MOD_COOKIE_PROTECTED | CookieAccess_Protected | Players can read it, only plugins change it. |
SOURCE2MOD_COOKIE_PRIVATE | CookieAccess_Private | Hidden from players. |
| Limit | |
|---|---|
| Name | 30 bytes (SOURCE2MOD_COOKIE_MAX_NAME_LENGTH) |
| Value | 100 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.
// 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");
});public void OnPluginStart()
{
g_HudCookie = Source2ModCookies.Register("myplugin_hud", "Speed HUD: 1 on, 0 off", CookieAccess_Public);
Source2ModCommands.RegisterConCommand("hud", Cmd_Hud);
}
void Cmd_Hud(Source2ModCommandContext context, Source2ModCommandArgs args, bool post)
{
CPlayerSlot slot = context.GetPlayerSlot();
if (!slot.IsValid())
return;
if (!Source2ModCookies.AreCached(slot))
{
Source2ModCommands.ReplyToCommand(context, "Your settings are still loading, try again in a moment.");
return;
}
char value[8];
Source2ModCookies.Get(slot, g_HudCookie, value, sizeof(value));
bool on = !Source2ModUtils.Equal(value, "0"); // no value yet = the default, on
Source2ModCookies.Set(slot, g_HudCookie, on ? "0" : "1");
Source2ModCommands.ReplyToCommand(context, "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)isfalse;- reads return
falseand"", 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.
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));
}bool g_bHud[64];
void ApplyHud(CPlayerSlot slot)
{
char value[8];
g_bHud[slot.Get()] = !Source2ModCookies.Get(slot, g_HudCookie, value, sizeof(value))
|| !Source2ModUtils.Equal(value, "0");
}
public void OnPluginStart()
{
g_HudCookie = Source2ModCookies.Register("myplugin_hud", "Speed HUD: 1 on, 0 off", CookieAccess_Public);
// Loaded late: players already on the server had their values loaded before.
for (int i = 0; i < 64; i++)
{
CPlayerSlot slot = CPlayerSlot(i);
if (Source2ModCookies.AreCached(slot))
ApplyHud(slot);
}
}
public void Source2Mod_OnClientCookiesCached(CPlayerSlot slot)
{
ApplyHud(slot);
}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.
g_pSource2ModCookies->SetCookieBySteamID(76561198012345678ull, g_HudCookie, "0");// SteamID64, "STEAM_1:Y:Z" or "[U:1:N]".
Source2ModCookies.SetBySteamID("76561198012345678", g_HudCookie, "0");The database
Values live in an SQLite file, addons/source2mod/data/clientprefs.sq3,
unless ClientPrefsDatabase in configs/core.json names another:
{
"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
AreCookiesCachedis true,GetCookiereturnsfalseand"". A plugin that reads inOnClientPutInServerand saves the default back overwrites the player's real value once it arrives. Read in the cached callback. - Names are global. Two plugins registering
hudshare 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.
SetCookiereturnsfalsefor 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.
SQL
Asynchronous SQL on MySQL or SQLite — one-off serverless queries with placeholders, or a persistent connection with queries and transactions.
Files and KeyValues
Reading and writing files and folders, KeyValues trees and the streaming KeyValues parser, and the text, time, regex and logging helpers in ISource2ModUtils.