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#[derive(Debug, Clone, Default)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub struct Guild {
20 pub id: u32,
22 pub name: String,
24 pub description: String,
26 pub emblem: Emblem,
29
30 pub honor: u32,
32 pub rank: u32,
34 pub joined: Option<DateTime<Local>>,
36
37 pub own_treasure_skill: u16,
39 pub total_treasure_skill: u16,
41 pub own_instructor_skill: u16,
43 pub total_instructor_skill: u16,
45
46 pub upgrade_price: EnumMap<GuildSkill, NormalCost>,
48
49 pub finished_raids: u16,
51
52 pub defending: Option<PlanedBattle>,
55 pub attacking: Option<PlanedBattle>,
58 pub next_attack_possible: Option<DateTime<Local>>,
60
61 pub pet_id: u32,
63 pub own_pet_lvl: u16,
65 pub pet_max_lvl: u16,
68 pub hydra: GuildHydra,
70 pub portal: GuildPortal,
72
73 member_count: u8,
76 pub members: Vec<GuildMemberData>,
78 pub chat: Vec<ChatMessage>,
80 pub whispers: Vec<ChatMessage>,
82 pub fightable_guilds: Vec<FightableGuild>,
85}
86
87#[derive(Debug, Clone, Default)]
89#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
90pub struct GuildHydra {
91 pub last_battle: Option<DateTime<Local>>,
93 pub last_full: Option<DateTime<Local>>,
95 pub next_battle: Option<DateTime<Local>>,
98 pub remaining_fights: u16,
100 pub current_life: u64,
102 pub max_life: u64,
104 pub attributes: EnumMap<AttributeType, u32>,
106}
107
108#[derive(Debug, Clone, PartialEq, Default)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub struct FightableGuild {
113 pub id: u32,
115 pub name: String,
117 pub emblem: Emblem,
119 pub number_of_members: u8,
121 pub members_min_level: u32,
123 pub members_max_level: u32,
125 pub members_average_level: u32,
127 pub rank: u32,
129 pub honor: u32,
131}
132
133#[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 #[must_use]
143 pub fn server_encode(&self) -> String {
144 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#[derive(Debug, Clone, Default)]
156#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
157pub struct ChatMessage {
158 pub user: String,
161 pub time: NaiveTime,
164 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 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 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 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#[derive(Debug, Default, Clone)]
419#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
420pub struct PlanedBattle {
421 pub other: u32,
423 pub date: DateTime<Local>,
425}
426
427impl PlanedBattle {
428 #[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#[derive(Debug, Default, Clone)]
455#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
456pub struct GuildPortal {
457 pub damage_bonus: u8,
459 pub defeated_count: u8,
462 pub life_percentage: u8,
464}
465
466#[derive(Debug, Copy, Clone, FromPrimitive)]
468#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
469pub enum BattlesJoined {
470 Defense = 1,
472 Attack = 10,
474 Both = 11,
477}
478
479#[derive(Debug, Clone, Default)]
481#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
482pub struct GuildMemberData {
483 pub name: String,
485 pub battles_joined: Option<BattlesJoined>,
487 pub level: u16,
489 pub last_online: Option<DateTime<Local>>,
492 pub treasure_skill: u16,
494 pub instructor_skill: u16,
496 pub guild_pet_lvl: u16,
498
499 pub guild_rank: GuildRank,
501 pub portal_fought: Option<DateTime<Local>>,
504 pub potions: [Option<ItemType>; 3],
508 pub knights: u8,
510}
511
512#[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#[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}