Skip to content

Achievement Status Management

This page covers how to query and manage the unlock status of achievements for users, including checking if achievements are unlocked and handling status changes.

Checking Unlock Status

Query whether a specific achievement is unlocked for a user:

// Check if achievement is unlocked
sb_achievement_progress_t progress;
if (sb_get_cached_progress(gs, user_id, "wizard_mode_complete", &progress)) {
    if (progress.unlocked) {
        printf("Achievement is unlocked!\n");
        printf("Unlocked at: %s\n", progress.unlocked_at);
    } else {
        printf("Achievement not yet unlocked\n");
        printf("Current progress: %d\n", progress.progress);
    }
} else {
    printf("No progress data for this user/achievement\n");
}
// Check if achievement is unlocked
auto progress = gs.getProgress(userId, "wizard_mode_complete");
if (progress) {
    if (progress->unlocked) {
        std::cout << "Achievement is unlocked!\n";
        std::cout << "Unlocked at: " << progress->unlockedAt << "\n";
    } else {
        std::cout << "Achievement not yet unlocked\n";
        std::cout << "Current progress: " << progress->progress << "\n";
    }
} else {
    std::cout << "No progress data for this user/achievement\n";
}
# Check if achievement is unlocked
progress = gs.get_progress(user_id, "wizard_mode_complete")
if progress:
    if progress.unlocked:
        print("Achievement is unlocked!")
        print(f"Unlocked at: {progress.unlocked_at}")
    else:
        print("Achievement not yet unlocked")
        print(f"Current progress: {progress.progress}")
else:
    print("No progress data for this user/achievement")

Getting All User Achievements

Retrieve the complete achievement status for a user:

// Count unlocked achievements by iterating all cached achievements
void display_user_summary(sb_game_handle_t gs, int64_t user_id) {
    size_t total = sb_get_cached_achievements_count(gs);
    int unlocked = 0, with_progress = 0;

    for (size_t i = 0; i < total; i++) {
        sb_achievement_t ach;
        if (!sb_get_cached_achievement_at(gs, i, &ach)) continue;

        sb_achievement_progress_t prog;
        if (sb_get_cached_progress(gs, user_id, ach.key, &prog)) {
            with_progress++;
            if (prog.unlocked) unlocked++;
        }
    }
    printf("Achievements: %d/%d unlocked\n", unlocked, with_progress);
}
// Get all achievement progress for a user
auto allProgress = gs.getUserProgress(userId);

if (allProgress) {
    int unlocked = 0;
    int total = allProgress->size();

    for (const auto& progress : *allProgress) {
        if (progress.unlocked) {
            unlocked++;
        }
    }

    std::cout << "Achievements: " << unlocked << "/" << total << " unlocked\n";
}
# Get all achievement progress for a user
all_progress = gs.get_user_progress(user_id)

if all_progress:
    unlocked = sum(1 for p in all_progress if p.unlocked)
    total = len(all_progress)
    print(f"Achievements: {unlocked}/{total} unlocked")

Categorizing Achievement Status

Organize achievements by their current status:

void display_categorized(sb_game_handle_t gs, int64_t user_id) {
    size_t total = sb_get_cached_achievements_count(gs);
    int unlocked = 0, in_progress = 0, locked = 0;

    for (size_t i = 0; i < total; i++) {
        sb_achievement_t ach;
        if (!sb_get_cached_achievement_at(gs, i, &ach)) continue;

        sb_achievement_progress_t prog;
        if (sb_get_cached_progress(gs, user_id, ach.key, &prog)) {
            if (prog.unlocked) unlocked++;
            else if (prog.progress > 0) in_progress++;
            else locked++;
        } else {
            locked++;
        }
    }
    printf("Unlocked: %d, In Progress: %d, Locked: %d\n",
           unlocked, in_progress, locked);
}
struct AchievementsByStatus {
    std::vector<scorbit::Achievement> unlocked;
    std::vector<scorbit::Achievement> inProgress;
    std::vector<scorbit::Achievement> locked;
};

AchievementsByStatus categorizeAchievements(
    scorbit::GameState& gs,
    int64_t userId
) {
    AchievementsByStatus result;
    auto achievements = gs.getAchievements();

    for (const auto& ach : achievements) {
        auto progress = gs.getProgress(userId, ach.key);

        if (progress && progress->unlocked) {
            result.unlocked.push_back(ach);
        } else if (progress && progress->progress > 0) {
            result.inProgress.push_back(ach);
        } else {
            result.locked.push_back(ach);
        }
    }

    return result;
}

// Usage
auto status = categorizeAchievements(gs, userId);
std::cout << "Unlocked: " << status.unlocked.size() << "\n";
std::cout << "In Progress: " << status.inProgress.size() << "\n";
std::cout << "Locked: " << status.locked.size() << "\n";
def categorize_achievements(gs, user_id):
    """Categorize achievements by status."""
    achievements = gs.get_achievements()

    unlocked = []
    in_progress = []
    locked = []

    for ach in achievements:
        progress = gs.get_progress(user_id, ach.key)

        if progress and progress.unlocked:
            unlocked.append(ach)
        elif progress and progress.progress > 0:
            in_progress.append(ach)
        else:
            locked.append(ach)

    return {
        'unlocked': unlocked,
        'in_progress': in_progress,
        'locked': locked
    }

# Usage
status = categorize_achievements(gs, user_id)
print(f"Unlocked: {len(status['unlocked'])}")
print(f"In Progress: {len(status['in_progress'])}")
print(f"Locked: {len(status['locked'])}")

Handling Status Changes

Register a callback to handle all achievement status changes:

void on_achievement_event(const sb_event_t* event, void* user_data) {
    sb_event_type_t type = sb_event_type(event);

    const char *key = NULL;
    const char *name = NULL;
    const char *user_id = NULL;
    const char *username = NULL;
    const char *icon_url = NULL;

    switch (type) {
        case SB_EVT_ACHIEVEMENT_UNLOCKED: {
            bool is_trophy = false;
            if (sb_event_achievement_unlocked(event, &key, &name, &user_id,
                                              &username, &icon_url, &is_trophy)) {
                // Update UI: show as unlocked
                set_achievement_unlocked(key, user_id, true);
                play_unlock_animation(key);
            }
            break;
        }
        case SB_EVT_ACHIEVEMENT_LOCKED: {
            if (sb_event_achievement_locked(event, &key, &name, &user_id,
                                            &username, &icon_url)) {
                // Update UI: show as locked (trophy revoked)
                set_achievement_unlocked(key, user_id, false);
                show_trophy_revoked_notification(name);
            }
            break;
        }
        case SB_EVT_ACHIEVEMENT_PROGRESS: {
            int current_value = 0;
            int target_value = 0;
            if (sb_event_achievement_progress(event, &key, &name, &user_id,
                                              &username, &icon_url,
                                              &current_value, &target_value)) {
                // Update UI: refresh progress display
                update_achievement_progress(key, user_id, current_value, target_value);
            }
            break;
        }
    }
}
void onEvent(const scorbit::Event& event) {
    std::string key, name, userId, username, iconUrl;

    switch (event.type()) {
        case scorbit::EventType::AchievementUnlocked: {
            bool isTrophy = false;
            if (event.getAchievementUnlocked(key, name, userId, username, iconUrl, isTrophy)) {
                // Update UI: show as unlocked
                setAchievementUnlocked(key, userId, true);
                playUnlockAnimation(key);
            }
            break;
        }
        case scorbit::EventType::AchievementLocked: {
            if (event.getAchievementLocked(key, name, userId, username, iconUrl)) {
                // Update UI: show as locked (trophy revoked)
                setAchievementUnlocked(key, userId, false);
                showTrophyRevokedNotification(name);
            }
            break;
        }
        case scorbit::EventType::AchievementProgress: {
            int currentValue = 0, targetValue = 0;
            if (event.getAchievementProgress(key, name, userId, username, iconUrl,
                                             currentValue, targetValue)) {
                // Update UI: refresh progress display
                updateAchievementProgress(key, userId, currentValue, targetValue);
            }
            break;
        }
        default:
            break;
    }
}
def on_event(event):
    if event.type() == scorbit.EventType.AchievementUnlocked:
        success, key, name, user_id, username, icon_url, is_trophy = \
            event.get_achievement_unlocked()
        if success:
            # Update UI: show as unlocked
            set_achievement_unlocked(key, user_id, True)
            play_unlock_animation(key)

    elif event.type() == scorbit.EventType.AchievementLocked:
        success, key, name, user_id, username, icon_url = \
            event.get_achievement_locked()
        if success:
            # Update UI: show as locked (trophy revoked)
            set_achievement_unlocked(key, user_id, False)
            show_trophy_revoked_notification(name)

    elif event.type() == scorbit.EventType.AchievementProgress:
        success, key, name, user_id, username, icon_url, current_value, target_value = \
            event.get_achievement_progress()
        if success:
            # Update UI: refresh progress display
            update_achievement_progress(key, user_id, current_value, target_value)

Using Triggered Callbacks

For simplified status change handling, use the triggered callback:

// Callback for achievement status changes
void on_achievement_triggered(
    const char* key,
    int64_t user_id,
    bool unlocked,
    int progress,
    void* user_data
) {
    if (unlocked) {
        printf("Achievement %s unlocked for user %lld\n", key, user_id);
    } else {
        printf("Achievement %s progress: %d for user %lld\n", key, progress, user_id);
    }
}

// Register the callback
sb_set_achievement_triggered_callback(gs, on_achievement_triggered, NULL);
// Register a simplified callback
gs.setAchievementTriggeredCallback(
    [](const std::string& key, int64_t userId, bool unlocked, int progress) {
        if (unlocked) {
            std::cout << "Achievement " << key << " unlocked for user " << userId << "\n";
        } else {
            std::cout << "Achievement " << key << " progress: " << progress
                      << " for user " << userId << "\n";
        }
    }
);
def on_achievement_triggered(key, user_id, unlocked, progress):
    if unlocked:
        print(f"Achievement {key} unlocked for user {user_id}")
    else:
        print(f"Achievement {key} progress: {progress} for user {user_id}")

# Register the callback
gs.set_achievement_triggered_callback(on_achievement_triggered)

Status Display Helper

Create a comprehensive status display:

void display_achievement_status(sb_game_handle_t gs, int64_t user_id,
                                const char *key) {
    sb_achievement_t ach;
    if (!sb_get_cached_achievement(gs, key, &ach)) {
        printf("Achievement not found: %s\n", key);
        return;
    }

    sb_achievement_progress_t prog;
    bool has = sb_get_cached_progress(gs, user_id, key, &prog);

    // Handle obscured
    const char *name = ach.name;
    const char *desc = ach.description;
    if (ach.obscure && !(has && prog.unlocked)) {
        name = "???";
        desc = "Hidden achievement";
    }

    printf("%s\n  %s\n", name, desc);
    if (has && prog.unlocked)
        printf("  Status: ✓ UNLOCKED\n");
    else if (has && ach.count > 1 && prog.progress > 0)
        printf("  Status: In Progress (%d/%d)\n", prog.progress, ach.count);
    else
        printf("  Status: Locked\n");

    if (ach.is_trophy)
        printf("  Type: Trophy (can be revoked)\n");
}
void displayAchievementStatus(
    scorbit::GameState& gs,
    int64_t userId,
    const std::string& key
) {
    auto ach = gs.getAchievement(key);
    auto progress = gs.getProgress(userId, key);

    if (!ach) {
        std::cout << "Achievement not found: " << key << "\n";
        return;
    }

    // Handle obscured achievements
    std::string displayName = ach->name;
    std::string displayDesc = ach->description;
    bool isObscured = ach->obscure;

    if (isObscured && (!progress || !progress->unlocked)) {
        displayName = "???";
        displayDesc = "Hidden achievement";
    }

    std::cout << displayName << "\n";
    std::cout << "  " << displayDesc << "\n";

    if (progress) {
        if (progress->unlocked) {
            std::cout << "  Status: ✓ UNLOCKED\n";
            std::cout << "  Unlocked: " << progress->unlockedAt << "\n";
        } else if (ach->count > 1) {
            float pct = (float)progress->progress / ach->count * 100;
            std::cout << "  Status: In Progress\n";
            std::cout << "  Progress: " << progress->progress << "/"
                      << ach->count << " (" << (int)pct << "%)\n";
        } else {
            std::cout << "  Status: Locked\n";
        }
    } else {
        std::cout << "  Status: Locked (no progress)\n";
    }

    if (ach->isTrophy) {
        std::cout << "  Type: Trophy (can be revoked)\n";
    }
}
def display_achievement_status(gs, user_id, key):
    ach = gs.get_achievement(key)
    if not ach:
        print(f"Achievement not found: {key}")
        return

    progress = gs.get_progress(user_id, key)
    is_unlocked = progress and progress.unlocked
    is_obscured = ach.obscure and not is_unlocked

    name = "???" if is_obscured else ach.name
    desc = "Hidden achievement" if is_obscured else ach.description

    print(f"{name}\n  {desc}")
    if is_unlocked:
        print(f"  Status: ✓ UNLOCKED")
    elif progress and ach.count > 1 and progress.progress > 0:
        pct = progress.progress * 100 // ach.count
        print(f"  Status: In Progress ({progress.progress}/{ach.count}, {pct}%)")
    else:
        print("  Status: Locked")

    if ach.is_trophy:
        print("  Type: Trophy (can be revoked)")

Best Practices

1. Cache Status for Quick Access

// Maintain a local status cache updated by events
std::unordered_map<std::string, bool> achievementStatus;

void onEvent(const scorbit::Event& event) {
    std::string key, name, userId, username, iconUrl;
    bool isTrophy;

    if (event.type() == scorbit::EventType::AchievementUnlocked) {
        if (event.getAchievementUnlocked(key, name, userId, username, iconUrl, isTrophy)) {
            achievementStatus[key] = true;
        }
    } else if (event.type() == scorbit::EventType::AchievementLocked) {
        if (event.getAchievementLocked(key, name, userId, username, iconUrl)) {
            achievementStatus[key] = false;
        }
    }
}

2. Handle Unknown Users Gracefully

auto progress = gs.getProgress(userId, key);
if (!progress) {
    // User has no progress data - treat as locked with 0 progress
    showLockedState();
}

3. Respect Visibility Settings

auto ach = gs.getAchievement(key);
if (!ach->visible && !progress->unlocked) {
    // Don't show this achievement at all
    return;
}