Guides

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.

ISource2ModFiles reads and writes files, lists folders, and handles KeyValues — Valve's KV1 text, the format of SourceMod's configs and phrase files. ISource2ModUtils holds the small helpers both languages share: text, time, regex and file logging.

plugin.cpp
#include "source2mod/ISource2ModFiles.h"
#include "source2mod/ISource2ModUtils.h"

In SourcePawn they are Source2ModFiles, Source2ModFile, Source2ModKeyValues, Source2ModKvParser, Source2ModUtils, Source2ModRegex and Source2ModProfiler, all from #include <source2mod/api>.

A C++ plugin that only touches its own files can as well use the standard library. These calls exist so a C++ plugin and a script agree on where a path lands and read KeyValues the same way.

Paths

A path that is not absolute is under the game folder (.../game/csgo), as in SourceMod — not under addons/source2mod. ResolvePath(path) says where one lands. For the core's own folders ask ISource2ModPaths instead of spelling them out.

Files

ReadFile returns the whole file; WriteFile creates or replaces it, or appends with append set, and makes missing folders on the way.

plugin.cpp
constexpr const char* VISITS_FILE = "addons/source2mod/data/myplugin/visits.txt";

void RecordVisit(const char* name)
{
    std::string line = g_pSource2ModUtils->FormatTime("%Y-%m-%d %H:%M:%S") + " " + name + "\n";
    g_pSource2ModFiles->WriteFile(VISITS_FILE, line.data(), line.size(), true); // append; the folders are made
}

void PrintVisits()
{
    std::string contents;
    if (g_pSource2ModFiles->ReadFile(VISITS_FILE, contents))
        g_Source2ModAPI->ConPrintf("%s", contents.c_str());
}

In SourcePawn ReadFile returns the bytes written into the buffer, -1 when the file cannot be read; the C++ one takes any size and binary data.

Call
FileExists(path) / DirExists(path)
FileSize(path)Bytes, -1 when there is no such file.
FileTime(path)Last change, Unix time; -1 when there is no such file.
DeleteFile(path) / RenameFile(from, to)
CreateDirectory(path)Makes the folder and those above it.
RemoveDirectory(path)Only an empty folder.

Line by line (SourcePawn)

A file too big for one buffer is read a line at a time with Source2ModFile. Open takes fopen's modes ("r", "w", "a", "r+", ... with "b" for binary); writing modes make missing folders. C++ plugins use the standard library for this.

plugin.sp
void PrintLines(const char[] path)
{
    Source2ModFile file = Source2ModFile.Open(path, "r");
    if (file == null)
        return;

    char line[512];
    int number;
    while (file.ReadLine(line, sizeof(line)))
        Source2ModApi.ConPrintf("%d: %s\n", ++number, line);

    delete file;
}

ReadLine drops the line break. WriteString writes text as it is, WriteLine formats and adds a line break; Seek, Position, EndOfFile and Flush are there as well. delete closes the file.

Folders

ListDirectory gives the entries of a folder without . and ... In C++ a callback gets each name and whether it is a folder (return false to stop); in SourcePawn it is an ArrayList of names in which folders end in /.

plugin.cpp
g_pSource2ModFiles->ListDirectory("cfg", [](const char* name, bool isDirectory)
{
    if (!isDirectory)
        g_Source2ModAPI->ConPrintf("%s\n", name);
    return true; // false stops
});

Both report a folder that cannot be read: false in C++, null in SourcePawn.

KeyValues

configs/MyPlugin/prices.txt
"Prices"
{
    "weapon_ak47"
    {
        "price"     "2700"
    }
    "weapon_awp"
    {
        "price"     "4750"
        "vip"       "1"
    }
}

A key is either a section (subkeys, no value) or a value. Names compare case-insensitively, and any key argument may be a path, "weapon_awp/price". Values convert on read: the string "5" reads as 5.

The two languages walk a tree differently. In C++ every key is an ISource2ModKeyValues* you hold and iterate. In SourcePawn a Source2ModKeyValues handle has a cursor, as in SourceMod: it starts at the root, JumpToKey and the Goto* methods move it down, GoBack one level up, Rewind back to the root, and key arguments are relative to it.

plugin.cpp
void LoadPrices()
{
    const char* dir = PLUGIN_CONFIG_DIR();
    if (!dir)
        return;
    std::string path = std::string(dir) + "/prices.txt";

    ISource2ModKeyValues* kv = g_pSource2ModFiles->CreateKeyValues("Prices");
    if (!kv->LoadFromFile(path.c_str()))
    {
        g_Source2ModAPI->ConPrintf("Cannot read %s\n", path.c_str());
        kv->Release();
        return;
    }

    for (ISource2ModKeyValues* weapon = kv->GetFirstTrueSubKey(); weapon; weapon = weapon->GetNextTrueSubKey())
    {
        int price = weapon->GetInt("price");
        bool vip = weapon->GetInt("vip") != 0;
        g_Source2ModAPI->ConPrintf("%s: $%d%s\n", weapon->GetName(), price, vip ? " (VIP)" : "");
    }

    kv->Release();
}

Loading replaces the tree with the file's top section, its name included. A missing key reads as the default you pass (GetInt("vip") is 0 for the AK above).

WalkC++SourcePawn
Sections onlyGetFirstTrueSubKey / GetNextTrueSubKeyGotoFirstSubKey() / GotoNextKey()
Values onlyGetFirstValue / GetNextValue—
EverythingGetFirstSubKey / GetNextKeyGotoFirstSubKey(false) / GotoNextKey(false)

Writing

Setting a path makes the keys on the way. SaveToFile writes Valve's layout.

plugin.cpp
constexpr const char* STATS_FILE = "addons/source2mod/data/myplugin/stats.txt";

void SaveStats(uint64 steamId, int kills, int deaths)
{
    ISource2ModKeyValues* kv = g_pSource2ModFiles->CreateKeyValues("Stats");
    kv->LoadFromFile(STATS_FILE); // false while there is no file yet

    ISource2ModKeyValues* player = kv->FindKey(std::to_string(steamId).c_str(), true); // made when missing
    player->SetInt("kills", kills);
    player->SetInt("deaths", deaths);

    kv->SaveToFile(STATS_FILE);
    kv->Release();
}
TypeC++SourcePawn
StringGetString / SetStringGetString / SetString
IntGetInt / SetIntGetNum / SetNum
FloatGetFloat / SetFloatGetFloat / SetFloat
64-bit (SteamIDs)GetUint64 / SetUint64GetUInt64 / SetUInt64, as decimal text
ColorGetColor / SetColor, r | g << 8 | b << 16 | a << 24GetColor / SetColor, int[4]

GetDataType tells them apart (SOURCE2MOD_KV_NONE / KvType_None is a section). Further:

  • RemoveKey / DeleteKey remove a key under this one; DeleteThis (SourcePawn) removes the cursor's key and moves to its parent.
  • Clone() is a detached copy — Release() / delete it.
  • CopySubkeys (C++) / Import (SourcePawn) copy another tree's keys in.
  • Merge adds another tree's keys and replaces keys of the same name whole — a section is not merged key by key.
  • LoadFromString / SaveToString do the same as the file calls on text. SaveToString returns the size the whole text needs, even when the buffer was smaller.
  • escapes on load and save reads and writes \n, \t, \\, \" as escapes. It is off by default.

KeyValues3, the format of CS2's own files, is not here: the engine's KV3 (tier1/keyvalues3.h in the SDK) reads and writes it.

Streaming parser

To read a large file once, or to pick a few values out of it, there is no need to build a tree: the streaming parser (SourceMod's SMC) calls you per section, key/value and line as it reads. Return halt to stop early — that is not an error.

plugin.cpp
class VersionReader final : public ISource2ModKvListener
{
public:
    std::string version;

    ESource2ModKvParseResult OnKeyValue(const char* key, const char* value, bool keyQuoted, bool valueQuoted) override
    {
        if (!g_pSource2ModUtils->StrEqual(key, "version"))
            return SOURCE2MOD_KVPARSE_CONTINUE;

        version = value;
        return SOURCE2MOD_KVPARSE_HALT; // found it: the rest of the file is not read
    }
};

void ReadVersion(const char* path)
{
    VersionReader reader;
    int line = 0, col = 0;
    ESource2ModKvError error = g_pSource2ModFiles->ParseKeyValuesFile(path, &reader, &line, &col);
    if (error != SOURCE2MOD_KVERROR_OK)
        g_Source2ModAPI->ConPrintf("%s:%d:%d: %s\n", path, line, col, g_pSource2ModFiles->KvErrorString(error));
}
EventC++ (ISource2ModKvListener)SourcePawn (Source2ModKvParser property)
Start / endOnStart() / OnEnd(halted, failed)OnStart / OnEnd
"name" {OnEnterSection(name, quoted)OnEnterSection
"key" "value"OnKeyValue(key, value, keyQuoted, valueQuoted)OnKeyValue
}OnLeaveSection()OnLeaveSection
Every lineOnRawLine(line, lineNumber)OnRawLine

Callbacks you do not override or set are skipped. Halt-fail stops with the custom error (SOURCE2MOD_KVERROR_CUSTOM / KvError_Custom). Unlike LoadFromFile, the parser reads escapes by default. ParseKeyValues / ParseString parse text instead of a file.

Utilities

ISource2ModUtils and Source2ModUtils are the same helpers in both languages, so a C++ plugin and a script writing the same log or formatting the same time get the same result. Where C++ has the standard library (sorting, maps, conversions), only SourcePawn has a call.

AreaC++ (g_pSource2ModUtils)SourcePawn
Split / joinExplode → std::vector<std::string>, ImplodeSource2ModUtils.Explode → ArrayList, Implode
Edit, searchReplace, Trim, Find, StrEqualReplace, Trim, Find, Equal, Compare, CompareN, Copy, Length
Conversionsstandard libraryIntToString, StringToInt, FloatToString, StringToFloat
TimeGetTime, FormatTime (strftime, local time), ParseTime, GetTickCount (ms, steady)the same names
RegexCompileRegex → ISource2ModRegexSource2ModRegex.Compile
ProfilingGetTickCount differencesSource2ModProfiler (Start, Stop, Time in seconds)
Sortingstd::sortSortInts, SortFloats, SortStrings, SortCustom; SortOrder_Ascending / _Descending / _Random
Integer-keyed mapstd::unordered_mapIntMap (from source2mod/adt, like StringMap with int keys)
File logLogToFile(g_PluginID, path, message)LogToFile(path, format, ...)

LogToFile appends L <date> - <time>: [<plugin>] <message> to a file under the game folder (unless absolute) and makes missing folders; in C++ passing 0 as the owner leaves the plugin name out.

plugin.cpp
void LogBan(const char* input)
{
    ISource2ModRegex* re = g_pSource2ModUtils->CompileRegex("STEAM_[0-5]:[01]:\\d+");
    if (!re)
        return;

    if (re->Match(input) > 0)
    {
        std::string message = std::string(re->GetSubString(0)) + " banned";
        g_pSource2ModUtils->LogToFile(g_PluginID, "addons/source2mod/logs/bans.log", message.c_str());
    }

    re->Release();
}

Match returns the number of substrings of the match (the whole match plus its captures), 0 for none; GetSubString(0) is the whole match. MatchAll finds every match, and the match argument of GetSubString / CaptureCount / MatchEnd picks one. A bad pattern gives nullptr / null and the reason in the error argument. Flags: SOURCE2MOD_REGEX_CASELESS, _MULTILINE, _DOTALL in C++, RegexFlag_Caseless, _Multiline, _DotAll in SourcePawn.

Pitfalls

  • Relative means the game folder. "myplugin/data.txt" lands in csgo/myplugin/data.txt, not under addons/source2mod. Build paths from ISource2ModPaths or spell out addons/source2mod/....
  • The cursor decides what is saved. In SourcePawn SaveToFile and Clone act on the key under the cursor. Rewind() first to write the whole tree.
  • Key pointers die with their key. In C++ a key pointer stays valid until that key or one above it is removed, or the root is released. A GetString result is valid until the key changes — copy it to keep it. Only a root (from CreateKeyValues or Clone) is Release()d.
  • Merge is not deep. A section of the same name is replaced whole.
  • Regex is ECMAScript, not PCRE. Patterns are std::regex's syntax; some PCRE-only constructs do not compile. Check CompileRegex for nullptr and log the error.
  • Not for hot paths. These calls go to disk on the main thread. Load configs in Load() / OnPluginStart() or on map start, not per frame or per shot.

Reference

On this page