Skip to content

Achievement Groups

Achievement groups allow you to organize related achievements together for better display and categorization. This is useful for creating achievement tiers, collections, or thematic groupings.

Understanding Groups

A group is a collection of related achievements where:

  • Each achievement can belong to at most one group
  • Grouped achievements share a common group_id
  • Groups are primarily for organizational and display purposes

Identifying Grouped Achievements

Check if an achievement belongs to a group:

sb_achievement_t achievement;
if (sb_get_cached_achievement(gs, "multiball_bronze", &achievement)) {
    if (achievement.group_id > 0) {
        printf("This achievement belongs to group %d\n", achievement.group_id);
    } else {
        printf("This is a standalone achievement\n");
    }
}
auto achievement = gs.getAchievement("multiball_bronze");
if (achievement) {
    if (achievement->groupId > 0) {
        std::cout << "This achievement belongs to group " << achievement->groupId << "\n";
    } else {
        std::cout << "This is a standalone achievement\n";
    }
}
achievement = gs.get_achievement("multiball_bronze")
if achievement:
    if achievement.group_id > 0:
        print(f"This achievement belongs to group {achievement.group_id}")
    else:
        print("This is a standalone achievement")

Finding Group Members

To find all achievements in a specific group:

// Find all achievements in a group, returns count found
size_t get_group_members(sb_game_handle_t gs, int group_id,
                         sb_achievement_t *out, size_t max) {
    size_t total = sb_get_cached_achievements_count(gs);
    size_t found = 0;
    for (size_t i = 0; i < total && found < max; i++) {
        sb_achievement_t ach;
        if (sb_get_cached_achievement_at(gs, i, &ach)
            && ach.group_id == group_id) {
            out[found++] = ach;
        }
    }
    return found;
}
// Find all achievements in a group
std::vector<scorbit::Achievement> getGroupMembers(
    scorbit::GameState& gs,
    int groupId
) {
    std::vector<scorbit::Achievement> members;
    auto allAchievements = gs.getAchievements();

    for (const auto& ach : allAchievements) {
        if (ach.groupId == groupId) {
            members.push_back(ach);
        }
    }
    return members;
}

// Usage
auto multiballGroup = getGroupMembers(gs, 1);
std::cout << "Group 1 has " << multiballGroup.size() << " achievements\n";
def get_group_members(gs, group_id):
    """Find all achievements in a specific group."""
    all_achievements = gs.get_achievements()
    return [a for a in all_achievements if a.group_id == group_id]

# Usage
multiball_group = get_group_members(gs, 1)
print(f"Group 1 has {len(multiball_group)} achievements")

Example: Tiered Achievement System

A common pattern is tiered achievements (Bronze → Silver → Gold):

Group: "Multiball Achievements" (group_id: 5)
├── multiball_bronze   - "Start 10 multiballs"  (count: 10)
├── multiball_silver   - "Start 50 multiballs"  (count: 50)
└── multiball_gold     - "Start 100 multiballs" (count: 100)
void display_tiered_progress(sb_game_handle_t gs, int64_t user_id, int group_id) {
    sb_achievement_t members[32];
    size_t count = get_group_members(gs, group_id, members, 32);

    for (size_t i = 0; i < count; i++) {
        sb_achievement_progress_t prog;
        bool unlocked = sb_get_cached_progress(gs, user_id, members[i].key, &prog)
                        && prog.unlocked;
        int current = unlocked ? prog.progress : 0;
        printf("%s %s: %d/%d\n", unlocked ? "✓" : "○",
               members[i].name, current, members[i].count);
    }
}
// Display tiered achievement progress
void displayTieredProgress(scorbit::GameState& gs, int64_t userId, int groupId) {
    auto members = getGroupMembers(gs, groupId);

    // Sort by target count for tier ordering
    std::sort(members.begin(), members.end(),
        [](const auto& a, const auto& b) { return a.count < b.count; });

    for (const auto& ach : members) {
        auto progress = gs.getProgress(userId, ach.key);
        int current = progress ? progress->progress : 0;
        bool unlocked = progress ? progress->unlocked : false;

        std::cout << (unlocked ? "✓ " : "○ ")
                  << ach.name << ": "
                  << current << "/" << ach.count << "\n";
    }
}
def display_tiered_progress(gs, user_id, group_id):
    """Display tiered achievement progress."""
    members = get_group_members(gs, group_id)

    # Sort by target count for tier ordering
    members.sort(key=lambda a: a.count)

    for ach in members:
        progress = gs.get_progress(user_id, ach.key)
        current = progress.progress if progress else 0
        unlocked = progress.unlocked if progress else False

        status = "✓" if unlocked else "○"
        print(f"{status} {ach.name}: {current}/{ach.count}")

Displaying Grouped Achievements

When displaying achievements, consider grouping them visually:

void display_grouped_achievements(sb_game_handle_t gs) {
    size_t total = sb_get_cached_achievements_count(gs);

    // Collect unique group IDs (simple approach)
    int seen_groups[64];
    size_t num_groups = 0;

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

        // Check if group already seen
        bool found = false;
        for (size_t g = 0; g < num_groups; g++) {
            if (seen_groups[g] == ach.group_id) { found = true; break; }
        }
        if (!found && num_groups < 64)
            seen_groups[num_groups++] = ach.group_id;
    }

    // Display each group
    for (size_t g = 0; g < num_groups; g++) {
        printf("=== Group %d ===\n", seen_groups[g]);
        for (size_t i = 0; i < total; i++) {
            sb_achievement_t ach;
            if (sb_get_cached_achievement_at(gs, i, &ach)
                && ach.group_id == seen_groups[g])
                printf("  %s\n", ach.name);
        }
    }

    // Display ungrouped
    printf("=== Other Achievements ===\n");
    for (size_t i = 0; i < total; i++) {
        sb_achievement_t ach;
        if (sb_get_cached_achievement_at(gs, i, &ach) && ach.group_id <= 0)
            printf("  %s\n", ach.name);
    }
}
// Organize achievements by group for display
std::map<int, std::vector<scorbit::Achievement>> byGroup;
std::vector<scorbit::Achievement> ungrouped;

for (const auto& ach : gs.getAchievements()) {
    if (ach.groupId > 0) {
        byGroup[ach.groupId].push_back(ach);
    } else {
        ungrouped.push_back(ach);
    }
}

// Display grouped achievements
for (const auto& [groupId, members] : byGroup) {
    std::cout << "=== Group " << groupId << " ===\n";
    for (const auto& ach : members) {
        std::cout << "  " << ach.name << "\n";
    }
}

// Display standalone achievements
std::cout << "=== Other Achievements ===\n";
for (const auto& ach : ungrouped) {
    std::cout << "  " << ach.name << "\n";
}
from collections import defaultdict

# Organize achievements by group for display
by_group = defaultdict(list)
ungrouped = []

for ach in gs.get_achievements():
    if ach.group_id > 0:
        by_group[ach.group_id].append(ach)
    else:
        ungrouped.append(ach)

# Display grouped achievements
for group_id, members in by_group.items():
    print(f"=== Group {group_id} ===")
    for ach in members:
        print(f"  {ach.name}")

# Display standalone achievements
print("=== Other Achievements ===")
for ach in ungrouped:
    print(f"  {ach.name}")

Group Progress Summary

Show overall progress for a group:

void display_group_summary(sb_game_handle_t gs, int64_t user_id, int group_id) {
    sb_achievement_t members[32];
    size_t count = get_group_members(gs, group_id, members, 32);
    int unlocked = 0;

    for (size_t i = 0; i < count; i++) {
        sb_achievement_progress_t prog;
        if (sb_get_cached_progress(gs, user_id, members[i].key, &prog)
            && prog.unlocked)
            unlocked++;
    }
    printf("Group %d: %d/%zu complete\n", group_id, unlocked, count);
}
void displayGroupSummary(scorbit::GameState& gs, int64_t userId, int groupId) {
    auto members = getGroupMembers(gs, groupId);

    int totalUnlocked = 0;
    int totalCount = members.size();

    for (const auto& ach : members) {
        auto progress = gs.getProgress(userId, ach.key);
        if (progress && progress->unlocked) {
            totalUnlocked++;
        }
    }

    std::cout << "Group " << groupId << ": "
              << totalUnlocked << "/" << totalCount << " complete\n";
}
def display_group_summary(gs, user_id, group_id):
    """Show overall progress for a group."""
    members = get_group_members(gs, group_id)

    total_unlocked = 0
    for ach in members:
        progress = gs.get_progress(user_id, ach.key)
        if progress and progress.unlocked:
            total_unlocked += 1

    print(f"Group {group_id}: {total_unlocked}/{len(members)} complete")

UI Considerations

  • Show group progress (e.g., "3/5 completed")
  • Collapse completed groups to reduce clutter
  • Highlight the next achievable tier in a group
  • Use visual indicators to show group relationships