1#![allow(clippy::module_name_repetitions)]
2use chrono::{DateTime, Local, NaiveTime};
3use enum_map::EnumMap;
4use log::warn;
5use num_derive::FromPrimitive;
6
7use super::{
8 ArrSkip, AttributeType, CCGet, CFPGet, CGet, CSTGet, NormalCost, Potion,
9 SFError, ServerTime,
10 items::{ItemType, PotionSize, PotionType},
11 update_enum_map,
12};
13use crate::misc::{from_sf_string, soft_into, warning_parse};
14
15#[derive(Debug, Clone, Default)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub struct Guild {
19 pub id: u32,
21 pub name: String,
23 pub description: String,
25 pub emblem: Emblem,
28
29 pub honor: u32,
31 pub rank: u32,
33 pub joined: DateTime<Local>,
35
36 pub own_treasure_skill: u16,
38 pub own_treasure_upgrade: NormalCost,
40 pub total_treasure_skill: u16,
42
43 pub own_instructor_skill: u16,
45 pub own_instructor_upgrade: NormalCost,
47 pub total_instructor_skill: u16,
49
50 pub finished_raids: u16,
52
53 pub defending: Option<PlanedBattle>,
56 pub attacking: Option<PlanedBattle>,
59 pub next_attack_possible: Option<DateTime<Local>>,
61
62 pub pet_id: u32,
64 pub pet_max_lvl: u16,
66 pub hydra: GuildHydra,
68 pub portal: GuildPortal,
70
71 member_count: u8,
74 pub members: Vec<GuildMemberData>,
76 pub chat: Vec<ChatMessage>,
78 pub whispers: Vec<ChatMessage>,
80
81 pub fightable_guilds: Vec<FightableGuild>,
84}
85
86#[derive(Debug, Clone, Default)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub struct GuildHydra {
90 pub last_battle: Option<DateTime<Local>>,
92 pub last_full: Option<DateTime<Local>>,
94 pub next_battle: Option<DateTime<Local>>,
97 pub remaining_fights: u16,
99 pub current_life: u64,
101 pub max_life: u64,
103 pub attributes: EnumMap<AttributeType, u32>,
105}
106
107#[derive(Debug, Clone, PartialEq, Default)]
110#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
111pub struct FightableGuild {
112 pub id: u32,
114 pub name: String,
116 pub emblem: Emblem,
118 pub number_of_members: u8,
120 pub members_min_level: u32,
122 pub members_max_level: u32,
124 pub members_average_level: u32,
126 pub rank: u32,
128 pub honor: u32,
130}
131
132#[derive(Debug, Clone, Default, PartialEq)]
134#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
135pub struct Emblem {
136 raw: String,
137}
138
139impl Emblem {
140 #[must_use]
142 pub fn server_encode(&self) -> String {
143 self.raw.clone()
145 }
146
147 pub(crate) fn update(&mut self, str: &str) {
148 self.raw.clear();
149 self.raw.push_str(str);
150 }
151}
152
153#[derive(Debug, Clone, Default)]
155#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
156pub struct ChatMessage {
157 pub user: String,
160 pub time: NaiveTime,
163 pub message: String,
165}
166
167impl ChatMessage {
168 pub(crate) fn parse_messages(data: &str) -> Vec<ChatMessage> {
169 data.split('/')
170 .filter_map(|msg| {
171 let (time, rest) = msg.split_once(' ')?;
172 let (name, msg) = rest.split_once(':')?;
173 let msg = from_sf_string(msg.trim_start_matches(['§', ' ']));
174 let time = NaiveTime::parse_from_str(time, "%H:%M").ok()?;
175 Some(ChatMessage {
176 user: name.to_string(),
177 time,
178 message: msg,
179 })
180 })
181 .collect()
182 }
183}
184
185impl Guild {
186 pub(crate) fn update_group_save(
187 &mut self,
188 val: &str,
189 server_time: ServerTime,
190 ) -> Result<(), SFError> {
191 let data: Vec<_> = val
192 .split('/')
193 .map(|c| c.trim().parse::<i64>().unwrap_or_default())
194 .collect();
195
196 let member_count = data.csiget(3, "member count", 0)?;
197 self.member_count = member_count;
198 self.members
199 .resize_with(member_count as usize, Default::default);
200
201 for (offset, member) in self.members.iter_mut().enumerate() {
202 member.battles_joined =
203 data.cfpget(445 + offset, "member fights joined", |x| x % 100)?;
204 member.level = data.csiget(64 + offset, "member level", 0)?;
205 member.last_online =
206 data.cstget(114 + offset, "member last online", server_time)?;
207 member.treasure_skill =
208 data.csiget(214 + offset, "member treasure skill", 0)?;
209 member.instructor_skill =
210 data.csiget(264 + offset, "member master skill", 0)?;
211 member.guild_rank = match data.cget(314 + offset, "member rank")? {
212 1 => GuildRank::Leader,
213 2 => GuildRank::Officer,
214 3 => GuildRank::Member,
215 4 => GuildRank::Invited,
216 x => {
217 warn!("Unknown guild rank: {x}");
218 GuildRank::Invited
219 }
220 };
221 member.portal_fought =
222 data.cstget(164 + offset, "member portal fought", server_time)?;
223 member.guild_pet_lvl =
224 data.csiget(390 + offset, "member pet skill", 0)?;
225 }
226
227 self.honor = data.csiget(13, "guild honor", 0)?;
228 self.id = data.csiget(0, "guild id", 0)?;
229
230 self.finished_raids = data.csiget(8, "finished raids", 0)?;
231
232 self.attacking = PlanedBattle::parse(
233 data.skip(364, "attacking guild")?,
234 server_time,
235 )?;
236
237 self.defending = PlanedBattle::parse(
238 data.skip(366, "attacking guild")?,
239 server_time,
240 )?;
241
242 self.next_attack_possible =
243 data.cstget(365, "guild next attack time", server_time)?;
244
245 self.pet_id = data.csiget(377, "gpet id", 0)?;
246 self.pet_max_lvl = data.csiget(378, "gpet max lvl", 0)?;
247
248 self.hydra.last_battle =
249 data.cstget(382, "hydra pet lb", server_time)?;
250 self.hydra.last_full =
251 data.cstget(381, "hydra last defeat", server_time)?;
252
253 self.hydra.current_life = data.csiget(383, "ghydra clife", u64::MAX)?;
254 self.hydra.max_life = data.csiget(384, "ghydra max clife", u64::MAX)?;
255
256 update_enum_map(
257 &mut self.hydra.attributes,
258 data.skip(385, "hydra attributes")?,
259 );
260
261 self.total_treasure_skill =
262 data.csimget(6, "guild total treasure skill", 0, |x| x & 0xFFFF)?;
263 self.total_instructor_skill =
264 data.csimget(7, "guild total instructor skill", 0, |x| x & 0xFFFF)?;
265
266 self.portal.life_percentage =
267 data.csimget(6, "guild portal life p", 100, |x| x >> 16)?;
268 self.portal.defeated_count =
269 data.csimget(7, "guild portal progress", 0, |x| x >> 16)?;
270
271 Ok(())
272 }
273
274 pub(crate) fn update_member_names(&mut self, val: &str) {
275 let names: Vec<_> = val
276 .split(',')
277 .map(std::string::ToString::to_string)
278 .collect();
279 self.members.resize_with(names.len(), Default::default);
280 for (member, name) in self.members.iter_mut().zip(names) {
281 member.name = name;
282 }
283 }
284
285 pub(crate) fn update_group_knights(&mut self, val: &str) {
286 let data: Vec<i64> = val
287 .trim_end_matches(',')
288 .split(',')
289 .flat_map(str::parse)
290 .collect();
291
292 self.members.resize_with(data.len(), Default::default);
293 for (member, count) in self.members.iter_mut().zip(data) {
294 member.knights = soft_into(count, "guild knight", 0);
295 }
296 }
297
298 pub(crate) fn update_member_potions(&mut self, val: &str) {
299 let data = val
300 .trim_end_matches(',')
301 .split(',')
302 .map(|c| {
303 warning_parse(c, "member potion", |a| a.parse::<i64>().ok())
304 .unwrap_or_default()
305 })
306 .collect::<Vec<_>>();
307
308 let potions = data.len() / 2;
309 let member = potions / 3;
310 self.members.resize_with(member, Default::default);
311
312 let mut data = data.into_iter();
313
314 let quick_potion = |int: i64| {
315 Some(ItemType::Potion(Potion {
316 typ: PotionType::parse(int)?,
317 size: PotionSize::parse(int)?,
318 expires: None,
319 }))
320 };
321
322 for member in &mut self.members {
323 for potion in &mut member.potions {
324 *potion = data
325 .next()
326 .or_else(|| {
327 warn!("Invalid member potion len");
328 None
329 })
330 .and_then(quick_potion);
331 _ = data.next();
332 }
333 }
334 }
335
336 pub(crate) fn update_description_embed(&mut self, data: &str) {
337 let Some((emblem, description)) = data.split_once('§') else {
338 self.description = from_sf_string(data);
339 return;
340 };
341
342 self.description = from_sf_string(description);
343 self.emblem.update(emblem);
344 }
345
346 pub(crate) fn update_group_prices(
347 &mut self,
348 data: &[i64],
349 ) -> Result<(), SFError> {
350 self.own_treasure_upgrade.silver =
351 data.csiget(0, "treasure upgr. silver", 0)?;
352 self.own_treasure_upgrade.mushrooms =
353 data.csiget(1, "treasure upgr. mush", 0)?;
354 self.own_instructor_upgrade.silver =
355 data.csiget(2, "instr upgr. silver", 0)?;
356 self.own_instructor_upgrade.mushrooms =
357 data.csiget(3, "instr upgr. mush", 0)?;
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)]
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}