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 = "Since server update v30.500, this field is no longer \
374                    available and will be removed from the API in the future"]
375    pub guild_joined: Option<DateTime<Local>>,
376    /// The mount the player currently ahs rented
377    pub mount: Option<Mount>,
378    /// The time at which the others mount will expire
379    pub mount_end: Option<DateTime<Local>>,
380    /// Information about the players visual apperarence
381    pub portrait: Portrait,
382    /// The relation the own character has set towards this player
383    pub relationship: Relationship,
384    /// The level their fortress wall would have in combat
385    pub wall_combat_lvl: u16,
386    /// The equipment this player is currently wearing
387    pub equipment: Equipment,
388
389    pub experience: u64,
390    pub next_level_xp: u64,
391
392    pub honor: u32,
393    pub rank: u32,
394    /// The hp bonus in percent this player has from the personal demon portal
395    pub portal_hp_bonus: u32,
396    /// The damage bonus in percent this player has from the guild demon portal
397    pub portal_dmg_bonus: u32,
398    /// The base level of attributes, if no armor & other bonuses are
399    /// considered
400    pub attribute_basis: EnumMap<AttributeType, u32>,
401    /// The amount of bonus attribuets from equipment & other things
402    pub attribute_additions: EnumMap<AttributeType, u32>,
403    /// The amount of times the player has manually bought an attribute
404    pub attribute_times_bought: EnumMap<AttributeType, u32>,
405    /// The bonus to attributes from pets
406    pub attribute_pet_bonus: EnumMap<AttributeType, u32>,
407    /// The class of this player
408    pub class: Class,
409    /// The race this player is of
410    pub race: Race,
411    /// None if they do not have a scrapbook
412    pub scrapbook_count: Option<u32>,
413    /// The potions this player has currently equipped
414    pub active_potions: [Option<Potion>; 3],
415    /// The total amount of armor
416    pub armor: u64,
417    /// The minimum base damage (from their weapon)
418    pub min_damage: u32,
419    /// The maximum base damage (from their weapon)
420    pub max_damage: u32,
421    /// All available data about their fortress, if any
422    pub fortress: Option<OtherFortress>,
423    /// The level of their gladiator in the underworld
424    pub gladiator_lvl: u32,
425    /// Is the player considered to be a VIP by the game
426    pub is_vip: bool,
427}
428
429#[derive(Debug, Default, Clone)]
430#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
431pub struct OtherFortress {
432    /// The total amount of upgrades this player has for their fortress
433    pub upgrade_count: u32,
434    /// The amount of soldiers suggested to use when attacking this players
435    /// fortress
436    pub soldier_advice: u16,
437    /// The amount of stone we are expected to gain from raiding this players
438    /// fortress
439    pub lootable_wood: u64,
440    /// The amount of stone we are expected to gain from raiding this players
441    /// fortress
442    pub lootable_stone: u64,
443    /// The amount of archers defending this players fortress
444    pub archer_count: u16,
445    /// The amount of mages defending this players fortress
446    pub mage_count: u16,
447    /// The rank this player has achieved in the fortress
448    pub rank: u32,
449}
450
451#[derive(Debug, Default, Clone, FromPrimitive, Copy, PartialEq)]
452#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
453pub enum Relationship {
454    #[default]
455    Ignored = -1,
456    Normal = 0,
457    Friend = 1,
458}
459
460impl OtherPlayer {
461    pub(crate) fn update_pet_bonus(
462        &mut self,
463        data: &[u32],
464    ) -> Result<(), SFError> {
465        let atr = &mut self.attribute_pet_bonus;
466        // The order of these makes no sense. It is neither pet,
467        // nor attribute order
468        *atr.get_mut(AttributeType::Constitution) = data.cget(1, "pet con")?;
469        *atr.get_mut(AttributeType::Dexterity) = data.cget(2, "pet dex")?;
470        *atr.get_mut(AttributeType::Intelligence) = data.cget(3, "pet int")?;
471        *atr.get_mut(AttributeType::Luck) = data.cget(4, "pet luck")?;
472        *atr.get_mut(AttributeType::Strength) = data.cget(5, "pet str")?;
473        Ok(())
474    }
475
476    pub(crate) fn update_fortress(
477        &mut self,
478        data: &[i64],
479    ) -> Result<(), SFError> {
480        let ft = self.fortress.get_or_insert_default();
481        ft.upgrade_count = data.csiget(0, "other ft upgrades", 0)?;
482        ft.soldier_advice = data.csiget(1, "other soldier advice", 0)?;
483        ft.mage_count = data.csiget(2, "other mage count", 0)?;
484        ft.archer_count = data.csiget(3, "other soldier advice", 0)?;
485        ft.lootable_wood = data.csiget(4, "other lootable wood", 0)?;
486        ft.lootable_stone = data.csiget(5, "other lootable stone", 0)?;
487        Ok(())
488    }
489
490    pub(crate) fn update(
491        &mut self,
492        data: &[i64],
493        server_time: ServerTime,
494    ) -> Result<(), SFError> {
495        // 0
496        self.player_id = data.csiget(1, "player id", 0)?;
497        // 0
498        self.level = data.csimget(3, "level", 0, |a| a & 0xFFFF)?;
499        self.experience = data.csiget(4, "experience", 0)?;
500        self.next_level_xp = data.csiget(5, "xp to next lvl", 0)?;
501        self.honor = data.csiget(6, "honor", 0)?;
502        self.rank = data.csiget(7, "rank", 0)?;
503        self.portrait =
504            Portrait::parse(data.skip(8, "portrait")?).unwrap_or_default();
505        //////// portrait
506        // 4
507        // 206
508        // 203
509        // 2
510        // 0
511        // 2
512        // 7
513        // 2
514        // 0
515        // 0
516        self.race = data.cfpuget(18, "char race", |a| a)?;
517        // 2
518        //////
519        self.class = data.cfpuget(20, "character class", |a| a - 1)?;
520        self.mount = data.cfpget(21, "character mount", |a| a & 0xFF)?;
521        // 3
522        // 0
523        self.armor = data.csiget(23, "total armor", 0)?;
524        self.min_damage = data.csiget(24, "min damage", 0)?;
525        self.max_damage = data.csiget(25, "max damage", 0)?;
526        self.portal_dmg_bonus = data.cimget(26, "portal dmg bonus", |a| a)?;
527        // 4280492      // ???
528        self.portal_hp_bonus = data.csimget(28, "portal hp bonus", 0, |a| a)?;
529        self.mount_end = data.cstget(29, "mount end", server_time)?;
530        update_enum_map(
531            &mut self.attribute_basis,
532            data.skip(30, "char attr basis")?,
533        );
534        update_enum_map(
535            &mut self.attribute_additions,
536            data.skip(35, "char attr adds")?,
537        );
538        update_enum_map(
539            &mut self.attribute_times_bought,
540            data.skip(40, "char attr tb")?,
541        );
542        // 0
543        // 0
544        // 0
545        // 0
546        // 0
547        // 17
548        // 0
549        // 0
550        // 0
551        // 66
552        // 0
553        // 7
554        // 18
555        // 0
556        // 0
557        // 0
558        // 0
559        // 0
560        // 0
561        // 0
562
563        // 80315 // guild id
564        let sb_count = data.cget(66, "scrapbook count")?;
565        if sb_count >= 10000 {
566            self.scrapbook_count =
567                Some(soft_into(sb_count - 10000, "scrapbook count", 0));
568        }
569        // 0
570        // 31
571        self.gladiator_lvl = data.csiget(69, "gladiator lvl", 0)?;
572
573        Ok(())
574    }
575}
576
577#[derive(Debug, Clone, FromPrimitive)]
578#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
579pub enum CombatMessageType {
580    Arena = 0,
581    Quest = 1,
582    GuildFight = 2,
583    GuildRaid = 3,
584    Dungeon = 4,
585    TowerFight = 5,
586    LostFight = 6,
587    WonFight = 7,
588    FortressFight = 8,
589    FortressDefense = 9,
590    ShadowWorld = 12,
591    FortressDefenseAlreadyCountered = 109,
592    PetAttack = 14,
593    PetDefense = 15,
594    Underworld = 16,
595    Twister = 25,
596    GuildFightLost = 26,
597    GuildFightWon = 27,
598}
599
600#[derive(Debug, Clone, FromPrimitive)]
601#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
602pub enum MessageType {
603    Normal,
604    GuildInvite,
605    GuildKicked,
606}
607
608#[derive(Debug, Clone)]
609#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
610pub struct CombatLogEntry {
611    pub msg_id: i64,
612    pub player_name: String,
613    pub won: bool,
614    pub battle_type: CombatMessageType,
615    pub time: DateTime<Local>,
616}
617
618impl CombatLogEntry {
619    pub(crate) fn parse(
620        data: &[&str],
621        server_time: ServerTime,
622    ) -> Result<CombatLogEntry, SFError> {
623        let msg_id = data.cfsuget(0, "combat msg_id")?;
624        let battle_t: i64 = data.cfsuget(3, "battle t")?;
625        let time_stamp: i64 = data.cfsuget(4, "combat log time")?;
626        let time = server_time
627            .convert_to_local(time_stamp, "combat time")
628            .ok_or_else(|| {
629                SFError::ParsingError("combat time", time_stamp.to_string())
630            })?;
631
632        let mt = FromPrimitive::from_i64(battle_t).ok_or_else(|| {
633            SFError::ParsingError("combat mt", format!("{battle_t} @ {time:?}"))
634        })?;
635
636        Ok(CombatLogEntry {
637            msg_id,
638            player_name: data.cget(1, "clog player")?.to_string(),
639            won: data.cget(2, "clog won")? == "1",
640            battle_type: mt,
641            time,
642        })
643    }
644}
645
646#[derive(Debug, Clone)]
647#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
648pub struct InboxEntry {
649    pub msg_typ: MessageType,
650    pub from: String,
651    pub msg_id: i32,
652    pub title: String,
653    pub date: DateTime<Local>,
654    pub read: bool,
655}
656
657impl InboxEntry {
658    pub(crate) fn parse(
659        msg: &str,
660        server_time: ServerTime,
661    ) -> Result<InboxEntry, SFError> {
662        let parts = msg.splitn(4, ',').collect::<Vec<_>>();
663        let Some((title, date)) =
664            parts.cget(3, "msg title/date")?.rsplit_once(',')
665        else {
666            return Err(SFError::ParsingError(
667                "title/msg comma",
668                msg.to_string(),
669            ));
670        };
671
672        let msg_typ = match title {
673            "3" => MessageType::GuildKicked,
674            "5" => MessageType::GuildInvite,
675            x if x.chars().all(|a| a.is_ascii_digit()) => {
676                return Err(SFError::ParsingError(
677                    "msg typ",
678                    title.to_string(),
679                ));
680            }
681            _ => MessageType::Normal,
682        };
683
684        let Some(date) = date
685            .parse()
686            .ok()
687            .and_then(|a| server_time.convert_to_local(a, "msg_date"))
688        else {
689            return Err(SFError::ParsingError("msg date", date.to_string()));
690        };
691
692        Ok(InboxEntry {
693            msg_typ,
694            date,
695            from: parts.cget(1, "inbox from")?.to_string(),
696            msg_id: parts.cfsuget(0, "msg_id")?,
697            title: from_sf_string(title.trim_end_matches('\t')),
698            read: parts.cget(2, "inbox read")? == "1",
699        })
700    }
701}
702
703#[derive(Debug, Clone, Default)]
704#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
705pub struct OtherGuild {
706    pub name: String,
707
708    pub attacks: Option<String>,
709    pub defends_against: Option<String>,
710
711    pub rank: u16,
712    pub attack_cost: u32,
713    pub description: String,
714    pub emblem: Emblem,
715    pub honor: u32,
716    pub finished_raids: u16,
717    // should just be members.len(), right?
718    member_count: u8,
719    pub members: Vec<OtherGuildMember>,
720}
721
722#[derive(Debug, Clone, Default)]
723#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
724pub struct OtherGuildMember {
725    pub name: String,
726    pub instructor_lvl: u16,
727    pub treasure_lvl: u16,
728    pub rank: GuildRank,
729    pub level: u16,
730    pub pet_lvl: u16,
731    pub last_active: Option<DateTime<Local>>,
732}
733impl OtherGuild {
734    pub(crate) fn update(
735        &mut self,
736        val: &str,
737        server_time: ServerTime,
738    ) -> Result<(), SFError> {
739        let data: Vec<_> = val
740            .split('/')
741            .map(|c| c.trim().parse::<i64>().unwrap_or_default())
742            .collect();
743
744        self.member_count = data.csiget(3, "member count", 0)?;
745        let member_count = self.member_count as usize;
746        self.finished_raids = data.csiget(8, "raid count", 0)?;
747        self.honor = data.csiget(13, "other guild honor", 0)?;
748
749        self.members.resize_with(member_count, Default::default);
750
751        for (i, member) in &mut self.members.iter_mut().enumerate() {
752            member.level =
753                data.csiget(64 + i, "other guild member level", 0)?;
754            member.last_active =
755                data.cstget(114 + i, "other guild member active", server_time)?;
756            member.treasure_lvl =
757                data.csiget(214 + i, "other guild member treasure levels", 0)?;
758            member.instructor_lvl = data.csiget(
759                264 + i,
760                "other guild member instructor levels",
761                0,
762            )?;
763            member.rank = data
764                .cfpget(314 + i, "other guild member ranks", |q| q)?
765                .unwrap_or_default();
766            member.pet_lvl =
767                data.csiget(390 + i, "other guild pet levels", 0)?;
768        }
769        Ok(())
770    }
771}
772
773#[derive(Debug, Clone, Default)]
774#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
775pub struct RelationEntry {
776    pub id: PlayerId,
777    pub name: String,
778    pub guild: String,
779    pub level: u16,
780    pub relation: Relationship,
781}
782
783#[derive(Debug, Clone)]
784#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
785pub struct ClaimableMail {
786    pub msg_id: i64,
787    pub typ: ClaimableMailType,
788    pub status: ClaimableStatus,
789    pub name: String,
790    pub received: Option<DateTime<Local>>,
791    pub claimable_until: Option<DateTime<Local>>,
792}
793
794#[derive(Debug, Clone, PartialEq, Eq, Copy)]
795#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
796pub enum ClaimableStatus {
797    Unread,
798    Read,
799    Claimed,
800}
801
802#[derive(Debug, Clone, PartialEq, Eq, Default, FromPrimitive)]
803#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
804pub enum ClaimableMailType {
805    Coupon = 10,
806    SupermanDelivery = 11,
807    TwitchDrop = 12,
808    #[default]
809    GenericDelivery,
810}
811
812#[derive(Debug, Clone, Default)]
813#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
814pub struct ClaimablePreview {
815    pub items: Vec<Item>,
816    pub resources: Vec<Reward>,
817}