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 pub combat_log: Vec<CombatLogEntry>,
26 pub inbox_capacity: u16,
28 pub inbox: Vec<InboxEntry>,
30 pub claimables: Vec<ClaimableMail>,
32 pub open_msg: Option<String>,
35 pub open_claimable: Option<ClaimablePreview>,
38}
39
40#[derive(Debug, Clone, Default)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub struct HallOfFames {
45 pub players_total: u32,
47 pub players: Vec<HallOfFamePlayer>,
49
50 pub guilds_total: Option<u32>,
53 pub guilds: Vec<HallOfFameGuild>,
55
56 pub fortresses_total: Option<u32>,
59 pub fortresses: Vec<HallOfFameFortress>,
61
62 pub pets_total: Option<u32>,
65 pub pets: Vec<HallOfFamePets>,
67
68 pub hellevator_total: Option<u32>,
69 pub hellevator: Vec<HallOfFameHellevator>,
70
71 pub underworlds_total: Option<u32>,
74 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#[derive(Debug, Clone, Default)]
89#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
90pub struct Lookup {
91 players: HashMap<PlayerId, OtherPlayer>,
94 name_to_id: HashMap<String, PlayerId>,
95
96 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 #[must_use]
112 pub fn lookup_pid(&self, pid: PlayerId) -> Option<&OtherPlayer> {
113 self.players.get(&pid)
114 }
115
116 #[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 #[allow(clippy::must_use_unit)]
125 pub fn remove_pid(&mut self, pid: PlayerId) -> Option<OtherPlayer> {
126 self.players.remove(&pid)
127 }
128
129 #[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 pub fn reset_lookups(&mut self) {
138 self.players = HashMap::default();
139 self.name_to_id = HashMap::default();
140 }
141}
142
143#[derive(Debug, Default, Clone)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
147pub struct HallOfFamePlayer {
148 pub rank: u32,
150 pub name: String,
152 pub guild: Option<String>,
155 pub level: u32,
157 pub honor: u32,
159 pub class: Class,
161 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#[derive(Debug, Default, Clone)]
201#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
202pub struct HallOfFameGuild {
203 pub name: String,
205 pub rank: u32,
207 pub leader: String,
209 pub member_count: u32,
211 pub honor: u32,
213 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#[derive(Debug, Default, Clone)]
304#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
305pub struct HallOfFameFortress {
306 pub name: String,
308 pub rank: u32,
310 pub guild: Option<String>,
313 pub upgrade: u32,
315 pub honor: u32,
317}
318
319#[derive(Debug, Default, Clone)]
321#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
322pub struct HallOfFamePets {
323 pub name: String,
325 pub rank: u32,
327 pub guild: Option<String>,
330 pub collected: u32,
332 pub honor: u32,
334 pub unknown: i64,
337}
338
339#[derive(Debug, Default, Clone)]
341#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
342pub struct HallOfFameUnderworld {
343 pub rank: u32,
345 pub name: String,
347 pub guild: Option<String>,
350 pub upgrade: u32,
352 pub honor: u32,
354 pub unknown: i64,
357}
358
359#[derive(Debug, Default, Clone)]
362#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
363pub struct OtherPlayer {
364 pub player_id: PlayerId,
367 pub name: String,
369 pub level: u16,
371 pub description: String,
373 pub guild: Option<String>,
375 pub mount: Option<Mount>,
377 pub portrait: Portrait,
379 pub relationship: Relationship,
381 pub wall_combat_lvl: u16,
383 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 pub portal_hp_bonus: u32,
394 pub portal_dmg_bonus: u32,
396
397 pub base_attributes: EnumMap<AttributeType, u32>,
398 pub bonus_attributes: EnumMap<AttributeType, u32>,
399 pub pet_attribute_bonus_perc: EnumMap<AttributeType, u32>,
401
402 pub class: Class,
403 pub race: Race,
404
405 pub mirror: Mirror,
406
407 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 *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 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 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}