Skip to main content

sf_api/gamestate/
mod.rs

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