hypixel-sdk 0.1.0

Async client for the Hypixel Public API with typed models and SkyBlock helpers
Documentation
use hypixel::HypixelClient;
use hypixel::models::skyblock::SkyBlockMember;
use hypixel::util::leveling;
use serde_json::Value;

#[tokio::main]
async fn main() -> hypixel::Result<()> {
    dotenvy::dotenv().ok();
    let key = std::env::var("HYPIXEL_API_KEY").expect("set HYPIXEL_API_KEY");
    let uuid = std::env::args()
        .nth(1)
        .unwrap_or_else(|| "be5c42d4ea454601a57b944c6413f3cd".to_string());
    let member_id = uuid.replace('-', "");
    let client = HypixelClient::new(key);

    let profiles = client.skyblock_profiles(&uuid).await?;
    let profile = profiles
        .iter()
        .find(|p| p.selected)
        .or_else(|| profiles.first())
        .expect("player has no SkyBlock profiles");

    println!(
        "Profile: {} [{}]",
        profile.cute_name.as_deref().unwrap_or("?"),
        profile.game_mode.as_deref().unwrap_or("normal")
    );
    if let Some(banking) = &profile.banking {
        println!("Bank balance: {:.0}", banking.balance);
    }

    let member = profile
        .members
        .get(&member_id)
        .expect("uuid is not a member of this profile");

    print_currencies(member);
    print_skills(member, &client).await?;
    print_combat(member);
    print_collections_summary(member);

    Ok(())
}

fn print_currencies(member: &SkyBlockMember) {
    if let Some(purse) = member
        .currencies
        .as_ref()
        .and_then(|c| c.get("coin_purse"))
        .and_then(Value::as_f64)
    {
        println!("Purse: {purse:.0}");
    }
}

async fn print_skills(member: &SkyBlockMember, client: &HypixelClient) -> hypixel::Result<()> {
    let Some(experience) = skill_experience(member) else {
        return Ok(());
    };
    let skills = client.resource_skyblock_skills().await?;

    println!("\nSkills:");
    let mut entries: Vec<(&String, &Value)> = experience.iter().collect();
    entries.sort_by_key(|(name, _)| name.to_owned());
    for (raw_name, xp) in entries {
        let name = raw_name.strip_prefix("SKILL_").unwrap_or(raw_name);
        let Some(skill) = skills.skills.get(name) else {
            continue;
        };
        let progress = leveling::skill_level(xp.as_f64().unwrap_or(0.0), skill);
        println!(
            "  {name:<14} level {:>2}/{:<2}  ({:.0} xp)",
            progress.level, skill.max_level, progress.total_xp
        );
    }
    Ok(())
}

fn skill_experience(member: &SkyBlockMember) -> Option<&serde_json::Map<String, Value>> {
    member
        .player_data
        .as_ref()
        .and_then(|d| d.get("experience"))
        .and_then(Value::as_object)
}

fn print_combat(member: &SkyBlockMember) {
    if let Some(catacombs) = member
        .dungeons
        .as_ref()
        .and_then(|d| d.get("dungeon_types"))
        .and_then(|t| t.get("catacombs"))
        .and_then(|c| c.get("experience"))
        .and_then(Value::as_f64)
    {
        println!("\nCatacombs experience: {catacombs:.0}");
    }

    if let Some(bosses) = member
        .slayer
        .as_ref()
        .and_then(|s| s.get("slayer_bosses"))
        .and_then(Value::as_object)
    {
        let total: f64 = bosses
            .values()
            .filter_map(|b| b.get("xp").and_then(Value::as_f64))
            .sum();
        println!("Total slayer experience: {total:.0}");
    }
}

fn print_collections_summary(member: &SkyBlockMember) {
    if let Some(count) = member
        .extra
        .get("collection")
        .and_then(Value::as_object)
        .map(|c| c.len())
    {
        println!("Tracked collections: {count}");
    }
}