hypixel-sdk 0.1.1

Async client for the Hypixel Public API with typed models and SkyBlock helpers
Documentation
use std::collections::HashMap;

use crate::models::skyblock::Bazaar;

/// A lookup of unit prices keyed by SkyBlock item id, assembled from market data.
#[derive(Debug, Clone, Default)]
pub struct PriceBook {
    prices: HashMap<String, f64>,
}

impl PriceBook {
    /// Create an empty price book.
    pub fn new() -> Self {
        Self::default()
    }

    /// Seed unit prices from a bazaar snapshot, using each product's instant-sell
    /// (buy-order) price. Bazaar ids are uppercased to match item ids.
    pub fn with_bazaar(mut self, bazaar: &Bazaar) -> Self {
        for (id, product) in &bazaar.products {
            if let Some(status) = &product.quick_status {
                self.prices.insert(id.to_uppercase(), status.buy_price);
            }
        }
        self
    }

    /// Overlay lowest-BIN prices (keyed by item id). BIN prices only fill items
    /// that have no price yet; existing bazaar prices take precedence.
    pub fn with_lowest_bin(mut self, lowest_bin: &HashMap<String, i64>) -> Self {
        for (id, price) in lowest_bin {
            self.prices
                .entry(id.to_uppercase())
                .or_insert(*price as f64);
        }
        self
    }

    /// Insert or override a single price.
    pub fn insert(&mut self, item_id: impl Into<String>, price: f64) {
        self.prices.insert(item_id.into().to_uppercase(), price);
    }

    /// Look up the unit price for an item id.
    pub fn price(&self, item_id: &str) -> Option<f64> {
        self.prices.get(&item_id.to_uppercase()).copied()
    }
}

/// One item stack to be valued.
#[derive(Debug, Clone, PartialEq)]
pub struct ItemStack {
    /// SkyBlock item id (e.g. `HYPERION`).
    pub item_id: String,
    /// Number of items in the stack.
    pub count: i64,
}

/// The valuation of a single [`ItemStack`].
#[derive(Debug, Clone, PartialEq)]
pub struct ItemValuation {
    pub item_id: String,
    pub count: i64,
    pub unit_price: f64,
    pub total: f64,
}

/// An itemized networth estimate.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Networth {
    /// Summed value of every priced stack.
    pub total: f64,
    /// Per-stack valuations that were successfully priced.
    pub items: Vec<ItemValuation>,
    /// Item ids that had no price in the [`PriceBook`].
    pub unpriced: Vec<String>,
}

/// Estimate the networth of a set of item stacks against a [`PriceBook`].
pub fn value_items(stacks: &[ItemStack], prices: &PriceBook) -> Networth {
    let mut networth = Networth::default();
    for stack in stacks {
        match prices.price(&stack.item_id) {
            Some(unit_price) => {
                let total = unit_price * stack.count as f64;
                networth.total += total;
                networth.items.push(ItemValuation {
                    item_id: stack.item_id.clone(),
                    count: stack.count,
                    unit_price,
                    total,
                });
            }
            None => networth.unpriced.push(stack.item_id.clone()),
        }
    }
    networth
}

/// Extract item stacks (SkyBlock id + count) from a decoded inventory NBT value.
///
/// Reads the standard `{ "i": [ { "Count", "tag": { "ExtraAttributes": { "id" } } } ] }`
/// layout; empty slots and items without a SkyBlock id are skipped.
#[cfg(feature = "nbt")]
pub fn extract_item_stacks(nbt: &fastnbt::Value) -> Vec<ItemStack> {
    use fastnbt::Value;

    fn as_compound(v: &Value) -> Option<&std::collections::HashMap<String, Value>> {
        match v {
            Value::Compound(map) => Some(map),
            _ => None,
        }
    }

    let items = match nbt {
        Value::Compound(root) => match root.get("i") {
            Some(Value::List(list)) => list,
            _ => return Vec::new(),
        },
        Value::List(list) => list,
        _ => return Vec::new(),
    };

    let mut stacks = Vec::new();
    for entry in items {
        let Some(item) = as_compound(entry) else {
            continue;
        };
        let count = match item.get("Count") {
            Some(Value::Byte(b)) => *b as i64,
            Some(Value::Short(s)) => *s as i64,
            Some(Value::Int(i)) => *i as i64,
            _ => 1,
        };
        let item_id = as_compound(item.get("tag").unwrap_or(&Value::Byte(0)))
            .and_then(|tag| as_compound(tag.get("ExtraAttributes").unwrap_or(&Value::Byte(0))))
            .and_then(|attrs| match attrs.get("id") {
                Some(Value::String(s)) => Some(s.clone()),
                _ => None,
            });
        if let Some(item_id) = item_id {
            stacks.push(ItemStack { item_id, count });
        }
    }
    stacks
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn values_known_items_and_tracks_unpriced() {
        let mut book = PriceBook::new();
        book.insert("HYPERION", 1_000_000.0);
        let stacks = vec![
            ItemStack {
                item_id: "hyperion".into(),
                count: 2,
            },
            ItemStack {
                item_id: "MYSTERY".into(),
                count: 1,
            },
        ];
        let nw = value_items(&stacks, &book);
        assert_eq!(nw.total, 2_000_000.0);
        assert_eq!(nw.items.len(), 1);
        assert_eq!(nw.unpriced, vec!["MYSTERY".to_string()]);
    }
}