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