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