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