Skip to main content

sf_api/gamestate/
guild.rs

1#![allow(clippy::module_name_repetitions)]
2use chrono::{DateTime, Local, NaiveTime};
3use enum_map::{Enum, EnumMap};
4use log::warn;
5use num_derive::FromPrimitive;
6use strum::{EnumIter, IntoEnumIterator};
7
8use super::{
9    ArrSkip, AttributeType, CCGet, CFPGet, CGet, CSTGet, NormalCost, Potion,
10    SFError, ServerTime,
11    items::{ItemType, PotionSize, PotionType},
12    update_enum_map,
13};
14use crate::misc::{from_sf_string, soft_into, warning_parse};
15
16/// Information about the characters current guild
17#[derive(Debug, Clone, Default)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub struct Guild {
20    /// The internal server id of this guild
21    pub id: u32,
22    /// The name of the guild
23    pub name: String,
24    /// The description text of the guild
25    pub description: String,
26    /// This is guilds emblem. Currently this is unparsed, so you only have
27    /// access to the raw string
28    pub emblem: Emblem,
29
30    /// The honor this guild has earned
31    pub honor: u32,
32    /// The rank in the Hall of Fame this guild has
33    pub rank: u32,
34    /// The date at which the character joined this guild
35    pub joined: Option<DateTime<Local>>,
36
37    /// The skill you yourself contribute to the guild
38    pub own_treasure_skill: u16,
39    /// The total amount of treasure skill the guild has
40    pub total_treasure_skill: u16,
41    /// The skill you yourself contribute to the guild
42    pub own_instructor_skill: u16,
43    /// The total amount of instructor skill the guild has
44    pub total_instructor_skill: u16,
45
46    /// The price to pay to upgrade the given skill
47    pub upgrade_price: EnumMap<GuildSkill, NormalCost>,
48
49    /// How many raids this guild has completed already
50    pub finished_raids: u16,
51
52    /// If the guild is defending against another guild, this will contain
53    /// information about the upcoming battle
54    pub defending: Option<PlanedBattle>,
55    /// If the guild is attacking another guild, this will contain
56    /// information about the upcoming battle
57    pub attacking: Option<PlanedBattle>,
58    /// The next time the guild can attack another guild.
59    pub next_attack_possible: Option<DateTime<Local>>,
60
61    /// The id of the pet, that is currently selected as the guild pet
62    pub pet_id: u32,
63    /// The level of your guild pet
64    pub own_pet_lvl: u16,
65    /// The level of your guild pet
66    /// The maximum level, that the pet can be at
67    pub pet_max_lvl: u16,
68    /// All information about the hydra the guild pet can fight
69    pub hydra: GuildHydra,
70    /// The thing each player can enter and fight once a day
71    pub portal: GuildPortal,
72
73    // This should just be members.len(). I think this is only in the API
74    // because they are bad at varsize arrays or smth.
75    member_count: u8,
76    /// Information about the members of the guild. This includes the player
77    pub members: Vec<GuildMemberData>,
78    /// The chat messages, that get sent in the guild chat
79    pub chat: Vec<ChatMessage>,
80    /// The whisper messages, that a player can receive
81    pub whispers: Vec<ChatMessage>,
82    /// A list of guilds which can be fought, must first be fetched by sending
83    /// `Command::GuildGetFightableTargets`
84    pub fightable_guilds: Vec<FightableGuild>,
85}
86
87/// The hydra, that the guild pet can fight
88#[derive(Debug, Clone, Default)]
89#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
90pub struct GuildHydra {
91    /// The last time the hydra has been fought
92    pub last_battle: Option<DateTime<Local>>,
93    /// The last time the hydra has been seen with full health
94    pub last_full: Option<DateTime<Local>>,
95    /// This seems to be `last_battle + 30 min`. I can only do 1 battle/day,
96    /// but I think this should be the next possible fight
97    pub next_battle: Option<DateTime<Local>>,
98    /// The amount of times the player can still fight the hydra
99    pub remaining_fights: u16,
100    /// The current life of the guilds hydra
101    pub current_life: u64,
102    /// The maximum life the hydra can have
103    pub max_life: u64,
104    /// The attributes the hydra has
105    pub attributes: EnumMap<AttributeType, u32>,
106}
107
108/// Contains information about another guild which can be fought.
109/// Must first be fetched by sending `Command::GuildGetFightableTargets`
110#[derive(Debug, Clone, PartialEq, Default)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub struct FightableGuild {
113    /// Id of the guild
114    pub id: u32,
115    /// Name of the guild
116    pub name: String,
117    /// Emblem of the guild
118    pub emblem: Emblem,
119    /// Number of members the guild currently has
120    pub number_of_members: u8,
121    /// The lowest level a member of the guild has
122    pub members_min_level: u32,
123    /// The highest level a member of the guild has
124    pub members_max_level: u32,
125    /// The average level of the guild members
126    pub members_average_level: u32,
127    /// The rank of the guild in the hall of fame
128    pub rank: u32,
129    /// The amount of honor the guild currently has
130    pub honor: u32,
131}
132
133/// The customizable emblem each guild has
134#[derive(Debug, Clone, Default, PartialEq)]
135#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
136pub struct Emblem {
137    raw: String,
138}
139
140impl Emblem {
141    /// Returns the guild emblem in its server encoded form
142    #[must_use]
143    pub fn server_encode(&self) -> String {
144        // TODO: Actually parse this
145        self.raw.clone()
146    }
147
148    pub(crate) fn update(&mut self, str: &str) {
149        self.raw.clear();
150        self.raw.push_str(str);
151    }
152}
153
154/// A message, that the player has received, or has sent to others via the chat
155#[derive(Debug, Clone, Default)]
156#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
157pub struct ChatMessage {
158    /// The user this message originated from. Note that this might not be in
159    /// the guild member list in some cases
160    pub user: String,
161    /// The time at which this message has been sent. I have not checked the
162    /// timezone here. Might be UTC/Your TZ/Server TZ
163    pub time: NaiveTime,
164    /// The actual message, that got sent
165    pub message: String,
166}
167
168impl ChatMessage {
169    pub(crate) fn parse_messages(data: &str) -> Vec<ChatMessage> {
170        data.split('/')
171            .filter_map(|msg| {
172                let (time, rest) = msg.split_once(' ')?;
173                let (name, msg) = rest.split_once(':')?;
174                let msg = from_sf_string(msg.trim_start_matches(['§', ' ']));
175                let time = NaiveTime::parse_from_str(time, "%H:%M").ok()?;
176                Some(ChatMessage {
177                    user: name.to_string(),
178                    time,
179                    message: msg,
180                })
181            })
182            .collect()
183    }
184}
185
186impl Guild {
187    pub(crate) fn update_group_save(
188        &mut self,
189        val: &str,
190        server_time: ServerTime,
191    ) -> Result<(), SFError> {
192        let data: Vec<_> = val
193            .split('/')
194            .map(|c| c.trim().parse::<i64>().unwrap_or_default())
195            .collect();
196
197        let member_count = data.csiget(3, "member count", 0)?;
198        self.member_count = member_count;
199        self.members
200            .resize_with(member_count as usize, Default::default);
201
202        for (offset, member) in self.members.iter_mut().enumerate() {
203            member.battles_joined =
204                data.cfpget(445 + offset, "member fights joined", |x| x % 100)?;
205            member.level = data.csiget(64 + offset, "member level", 0)?;
206            member.last_online =
207                data.cstget(114 + offset, "member last online", server_time)?;
208            member.treasure_skill =
209                data.csiget(214 + offset, "member treasure skill", 0)?;
210            member.instructor_skill =
211                data.csiget(264 + offset, "member master skill", 0)?;
212            member.guild_rank = match data.cget(314 + offset, "member rank")? {
213                1 => GuildRank::Leader,
214                2 => GuildRank::Officer,
215                3 => GuildRank::Member,
216                4 => GuildRank::Invited,
217                x => {
218                    warn!("Unknown guild rank: {x}");
219                    GuildRank::Invited
220                }
221            };
222            member.portal_fought =
223                data.cstget(164 + offset, "member portal fought", server_time)?;
224            member.guild_pet_lvl =
225                data.csiget(390 + offset, "member pet skill", 0)?;
226        }
227
228        self.honor = data.csiget(13, "guild honor", 0)?;
229        self.id = data.csiget(0, "guild id", 0)?;
230
231        self.finished_raids = data.csiget(8, "finished raids", 0)?;
232
233        self.attacking = PlanedBattle::parse(
234            data.skip(364, "attacking guild")?,
235            server_time,
236        )?;
237
238        self.defending = PlanedBattle::parse(
239            data.skip(366, "attacking guild")?,
240            server_time,
241        )?;
242
243        self.next_attack_possible =
244            data.cstget(365, "guild next attack time", server_time)?;
245
246        self.pet_id = data.csiget(377, "gpet id", 0)?;
247        self.pet_max_lvl = data.csiget(378, "gpet max lvl", 0)?;
248
249        self.hydra.last_battle =
250            data.cstget(382, "hydra pet lb", server_time)?;
251        self.hydra.last_full =
252            data.cstget(381, "hydra last defeat", server_time)?;
253
254        self.hydra.current_life = data.csiget(383, "ghydra clife", u64::MAX)?;
255        self.hydra.max_life = data.csiget(384, "ghydra max clife", u64::MAX)?;
256
257        update_enum_map(
258            &mut self.hydra.attributes,
259            data.skip(385, "hydra attributes")?,
260        );
261
262        self.total_treasure_skill =
263            data.csimget(6, "guild total treasure skill", 0, |x| x & 0xFFFF)?;
264        self.total_instructor_skill =
265            data.csimget(7, "guild total instructor skill", 0, |x| x & 0xFFFF)?;
266
267        self.portal.life_percentage =
268            data.csimget(6, "guild portal life p", 100, |x| x >> 16)?;
269        self.portal.defeated_count =
270            data.csimget(7, "guild portal progress", 0, |x| x >> 16)?;
271
272        Ok(())
273    }
274
275    pub(crate) fn update_member_names(&mut self, val: &str) {
276        let names: Vec<_> = val
277            .split(',')
278            .map(std::string::ToString::to_string)
279            .collect();
280        self.members.resize_with(names.len(), Default::default);
281        for (member, name) in self.members.iter_mut().zip(names) {
282            member.name = name;
283        }
284    }
285
286    pub(crate) fn update_group_knights(&mut self, val: &str) {
287        let data: Vec<i64> = val
288            .trim_end_matches(',')
289            .split(',')
290            .flat_map(str::parse)
291            .collect();
292
293        self.members.resize_with(data.len(), Default::default);
294        for (member, count) in self.members.iter_mut().zip(data) {
295            member.knights = soft_into(count, "guild knight", 0);
296        }
297    }
298
299    pub(crate) fn update_member_potions(&mut self, val: &str) {
300        let data = val
301            .trim_end_matches(',')
302            .split(',')
303            .map(|c| {
304                warning_parse(c, "member potion", |a| a.parse::<i64>().ok())
305                    .unwrap_or_default()
306            })
307            .collect::<Vec<_>>();
308
309        let potions = data.len() / 2;
310        let member = potions / 3;
311        self.members.resize_with(member, Default::default);
312
313        let mut data = data.into_iter();
314
315        let quick_potion = |int: i64| {
316            Some(ItemType::Potion(Potion {
317                typ: PotionType::parse(int)?,
318                size: PotionSize::parse(int)?,
319                expires: None,
320            }))
321        };
322
323        for member in &mut self.members {
324            for potion in &mut member.potions {
325                *potion = data
326                    .next()
327                    .or_else(|| {
328                        warn!("Invalid member potion len");
329                        None
330                    })
331                    .and_then(quick_potion);
332                _ = data.next();
333            }
334        }
335    }
336
337    pub(crate) fn update_description_embed(&mut self, data: &str) {
338        let Some((emblem, description)) = data.split_once('§') else {
339            self.description = from_sf_string(data);
340            return;
341        };
342
343        self.description = from_sf_string(description);
344        self.emblem.update(emblem);
345    }
346
347    pub(crate) fn update_group_prices(
348        &mut self,
349        data: &[i64],
350    ) -> Result<(), SFError> {
351        for (idx, skill) in GuildSkill::iter().enumerate() {
352            let skill = &mut self.upgrade_price[skill];
353            skill.silver =
354                data.csiget(idx * 2, "guild upgr. silver", u64::MAX)?;
355            skill.mushrooms =
356                data.csiget(1 + idx * 2, "guild upgr. mush", u16::MAX)?;
357        }
358        Ok(())
359    }
360
361    #[allow(clippy::indexing_slicing)]
362    pub(crate) fn update_fightable_targets(
363        &mut self,
364        data: &str,
365    ) -> Result<(), SFError> {
366        const SIZE: usize = 9;
367
368        // Delete any old data
369        self.fightable_guilds.clear();
370
371        let entries = data.trim_end_matches('/').split('/').collect::<Vec<_>>();
372
373        let target_counts = entries.len() / SIZE;
374
375        // Check if the data is valid
376        if target_counts * SIZE != entries.len() {
377            warn!("Invalid fightable targets len");
378            return Err(SFError::ParsingError(
379                "Fightable targets invalid length",
380                data.to_string(),
381            ));
382        }
383
384        // Reserve space for the new data
385        self.fightable_guilds.reserve(entries.len() / SIZE);
386
387        for i in 0..entries.len() / SIZE {
388            let offset = i * SIZE;
389
390            self.fightable_guilds.push(FightableGuild {
391                id: entries[offset].parse().unwrap_or_default(),
392                name: from_sf_string(entries[offset + 1]),
393                emblem: Emblem {
394                    raw: entries[offset + 2].to_string(),
395                },
396                number_of_members: entries[offset + 3]
397                    .parse()
398                    .unwrap_or_default(),
399                members_min_level: entries[offset + 4]
400                    .parse()
401                    .unwrap_or_default(),
402                members_max_level: entries[offset + 5]
403                    .parse()
404                    .unwrap_or_default(),
405                members_average_level: entries[offset + 6]
406                    .parse()
407                    .unwrap_or_default(),
408                rank: entries[offset + 7].parse().unwrap_or_default(),
409                honor: entries[offset + 8].parse().unwrap_or_default(),
410            });
411        }
412
413        Ok(())
414    }
415}
416
417/// A guild battle, that is scheduled to take place at a certain place and time
418#[derive(Debug, Default, Clone)]
419#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
420pub struct PlanedBattle {
421    /// The guild this battle will be against
422    pub other: u32,
423    /// The date & time this battle will be at
424    pub date: DateTime<Local>,
425}
426
427impl PlanedBattle {
428    /// Checks if the battle is a raid
429    #[must_use]
430    pub fn is_raid(&self) -> bool {
431        self.other == 1_000_000
432    }
433
434    #[allow(clippy::similar_names)]
435    fn parse(
436        data: &[i64],
437        server_time: ServerTime,
438    ) -> Result<Option<Self>, SFError> {
439        let other = data.cget(0, "gbattle other")?;
440        let other = match other.try_into() {
441            Ok(x) if x > 1 => Some(x),
442            _ => None,
443        };
444        let date = data.cget(1, "gbattle time")?;
445        let date = server_time.convert_to_local(date, "next guild fight");
446        Ok(match (other, date) {
447            (Some(other), Some(date)) => Some(Self { other, date }),
448            _ => None,
449        })
450    }
451}
452
453/// The portal a guild has
454#[derive(Debug, Default, Clone)]
455#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
456pub struct GuildPortal {
457    /// The damage bonus in percent the guild portal gives to its members
458    pub damage_bonus: u8,
459    /// The amount of times the portal enemy has already been defeated. You can
460    /// easily convert this int oct & stage if you want
461    pub defeated_count: u8,
462    /// The percentage of life the portal enemy still has
463    pub life_percentage: u8,
464}
465
466/// Which battles a member will participate in
467#[derive(Debug, Copy, Clone, FromPrimitive)]
468#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
469pub enum BattlesJoined {
470    /// The player has only joined the defense of the guild
471    Defense = 1,
472    /// The player has only joined the offensive attack against another guild
473    Attack = 10,
474    /// The player has only joined both the offense and defensive battles of
475    /// the guild
476    Both = 11,
477}
478
479/// A member of a guild
480#[derive(Debug, Clone, Default)]
481#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
482pub struct GuildMemberData {
483    /// The name of the member
484    pub name: String,
485    /// Which battles this member will participate in
486    pub battles_joined: Option<BattlesJoined>,
487    /// The level of this member
488    pub level: u16,
489    /// The last time this player was online (last time they sent an update
490    /// command)
491    pub last_online: Option<DateTime<Local>>,
492    /// The level, that this member has upgraded their treasure to
493    pub treasure_skill: u16,
494    /// The level, that this member has upgraded their instructor to
495    pub instructor_skill: u16,
496    /// The level of this members guild pet
497    pub guild_pet_lvl: u16,
498
499    /// The rank this member has in the guild
500    pub guild_rank: GuildRank,
501    /// The last time this member has fought the portal. This is basically a
502    /// dynamic check if they have fought it today, because today changes
503    pub portal_fought: Option<DateTime<Local>>,
504    /// The potions this player has active. This will always be potion, no
505    /// other item type
506    // TODO: make this explicit
507    pub potions: [Option<ItemType>; 3],
508    /// The level of this members hall of knights
509    pub knights: u8,
510}
511
512/// The rank a member can have in a guild
513#[derive(Debug, Clone, Copy, FromPrimitive, Default)]
514#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
515#[allow(missing_docs)]
516pub enum GuildRank {
517    Leader = 1,
518    Officer = 2,
519    #[default]
520    Member = 3,
521    Invited = 4,
522}
523
524/// Something the player can upgrade in the guild
525#[derive(Debug, Clone, Copy, PartialEq, Enum, Eq, EnumIter)]
526#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
527#[allow(missing_docs)]
528pub enum GuildSkill {
529    Treasure = 0,
530    Instructor,
531    Pet,
532}