genius_invokation/cards/deck_url/
mod.rs

1use std::{fmt, iter};
2use crate::*;
3
4const URL_STARTER: &'static str = "genshin.hotgames.gg/tcg/deck-builder?deck=";
5
6#[derive(Debug)]
7#[cfg_attr(docsrs, doc(cfg(feature = "deck-url")))]
8pub enum UrlDeckError<'s> {
9    InvalidUrl(&'s str),
10    UnknownVersion(&'s str),
11    UnknownCard(&'s str),
12}
13
14/// Creates a `Card` iterator from a [deck builder url] (if valid)
15/// 
16/// ## Example
17/// 
18/// ```
19/// use genius_invokation::{Deck, CharacterCard::*, deck_from_url};
20/// 
21/// let url = "https://genshin.hotgames.gg/tcg/deck-builder?deck=1.6.MC.MD.MF.MG.MH.MI.MJ.MK.ML.MM.MN.MO.MP.MV.MY.e.g8.gB.gD.gF.gb.ge.gh.gk.gt.gv.gx.gz.wj.wl.wm&ver=1&lang=en&author=DefaultDeck";
22/// let iterator = deck_from_url(url).unwrap();
23/// let deck = Deck::from_iter(iterator).unwrap();
24/// 
25/// assert_eq!(deck.characters(), &[Kaeya, Sucrose, Diluc]);
26/// ```
27/// 
28/// [deck builder url]: https://genshin.hotgames.gg/tcg/deck-builder
29#[cfg_attr(docsrs, doc(cfg(feature = "deck-url")))]
30pub fn deck_from_url<'s>(url: &'s str) -> Result<impl Iterator<Item=Card> + 's, UrlDeckError<'s>> {
31    let index = url.find(URL_STARTER).ok_or_else(|| UrlDeckError::InvalidUrl(url))?;
32    let start = index + URL_STARTER.len();
33
34    let mut split = url[start..].split('&');
35    let deck  = split.next().ok_or_else(|| UrlDeckError::InvalidUrl(url))?;
36    let version = get_version(split.next()).ok_or_else(|| UrlDeckError::InvalidUrl(url))?;
37
38    if version == "1" {
39        decode(deck)
40    } else {
41        Err(UrlDeckError::UnknownVersion(version))
42    }
43}
44
45fn decode<'s>(deck: &'s str) -> Result<impl Iterator<Item=Card> + 's, UrlDeckError<'s>> {
46    verify_iterator(deck.split('.').map(|card| decode_card(card)))?;
47
48    Ok(deck.split('.').map(|card| decode_card(card).unwrap()).flatten())
49}
50
51fn decode_card<'s>(card: &'s str) -> Result<impl Iterator<Item=Card> + fmt::Debug + 's, &'s str> {
52    let (card, raw_amount) = card.split_once('-').unwrap_or((card, "1"));
53    let amount = str::parse::<usize>(raw_amount).map_err(|_| card)?;
54
55    let card = match card.len() {
56        1 => decode_character_card(card),
57        2 => decode_action_card(card),
58        _ => Err(card)
59    }?;
60
61    Ok(iter::repeat(card).take(amount))
62}
63
64fn get_version<'s>(version: Option<&'s str>) -> Option<&'s str> {
65    version.map(|str| str.strip_prefix("ver=")).flatten()
66}
67
68fn verify_iterator<'s, I>(
69    mut iterator: impl Iterator<Item=Result<I, &'s str>> + 's
70) -> Result<(), UrlDeckError<'s>>
71    where I: Iterator<Item=Card> + fmt::Debug
72{
73    if let Some(error) = iterator.find(|item| item.is_err()) {
74        Err(UrlDeckError::UnknownCard(error.unwrap_err()))
75    } else {
76        Ok(())
77    }
78}
79
80fn decode_character_card<'s>(card: &'s str) -> Result<Card, &'s str> {
81    match card {
82        "0" => Ok(Card::Character(CharacterCard::Ganyu)),
83        "1" => Ok(Card::Character(CharacterCard::Kaeya)),
84        "2" => Ok(Card::Character(CharacterCard::Chongyun)),
85        "3" => Ok(Card::Character(CharacterCard::KamisatoAyaka)),
86        "4" => Ok(Card::Character(CharacterCard::Xingqiu)),
87        "5" => Ok(Card::Character(CharacterCard::Mona)),
88        "6" => Ok(Card::Character(CharacterCard::Diluc)),
89        "7" => Ok(Card::Character(CharacterCard::Xiangling)),
90        "8" => Ok(Card::Character(CharacterCard::Bennett)),
91        "a" => Ok(Card::Character(CharacterCard::Yoimiya)),
92        "b" => Ok(Card::Character(CharacterCard::Fischl)),
93        "c" => Ok(Card::Character(CharacterCard::Razor)),
94        "d" => Ok(Card::Character(CharacterCard::Keqing)),
95        "e" => Ok(Card::Character(CharacterCard::Sucrose)),
96        "f" => Ok(Card::Character(CharacterCard::Jean)),
97        "g" => Ok(Card::Character(CharacterCard::Ningguang)),
98        "h" => Ok(Card::Character(CharacterCard::Noelle)),
99        "i" => Ok(Card::Character(CharacterCard::Collei)),
100        "j" => Ok(Card::Character(CharacterCard::RhodeiaOfLoch)),
101        "k" => Ok(Card::Character(CharacterCard::FatuiPyroAgent)),
102        "l" => Ok(Card::Character(CharacterCard::MaguuKenki)),
103        "m" => Ok(Card::Character(CharacterCard::StonehideLawachurl)),
104        "n" => Ok(Card::Character(CharacterCard::Diona)),
105        "o" => Ok(Card::Character(CharacterCard::Cyno)),
106        "p" => Ok(Card::Character(CharacterCard::Barbara)),
107        "q" => Ok(Card::Character(CharacterCard::MirrorMaiden)),
108        "r" => Ok(Card::Character(CharacterCard::JadeplumeTerrorshroom)),
109        _ => Err(card)
110    }
111}
112
113fn decode_action_card<'s>(card: &'s str) -> Result<Card, &'s str> {
114    if let Some(decoded) = decode_equip_card(card) { return Ok(Card::Action(decoded)) }
115    if let Some(decoded) = decode_event_card(card) { return Ok(Card::Action(decoded)) }
116    if let Some(decoded) = decode_support_card(card) { return Ok(Card::Action(decoded)) }
117
118    Err(card)
119}
120
121fn decode_equip_card(card: &str) -> Option<ActionCard> {
122    match card {
123        "84" => Some(EquipmentCard::Talent(TalentCard::UndividedHeart)),
124        "85" => Some(EquipmentCard::Talent(TalentCard::ColdBloodedStrike)),
125        "86" => Some(EquipmentCard::Talent(TalentCard::SteadyBreathing)),
126        "87" => Some(EquipmentCard::Talent(TalentCard::KantenSenmyouBlessing)),
127        "88" => Some(EquipmentCard::Talent(TalentCard::TheScentRemained)),
128        "89" => Some(EquipmentCard::Talent(TalentCard::ProphecyOfSubmersion)),
129        "8a" => Some(EquipmentCard::Talent(TalentCard::FlowingFlame)),
130        "8b" => Some(EquipmentCard::Talent(TalentCard::Crossfire)),
131        "8c" => Some(EquipmentCard::Talent(TalentCard::GrandExpectation)),
132        "8e" => Some(EquipmentCard::Talent(TalentCard::NaganoharaMeteorSwarm)),
133        "8f" => Some(EquipmentCard::Talent(TalentCard::StellarPredator)),
134        "8g" => Some(EquipmentCard::Talent(TalentCard::Awakening)),
135        "8h" => Some(EquipmentCard::Talent(TalentCard::ThunderingPenance)),
136        "8i" => Some(EquipmentCard::Talent(TalentCard::ChaoticEntropy)),
137        "8j" => Some(EquipmentCard::Talent(TalentCard::LandsOfDandelion)),
138        "8k" => Some(EquipmentCard::Talent(TalentCard::StrategicReserve)),
139        "8l" => Some(EquipmentCard::Talent(TalentCard::IGotYourBack)),
140        "8m" => Some(EquipmentCard::Talent(TalentCard::FloralSidewinder)),
141        "8n" => Some(EquipmentCard::Talent(TalentCard::StreamingSurge)),
142        "8o" => Some(EquipmentCard::Talent(TalentCard::PaidInFull)),
143        "8p" => Some(EquipmentCard::Talent(TalentCard::TranscendentAutomaton)),
144        "8q" => Some(EquipmentCard::Talent(TalentCard::StonehideReforged)),
145        "8r" => Some(EquipmentCard::Talent(TalentCard::ShakenNotPurred)),
146        "8s" => Some(EquipmentCard::Talent(TalentCard::FeatherfallJudgment)),
147        "8t" => Some(EquipmentCard::Talent(TalentCard::GloriousSeason)),
148        "8u" => Some(EquipmentCard::Talent(TalentCard::MirrorCage)),
149        "8v" => Some(EquipmentCard::Talent(TalentCard::ProliferatingSpores)),
150        "g8" => Some(EquipmentCard::Weapon(WeaponCard::MagicGuide)),
151        "g9" => Some(EquipmentCard::Weapon(WeaponCard::SacrificialFragments)),
152        "ga" => Some(EquipmentCard::Weapon(WeaponCard::SkywardAtlas)),
153        "gb" => Some(EquipmentCard::Weapon(WeaponCard::RavenBow)),
154        "gc" => Some(EquipmentCard::Weapon(WeaponCard::SacrificialBow)),
155        "gd" => Some(EquipmentCard::Weapon(WeaponCard::SkywardHarp)),
156        "ge" => Some(EquipmentCard::Weapon(WeaponCard::WhiteIronGreatsword)),
157        "gf" => Some(EquipmentCard::Weapon(WeaponCard::SacrificialGreatsword)),
158        "gg" => Some(EquipmentCard::Weapon(WeaponCard::WolfsGravestone)),
159        "gh" => Some(EquipmentCard::Weapon(WeaponCard::WhiteTassel)),
160        "gi" => Some(EquipmentCard::Weapon(WeaponCard::LithicSpear)),
161        "gj" => Some(EquipmentCard::Weapon(WeaponCard::SkywardSpine)),
162        "gk" => Some(EquipmentCard::Weapon(WeaponCard::TravelersHandySword)),
163        "gl" => Some(EquipmentCard::Weapon(WeaponCard::SacrificialSword)),
164        "gm" => Some(EquipmentCard::Weapon(WeaponCard::AquilaFavonia)),
165        "gn" => Some(EquipmentCard::Artifact(ArtifactCard::AdventurersBandana)),
166        "go" => Some(EquipmentCard::Artifact(ArtifactCard::LuckyDogsSilverCirclet)),
167        "gp" => Some(EquipmentCard::Artifact(ArtifactCard::TravelingDoctorsHandkerchief)),
168        "gq" => Some(EquipmentCard::Artifact(ArtifactCard::GamblersEarrings)),
169        "gr" => Some(EquipmentCard::Artifact(ArtifactCard::InstructorsCap)),
170        "gs" => Some(EquipmentCard::Artifact(ArtifactCard::ExilesCirclet)),
171        "gt" => Some(EquipmentCard::Artifact(ArtifactCard::BrokenRimesEcho)),
172        "gu" => Some(EquipmentCard::Artifact(ArtifactCard::BlizzardStrayer)),
173        "gv" => Some(EquipmentCard::Artifact(ArtifactCard::WineStainedTricorne)),
174        "gw" => Some(EquipmentCard::Artifact(ArtifactCard::HeartOfDepth)),
175        "gx" => Some(EquipmentCard::Artifact(ArtifactCard::WitchsScorchingHat)),
176        "gy" => Some(EquipmentCard::Artifact(ArtifactCard::CrimsonWitchOfFlames)),
177        "gz" => Some(EquipmentCard::Artifact(ArtifactCard::ThunderSummonersCrown)),
178        "gA" => Some(EquipmentCard::Artifact(ArtifactCard::ThunderingFury)),
179        "gB" => Some(EquipmentCard::Artifact(ArtifactCard::ViridescentVenerersDiadem)),
180        "gC" => Some(EquipmentCard::Artifact(ArtifactCard::ViridescentVenerer)),
181        "gD" => Some(EquipmentCard::Artifact(ArtifactCard::MaskOfSolitudeBasalt)),
182        "gE" => Some(EquipmentCard::Artifact(ArtifactCard::ArchaicPetra)),
183        "gF" => Some(EquipmentCard::Artifact(ArtifactCard::LaurelCoronet)),
184        "gG" => Some(EquipmentCard::Artifact(ArtifactCard::DeepwoodMemories)),
185        _ => None
186    }.map(|card| ActionCard::Equipment(card))
187}
188
189fn decode_event_card(card: &str) -> Option<ActionCard> {
190    match card {
191        "Mo" => Some(EventCard::Resonance(ElementalResonanceCard::WovenIce)),
192        "Mp" => Some(EventCard::Resonance(ElementalResonanceCard::ShatteringIce)),
193        "Mq" => Some(EventCard::Resonance(ElementalResonanceCard::WovenWaters)),
194        "Mr" => Some(EventCard::Resonance(ElementalResonanceCard::SoothingWater)),
195        "Ms" => Some(EventCard::Resonance(ElementalResonanceCard::WovenFlames)),
196        "Mt" => Some(EventCard::Resonance(ElementalResonanceCard::FerventFlames)),
197        "Mu" => Some(EventCard::Resonance(ElementalResonanceCard::WovenThunder)),
198        "Mv" => Some(EventCard::Resonance(ElementalResonanceCard::HighVoltage)),
199        "Mw" => Some(EventCard::Resonance(ElementalResonanceCard::WovenWinds)),
200        "Mx" => Some(EventCard::Resonance(ElementalResonanceCard::ImpetuousWinds)),
201        "My" => Some(EventCard::Resonance(ElementalResonanceCard::WovenStone)),
202        "Mz" => Some(EventCard::Resonance(ElementalResonanceCard::EnduringRock)),
203        "MA" => Some(EventCard::Resonance(ElementalResonanceCard::WovenWeeds)),
204        "MB" => Some(EventCard::Resonance(ElementalResonanceCard::SprawlingGreenery)),
205        "MC" => Some(EventCard::Normal(NormalEventCard::TheBestestTravelCompanion)),
206        "MD" => Some(EventCard::Normal(NormalEventCard::ChangingShifts)),
207        "ME" => Some(EventCard::Normal(NormalEventCard::TossUp)),
208        "MF" => Some(EventCard::Normal(NormalEventCard::Strategize)),
209        "MG" => Some(EventCard::Normal(NormalEventCard::IHaventLostYet)),
210        "MH" => Some(EventCard::Normal(NormalEventCard::LeaveItToMe)),
211        "MI" => Some(EventCard::Normal(NormalEventCard::WhenTheCraneReturned)),
212        "MJ" => Some(EventCard::Normal(NormalEventCard::Starsigns)),
213        "MK" => Some(EventCard::Normal(NormalEventCard::CalxsArts)),
214        "ML" => Some(EventCard::Normal(NormalEventCard::MasterOfWeaponry)),
215        "MM" => Some(EventCard::Normal(NormalEventCard::BlessingOfTheDivineRelicsInstallation)),
216        "MN" => Some(EventCard::Normal(NormalEventCard::QuickKnit)),
217        "MO" => Some(EventCard::Normal(NormalEventCard::SendOff)),
218        "MP" => Some(EventCard::Normal(NormalEventCard::GuardiansOath)),
219        "MQ" => Some(EventCard::Normal(NormalEventCard::AbyssalSummons)),
220        "MR" => Some(EventCard::Food(FoodCard::JueyunGuoba)),
221        "MS" => Some(EventCard::Food(FoodCard::AdeptusTemptation)),
222        "MT" => Some(EventCard::Food(FoodCard::LotusFlowerCrisp)),
223        "MU" => Some(EventCard::Food(FoodCard::NorthernSmokedChicken)),
224        "MV" => Some(EventCard::Food(FoodCard::SweetMadame)),
225        "MW" => Some(EventCard::Food(FoodCard::MondstadtHashBrown)),
226        "MX" => Some(EventCard::Food(FoodCard::MushroomPizza)),
227        "MY" => Some(EventCard::Food(FoodCard::MintyMeatRolls)),
228        _ => None
229    }.map(|card| ActionCard::Event(card))
230}
231
232fn decode_support_card(card: &str) -> Option<ActionCard> {
233    match card {
234        "wg" => Some(SupportCard::Location(LocationCard::LiyueHarborWharf)),
235        "wh" => Some(SupportCard::Location(LocationCard::KnightsOfFavoniusLibrary)),
236        "wi" => Some(SupportCard::Location(LocationCard::JadeChamber)),
237        "wj" => Some(SupportCard::Location(LocationCard::DawnWinery)),
238        "wk" => Some(SupportCard::Location(LocationCard::WangshuInn)),
239        "wl" => Some(SupportCard::Location(LocationCard::FavoniusCathedral)),
240        "wm" => Some(SupportCard::Companion(CompanionCard::Paimon)),
241        "wn" => Some(SupportCard::Companion(CompanionCard::Katheryne)),
242        "wo" => Some(SupportCard::Companion(CompanionCard::Timaeus)),
243        "wp" => Some(SupportCard::Companion(CompanionCard::Wagner)),
244        "wq" => Some(SupportCard::Companion(CompanionCard::ChefMao)),
245        "wr" => Some(SupportCard::Companion(CompanionCard::Tubby)),
246        "ws" => Some(SupportCard::Companion(CompanionCard::Timmie)),
247        "wt" => Some(SupportCard::Companion(CompanionCard::Liben)),
248        "wu" => Some(SupportCard::Companion(CompanionCard::ChangTheNinth)),
249        "wv" => Some(SupportCard::Companion(CompanionCard::Ellin)),
250        "ww" => Some(SupportCard::Companion(CompanionCard::IronTongueTian)),
251        "wx" => Some(SupportCard::Companion(CompanionCard::LiuSu)),
252        "wy" => Some(SupportCard::Item(ItemCard::ParametricTransformer)),
253        "wz" => Some(SupportCard::Item(ItemCard::NRE)),
254        _ => None
255    }.map(|card| ActionCard::Support(card))
256}
257
258#[cfg(test)]
259mod tests {
260    #[test]
261    fn default_deck() {
262        let url = "https://genshin.hotgames.gg/tcg/deck-builder?deck=1.6.MC.MD.MF.MG.MH.MI.MJ.MK.ML.MM.MN.MO.MP.MV.MY.e.g8.gB.gD.gF.gb.ge.gh.gk.gt.gv.gx.gz.wj.wl.wm&ver=1&lang=en&author=DefaultDeck";
263
264        assert!(super::deck_from_url(url).is_ok());
265    }
266}