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.
#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.
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());
}#include <source2mod/api>
#define VISITS_FILE "addons/source2mod/data/myplugin/visits.txt"
void RecordVisit(const char[] name)
{
char time[32], line[256];
Source2ModUtils.FormatTime(time, sizeof(time), "%Y-%m-%d %H:%M:%S");
Source2ModApi.Format(line, sizeof(line), "%s %s\n", time, name);
Source2ModFiles.WriteFile(VISITS_FILE, line, true); // append; the folders are made
}
void PrintVisits()
{
char contents[4096];
if (Source2ModFiles.ReadFile(VISITS_FILE, contents, sizeof(contents)) >= 0)
Source2ModApi.ConPrintf("%s", contents);
}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.
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 /.
g_pSource2ModFiles->ListDirectory("cfg", [](const char* name, bool isDirectory)
{
if (!isDirectory)
g_Source2ModAPI->ConPrintf("%s\n", name);
return true; // false stops
});void ListConfigs()
{
ArrayList entries = Source2ModFiles.ListDirectory("cfg");
if (entries == null)
return;
char name[PLATFORM_MAX_PATH];
for (int i = 0; i < entries.Length; i++)
{
entries.GetString(i, name, sizeof(name));
int length = Source2ModUtils.Length(name);
if (length > 0 && name[length - 1] == '/')
continue; // a folder
Source2ModApi.ConPrintf("%s\n", name);
}
delete entries;
}Both report a folder that cannot be read: false in C++, null in
SourcePawn.
KeyValues
"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.
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();
}void LoadPrices()
{
char dir[PLATFORM_MAX_PATH], path[PLATFORM_MAX_PATH];
Source2ModPaths.PluginConfigDirectory(dir, sizeof(dir));
Source2ModApi.Format(path, sizeof(path), "%s/prices.txt", dir);
Source2ModKeyValues kv = new Source2ModKeyValues("Prices");
if (!kv.LoadFromFile(path))
{
Source2ModApi.LogError("Cannot read %s", path);
delete kv;
return;
}
if (kv.GotoFirstSubKey())
{
char weapon[64];
do
{
kv.GetSectionName(weapon, sizeof(weapon));
int price = kv.GetNum("price");
bool vip = kv.GetNum("vip") != 0;
Source2ModApi.ConPrintf("%s: $%d%s\n", weapon, price, vip ? " (VIP)" : "");
}
while (kv.GotoNextKey());
}
delete kv;
}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).
| Walk | C++ | SourcePawn |
|---|---|---|
| Sections only | GetFirstTrueSubKey / GetNextTrueSubKey | GotoFirstSubKey() / GotoNextKey() |
| Values only | GetFirstValue / GetNextValue | — |
| Everything | GetFirstSubKey / GetNextKey | GotoFirstSubKey(false) / GotoNextKey(false) |
Writing
Setting a path makes the keys on the way. SaveToFile writes Valve's
layout.
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();
}#define STATS_FILE "addons/source2mod/data/myplugin/stats.txt"
void SaveStats(const char[] steamId, int kills, int deaths)
{
Source2ModKeyValues kv = new Source2ModKeyValues("Stats");
kv.LoadFromFile(STATS_FILE); // false while there is no file yet
kv.JumpToKey(steamId, true); // made when missing
kv.SetNum("kills", kills);
kv.SetNum("deaths", deaths);
kv.Rewind(); // SaveToFile writes what is under the cursor
kv.SaveToFile(STATS_FILE);
delete kv;
}| Type | C++ | SourcePawn |
|---|---|---|
| String | GetString / SetString | GetString / SetString |
| Int | GetInt / SetInt | GetNum / SetNum |
| Float | GetFloat / SetFloat | GetFloat / SetFloat |
| 64-bit (SteamIDs) | GetUint64 / SetUint64 | GetUInt64 / SetUInt64, as decimal text |
| Color | GetColor / SetColor, r | g << 8 | b << 16 | a << 24 | GetColor / SetColor, int[4] |
GetDataType tells them apart (SOURCE2MOD_KV_NONE / KvType_None is a
section). Further:
RemoveKey/DeleteKeyremove a key under this one;DeleteThis(SourcePawn) removes the cursor's key and moves to its parent.Clone()is a detached copy —Release()/deleteit.CopySubkeys(C++) /Import(SourcePawn) copy another tree's keys in.Mergeadds another tree's keys and replaces keys of the same name whole — a section is not merged key by key.LoadFromString/SaveToStringdo the same as the file calls on text.SaveToStringreturns the size the whole text needs, even when the buffer was smaller.escapeson 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.
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));
}char g_Version[32];
Source2ModKvParseResult Kv_KeyValue(Handle parser, const char[] key, const char[] value, bool keyQuoted, bool valueQuoted)
{
if (!Source2ModUtils.Equal(key, "version"))
return KvParse_Continue;
Source2ModUtils.Copy(g_Version, sizeof(g_Version), value);
return KvParse_Halt; // found it: the rest of the file is not read
}
void ReadVersion(const char[] path)
{
Source2ModKvParser parser = new Source2ModKvParser();
parser.OnKeyValue = Kv_KeyValue;
int line, col;
Source2ModKvError error = parser.ParseFile(path, line, col);
if (error != KvError_Okay)
{
char message[128];
Source2ModKvParser.GetErrorString(error, message, sizeof(message));
Source2ModApi.LogError("%s:%d:%d: %s", path, line, col, message);
}
delete parser;
}| Event | C++ (ISource2ModKvListener) | SourcePawn (Source2ModKvParser property) |
|---|---|---|
| Start / end | OnStart() / OnEnd(halted, failed) | OnStart / OnEnd |
"name" { | OnEnterSection(name, quoted) | OnEnterSection |
"key" "value" | OnKeyValue(key, value, keyQuoted, valueQuoted) | OnKeyValue |
} | OnLeaveSection() | OnLeaveSection |
| Every line | OnRawLine(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.
| Area | C++ (g_pSource2ModUtils) | SourcePawn |
|---|---|---|
| Split / join | Explode → std::vector<std::string>, Implode | Source2ModUtils.Explode → ArrayList, Implode |
| Edit, search | Replace, Trim, Find, StrEqual | Replace, Trim, Find, Equal, Compare, CompareN, Copy, Length |
| Conversions | standard library | IntToString, StringToInt, FloatToString, StringToFloat |
| Time | GetTime, FormatTime (strftime, local time), ParseTime, GetTickCount (ms, steady) | the same names |
| Regex | CompileRegex → ISource2ModRegex | Source2ModRegex.Compile |
| Profiling | GetTickCount differences | Source2ModProfiler (Start, Stop, Time in seconds) |
| Sorting | std::sort | SortInts, SortFloats, SortStrings, SortCustom; SortOrder_Ascending / _Descending / _Random |
| Integer-keyed map | std::unordered_map | IntMap (from source2mod/adt, like StringMap with int keys) |
| File log | LogToFile(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.
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();
}void LogBan(const char[] input)
{
Source2ModRegex re = Source2ModRegex.Compile("STEAM_[0-5]:[01]:\\d+");
if (re == null)
return;
if (re.Match(input) > 0)
{
char steamId[32];
re.GetSubString(0, steamId, sizeof(steamId));
Source2ModUtils.LogToFile("addons/source2mod/logs/bans.log", "%s banned", steamId);
}
delete re;
}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 incsgo/myplugin/data.txt, not underaddons/source2mod. Build paths fromISource2ModPathsor spell outaddons/source2mod/.... - The cursor decides what is saved. In SourcePawn
SaveToFileandCloneact 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
GetStringresult is valid until the key changes — copy it to keep it. Only a root (fromCreateKeyValuesorClone) isRelease()d. Mergeis 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. CheckCompileRegexfornullptrand 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
ISource2ModFiles— files, folders, KeyValues and the parser.ISource2ModUtils— text, time, regex and logging.
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.
Maps, GeoIP and features
The current and next map, map changes, map history and map lists; where a player or an IP address is from; and checking for optional features before using them.