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#[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}