1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::models::skyblock::Bazaar;
6
7#[derive(Debug, Clone, Default)]
9pub struct PriceBook {
10 prices: HashMap<String, f64>,
11}
12
13impl PriceBook {
14 pub fn new() -> Self {
16 Self::default()
17 }
18
19 pub fn with_bazaar(mut self, bazaar: &Bazaar) -> Self {
22 for (id, product) in &bazaar.products {
23 if let Some(status) = &product.quick_status {
24 self.prices.insert(id.to_uppercase(), status.buy_price);
25 }
26 }
27 self
28 }
29
30 pub fn with_lowest_bin(mut self, lowest_bin: &HashMap<String, i64>) -> Self {
33 for (id, price) in lowest_bin {
34 self.prices
35 .entry(id.to_uppercase())
36 .or_insert(*price as f64);
37 }
38 self
39 }
40
41 pub fn insert(&mut self, item_id: impl Into<String>, price: f64) {
43 self.prices.insert(item_id.into().to_uppercase(), price);
44 }
45
46 pub fn price(&self, item_id: &str) -> Option<f64> {
48 self.prices.get(&item_id.to_uppercase()).copied()
49 }
50
51 pub fn prices(&self) -> &HashMap<String, f64> {
53 &self.prices
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub struct ItemStack {
60 pub item_id: String,
62 pub count: i64,
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize)]
68pub struct ItemValuation {
69 pub item_id: String,
70 pub count: i64,
71 pub unit_price: f64,
72 pub total: f64,
73}
74
75#[derive(Debug, Clone, Default, PartialEq, Serialize)]
77pub struct Networth {
78 pub total: f64,
80 pub items: Vec<ItemValuation>,
82 pub unpriced: Vec<String>,
84}
85
86#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
88pub struct ItemModifiers {
89 #[serde(default)]
91 pub recombobulated: bool,
92 #[serde(default)]
94 pub hot_potato_books: i64,
95 #[serde(default)]
97 pub art_of_war: i64,
98 #[serde(default)]
100 pub enchantments: HashMap<String, i64>,
101}
102
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
105pub struct DetailedItemStack {
106 pub item_id: String,
107 pub count: i64,
108 #[serde(default)]
109 pub modifiers: ItemModifiers,
110}
111
112#[derive(Debug, Clone, PartialEq, Serialize)]
114pub struct DetailedItemValuation {
115 pub item_id: String,
116 pub count: i64,
117 pub base_value: f64,
119 pub modifiers_value: f64,
121 pub total: f64,
122}
123
124#[derive(Debug, Clone, Default, PartialEq, Serialize)]
126pub struct DetailedNetworth {
127 pub total: f64,
128 pub items: Vec<DetailedItemValuation>,
129 pub unpriced: Vec<String>,
131}
132
133pub fn value_items_detailed(stacks: &[DetailedItemStack], prices: &PriceBook) -> DetailedNetworth {
142 let mut networth = DetailedNetworth::default();
143 for stack in stacks {
144 let Some(unit_price) = prices.price(&stack.item_id) else {
145 networth.unpriced.push(stack.item_id.clone());
146 continue;
147 };
148 let m = &stack.modifiers;
149 let mut modifiers_value = 0.0;
150 if m.recombobulated {
151 modifiers_value += prices.price("RECOMBOBULATOR_3000").unwrap_or(0.0);
152 }
153 if m.hot_potato_books > 0 {
154 let plain = m.hot_potato_books.min(10);
155 let fuming = (m.hot_potato_books - 10).max(0);
156 modifiers_value += plain as f64 * prices.price("HOT_POTATO_BOOK").unwrap_or(0.0);
157 modifiers_value += fuming as f64 * prices.price("FUMING_POTATO_BOOK").unwrap_or(0.0);
158 }
159 if m.art_of_war > 0 {
160 modifiers_value += m.art_of_war as f64 * prices.price("THE_ART_OF_WAR").unwrap_or(0.0);
161 }
162 for (enchant, level) in &m.enchantments {
163 let id = format!("ENCHANTMENT_{}_{}", enchant.to_uppercase(), level);
164 modifiers_value += prices.price(&id).unwrap_or(0.0);
165 }
166
167 let base_value = unit_price * stack.count as f64;
168 let total = base_value + modifiers_value;
169 networth.total += total;
170 networth.items.push(DetailedItemValuation {
171 item_id: stack.item_id.clone(),
172 count: stack.count,
173 base_value,
174 modifiers_value,
175 total,
176 });
177 }
178 networth
179}
180
181pub fn value_items(stacks: &[ItemStack], prices: &PriceBook) -> Networth {
183 let mut networth = Networth::default();
184 for stack in stacks {
185 match prices.price(&stack.item_id) {
186 Some(unit_price) => {
187 let total = unit_price * stack.count as f64;
188 networth.total += total;
189 networth.items.push(ItemValuation {
190 item_id: stack.item_id.clone(),
191 count: stack.count,
192 unit_price,
193 total,
194 });
195 }
196 None => networth.unpriced.push(stack.item_id.clone()),
197 }
198 }
199 networth
200}
201
202#[cfg(feature = "nbt")]
207pub fn extract_item_stacks(nbt: &fastnbt::Value) -> Vec<ItemStack> {
208 extract_item_stacks_detailed(nbt)
209 .into_iter()
210 .map(|stack| ItemStack {
211 item_id: stack.item_id,
212 count: stack.count,
213 })
214 .collect()
215}
216
217#[cfg(feature = "nbt")]
224pub fn extract_item_stacks_detailed(nbt: &fastnbt::Value) -> Vec<DetailedItemStack> {
225 use fastnbt::Value;
226
227 fn as_compound(v: &Value) -> Option<&std::collections::HashMap<String, Value>> {
228 match v {
229 Value::Compound(map) => Some(map),
230 _ => None,
231 }
232 }
233
234 fn as_int(v: &Value) -> Option<i64> {
235 match v {
236 Value::Byte(n) => Some(*n as i64),
237 Value::Short(n) => Some(*n as i64),
238 Value::Int(n) => Some(*n as i64),
239 Value::Long(n) => Some(*n),
240 _ => None,
241 }
242 }
243
244 let items = match nbt {
245 Value::Compound(root) => match root.get("i") {
246 Some(Value::List(list)) => list,
247 _ => return Vec::new(),
248 },
249 Value::List(list) => list,
250 _ => return Vec::new(),
251 };
252
253 let mut stacks = Vec::new();
254 for entry in items {
255 let Some(item) = as_compound(entry) else {
256 continue;
257 };
258 let count = item.get("Count").and_then(as_int).unwrap_or(1);
259 let Some(attrs) = as_compound(item.get("tag").unwrap_or(&Value::Byte(0)))
260 .and_then(|tag| as_compound(tag.get("ExtraAttributes").unwrap_or(&Value::Byte(0))))
261 else {
262 continue;
263 };
264 let Some(Value::String(item_id)) = attrs.get("id") else {
265 continue;
266 };
267
268 let mut modifiers = ItemModifiers {
269 recombobulated: attrs.get("rarity_upgrades").and_then(as_int).unwrap_or(0) > 0,
270 hot_potato_books: attrs.get("hot_potato_count").and_then(as_int).unwrap_or(0),
271 art_of_war: attrs.get("art_of_war_count").and_then(as_int).unwrap_or(0),
272 ..ItemModifiers::default()
273 };
274 if let Some(enchants) = attrs.get("enchantments").and_then(as_compound) {
275 for (name, level) in enchants {
276 if let Some(level) = as_int(level) {
277 modifiers.enchantments.insert(name.clone(), level);
278 }
279 }
280 }
281
282 stacks.push(DetailedItemStack {
283 item_id: item_id.clone(),
284 count,
285 modifiers,
286 });
287 }
288 stacks
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn detailed_valuation_prices_modifiers() {
297 let mut book = PriceBook::new();
298 book.insert("ASPECT_OF_THE_END", 100_000.0);
299 book.insert("RECOMBOBULATOR_3000", 8_000_000.0);
300 book.insert("HOT_POTATO_BOOK", 80_000.0);
301 book.insert("FUMING_POTATO_BOOK", 1_200_000.0);
302 book.insert("ENCHANTMENT_SHARPNESS_6", 500_000.0);
303
304 let stack = DetailedItemStack {
305 item_id: "ASPECT_OF_THE_END".into(),
306 count: 1,
307 modifiers: ItemModifiers {
308 recombobulated: true,
309 hot_potato_books: 12,
310 art_of_war: 0,
311 enchantments: [("sharpness".to_string(), 6i64)].into_iter().collect(),
312 },
313 };
314 let nw = value_items_detailed(&[stack], &book);
315 assert_eq!(nw.items.len(), 1);
316 let expected_modifiers = 8_000_000.0 + 10.0 * 80_000.0 + 2.0 * 1_200_000.0 + 500_000.0;
317 assert!((nw.items[0].modifiers_value - expected_modifiers).abs() < 1e-6);
318 assert!((nw.total - (100_000.0 + expected_modifiers)).abs() < 1e-6);
319 }
320
321 #[test]
322 fn values_known_items_and_tracks_unpriced() {
323 let mut book = PriceBook::new();
324 book.insert("HYPERION", 1_000_000.0);
325 let stacks = vec![
326 ItemStack {
327 item_id: "hyperion".into(),
328 count: 2,
329 },
330 ItemStack {
331 item_id: "MYSTERY".into(),
332 count: 1,
333 },
334 ];
335 let nw = value_items(&stacks, &book);
336 assert_eq!(nw.total, 2_000_000.0);
337 assert_eq!(nw.items.len(), 1);
338 assert_eq!(nw.unpriced, vec!["MYSTERY".to_string()]);
339 }
340}