Guides

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.

plugin.cpp
#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:

NameIsChanged to with
de_dust2A map the server haschangelevel
workshop/3070212801 or 3070212801A Workshop map by its IDhost_workshop_map
Any other nameAssumed to be a map of the server's Workshop collectionds_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_FOUNDFindMap_FoundThe server has a map by exactly this name.
SOURCE2MOD_FINDMAP_FUZZY_MATCHFindMap_FuzzyMatchOne map of the map lists starts with or contains it (dus → de_dust2); found holds the full name.
SOURCE2MOD_FINDMAP_POSSIBLY_AVAILABLEFindMap_PossiblyAvailableA Workshop ID or name: may well exist, cannot be checked before changing.
SOURCE2MOD_FINDMAP_NOT_FOUNDFindMap_NotFoundNothing by this name.
plugin.cpp
// 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");

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.

plugin.cpp
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");

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:

configs/maplists.json
{
  "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.

plugin.cpp
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());
}
C++SourcePawn
SOURCE2MOD_MAPLIST_MAPSFOLDERMAPLIST_FLAG_MAPSFOLDERWhen no list can be read, every map in the maps folder instead.
SOURCE2MOD_MAPLIST_NO_DEFAULTMAPLIST_FLAG_NO_DEFAULTDo not fall back to default / mapcyclefile.
—MAPLIST_FLAG_CLEARARRAYClear 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:

configs/core.json
{
  "GeoIPDatabase": "geoip/GeoLite2-City.mmdb"
}

so by default addons/source2mod/geoip/GeoLite2-City.mmdb.

DatabaseHas
GeoLite2-CityEverything below: country, continent, region, city, time zone, postal code, location.
GeoLite2-CountryCountry 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

plugin.cpp
// 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);
}
C++SourcePawnExample
SOURCE2MOD_GEOIP_COUNTRY_CODEGeoIP_CountryCodeUS (ISO 3166-1 alpha-2)
SOURCE2MOD_GEOIP_COUNTRYGeoIP_CountryUnited States
SOURCE2MOD_GEOIP_CONTINENT_CODEGeoIP_ContinentCodeNA
SOURCE2MOD_GEOIP_CONTINENTGeoIP_ContinentNorth America
SOURCE2MOD_GEOIP_REGION_CODEGeoIP_RegionCodeCA — City database
SOURCE2MOD_GEOIP_REGIONGeoIP_RegionCalifornia — City database
SOURCE2MOD_GEOIP_CITYGeoIP_CityMountain View — City database
SOURCE2MOD_GEOIP_TIMEZONEGeoIP_TimezoneAmerica/Los_Angeles — City database
SOURCE2MOD_GEOIP_POSTAL_CODEGeoIP_PostalCodeCity 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:

plugin.cpp
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);

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++ typeSourcePawnName is
SOURCE2MOD_FEATURE_NATIVEFeature_NativeA SourcePawn native — the core's or one a plugin registered. A plugin native is unavailable while its plugin is unloaded.
SOURCE2MOD_FEATURE_CAPABILITYFeature_CapabilityA part of the core that can be missing at runtime (below).
SOURCE2MOD_FEATURE_INTERFACEFeature_InterfaceA C++ interface by its versioned name, ISource2ModMenus007 — the core's at that exact revision, or one a plugin serves.
SOURCE2MOD_FEATURE_LIBRARYFeature_LibraryA library a plugin registered (RegisterLibrary).
CapabilityC++Available when
sourcepawnSOURCE2MOD_CAPABILITY_SOURCEPAWNAlways, in this core.
geoipSOURCE2MOD_CAPABILITY_GEOIPA GeoLite2 database was found and opened.
clientprefsSOURCE2MOD_CAPABILITY_CLIENTPREFSThe cookie database is connected.
gamesystemsSOURCE2MOD_CAPABILITY_GAMESYSTEMSRegistering 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:

plugin.cpp
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);

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. FindMap answers possibly available for a Workshop ID or name, and SetNextMap accepts it; a wrong one only shows when the level changes. IsMapValid is false for Workshop maps not downloaded yet.
  • Country-only databases have no city. City, region, time zone, postal code and location lookups return false with GeoLite2-Country.
  • Bots and LAN addresses have no country. LookupPlayer returns false for 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

On this page