Displaying Achievements¶
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.
This page covers how to show achievements on the machine: unlock celebrations, achievement lists, progress, and DMD frames. The data comes from the definitions and progress described in Definitions, Progress and Events.
Visibility and Masking¶
Apply these rules for the player being displayed:
| Definition | Player hasn't earned it | Player has earned it |
|---|---|---|
visible is false |
Show nothing at all, not even a placeholder | Show nothing: it is an ingredient of another achievement |
obscure is true |
Show the placeholder image (obscure_image_url) with masked name and description |
Show the real name, description and icon |
| Neither | Show it normally, with progress if you like | Show it normally |
Never show progress for an achievement the player can't see under these rules.
The Scorbit app always shows a player's full achievement catalogue. Showing achievements the player hasn't earned on the machine is optional, for example in attract mode or between balls.
Progress¶
Compute the progress you display from the player's live record:
- One counting rule compared with
GE(a mode, completion, stack or event count, or games played): show the rule's currentvalueover itstarget, such as "4,213 / 10,000 spins". A counting rule compared withEQ,LEorNEis either satisfied or not. - Several rules: show how many rules are satisfied out of the total, such as "2 of 3".
- Score and timer rules are either satisfied or not. Never show them as a fraction.
// Returns progress from 0.0 to 1.0, or -1 when there is nothing to show.
double achievement_fraction(sb_game_handle_t gs, sb_player_t player, const char *key)
{
sb_achievement_t a;
sb_achievement_progress_t progress;
if (!sb_achievement_find(gs, key, &a)
|| !sb_achievement_player_progress(gs, player, key, &progress)) {
return -1.0;
}
if (progress.held) {
return 1.0;
}
size_t rules_count = a.rules_count;
if (rules_count == 1) {
sb_achievement_rule_t rule;
sb_achievement_rule_progress_t rp;
if (sb_achievement_rule_at(gs, key, 0, &rule)
&& sb_achievement_player_rule_progress(gs, player, key, 0, &rp)) {
bool counting = rule.type == SB_ACHIEVEMENT_RULE_MODE
|| rule.type == SB_ACHIEVEMENT_RULE_MODE_COMPLETED
|| rule.type == SB_ACHIEVEMENT_RULE_MODE_STACK
|| rule.type == SB_ACHIEVEMENT_RULE_EVENT
|| rule.type == SB_ACHIEVEMENT_RULE_SESSION;
if (counting && rule.comparison == SB_ACHIEVEMENT_GE && rp.target > 0) {
double f = (double)rp.value / (double)rp.target;
return f > 1.0 ? 1.0 : (f < 0.0 ? 0.0 : f);
}
return rp.satisfied ? 1.0 : 0.0;
}
return -1.0;
}
size_t satisfied = 0;
for (size_t i = 0; i < rules_count; ++i) {
sb_achievement_rule_progress_t rp;
// Fails if definitions changed since sb_achievement_find: no progress to show
if (!sb_achievement_player_rule_progress(gs, player, key, i, &rp)) {
return -1.0;
}
if (rp.satisfied) {
++satisfied;
}
}
return rules_count ? (double)satisfied / (double)rules_count : -1.0;
}
// Returns progress from 0.0 to 1.0, or std::nullopt when there is nothing to show.
std::optional<double> achievementFraction(scorbit::GameState &gs, sb_player_t player,
const std::string &key)
{
using T = scorbit::AchievementRuleType;
scorbit::Achievement a;
scorbit::AchievementProgress progress;
if (!gs.findAchievement(key, a) || !gs.getAchievementProgress(player, key, progress)) {
return std::nullopt;
}
if (progress.held) {
return 1.0;
}
// Both come from separate calls; a definitions refresh in between can change the rule count
if (a.rules.size() != progress.rules.size()) {
return std::nullopt;
}
if (a.rules.size() == 1) {
const auto &rule = a.rules[0];
const auto &rp = progress.rules[0];
const bool counting = rule.type == T::Mode || rule.type == T::ModeCompleted
|| rule.type == T::ModeStack || rule.type == T::Event
|| rule.type == T::Session;
if (counting && rule.comparison == scorbit::AchievementComparison::Ge
&& rp.target > 0) {
return std::clamp(static_cast<double>(rp.value) / rp.target, 0.0, 1.0);
}
return rp.satisfied ? 1.0 : 0.0;
}
const auto satisfied = std::count_if(progress.rules.begin(), progress.rules.end(),
[](const auto &r) { return r.satisfied; });
return progress.rules.empty()
? std::nullopt
: std::optional<double>(static_cast<double>(satisfied) / progress.rules.size());
}
COUNTING = {
scorbit.AchievementRuleType.Mode,
scorbit.AchievementRuleType.ModeCompleted,
scorbit.AchievementRuleType.ModeStack,
scorbit.AchievementRuleType.Event,
scorbit.AchievementRuleType.Session,
}
def achievement_fraction(gs, player, key):
"""Progress from 0.0 to 1.0, or None when there is nothing to show."""
a = gs.find_achievement(key)
progress = gs.get_achievement_progress(player, key)
if a is None or progress is None:
return None
if progress.held:
return 1.0
# Both come from separate calls; a definitions refresh in between can change the rule count
if len(a.rules) != len(progress.rules):
return None
if len(a.rules) == 1:
rule, rp = a.rules[0], progress.rules[0]
if (rule.type in COUNTING
and rule.comparison == scorbit.AchievementComparison.Ge
and rp.target > 0):
return max(0.0, min(1.0, rp.value / rp.target))
return 1.0 if rp.satisfied else 0.0
if not progress.rules:
return None
return sum(1 for r in progress.rules if r.satisfied) / len(progress.rules)
A rule whose prerequisite the machine can't judge (judged is false) counts as not yet satisfied
on the machine. The server still decides that achievement correctly.
Groups and Tiers¶
Achievements that share a group_id form a ladder, such as 100, 1,000 and 10,000 spins. Order a
group by display_position (1, 2, 3, ...). A common display shows the highest tier the player
holds, plus progress toward the next one. Groups never affect evaluation: each tier is earned on
its own rules.
Celebrating an Unlock¶
- Honour
notify_when_achievedwhen deciding how prominently to announce an unlock. - Present an unlock when its
AchievementUpdatedevent arrives withUnlockedLocally, or withConfirmedif you didn't celebrate it already. - Celebrate once per achievement and player, and be ready to take a celebration back on
Retracted. - Use the badge from
icon_url. Badges can be high-resolution images or animations, so load them away from the event callback. - You may show an unlock immediately or hold it until the end of the ball, when the display is free. The decision has already been made either way.
- Hide or mask obscured and invisible achievements consistently: an invisible ingredient should never be celebrated by name.
DMD Frames¶
On dot-matrix display machines, creators can draw a DMD frame for each achievement at the exact resolution and palette of the title's display. Frames are packaged per title into a versioned bundle.
- The SDK downloads the bundle automatically together with the definitions, and again only when the bundle version changes, so a frame is normally on the machine before an unlock needs it. The download runs in the background and can fail, so always handle a missing frame, for example by falling back to text.
- The bundle is stored in the data directory set with
set_data_dir(see Feeding Achievements). - Read a frame by achievement key. The SDK returns the frame's bytes as stored in the bundle, or nothing when the bundle has no frame for that key.
const char *key = "game-cv-boom-balloon"; // e.g. from an AchievementUpdated event
const uint8_t *data = NULL;
size_t size = 0;
if (sb_achievement_frame(gs, key, &data, &size)) {
// data is valid until the next achievements call on this thread: copy or draw it now
show_dmd_frame(data, size); // User-implemented
}
Frames support transparency so they can be overlaid on the game display. Show one immediately at unlock, or at the end of the ball.