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 level of the fortifications of that player
438    pub fortifications_level: 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.fortifications_level =
485            data.csiget(1, "other soldier fortifications", 0)?;
486        ft.archer_count = data.csiget(2, "other mage count", 0)?;
487        ft.mage_count = data.csiget(3, "other soldier advice", 0)?;
488        ft.lootable_wood = data.csiget(4, "other lootable wood", 0)?;
489        ft.lootable_stone = data.csiget(5, "other lootable stone", 0)?;
490        Ok(())
491    }
492
493    pub(crate) fn update(
494        &mut self,
495        data: &[i64],
496        server_time: ServerTime,
497    ) -> Result<(), SFError> {
498        // 0
499        self.player_id = data.csiget(1, "player id", 0)?;
500        // 0
501        self.level = data.csimget(3, "level", 0, |a| a & 0xFFFF)?;
502        self.experience = data.csiget(4, "experience", 0)?;
503        self.next_level_xp = data.csiget(5, "xp to next lvl", 0)?;
504        self.honor = data.csiget(6, "honor", 0)?;
505        self.rank = data.csiget(7, "rank", 0)?;
506        self.portrait =
507            Portrait::parse(data.skip(8, "portrait")?).unwrap_or_default();
508        //////// portrait
509        // 4
510        // 206
511        // 203
512        // 2
513        // 0
514        // 2
515        // 7
516        // 2
517        // 0
518        // 0
519        self.race = data.cfpuget(18, "char race", |a| a)?;
520        // 2
521        //////
522        self.class = data.cfpuget(20, "character class", |a| a - 1)?;
523        self.mount = data.cfpget(21, "character mount", |a| a & 0xFF)?;
524        // 3
525        // 0
526        self.armor = data.csiget(23, "total armor", 0)?;
527        self.min_damage = data.csiget(24, "min damage", 0)?;
528        self.max_damage = data.csiget(25, "max damage", 0)?;
529        self.portal_dmg_bonus = data.cimget(26, "portal dmg bonus", |a| a)?;
530        // 4280492      // ???
531        self.portal_hp_bonus = data.csimget(28, "portal hp bonus", 0, |a| a)?;
532        self.mount_end = data.cstget(29, "mount end", server_time)?;
533        update_enum_map(
534            &mut self.attribute_basis,
535            data.skip(30, "char attr basis")?,
536        );
537        update_enum_map(
538            &mut self.attribute_additions,
539            data.skip(35, "char attr adds")?,
540        );
541        update_enum_map(
542            &mut self.attribute_times_bought,
543            data.skip(40, "char attr tb")?,
544        );
545        // 0
546        // 0
547        // 0
548        // 0
549        // 0
550        // 17
551        // 0
552        // 0
553        // 0
554        // 66
555        // 0
556        // 7
557        // 18
558        // 0
559        // 0
560        // 0
561        // 0
562        // 0
563        // 0
564        // 0
565
566        // 80315 // guild id
567        let sb_count = data.cget(66, "scrapbook count")?;
568        if sb_count >= 10000 {
569            self.scrapbook_count =
570                Some(soft_into(sb_count - 10000, "scrapbook count", 0));
571        }
572        // 0
573        // 31
574        self.gladiator_lvl = data.csiget(69, "gladiator lvl", 0)?;
575
576        Ok(())
577    }
578}
579
580#[derive(Debug, Clone, FromPrimitive)]
581#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
582pub enum CombatMessageType {
583    Arena = 0,
584    Quest = 1,
585    GuildFight = 2,
586    GuildRaid = 3,
587    Dungeon = 4,
588    TowerFight = 5,
589    LostFight = 6,
590    WonFight = 7,
591    FortressFight = 8,
592    FortressDefense = 9,
593    ShadowWorld = 12,
594    FortressDefenseAlreadyCountered = 109,
595    PetAttack = 14,
596    PetDefense = 15,
597    Underworld = 16,
598    Twister = 25,
599    GuildFightLost = 26,
600    GuildFightWon = 27,
601}
602
603#[derive(Debug, Clone, FromPrimitive)]
604#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
605pub enum MessageType {
606    Normal,
607    GuildInvite,
608    GuildKicked,
609}
610
611#[derive(Debug, Clone)]
612#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
613pub struct CombatLogEntry {
614    pub msg_id: i64,
615    pub player_name: String,
616    pub won: bool,
617    pub battle_type: CombatMessageType,
618    pub time: DateTime<Local>,
619}
620
621impl CombatLogEntry {
622    pub(crate) fn parse(
623        data: &[&str],
624        server_time: ServerTime,
625    ) -> Result<CombatLogEntry, SFError> {
626        let msg_id = data.cfsuget(0, "combat msg_id")?;
627        let battle_t: i64 = data.cfsuget(3, "battle t")?;
628        let time_stamp: i64 = data.cfsuget(4, "combat log time")?;
629        let time = server_time
630            .convert_to_local(time_stamp, "combat time")
631            .ok_or_else(|| {
632                SFError::ParsingError("combat time", time_stamp.to_string())
633            })?;
634
635        let mt = FromPrimitive::from_i64(battle_t).ok_or_else(|| {
636            SFError::ParsingError("combat mt", format!("{battle_t} @ {time:?}"))
637        })?;
638
639        Ok(CombatLogEntry {
640            msg_id,
641            player_name: data.cget(1, "clog player")?.to_string(),
642            won: data.cget(2, "clog won")? == "1",
643            battle_type: mt,
644            time,
645        })
646    }
647}
648
649#[derive(Debug, Clone)]
650#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
651pub struct InboxEntry {
652    pub msg_typ: MessageType,
653    pub from: String,
654    pub msg_id: i32,
655    pub title: String,
656    pub date: DateTime<Local>,
657    pub read: bool,
658}
659
660impl InboxEntry {
661    pub(crate) fn parse(
662        msg: &str,
663        server_time: ServerTime,
664    ) -> Result<InboxEntry, SFError> {
665        let parts = msg.splitn(4, ',').collect::<Vec<_>>();
666        let Some((title, date)) =
667            parts.cget(3, "msg title/date")?.rsplit_once(',')
668        else {
669            return Err(SFError::ParsingError(
670                "title/msg comma",
671                msg.to_string(),
672            ));
673        };
674
675        let msg_typ = match title {
676            "3" => MessageType::GuildKicked,
677            "5" => MessageType::GuildInvite,
678            x if x.chars().all(|a| a.is_ascii_digit()) => {
679                return Err(SFError::ParsingError(
680                    "msg typ",
681                    title.to_string(),
682                ));
683            }
684            _ => MessageType::Normal,
685        };
686
687        let Some(date) = date
688            .parse()
689            .ok()
690            .and_then(|a| server_time.convert_to_local(a, "msg_date"))
691        else {
692            return Err(SFError::ParsingError("msg date", date.to_string()));
693        };
694
695        Ok(InboxEntry {
696            msg_typ,
697            date,
698            from: parts.cget(1, "inbox from")?.to_string(),
699            msg_id: parts.cfsuget(0, "msg_id")?,
700            title: from_sf_string(title.trim_end_matches('\t')),
701            read: parts.cget(2, "inbox read")? == "1",
702        })
703    }
704}
705
706#[derive(Debug, Clone, Default)]
707#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
708pub struct OtherGuild {
709    pub name: String,
710
711    pub attacks: Option<String>,
712    pub defends_against: Option<String>,
713
714    pub rank: u16,
715    pub attack_cost: u32,
716    pub description: String,
717    pub emblem: Emblem,
718    pub honor: u32,
719    pub finished_raids: u16,
720    // should just be members.len(), right?
721    member_count: u8,
722    pub members: Vec<OtherGuildMember>,
723}
724
725#[derive(Debug, Clone, Default)]
726#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
727pub struct OtherGuildMember {
728    pub name: String,
729    pub instructor_lvl: u16,
730    pub treasure_lvl: u16,
731    pub rank: GuildRank,
732    pub level: u16,
733    pub pet_lvl: u16,
734    pub last_active: Option<DateTime<Local>>,
735}
736impl OtherGuild {
737    pub(crate) fn update(
738        &mut self,
739        val: &str,
740        server_time: ServerTime,
741    ) -> Result<(), SFError> {
742        let data: Vec<_> = val
743            .split('/')
744            .map(|c| c.trim().parse::<i64>().unwrap_or_default())
745            .collect();
746
747        self.member_count = data.csiget(3, "member count", 0)?;
748        let member_count = self.member_count as usize;
749        self.finished_raids = data.csiget(8, "raid count", 0)?;
750        self.honor = data.csiget(13, "other guild honor", 0)?;
751
752        self.members.resize_with(member_count, Default::default);
753
754        for (i, member) in &mut self.members.iter_mut().enumerate() {
755            member.level =
756                data.csiget(64 + i, "other guild member level", 0)?;
757            member.last_active =
758                data.cstget(114 + i, "other guild member active", server_time)?;
759            member.treasure_lvl =
760                data.csiget(214 + i, "other guild member treasure levels", 0)?;
761            member.instructor_lvl = data.csiget(
762                264 + i,
763                "other guild member instructor levels",
764                0,
765            )?;
766            member.rank = data
767                .cfpget(314 + i, "other guild member ranks", |q| q)?
768                .unwrap_or_default();
769            member.pet_lvl =
770                data.csiget(390 + i, "other guild pet levels", 0)?;
771        }
772        Ok(())
773    }
774}
775
776#[derive(Debug, Clone, Default)]
777#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
778pub struct RelationEntry {
779    pub id: PlayerId,
780    pub name: String,
781    pub guild: String,
782    pub level: u16,
783    pub relation: Relationship,
784}
785
786#[derive(Debug, Clone)]
787#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
788pub struct ClaimableMail {
789    pub msg_id: i64,
790    pub typ: ClaimableMailType,
791    pub status: ClaimableStatus,
792    pub name: String,
793    pub received: Option<DateTime<Local>>,
794    pub claimable_until: Option<DateTime<Local>>,
795}
796
797#[derive(Debug, Clone, PartialEq, Eq, Copy)]
798#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
799pub enum ClaimableStatus {
800    Unread,
801    Read,
802    Claimed,
803}
804
805#[derive(Debug, Clone, PartialEq, Eq, Default, FromPrimitive)]
806#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
807pub enum ClaimableMailType {
808    Coupon = 10,
809    SupermanDelivery = 11,
810    TwitchDrop = 12,
811    #[default]
812    GenericDelivery,
813}
814
815#[derive(Debug, Clone, Default)]
816#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
817pub struct ClaimablePreview {
818    pub items: Vec<Item>,
819    pub resources: Vec<Reward>,
820}