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