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.
Three small interfaces: ISource2ModMaps for what a map vote, map chooser
or admin map command builds on, ISource2ModGeoIP for a player's country
and city, and ISource2ModFeatures to ask whether something optional is
there before relying on it.
#include "source2mod/ISource2ModMaps.h"
#include "source2mod/ISource2ModGeoIP.h"
#include "source2mod/ISource2ModFeatures.h"In SourcePawn they are Source2ModMaps, Source2ModGeoIP and
Source2ModFeatures from #include <source2mod/api>.
Maps
A map is named the way the console takes it:
| Name | Is | Changed to with |
|---|---|---|
de_dust2 | A map the server has | changelevel |
workshop/3070212801 or 3070212801 | A Workshop map by its ID | host_workshop_map |
| Any other name | Assumed to be a map of the server's Workshop collection | ds_workshop_changelevel |
The core has no map vote, map chooser or nominations of its own — these calls are what such a plugin builds on.
Current and next map
GetCurrentMap() is the map running now. SetNextMap() sets the map the
match moves to when it ends — the game's nextlevel as well; for a Workshop
map the core changes level itself when the match ends. GetNextMap() is
what SetNextMap() set, else nextlevel, else the map after the current
one in the mapcyclefile list.
FindMap() turns what an admin typed into a map name:
| C++ | SourcePawn | |
|---|---|---|
SOURCE2MOD_FINDMAP_FOUND | FindMap_Found | The server has a map by exactly this name. |
SOURCE2MOD_FINDMAP_FUZZY_MATCH | FindMap_FuzzyMatch | One map of the map lists starts with or contains it (dus → de_dust2); found holds the full name. |
SOURCE2MOD_FINDMAP_POSSIBLY_AVAILABLE | FindMap_PossiblyAvailable | A Workshop ID or name: may well exist, cannot be checked before changing. |
SOURCE2MOD_FINDMAP_NOT_FOUND | FindMap_NotFound | Nothing by this name. |
// Load()
g_pSource2ModCommands->RegisterConCommand("nextmap", [](const Source2ModCommandContext& ctx, const Source2ModCommandArgs& args, bool post)
{
const char* next = g_pSource2ModMaps->GetNextMap();
REPLY_TO_COMMAND(ctx, "Map: %s, next: %s", g_pSource2ModMaps->GetCurrentMap(), *next ? next : "not set");
});
g_pSource2ModCommands->RegisterConCommand("setnextmap", [](const Source2ModCommandContext& ctx, const Source2ModCommandArgs& args, bool post)
{
std::string map;
Source2ModFindMapResult result = g_pSource2ModMaps->FindMap(args.Arg(1), map);
if (result == SOURCE2MOD_FINDMAP_NOT_FOUND)
{
REPLY_TO_COMMAND(ctx, "No map %s.", args.Arg(1));
return;
}
g_pSource2ModMaps->SetNextMap(map.c_str());
REPLY_TO_COMMAND(ctx, "Next map: %s%s", map.c_str(),
result == SOURCE2MOD_FINDMAP_POSSIBLY_AVAILABLE ? " (Workshop, not checked)" : "");
}, "myplugin.admin.map");#include <source2mod/api>
public void OnPluginStart()
{
Source2ModCommands.RegisterConCommand("nextmap", Cmd_NextMap);
Source2ModCommands.RegisterConCommand("setnextmap", Cmd_SetNextMap, "myplugin.admin.map");
}
void Cmd_NextMap(Source2ModCommandContext context, Source2ModCommandArgs args, bool post)
{
char current[PLATFORM_MAX_PATH], next[PLATFORM_MAX_PATH];
Source2ModMaps.GetCurrentMap(current, sizeof(current));
if (!Source2ModMaps.GetNextMap(next, sizeof(next)))
Source2ModUtils.Copy(next, sizeof(next), "not set");
Source2ModCommands.ReplyToCommand(context, "Map: %s, next: %s", current, next);
}
void Cmd_SetNextMap(Source2ModCommandContext context, Source2ModCommandArgs args, bool post)
{
char typed[PLATFORM_MAX_PATH], map[PLATFORM_MAX_PATH];
args.Arg(1, typed, sizeof(typed));
FindMapResult result = Source2ModMaps.FindMap(typed, map, sizeof(map));
if (result == FindMap_NotFound)
{
Source2ModCommands.ReplyToCommand(context, "No map %s.", typed);
return;
}
Source2ModMaps.SetNextMap(map);
Source2ModCommands.ReplyToCommand(context, "Next map: %s%s", map,
result == FindMap_PossiblyAvailable ? " (Workshop, not checked)" : "");
}IsMapValid(map) is the engine's check: whether the server has the map. A
Workshop map that is not downloaded yet is not valid — use FindMap for
names a player or admin typed.
Changing the map and the history
ChangeLevel(map, reason) changes the map now (SourceMod's
ForceChangeLevel), in any of the forms above. The reason is kept in the
map history, which holds the last 32 maps, newest first.
for (int i = 0; i < g_pSource2ModMaps->GetMapHistorySize(); i++)
{
Source2ModMapHistoryEntry entry;
if (g_pSource2ModMaps->GetMapHistory(i, entry))
g_Source2ModAPI->ConPrintf("%s (%s)\n", entry.map.c_str(), entry.reason.c_str());
}
g_pSource2ModMaps->ChangeLevel("de_mirage", "Vote passed");char map[PLATFORM_MAX_PATH], reason[128];
int startTime;
for (int i = 0; i < Source2ModMaps.GetMapHistorySize(); i++)
{
Source2ModMaps.GetMapHistory(i, map, sizeof(map), reason, sizeof(reason), startTime);
Source2ModApi.ConPrintf("%s (%s)\n", map, reason);
}
Source2ModMaps.ChangeLevel("de_mirage", "Vote passed");Index 0 is the map before this one. Each entry also has the time the map
started (Unix time); a map left without a reason says
Normal level change.
Map lists
Map lists come from configs/maplists.json — SourceMod's maplists.cfg as
JSON. A list is a file, another list's name, or the maps themselves:
{
"mapcyclefile": { "file": "mapcycle.txt" },
"default": { "target": "mapcyclefile" },
"myplugin": { "maps": [ "de_dust2", "de_mirage", "workshop/3070212801" ] }
}file is relative to the game folder; in a list file every line is a map,
and empty lines and lines starting with //, # or ; are skipped. A list
that is not in the file falls back to default, default to
mapcyclefile, and that to mapcycle.txt in the game folder — so a plugin
can read its own list name and work before the owner configured it.
std::vector<std::string> g_Maps;
void LoadMapList()
{
g_Maps.clear();
if (!g_pSource2ModMaps->ReadMapList("myplugin", g_Maps, SOURCE2MOD_MAPLIST_MAPSFOLDER))
return;
for (size_t i = 0; i < g_Maps.size(); i++)
g_Source2ModAPI->ConPrintf("%zu. %s\n", i + 1, g_Maps[i].c_str());
}ArrayList g_Maps;
void LoadMapList()
{
delete g_Maps;
g_Maps = Source2ModMaps.ReadMapList("myplugin", MAPLIST_FLAG_MAPSFOLDER);
if (g_Maps == null)
return;
char map[PLATFORM_MAX_PATH];
for (int i = 0; i < g_Maps.Length; i++)
{
g_Maps.GetString(i, map, sizeof(map));
Source2ModApi.ConPrintf("%d. %s\n", i + 1, map);
}
}| C++ | SourcePawn | |
|---|---|---|
SOURCE2MOD_MAPLIST_MAPSFOLDER | MAPLIST_FLAG_MAPSFOLDER | When no list can be read, every map in the maps folder instead. |
SOURCE2MOD_MAPLIST_NO_DEFAULT | MAPLIST_FLAG_NO_DEFAULT | Do not fall back to default / mapcyclefile. |
| — | MAPLIST_FLAG_CLEARARRAY | Clear the ArrayList passed in before adding. |
The C++ call appends to the vector and leaves it alone when nothing could be
read. In SourcePawn ReadMapList makes a new ArrayList when none is
passed (items of PLATFORM_MAX_PATH bytes) and returns null when nothing
could be read; the caller deletes it.
SourceMod's own names — GetCurrentMap, IsMapValid, FindMap,
SetNextMap, GetNextMap, ForceChangeLevel, GetMapHistory,
ReadMapList, ... — are there as well for ported scripts.
GeoIP
ISource2ModGeoIP looks addresses up in a MaxMind GeoLite2 database
(.mmdb).
The database
The database is not shipped. Download it from MaxMind (a free GeoLite2
account) and put it where GeoIPDatabase in configs/core.json says —
relative to addons/source2mod:
{
"GeoIPDatabase": "geoip/GeoLite2-City.mmdb"
}so by default addons/source2mod/geoip/GeoLite2-City.mmdb.
| Database | Has |
|---|---|
| GeoLite2-City | Everything below: country, continent, region, city, time zone, postal code, location. |
| GeoLite2-Country | Country and continent only — smaller. |
Without a database every lookup returns false and the core says so once in
its log. IsLoaded() says whether one is open, GetDatabaseType() which
(GeoLite2-City, GeoLite2-Country, "" for none), and the geoip
capability is the feature check.
Looking up
// api->AddListener(this, this) in Load()
void MyPlugin::OnClientPutInServer(CPlayerSlot slot, const char* name, int type, uint64 xuid)
{
char country[64];
if (!g_pSource2ModGeoIP->LookupPlayer(slot, SOURCE2MOD_GEOIP_COUNTRY, country, sizeof(country)))
return; // a bot, a LAN address, or no database
g_Source2ModAPI->ConPrintf("%s connected from %s\n", name, country);
}#include <source2mod/api>
public void Source2Mod_OnClientPutInServer(CPlayerSlot slot, const char[] name, int type, const char[] xuid)
{
char country[64];
if (!Source2ModGeoIP.LookupPlayer(slot, GeoIP_Country, country, sizeof(country)))
return; // a bot, a LAN address, or no database
Source2ModApi.ConPrintf("%s connected from %s\n", name, country);
}| C++ | SourcePawn | Example |
|---|---|---|
SOURCE2MOD_GEOIP_COUNTRY_CODE | GeoIP_CountryCode | US (ISO 3166-1 alpha-2) |
SOURCE2MOD_GEOIP_COUNTRY | GeoIP_Country | United States |
SOURCE2MOD_GEOIP_CONTINENT_CODE | GeoIP_ContinentCode | NA |
SOURCE2MOD_GEOIP_CONTINENT | GeoIP_Continent | North America |
SOURCE2MOD_GEOIP_REGION_CODE | GeoIP_RegionCode | CA — City database |
SOURCE2MOD_GEOIP_REGION | GeoIP_Region | California — City database |
SOURCE2MOD_GEOIP_CITY | GeoIP_City | Mountain View — City database |
SOURCE2MOD_GEOIP_TIMEZONE | GeoIP_Timezone | America/Los_Angeles — City database |
SOURCE2MOD_GEOIP_POSTAL_CODE | GeoIP_PostalCode | City database |
Lookup(ip, field, ...) does the same for an address — IPv4 or IPv6, a
port (1.2.3.4:27005) is cut off. Names come in English unless a language
is passed: en, de, es, fr, ja, pt-BR, ru, zh-CN; a name the
database lacks in that language falls back to English. Anything else in the
record is reachable by its path:
char code[8], city[64], countryDe[64], region[16];
g_pSource2ModGeoIP->Lookup(ip, SOURCE2MOD_GEOIP_COUNTRY_CODE, code, sizeof(code));
g_pSource2ModGeoIP->Lookup(ip, SOURCE2MOD_GEOIP_CITY, city, sizeof(city)); // City database only
g_pSource2ModGeoIP->Lookup(ip, SOURCE2MOD_GEOIP_COUNTRY, countryDe, sizeof(countryDe), "de"); // the name in German
g_pSource2ModGeoIP->LookupPath(ip, "subdivisions/0/iso_code", region, sizeof(region));
double latitude = 0.0, longitude = 0.0;
g_pSource2ModGeoIP->GetLocation(ip, latitude, longitude);char code[8], city[64], countryDe[64], region[16];
Source2ModGeoIP.Lookup(ip, GeoIP_CountryCode, code, sizeof(code));
Source2ModGeoIP.Lookup(ip, GeoIP_City, city, sizeof(city)); // City database only
Source2ModGeoIP.Lookup(ip, GeoIP_Country, countryDe, sizeof(countryDe), "de"); // the name in German
Source2ModGeoIP.LookupPath(ip, "subdivisions/0/iso_code", region, sizeof(region));
float latitude, longitude;
Source2ModGeoIP.GetLocation(ip, latitude, longitude);LookupPath reads any string, number or bool of the record — keys and array
indices separated by /, as in MaxMind's own record layout.
GetPlayerIP(slot, ...) is a player's address without the port (false for
an empty slot or a bot). SourceMod's geoip.inc names (GeoipCode2,
GeoipCountry, GeoipCity, ...) are there too, except GeoipCode3.
Features
ISource2ModFeatures is SourceMod's GetFeatureStatus for both kinds of
plugin: ask before calling something that may not be there.
| C++ type | SourcePawn | Name is |
|---|---|---|
SOURCE2MOD_FEATURE_NATIVE | Feature_Native | A SourcePawn native — the core's or one a plugin registered. A plugin native is unavailable while its plugin is unloaded. |
SOURCE2MOD_FEATURE_CAPABILITY | Feature_Capability | A part of the core that can be missing at runtime (below). |
SOURCE2MOD_FEATURE_INTERFACE | Feature_Interface | A C++ interface by its versioned name, ISource2ModMenus007 — the core's at that exact revision, or one a plugin serves. |
SOURCE2MOD_FEATURE_LIBRARY | Feature_Library | A library a plugin registered (RegisterLibrary). |
| Capability | C++ | Available when |
|---|---|---|
sourcepawn | SOURCE2MOD_CAPABILITY_SOURCEPAWN | Always, in this core. |
geoip | SOURCE2MOD_CAPABILITY_GEOIP | A GeoLite2 database was found and opened. |
clientprefs | SOURCE2MOD_CAPABILITY_CLIENTPREFS | The cookie database is connected. |
gamesystems | SOURCE2MOD_CAPABILITY_GAMESYSTEMS | Registering game systems works. |
GetFeatureStatus(type, name) answers available, unavailable (known, not
usable now) or unknown — SourceMod's numbers, 0, 1 and 2. The shortcuts
below answer yes or no:
bool g_bCountryTags = false;
IMyShop* g_pShop = nullptr;
// Load()
g_bCountryTags = g_pSource2ModFeatures->HasCapability(SOURCE2MOD_CAPABILITY_GEOIP);
if (!g_bCountryTags)
g_Source2ModAPI->ConPrintf("No GeoIP database; country tags are off.\n");
if (g_pSource2ModFeatures->IsLibraryAvailable("myshop"))
g_Source2ModAPI->ConPrintf("myshop is loaded, the shop menu entry is on.\n");
// An interface another plugin serves, at the revision this plugin was built against.
if (g_pSource2ModFeatures->IsInterfaceAvailable("IMyShop002"))
g_pShop = (IMyShop*)api->Source2ModFactory("IMyShop002", nullptr, nullptr);#include <source2mod/api>
bool g_bCountryTags;
public void OnPluginStart()
{
g_bCountryTags = Source2ModFeatures.HasCapability("geoip");
if (!g_bCountryTags)
Source2ModApi.Log("No GeoIP database; country tags are off.");
if (Source2ModFeatures.IsLibraryAvailable("myshop"))
Source2ModApi.Log("myshop is loaded, the shop menu entry is on.");
if (!Source2ModFeatures.IsNativeAvailable("MyShop_GetCredits"))
Source2ModApi.Log("MyShop_GetCredits is not there (yet).");
}Unlike asking Source2ModFactory directly, IsInterfaceAvailable does not
log a miss. In SourcePawn, SourceMod's GetFeatureStatus /
RequireFeature take the same types: FeatureType_Native and
FeatureType_Capability are the same numbers as Feature_Native and
Feature_Capability.
Pitfalls
- Workshop names cannot be checked.
FindMapanswers possibly available for a Workshop ID or name, andSetNextMapaccepts it; a wrong one only shows when the level changes.IsMapValidisfalsefor Workshop maps not downloaded yet. - Country-only databases have no city. City, region, time zone, postal
code and location lookups return
falsewith GeoLite2-Country. - Bots and LAN addresses have no country.
LookupPlayerreturnsfalsefor a bot; a private address is not in the database. - GeoIP is optional. A server owner who never downloaded the database
still runs your plugin: check
HasCapability("geoip")and fall back, instead of showing empty tags. - A library is gone while its plugin is unloaded. Check when you need
it, or react to
OnLibraryAdded/OnLibraryRemoved(SourcePawn), not once at load only.
Reference
ISource2ModMaps— maps, map changes and map lists.ISource2ModGeoIP— lookups and fields.ISource2ModFeatures— feature types and capabilities.