diff --git a/crates/foreversight/README.md b/crates/foreversight/README.md index 5a436db..2f9d5ca 100644 --- a/crates/foreversight/README.md +++ b/crates/foreversight/README.md @@ -83,6 +83,10 @@ nix shell nixpkgs#lua5_1 -c lua generate.lua > Sample.lua - `GetInventoryItemID` gives `0`, not `nil`, for the ammo slot (0), which Forever doesn't have. - Item links use the newer quality markup, like `|cnIQ1:|Hitem:…|h[Name]|h|r`. +- Items with random enchantments, like "Short Bastard Sword of Fire + Protection", share an item ID. The version is a bonus ID in the link, not a + suffix ID: `|Hitem:3192::::::::6:1486:::1:12741::::::|h`. Wowhead takes it as + `?bonus=12741`. ## Open questions @@ -90,10 +94,6 @@ nix shell nixpkgs#lua5_1 -c lua generate.lua > Sample.lua Forever. Check it in game with `/dump select(4, GetBuildInfo())`. - The addon handles both the classic bank (bank container plus bank bags) and the newer bank tabs. We don't know yet which one Forever has. -- Items with random enchantments, like "of the Eagle", share an item ID, so - they all link to the same Wowhead page. Their links should have the suffix - ID after the item ID, but we haven't seen a real one from Forever yet, or - checked whether Wowhead's Forever database takes `?rand=`. - Is using Wowhead's tooltip data and image CDN for icons OK under its terms? Blizzard's Game Data API has official item media, but needs API credentials, and may not cover Forever. diff --git a/crates/foreversight/fixtures/Foreversight.lua b/crates/foreversight/fixtures/Foreversight.lua index a62a989..771dbac 100644 --- a/crates/foreversight/fixtures/Foreversight.lua +++ b/crates/foreversight/fixtures/Foreversight.lua @@ -2,142 +2,163 @@ ForeversightDB = { ["version"] = 1, ["characters"] = { -["Pious Egret-Classic Beta PvE"] = { -["money"] = 213, -["class"] = "PRIEST", -["updatedAt"] = 1790528379, -["bags"] = { +["Pal Egret-Classic Beta PvE"] = { +["realm"] = "Classic Beta PvE", +["money"] = 639, +["class"] = "PALADIN", +["className"] = "Paladin", +["skills"] = { { -["items"] = { +["name"] = "Mining", +["profession"] = "primary", +["skillLine"] = 186, +["maxRank"] = 75, +["rank"] = 3, }, -["size"] = 6, }, +["equipment"] = { +nil, +nil, +nil, { -["items"] = { +["id"] = 6117, +["link"] = "|cnIQ1:|Hitem:6117::::::::6:1486::75:::::::|h[Squire's Shirt]|h|r", +["count"] = 1, +["quality"] = 1, }, -["size"] = 6, +{ +["id"] = 1364, +["link"] = "|cnIQ0:|Hitem:1364::::::::6:1486:::::::::|h[Ragged Leather Vest]|h|r", +["count"] = 1, +["quality"] = 0, }, -nil, { -["items"] = { +["id"] = 2172, +["link"] = "|cnIQ1:|Hitem:2172::::::::6:1486::11:::::::|h[Rustic Belt]|h|r", +["count"] = 1, +["quality"] = 1, +}, { -["id"] = 2901, -["link"] = "|cnIQ1:|Hitem:2901::::::::6:1487::14:::::::|h[Mining Pick]|h|r", +["id"] = 2654, +["link"] = "|cnIQ0:|Hitem:2654::::::::6:1486:::::::::|h[Flimsy Chain Pants]|h|r", +["count"] = 1, +["quality"] = 0, +}, +{ +["id"] = 1367, +["link"] = "|cnIQ0:|Hitem:1367::::::::6:1486:::::::::|h[Ragged Leather Boots]|h|r", +["count"] = 1, +["quality"] = 0, +}, +{ +["id"] = 1370, +["link"] = "|cnIQ0:|Hitem:1370::::::::6:1486:::::::::|h[Ragged Leather Bracers]|h|r", +["count"] = 1, +["quality"] = 0, +}, +{ +["id"] = 2547, +["link"] = "|cnIQ1:|Hitem:2547::::::::6:1486::11:::::::|h[Boar Handler Gloves]|h|r", ["count"] = 1, ["quality"] = 1, }, -[3] = { -["id"] = 2835, -["link"] = "|cnIQ1:|Hitem:2835::::::::6:1487:::::::::|h[Rough Stone]|h|r", -["count"] = 3, +[16] = { +["id"] = 5761, +["link"] = "|cnIQ1:|Hitem:5761::::::::6:1486::11:::::::|h[Anvilmar Sledge]|h|r", +["count"] = 1, ["quality"] = 1, }, +[15] = { +["id"] = 2652, +["link"] = "|cnIQ0:|Hitem:2652::::::::6:1486:::::::::|h[Flimsy Chain Cloak]|h|r", +["count"] = 1, +["quality"] = 0, }, -["size"] = 4, +}, +["race"] = "Dwarf", +["name"] = "Pal", +["faction"] = "Alliance", +["updatedAt"] = 1790540693, +["level"] = 6, +["bags"] = { +{ +["items"] = { +}, +["size"] = 6, }, [0] = { ["items"] = { { ["id"] = 6948, -["link"] = "|cnIQ1:|Hitem:6948::::::::6:1487::75:::::::|h[Hearthstone]|h|r", +["link"] = "|cnIQ1:|Hitem:6948::::::::6:1486::75:::::::|h[Hearthstone]|h|r", ["count"] = 1, ["quality"] = 1, }, { -["id"] = 4536, -["link"] = "|cnIQ1:|Hitem:4536::::::::6:1487:::::::::|h[Shiny Red Apple]|h|r", -["count"] = 4, +["id"] = 961, +["link"] = "|cnIQ1:|Hitem:961::::::::6:1486:::::::::|h[Healing Herb]|h|r", +["count"] = 3, ["quality"] = 1, }, { -["id"] = 2070, -["link"] = "|cnIQ1:|Hitem:2070::::::::6:1487:::::::::|h[Darnassian Bleu]|h|r", -["count"] = 6, +["id"] = 4540, +["link"] = "|cnIQ1:|Hitem:4540::::::::6:1486:::::::::|h[Tough Hunk of Bread]|h|r", +["count"] = 4, ["quality"] = 1, }, { ["id"] = 159, -["link"] = "|cnIQ1:|Hitem:159::::::::6:1487:::::::::|h[Refreshing Spring Water]|h|r", -["count"] = 9, +["link"] = "|cnIQ1:|Hitem:159::::::::6:1486:::::::::|h[Refreshing Spring Water]|h|r", +["count"] = 12, ["quality"] = 1, }, { -["id"] = 51, -["link"] = "|cnIQ1:|Hitem:51::::::::6:1487::75:::::::|h[Neophyte's Boots]|h|r", -["count"] = 1, +["id"] = 117, +["link"] = "|cnIQ1:|Hitem:117::::::::6:1486:::::::::|h[Tough Jerky]|h|r", +["count"] = 20, ["quality"] = 1, }, { -["id"] = 2672, -["link"] = "|cnIQ1:|Hitem:2672::::::::6:1487:::::::::|h[Stringy Wolf Meat]|h|r", -["count"] = 2, +["id"] = 6176, +["link"] = "|cnIQ1:|Hitem:6176::::::::6:1486::11:::::::|h[Dwarven Kite Shield]|h|r", +["count"] = 1, ["quality"] = 1, }, { ["id"] = 2589, -["link"] = "|cnIQ1:|Hitem:2589::::::::6:1487:::::::::|h[Linen Cloth]|h|r", -["count"] = 20, +["link"] = "|cnIQ1:|Hitem:2589::::::::6:1486:::::::::|h[Linen Cloth]|h|r", +["count"] = 19, ["quality"] = 1, }, { -["id"] = 11475, -["link"] = "|cnIQ1:|Hitem:11475::::::::6:1487::11:::::::|h[Wine-stained Cloak]|h|r", +["id"] = 117, +["link"] = "|cnIQ1:|Hitem:117::::::::6:1486:::::::::|h[Tough Jerky]|h|r", ["count"] = 1, ["quality"] = 1, }, { -["id"] = 1380, -["link"] = "|cnIQ0:|Hitem:1380::::::::6:1487:::::::::|h[Frayed Robe]|h|r", +["id"] = 7074, +["link"] = "|cnIQ0:|Hitem:7074::::::::6:1486:::::::::|h[Chipped Claw]|h|r", ["count"] = 1, ["quality"] = 0, }, { -["id"] = 279935, -["link"] = "|cnIQ1:|Hitem:279935::::::::6:1487:::::::::|h[Book Bag]|h|r", -["count"] = 1, -["quality"] = 1, -}, -{ -["id"] = 1476, -["link"] = "|cnIQ0:|Hitem:1476::::::::6:1487:::::::::|h[Snapped Spider Limb]|h|r", -["count"] = 3, -["quality"] = 0, -}, -{ -["id"] = 5465, -["link"] = "|cnIQ1:|Hitem:5465::::::::6:1487:::::::::|h[Small Spider Leg]|h|r", -["count"] = 4, -["quality"] = 1, -}, -{ -["id"] = 2590, -["link"] = "|cnIQ0:|Hitem:2590::::::::6:1487:::::::::|h[Forest Spider Webbing]|h|r", -["count"] = 2, -["quality"] = 0, -}, -{ -["id"] = 769, -["link"] = "|cnIQ1:|Hitem:769::::::::6:1487:::::::::|h[Chunk of Boar Meat]|h|r", +["id"] = 286355, +["link"] = "|cnIQ1:|Hitem:286355::::::::6:1486:::::::::|h[Crate of Candles]|h|r", ["count"] = 1, ["quality"] = 1, }, { -["id"] = 2840, -["link"] = "|cnIQ1:|Hitem:2840::::::::6:1487:::::::::|h[Copper Bar]|h|r", -["count"] = 3, -["quality"] = 1, -}, -{ -["id"] = 771, -["link"] = "|cnIQ0:|Hitem:771::::::::6:1487:::::::::|h[Chipped Boar Tusk]|h|r", +["id"] = 3192, +["link"] = "|cnIQ2:|Hitem:3192::::::::6:1486:::1:12741::::::|h[Short Bastard Sword of Fire Protection]|h|r", ["count"] = 1, -["quality"] = 0, +["quality"] = 2, }, { -["id"] = 3171, -["link"] = "|cnIQ0:|Hitem:3171::::::::6:1487:::::::::|h[Broken Boar Tusk]|h|r", +["id"] = 2886, +["link"] = "|cnIQ1:|Hitem:2886::::::::6:1486:::::::::|h[Crag Boar Rib]|h|r", ["count"] = 1, -["quality"] = 0, +["quality"] = 1, }, }, ["size"] = 20, @@ -148,6 +169,17 @@ nil, ["size"] = 32, }, }, +["surname"] = "Egret", +}, +["Pious Egret-Classic Beta PvE"] = { +["mailUpdatedAt"] = 1790541667, +["level"] = 6, +["faction"] = "Alliance", +["realm"] = "Classic Beta PvE", +["money"] = 213, +["class"] = "PRIEST", +["mailNotShown"] = 0, +["updatedAt"] = 1790541627, ["equipment"] = { nil, nil, @@ -209,155 +241,157 @@ nil, }, ["race"] = "Human", ["name"] = "Pious", -["faction"] = "Alliance", ["className"] = "Priest", -["level"] = 6, -["realm"] = "Classic Beta PvE", -["surname"] = "Egret", +["mail"] = { }, -["Pal Egret-Classic Beta PvE"] = { -["money"] = 639, -["class"] = "PALADIN", -["realm"] = "Classic Beta PvE", -["className"] = "Paladin", -["equipment"] = { -nil, -nil, -nil, +["skills"] = { { -["id"] = 6117, -["quality"] = 1, -["count"] = 1, -["link"] = "|cnIQ1:|Hitem:6117::::::::6:1486::75:::::::|h[Squire's Shirt]|h|r", +["name"] = "Mining", +["profession"] = "primary", +["skillLine"] = 186, +["maxRank"] = 75, +["rank"] = 8, }, { -["id"] = 1364, -["quality"] = 0, -["count"] = 1, -["link"] = "|cnIQ0:|Hitem:1364::::::::6:1486:::::::::|h[Ragged Leather Vest]|h|r", +["name"] = "Cooking", +["profession"] = "secondary", +["skillLine"] = 185, +["maxRank"] = 75, +["rank"] = 1, }, -{ -["id"] = 2172, -["quality"] = 1, -["count"] = 1, -["link"] = "|cnIQ1:|Hitem:2172::::::::6:1486::11:::::::|h[Rustic Belt]|h|r", }, +["bags"] = { { -["id"] = 2654, -["quality"] = 0, -["count"] = 1, -["link"] = "|cnIQ0:|Hitem:2654::::::::6:1486:::::::::|h[Flimsy Chain Pants]|h|r", +["items"] = { }, -{ -["id"] = 1367, -["quality"] = 0, -["count"] = 1, -["link"] = "|cnIQ0:|Hitem:1367::::::::6:1486:::::::::|h[Ragged Leather Boots]|h|r", +["size"] = 6, }, { -["id"] = 1370, -["quality"] = 0, -["count"] = 1, -["link"] = "|cnIQ0:|Hitem:1370::::::::6:1486:::::::::|h[Ragged Leather Bracers]|h|r", +["items"] = { +}, +["size"] = 6, }, +nil, { -["id"] = 2547, -["quality"] = 1, +["items"] = { +{ +["id"] = 2901, +["link"] = "|cnIQ1:|Hitem:2901::::::::6:1487::14:::::::|h[Mining Pick]|h|r", ["count"] = 1, -["link"] = "|cnIQ1:|Hitem:2547::::::::6:1486::11:::::::|h[Boar Handler Gloves]|h|r", -}, -[16] = { -["id"] = 5761, ["quality"] = 1, -["count"] = 1, -["link"] = "|cnIQ1:|Hitem:5761::::::::6:1486::11:::::::|h[Anvilmar Sledge]|h|r", }, -[15] = { -["id"] = 2652, -["quality"] = 0, -["count"] = 1, -["link"] = "|cnIQ0:|Hitem:2652::::::::6:1486:::::::::|h[Flimsy Chain Cloak]|h|r", +[3] = { +["id"] = 2835, +["link"] = "|cnIQ1:|Hitem:2835::::::::6:1487:::::::::|h[Rough Stone]|h|r", +["count"] = 3, +["quality"] = 1, }, }, -["race"] = "Dwarf", -["name"] = "Pal", -["faction"] = "Alliance", -["updatedAt"] = 1790528345, -["level"] = 6, -["bags"] = { -{ -["items"] = { -}, -["size"] = 6, +["size"] = 4, }, [0] = { ["items"] = { { ["id"] = 6948, -["quality"] = 1, +["link"] = "|cnIQ1:|Hitem:6948::::::::6:1487::75:::::::|h[Hearthstone]|h|r", ["count"] = 1, -["link"] = "|cnIQ1:|Hitem:6948::::::::6:1486::75:::::::|h[Hearthstone]|h|r", +["quality"] = 1, }, { -["id"] = 961, +["id"] = 4536, +["link"] = "|cnIQ1:|Hitem:4536::::::::6:1487:::::::::|h[Shiny Red Apple]|h|r", +["count"] = 4, ["quality"] = 1, -["count"] = 3, -["link"] = "|cnIQ1:|Hitem:961::::::::6:1486:::::::::|h[Healing Herb]|h|r", }, { -["id"] = 4540, +["id"] = 2070, +["link"] = "|cnIQ1:|Hitem:2070::::::::6:1487:::::::::|h[Darnassian Bleu]|h|r", +["count"] = 6, ["quality"] = 1, -["count"] = 4, -["link"] = "|cnIQ1:|Hitem:4540::::::::6:1486:::::::::|h[Tough Hunk of Bread]|h|r", }, { ["id"] = 159, +["link"] = "|cnIQ1:|Hitem:159::::::::6:1487:::::::::|h[Refreshing Spring Water]|h|r", +["count"] = 9, ["quality"] = 1, -["count"] = 12, -["link"] = "|cnIQ1:|Hitem:159::::::::6:1486:::::::::|h[Refreshing Spring Water]|h|r", }, { -["id"] = 117, +["id"] = 51, +["link"] = "|cnIQ1:|Hitem:51::::::::6:1487::75:::::::|h[Neophyte's Boots]|h|r", +["count"] = 1, ["quality"] = 1, -["count"] = 20, -["link"] = "|cnIQ1:|Hitem:117::::::::6:1486:::::::::|h[Tough Jerky]|h|r", }, { -["id"] = 6176, +["id"] = 2672, +["link"] = "|cnIQ1:|Hitem:2672::::::::6:1487:::::::::|h[Stringy Wolf Meat]|h|r", +["count"] = 2, ["quality"] = 1, -["count"] = 1, -["link"] = "|cnIQ1:|Hitem:6176::::::::6:1486::11:::::::|h[Dwarven Kite Shield]|h|r", }, { ["id"] = 2589, +["link"] = "|cnIQ1:|Hitem:2589::::::::6:1487:::::::::|h[Linen Cloth]|h|r", +["count"] = 20, ["quality"] = 1, -["count"] = 19, -["link"] = "|cnIQ1:|Hitem:2589::::::::6:1486:::::::::|h[Linen Cloth]|h|r", }, { -["id"] = 117, -["quality"] = 1, +["id"] = 11475, +["link"] = "|cnIQ1:|Hitem:11475::::::::6:1487::11:::::::|h[Wine-stained Cloak]|h|r", ["count"] = 1, -["link"] = "|cnIQ1:|Hitem:117::::::::6:1486:::::::::|h[Tough Jerky]|h|r", +["quality"] = 1, }, { -["id"] = 7074, +["id"] = 1380, +["link"] = "|cnIQ0:|Hitem:1380::::::::6:1487:::::::::|h[Frayed Robe]|h|r", +["count"] = 1, ["quality"] = 0, +}, +{ +["id"] = 279935, +["link"] = "|cnIQ1:|Hitem:279935::::::::6:1487:::::::::|h[Book Bag]|h|r", ["count"] = 1, -["link"] = "|cnIQ0:|Hitem:7074::::::::6:1486:::::::::|h[Chipped Claw]|h|r", +["quality"] = 1, }, { -["id"] = 286355, +["id"] = 1476, +["link"] = "|cnIQ0:|Hitem:1476::::::::6:1487:::::::::|h[Snapped Spider Limb]|h|r", +["count"] = 3, +["quality"] = 0, +}, +{ +["id"] = 5465, +["link"] = "|cnIQ1:|Hitem:5465::::::::6:1487:::::::::|h[Small Spider Leg]|h|r", +["count"] = 4, ["quality"] = 1, +}, +{ +["id"] = 2590, +["link"] = "|cnIQ0:|Hitem:2590::::::::6:1487:::::::::|h[Forest Spider Webbing]|h|r", +["count"] = 2, +["quality"] = 0, +}, +{ +["id"] = 769, +["link"] = "|cnIQ1:|Hitem:769::::::::6:1487:::::::::|h[Chunk of Boar Meat]|h|r", ["count"] = 1, -["link"] = "|cnIQ1:|Hitem:286355::::::::6:1486:::::::::|h[Crate of Candles]|h|r", +["quality"] = 1, }, -nil, { -["id"] = 2886, +["id"] = 2840, +["link"] = "|cnIQ1:|Hitem:2840::::::::6:1487:::::::::|h[Copper Bar]|h|r", +["count"] = 3, ["quality"] = 1, +}, +{ +["id"] = 771, +["link"] = "|cnIQ0:|Hitem:771::::::::6:1487:::::::::|h[Chipped Boar Tusk]|h|r", ["count"] = 1, -["link"] = "|cnIQ1:|Hitem:2886::::::::6:1486:::::::::|h[Crag Boar Rib]|h|r", +["quality"] = 0, +}, +{ +["id"] = 3171, +["link"] = "|cnIQ0:|Hitem:3171::::::::6:1487:::::::::|h[Broken Boar Tusk]|h|r", +["count"] = 1, +["quality"] = 0, }, }, ["size"] = 20, diff --git a/crates/foreversight/src/app_view.rs b/crates/foreversight/src/app_view.rs index 8f745f9..c0224ee 100644 --- a/crates/foreversight/src/app_view.rs +++ b/crates/foreversight/src/app_view.rs @@ -479,7 +479,11 @@ fn result_row( .style(|s| s.flex_1().flex().flex_col().gap_0p5()) .child( Link::root() - .id(format!("item-{}", item.item.item_id)) + .id(format!( + "item-{}-{}", + item.item.wowhead_url(), + item.item.name + )) .href(item.item.wowhead_url()) .style(move |s| { // Only as wide as the name, so the rest of the row diff --git a/crates/foreversight/src/inventory.rs b/crates/foreversight/src/inventory.rs index 65fa757..1dbe149 100644 --- a/crates/foreversight/src/inventory.rs +++ b/crates/foreversight/src/inventory.rs @@ -12,6 +12,7 @@ pub struct Inventory { /// All the stacks of one item, across characters. pub struct Item { pub item_id: u32, + pub bonus_ids: Vec, pub name: String, pub quality: Quality, pub holdings: Vec, @@ -22,7 +23,12 @@ pub struct Item { impl Item { /// The item's page in Wowhead's database for Forever. pub fn wowhead_url(&self) -> String { - format!("https://www.wowhead.com/forever/item={}", self.item_id) + let mut url = format!("https://www.wowhead.com/forever/item={}", self.item_id); + if !self.bonus_ids.is_empty() { + let bonus_ids: Vec<_> = self.bonus_ids.iter().map(u32::to_string).collect(); + url += &format!("?bonus={}", bonus_ids.join(":")); + } + url } } @@ -49,15 +55,16 @@ impl Inventory { pub fn new(mut characters: Vec) -> Self { characters.sort_by_cached_key(|character| (character.realm.clone(), character.full_name())); - // Items with random enchantments, like "of the Bear", share an ID but - // not a name, so group by both. - let mut items = BTreeMap::<(u32, &str), Item>::new(); + // Versions of an item, like "of the Bear", share an ID but not their + // bonus IDs or name, so group by all three. + let mut items = BTreeMap::<(u32, &[u32], &str), Item>::new(); for (index, character) in characters.iter().enumerate() { for stack in &character.items { let item = items - .entry((stack.item_id, &stack.name)) + .entry((stack.item_id, &stack.bonus_ids, &stack.name)) .or_insert_with(|| Item { item_id: stack.item_id, + bonus_ids: stack.bonus_ids.clone(), name: stack.name.clone(), quality: stack.quality, holdings: Vec::new(), @@ -140,6 +147,7 @@ mod tests { .iter() .map(|&(item_id, name, count, location, expires_at)| ItemStack { item_id, + bonus_ids: Vec::new(), name: name.into(), quality: Quality::Common, count, @@ -253,4 +261,22 @@ mod tests { ] ); } + + #[test] + fn links_to_item_versions() { + let globals = crate::lua::parse(include_str!("../fixtures/Foreversight.lua")).unwrap(); + let characters = crate::model::characters_from_saved_variables(&globals).unwrap(); + let inventory = Inventory::new(characters); + + let matches = inventory.search("short bastard sword", None); + assert_eq!( + matches[0].item.wowhead_url(), + "https://www.wowhead.com/forever/item=3192?bonus=12741" + ); + let matches = inventory.search("rustic belt", None); + assert_eq!( + matches[0].item.wowhead_url(), + "https://www.wowhead.com/forever/item=2172" + ); + } } diff --git a/crates/foreversight/src/model.rs b/crates/foreversight/src/model.rs index 3daf802..1886d8c 100644 --- a/crates/foreversight/src/model.rs +++ b/crates/foreversight/src/model.rs @@ -96,6 +96,9 @@ impl ProfessionKind { #[derive(Debug, Clone, PartialEq)] pub struct ItemStack { pub item_id: u32, + /// The bonus IDs that pick the item's version, like the "of Fire + /// Protection" in "Short Bastard Sword of Fire Protection". + pub bonus_ids: Vec, pub name: String, pub quality: Quality, pub count: u32, @@ -265,12 +268,13 @@ fn item_stack(value: &Value, location: Location) -> Option { let table = value.as_table()?; // Older versions of the addon recorded Forever's missing ammo slot as item 0. let item_id = table.number("id").filter(|&id| id != 0.)? as u32; - let name = table - .str("link") + let link = table.str("link"); + let name = link .and_then(name_from_link) .map_or_else(|| format!("Item {item_id}"), str::to_string); Some(ItemStack { item_id, + bonus_ids: link.map(bonus_ids_from_link).unwrap_or_default(), name, quality: table .number("quality") @@ -289,6 +293,30 @@ fn name_from_link(link: &str) -> Option<&str> { Some(&link[start..end]) } +/// Gets an item's bonus IDs from its link, like +/// `|cnIQ2:|Hitem:3192::::::::6:1486:::1:12741::::::|h[…]|h|r`. +/// +/// After the item ID, an item string has eleven more fields, like enchantments +/// and gems, then the number of bonus IDs, then the bonus IDs. +fn bonus_ids_from_link(link: &str) -> Vec { + let Some(start) = link.find("item:") else { + return Vec::new(); + }; + let item_string = link[start + "item:".len()..] + .split('|') + .next() + .unwrap_or_default(); + let mut fields = item_string.split(':').skip(12); + let count = fields + .next() + .and_then(|count| count.parse().ok()) + .unwrap_or(0); + fields + .take(count) + .filter_map(|id| id.parse().ok()) + .collect() +} + #[cfg(test)] mod tests { use super::*; @@ -418,4 +446,22 @@ mod tests { ); assert_eq!(name_from_link("not a link"), None); } + + #[test] + fn gets_bonus_ids_from_links() { + assert_eq!( + bonus_ids_from_link( + "|cnIQ2:|Hitem:3192::::::::6:1486:::1:12741::::::|h[Short Bastard Sword of Fire Protection]|h|r" + ), + [12741] + ); + assert_eq!( + bonus_ids_from_link("|cnIQ1:|Hitem:2172::::::::6:1486::11:::::::|h[Rustic Belt]|h|r"), + [] as [u32; 0] + ); + assert_eq!( + bonus_ids_from_link("|cffffffff|Hitem:6948|h[Hearthstone]|h|r"), + [] as [u32; 0] + ); + } }