Creating achievement plugins
nosoop edited this page 4 years ago

The database

Achievements are stored in an SQLite3 database. It's acceptable for single-server use, though it shouldn't be too difficult to adapt this to other database implementations (most queries are threaded).

There are three tables:

  • achievements: This stores the base list of achievements. An achievement name is mapped to its unique identifier, and there is a constraint for non-duplicate achievement names. The other columns are for custom display implementations and are probably deprecated.
  • achievement_status: This stores the progress of players' achievements. It refers to the achievement_id in the previous table (probably should make it a foreign key), and associates it with a Steam account and some arbitrary data that plugins can use to track achievement progress / state. The achieved column is a Unix timestamp; an achievement is considered unlocked if the value in the column is non-zero.
  • achievement_languages: Maps achievement names from the achievements table to language shortcodes, to localized names and descriptions. This is used to display / manage translated strings for players.

The plugin will handle populating most of these; you just need to run the provided init script to create the empty database and update the localization strings to set human-friendly names / descriptions once you've declared some achievements.

The plugin

Declaring an achievement

Calling CustomAchievement looks up the given name in the database and returns an existing ID, or allocates a new ID in the database and returns that if the name doesn't already exist.

#include <custom_achievements>

// store the achievement identifier in global scope
CustomAchievement g_OneShotAchievement;

// you may want to use a library availability callback to make sure everything gets updated
public void OnAllPluginsLoaded() {
    g_OneShotAchievement = CustomAchievement("#some_achievement", AchievementStyle_Single);
}

The database will fill in a placeholder localized name / description in the achievement_languages table for the default server language.

Making a one-shot achievement

One-shot achievements don't have any persistent state associated with them, other than being flagged as achieved or not.

When a player does something achievement-worthy, all you need to do is call CustomAchievement.Award() with a client index.

void ClientDoesSomethingCool(int client) {
    g_OneShotAchievement.Award(client);
}

If the player hasn't already obtained this achievement, all the nearby players will see the achievement particles and a message in chat telling players about their feat.

Using a progress-tracking achievement

Every achievement can have metadata associated with each Steam account. Metadata is an arbitrary string; it's up to the plugin to decide what to store there and how to parse it.

// in this example, achievement metadata is a string representation of an integer value
// assume that g_ProgressAchievement was previously initalized and include is present

CustomAchievement g_ProgressAchievement;
int g_nThingCount[MAXPLAYERS + 1];

public void OnClientAuthorized(int client) {
    // reset count while we wait for the threaded query to call back
    g_nThingCount[client] = 0;
    g_ProgressAchievement.FetchMetadata(client, OnProgressLoaded, GetClientUserId(client));
}

// last argument is `any data` but we're storing an `int userid` there
public void OnProgressLoaded(CustomAchievement achievement, const char[] metadata, int userid) {
    int client = GetClientOfUserId(userid);
    if (client) {
        g_nThingCount[client] = StringToInt(metadata);
    }
}

public void OnClientDisconnect(int client) {
    char numberValue[16];
    IntToString(g_nThingCount[client], numberValue, sizeof(numberValue));
    g_ProgressAchievement.StoreMetadata(client, numberValue);
}

void ClientDidCoolThingButDoItAgainProbably(int client) {
    if (g_nThingCount[client]++ > 5) {
        g_ProgressAchievement.Award(client);
    }
}

Metadata is meant to keep everything in one place (making it easy to join tables when you want to present that information to players). If you don't need that functionality, Client Preferences might be preferable as a primary data source.

The achievement viewer

There isn't a built-in one; sorry. I personally use an MOTD view with a static page that fetches player data from my server's monolithic API backend and parses it out client-side.

Grab the data directly from the database with your choice of technology and parse out the achievement-specific metadata.