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