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