Skip to main content

hypixel/util/
networth.rs

1use std::collections::HashMap;
2
3use crate::models::skyblock::Bazaar;
4
5/// A lookup of unit prices keyed by SkyBlock item id, assembled from market data.
6#[derive(Debug, Clone, Default)]
7pub struct PriceBook {
8    prices: HashMap<String, f64>,
9}
10
11impl PriceBook {
12    /// Create an empty price book.
13    pub fn new() -> Self {
14        Self::default()
15    }
16
17    /// Seed unit prices from a bazaar snapshot, using each product's instant-sell
18    /// (buy-order) price. Bazaar ids are uppercased to match item ids.
19    pub fn with_bazaar(mut self, bazaar: &Bazaar) -> Self {
20        for (id, product) in &bazaar.products {
21            if let Some(status) = &product.quick_status {
22                self.prices.insert(id.to_uppercase(), status.buy_price);
23            }
24        }
25        self
26    }
27
28    /// Overlay lowest-BIN prices (keyed by item id). BIN prices only fill items
29    /// that have no price yet; existing bazaar prices take precedence.
30    pub fn with_lowest_bin(mut self, lowest_bin: &HashMap<String, i64>) -> Self {
31        for (id, price) in lowest_bin {
32            self.prices
33                .entry(id.to_uppercase())
34                .or_insert(*price as f64);
35        }
36        self
37    }
38
39    /// Insert or override a single price.
40    pub fn insert(&mut self, item_id: impl Into<String>, price: f64) {
41        self.prices.insert(item_id.into().to_uppercase(), price);
42    }
43
44    /// Look up the unit price for an item id.
45    pub fn price(&self, item_id: &str) -> Option<f64> {
46        self.prices.get(&item_id.to_uppercase()).copied()
47    }
48}
49
50/// One item stack to be valued.
51#[derive(Debug, Clone, PartialEq)]
52pub struct ItemStack {
53    /// SkyBlock item id (e.g. `HYPERION`).
54    pub item_id: String,
55    /// Number of items in the stack.
56    pub count: i64,
57}
58
59/// The valuation of a single [`ItemStack`].
60#[derive(Debug, Clone, PartialEq)]
61pub struct ItemValuation {
62    pub item_id: String,
63    pub count: i64,
64    pub unit_price: f64,
65    pub total: f64,
66}
67
68/// An itemized networth estimate.
69#[derive(Debug, Clone, Default, PartialEq)]
70pub struct Networth {
71    /// Summed value of every priced stack.
72    pub total: f64,
73    /// Per-stack valuations that were successfully priced.
74    pub items: Vec<ItemValuation>,
75    /// Item ids that had no price in the [`PriceBook`].
76    pub unpriced: Vec<String>,
77}
78
79/// Estimate the networth of a set of item stacks against a [`PriceBook`].
80pub fn value_items(stacks: &[ItemStack], prices: &PriceBook) -> Networth {
81    let mut networth = Networth::default();
82    for stack in stacks {
83        match prices.price(&stack.item_id) {
84            Some(unit_price) => {
85                let total = unit_price * stack.count as f64;
86                networth.total += total;
87                networth.items.push(ItemValuation {
88                    item_id: stack.item_id.clone(),
89                    count: stack.count,
90                    unit_price,
91                    total,
92                });
93            }
94            None => networth.unpriced.push(stack.item_id.clone()),
95        }
96    }
97    networth
98}
99
100/// Extract item stacks (SkyBlock id + count) from a decoded inventory NBT value.
101///
102/// Reads the standard `{ "i": [ { "Count", "tag": { "ExtraAttributes": { "id" } } } ] }`
103/// layout; empty slots and items without a SkyBlock id are skipped.
104#[cfg(feature = "nbt")]
105pub fn extract_item_stacks(nbt: &fastnbt::Value) -> Vec<ItemStack> {
106    use fastnbt::Value;
107
108    fn as_compound(v: &Value) -> Option<&std::collections::HashMap<String, Value>> {
109        match v {
110            Value::Compound(map) => Some(map),
111            _ => None,
112        }
113    }
114
115    let items = match nbt {
116        Value::Compound(root) => match root.get("i") {
117            Some(Value::List(list)) => list,
118            _ => return Vec::new(),
119        },
120        Value::List(list) => list,
121        _ => return Vec::new(),
122    };
123
124    let mut stacks = Vec::new();
125    for entry in items {
126        let Some(item) = as_compound(entry) else {
127            continue;
128        };
129        let count = match item.get("Count") {
130            Some(Value::Byte(b)) => *b as i64,
131            Some(Value::Short(s)) => *s as i64,
132            Some(Value::Int(i)) => *i as i64,
133            _ => 1,
134        };
135        let item_id = as_compound(item.get("tag").unwrap_or(&Value::Byte(0)))
136            .and_then(|tag| as_compound(tag.get("ExtraAttributes").unwrap_or(&Value::Byte(0))))
137            .and_then(|attrs| match attrs.get("id") {
138                Some(Value::String(s)) => Some(s.clone()),
139                _ => None,
140            });
141        if let Some(item_id) = item_id {
142            stacks.push(ItemStack { item_id, count });
143        }
144    }
145    stacks
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn values_known_items_and_tracks_unpriced() {
154        let mut book = PriceBook::new();
155        book.insert("HYPERION", 1_000_000.0);
156        let stacks = vec![
157            ItemStack {
158                item_id: "hyperion".into(),
159                count: 2,
160            },
161            ItemStack {
162                item_id: "MYSTERY".into(),
163                count: 1,
164            },
165        ];
166        let nw = value_items(&stacks, &book);
167        assert_eq!(nw.total, 2_000_000.0);
168        assert_eq!(nw.items.len(), 1);
169        assert_eq!(nw.unpriced, vec!["MYSTERY".to_string()]);
170    }
171}