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 whosevisibleis false, and usenotify_when_achievedto 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 withAlreadyHeld. Keep a set ofkey + user idpairs you have celebrated this game. - Be ready to retract. An unlock you showed on
UnlockedLocallycan come back asRetracted, 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.
UnlockedLocallyis 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,
Progresscan 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)) {
// ...
}
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 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.