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 pub combat_log: Vec<CombatLogEntry>,
23 pub inbox_capacity: u16,
25 pub inbox: Vec<InboxEntry>,
27 pub claimables: Vec<ClaimableMail>,
29 pub open_msg: Option<String>,
32 pub open_claimable: Option<ClaimablePreview>,
35}
36
37#[derive(Debug, Clone, Default)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41pub struct HallOfFames {
42 pub players_total: u32,
44 pub players: Vec<HallOfFamePlayer>,
46
47 pub guilds_total: Option<u32>,
50 pub guilds: Vec<HallOfFameGuild>,
52
53 pub fortresses_total: Option<u32>,
56 pub fortresses: Vec<HallOfFameFortress>,
58
59 pub pets_total: Option<u32>,
62 pub pets: Vec<HallOfFamePets>,
64
65 pub hellevator_total: Option<u32>,
66 pub hellevator: Vec<HallOfFameHellevator>,
67
68 pub underworlds_total: Option<u32>,
71 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#[derive(Debug, Clone, Default)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87pub struct Lookup {
88 players: HashMap<PlayerId, OtherPlayer>,
91 name_to_id: HashMap<String, PlayerId>,
92
93 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 #[must_use]
109 pub fn lookup_pid(&self, pid: PlayerId) -> Option<&OtherPlayer> {
110 self.players.get(&pid)
111 }
112
113 #[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 #[allow(clippy::must_use_unit)]
122 pub fn remove_pid(&mut self, pid: PlayerId) -> Option<OtherPlayer> {
123 self.players.remove(&pid)
124 }
125
126 #[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 pub fn reset_lookups(&mut self) {
135 self.players = HashMap::default();
136 self.name_to_id = HashMap::default();
137 }
138}
139
140#[derive(Debug, Default, Clone)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
144pub struct HallOfFamePlayer {
145 pub rank: u32,
147 pub name: String,
149 pub guild: Option<String>,
152 pub level: u32,
154 pub honor: u32,
156 pub class: Class,
158 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#[derive(Debug, Default, Clone)]
198#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
199pub struct HallOfFameGuild {
200 pub name: String,
202 pub rank: u32,
204 pub leader: String,
206 pub member_count: u32,
208 pub honor: u32,
210 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#[derive(Debug, Default, Clone)]
301#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
302pub struct HallOfFameFortress {
303 pub name: String,
305 pub rank: u32,
307 pub guild: Option<String>,
310 pub upgrade: u32,
312 pub honor: u32,
314}
315
316#[derive(Debug, Default, Clone)]
318#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
319pub struct HallOfFamePets {
320 pub name: String,
322 pub rank: u32,
324 pub guild: Option<String>,
327 pub collected: u32,
329 pub honor: u32,
331 pub unknown: i64,
334}
335
336#[derive(Debug, Default, Clone)]
338#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
339pub struct HallOfFameUnderworld {
340 pub rank: u32,
342 pub name: String,
344 pub guild: Option<String>,
347 pub upgrade: u32,
349 pub honor: u32,
351 pub unknown: i64,
354}
355
356#[derive(Debug, Default, Clone)]
359#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
360pub struct OtherPlayer {
361 pub player_id: PlayerId,
364 pub name: String,
366 pub level: u16,
368 pub description: String,
370 pub guild: Option<String>,
372 #[deprecated = "v29.500 overhauled the parsing of normal & other players. \
374 This field is not longer available in the new data. As \
375 such, this field may become unavailable at any point, \
376 once the old data is on longer served by the server"]
377 pub guild_joined: Option<DateTime<Local>>,
378 pub mount: Option<Mount>,
380 pub mount_end: Option<DateTime<Local>>,
382 pub portrait: Portrait,
384 pub relationship: Relationship,
386 pub wall_combat_lvl: u16,
388 pub equipment: Equipment,
390
391 pub experience: u64,
392 pub next_level_xp: u64,
393
394 pub honor: u32,
395 pub rank: u32,
396 pub portal_hp_bonus: u32,
398 pub portal_dmg_bonus: u32,
400 pub attribute_basis: EnumMap<AttributeType, u32>,
403 pub attribute_additions: EnumMap<AttributeType, u32>,
405 pub attribute_times_bought: EnumMap<AttributeType, u32>,
407 pub attribute_pet_bonus: EnumMap<AttributeType, u32>,
409 pub class: Class,
411 pub race: Race,
413 pub scrapbook_count: Option<u32>,
415 pub active_potions: [Option<Potion>; 3],
417 pub armor: u64,
419 pub min_damage: u32,
421 pub max_damage: u32,
423 pub fortress: Option<OtherFortress>,
425 pub gladiator_lvl: u32,
427 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 pub upgrade_count: u32,
436 pub soldier_advice: u16,
439 pub lootable_wood: u64,
442 pub lootable_stone: u64,
445 pub archer_count: u16,
447 pub mage_count: u16,
449 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 *atr.get_mut(AttributeType::Constitution) = data.cget(1, "pet con")?;
471 *atr.get_mut(AttributeType::Dexterity) = data.cget(2, "pet dex")?;
472 *atr.get_mut(AttributeType::Intelligence) = data.cget(3, "pet int")?;
473 *atr.get_mut(AttributeType::Luck) = data.cget(4, "pet luck")?;
474 *atr.get_mut(AttributeType::Strength) = data.cget(5, "pet str")?;
475 Ok(())
476 }
477
478 pub(crate) fn update_fortress(
479 &mut self,
480 data: &[i64],
481 ) -> Result<(), SFError> {
482 let ft = self.fortress.get_or_insert_default();
483 ft.upgrade_count = data.csiget(0, "other ft upgrades", 0)?;
484 ft.soldier_advice = data.csiget(1, "other soldier advice", 0)?;
485 ft.mage_count = data.csiget(2, "other mage count", 0)?;
486 ft.archer_count = data.csiget(3, "other soldier advice", 0)?;
487 ft.lootable_wood = data.csiget(4, "other lootable wood", 0)?;
488 ft.lootable_stone = data.csiget(5, "other lootable stone", 0)?;
489 Ok(())
490 }
491
492 pub(crate) fn update(
493 &mut self,
494 data: &[i64],
495 server_time: ServerTime,
496 ) -> Result<(), SFError> {
497 self.player_id = data.csiget(1, "player id", 0)?;
499 self.level = data.csimget(3, "level", 0, |a| a & 0xFFFF)?;
501 self.experience = data.csiget(4, "experience", 0)?;
502 self.next_level_xp = data.csiget(5, "xp to next lvl", 0)?;
503 self.honor = data.csiget(6, "honor", 0)?;
504 self.rank = data.csiget(7, "rank", 0)?;
505 self.portrait =
506 Portrait::parse(data.skip(8, "portrait")?).unwrap_or_default();
507 self.race = data.cfpuget(18, "char race", |a| a)?;
519 self.class = data.cfpuget(20, "character class", |a| a - 1)?;
522 self.mount = data.cfpget(21, "character mount", |a| a & 0xFF)?;
523 self.armor = data.csiget(23, "total armor", 0)?;
526 self.min_damage = data.csiget(24, "min damage", 0)?;
527 self.max_damage = data.csiget(25, "max damage", 0)?;
528 self.portal_dmg_bonus = data.cimget(26, "portal dmg bonus", |a| a)?;
529 self.portal_hp_bonus = data.csimget(28, "portal hp bonus", 0, |a| a)?;
531 self.mount_end = data.cstget(29, "mount end", server_time)?;
532 update_enum_map(
533 &mut self.attribute_basis,
534 data.skip(30, "char attr basis")?,
535 );
536 update_enum_map(
537 &mut self.attribute_additions,
538 data.skip(35, "char attr adds")?,
539 );
540 update_enum_map(
541 &mut self.attribute_times_bought,
542 data.skip(40, "char attr tb")?,
543 );
544 let sb_count = data.cget(66, "scrapbook count")?;
567 if sb_count >= 10000 {
568 self.scrapbook_count =
569 Some(soft_into(sb_count - 10000, "scrapbook count", 0));
570 }
571 self.gladiator_lvl = data.csiget(69, "gladiator lvl", 0)?;
574
575 Ok(())
576 }
577}
578
579#[derive(Debug, Clone, FromPrimitive)]
580#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
581pub enum CombatMessageType {
582 Arena = 0,
583 Quest = 1,
584 GuildFight = 2,
585 GuildRaid = 3,
586 Dungeon = 4,
587 TowerFight = 5,
588 LostFight = 6,
589 WonFight = 7,
590 FortressFight = 8,
591 FortressDefense = 9,
592 ShadowWorld = 12,
593 FortressDefenseAlreadyCountered = 109,
594 PetAttack = 14,
595 PetDefense = 15,
596 Underworld = 16,
597 Twister = 25,
598 GuildFightLost = 26,
599 GuildFightWon = 27,
600}
601
602#[derive(Debug, Clone, FromPrimitive)]
603#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
604pub enum MessageType {
605 Normal,
606 GuildInvite,
607 GuildKicked,
608}
609
610#[derive(Debug, Clone)]
611#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
612pub struct CombatLogEntry {
613 pub msg_id: i64,
614 pub player_name: String,
615 pub won: bool,
616 pub battle_type: CombatMessageType,
617 pub time: DateTime<Local>,
618}
619
620impl CombatLogEntry {
621 pub(crate) fn parse(
622 data: &[&str],
623 server_time: ServerTime,
624 ) -> Result<CombatLogEntry, SFError> {
625 let msg_id = data.cfsuget(0, "combat msg_id")?;
626 let battle_t: i64 = data.cfsuget(3, "battle t")?;
627 let time_stamp: i64 = data.cfsuget(4, "combat log time")?;
628 let time = server_time
629 .convert_to_local(time_stamp, "combat time")
630 .ok_or_else(|| {
631 SFError::ParsingError("combat time", time_stamp.to_string())
632 })?;
633
634 let mt = FromPrimitive::from_i64(battle_t).ok_or_else(|| {
635 SFError::ParsingError("combat mt", format!("{battle_t} @ {time:?}"))
636 })?;
637
638 Ok(CombatLogEntry {
639 msg_id,
640 player_name: data.cget(1, "clog player")?.to_string(),
641 won: data.cget(2, "clog won")? == "1",
642 battle_type: mt,
643 time,
644 })
645 }
646}
647
648#[derive(Debug, Clone)]
649#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
650pub struct InboxEntry {
651 pub msg_typ: MessageType,
652 pub from: String,
653 pub msg_id: i32,
654 pub title: String,
655 pub date: DateTime<Local>,
656 pub read: bool,
657}
658
659impl InboxEntry {
660 pub(crate) fn parse(
661 msg: &str,
662 server_time: ServerTime,
663 ) -> Result<InboxEntry, SFError> {
664 let parts = msg.splitn(4, ',').collect::<Vec<_>>();
665 let Some((title, date)) =
666 parts.cget(3, "msg title/date")?.rsplit_once(',')
667 else {
668 return Err(SFError::ParsingError(
669 "title/msg comma",
670 msg.to_string(),
671 ));
672 };
673
674 let msg_typ = match title {
675 "3" => MessageType::GuildKicked,
676 "5" => MessageType::GuildInvite,
677 x if x.chars().all(|a| a.is_ascii_digit()) => {
678 return Err(SFError::ParsingError(
679 "msg typ",
680 title.to_string(),
681 ));
682 }
683 _ => MessageType::Normal,
684 };
685
686 let Some(date) = date
687 .parse()
688 .ok()
689 .and_then(|a| server_time.convert_to_local(a, "msg_date"))
690 else {
691 return Err(SFError::ParsingError("msg date", date.to_string()));
692 };
693
694 Ok(InboxEntry {
695 msg_typ,
696 date,
697 from: parts.cget(1, "inbox from")?.to_string(),
698 msg_id: parts.cfsuget(0, "msg_id")?,
699 title: from_sf_string(title.trim_end_matches('\t')),
700 read: parts.cget(2, "inbox read")? == "1",
701 })
702 }
703}
704
705#[derive(Debug, Clone, Default)]
706#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
707pub struct OtherGuild {
708 pub name: String,
709
710 pub attacks: Option<String>,
711 pub defends_against: Option<String>,
712
713 pub rank: u16,
714 pub attack_cost: u32,
715 pub description: String,
716 pub emblem: Emblem,
717 pub honor: u32,
718 pub finished_raids: u16,
719 member_count: u8,
721 pub members: Vec<OtherGuildMember>,
722}
723
724#[derive(Debug, Clone, Default)]
725#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
726pub struct OtherGuildMember {
727 pub name: String,
728 pub instructor_lvl: u16,
729 pub treasure_lvl: u16,
730 pub rank: GuildRank,
731 pub level: u16,
732 pub pet_lvl: u16,
733 pub last_active: Option<DateTime<Local>>,
734}
735impl OtherGuild {
736 pub(crate) fn update(
737 &mut self,
738 val: &str,
739 server_time: ServerTime,
740 ) -> Result<(), SFError> {
741 let data: Vec<_> = val
742 .split('/')
743 .map(|c| c.trim().parse::<i64>().unwrap_or_default())
744 .collect();
745
746 self.member_count = data.csiget(3, "member count", 0)?;
747 let member_count = self.member_count as usize;
748 self.finished_raids = data.csiget(8, "raid count", 0)?;
749 self.honor = data.csiget(13, "other guild honor", 0)?;
750
751 self.members.resize_with(member_count, Default::default);
752
753 for (i, member) in &mut self.members.iter_mut().enumerate() {
754 member.level =
755 data.csiget(64 + i, "other guild member level", 0)?;
756 member.last_active =
757 data.cstget(114 + i, "other guild member active", server_time)?;
758 member.treasure_lvl =
759 data.csiget(214 + i, "other guild member treasure levels", 0)?;
760 member.instructor_lvl = data.csiget(
761 264 + i,
762 "other guild member instructor levels",
763 0,
764 )?;
765 member.rank = data
766 .cfpget(314 + i, "other guild member ranks", |q| q)?
767 .unwrap_or_default();
768 member.pet_lvl =
769 data.csiget(390 + i, "other guild pet levels", 0)?;
770 }
771 Ok(())
772 }
773}
774
775#[derive(Debug, Clone, Default)]
776#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
777pub struct RelationEntry {
778 pub id: PlayerId,
779 pub name: String,
780 pub guild: String,
781 pub level: u16,
782 pub relation: Relationship,
783}
784
785#[derive(Debug, Clone)]
786#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
787pub struct ClaimableMail {
788 pub msg_id: i64,
789 pub typ: ClaimableMailType,
790 pub status: ClaimableStatus,
791 pub name: String,
792 pub received: Option<DateTime<Local>>,
793 pub claimable_until: Option<DateTime<Local>>,
794}
795
796#[derive(Debug, Clone, PartialEq, Eq, Copy)]
797#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
798pub enum ClaimableStatus {
799 Unread,
800 Read,
801 Claimed,
802}
803
804#[derive(Debug, Clone, PartialEq, Eq, Default, FromPrimitive)]
805#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
806pub enum ClaimableMailType {
807 Coupon = 10,
808 SupermanDelivery = 11,
809 TwitchDrop = 12,
810 #[default]
811 GenericDelivery,
812}
813
814#[derive(Debug, Clone, Default)]
815#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
816pub struct ClaimablePreview {
817 pub items: Vec<Item>,
818 pub resources: Vec<Reward>,
819}