Skip to main content

sf_api/gamestate/
social.rs

1use std::collections::HashMap;
2
3use chrono::{DateTime, Local};
4use enum_map::EnumMap;
5use log::warn;
6use num_derive::FromPrimitive;
7use num_traits::FromPrimitive;
8
9use super::{
10    AttributeType, Class, Emblem, Flag, Item, Potion, Race, Reward, SFError,
11    ServerTime,
12    character::{Mount, Portrait},
13    guild::GuildRank,
14    items::Equipment,
15};
16use crate::{PlayerId, misc::*};
17
18#[derive(Debug, Clone, Default)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20pub struct Mail {
21    /// All the fights, that the character has stored for some reason
22    pub combat_log: Vec<CombatLogEntry>,
23    /// The amount of messages the inbox can store
24    pub inbox_capacity: u16,
25    /// Messages and notifications
26    pub inbox: Vec<InboxEntry>,
27    /// Items and resources from item codes/twitch drops, that you can claim
28    pub claimables: Vec<ClaimableMail>,
29    /// If you open a message (via command), this here will contain the opened
30    /// message
31    pub open_msg: Option<String>,
32    /// A preview of a claimable. You can get this via
33    /// `Command::ClaimablePreview`
34    pub open_claimable: Option<ClaimablePreview>,
35}
36
37/// Contains information about everything involving other players on the server.
38/// This mainly revolves around the Hall of Fame
39#[derive(Debug, Clone, Default)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41pub struct HallOfFames {
42    /// The amount of accounts on the server
43    pub players_total: u32,
44    /// A list of hall of fame players fetched during the last command
45    pub players: Vec<HallOfFamePlayer>,
46
47    /// The amount of guilds on this server. Will only be set after querying
48    /// the guild Hall of Fame, or looking at your own guild
49    pub guilds_total: Option<u32>,
50    /// A list of hall of fame guilds fetched during the last command
51    pub guilds: Vec<HallOfFameGuild>,
52
53    /// The amount of fortresses on this server. Will only be set after
54    /// querying the fortress HOF
55    pub fortresses_total: Option<u32>,
56    /// A list of hall of fame fortresses fetched during the last command
57    pub fortresses: Vec<HallOfFameFortress>,
58
59    /// The amount of players with pets on this server. Will only be set after
60    /// querying the pet HOF
61    pub pets_total: Option<u32>,
62    /// A list of hall of fame pet players fetched during the last command
63    pub pets: Vec<HallOfFamePets>,
64
65    pub hellevator_total: Option<u32>,
66    pub hellevator: Vec<HallOfFameHellevator>,
67
68    /// The amount of players with underworlds on this server. Will only be set
69    /// after querying the pet HOF
70    pub underworlds_total: Option<u32>,
71    /// A list of hall of fame pet players fetched during the last command
72    pub underworlds: Vec<HallOfFameUnderworld>,
73}
74
75#[derive(Debug, Clone, Default)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77pub struct HallOfFameHellevator {
78    pub rank: usize,
79    pub name: String,
80    pub tokens: u64,
81}
82
83/// Contains the results of `ViewGuild` & `ViewPlayer` commands. You can access
84/// the player info via functions and the guild data directly
85#[derive(Debug, Clone, Default)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87pub struct Lookup {
88    /// This can be accessed by using the `lookup_pid()`/`lookup_name()`
89    /// methods on `Lookup`
90    players: HashMap<PlayerId, OtherPlayer>,
91    name_to_id: HashMap<String, PlayerId>,
92
93    /// Guild that the character has looked at
94    pub guilds: HashMap<String, OtherGuild>,
95}
96
97impl Lookup {
98    pub(crate) fn insert_lookup(&mut self, other: OtherPlayer) {
99        if other.name.is_empty() || other.player_id == 0 {
100            warn!("Skipping invalid player insert");
101            return;
102        }
103        self.name_to_id.insert(other.name.clone(), other.player_id);
104        self.players.insert(other.player_id, other);
105    }
106
107    /// Checks to see if we have queried a player with that player id
108    #[must_use]
109    pub fn lookup_pid(&self, pid: PlayerId) -> Option<&OtherPlayer> {
110        self.players.get(&pid)
111    }
112
113    /// Checks to see if we have queried a player with the given name
114    #[must_use]
115    pub fn lookup_name(&self, name: &str) -> Option<&OtherPlayer> {
116        let other_pos = self.name_to_id.get(name)?;
117        self.players.get(other_pos)
118    }
119
120    /// Removes the information about another player based on their id
121    #[allow(clippy::must_use_unit)]
122    pub fn remove_pid(&mut self, pid: PlayerId) -> Option<OtherPlayer> {
123        self.players.remove(&pid)
124    }
125
126    /// Removes the information about another player based on their name
127    #[allow(clippy::must_use_unit)]
128    pub fn remove_name(&mut self, name: &str) -> Option<OtherPlayer> {
129        let other_pos = self.name_to_id.remove(name)?;
130        self.players.remove(&other_pos)
131    }
132
133    /// Clears out all players, that have previously been queried
134    pub fn reset_lookups(&mut self) {
135        self.players = HashMap::default();
136        self.name_to_id = HashMap::default();
137    }
138}
139
140/// Basic information about one character on the server. To get more
141/// information, you need to query this player via the `ViewPlayer` command
142#[derive(Debug, Default, Clone)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
144pub struct HallOfFamePlayer {
145    /// The rank of this player
146    pub rank: u32,
147    /// The name of this player. Used to query more information
148    pub name: String,
149    /// The guild this player is currently in. If this is None, the player is
150    /// not in a guild
151    pub guild: Option<String>,
152    /// The level of this player
153    pub level: u32,
154    /// The amount of fame this player has
155    pub honor: u32,
156    /// The class of this player
157    pub class: Class,
158    /// The Flag of this player, if they have set any
159    pub flag: Option<Flag>,
160}
161
162impl HallOfFamePlayer {
163    pub(crate) fn parse(val: &str) -> Result<Self, SFError> {
164        let data: Vec<_> = val.split(',').collect();
165        let rank = data.cfsuget(0, "hof player rank")?;
166        let name = data.cget(1, "hof player name")?.to_string();
167        let guild = Some(data.cget(2, "hof player guild")?.to_string())
168            .filter(|a| !a.is_empty());
169        let level = data.cfsuget(3, "hof player level")?;
170        let honor = data.cfsuget(4, "hof player fame")?;
171        let class: i64 = data.cfsuget(5, "hof player class")?;
172        let Some(class) = FromPrimitive::from_i64(class - 1) else {
173            warn!("Invalid hof class: {class} - {data:?}");
174            return Err(SFError::ParsingError(
175                "hof player class",
176                class.to_string(),
177            ));
178        };
179
180        let raw_flag = data.get(6).copied().unwrap_or_default();
181        let flag = Flag::parse(raw_flag);
182
183        Ok(HallOfFamePlayer {
184            rank,
185            name,
186            guild,
187            level,
188            honor,
189            class,
190            flag,
191        })
192    }
193}
194
195/// Basic information about one guild on the server. To get more information,
196/// you need to query this player via the `ViewGuild` command
197#[derive(Debug, Default, Clone)]
198#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
199pub struct HallOfFameGuild {
200    /// The name of the guild
201    pub name: String,
202    /// The rank of the guild
203    pub rank: u32,
204    /// The leader of the guild
205    pub leader: String,
206    /// The amount of members this guild has
207    pub member_count: u32,
208    /// The amount of honor this guild has
209    pub honor: u32,
210    /// Whether or not this guild is already being attacked
211    pub is_attacked: bool,
212}
213
214impl HallOfFameGuild {
215    pub(crate) fn parse(val: &str) -> Result<Self, SFError> {
216        let data: Vec<_> = val.split(',').collect();
217        let rank = data.cfsuget(0, "hof guild rank")?;
218        let name = data.cget(1, "hof guild name")?.to_string();
219        let leader = data.cget(2, "hof guild leader")?.to_string();
220        let member = data.cfsuget(3, "hof guild member")?;
221        let honor = data.cfsuget(4, "hof guild fame")?;
222        let attack_status: u8 = data.cfsuget(5, "hof guild atk")?;
223
224        Ok(HallOfFameGuild {
225            rank,
226            name,
227            leader,
228            member_count: member,
229            honor,
230            is_attacked: attack_status == 1u8,
231        })
232    }
233}
234
235impl HallOfFamePets {
236    pub(crate) fn parse(val: &str) -> Result<Self, SFError> {
237        let data: Vec<_> = val.split(',').collect();
238        let rank = data.cfsuget(0, "hof pet rank")?;
239        let name = data.cget(1, "hof pet player")?.to_string();
240        let guild = Some(data.cget(2, "hof pet guild")?.to_string())
241            .filter(|a| !a.is_empty());
242        let collected = data.cfsuget(3, "hof pets collected")?;
243        let honor = data.cfsuget(4, "hof pets fame")?;
244        let unknown = data.cfsuget(5, "hof pets uk")?;
245
246        Ok(HallOfFamePets {
247            name,
248            rank,
249            guild,
250            collected,
251            honor,
252            unknown,
253        })
254    }
255}
256
257impl HallOfFameFortress {
258    pub(crate) fn parse(val: &str) -> Result<Self, SFError> {
259        let data: Vec<_> = val.split(',').collect();
260        let rank = data.cfsuget(0, "hof ft rank")?;
261        let name = data.cget(1, "hof ft player")?.to_string();
262        let guild = Some(data.cget(2, "hof ft guild")?.to_string())
263            .filter(|a| !a.is_empty());
264        let upgrade = data.cfsuget(3, "hof ft collected")?;
265        let honor = data.cfsuget(4, "hof ft fame")?;
266
267        Ok(HallOfFameFortress {
268            name,
269            rank,
270            guild,
271            upgrade,
272            honor,
273        })
274    }
275}
276
277impl HallOfFameUnderworld {
278    pub(crate) fn parse(val: &str) -> Result<Self, SFError> {
279        let data: Vec<_> = val.split(',').collect();
280        let rank = data.cfsuget(0, "hof ft rank")?;
281        let name = data.cget(1, "hof ft player")?.to_string();
282        let guild = Some(data.cget(2, "hof ft guild")?.to_string())
283            .filter(|a| !a.is_empty());
284        let upgrade = data.cfsuget(3, "hof ft collected")?;
285        let honor = data.cfsuget(4, "hof ft fame")?;
286        let unknown = data.cfsuget(5, "hof pets uk")?;
287
288        Ok(HallOfFameUnderworld {
289            rank,
290            name,
291            guild,
292            upgrade,
293            honor,
294            unknown,
295        })
296    }
297}
298
299/// Basic information about one guild on the server
300#[derive(Debug, Default, Clone)]
301#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
302pub struct HallOfFameFortress {
303    /// The name of the person, that owns this fort
304    pub name: String,
305    /// The rank of this fortress in the fortress Hall of Fame
306    pub rank: u32,
307    /// If the player, that owns this fort is in a guild, this will contain the
308    /// guild name
309    pub guild: Option<String>,
310    /// The amount of upgrades, that have been built in this fortress
311    pub upgrade: u32,
312    /// The amount of honor this fortress has gained
313    pub honor: u32,
314}
315
316/// Basic information about one players pet collection on the server
317#[derive(Debug, Default, Clone)]
318#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
319pub struct HallOfFamePets {
320    /// The name of the player, that has these pets
321    pub name: String,
322    /// The rank of this players pet collection
323    pub rank: u32,
324    /// If the player, that owns these pets is in a guild, this will contain
325    /// the guild name
326    pub guild: Option<String>,
327    /// The amount of pets collected
328    pub collected: u32,
329    /// The amount of honro this pet collection has gained
330    pub honor: u32,
331    /// For guilds the value at this position is the attacked status, but no
332    /// idea, what it means here
333    pub unknown: i64,
334}
335
336/// Basic information about one players underworld on the server
337#[derive(Debug, Default, Clone)]
338#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
339pub struct HallOfFameUnderworld {
340    /// The rank this underworld has
341    pub rank: u32,
342    /// The name of the player, that owns this underworld
343    pub name: String,
344    /// If the player, that owns this underworld is in a guild, this will
345    /// contain the guild name
346    pub guild: Option<String>,
347    /// The amount of upgrades this underworld has
348    pub upgrade: u32,
349    /// The amount of honor this underworld has
350    pub honor: u32,
351    /// For guilds the value at this position is the attacked status, but no
352    /// idea, what it means here
353    pub unknown: i64,
354}
355
356/// All information about another player, that was queried via the `ViewPlayer`
357/// command
358#[derive(Debug, Default, Clone)]
359#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
360pub struct OtherPlayer {
361    /// The id of this player. This is mainly just useful to lookup this player
362    /// in `Lookup`, if you do not know the name
363    pub player_id: PlayerId,
364    /// The name of the player
365    pub name: String,
366    /// The level of the player
367    pub level: u16,
368    /// The description this player has set for themselves
369    pub description: String,
370    /// If the player is in a guild, this will contain the name
371    pub guild: Option<String>,
372    /// The time at which this player joined their guild, if any
373    #[deprecated = "v29.500 overhauled the parsing of normal & other players. \
374                    This field is not longer available in the new data. As \
375                    such, this field may become unavailable at any point, \
376                    once the old data is on longer served by the server"]
377    pub guild_joined: Option<DateTime<Local>>,
378    /// The mount the player currently ahs rented
379    pub mount: Option<Mount>,
380    /// The time at which the others mount will expire
381    pub mount_end: Option<DateTime<Local>>,
382    /// Information about the players visual apperarence
383    pub portrait: Portrait,
384    /// The relation the own character has set towards this player
385    pub relationship: Relationship,
386    /// The level their fortress wall would have in combat
387    pub wall_combat_lvl: u16,
388    /// The equipment this player is currently wearing
389    pub equipment: Equipment,
390
391    pub experience: u64,
392    pub next_level_xp: u64,
393
394    pub honor: u32,
395    pub rank: u32,
396    /// The hp bonus in percent this player has from the personal demon portal
397    pub portal_hp_bonus: u32,
398    /// The damage bonus in percent this player has from the guild demon portal
399    pub portal_dmg_bonus: u32,
400    /// The base level of attributes, if no armor & other bonuses are
401    /// considered
402    pub attribute_basis: EnumMap<AttributeType, u32>,
403    /// The amount of bonus attribuets from equipment & other things
404    pub attribute_additions: EnumMap<AttributeType, u32>,
405    /// The amount of times the player has manually bought an attribute
406    pub attribute_times_bought: EnumMap<AttributeType, u32>,
407    /// The bonus to attributes from pets
408    pub attribute_pet_bonus: EnumMap<AttributeType, u32>,
409    /// The class of this player
410    pub class: Class,
411    /// The race this player is of
412    pub race: Race,
413    /// None if they do not have a scrapbook
414    pub scrapbook_count: Option<u32>,
415    /// The potions this player has currently equipped
416    pub active_potions: [Option<Potion>; 3],
417    /// The total amount of armor
418    pub armor: u64,
419    /// The minimum base damage (from their weapon)
420    pub min_damage: u32,
421    /// The maximum base damage (from their weapon)
422    pub max_damage: u32,
423    /// All available data about their fortress, if any
424    pub fortress: Option<OtherFortress>,
425    /// The level of their gladiator in the underworld
426    pub gladiator_lvl: u32,
427    /// Is the player considered to be a VIP by the game
428    pub is_vip: bool,
429}
430
431#[derive(Debug, Default, Clone)]
432#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
433pub struct OtherFortress {
434    /// The total amount of upgrades this player has for their fortress
435    pub upgrade_count: u32,
436    /// The amount of soldiers suggested to use when attacking this players
437    /// fortress
438    pub soldier_advice: u16,
439    /// The amount of stone we are expected to gain from raiding this players
440    /// fortress
441    pub lootable_wood: u64,
442    /// The amount of stone we are expected to gain from raiding this players
443    /// fortress
444    pub lootable_stone: u64,
445    /// The amount of archers defending this players fortress
446    pub archer_count: u16,
447    /// The amount of mages defending this players fortress
448    pub mage_count: u16,
449    /// The rank this player has achieved in the fortress
450    pub rank: u32,
451}
452
453#[derive(Debug, Default, Clone, FromPrimitive, Copy, PartialEq)]
454#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
455pub enum Relationship {
456    #[default]
457    Ignored = -1,
458    Normal = 0,
459    Friend = 1,
460}
461
462impl OtherPlayer {
463    pub(crate) fn update_pet_bonus(
464        &mut self,
465        data: &[u32],
466    ) -> Result<(), SFError> {
467        let atr = &mut self.attribute_pet_bonus;
468        // The order of these makes no sense. It is neither pet,
469        // nor attribute order
470        *atr.get_mut(AttributeType::Constitution) = data.cget(1, "pet con")?;
471        *atr.get_mut(AttributeType::Dexterity) = data.cget(2, "pet dex")?;
472        *atr.get_mut(AttributeType::Intelligence) = data.cget(3, "pet int")?;
473        *atr.get_mut(AttributeType::Luck) = data.cget(4, "pet luck")?;
474        *atr.get_mut(AttributeType::Strength) = data.cget(5, "pet str")?;
475        Ok(())
476    }
477
478    pub(crate) fn update_fortress(
479        &mut self,
480        data: &[i64],
481    ) -> Result<(), SFError> {
482        let ft = self.fortress.get_or_insert_default();
483        ft.upgrade_count = data.csiget(0, "other ft upgrades", 0)?;
484        ft.soldier_advice = data.csiget(1, "other soldier advice", 0)?;
485        ft.mage_count = data.csiget(2, "other mage count", 0)?;
486        ft.archer_count = data.csiget(3, "other soldier advice", 0)?;
487        ft.lootable_wood = data.csiget(4, "other lootable wood", 0)?;
488        ft.lootable_stone = data.csiget(5, "other lootable stone", 0)?;
489        Ok(())
490    }
491
492    pub(crate) fn update(
493        &mut self,
494        data: &[i64],
495        server_time: ServerTime,
496    ) -> Result<(), SFError> {
497        // 0
498        self.player_id = data.csiget(1, "player id", 0)?;
499        // 0
500        self.level = data.csimget(3, "level", 0, |a| a & 0xFFFF)?;
501        self.experience = data.csiget(4, "experience", 0)?;
502        self.next_level_xp = data.csiget(5, "xp to next lvl", 0)?;
503        self.honor = data.csiget(6, "honor", 0)?;
504        self.rank = data.csiget(7, "rank", 0)?;
505        self.portrait =
506            Portrait::parse(data.skip(8, "portrait")?).unwrap_or_default();
507        //////// portrait
508        // 4
509        // 206
510        // 203
511        // 2
512        // 0
513        // 2
514        // 7
515        // 2
516        // 0
517        // 0
518        self.race = data.cfpuget(18, "char race", |a| a)?;
519        // 2
520        //////
521        self.class = data.cfpuget(20, "character class", |a| a - 1)?;
522        self.mount = data.cfpget(21, "character mount", |a| a & 0xFF)?;
523        // 3
524        // 0
525        self.armor = data.csiget(23, "total armor", 0)?;
526        self.min_damage = data.csiget(24, "min damage", 0)?;
527        self.max_damage = data.csiget(25, "max damage", 0)?;
528        self.portal_dmg_bonus = data.cimget(26, "portal dmg bonus", |a| a)?;
529        // 4280492      // ???
530        self.portal_hp_bonus = data.csimget(28, "portal hp bonus", 0, |a| a)?;
531        self.mount_end = data.cstget(29, "mount end", server_time)?;
532        update_enum_map(
533            &mut self.attribute_basis,
534            data.skip(30, "char attr basis")?,
535        );
536        update_enum_map(
537            &mut self.attribute_additions,
538            data.skip(35, "char attr adds")?,
539        );
540        update_enum_map(
541            &mut self.attribute_times_bought,
542            data.skip(40, "char attr tb")?,
543        );
544        // 0
545        // 0
546        // 0
547        // 0
548        // 0
549        // 17
550        // 0
551        // 0
552        // 0
553        // 66
554        // 0
555        // 7
556        // 18
557        // 0
558        // 0
559        // 0
560        // 0
561        // 0
562        // 0
563        // 0
564
565        // 80315 // guild id
566        let sb_count = data.cget(66, "scrapbook count")?;
567        if sb_count >= 10000 {
568            self.scrapbook_count =
569                Some(soft_into(sb_count - 10000, "scrapbook count", 0));
570        }
571        // 0
572        // 31
573        self.gladiator_lvl = data.csiget(69, "gladiator lvl", 0)?;
574
575        Ok(())
576    }
577}
578
579#[derive(Debug, Clone, FromPrimitive)]
580#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
581pub enum CombatMessageType {
582    Arena = 0,
583    Quest = 1,
584    GuildFight = 2,
585    GuildRaid = 3,
586    Dungeon = 4,
587    TowerFight = 5,
588    LostFight = 6,
589    WonFight = 7,
590    FortressFight = 8,
591    FortressDefense = 9,
592    ShadowWorld = 12,
593    FortressDefenseAlreadyCountered = 109,
594    PetAttack = 14,
595    PetDefense = 15,
596    Underworld = 16,
597    Twister = 25,
598    GuildFightLost = 26,
599    GuildFightWon = 27,
600}
601
602#[derive(Debug, Clone, FromPrimitive)]
603#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
604pub enum MessageType {
605    Normal,
606    GuildInvite,
607    GuildKicked,
608}
609
610#[derive(Debug, Clone)]
611#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
612pub struct CombatLogEntry {
613    pub msg_id: i64,
614    pub player_name: String,
615    pub won: bool,
616    pub battle_type: CombatMessageType,
617    pub time: DateTime<Local>,
618}
619
620impl CombatLogEntry {
621    pub(crate) fn parse(
622        data: &[&str],
623        server_time: ServerTime,
624    ) -> Result<CombatLogEntry, SFError> {
625        let msg_id = data.cfsuget(0, "combat msg_id")?;
626        let battle_t: i64 = data.cfsuget(3, "battle t")?;
627        let time_stamp: i64 = data.cfsuget(4, "combat log time")?;
628        let time = server_time
629            .convert_to_local(time_stamp, "combat time")
630            .ok_or_else(|| {
631                SFError::ParsingError("combat time", time_stamp.to_string())
632            })?;
633
634        let mt = FromPrimitive::from_i64(battle_t).ok_or_else(|| {
635            SFError::ParsingError("combat mt", format!("{battle_t} @ {time:?}"))
636        })?;
637
638        Ok(CombatLogEntry {
639            msg_id,
640            player_name: data.cget(1, "clog player")?.to_string(),
641            won: data.cget(2, "clog won")? == "1",
642            battle_type: mt,
643            time,
644        })
645    }
646}
647
648#[derive(Debug, Clone)]
649#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
650pub struct InboxEntry {
651    pub msg_typ: MessageType,
652    pub from: String,
653    pub msg_id: i32,
654    pub title: String,
655    pub date: DateTime<Local>,
656    pub read: bool,
657}
658
659impl InboxEntry {
660    pub(crate) fn parse(
661        msg: &str,
662        server_time: ServerTime,
663    ) -> Result<InboxEntry, SFError> {
664        let parts = msg.splitn(4, ',').collect::<Vec<_>>();
665        let Some((title, date)) =
666            parts.cget(3, "msg title/date")?.rsplit_once(',')
667        else {
668            return Err(SFError::ParsingError(
669                "title/msg comma",
670                msg.to_string(),
671            ));
672        };
673
674        let msg_typ = match title {
675            "3" => MessageType::GuildKicked,
676            "5" => MessageType::GuildInvite,
677            x if x.chars().all(|a| a.is_ascii_digit()) => {
678                return Err(SFError::ParsingError(
679                    "msg typ",
680                    title.to_string(),
681                ));
682            }
683            _ => MessageType::Normal,
684        };
685
686        let Some(date) = date
687            .parse()
688            .ok()
689            .and_then(|a| server_time.convert_to_local(a, "msg_date"))
690        else {
691            return Err(SFError::ParsingError("msg date", date.to_string()));
692        };
693
694        Ok(InboxEntry {
695            msg_typ,
696            date,
697            from: parts.cget(1, "inbox from")?.to_string(),
698            msg_id: parts.cfsuget(0, "msg_id")?,
699            title: from_sf_string(title.trim_end_matches('\t')),
700            read: parts.cget(2, "inbox read")? == "1",
701        })
702    }
703}
704
705#[derive(Debug, Clone, Default)]
706#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
707pub struct OtherGuild {
708    pub name: String,
709
710    pub attacks: Option<String>,
711    pub defends_against: Option<String>,
712
713    pub rank: u16,
714    pub attack_cost: u32,
715    pub description: String,
716    pub emblem: Emblem,
717    pub honor: u32,
718    pub finished_raids: u16,
719    // should just be members.len(), right?
720    member_count: u8,
721    pub members: Vec<OtherGuildMember>,
722}
723
724#[derive(Debug, Clone, Default)]
725#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
726pub struct OtherGuildMember {
727    pub name: String,
728    pub instructor_lvl: u16,
729    pub treasure_lvl: u16,
730    pub rank: GuildRank,
731    pub level: u16,
732    pub pet_lvl: u16,
733    pub last_active: Option<DateTime<Local>>,
734}
735impl OtherGuild {
736    pub(crate) fn update(
737        &mut self,
738        val: &str,
739        server_time: ServerTime,
740    ) -> Result<(), SFError> {
741        let data: Vec<_> = val
742            .split('/')
743            .map(|c| c.trim().parse::<i64>().unwrap_or_default())
744            .collect();
745
746        self.member_count = data.csiget(3, "member count", 0)?;
747        let member_count = self.member_count as usize;
748        self.finished_raids = data.csiget(8, "raid count", 0)?;
749        self.honor = data.csiget(13, "other guild honor", 0)?;
750
751        self.members.resize_with(member_count, Default::default);
752
753        for (i, member) in &mut self.members.iter_mut().enumerate() {
754            member.level =
755                data.csiget(64 + i, "other guild member level", 0)?;
756            member.last_active =
757                data.cstget(114 + i, "other guild member active", server_time)?;
758            member.treasure_lvl =
759                data.csiget(214 + i, "other guild member treasure levels", 0)?;
760            member.instructor_lvl = data.csiget(
761                264 + i,
762                "other guild member instructor levels",
763                0,
764            )?;
765            member.rank = data
766                .cfpget(314 + i, "other guild member ranks", |q| q)?
767                .unwrap_or_default();
768            member.pet_lvl =
769                data.csiget(390 + i, "other guild pet levels", 0)?;
770        }
771        Ok(())
772    }
773}
774
775#[derive(Debug, Clone, Default)]
776#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
777pub struct RelationEntry {
778    pub id: PlayerId,
779    pub name: String,
780    pub guild: String,
781    pub level: u16,
782    pub relation: Relationship,
783}
784
785#[derive(Debug, Clone)]
786#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
787pub struct ClaimableMail {
788    pub msg_id: i64,
789    pub typ: ClaimableMailType,
790    pub status: ClaimableStatus,
791    pub name: String,
792    pub received: Option<DateTime<Local>>,
793    pub claimable_until: Option<DateTime<Local>>,
794}
795
796#[derive(Debug, Clone, PartialEq, Eq, Copy)]
797#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
798pub enum ClaimableStatus {
799    Unread,
800    Read,
801    Claimed,
802}
803
804#[derive(Debug, Clone, PartialEq, Eq, Default, FromPrimitive)]
805#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
806pub enum ClaimableMailType {
807    Coupon = 10,
808    SupermanDelivery = 11,
809    TwitchDrop = 12,
810    #[default]
811    GenericDelivery,
812}
813
814#[derive(Debug, Clone, Default)]
815#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
816pub struct ClaimablePreview {
817    pub items: Vec<Item>,
818    pub resources: Vec<Reward>,
819}