Skip to main content

sf_api/gamestate/
mod.rs

1pub mod arena;
2pub mod character;
3pub mod dungeons;
4pub mod fortress;
5pub mod guild;
6pub mod idle;
7pub mod items;
8pub mod legendary_dungeon;
9pub mod rewards;
10pub mod social;
11pub mod tavern;
12pub mod underworld;
13pub mod unlockables;
14
15use std::{
16    borrow::Borrow,
17    collections::{HashMap, HashSet},
18};
19
20use chrono::{DateTime, Duration, Local, NaiveDateTime};
21use enum_map::EnumMap;
22use log::{error, warn};
23use num_traits::FromPrimitive;
24use strum::{EnumCount, IntoEnumIterator};
25
26use crate::{
27    command::*,
28    error::*,
29    gamestate::{
30        arena::*, character::*, dungeons::*, fortress::*, guild::*, idle::*,
31        items::*, legendary_dungeon::*, rewards::*, social::*, tavern::*,
32        underworld::*, unlockables::*,
33    },
34    misc::*,
35    response::{Response, ResponseVal},
36};
37
38/// Represent the full state of the game at some point in time
39#[derive(Debug, Clone, Default)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41pub struct GameState {
42    /// Everything, that can be considered part of the character, or his
43    /// immediate surrounding and not the rest of the world
44    pub character: Character,
45    /// Information about quests and work
46    pub tavern: Tavern,
47    /// The place to fight other players
48    pub arena: Arena,
49    /// The last fight, that this player was involved in
50    pub last_fight: Option<Fight>,
51    /// Both shops. You can access a specific one either with `get()`,
52    /// `get_mut()`, or `[]` and the `ShopType` as the key.
53    pub shops: EnumMap<ShopType, Shop>,
54    pub shop_item_lvl: u32,
55    /// If the player is in a guild, this will contain information about it
56    pub guild: Option<Guild>,
57    /// Everything, that is time sensitive, like events, calendar, etc.
58    pub specials: TimedSpecials,
59    /// Everything, that can be found under the Dungeon tab
60    pub dungeons: Dungeons,
61    /// Contains information about the underworld, if it has been unlocked
62    pub underworld: Option<Underworld>,
63    /// Contains information about the fortress, if it has been unlocked
64    pub fortress: Option<Fortress>,
65    /// Information the pet collection, that a player can build over time
66    pub pets: Option<Pets>,
67    /// Contains information about the hellevator, if it is currently active
68    pub hellevator: HellevatorEvent,
69    /// Contains information about the legendary dungeons event, if it is
70    /// currently active
71    pub legendary_dungeon: LegendaryDungeonEvent,
72    /// Contains information about the blacksmith, if it has been unlocked
73    pub blacksmith: Option<Blacksmith>,
74    /// Contains information about the witch, if it has been unlocked
75    pub witch: Option<Witch>,
76    /// Tracker for small challenges, that a player can complete
77    pub achievements: Achievements,
78    /// The boring idle game
79    pub idle_game: Option<IdleGame>,
80    /// Contains the features this char is able to unlock right now
81    pub pending_unlocks: Vec<Unlockable>,
82    /// Anything related to hall of fames
83    pub hall_of_fames: HallOfFames,
84    /// Contains both other guilds & players, that you can look at via commands
85    pub lookup: Lookup,
86    /// Anything you can find in the mail tab of the official client
87    pub mail: Mail,
88    /// The language, that the player has set in the client. This will affect
89    /// the language of news messages in the inbox (Mail)
90    pub language: Option<Language>,
91    /// The raw timestamp, that the server has sent us
92    last_request_timestamp: i64,
93    /// The amount of sec, that the server is ahead of us in seconds (can be
94    /// negative)
95    server_time_diff: i64,
96}
97
98const SHOP_N: usize = 6;
99
100/// A shop, that you can buy items from
101#[derive(Debug, Clone)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
103pub struct Shop {
104    pub typ: ShopType,
105    /// The items this shop has for sale
106    pub items: [Item; SHOP_N],
107}
108
109impl Default for Shop {
110    fn default() -> Self {
111        let items = core::array::from_fn(|_| Item {
112            typ: ItemType::Unknown(0),
113            price: u32::MAX,
114            mushroom_price: u32::MAX,
115            model_id: 0,
116            class: None,
117            type_specific_val: 0,
118            attributes: EnumMap::default(),
119            gem_slot: None,
120            rune: None,
121            enchantment: None,
122            color: 0,
123            upgrade_count: 0,
124            item_quality: 0,
125            is_washed: false,
126            full_model_id: 0,
127        });
128
129        Self {
130            items,
131            typ: ShopType::Magic,
132        }
133    }
134}
135
136#[derive(Debug, Default, Clone, PartialEq, Eq, Copy)]
137#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
138pub struct ShopPosition {
139    pub typ: ShopType,
140    pub pos: usize,
141}
142
143impl std::fmt::Display for ShopPosition {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        write!(f, "{}/{}", self.typ as usize, self.pos + 1)
146    }
147}
148
149impl ShopPosition {
150    /// The 0 based index into the backpack vec, where the item is parsed into
151    #[must_use]
152    pub fn shop(&self) -> ShopType {
153        self.typ
154    }
155    /// The inventory type and position within it, where the item is stored
156    /// according to previous inventory management logic. This is what you use
157    /// for commands
158    #[must_use]
159    pub fn position(&self) -> usize {
160        self.pos
161    }
162}
163
164impl Shop {
165    /// Creates an iterator over the inventory slots.
166    pub fn iter(&self) -> impl Iterator<Item = (ShopPosition, &Item)> {
167        self.items
168            .iter()
169            .enumerate()
170            .map(|(pos, item)| (ShopPosition { typ: self.typ, pos }, item))
171    }
172
173    pub(crate) fn parse(
174        data: &[i64],
175        server_time: ServerTime,
176        typ: ShopType,
177    ) -> Result<Shop, SFError> {
178        let mut shop = Shop::default();
179        shop.typ = typ;
180        for (idx, item) in shop.items.iter_mut().enumerate() {
181            let d = data.skip(idx * ITEM_PARSE_LEN, "shop item")?;
182            let Some(p_item) = Item::parse(d, server_time)? else {
183                return Err(SFError::ParsingError(
184                    "shop item",
185                    format!("{d:?}"),
186                ));
187            };
188            *item = p_item;
189        }
190        Ok(shop)
191    }
192}
193
194impl GameState {
195    /// Constructs a new `GameState` from the provided response. The response
196    /// has to be the login response from a `Session`.
197    ///
198    /// # Errors
199    /// If the response contains any errors, or does not contain enough
200    /// information about the player to build a full `GameState`, this will
201    /// return a `ParsingError`, or `TooShortResponse` depending on the
202    /// exact error
203    pub fn new(response: Response) -> Result<Self, SFError> {
204        let mut res = Self::default();
205        res.update(response)?;
206        if res.character.level == 0 || res.character.name.is_empty() {
207            return Err(SFError::ParsingError(
208                "response did not contain full player state",
209                String::new(),
210            ));
211        }
212        Ok(res)
213    }
214
215    /// Updates the players information with the new data received from the
216    /// server. Any error that is encounters terminates the update process
217    ///
218    /// # Errors
219    /// Mainly returns `ParsingError` if the response does not exactly follow
220    /// the expected length, type and layout
221    pub fn update<R: Borrow<Response>>(
222        &mut self,
223        response: R,
224    ) -> Result<(), SFError> {
225        let response = response.borrow();
226        let new_vals = response.values();
227        // Because the conversion of all other timestamps relies on the servers
228        // timestamp, this has to be set first
229        if let Some(ts) = new_vals.get("timestamp").copied() {
230            let ts = ts.into("server time stamp")?;
231            let server_time = DateTime::from_timestamp(ts, 0).ok_or(
232                SFError::ParsingError("server time stamp", ts.to_string()),
233            )?;
234            self.server_time_diff = (server_time.naive_utc()
235                - response.received_at())
236            .num_seconds();
237            self.last_request_timestamp = ts;
238        }
239        let server_time = self.server_time();
240
241        self.last_fight = None;
242        self.mail.open_claimable = None;
243
244        let mut other_player: Option<OtherPlayer> = None;
245        let mut other_guild: Option<OtherGuild> = None;
246
247        let mut errors = vec![];
248        for (key, val) in new_vals.iter().map(|(a, b)| (*a, *b)) {
249            let res = self.apply_update_key(
250                key,
251                val,
252                &mut other_player,
253                &mut other_guild,
254                server_time,
255                new_vals,
256            );
257            if let Err(err) = res {
258                errors.push(err);
259            }
260        }
261
262        if let Some(og) = other_guild {
263            self.lookup.guilds.insert(og.name.clone(), og);
264        }
265        if let Some(other_player) = other_player {
266            self.lookup.insert_lookup(other_player);
267        }
268
269        // Dungeon portal is unlocked with level 99
270        if self.dungeons.portal.is_some() && self.character.level < 99 {
271            self.dungeons.portal = None;
272        }
273
274        if let Some(pets) = &self.pets
275            && pets.rank == 0
276        {
277            self.pets = None;
278        }
279        if let Some(t) = &self.guild
280            && t.name.is_empty()
281        {
282            self.guild = None;
283        }
284        if self.fortress.is_some() && self.character.level < 25 {
285            self.fortress = None;
286        }
287        if let Some(fortress) = &mut self.fortress {
288            for (typ, unit) in &mut fortress.units {
289                let building_lvl =
290                    fortress.buildings.get(typ.training_building()).level;
291                let limit_modifier = match typ {
292                    FortressUnitType::Magician => 1,
293                    FortressUnitType::Archer => 2,
294                    FortressUnitType::Soldier => 3,
295                };
296                unit.limit = building_lvl * limit_modifier;
297            }
298        }
299
300        if let Some(t) = &self.underworld
301            && t.buildings[UnderworldBuildingType::HeartOfDarkness].level < 1
302        {
303            self.underworld = None;
304        }
305
306        // Witch is automatically unlocked with level 66
307        if self.witch.is_some() && self.character.level < 66 {
308            self.witch = None;
309        }
310
311        match errors.len() {
312            0 => Ok(()),
313            1 => Err(errors.remove(0)),
314            _ => Err(SFError::NestedError(errors)),
315        }
316    }
317
318    pub(crate) fn updatete_relation_list(&mut self, val: &str) {
319        self.character.relations.clear();
320        for entry in val
321            .trim_end_matches(';')
322            .split(';')
323            .filter(|a| !a.is_empty())
324        {
325            let mut parts = entry.split(',');
326            let (
327                Some(id),
328                Some(name),
329                Some(guild),
330                Some(level),
331                Some(relation),
332            ) = (
333                parts.next().and_then(|a| a.parse().ok()),
334                parts.next().map(std::string::ToString::to_string),
335                parts.next().map(std::string::ToString::to_string),
336                parts.next().and_then(|a| a.parse().ok()),
337                parts.next().and_then(|a| match a {
338                    "-1" => Some(Relationship::Ignored),
339                    "1" => Some(Relationship::Friend),
340                    _ => None,
341                }),
342            )
343            else {
344                warn!("bad friendslist entry: {entry}");
345                continue;
346            };
347            self.character.relations.push(RelationEntry {
348                id,
349                name,
350                guild,
351                level,
352                relation,
353            });
354        }
355    }
356
357    pub(crate) fn update_gttime(
358        &mut self,
359        data: &[i64],
360        server_time: ServerTime,
361    ) -> Result<(), SFError> {
362        let d = &mut self.hellevator;
363        d.start = data.cstget(0, "event start", server_time)?;
364        d.end = data.cstget(1, "event end", server_time)?;
365        d.collect_time_end = data.cstget(3, "claim time end", server_time)?;
366        Ok(())
367    }
368
369    pub(crate) fn update_resources(
370        &mut self,
371        res: &[i64],
372    ) -> Result<(), SFError> {
373        self.character.mushrooms = res.csiget(1, "mushrooms", 0)?;
374        self.character.silver = res.csiget(2, "player silver", 0)?;
375        self.tavern.quicksand_glasses =
376            res.csiget(4, "quicksand glass count", 0)?;
377
378        self.specials.wheel.lucky_coins = res.csiget(3, "lucky coins", 0)?;
379        let bs = self.blacksmith.get_or_insert_with(Default::default);
380        bs.metal = res.csiget(9, "bs metal", 0)?;
381        bs.arcane = res.csiget(10, "bs arcane", 0)?;
382        let fortress = self.fortress.get_or_insert_with(Default::default);
383        fortress
384            .resources
385            .get_mut(FortressResourceType::Wood)
386            .current = res.csiget(5, "saved wood ", 0)?;
387        fortress
388            .resources
389            .get_mut(FortressResourceType::Stone)
390            .current = res.csiget(7, "saved stone", 0)?;
391
392        let pets = self.pets.get_or_insert_with(Default::default);
393        for (e_pos, element) in HabitatType::iter().enumerate() {
394            pets.habitats.get_mut(element).fruits =
395                res.csiget(12 + e_pos, "fruits", 0)?;
396        }
397
398        self.underworld
399            .get_or_insert_with(Default::default)
400            .souls_current = res.csiget(11, "uu souls saved", 0)?;
401        Ok(())
402    }
403
404    /// Returns the time of the server. This is just an 8 byte copy behind the
405    /// scenes, so feel free to NOT cache/optimize calling this in any way
406    #[must_use]
407    pub fn server_time(&self) -> ServerTime {
408        ServerTime(self.server_time_diff)
409    }
410
411    /// Given a header value like "fight4", this would give you the
412    /// corresponding fight[3]. In case that does not exist, it will be created
413    /// w/ the default
414    #[must_use]
415    fn get_fight(&mut self, header_name: &str) -> &mut SingleFight {
416        let id = fight_no_from_header(header_name);
417        let fights =
418            &mut self.last_fight.get_or_insert_with(Default::default).fights;
419
420        if fights.len() < id {
421            fights.resize(id, SingleFight::default());
422        }
423        #[allow(clippy::unwrap_used)]
424        fights.get_mut(id - 1).unwrap()
425    }
426
427    /// Updates the gamestate with the given key and value
428    #[allow(clippy::match_same_arms)]
429    fn apply_update_key(
430        &mut self,
431        key: &str,
432        val: ResponseVal<'_>,
433        other_player: &mut Option<OtherPlayer>,
434        other_guild: &mut Option<OtherGuild>,
435        server_time: ServerTime,
436        all_values: &HashMap<&str, ResponseVal<'_>>,
437    ) -> Result<(), SFError> {
438        match key {
439            "timestamp" => {
440                // Handled above
441            }
442            "Success" | "sucess" => {
443                // Whatever we did worked. Note that the server also
444                // sends this for bad requests from time to time :)
445            }
446            "login count" | "sessionid" | "cryptokey" | "cryptoid" => {
447                // Should already be handled when receiving the response
448            }
449            "preregister"
450            | "languagecodelist"
451            | "tracking"
452            | "skipvideo"
453            | "webshopid"
454            | "cidstring"
455            | "mountexpired"
456            | "tracking_netto"
457            | "tracking_coins"
458            | "tutorial_game_entry" => {
459                // Stuff that looks irrellevant
460            }
461            "ownplayername" => {
462                self.character.name.set(val.as_str());
463            }
464            "owndescription" => {
465                self.character.description = from_sf_string(val.as_str());
466            }
467            "wagesperhour" => {
468                self.tavern.guard_wage = val.into("tavern wage")?;
469            }
470            "skipallow" => {
471                let raw_skip = val.into::<i32>("skip allow")?;
472                self.tavern.mushroom_skip_allowed = raw_skip != 0;
473            }
474            "cryptoid not found" => return Err(SFError::ConnectionError),
475            "ownplayersave" => {
476                // Goodbye old friend...
477            }
478            "owngroupname" => self
479                .guild
480                .get_or_insert_with(Default::default)
481                .name
482                .set(val.as_str()),
483            "sfhomeid" => {}
484            "backpack" => {
485                let data: Vec<i64> = val.into_list("backpack")?;
486                self.character.inventory.backpack = data
487                    .chunks_exact(ITEM_PARSE_LEN)
488                    .map(|a| Item::parse(a, server_time))
489                    .collect::<Result<Vec<_>, _>>()?;
490            }
491            "itemlevelshop" => {
492                self.shop_item_lvl = val.into("shop lvl")?;
493            }
494            "storeitemsshakes" => {
495                let data: Vec<i64> = val.into_list("weapon store")?;
496                *self.shops.get_mut(ShopType::Weapon) =
497                    Shop::parse(&data, server_time, ShopType::Weapon)?;
498            }
499            "questofferitems" => {
500                for (chunk, quest) in val
501                    .into_list("quest items")?
502                    .chunks_exact(19)
503                    .zip(&mut self.tavern.quests)
504                {
505                    quest.item = Item::parse(chunk, server_time)?;
506                }
507            }
508            #[allow(
509                clippy::indexing_slicing,
510                clippy::cast_sign_loss,
511                clippy::cast_possible_truncation
512            )]
513            #[allow(deprecated)]
514            "toiletstate" => {
515                let vals: Vec<i64> = val.into_list("toilet state")?;
516                if vals.len() < 3 {
517                    return Ok(());
518                }
519                let toilet = self.tavern.toilet.get_or_insert_default();
520                toilet.sacrifices_left = vals[2] as u32;
521            }
522            "companionequipment" => {
523                let data: Vec<i64> = val.into_list("quest items")?;
524                if data.is_empty() {
525                    return Ok(());
526                }
527                for (idx, cmp) in self
528                    .dungeons
529                    .companions
530                    .get_or_insert_with(Default::default)
531                    .values_mut()
532                    .enumerate()
533                {
534                    let data = data.skip(
535                        (19 * EquipmentSlot::COUNT) * idx,
536                        "companion item",
537                    )?;
538                    cmp.equipment = Equipment::parse(data, server_time)?;
539                }
540            }
541            "storeitemsfidget" => {
542                let data: Vec<i64> = val.into_list("magic store")?;
543                *self.shops.get_mut(ShopType::Magic) =
544                    Shop::parse(&data, server_time, ShopType::Magic)?;
545            }
546            "ownplayersaveequipment" => {
547                let data: Vec<i64> = val.into_list("player equipment")?;
548                self.character.equipment =
549                    Equipment::parse(&data, server_time)?;
550            }
551            "systemmessagelist" => {}
552            "newslist" => {
553                self.mail.news_inbox.clear();
554                let data = val.as_str();
555                for msg in data.split(';').filter(|a| !a.trim().is_empty()) {
556                    match NewsEntry::parse(msg, server_time) {
557                        Ok(msg) => self.mail.news_inbox.push(msg),
558                        Err(e) => warn!("Invalid msg: {msg} {e}"),
559                    }
560                }
561            }
562            "dummieequipment" => {
563                let m: Vec<i64> = val.into_list("mannequin")?;
564                self.character.mannequin =
565                    Some(Equipment::parse(&m, server_time)?);
566            }
567            "owntower" => {
568                let data = val.into_list("tower")?;
569                let companions = self
570                    .dungeons
571                    .companions
572                    .get_or_insert_with(Default::default);
573
574                for (i, class) in CompanionClass::iter().enumerate() {
575                    let comp_start = 3 + i * 148;
576                    companions.get_mut(class).level =
577                        data.cget(comp_start, "comp level")?;
578                    update_enum_map(
579                        &mut companions.get_mut(class).attributes,
580                        data.skip(comp_start + 4, "comp attrs")?,
581                    );
582                }
583                // Why would they include this in the tower response???
584                self.underworld
585                    .get_or_insert_with(Default::default)
586                    .update(&data, server_time)?;
587            }
588            "owngrouprank" => {
589                self.guild.get_or_insert_with(Default::default).rank =
590                    val.into("group rank")?;
591            }
592            "owngroupattack" | "owngroupdefense" => {
593                // Annoying
594            }
595            "owngrouprequirement" | "othergrouprequirement" => {
596                // TODO:
597            }
598            "owngroupsave" => {
599                self.guild
600                    .get_or_insert_with(Default::default)
601                    .update_group_save(val.as_str(), server_time)?;
602            }
603            "owngroupmember" => self
604                .guild
605                .get_or_insert_with(Default::default)
606                .update_member_names(val.as_str()),
607            "owngrouppotion" => {
608                self.guild
609                    .get_or_insert_with(Default::default)
610                    .update_member_potions(val.as_str());
611            }
612            "unitprice" => {
613                self.fortress
614                    .get_or_insert_with(Default::default)
615                    .update_unit_prices(&val.into_list("fortress units")?)?;
616            }
617            "dicestatus" => {
618                let dices: Option<Vec<DiceType>> = val
619                    .into_list("dice status")?
620                    .into_iter()
621                    .map(FromPrimitive::from_u8)
622                    .collect();
623                self.tavern.dice_game.current_dice = dices.unwrap_or_default();
624            }
625            "dicereward" => {
626                let data: Vec<u32> = val.into_list("dice reward")?;
627                let win_typ: DiceType =
628                    data.cfpuget(0, "dice reward", |a| a - 1)?;
629                self.tavern.dice_game.reward = Some(DiceReward {
630                    win_typ,
631                    amount: data.cget(1, "dice reward amount")?,
632                });
633            }
634            "chathistory" => {
635                self.guild.get_or_insert_with(Default::default).chat =
636                    ChatMessage::parse_messages(val.as_str());
637            }
638            "chatwhisper" => {
639                self.guild.get_or_insert_with(Default::default).whispers =
640                    ChatMessage::parse_messages(val.as_str());
641            }
642            "upgradeprice" => {
643                self.fortress
644                    .get_or_insert_with(Default::default)
645                    .update_unit_upgrade_info(
646                        &val.into_list("fortress unit upgrade prices")?,
647                    )?;
648            }
649            "unitlevel" => {
650                self.fortress
651                    .get_or_insert_with(Default::default)
652                    .update_levels(&val.into_list("fortress unit levels")?)?;
653            }
654            "fortressprice" => {
655                self.fortress
656                    .get_or_insert_with(Default::default)
657                    .update_prices(
658                        &val.into_list("fortress upgrade prices")?,
659                    )?;
660            }
661            "Arenarank" => {
662                if let Some(uw) = self.underworld.as_mut() {
663                    uw.lure_suggestion =
664                        val.as_str().parse::<u32>().ok().map(LureSuggestion);
665                }
666            }
667            "witch" => {
668                // old witch data without price
669            }
670            "witchshop" => {
671                self.witch
672                    .get_or_insert_with(Default::default)
673                    .update(&val.into_list("witch")?)?;
674            }
675            "underworldupgradeprice" => {
676                self.underworld
677                    .get_or_insert_with(Default::default)
678                    .update_underworld_unit_prices(
679                        &val.into_list("underworld upgrade prices")?,
680                    )?;
681            }
682            "unlockfeature" => {
683                self.pending_unlocks =
684                    Unlockable::parse(&val.into_list("unlock")?)?;
685            }
686            "dungeonprogresslight" => self.dungeons.update_progress(
687                &val.into_list("dungeon progress light")?,
688                DungeonType::Light,
689            ),
690            "dungeonprogressshadow" => self.dungeons.update_progress(
691                &val.into_list("dungeon progress shadow")?,
692                DungeonType::Shadow,
693            ),
694            "portalprogress" => {
695                self.dungeons
696                    .portal
697                    .get_or_insert_with(Default::default)
698                    .update(&val.into_list("portal progress")?, server_time)?;
699            }
700            "owntowerlevel" => {
701                // Already in dungeons
702            }
703            "serverversion" => {
704                // Handled in session
705            }
706            "stoneperhournextlevel" => {
707                self.fortress
708                    .get_or_insert_with(Default::default)
709                    .resources
710                    .get_mut(FortressResourceType::Stone)
711                    .production
712                    .per_hour_next_lvl = val.into("stone next lvl")?;
713            }
714            "woodperhournextlevel" => {
715                self.fortress
716                    .get_or_insert_with(Default::default)
717                    .resources
718                    .get_mut(FortressResourceType::Wood)
719                    .production
720                    .per_hour_next_lvl = val.into("wood next lvl")?;
721            }
722            "shadowlevel" | "dungeonlevel" => {
723                // We just look at the db
724            }
725            "gttime" => {
726                self.update_gttime(&val.into_list("gttime")?, server_time)?;
727            }
728            "gtsave" => {
729                self.hellevator
730                    .active
731                    .get_or_insert_with(Default::default)
732                    .update(&val.into_list("gtsave")?, server_time)?;
733            }
734            "maxrank" => {
735                self.hall_of_fames.players_total = val.into("player count")?;
736            }
737            "achievement" => {
738                self.achievements.update(&val.into_list("achievements")?)?;
739            }
740            "groupskillprice" => {
741                self.guild
742                    .get_or_insert_with(Default::default)
743                    .update_group_prices(
744                        &val.into_list("guild skill prices")?,
745                    )?;
746            }
747            "soldieradvice" => {
748                let advice: u16 = val.into("soldier advice")?;
749                if advice > 0 {
750                    other_player
751                        .get_or_insert_default()
752                        .fortress
753                        .get_or_insert_default()
754                        .soldier_advice = advice;
755                }
756                // Replaced
757            }
758            "owngroupdescription" => self
759                .guild
760                .get_or_insert_with(Default::default)
761                .update_description_embed(val.as_str()),
762            "idle" => {
763                self.idle_game = IdleGame::parse_idle_game(
764                    &val.into_list("idle game")?,
765                    server_time,
766                );
767            }
768            "resources" => {
769                self.update_resources(&val.into_list("resources")?)?;
770            }
771            "chattime" => {
772                // let _chat_time = server_time
773                //     .convert_to_local(val.into("chat time")?, "chat
774                // time"); Pretty sure this is the time something last
775                // happened in chat, but nobody cares and messages have a
776                // time
777            }
778            "maxpetlevel" => {
779                self.pets.get_or_insert_with(Default::default).max_pet_level =
780                    val.into("max pet lvl")?;
781            }
782            "otherdescription" => {
783                other_player
784                    .get_or_insert_with(Default::default)
785                    .description = from_sf_string(val.as_str());
786            }
787            "otherplayergroupname" => {
788                let guild =
789                    Some(val.as_str().to_string()).filter(|a| !a.is_empty());
790                other_player.get_or_insert_with(Default::default).guild = guild;
791            }
792            "otherplayername" => {
793                other_player
794                    .get_or_insert_with(Default::default)
795                    .name
796                    .set(val.as_str());
797            }
798            "otherplayersaveequipment" => {
799                let data: Vec<i64> = val.into_list("other player equipment")?;
800                other_player.get_or_insert_with(Default::default).equipment =
801                    Equipment::parse(&data, server_time)?;
802            }
803            "fortresspricereroll" => {
804                self.fortress
805                    .get_or_insert_with(Default::default)
806                    .opponent_reroll_price = val.into("fortress reroll")?;
807            }
808            "fortresswalllevel" => {
809                self.fortress
810                    .get_or_insert_with(Default::default)
811                    .wall_combat_lvl = val.into("fortress wall lvl")?;
812            }
813            "dragongoldbonus" => {
814                self.character.mount_dragon_refund = val.into("dragon gold")?;
815            }
816            "wheelresult" => {
817                // NOTE: These are the reqs to unlock the upgrade, not a
818                // check if it is actually upgraded
819                let upgraded = self.character.level >= 95
820                    && self.pets.is_some()
821                    && self.underworld.is_some();
822                self.specials.wheel.result = Some(WheelReward::parse(
823                    &val.into_list("wheel result")?,
824                    upgraded,
825                )?);
826            }
827            "dailyreward" => {
828                // Dead since last update
829            }
830            "calenderreward" => {
831                // Probably removed and should be irrelevant
832            }
833            "oktoberfest" => {
834                // Not sure if this is still used, but it seems to just be
835                // empty.
836                if !val.as_str().is_empty() {
837                    warn!("oktoberfest response is not empty: {val}");
838                }
839            }
840            "usersettings" => {
841                // Contains language and flag settings
842                let vals = &val.as_str().split('/').collect::<Vec<_>>();
843                self.language = Language::parse(vals.cget(0, "client lang")?);
844
845                let v = match vals.cget(4, "questing setting")? {
846                    "a" => ExpeditionSetting::PreferExpeditions,
847                    "0" | "b" => ExpeditionSetting::PreferQuests,
848                    x => {
849                        error!("Weird expedition settings: {x}");
850                        ExpeditionSetting::PreferQuests
851                    }
852                };
853                self.tavern.questing_preference = v;
854            }
855            "mailinvoice" => {
856                // Incomplete email address
857            }
858            "calenderinfo" => {
859                // This is twice in the original response.
860                // This API sucks LMAO
861                let data: Vec<i64> = val.into_list("calendar")?;
862                self.specials.calendar.rewards.clear();
863                for p in data.chunks_exact(2) {
864                    let reward = CalendarReward::parse(p)?;
865                    self.specials.calendar.rewards.push(reward);
866                }
867            }
868            "othergroupattack" => {
869                other_guild.get_or_insert_with(Default::default).attacks =
870                    Some(val.to_string());
871            }
872            "othergroupdefense" => {
873                other_guild
874                    .get_or_insert_with(Default::default)
875                    .defends_against = Some(val.to_string());
876            }
877            "inboxcapacity" => {
878                self.mail.inbox_capacity = val.into("inbox cap")?;
879            }
880            "magicregistration" => {
881                // Pretty sure this means you have not provided a pw or
882                // mail. Just a name and clicked play
883            }
884            "Ranklistplayer" => {
885                self.hall_of_fames.players.clear();
886                for player in val.as_str().trim_matches(';').split(';') {
887                    // Stop parsing once we receive an empty player
888                    if player.ends_with(",,,0,0,0,") {
889                        break;
890                    }
891
892                    match HallOfFamePlayer::parse(player) {
893                        Ok(x) => {
894                            self.hall_of_fames.players.push(x);
895                        }
896                        Err(err) => warn!("{err}"),
897                    }
898                }
899            }
900            "ranklistgroup" => {
901                self.hall_of_fames.guilds.clear();
902                for guild in val.as_str().trim_matches(';').split(';') {
903                    match HallOfFameGuild::parse(guild) {
904                        Ok(x) => {
905                            self.hall_of_fames.guilds.push(x);
906                        }
907                        Err(err) => warn!("{err}"),
908                    }
909                }
910            }
911            "maxrankgroup" => {
912                self.hall_of_fames.guilds_total = Some(val.into("guild max")?);
913            }
914            "maxrankPets" => {
915                self.hall_of_fames.pets_total = Some(val.into("pet rank max")?);
916            }
917            "RanklistPets" => {
918                self.hall_of_fames.pets.clear();
919                for entry in val.as_str().trim_matches(';').split(';') {
920                    match HallOfFamePets::parse(entry) {
921                        Ok(x) => {
922                            self.hall_of_fames.pets.push(x);
923                        }
924                        Err(err) => warn!("{err}"),
925                    }
926                }
927            }
928            "ranklistfortress" | "Ranklistfortress" => {
929                self.hall_of_fames.fortresses.clear();
930                for guild in val.as_str().trim_matches(';').split(';') {
931                    match HallOfFameFortress::parse(guild) {
932                        Ok(x) => {
933                            self.hall_of_fames.fortresses.push(x);
934                        }
935                        Err(err) => warn!("{err}"),
936                    }
937                }
938            }
939            "ranklistunderworld" => {
940                self.hall_of_fames.underworlds.clear();
941                for entry in val.as_str().trim_matches(';').split(';') {
942                    match HallOfFameUnderworld::parse(entry) {
943                        Ok(x) => {
944                            self.hall_of_fames.underworlds.push(x);
945                        }
946                        Err(err) => warn!("{err}"),
947                    }
948                }
949            }
950            "gamblegoldvalue" => {
951                self.tavern.gamble_result =
952                    Some(GambleResult::SilverChange(val.into("gold gamble")?));
953            }
954            "gamblecoinvalue" => {
955                self.tavern.gamble_result = Some(GambleResult::MushroomChange(
956                    val.into("gold gamble")?,
957                ));
958            }
959            "maxrankFortress" => {
960                self.hall_of_fames.fortresses_total =
961                    Some(val.into("fortress max")?);
962            }
963            "underworldprice" => self
964                .underworld
965                .get_or_insert_with(Default::default)
966                .update_building_prices(&val.into_list("ub prices")?)?,
967            "owngroupknights" => self
968                .guild
969                .get_or_insert_with(Default::default)
970                .update_group_knights(val.as_str()),
971            "friendlist" => self.updatete_relation_list(val.as_str()),
972            "legendaries" => {
973                if val.as_str().chars().any(|a| a != 'A') {
974                    warn!("Found a legendaries value, that is not just AAA..");
975                }
976            }
977            "smith" => {
978                let data: Vec<i64> = val.into_list("smith")?;
979                let bs = self.blacksmith.get_or_insert_with(Default::default);
980
981                bs.dismantle_left = data.csiget(0, "dismantles left", 0)?;
982                bs.last_dismantled = data.cstget(1, "bs time", server_time)?;
983            }
984            "fortressGroupPrice" => {
985                self.fortress
986                    .get_or_insert_with(Default::default)
987                    .hall_of_knights_upgrade_price = FortressCost::parse(
988                    &val.into_list("hall of knights prices")?,
989                )?;
990            }
991            "goldperhournextlevel" => {
992                // I dont think this matters
993            }
994            "underworldmaxsouls" => {
995                // This should already be in resources
996            }
997            "dailytaskrewardpreview" => {
998                let vals: Vec<i64> =
999                    val.into_list("event task reward preview")?;
1000                self.specials.tasks.daily.rewards = parse_rewards(&vals);
1001            }
1002            "expeditionevent" => {
1003                let data: Vec<i64> = val.into_list("exp event")?;
1004                self.tavern.expeditions.start =
1005                    data.cstget(0, "expedition start", server_time)?;
1006                let end = data.cstget(1, "expedition end", server_time)?;
1007                self.tavern.expeditions.end = end;
1008            }
1009            "expeditions" => {
1010                let data: Vec<i64> = val.into_list("exp event")?;
1011
1012                if !data.len().is_multiple_of(8) {
1013                    warn!(
1014                        "Available expeditions have weird size: {data:?} {}",
1015                        data.len()
1016                    );
1017                }
1018                self.tavern.expeditions.available = data
1019                    .chunks_exact(8)
1020                    .map(|data| {
1021                        Ok(AvailableExpedition {
1022                            target: data
1023                                .cfpget(0, "expedition typ", |a| a)?
1024                                .unwrap_or_default(),
1025                            location_1: data
1026                                .cfpget(4, "exp loc 1", |a| a)?
1027                                .unwrap_or_default(),
1028                            location_2: data
1029                                .cfpget(5, "exp loc 2", |a| a)?
1030                                .unwrap_or_default(),
1031                            thirst_for_adventure_sec: data
1032                                .csiget(6, "exp alu", 600)?,
1033                            special: data.cfpget(7, "exp special", |a| a)?,
1034                        })
1035                    })
1036                    .collect::<Result<_, _>>()?;
1037            }
1038            "expeditionrewardresources" => {
1039                // I would assume, that everything we get is just update
1040                // elsewhere, so I dont care about parsing this
1041            }
1042            "expeditionreward" => {
1043                // This works, but I dont think anyone cares about that. It
1044                // will just be in the inv. anyways
1045                // let data:Vec<i64> = val.into_list("expedition reward")?;
1046                // for chunk in data.chunks_exact(ITEM_PARSE_LEN){
1047                //     let item = Item::parse(chunk, server_time);
1048                //     println!("{item:#?}");
1049                // }
1050            }
1051            "expeditionmonster" => {
1052                let data: Vec<i64> = val.into_list("expedition monster")?;
1053                let exp = self
1054                    .tavern
1055                    .expeditions
1056                    .active
1057                    .get_or_insert_with(Default::default);
1058
1059                exp.boss = ExpeditionBoss {
1060                    id: data
1061                        .cfpget(0, "expedition monster", |a| -a)?
1062                        .unwrap_or_default(),
1063                    items: soft_into(
1064                        data.get(1).copied().unwrap_or_default(),
1065                        "exp monster items",
1066                        3,
1067                    ),
1068                };
1069            }
1070            "expeditionhalftime" => {
1071                let data: Vec<i64> = val.into_list("halftime exp")?;
1072                let exp = self
1073                    .tavern
1074                    .expeditions
1075                    .active
1076                    .get_or_insert_with(Default::default);
1077
1078                exp.halftime_for_boss_id =
1079                    -data.cget(0, "halftime for boss id")?;
1080                exp.rewards = data
1081                    .skip(1, "halftime choice")?
1082                    .chunks_exact(2)
1083                    .map(Reward::parse)
1084                    .collect::<Result<_, _>>()?;
1085            }
1086            "expeditionstate" => {
1087                let data: Vec<i64> = val.into_list("exp state")?;
1088                let exp = self
1089                    .tavern
1090                    .expeditions
1091                    .active
1092                    .get_or_insert_with(Default::default);
1093                exp.floor_stage = data.cget(2, "floor stage")?;
1094
1095                exp.target_thing = data
1096                    .cfpget(3, "expedition target", |a| a)?
1097                    .unwrap_or_default();
1098                exp.target_current = data.csiget(7, "exp current", 100)?;
1099                exp.target_amount = data.csiget(8, "exp target", 100)?;
1100
1101                exp.current_floor = data.csiget(0, "clearing", 0)?;
1102                exp.heroism = data.csiget(13, "heroism", 0)?;
1103
1104                exp.busy_since = data.cstget(15, "exp start", server_time)?;
1105                exp.busy_until = data.cstget(16, "exp busy", server_time)?;
1106
1107                for (x, item) in data
1108                    .skip(9, "exp items")?
1109                    .iter()
1110                    .copied()
1111                    .zip(&mut exp.items)
1112                {
1113                    *item = match FromPrimitive::from_i64(x) {
1114                        None if x != 0 => {
1115                            warn!("Unknown item: {x}");
1116                            Some(ExpeditionThing::Unknown)
1117                        }
1118                        x => x,
1119                    };
1120                }
1121            }
1122            "expeditioncrossroad" => {
1123                // 3/3/132/0/2/2
1124                let data: Vec<i64> = val.into_list("cross")?;
1125                let exp = self
1126                    .tavern
1127                    .expeditions
1128                    .active
1129                    .get_or_insert_with(Default::default);
1130                exp.update_encounters(&data);
1131            }
1132            "eventtasklist" => {
1133                let data: Vec<i64> = val.into_list("etl")?;
1134                self.specials.tasks.event.tasks.clear();
1135                for c in data.chunks_exact(4) {
1136                    let task = Task::parse(c)?;
1137                    self.specials.tasks.event.tasks.push(task);
1138                }
1139            }
1140            "eventtaskrewardpreview" => {
1141                let vals: Vec<i64> =
1142                    val.into_list("event task reward preview")?;
1143
1144                self.specials.tasks.event.rewards = parse_rewards(&vals);
1145            }
1146            "dailytasklist" => {
1147                let data: Vec<i64> = val.into_list("daily tasks list")?;
1148                self.specials.tasks.daily.tasks.clear();
1149
1150                // I think the first value here is the amount of > 1 bell
1151                // quests
1152                for d in data.skip(1, "daily tasks")?.chunks_exact(4) {
1153                    self.specials.tasks.daily.tasks.push(Task::parse(d)?);
1154                }
1155            }
1156            "eventtaskinfo" => {
1157                let data: Vec<i64> = val.into_list("eti")?;
1158                self.specials.tasks.event.theme = data
1159                    .cfpget(2, "event task theme", |a| a)?
1160                    .unwrap_or(EventTaskTheme::Unknown);
1161                self.specials.tasks.event.start =
1162                    data.cstget(0, "event t start", server_time)?;
1163                self.specials.tasks.event.end =
1164                    data.cstget(1, "event t end", server_time)?;
1165            }
1166            "scrapbook" => {
1167                self.character.scrapbook = ScrapBook::parse(val.as_str());
1168            }
1169            "dungeonfaces" | "shadowfaces" => {
1170                // Gets returned after winning a dungeon fight. This looks a
1171                // bit like a reward, but that should be handled in fight
1172                // parsing already?
1173            }
1174            "messagelist" => {
1175                let data = val.as_str();
1176                self.mail.inbox.clear();
1177                for msg in data.split(';').filter(|a| !a.trim().is_empty()) {
1178                    match InboxEntry::parse(msg, server_time) {
1179                        Ok(msg) => self.mail.inbox.push(msg),
1180                        Err(e) => warn!("Invalid msg: {msg} {e}"),
1181                    }
1182                }
1183            }
1184            "messagetext" => {
1185                self.mail.open_msg = Some(from_sf_string(val.as_str()));
1186            }
1187            "combatloglist" => {
1188                self.mail.combat_log.clear();
1189                for entry in val.as_str().split(';') {
1190                    let parts = entry.split(',').collect::<Vec<_>>();
1191                    if parts.iter().all(|a| a.is_empty()) {
1192                        continue;
1193                    }
1194                    match CombatLogEntry::parse(&parts, server_time) {
1195                        Ok(cle) => {
1196                            self.mail.combat_log.push(cle);
1197                        }
1198                        Err(e) => {
1199                            warn!(
1200                                "Unable to parse combat log entry: {parts:?} \
1201                                 - {e}"
1202                            );
1203                        }
1204                    }
1205                }
1206            }
1207            "maxupgradelevel" => {
1208                self.fortress
1209                    .get_or_insert_with(Default::default)
1210                    .building_max_lvl = val.into("max upgrade lvl")?;
1211            }
1212            "singleportalenemylevel" => {
1213                self.dungeons
1214                    .portal
1215                    .get_or_insert_with(Default::default)
1216                    .enemy_level = val.into("portal lvl").unwrap_or(u32::MAX);
1217            }
1218            "ownpetsstats" => {
1219                self.pets
1220                    .get_or_insert_with(Default::default)
1221                    .update_pet_stat(&val.into_list("pet stats")?);
1222            }
1223            "ownpets" => {
1224                let data = val.into_list("own pets")?;
1225                self.pets
1226                    .get_or_insert_with(Default::default)
1227                    .update(&data, server_time)?;
1228            }
1229            "petsdefensetype" => {
1230                let pet_id = val.into("pet def typ")?;
1231                self.pets
1232                    .get_or_insert_with(Default::default)
1233                    .opponent
1234                    .habitat = Some(HabitatType::from_typ_id(pet_id).ok_or(
1235                    SFError::ParsingError("pet def typ", format!("{pet_id}")),
1236                )?);
1237            }
1238            "otherplayersavecharacter" => {
1239                other_player
1240                    .get_or_insert_default()
1241                    .update(&val.into_list("other player")?, server_time)?;
1242            }
1243            "otherplayersavepotions" => {
1244                other_player.get_or_insert_default().active_potions =
1245                    items::parse_active_potions(
1246                        &val.into_list("other potions")?,
1247                        server_time,
1248                    );
1249            }
1250            "otherplayer" => {
1251                let data: Vec<i64> = val.into_list("other player")?;
1252                #[allow(deprecated)]
1253                {
1254                    other_player.get_or_insert_default().guild_joined =
1255                        data.cstget(166, "other joined guild", server_time)?;
1256                }
1257            }
1258            "otherplayerfriendstatus" => {
1259                other_player
1260                    .get_or_insert_with(Default::default)
1261                    .relationship = warning_parse(
1262                    val.into::<i32>("other friend")?,
1263                    "other friend",
1264                    FromPrimitive::from_i32,
1265                )
1266                .unwrap_or_default();
1267            }
1268            "otherplayerpetbonus" => {
1269                other_player
1270                    .get_or_insert_with(Default::default)
1271                    .update_pet_bonus(&val.into_list("o pet bonus")?)?;
1272            }
1273            "otherplayerunitlevel" => {
1274                let data: Vec<i64> =
1275                    val.into_list("other player unit level")?;
1276                // This includes other levels, but they are handled
1277                // elsewhere I think
1278                other_player
1279                    .get_or_insert_with(Default::default)
1280                    .wall_combat_lvl = data.csiget(0, "wall_lvl", 0)?;
1281            }
1282            "petsrank" => {
1283                self.pets.get_or_insert_with(Default::default).rank =
1284                    val.into("pet rank")?;
1285            }
1286
1287            "maxrankUnderworld" => {
1288                self.hall_of_fames.underworlds_total =
1289                    Some(val.into("mrank under")?);
1290            }
1291            "otherplayerfortressrank" => {
1292                match val.into::<i64>("other player fortress rank")? {
1293                    ..=-1 => {}
1294                    x => {
1295                        let rank = x.try_into().unwrap_or(1);
1296                        other_player
1297                            .get_or_insert_default()
1298                            .fortress
1299                            .get_or_insert_default()
1300                            .rank = rank;
1301                    }
1302                }
1303            }
1304            "workreward" => {
1305                // Should be irrelevant
1306            }
1307            x if x.starts_with("winnerid") => {
1308                // For all winnerid's, except the last one, the winnerid
1309                // value contains the fightversion as well
1310                let raw_winner_id = val
1311                    .as_str()
1312                    .split_once(|a: char| !a.is_ascii_digit())
1313                    .map_or(val.as_str(), |a| a.0);
1314                if let Ok(winner_id) = raw_winner_id.parse() {
1315                    self.get_fight(x).winner_id = winner_id;
1316                } else {
1317                    error!("Invalid winner id: {raw_winner_id}");
1318                }
1319            }
1320            "fightresult" => {
1321                let data: Vec<i64> = val.into_list("fight result")?;
1322                self.last_fight
1323                    .get_or_insert_with(Default::default)
1324                    .update_result(&data, server_time)?;
1325                // Note: The sub_key from this, can improve fighter parsing
1326            }
1327            x if x.starts_with("fightheader") => {
1328                self.get_fight(x).update_fighters(val.as_str());
1329            }
1330            "fightgroups" => {
1331                let fight =
1332                    self.last_fight.get_or_insert_with(Default::default);
1333                fight.update_groups(val.as_str());
1334            }
1335            "fightadditionalplayers" => {
1336                // This should be players in guild battles, that have not
1337                // participapted. I dont think this matters
1338            }
1339            "fightversion" => {
1340                // This key is unreliable and partially merged into
1341                // winnerid, so I just parse this in the fight response
1342                // below, where it is actually used
1343            }
1344            x if x.starts_with("fight") && x.len() <= 7 => {
1345                let fight_no = fight_no_from_header(x);
1346                let wkey = format!("winnerid{fight_no}");
1347                let version = if let Some(winner_id) =
1348                    all_values.get(wkey.as_str())
1349                {
1350                    // For unknown reasons, the fightversion is merged
1351                    // into the winnerid for all fights, except the last
1352                    // one
1353                    winner_id.as_str().split_once("fightversion:").map(|a| a.1)
1354                } else {
1355                    // The last fight uses the normal fightversion
1356                    // header
1357                    all_values.get("fightversion").map(|a| a.as_str())
1358                };
1359                let fight = self.get_fight(x);
1360                if let Some(version) = version.and_then(|a| a.parse().ok()) {
1361                    fight.update_rounds(val.as_str(), version)?;
1362                } else {
1363                    fight.actions.clear();
1364                }
1365            }
1366            "othergroupname" => {
1367                other_guild
1368                    .get_or_insert_with(Default::default)
1369                    .name
1370                    .set(val.as_str());
1371            }
1372            "othergrouprank" => {
1373                other_guild.get_or_insert_with(Default::default).rank =
1374                    val.into("other group rank")?;
1375            }
1376            "othergroupfightcost" => {
1377                other_guild.get_or_insert_with(Default::default).attack_cost =
1378                    val.into("other group fighting cost")?;
1379            }
1380            "othergroupmember" => {
1381                let names: Vec<_> = val.as_str().split(',').collect();
1382                let og = other_guild.get_or_insert_with(Default::default);
1383                og.members.resize_with(names.len(), Default::default);
1384                for (m, n) in og.members.iter_mut().zip(names) {
1385                    m.name.set(n);
1386                }
1387            }
1388            "othergroupdescription" => {
1389                let guild = other_guild.get_or_insert_with(Default::default);
1390                let (emblem, desc) =
1391                    val.as_str().split_once('§').unwrap_or(("", val.as_str()));
1392
1393                guild.emblem.update(emblem);
1394                guild.description = from_sf_string(desc);
1395            }
1396            "othergroup" => {
1397                other_guild
1398                    .get_or_insert_with(Default::default)
1399                    .update(val.as_str(), server_time)?;
1400            }
1401            "reward" => {
1402                // This is the task reward, which you should already know
1403                // from collecting
1404            }
1405            "gtdailypoints" => {
1406                self.hellevator
1407                    .active
1408                    .get_or_insert_with(Default::default)
1409                    .guild_points_today = val.into("gtdaily").unwrap_or(0);
1410            }
1411            "gtchest" => {
1412                // 2500/0/5000/1/7500/2/10000/0/12500/1/15000/2/17500/0/
1413                // 20000/1/22500/2/25000/0/27500/1/30000/2/32500/0/35000/1/
1414                // 37500/2/40000/0/42500/1/45000/2/47500/0/50000/1/57500/2/
1415                // 65000/0/72500/1/80000/2/87500/0/95000/1/102500/2/110000/
1416                // 0/117500/1/125000/2/137500/0/150000/1/162500/2/175000/0/
1417                // 187500/1/200000/2/212500/0/225000/1/237500/2/250000/0/
1418                // 272500/1/295000/2/317500/0/340000/1/362500/2/385000/0/
1419                // 407500/1/430000/2/452500/0/475000/1
1420            }
1421            "gtraidparticipants" => {
1422                let all: Vec<_> = val.as_str().split('/').collect();
1423                let hellevator =
1424                    self.hellevator.active.get_or_insert_with(Default::default);
1425
1426                for floor in &mut hellevator.guild_raid_floors {
1427                    floor.today_assigned.clear();
1428                }
1429
1430                #[allow(clippy::indexing_slicing)]
1431                for part in all.chunks_exact(2) {
1432                    // The name of the guild member
1433                    let name = part[0];
1434                    // should be the dungeon they signed up for today
1435                    let val: usize = part
1436                        .cget(1, "hell raid part")
1437                        .ok()
1438                        .and_then(|a| a.parse().ok())
1439                        .unwrap_or(0);
1440                    if val > 0 {
1441                        if val > hellevator.guild_raid_floors.len() {
1442                            hellevator
1443                                .guild_raid_floors
1444                                .resize_with(val, Default::default);
1445                        }
1446                        if let Some(floor) =
1447                            hellevator.guild_raid_floors.get_mut(val - 1)
1448                        {
1449                            floor.today_assigned.push(name.to_string());
1450                        }
1451                    }
1452                }
1453            }
1454            "gtraidparticipantsyesterday" => {
1455                let all: Vec<_> = val.as_str().split('/').collect();
1456
1457                let hellevator =
1458                    self.hellevator.active.get_or_insert_with(Default::default);
1459
1460                for floor in &mut hellevator.guild_raid_floors {
1461                    floor.yesterday_assigned.clear();
1462                }
1463
1464                #[allow(clippy::indexing_slicing)]
1465                for part in all.chunks_exact(2) {
1466                    // The name of the guild member
1467                    let name = part[0];
1468                    // should be the dungeon they signed up for today
1469                    let val: usize = part
1470                        .cget(1, "hell raid part yd")
1471                        .ok()
1472                        .and_then(|a| a.parse().ok())
1473                        .unwrap_or(0);
1474                    if val > 0 {
1475                        if val > hellevator.guild_raid_floors.len() {
1476                            hellevator
1477                                .guild_raid_floors
1478                                .resize_with(val, Default::default);
1479                        }
1480                        if let Some(floor) =
1481                            hellevator.guild_raid_floors.get_mut(val - 1)
1482                        {
1483                            floor.yesterday_assigned.push(name.to_string());
1484                        }
1485                    }
1486                }
1487            }
1488            "gtrank" => {
1489                self.hellevator
1490                    .active
1491                    .get_or_insert_with(Default::default)
1492                    .guild_rank = val.into("gt rank").unwrap_or(0);
1493            }
1494            "gtrankingmax" => {
1495                self.hall_of_fames.hellevator_total =
1496                    val.into("gt rank max").ok();
1497            }
1498            "gtbracketlist" => {
1499                self.hellevator
1500                    .active
1501                    .get_or_insert_with(Default::default)
1502                    .brackets =
1503                    val.into_list("gtbracketlist").unwrap_or_default();
1504            }
1505            "gtraidfights" => {
1506                let data: Vec<i64> =
1507                    val.into_list("gt raids").unwrap_or_default();
1508
1509                let hellevator =
1510                    self.hellevator.active.get_or_insert_with(Default::default);
1511
1512                hellevator.guild_raid_signup_start = data
1513                    .cstget(0, "h raid signup start", server_time)?
1514                    .unwrap_or_default();
1515
1516                hellevator.guild_raid_start = data
1517                    .cstget(1, "h raid next attack", server_time)?
1518                    .unwrap_or_default();
1519
1520                let start = data.skip(2, "hellevator_fights")?;
1521
1522                let floor_count = start.len() / 5;
1523
1524                if floor_count > hellevator.guild_raid_floors.len() {
1525                    hellevator
1526                        .guild_raid_floors
1527                        .resize_with(floor_count, Default::default);
1528                }
1529                #[allow(clippy::indexing_slicing)]
1530                for (data, floor) in
1531                    start.chunks_exact(5).zip(&mut hellevator.guild_raid_floors)
1532                {
1533                    // FIXME: What are these?
1534                    floor.today = data[1];
1535                    floor.yesterday = data[2];
1536                    floor.point_reward =
1537                        data.csiget(3, "floor t-reward", 0).unwrap_or(0);
1538                    floor.silver_reward =
1539                        data.csiget(4, "floor c-reward", 0).unwrap_or(0);
1540                }
1541            }
1542            "gtmonsterreward" => {
1543                let data: Vec<i64> =
1544                    val.into_list("gt m reward").unwrap_or_default();
1545
1546                let hellevator =
1547                    self.hellevator.active.get_or_insert_with(Default::default);
1548                hellevator.monster_rewards.clear();
1549
1550                for chunk in data.chunks_exact(3) {
1551                    let raw_typ = chunk.cget(0, "gt monster reward typ")?;
1552                    if raw_typ <= 0 {
1553                        continue;
1554                    }
1555                    let one = chunk
1556                        .csiget(1, "gt monster reward typ", 0)
1557                        .unwrap_or(0);
1558                    if one != 0 {
1559                        warn!("hellevator monster t: {one}");
1560                    }
1561                    let typ = HellevatorMonsterRewardTyp::parse(raw_typ);
1562                    let amount: u64 =
1563                        chunk.csiget(2, "gt monster reward amount", 0)?;
1564                    hellevator
1565                        .monster_rewards
1566                        .push(HellevatorMonsterReward { typ, amount });
1567                }
1568            }
1569            "gtdailyreward" => {
1570                self.hellevator
1571                    .active
1572                    .get_or_insert_with(Default::default)
1573                    .rewards_today = HellevatorDailyReward::parse(
1574                    &val.into_list("hdrtd").unwrap_or_default(),
1575                );
1576            }
1577            "gtdailyrewardnext" => {
1578                self.hellevator
1579                    .active
1580                    .get_or_insert_with(Default::default)
1581                    .rewards_next = HellevatorDailyReward::parse(
1582                    &val.into_list("hdrnd").unwrap_or_default(),
1583                );
1584            }
1585            "gtdailyrewardyesterday" => {
1586                self.hellevator
1587                    .active
1588                    .get_or_insert_with(Default::default)
1589                    .rewards_yesterday = HellevatorDailyReward::parse(
1590                    &val.into_list("hdryd").unwrap_or_default(),
1591                );
1592            }
1593            "gtdailyrewardclaimed" => {
1594                if let Some(hellevator) = self.hellevator.active.as_mut() {
1595                    // This response key is sent when either yesterday's or
1596                    // today's daily reward was claimed. To check whether
1597                    // yesterday's daily reward was claimed, we check if
1598                    // "gtdailyreward" is missing in the response, since it
1599                    // is only included if today's daily reward was claimed.
1600                    if !all_values.contains_key("gtdailyreward") {
1601                        // The game doesn't update this value itself, so we
1602                        // do it manually.
1603                        hellevator.rewards_yesterday = None;
1604                    }
1605                }
1606            }
1607            "gtranking" => {
1608                self.hall_of_fames.hellevator = val
1609                    .as_str()
1610                    .split(';')
1611                    .filter(|a| !a.is_empty())
1612                    .map(|chunk| chunk.split(',').collect())
1613                    .flat_map(|chunk: Vec<_>| -> Result<_, SFError> {
1614                        Ok(HallOfFameHellevator {
1615                            rank: chunk.cfsuget(0, "hh rank")?,
1616                            name: chunk.cget(1, "hh name")?.to_string(),
1617                            tokens: chunk.cfsuget(2, "hh tokens")?,
1618                        })
1619                    })
1620                    .collect();
1621            }
1622            "gtpreviewreward" => {
1623                // TODO: these are the previews of the rewards per rank
1624                // 1:17/0/1/16/0/1/8/1/64200/9/1/96300/4/1/3201877800/,2:18/
1625                // 0/1/16/0/1/8/1/64200/9/1/96300/4/1/3201877800/,3:19/0/1/
1626                // 16/0/1/8/1/64200/9/1/96300/4/1/3201877800/,4:16/0/1/8/1/
1627                // 61632/9/1/92448/4/1/3041783910/,5:16/0/1/8/1/59064/9/1/
1628                // 88596/4/1/2881690020/,6:16/0/1/8/1/56496/9/1/84744/4/1/
1629                // 2721596130/,7:16/0/1/8/1/53928/9/1/80892/4/1/2561502240/,
1630                // 8:16/0/1/8/1/51360/9/1/77040/4/1/2401408350/,9:16/0/1/8/
1631                // 1/48792/9/1/73188/4/1/2241314460/,10:16/0/1/8/1/46224/9/
1632                // 1/69336/4/1/2241314460/,11:16/0/1/8/1/43656/9/1/65484/4/
1633                // 1/2081220570/,12:16/0/1/8/1/41088/9/1/61632/4/1/
1634                // 2081220570/,13:16/0/1/8/1/38520/9/1/57780/4/1/1921126680/
1635                // ,14:16/0/1/8/1/35952/9/1/53928/4/1/1921126680/,15:16/0/1/
1636                // 8/1/33384/9/1/50076/4/1/1761032790/,16:16/0/1/8/1/30816/
1637                // 9/1/46224/4/1/1761032790/,17:8/1/28248/9/1/42372/4/1/
1638                // 1600938900/,18:8/1/25680/9/1/38520/4/1/1600938900/,19:4/
1639                // 1/1440845010/,20:4/1/1280751120/,21:4/1/1120657230/,22:4/
1640                // 1/960563340/,23:4/1/800469450/,24:4/1/640375560/,25:4/1/
1641                // 480281670/,
1642            }
1643            "gtmonster" => {
1644                self.hellevator
1645                    .active
1646                    .get_or_insert_with(Default::default)
1647                    .current_monster = HellevatorMonster::parse(
1648                    &val.into_list("h monster").unwrap_or_default(),
1649                )
1650                .ok();
1651            }
1652            "gtbonus" => {
1653                self.hellevator
1654                    .active
1655                    .get_or_insert_with(Default::default)
1656                    .daily_treat_bonus = val
1657                    .into_list("gt bonus")
1658                    .and_then(|a| HellevatorTreatBonus::parse(&a))
1659                    .ok();
1660            }
1661            "pendingrewards" => {
1662                let vals: Vec<_> = val.as_str().split('/').collect();
1663                self.mail.claimables = vals
1664                    .chunks_exact(6)
1665                    .flat_map(|chunk| -> Result<ClaimableMail, SFError> {
1666                        let start = chunk.cfsuget(4, "p reward start")?;
1667                        let end = chunk.cfsuget(5, "p reward end")?;
1668
1669                        let status = match chunk.cget(1, "p read")? {
1670                            "0" => ClaimableStatus::Unread,
1671                            "1" => ClaimableStatus::Read,
1672                            "2" => ClaimableStatus::Claimed,
1673                            x => {
1674                                warn!("Unknown claimable status: {x}");
1675                                ClaimableStatus::Claimed
1676                            }
1677                        };
1678
1679                        Ok(ClaimableMail {
1680                            typ: FromPrimitive::from_i64(
1681                                chunk.cfsuget(2, "claimable typ")?,
1682                            )
1683                            .unwrap_or_default(),
1684                            msg_id: chunk.cfsuget(0, "msg_id")?,
1685                            status,
1686                            name: chunk.cget(3, "reward code")?.to_string(),
1687                            received: server_time
1688                                .convert_to_local(start, "p start"),
1689                            claimable_until: server_time
1690                                .convert_to_local(end, "p end"),
1691                        })
1692                    })
1693                    .collect();
1694            }
1695            "pendingrewardressources" => {
1696                let vals: Vec<i64> =
1697                    val.into_list("pendingrewardressources")?;
1698
1699                self.mail
1700                    .open_claimable
1701                    .get_or_insert_with(Default::default)
1702                    .resources = vals
1703                    .chunks_exact(2)
1704                    .flat_map(|chunk| -> Result<Reward, SFError> {
1705                        Ok(Reward {
1706                            typ: RewardType::parse(chunk.cget(0, "c typ")?),
1707                            amount: chunk.csiget(1, "c amount", 1)?,
1708                        })
1709                    })
1710                    .collect();
1711            }
1712            "pendingreward" => {
1713                let vals: Vec<i64> = val.into_list("pending item")?;
1714                self.mail
1715                    .open_claimable
1716                    .get_or_insert_with(Default::default)
1717                    .items = vals
1718                    .chunks_exact(ITEM_PARSE_LEN)
1719                    .flat_map(|a|
1720                            // Might be broken
1721                            Item::parse(a, server_time))
1722                    .flatten()
1723                    .collect();
1724            }
1725            "fightablegroups" => {
1726                self.guild
1727                    .get_or_insert_default()
1728                    .update_fightable_targets(val.as_str())?;
1729            }
1730            "adventscalendar" => {
1731                let vals: Vec<i64> = val.into_list("advent door")?;
1732                self.specials.advent_calendar = match vals.first() {
1733                    Some(0) | None => None,
1734                    _ => Reward::parse(&vals).ok(),
1735                };
1736            }
1737            "fortresschances" => {
1738                // chances for different gems to drop in the gem mine / 100
1739                // big/medium/small/orange/black/others
1740                // 3334/3333/3333/0/1700/8300
1741            }
1742            "deedsandtitlesplayersave" => {
1743                // The deeds of glory of the player
1744                // {pid}/{glory}/{rank}/{bronze}/{silver}/{gold}/2/2/0/1/58/0/
1745                // 59/0/60/0/0/0/0/0/0/0
1746            }
1747            "deedshelves" => {
1748                // deedshelves (subkey => 1)
1749                // 1
1750            }
1751            "titleinfos" => {
1752                // let data:Vec<_> = val.as_str().split('/').collect();
1753                // 1    // active title type?
1754                // 2    // hero titles?
1755                // 1644 // max rank?
1756
1757                // Top 3?
1758                // for slice in data.skip(3, "titleinfos")?.chunks_exact(2) {
1759                // LordBurggraf/1761696040
1760                // scyth3/1761696052/
1761                // Deinnachbar/1761696060
1762
1763                // }
1764            }
1765            "titleplayerinfos" => {
1766
1767                // {pid}/2/2/0/{rank}/1761884016/482/10/0/217/1761821390/482
1768            }
1769            "fortressstorage" => {
1770                self.fortress.get_or_insert_default().update_resources(
1771                    &val.into_list("ft resources")?,
1772                    server_time,
1773                )?;
1774            }
1775            "fortressunits" => {
1776                self.fortress
1777                    .get_or_insert_default()
1778                    .update_units(&val.into_list("ft units")?, server_time)?;
1779            }
1780            "fortress" => {
1781                self.fortress
1782                    .get_or_insert_default()
1783                    .update(&val.into_list("fortress")?, server_time)?;
1784            }
1785            "wheel" => {
1786                let data: Vec<i64> = val.into_list("wheel")?;
1787                // [0] => 2 ??
1788                self.specials.wheel.spins_today =
1789                    data.csiget(1, "lucky turns", 0)?;
1790                self.specials.wheel.next_free_spin =
1791                    data.cstget(2, "next lucky turn", server_time)?;
1792            }
1793            "dice" => {
1794                let data: Vec<i64> = val.into_list("dice")?;
1795                self.tavern.dice_game.next_free =
1796                    data.cstget(0, "dice next", server_time)?;
1797                self.tavern.dice_game.remaining =
1798                    data.csiget(1, "rem dice games", 0)?;
1799            }
1800            "charactergroup" => {
1801                let data: Vec<i64> = val.into_list("c group")?;
1802                let guild = self.guild.get_or_insert_with(Default::default);
1803                guild.own_treasure_skill =
1804                    data.csiget(0, "own treasure skill", 0)?;
1805                guild.own_instructor_skill =
1806                    data.csiget(1, "own instruction skill", 0)?;
1807                guild.hydra.next_battle =
1808                    data.cstget(2, "pet battle", server_time)?;
1809                guild.hydra.remaining_fights =
1810                    data.csiget(3, "remaining pet battles", 0)?;
1811                guild.own_pet_lvl = data.csiget(4, "own pet lvl", 0)?;
1812                guild.joined = data.cstget(5, "guild joined", server_time)?;
1813                // [6] => ????
1814            }
1815            "arena" => {
1816                let data: Vec<i64> = val.into_list("arena")?;
1817                self.arena.next_free_fight =
1818                    data.cstget(0, "next battle time", server_time)?;
1819                self.arena.fights_for_xp =
1820                    data.csiget(1, "arena xp fights", 0)?;
1821                for (idx, val) in self.arena.enemy_ids.iter_mut().enumerate() {
1822                    *val = data.csiget(2 + idx, "arena enemy id", 0)?;
1823                }
1824                // [5] => ??
1825            }
1826            "ownplayersavepotions" => {
1827                let data: Vec<i64> = val.into_list("potions")?;
1828                self.character.active_potions =
1829                    items::parse_active_potions(&data, server_time);
1830            }
1831            "arcanetoilet" => {
1832                let data: Vec<i64> = val.into_list("toilet")?;
1833
1834                // Toilet remains none as long as its level is 0
1835                let toilet_lvl = data.cget(0, "toilet lvl")?;
1836                if toilet_lvl > 0 {
1837                    self.tavern
1838                        .toilet
1839                        .get_or_insert_with(Default::default)
1840                        .update(&data, server_time)?;
1841                }
1842            }
1843            "vipstatus" => {
1844                other_player.get_or_insert_default().is_vip =
1845                    val.as_str() != "0";
1846            }
1847            "characterstatus" => {
1848                let data: Vec<i64> = val.into_list("char status")?;
1849
1850                self.tavern.current_action = CurrentAction::parse(
1851                    data.cget(1, "action id")?,
1852                    data.cget(2, "action sec")?,
1853                    data.cstget(3, "current action time", server_time)?,
1854                );
1855
1856                // NOTE: [4] contains the start
1857                self.tavern.beer_max = data.csiget(5, "beer total", 0)?;
1858
1859                self.tavern.thirst_for_adventure_sec =
1860                    data.csiget(6, "remaining ALU", 0)?;
1861                self.tavern.beer_drunk =
1862                    data.csiget(7, "beer drunk count", 0)?;
1863                self.specials.calendar.collected =
1864                    data.csiget(8, "calendar collected", 245)?;
1865                self.specials.calendar.next_possible =
1866                    data.cstget(9, "calendar next", server_time)?;
1867                // 0
1868                // 0
1869                // 0
1870                // 0
1871                // 0
1872                // 1513       // was [15]
1873                // 1541087831 // acc creation time?
1874                // 0
1875                // 0
1876                // 0
1877                self.pets
1878                    .get_or_insert_with(Default::default)
1879                    .next_free_exploration =
1880                    data.cstget(20, "pet next free exp", server_time)?;
1881                self.dungeons.next_free_fight =
1882                    data.cstget(21, "dungeon timer", server_time)?;
1883                if let Some(start) =
1884                    data.cstget(22, "dungeon timer", server_time)?
1885                {
1886                    self.legendary_dungeon
1887                        .active
1888                        .get_or_insert_default()
1889                        .healing_start = Some(start);
1890                }
1891                // 0
1892                // 1
1893                // 0
1894                // 0
1895                // 0
1896                // 0
1897                // 0
1898                // 0
1899                // 0
1900            }
1901            "ownplayersavecharacter" => {
1902                let data: Vec<i64> = val.into_list("char save")?;
1903
1904                // 1482984989 // creation time? secret id?
1905                self.character.player_id = data.csiget(1, "player id", 0)?;
1906                // 0
1907                self.character.level =
1908                    data.csimget(3, "level", 0, |a| a & 0xFFFF)?;
1909                self.character.experience = data.csiget(4, "experience", 0)?;
1910                self.character.next_level_xp =
1911                    data.csiget(5, "xp to next lvl", 0)?;
1912                self.character.honor = data.csiget(6, "honor", 0)?;
1913                self.character.rank = data.csiget(7, "rank", 0)?;
1914                self.character.portrait =
1915                    Portrait::parse(data.skip(8, "portrait")?)
1916                        .unwrap_or_default();
1917                ///// Portrait
1918                // 4
1919                // 206
1920                // 203
1921                // 2
1922                // 0
1923                // 2
1924                // 7
1925                // 2
1926                // 0
1927                // 0
1928                self.character.race = data.cfpuget(18, "char race", |a| a)?;
1929                // 2
1930                // ////
1931                self.character.class =
1932                    data.cfpuget(20, "character class", |a| a - 1)?;
1933                self.character.mount =
1934                    data.cfpget(21, "character mount", |a| a & 0xFF)?;
1935                // 0
1936                self.character.armor = data.csiget(23, "total armor", 0)?;
1937                self.character.min_damage = data.csiget(24, "min damage", 0)?;
1938                self.character.max_damage = data.csiget(25, "max damage", 0)?;
1939                self.guild
1940                    .get_or_insert_with(Default::default)
1941                    .portal
1942                    .damage_bonus =
1943                    data.cimget(26, "portal dmg bonus", |a| a)?;
1944                // 4280492 ??
1945                self.dungeons
1946                    .portal
1947                    .get_or_insert_with(Default::default)
1948                    .player_hp_bonus =
1949                    data.csimget(28, "portal hp bonus", 0, |a| a)?;
1950                self.character.mount_end =
1951                    data.cstget(29, "mount end", server_time)?;
1952                update_enum_map(
1953                    &mut self.character.attribute_basis,
1954                    data.skip(30, "char attr basis")?,
1955                );
1956                update_enum_map(
1957                    &mut self.character.attribute_additions,
1958                    data.skip(35, "char attr adds")?,
1959                );
1960                update_enum_map(
1961                    &mut self.character.attribute_times_bought,
1962                    data.skip(40, "char attr tb")?,
1963                );
1964                // 0
1965                // 0
1966                // 0
1967                // 0
1968                // 1
1969                // 17
1970                // 0
1971                // 0
1972                // 0
1973                // 66
1974                // 0
1975                // 7
1976                // 18
1977                // 0
1978                // 0
1979                // 0
1980                // 0
1981                // 0
1982                // 0
1983                // 0
1984                // 80315    // guild_id ?
1985                // 12122    // sb count
1986                // 0
1987                // 31
1988                // 15       // gladiators
1989            }
1990            "adventure" => {
1991                let data: Vec<i64> = val.into_list("char save")?;
1992                // 198 - Might be the person to give you the quest?
1993                // 198 - ??
1994                for (slice, quest) in data
1995                    .skip(2, "quests")?
1996                    .chunks_exact(7)
1997                    .zip(&mut self.tavern.quests)
1998                {
1999                    quest.update(slice)?;
2000                }
2001            }
2002            "events" => {
2003                let data: Vec<i64> = val.into_list("events")?;
2004                if data.len() < 8 {
2005                    return Ok(());
2006                }
2007                // [0] might be the events theme (tavern decoration)?
2008                self.specials.events.active.clear();
2009                let flags = data.cget(1, "events")?;
2010                for (idx, event) in Event::iter().enumerate() {
2011                    if (flags & (1 << idx)) > 0 {
2012                        self.specials.events.active.insert(event);
2013                    }
2014                }
2015                // NOTE: there are two end times, that seem to be identical.
2016                // I will just guess, that [4] is the correct one and [2] is
2017                // for the theme
2018                self.specials.events.ends =
2019                    data.cstget(4, "event end", server_time)?;
2020
2021                // [3],[5],[6],[7] are just 0, so no idea what they could be
2022            }
2023            "tavernspecialend" | "tavernspecialsub" | "tavernspecial" => {
2024                // Removed old way to serve events
2025            }
2026            "subscriptionstatus" => {}
2027            // Legendary Dungeons
2028            "iadungeonchances" => {
2029                // IDK
2030            }
2031            "iadungeontime" => {
2032                let dungeons = &mut self.legendary_dungeon;
2033
2034                let vals: Vec<i64> = val.into_list("iadungeontime")?;
2035                dungeons.theme = vals.cfpget(0, "ld theme", |x| x)?;
2036                dungeons.start = vals.cstget(1, "ld start", server_time)?;
2037                dungeons.end = vals.cstget(2, "ld end", server_time)?;
2038                dungeons.close = vals.cstget(3, "ld closes", server_time)?;
2039            }
2040            "iadungeonstatstotal" => {
2041                let dungeons =
2042                    self.legendary_dungeon.active.get_or_insert_default();
2043
2044                let data: Vec<i64> = val.into_list("iadungeonstatstotal")?;
2045                dungeons.total_stats = TotalStats::parse(&data)?;
2046            }
2047            "iadungeonstats" => {
2048                let dungeons =
2049                    self.legendary_dungeon.active.get_or_insert_default();
2050
2051                let data = val.into_list("iadungeonstats")?;
2052                dungeons.stats = Stats::parse(&data).unwrap_or_default();
2053            }
2054            "iadungeon" => {
2055                let data: Vec<i64> = val.into_list("iadungeon")?;
2056                let dungeons =
2057                    self.legendary_dungeon.active.get_or_insert_default();
2058                dungeons.update(&data)?;
2059                if !all_values.contains_key("iapendingitems") {
2060                    dungeons.pending_items.clear();
2061                }
2062            }
2063            "iapendingitems" => {
2064                let dungeons =
2065                    self.legendary_dungeon.active.get_or_insert_default();
2066                dungeons.pending_items.clear();
2067                let data: Vec<i64> = val.into_list("iapendingitems")?;
2068                let amount: i64 = data.cget(0, "pending amount")?;
2069                if amount < 1 {
2070                    return Ok(());
2071                }
2072                for slice in
2073                    data.skip(1, "ld items")?.chunks_exact(ITEM_PARSE_LEN)
2074                {
2075                    let Some(item) = Item::parse(slice, server_time)? else {
2076                        warn!("Could not parse pending ld item");
2077                        continue;
2078                    };
2079                    dungeons.pending_items.push(item);
2080                }
2081            }
2082            "ialootitem" => {
2083                // The stuff, that was looted after a fight
2084            }
2085            "iamerchant" => {
2086                let data: Vec<i64> = val.into_list("iamerchant")?;
2087
2088                self.legendary_dungeon
2089                    .active
2090                    .get_or_insert_default()
2091                    .merchant_offers = data
2092                    .chunks_exact(3)
2093                    .flat_map(MerchantOffer::parse)
2094                    .flatten()
2095                    .collect();
2096            }
2097            "iadungeon20cost" => {
2098                self.legendary_dungeon
2099                    .active
2100                    .get_or_insert_default()
2101                    .heal_quarter_cost = val.into("iadungeon20cost")?;
2102            }
2103            "iadungeonsoulstones" => {
2104                let dungeons =
2105                    self.legendary_dungeon.active.get_or_insert_default();
2106
2107                let data: Vec<i64> = val.into_list("iamerchant")?;
2108                let mut chunks = data.chunks_exact(6);
2109                dungeons.active_gems = chunks
2110                    .by_ref()
2111                    .take(3)
2112                    .flat_map(GemOfFate::parse)
2113                    .flatten()
2114                    .collect();
2115
2116                dungeons.available_gems =
2117                    chunks.flat_map(GemOfFate::parse).flatten().collect();
2118            }
2119            "iamap" => {
2120                // 25/-5159/1/-315/
2121                // 25/-5160/1/-315/
2122                // 25/-5172/1/-315/
2123                // 25/-5168/1/-315
2124
2125                // Parsing this sucks. The first value is amount of levels
2126                // and the 2. is monster_id, so far so good. The next 2
2127                // values are weird though. If you change -315 to 315, it
2128                // counts as having visited the shop one, but I have no
2129                // idea how to further improve that. In addition, you can
2130                // inc. 1 in steps of 4 to inc. max shops by one... but
2131                // that breaks the total count of floors?? It is also
2132                // unclear how effect & name can be changed, might be
2133                // dependant on `LegendaryDungeonsEventTheme`? Whatever is
2134                // may be, I don't think we really need this, as long as
2135                // it does not contain the gems
2136            }
2137            "otherplayerfortressinfo" => {
2138                other_player
2139                    .get_or_insert_default()
2140                    .update_fortress(&val.into_list("other ft")?)?;
2141            }
2142            x if x.contains("average") && x.ends_with("level") => {
2143                // We do not care about avg. item lvl
2144            }
2145            // This is the extra bonus effect all treats get that day
2146            x if x.contains("dungeonenemies") => {
2147                // I `think` we do not need this
2148            }
2149            x if x.starts_with("attbonus") => {
2150                // This is always 0s, so I have no idea what this could be
2151            }
2152            x => {
2153                warn!("Update ignored {x} -> {val:?}");
2154            }
2155        }
2156        Ok(())
2157    }
2158}
2159
2160/// Gets the number if the fight header, so for `fight4` it would return 4 and
2161/// for fight it would return 1
2162fn fight_no_from_header(header_name: &str) -> usize {
2163    let number_str =
2164        header_name.trim_start_matches(|a: char| !a.is_ascii_digit());
2165    let id: usize = number_str.parse().unwrap_or(1);
2166    id.max(1)
2167}
2168
2169/// Stores the time difference between the server and the client to parse the
2170/// response timestamps and to always be able to know the servers (timezoned)
2171/// time without sending new requests to ask it
2172#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2174pub struct ServerTime(i64);
2175
2176impl ServerTime {
2177    /// Converts the raw timestamp from the server to the local time.
2178    #[must_use]
2179    pub(crate) fn convert_to_local(
2180        self,
2181        timestamp: i64,
2182        name: &str,
2183    ) -> Option<DateTime<Local>> {
2184        if matches!(timestamp, 0 | -1 | 1 | 11) {
2185            // For some reason these can be bad
2186            return None;
2187        }
2188
2189        if !(1_000_000_000..=3_000_000_000).contains(&timestamp) {
2190            warn!("Weird time stamp: {timestamp} for {name}");
2191            return None;
2192        }
2193        DateTime::from_timestamp(timestamp - self.0, 0)?
2194            .naive_utc()
2195            .and_local_timezone(Local)
2196            .latest()
2197    }
2198
2199    /// The current time of the server in their time zone (whatever that might
2200    /// be). This uses the system time and calculates the offset to the
2201    /// servers time, so this is NOT the time at the last request, but the
2202    /// actual current time of the server.
2203    #[must_use]
2204    pub fn current(&self) -> NaiveDateTime {
2205        Local::now().naive_local() + Duration::seconds(self.0)
2206    }
2207
2208    #[must_use]
2209    pub fn next_midnight(&self) -> std::time::Duration {
2210        let current = self.current();
2211        let tomorrow = current.date() + Duration::days(1);
2212        let tomorrow = NaiveDateTime::from(tomorrow);
2213        let sec_until_midnight =
2214            (tomorrow - current).to_std().unwrap_or_default().as_secs();
2215        // Time stuff is weird so make sure this never skips a day + actual
2216        // amount
2217        std::time::Duration::from_secs(sec_until_midnight % (60 * 60 * 24))
2218    }
2219}
2220
2221// https://stackoverflow.com/a/59955929
2222trait StringSetExt {
2223    fn set(&mut self, s: &str);
2224}
2225
2226impl StringSetExt for String {
2227    /// Replace the contents of a string with a string slice. This is basically
2228    /// `self = s.to_string()`, but without the deallication of self +
2229    /// allocation of s for that
2230    fn set(&mut self, s: &str) {
2231        self.replace_range(.., s);
2232    }
2233}
2234
2235/// The cost of something
2236#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2237#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2238pub struct NormalCost {
2239    /// The amount of silver something costs
2240    pub silver: u64,
2241    /// The amount of mushrooms something costs
2242    pub mushrooms: u16,
2243}