Skip to content

Definitions, Progress and Events

Upcoming SDK release

Achievements ship in an upcoming Scorbit SDK release. This section documents that release ahead of time, so details may still change before it ships.

Everything the SDK decides about achievements reaches your game through one event, AchievementUpdated. The same game state handle also gives read access to the cached definitions and to each claimed player's live progress, for drawing displays.

The AchievementUpdated Event

Achievement updates arrive through the event callback you register on the configuration before creating the game state (see Events). Each event names an achievement key, a player slot, the player's user id, and a status:

Status Meaning What to do
UnlockedLocally The machine decided the player earned it. You may celebrate now.
Confirmed The server granted it, in its reply to the machine's report. If you already celebrated it on UnlockedLocally, nothing. Otherwise, celebrate now.
AlreadyHeld The server says the player already held it. Rare: it means the player earned it elsewhere after claiming here. Don't celebrate again.
Retracted The server refused an unlock the machine decided. Chain unlocks that depended on it are retracted too. Withdraw anything you showed for it.
Progress A lifetime achievement's values changed. Reported at the end of each ball, at the end of the game, and when you flush reports (sb_flush_achievement_reports / flushAchievementReports / flush_achievement_reports). Optionally update a progress display.

Trophies and cross-title chains are decided by the server, never by the machine. They are never reported as UnlockedLocally, and the SDK does not deliver the server's decision on them to game code, including a player losing a trophy. Leave their presentation to the Scorbit app.

An event may also carry the values of the rules the machine could judge, as (index, value, satisfied) entries, where index is the rule's position in the definition.

void events_callback(const sb_event_t *event, void *user_data)
{
    switch (sb_event_type(event)) {
    case SB_EVT_ACHIEVEMENT_UPDATED: {
        const char *key = NULL;
        const char *user_id = NULL;
        sb_player_t player = 0;
        sb_achievement_status_t status = SB_ACHIEVEMENT_PROGRESS;
        if (!sb_event_achievement_updated(event, &key, &player, &user_id, &status)) {
            break;
        }

        // key and user_id are valid during the callback only: copy what you keep
        switch (status) {
        case SB_ACHIEVEMENT_UNLOCKED_LOCALLY:
        case SB_ACHIEVEMENT_CONFIRMED:
            queue_celebration(key, user_id, player); // User-implemented; see the rules below
            break;
        case SB_ACHIEVEMENT_RETRACTED:
            withdraw_celebration(key, user_id);      // User-implemented
            break;
        case SB_ACHIEVEMENT_PROGRESS:
            for (size_t i = 0; i < sb_event_achievement_rules_count(event); ++i) {
                size_t index;
                int64_t value;
                bool satisfied;
                if (sb_event_achievement_rule(event, i, &index, &value, &satisfied)) {
                    update_progress(key, player, index, value, satisfied); // User-implemented
                }
            }
            break;
        default: // SB_ACHIEVEMENT_ALREADY_HELD: nothing to show
            break;
        }
    } break;
    default:
        break;
    }
}

// Before sb_create_game_state:
sb_config_set_event_callback(config, events_callback, NULL);
void eventsCallback(const scorbit::Event &event)
{
    if (event.type() != scorbit::EventType::AchievementUpdated) {
        return;
    }

    scorbit::AchievementUpdate update;
    if (!event.getAchievementUpdated(update)) {
        return;
    }

    switch (update.status) {
    case scorbit::AchievementStatus::UnlockedLocally:
    case scorbit::AchievementStatus::Confirmed:
        queueCelebration(update.key, update.userId, update.player); // See the rules below
        break;
    case scorbit::AchievementStatus::Retracted:
        withdrawCelebration(update.key, update.userId);
        break;
    case scorbit::AchievementStatus::Progress:
        for (const auto &rule : update.rules) {
            updateProgress(update.key, update.player, rule.index, rule.value, rule.satisfied);
        }
        break;
    case scorbit::AchievementStatus::AlreadyHeld:
        break;
    }
}

// Before createGameState:
config.setEventCallback(eventsCallback);
def events_callback(event):
    if event.type != scorbit.EventType.AchievementUpdated:
        return

    update = event.get_achievement_updated()
    if update is None:
        return

    if update.status in (scorbit.AchievementStatus.UnlockedLocally,
                         scorbit.AchievementStatus.Confirmed):
        queue_celebration(update.key, update.user_id, update.player)  # See the rules below
    elif update.status == scorbit.AchievementStatus.Retracted:
        withdraw_celebration(update.key, update.user_id)
    elif update.status == scorbit.AchievementStatus.Progress:
        for index, value, satisfied in update.rules:
            update_progress(update.key, update.player, index, value, satisfied)

# Before create_game_state:
config.set_event_callback(events_callback)

Rules for handling unlocks

  • Check the definition before celebrating. The samples leave this to queue_celebration. Look the key up (sb_achievement_find / findAchievement / find_achievement). Never celebrate one whose visible is false, and use notify_when_achieved to decide how prominently to announce it. See Celebrating an Unlock.
  • Celebrate once per achievement and player. A machine-decided unlock is followed by the server's Confirmed, and a repeated claim is answered with AlreadyHeld. Keep a set of key + user id pairs you have celebrated this game.
  • Be ready to retract. An unlock you showed on UnlockedLocally can come back as Retracted, for example when the achievement was archived mid-game or its scope didn't allow it here.
  • Return quickly. The event callback runs on an SDK thread. Queue artwork loading and animation for your own render loop, and never block in the callback.
  • Unlocks don't wait for the end of the ball. UnlockedLocally is raised the moment an unlock is decided, for single-game and lifetime achievements alike. The server's answer to a lifetime unlock follows the end-of-ball report. You choose when to present it: immediately, or at the end of the ball when the display is free.
  • Don't let progress interrupt play. At the end of a ball and of the game, Progress can arrive in a burst: one event per changed lifetime achievement for each claimed player. Coalesce them and update progress displays quietly. Never show one popup per event.

Reading Definitions

The SDK downloads definitions automatically once the machine is authenticated and paired. You can read them at any time, even between games, for example to build an achievement list for attract mode. Each definition carries its full rule list.

size_t count = sb_achievements_count(gs);
for (size_t i = 0; i < count; ++i) {
    sb_achievement_t a;
    if (!sb_achievement_at(gs, i, &a)) {
        continue;
    }

    // Strings are owned by the SDK and valid until the next achievements call on this
    // thread. Copy the key before passing it back to the SDK.
    char key[128];
    snprintf(key, sizeof key, "%s", a.key);
    printf("%s: %s (%zu rules)\n", key, a.name, a.rules_count);

    for (size_t r = 0; r < a.rules_count; ++r) {
        sb_achievement_rule_t rule;
        if (sb_achievement_rule_at(gs, key, r, &rule)) {
            printf("  rule %zu: type %d, target %lld\n", r, rule.type, (long long)rule.target);
        }
    }
}

sb_achievement_t one;
if (sb_achievement_find(gs, "game-cv-boom-balloon", &one)) {
    // ...
}
for (const auto &a : gs.getAchievements()) {
    std::cout << a.key << ": " << a.name << " (" << a.rules.size() << " rules)\n";
    for (const auto &rule : a.rules) {
        std::cout << "  type " << static_cast<int>(rule.type) << ", target " << rule.target
                  << "\n";
    }
}

scorbit::Achievement one;
if (gs.findAchievement("game-cv-boom-balloon", one)) {
    // ...
}
for a in gs.get_achievements():
    print(f"{a.key}: {a.name} ({len(a.rules)} rules)")
    for rule in a.rules:
        print(f"  {rule.type.name}, target {rule.target}")

one = gs.find_achievement("game-cv-boom-balloon")
if one is not None:
    ...

Definition fields

Field (C / C++ / Python) Meaning
key Immutable unique key
name, description Display text. Mask both for an obscured achievement the player hasn't earned.
scope Game, venue, event or global
evaluation In-session (this game) or unlimited (lifetime)
is_trophy / isTrophy Held by one player at a time; decided by the server only. The SDK never unlocks a trophy itself, but still tracks and reports the value players are ranked by. Games can also use it to label trophies.
visible When false, an ingredient that must never be listed
obscure When true, show the placeholder image and masked text until earned
notify_when_achieved / notifyWhenAchieved The creator asked for unlocks to be announced
icon_url / iconUrl, obscure_image_url / obscureImageUrl Badge and placeholder artwork
group_id, display_position Tier ladder membership and order. In C and C++ these are valid when has_group / hasGroup is true; in Python they are None when the achievement has no group.
rules rules_count with sb_achievement_rule_at in C; the rules list in C++ and Python

Reading a Player's Progress

For a claimed player, the SDK keeps a live record of every achievement it tracks: the value of each rule and whether it is satisfied. Read it by player slot and key, for example to draw progress between balls.

Progress is unavailable (the call returns false, or None in Python) when:

  • no game is in progress. Live progress exists only for players claimed in the current game;
  • the slot is not claimed, or the player's stored progress is still being fetched or the fetch failed. A failed fetch is retried at the next ball end;
  • the achievement is a single-game achievement the player already holds. Those are not tracked, because they can't be earned twice. To show one as earned, use the player's stored state from fetch_player_achievements (see Network Calls).

Lifetime achievements keep counting after they are earned, so their progress stays available.

sb_achievement_t a;
sb_achievement_progress_t progress;
const char *key = "game-cv-spin-master";

if (sb_achievement_find(gs, key, &a)
    && sb_achievement_player_progress(gs, player, key, &progress)) {
    size_t rules_count = a.rules_count;
    for (size_t i = 0; i < rules_count; ++i) {
        sb_achievement_rule_progress_t rule;
        if (sb_achievement_player_rule_progress(gs, player, key, i, &rule) && rule.judged) {
            printf("rule %zu: %lld of %lld%s\n", i, (long long)rule.value,
                   (long long)rule.target, rule.satisfied ? " (done)" : "");
        }
    }
    printf("held: %d, all rules met: %d\n", progress.held, progress.all_satisfied);
}
scorbit::AchievementProgress progress;
if (gs.getAchievementProgress(player, "game-cv-spin-master", progress)) {
    for (size_t i = 0; i < progress.rules.size(); ++i) {
        const auto &rule = progress.rules[i];
        if (rule.judged) {
            std::cout << "rule " << i << ": " << rule.value << " of " << rule.target
                      << (rule.satisfied ? " (done)" : "") << "\n";
        }
    }
}
progress = gs.get_achievement_progress(player, "game-cv-spin-master")
if progress is not None:
    for i, rule in enumerate(progress.rules):
        if rule.judged:
            done = " (done)" if rule.satisfied else ""
            print(f"rule {i}: {rule.value} of {rule.target}{done}")

Progress fields

Field Meaning
held The player holds the achievement, earned earlier or during this game
all_satisfied / allSatisfied Every rule is currently satisfied
confirmed The server granted it during this game, or said the player already held it
Per rule: judged False when the machine can't judge the rule, for example a prerequisite from another title. value is then 0.
Per rule: value The lifetime value for a lifetime achievement; this game's value otherwise
Per rule: target, satisfied The rule's target, and whether it is met

Progress lags commit slightly

Game state is processed asynchronously, so progress read immediately after commit() may not include that commit yet. Read progress at ball end, or on a Progress event.

Network Calls

These are rarely needed; the SDK does all of them automatically.

C C++ Python Purpose
sb_refresh_achievements refreshAchievements refresh_achievements Revalidate cached definitions now. Done automatically at boot and at every game start. It is a conditional request (ETag / If-None-Match): for an unchanged catalogue the server responds with HTTP 304 (Not Modified) and the cache is kept.
sb_flush_achievement_reports flushAchievementReports flush_achievement_reports Report changed lifetime progress now instead of at the end of the ball
sb_fetch_player_achievements fetchPlayerAchievements fetch_player_achievements Fetch a player's stored achievement state as JSON, by user id, for a profile screen. The reply lists only achievements the player has state for, so merge it with the definitions. The callback receives an error code and the JSON reply.
sb_download_achievement_frames downloadAchievementFrames download_achievement_frames Download the DMD frame bundle if it differs from the version announced by the last definitions refresh. See Displaying Achievements.

The user id for fetch_player_achievements is a string: the user_id carried by an AchievementUpdated event, or the player's id from a PlayersUpdated event, read with sb_event_player_id(event, player, &id) in C, or as PlayerInfo.id from getPlayersUpdated in C++ or get_players_updated() in Python.