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