1pub mod arena;
2pub mod character;
3pub mod dungeons;
4pub mod fortress;
5pub mod guild;
6pub mod idle;
7pub mod items;
8pub mod legendary_dungeon;
9pub mod rewards;
10pub mod social;
11pub mod tavern;
12pub mod underworld;
13pub mod unlockables;
14
15use std::{
16 borrow::Borrow,
17 collections::{HashMap, HashSet},
18};
19
20use chrono::{DateTime, Duration, Local, NaiveDateTime};
21use enum_map::EnumMap;
22use log::{error, warn};
23use num_traits::FromPrimitive;
24use strum::{EnumCount, IntoEnumIterator};
25
26use crate::{
27 command::*,
28 error::*,
29 gamestate::{
30 arena::*, character::*, dungeons::*, fortress::*, guild::*, idle::*,
31 items::*, legendary_dungeon::*, rewards::*, social::*, tavern::*,
32 underworld::*, unlockables::*,
33 },
34 misc::*,
35 response::{Response, ResponseVal},
36};
37
38#[derive(Debug, Clone, Default)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41pub struct GameState {
42 pub character: Character,
45 pub tavern: Tavern,
47 pub arena: Arena,
49 pub last_fight: Option<Fight>,
51 pub shops: EnumMap<ShopType, Shop>,
54 pub shop_item_lvl: u32,
55 pub guild: Option<Guild>,
57 pub specials: TimedSpecials,
59 pub dungeons: Dungeons,
61 pub underworld: Option<Underworld>,
63 pub fortress: Option<Fortress>,
65 pub pets: Option<Pets>,
67 pub hellevator: HellevatorEvent,
69 pub legendary_dungeon: LegendaryDungeonEvent,
72 pub blacksmith: Option<Blacksmith>,
74 pub witch: Option<Witch>,
76 pub achievements: Achievements,
78 pub idle_game: Option<IdleGame>,
80 pub pending_unlocks: Vec<Unlockable>,
82 pub hall_of_fames: HallOfFames,
84 pub lookup: Lookup,
86 pub mail: Mail,
88 pub language: Option<Language>,
91 last_request_timestamp: i64,
93 server_time_diff: i64,
96}
97
98const SHOP_N: usize = 6;
99
100#[derive(Debug, Clone)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
103pub struct Shop {
104 pub typ: ShopType,
105 pub items: [Item; SHOP_N],
107}
108
109impl Default for Shop {
110 fn default() -> Self {
111 let items = core::array::from_fn(|_| Item {
112 typ: ItemType::Unknown(0),
113 price: u32::MAX,
114 mushroom_price: u32::MAX,
115 model_id: 0,
116 class: None,
117 type_specific_val: 0,
118 attributes: EnumMap::default(),
119 gem_slot: None,
120 rune: None,
121 enchantment: None,
122 color: 0,
123 upgrade_count: 0,
124 item_quality: 0,
125 is_washed: false,
126 full_model_id: 0,
127 });
128
129 Self {
130 items,
131 typ: ShopType::Magic,
132 }
133 }
134}
135
136#[derive(Debug, Default, Clone, PartialEq, Eq, Copy)]
137#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
138pub struct ShopPosition {
139 pub typ: ShopType,
140 pub pos: usize,
141}
142
143impl std::fmt::Display for ShopPosition {
144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 write!(f, "{}/{}", self.typ as usize, self.pos + 1)
146 }
147}
148
149impl ShopPosition {
150 #[must_use]
152 pub fn shop(&self) -> ShopType {
153 self.typ
154 }
155 #[must_use]
159 pub fn position(&self) -> usize {
160 self.pos
161 }
162}
163
164impl Shop {
165 pub fn iter(&self) -> impl Iterator<Item = (ShopPosition, &Item)> {
167 self.items
168 .iter()
169 .enumerate()
170 .map(|(pos, item)| (ShopPosition { typ: self.typ, pos }, item))
171 }
172
173 pub(crate) fn parse(
174 data: &[i64],
175 server_time: ServerTime,
176 typ: ShopType,
177 ) -> Result<Shop, SFError> {
178 let mut shop = Shop::default();
179 shop.typ = typ;
180 for (idx, item) in shop.items.iter_mut().enumerate() {
181 let d = data.skip(idx * ITEM_PARSE_LEN, "shop item")?;
182 let Some(p_item) = Item::parse(d, server_time)? else {
183 return Err(SFError::ParsingError(
184 "shop item",
185 format!("{d:?}"),
186 ));
187 };
188 *item = p_item;
189 }
190 Ok(shop)
191 }
192}
193
194impl GameState {
195 pub fn new(response: Response) -> Result<Self, SFError> {
204 let mut res = Self::default();
205 res.update(response)?;
206 if res.character.level == 0 || res.character.name.is_empty() {
207 return Err(SFError::ParsingError(
208 "response did not contain full player state",
209 String::new(),
210 ));
211 }
212 Ok(res)
213 }
214
215 pub fn update<R: Borrow<Response>>(
222 &mut self,
223 response: R,
224 ) -> Result<(), SFError> {
225 let response = response.borrow();
226 let new_vals = response.values();
227 if let Some(ts) = new_vals.get("timestamp").copied() {
230 let ts = ts.into("server time stamp")?;
231 let server_time = DateTime::from_timestamp(ts, 0).ok_or(
232 SFError::ParsingError("server time stamp", ts.to_string()),
233 )?;
234 self.server_time_diff = (server_time.naive_utc()
235 - response.received_at())
236 .num_seconds();
237 self.last_request_timestamp = ts;
238 }
239 let server_time = self.server_time();
240
241 self.last_fight = None;
242 self.mail.open_claimable = None;
243
244 let mut other_player: Option<OtherPlayer> = None;
245 let mut other_guild: Option<OtherGuild> = None;
246
247 let mut errors = vec![];
248 for (key, val) in new_vals.iter().map(|(a, b)| (*a, *b)) {
249 let res = self.apply_update_key(
250 key,
251 val,
252 &mut other_player,
253 &mut other_guild,
254 server_time,
255 new_vals,
256 );
257 if let Err(err) = res {
258 errors.push(err);
259 }
260 }
261
262 if let Some(og) = other_guild {
263 self.lookup.guilds.insert(og.name.clone(), og);
264 }
265 if let Some(other_player) = other_player {
266 self.lookup.insert_lookup(other_player);
267 }
268
269 if self.dungeons.portal.is_some() && self.character.level < 99 {
271 self.dungeons.portal = None;
272 }
273
274 if let Some(pets) = &self.pets
275 && pets.rank == 0
276 {
277 self.pets = None;
278 }
279 if let Some(t) = &self.guild
280 && t.name.is_empty()
281 {
282 self.guild = None;
283 }
284 if self.fortress.is_some() && self.character.level < 25 {
285 self.fortress = None;
286 }
287 if let Some(fortress) = &mut self.fortress {
288 for (typ, unit) in &mut fortress.units {
289 let building_lvl =
290 fortress.buildings.get(typ.training_building()).level;
291 let limit_modifier = match typ {
292 FortressUnitType::Magician => 1,
293 FortressUnitType::Archer => 2,
294 FortressUnitType::Soldier => 3,
295 };
296 unit.limit = building_lvl * limit_modifier;
297 }
298 }
299
300 if let Some(t) = &self.underworld
301 && t.buildings[UnderworldBuildingType::HeartOfDarkness].level < 1
302 {
303 self.underworld = None;
304 }
305
306 if self.witch.is_some() && self.character.level < 66 {
308 self.witch = None;
309 }
310
311 match errors.len() {
312 0 => Ok(()),
313 1 => Err(errors.remove(0)),
314 _ => Err(SFError::NestedError(errors)),
315 }
316 }
317
318 pub(crate) fn updatete_relation_list(&mut self, val: &str) {
319 self.character.relations.clear();
320 for entry in val
321 .trim_end_matches(';')
322 .split(';')
323 .filter(|a| !a.is_empty())
324 {
325 let mut parts = entry.split(',');
326 let (
327 Some(id),
328 Some(name),
329 Some(guild),
330 Some(level),
331 Some(relation),
332 ) = (
333 parts.next().and_then(|a| a.parse().ok()),
334 parts.next().map(std::string::ToString::to_string),
335 parts.next().map(std::string::ToString::to_string),
336 parts.next().and_then(|a| a.parse().ok()),
337 parts.next().and_then(|a| match a {
338 "-1" => Some(Relationship::Ignored),
339 "1" => Some(Relationship::Friend),
340 _ => None,
341 }),
342 )
343 else {
344 warn!("bad friendslist entry: {entry}");
345 continue;
346 };
347 self.character.relations.push(RelationEntry {
348 id,
349 name,
350 guild,
351 level,
352 relation,
353 });
354 }
355 }
356
357 pub(crate) fn update_gttime(
358 &mut self,
359 data: &[i64],
360 server_time: ServerTime,
361 ) -> Result<(), SFError> {
362 let d = &mut self.hellevator;
363 d.start = data.cstget(0, "event start", server_time)?;
364 d.end = data.cstget(1, "event end", server_time)?;
365 d.collect_time_end = data.cstget(3, "claim time end", server_time)?;
366 Ok(())
367 }
368
369 pub(crate) fn update_resources(
370 &mut self,
371 res: &[i64],
372 ) -> Result<(), SFError> {
373 self.character.mushrooms = res.csiget(1, "mushrooms", 0)?;
374 self.character.silver = res.csiget(2, "player silver", 0)?;
375 self.tavern.quicksand_glasses =
376 res.csiget(4, "quicksand glass count", 0)?;
377
378 self.specials.wheel.lucky_coins = res.csiget(3, "lucky coins", 0)?;
379 let bs = self.blacksmith.get_or_insert_with(Default::default);
380 bs.metal = res.csiget(9, "bs metal", 0)?;
381 bs.arcane = res.csiget(10, "bs arcane", 0)?;
382 let fortress = self.fortress.get_or_insert_with(Default::default);
383 fortress
384 .resources
385 .get_mut(FortressResourceType::Wood)
386 .current = res.csiget(5, "saved wood ", 0)?;
387 fortress
388 .resources
389 .get_mut(FortressResourceType::Stone)
390 .current = res.csiget(7, "saved stone", 0)?;
391
392 let pets = self.pets.get_or_insert_with(Default::default);
393 for (e_pos, element) in HabitatType::iter().enumerate() {
394 pets.habitats.get_mut(element).fruits =
395 res.csiget(12 + e_pos, "fruits", 0)?;
396 }
397
398 self.underworld
399 .get_or_insert_with(Default::default)
400 .souls_current = res.csiget(11, "uu souls saved", 0)?;
401 Ok(())
402 }
403
404 #[must_use]
407 pub fn server_time(&self) -> ServerTime {
408 ServerTime(self.server_time_diff)
409 }
410
411 #[must_use]
415 fn get_fight(&mut self, header_name: &str) -> &mut SingleFight {
416 let id = fight_no_from_header(header_name);
417 let fights =
418 &mut self.last_fight.get_or_insert_with(Default::default).fights;
419
420 if fights.len() < id {
421 fights.resize(id, SingleFight::default());
422 }
423 #[allow(clippy::unwrap_used)]
424 fights.get_mut(id - 1).unwrap()
425 }
426
427 #[allow(clippy::match_same_arms)]
429 fn apply_update_key(
430 &mut self,
431 key: &str,
432 val: ResponseVal<'_>,
433 other_player: &mut Option<OtherPlayer>,
434 other_guild: &mut Option<OtherGuild>,
435 server_time: ServerTime,
436 all_values: &HashMap<&str, ResponseVal<'_>>,
437 ) -> Result<(), SFError> {
438 match key {
439 "timestamp" => {
440 }
442 "Success" | "sucess" => {
443 }
446 "login count" | "sessionid" | "cryptokey" | "cryptoid" => {
447 }
449 "preregister"
450 | "languagecodelist"
451 | "tracking"
452 | "skipvideo"
453 | "webshopid"
454 | "cidstring"
455 | "mountexpired"
456 | "tracking_netto"
457 | "tracking_coins"
458 | "tutorial_game_entry" => {
459 }
461 "ownplayername" => {
462 self.character.name.set(val.as_str());
463 }
464 "owndescription" => {
465 self.character.description = from_sf_string(val.as_str());
466 }
467 "wagesperhour" => {
468 self.tavern.guard_wage = val.into("tavern wage")?;
469 }
470 "skipallow" => {
471 let raw_skip = val.into::<i32>("skip allow")?;
472 self.tavern.mushroom_skip_allowed = raw_skip != 0;
473 }
474 "cryptoid not found" => return Err(SFError::ConnectionError),
475 "ownplayersave" => {
476 }
478 "owngroupname" => self
479 .guild
480 .get_or_insert_with(Default::default)
481 .name
482 .set(val.as_str()),
483 "sfhomeid" => {}
484 "backpack" => {
485 let data: Vec<i64> = val.into_list("backpack")?;
486 self.character.inventory.backpack = data
487 .chunks_exact(ITEM_PARSE_LEN)
488 .map(|a| Item::parse(a, server_time))
489 .collect::<Result<Vec<_>, _>>()?;
490 }
491 "itemlevelshop" => {
492 self.shop_item_lvl = val.into("shop lvl")?;
493 }
494 "storeitemsshakes" => {
495 let data: Vec<i64> = val.into_list("weapon store")?;
496 *self.shops.get_mut(ShopType::Weapon) =
497 Shop::parse(&data, server_time, ShopType::Weapon)?;
498 }
499 "questofferitems" => {
500 for (chunk, quest) in val
501 .into_list("quest items")?
502 .chunks_exact(19)
503 .zip(&mut self.tavern.quests)
504 {
505 quest.item = Item::parse(chunk, server_time)?;
506 }
507 }
508 #[allow(
509 clippy::indexing_slicing,
510 clippy::cast_sign_loss,
511 clippy::cast_possible_truncation
512 )]
513 #[allow(deprecated)]
514 "toiletstate" => {
515 let vals: Vec<i64> = val.into_list("toilet state")?;
516 if vals.len() < 3 {
517 return Ok(());
518 }
519 let toilet = self.tavern.toilet.get_or_insert_default();
520 toilet.sacrifices_left = vals[2] as u32;
521 }
522 "companionequipment" => {
523 let data: Vec<i64> = val.into_list("quest items")?;
524 if data.is_empty() {
525 return Ok(());
526 }
527 for (idx, cmp) in self
528 .dungeons
529 .companions
530 .get_or_insert_with(Default::default)
531 .values_mut()
532 .enumerate()
533 {
534 let data = data.skip(
535 (19 * EquipmentSlot::COUNT) * idx,
536 "companion item",
537 )?;
538 cmp.equipment = Equipment::parse(data, server_time)?;
539 }
540 }
541 "storeitemsfidget" => {
542 let data: Vec<i64> = val.into_list("magic store")?;
543 *self.shops.get_mut(ShopType::Magic) =
544 Shop::parse(&data, server_time, ShopType::Magic)?;
545 }
546 "ownplayersaveequipment" => {
547 let data: Vec<i64> = val.into_list("player equipment")?;
548 self.character.equipment =
549 Equipment::parse(&data, server_time)?;
550 }
551 "systemmessagelist" => {}
552 "newslist" => {
553 self.mail.news_inbox.clear();
554 let data = val.as_str();
555 for msg in data.split(';').filter(|a| !a.trim().is_empty()) {
556 match NewsEntry::parse(msg, server_time) {
557 Ok(msg) => self.mail.news_inbox.push(msg),
558 Err(e) => warn!("Invalid msg: {msg} {e}"),
559 }
560 }
561 }
562 "dummieequipment" => {
563 let m: Vec<i64> = val.into_list("mannequin")?;
564 self.character.mannequin =
565 Some(Equipment::parse(&m, server_time)?);
566 }
567 "owntower" => {
568 let data = val.into_list("tower")?;
569 let companions = self
570 .dungeons
571 .companions
572 .get_or_insert_with(Default::default);
573
574 for (i, class) in CompanionClass::iter().enumerate() {
575 let comp_start = 3 + i * 148;
576 companions.get_mut(class).level =
577 data.cget(comp_start, "comp level")?;
578 update_enum_map(
579 &mut companions.get_mut(class).attributes,
580 data.skip(comp_start + 4, "comp attrs")?,
581 );
582 }
583 self.underworld
585 .get_or_insert_with(Default::default)
586 .update(&data, server_time)?;
587 }
588 "owngrouprank" => {
589 self.guild.get_or_insert_with(Default::default).rank =
590 val.into("group rank")?;
591 }
592 "owngroupattack" | "owngroupdefense" => {
593 }
595 "owngrouprequirement" | "othergrouprequirement" => {
596 }
598 "owngroupsave" => {
599 self.guild
600 .get_or_insert_with(Default::default)
601 .update_group_save(val.as_str(), server_time)?;
602 }
603 "owngroupmember" => self
604 .guild
605 .get_or_insert_with(Default::default)
606 .update_member_names(val.as_str()),
607 "owngrouppotion" => {
608 self.guild
609 .get_or_insert_with(Default::default)
610 .update_member_potions(val.as_str());
611 }
612 "unitprice" => {
613 self.fortress
614 .get_or_insert_with(Default::default)
615 .update_unit_prices(&val.into_list("fortress units")?)?;
616 }
617 "dicestatus" => {
618 let dices: Option<Vec<DiceType>> = val
619 .into_list("dice status")?
620 .into_iter()
621 .map(FromPrimitive::from_u8)
622 .collect();
623 self.tavern.dice_game.current_dice = dices.unwrap_or_default();
624 }
625 "dicereward" => {
626 let data: Vec<u32> = val.into_list("dice reward")?;
627 let win_typ: DiceType =
628 data.cfpuget(0, "dice reward", |a| a - 1)?;
629 self.tavern.dice_game.reward = Some(DiceReward {
630 win_typ,
631 amount: data.cget(1, "dice reward amount")?,
632 });
633 }
634 "chathistory" => {
635 self.guild.get_or_insert_with(Default::default).chat =
636 ChatMessage::parse_messages(val.as_str());
637 }
638 "chatwhisper" => {
639 self.guild.get_or_insert_with(Default::default).whispers =
640 ChatMessage::parse_messages(val.as_str());
641 }
642 "upgradeprice" => {
643 self.fortress
644 .get_or_insert_with(Default::default)
645 .update_unit_upgrade_info(
646 &val.into_list("fortress unit upgrade prices")?,
647 )?;
648 }
649 "unitlevel" => {
650 self.fortress
651 .get_or_insert_with(Default::default)
652 .update_levels(&val.into_list("fortress unit levels")?)?;
653 }
654 "fortressprice" => {
655 self.fortress
656 .get_or_insert_with(Default::default)
657 .update_prices(
658 &val.into_list("fortress upgrade prices")?,
659 )?;
660 }
661 "Arenarank" => {
662 if let Some(uw) = self.underworld.as_mut() {
663 uw.lure_suggestion =
664 val.as_str().parse::<u32>().ok().map(LureSuggestion);
665 }
666 }
667 "witch" => {
668 }
670 "witchshop" => {
671 self.witch
672 .get_or_insert_with(Default::default)
673 .update(&val.into_list("witch")?)?;
674 }
675 "underworldupgradeprice" => {
676 self.underworld
677 .get_or_insert_with(Default::default)
678 .update_underworld_unit_prices(
679 &val.into_list("underworld upgrade prices")?,
680 )?;
681 }
682 "unlockfeature" => {
683 self.pending_unlocks =
684 Unlockable::parse(&val.into_list("unlock")?)?;
685 }
686 "dungeonprogresslight" => self.dungeons.update_progress(
687 &val.into_list("dungeon progress light")?,
688 DungeonType::Light,
689 ),
690 "dungeonprogressshadow" => self.dungeons.update_progress(
691 &val.into_list("dungeon progress shadow")?,
692 DungeonType::Shadow,
693 ),
694 "portalprogress" => {
695 self.dungeons
696 .portal
697 .get_or_insert_with(Default::default)
698 .update(&val.into_list("portal progress")?, server_time)?;
699 }
700 "owntowerlevel" => {
701 }
703 "serverversion" => {
704 }
706 "stoneperhournextlevel" => {
707 self.fortress
708 .get_or_insert_with(Default::default)
709 .resources
710 .get_mut(FortressResourceType::Stone)
711 .production
712 .per_hour_next_lvl = val.into("stone next lvl")?;
713 }
714 "woodperhournextlevel" => {
715 self.fortress
716 .get_or_insert_with(Default::default)
717 .resources
718 .get_mut(FortressResourceType::Wood)
719 .production
720 .per_hour_next_lvl = val.into("wood next lvl")?;
721 }
722 "shadowlevel" | "dungeonlevel" => {
723 }
725 "gttime" => {
726 self.update_gttime(&val.into_list("gttime")?, server_time)?;
727 }
728 "gtsave" => {
729 self.hellevator
730 .active
731 .get_or_insert_with(Default::default)
732 .update(&val.into_list("gtsave")?, server_time)?;
733 }
734 "maxrank" => {
735 self.hall_of_fames.players_total = val.into("player count")?;
736 }
737 "achievement" => {
738 self.achievements.update(&val.into_list("achievements")?)?;
739 }
740 "groupskillprice" => {
741 self.guild
742 .get_or_insert_with(Default::default)
743 .update_group_prices(
744 &val.into_list("guild skill prices")?,
745 )?;
746 }
747 "soldieradvice" => {
748 let advice: u16 = val.into("soldier advice")?;
749 if advice > 0 {
750 other_player
751 .get_or_insert_default()
752 .fortress
753 .get_or_insert_default()
754 .soldier_advice = advice;
755 }
756 }
758 "owngroupdescription" => self
759 .guild
760 .get_or_insert_with(Default::default)
761 .update_description_embed(val.as_str()),
762 "idle" => {
763 self.idle_game = IdleGame::parse_idle_game(
764 &val.into_list("idle game")?,
765 server_time,
766 );
767 }
768 "resources" => {
769 self.update_resources(&val.into_list("resources")?)?;
770 }
771 "chattime" => {
772 }
778 "maxpetlevel" => {
779 self.pets.get_or_insert_with(Default::default).max_pet_level =
780 val.into("max pet lvl")?;
781 }
782 "otherdescription" => {
783 other_player
784 .get_or_insert_with(Default::default)
785 .description = from_sf_string(val.as_str());
786 }
787 "otherplayergroupname" => {
788 let guild =
789 Some(val.as_str().to_string()).filter(|a| !a.is_empty());
790 other_player.get_or_insert_with(Default::default).guild = guild;
791 }
792 "otherplayername" => {
793 other_player
794 .get_or_insert_with(Default::default)
795 .name
796 .set(val.as_str());
797 }
798 "otherplayersaveequipment" => {
799 let data: Vec<i64> = val.into_list("other player equipment")?;
800 other_player.get_or_insert_with(Default::default).equipment =
801 Equipment::parse(&data, server_time)?;
802 }
803 "fortresspricereroll" => {
804 self.fortress
805 .get_or_insert_with(Default::default)
806 .opponent_reroll_price = val.into("fortress reroll")?;
807 }
808 "fortresswalllevel" => {
809 self.fortress
810 .get_or_insert_with(Default::default)
811 .wall_combat_lvl = val.into("fortress wall lvl")?;
812 }
813 "dragongoldbonus" => {
814 self.character.mount_dragon_refund = val.into("dragon gold")?;
815 }
816 "wheelresult" => {
817 let upgraded = self.character.level >= 95
820 && self.pets.is_some()
821 && self.underworld.is_some();
822 self.specials.wheel.result = Some(WheelReward::parse(
823 &val.into_list("wheel result")?,
824 upgraded,
825 )?);
826 }
827 "dailyreward" => {
828 }
830 "calenderreward" => {
831 }
833 "oktoberfest" => {
834 if !val.as_str().is_empty() {
837 warn!("oktoberfest response is not empty: {val}");
838 }
839 }
840 "usersettings" => {
841 let vals = &val.as_str().split('/').collect::<Vec<_>>();
843 self.language = Language::parse(vals.cget(0, "client lang")?);
844
845 let v = match vals.cget(4, "questing setting")? {
846 "a" => ExpeditionSetting::PreferExpeditions,
847 "0" | "b" => ExpeditionSetting::PreferQuests,
848 x => {
849 error!("Weird expedition settings: {x}");
850 ExpeditionSetting::PreferQuests
851 }
852 };
853 self.tavern.questing_preference = v;
854 }
855 "mailinvoice" => {
856 }
858 "calenderinfo" => {
859 let data: Vec<i64> = val.into_list("calendar")?;
862 self.specials.calendar.rewards.clear();
863 for p in data.chunks_exact(2) {
864 let reward = CalendarReward::parse(p)?;
865 self.specials.calendar.rewards.push(reward);
866 }
867 }
868 "othergroupattack" => {
869 other_guild.get_or_insert_with(Default::default).attacks =
870 Some(val.to_string());
871 }
872 "othergroupdefense" => {
873 other_guild
874 .get_or_insert_with(Default::default)
875 .defends_against = Some(val.to_string());
876 }
877 "inboxcapacity" => {
878 self.mail.inbox_capacity = val.into("inbox cap")?;
879 }
880 "magicregistration" => {
881 }
884 "Ranklistplayer" => {
885 self.hall_of_fames.players.clear();
886 for player in val.as_str().trim_matches(';').split(';') {
887 if player.ends_with(",,,0,0,0,") {
889 break;
890 }
891
892 match HallOfFamePlayer::parse(player) {
893 Ok(x) => {
894 self.hall_of_fames.players.push(x);
895 }
896 Err(err) => warn!("{err}"),
897 }
898 }
899 }
900 "ranklistgroup" => {
901 self.hall_of_fames.guilds.clear();
902 for guild in val.as_str().trim_matches(';').split(';') {
903 match HallOfFameGuild::parse(guild) {
904 Ok(x) => {
905 self.hall_of_fames.guilds.push(x);
906 }
907 Err(err) => warn!("{err}"),
908 }
909 }
910 }
911 "maxrankgroup" => {
912 self.hall_of_fames.guilds_total = Some(val.into("guild max")?);
913 }
914 "maxrankPets" => {
915 self.hall_of_fames.pets_total = Some(val.into("pet rank max")?);
916 }
917 "RanklistPets" => {
918 self.hall_of_fames.pets.clear();
919 for entry in val.as_str().trim_matches(';').split(';') {
920 match HallOfFamePets::parse(entry) {
921 Ok(x) => {
922 self.hall_of_fames.pets.push(x);
923 }
924 Err(err) => warn!("{err}"),
925 }
926 }
927 }
928 "ranklistfortress" | "Ranklistfortress" => {
929 self.hall_of_fames.fortresses.clear();
930 for guild in val.as_str().trim_matches(';').split(';') {
931 match HallOfFameFortress::parse(guild) {
932 Ok(x) => {
933 self.hall_of_fames.fortresses.push(x);
934 }
935 Err(err) => warn!("{err}"),
936 }
937 }
938 }
939 "ranklistunderworld" => {
940 self.hall_of_fames.underworlds.clear();
941 for entry in val.as_str().trim_matches(';').split(';') {
942 match HallOfFameUnderworld::parse(entry) {
943 Ok(x) => {
944 self.hall_of_fames.underworlds.push(x);
945 }
946 Err(err) => warn!("{err}"),
947 }
948 }
949 }
950 "gamblegoldvalue" => {
951 self.tavern.gamble_result =
952 Some(GambleResult::SilverChange(val.into("gold gamble")?));
953 }
954 "gamblecoinvalue" => {
955 self.tavern.gamble_result = Some(GambleResult::MushroomChange(
956 val.into("gold gamble")?,
957 ));
958 }
959 "maxrankFortress" => {
960 self.hall_of_fames.fortresses_total =
961 Some(val.into("fortress max")?);
962 }
963 "underworldprice" => self
964 .underworld
965 .get_or_insert_with(Default::default)
966 .update_building_prices(&val.into_list("ub prices")?)?,
967 "owngroupknights" => self
968 .guild
969 .get_or_insert_with(Default::default)
970 .update_group_knights(val.as_str()),
971 "friendlist" => self.updatete_relation_list(val.as_str()),
972 "legendaries" => {
973 if val.as_str().chars().any(|a| a != 'A') {
974 warn!("Found a legendaries value, that is not just AAA..");
975 }
976 }
977 "smith" => {
978 let data: Vec<i64> = val.into_list("smith")?;
979 let bs = self.blacksmith.get_or_insert_with(Default::default);
980
981 bs.dismantle_left = data.csiget(0, "dismantles left", 0)?;
982 bs.last_dismantled = data.cstget(1, "bs time", server_time)?;
983 }
984 "fortressGroupPrice" => {
985 self.fortress
986 .get_or_insert_with(Default::default)
987 .hall_of_knights_upgrade_price = FortressCost::parse(
988 &val.into_list("hall of knights prices")?,
989 )?;
990 }
991 "goldperhournextlevel" => {
992 }
994 "underworldmaxsouls" => {
995 }
997 "dailytaskrewardpreview" => {
998 let vals: Vec<i64> =
999 val.into_list("event task reward preview")?;
1000 self.specials.tasks.daily.rewards = parse_rewards(&vals);
1001 }
1002 "expeditionevent" => {
1003 let data: Vec<i64> = val.into_list("exp event")?;
1004 self.tavern.expeditions.start =
1005 data.cstget(0, "expedition start", server_time)?;
1006 let end = data.cstget(1, "expedition end", server_time)?;
1007 self.tavern.expeditions.end = end;
1008 }
1009 "expeditions" => {
1010 let data: Vec<i64> = val.into_list("exp event")?;
1011
1012 if !data.len().is_multiple_of(8) {
1013 warn!(
1014 "Available expeditions have weird size: {data:?} {}",
1015 data.len()
1016 );
1017 }
1018 self.tavern.expeditions.available = data
1019 .chunks_exact(8)
1020 .map(|data| {
1021 Ok(AvailableExpedition {
1022 target: data
1023 .cfpget(0, "expedition typ", |a| a)?
1024 .unwrap_or_default(),
1025 location_1: data
1026 .cfpget(4, "exp loc 1", |a| a)?
1027 .unwrap_or_default(),
1028 location_2: data
1029 .cfpget(5, "exp loc 2", |a| a)?
1030 .unwrap_or_default(),
1031 thirst_for_adventure_sec: data
1032 .csiget(6, "exp alu", 600)?,
1033 special: data.cfpget(7, "exp special", |a| a)?,
1034 })
1035 })
1036 .collect::<Result<_, _>>()?;
1037 }
1038 "expeditionrewardresources" => {
1039 }
1042 "expeditionreward" => {
1043 }
1051 "expeditionmonster" => {
1052 let data: Vec<i64> = val.into_list("expedition monster")?;
1053 let exp = self
1054 .tavern
1055 .expeditions
1056 .active
1057 .get_or_insert_with(Default::default);
1058
1059 exp.boss = ExpeditionBoss {
1060 id: data
1061 .cfpget(0, "expedition monster", |a| -a)?
1062 .unwrap_or_default(),
1063 items: soft_into(
1064 data.get(1).copied().unwrap_or_default(),
1065 "exp monster items",
1066 3,
1067 ),
1068 };
1069 }
1070 "expeditionhalftime" => {
1071 let data: Vec<i64> = val.into_list("halftime exp")?;
1072 let exp = self
1073 .tavern
1074 .expeditions
1075 .active
1076 .get_or_insert_with(Default::default);
1077
1078 exp.halftime_for_boss_id =
1079 -data.cget(0, "halftime for boss id")?;
1080 exp.rewards = data
1081 .skip(1, "halftime choice")?
1082 .chunks_exact(2)
1083 .map(Reward::parse)
1084 .collect::<Result<_, _>>()?;
1085 }
1086 "expeditionstate" => {
1087 let data: Vec<i64> = val.into_list("exp state")?;
1088 let exp = self
1089 .tavern
1090 .expeditions
1091 .active
1092 .get_or_insert_with(Default::default);
1093 exp.floor_stage = data.cget(2, "floor stage")?;
1094
1095 exp.target_thing = data
1096 .cfpget(3, "expedition target", |a| a)?
1097 .unwrap_or_default();
1098 exp.target_current = data.csiget(7, "exp current", 100)?;
1099 exp.target_amount = data.csiget(8, "exp target", 100)?;
1100
1101 exp.current_floor = data.csiget(0, "clearing", 0)?;
1102 exp.heroism = data.csiget(13, "heroism", 0)?;
1103
1104 exp.busy_since = data.cstget(15, "exp start", server_time)?;
1105 exp.busy_until = data.cstget(16, "exp busy", server_time)?;
1106
1107 for (x, item) in data
1108 .skip(9, "exp items")?
1109 .iter()
1110 .copied()
1111 .zip(&mut exp.items)
1112 {
1113 *item = match FromPrimitive::from_i64(x) {
1114 None if x != 0 => {
1115 warn!("Unknown item: {x}");
1116 Some(ExpeditionThing::Unknown)
1117 }
1118 x => x,
1119 };
1120 }
1121 }
1122 "expeditioncrossroad" => {
1123 let data: Vec<i64> = val.into_list("cross")?;
1125 let exp = self
1126 .tavern
1127 .expeditions
1128 .active
1129 .get_or_insert_with(Default::default);
1130 exp.update_encounters(&data);
1131 }
1132 "eventtasklist" => {
1133 let data: Vec<i64> = val.into_list("etl")?;
1134 self.specials.tasks.event.tasks.clear();
1135 for c in data.chunks_exact(4) {
1136 let task = Task::parse(c)?;
1137 self.specials.tasks.event.tasks.push(task);
1138 }
1139 }
1140 "eventtaskrewardpreview" => {
1141 let vals: Vec<i64> =
1142 val.into_list("event task reward preview")?;
1143
1144 self.specials.tasks.event.rewards = parse_rewards(&vals);
1145 }
1146 "dailytasklist" => {
1147 let data: Vec<i64> = val.into_list("daily tasks list")?;
1148 self.specials.tasks.daily.tasks.clear();
1149
1150 for d in data.skip(1, "daily tasks")?.chunks_exact(4) {
1153 self.specials.tasks.daily.tasks.push(Task::parse(d)?);
1154 }
1155 }
1156 "eventtaskinfo" => {
1157 let data: Vec<i64> = val.into_list("eti")?;
1158 self.specials.tasks.event.theme = data
1159 .cfpget(2, "event task theme", |a| a)?
1160 .unwrap_or(EventTaskTheme::Unknown);
1161 self.specials.tasks.event.start =
1162 data.cstget(0, "event t start", server_time)?;
1163 self.specials.tasks.event.end =
1164 data.cstget(1, "event t end", server_time)?;
1165 }
1166 "scrapbook" => {
1167 self.character.scrapbook = ScrapBook::parse(val.as_str());
1168 }
1169 "dungeonfaces" | "shadowfaces" => {
1170 }
1174 "messagelist" => {
1175 let data = val.as_str();
1176 self.mail.inbox.clear();
1177 for msg in data.split(';').filter(|a| !a.trim().is_empty()) {
1178 match InboxEntry::parse(msg, server_time) {
1179 Ok(msg) => self.mail.inbox.push(msg),
1180 Err(e) => warn!("Invalid msg: {msg} {e}"),
1181 }
1182 }
1183 }
1184 "messagetext" => {
1185 self.mail.open_msg = Some(from_sf_string(val.as_str()));
1186 }
1187 "combatloglist" => {
1188 self.mail.combat_log.clear();
1189 for entry in val.as_str().split(';') {
1190 let parts = entry.split(',').collect::<Vec<_>>();
1191 if parts.iter().all(|a| a.is_empty()) {
1192 continue;
1193 }
1194 match CombatLogEntry::parse(&parts, server_time) {
1195 Ok(cle) => {
1196 self.mail.combat_log.push(cle);
1197 }
1198 Err(e) => {
1199 warn!(
1200 "Unable to parse combat log entry: {parts:?} \
1201 - {e}"
1202 );
1203 }
1204 }
1205 }
1206 }
1207 "maxupgradelevel" => {
1208 self.fortress
1209 .get_or_insert_with(Default::default)
1210 .building_max_lvl = val.into("max upgrade lvl")?;
1211 }
1212 "singleportalenemylevel" => {
1213 self.dungeons
1214 .portal
1215 .get_or_insert_with(Default::default)
1216 .enemy_level = val.into("portal lvl").unwrap_or(u32::MAX);
1217 }
1218 "ownpetsstats" => {
1219 self.pets
1220 .get_or_insert_with(Default::default)
1221 .update_pet_stat(&val.into_list("pet stats")?);
1222 }
1223 "ownpets" => {
1224 let data = val.into_list("own pets")?;
1225 self.pets
1226 .get_or_insert_with(Default::default)
1227 .update(&data, server_time)?;
1228 }
1229 "petsdefensetype" => {
1230 let pet_id = val.into("pet def typ")?;
1231 self.pets
1232 .get_or_insert_with(Default::default)
1233 .opponent
1234 .habitat = Some(HabitatType::from_typ_id(pet_id).ok_or(
1235 SFError::ParsingError("pet def typ", format!("{pet_id}")),
1236 )?);
1237 }
1238 "otherplayersavecharacter" => {
1239 other_player
1240 .get_or_insert_default()
1241 .update(&val.into_list("other player")?, server_time)?;
1242 }
1243 "otherplayersavepotions" => {
1244 other_player.get_or_insert_default().active_potions =
1245 items::parse_active_potions(
1246 &val.into_list("other potions")?,
1247 server_time,
1248 );
1249 }
1250 "otherplayer" => {
1251 let data: Vec<i64> = val.into_list("other player")?;
1252 #[allow(deprecated)]
1253 {
1254 other_player.get_or_insert_default().guild_joined =
1255 data.cstget(166, "other joined guild", server_time)?;
1256 }
1257 }
1258 "otherplayerfriendstatus" => {
1259 other_player
1260 .get_or_insert_with(Default::default)
1261 .relationship = warning_parse(
1262 val.into::<i32>("other friend")?,
1263 "other friend",
1264 FromPrimitive::from_i32,
1265 )
1266 .unwrap_or_default();
1267 }
1268 "otherplayerpetbonus" => {
1269 other_player
1270 .get_or_insert_with(Default::default)
1271 .update_pet_bonus(&val.into_list("o pet bonus")?)?;
1272 }
1273 "otherplayerunitlevel" => {
1274 let data: Vec<i64> =
1275 val.into_list("other player unit level")?;
1276 other_player
1279 .get_or_insert_with(Default::default)
1280 .wall_combat_lvl = data.csiget(0, "wall_lvl", 0)?;
1281 }
1282 "petsrank" => {
1283 self.pets.get_or_insert_with(Default::default).rank =
1284 val.into("pet rank")?;
1285 }
1286
1287 "maxrankUnderworld" => {
1288 self.hall_of_fames.underworlds_total =
1289 Some(val.into("mrank under")?);
1290 }
1291 "otherplayerfortressrank" => {
1292 match val.into::<i64>("other player fortress rank")? {
1293 ..=-1 => {}
1294 x => {
1295 let rank = x.try_into().unwrap_or(1);
1296 other_player
1297 .get_or_insert_default()
1298 .fortress
1299 .get_or_insert_default()
1300 .rank = rank;
1301 }
1302 }
1303 }
1304 "workreward" => {
1305 }
1307 x if x.starts_with("winnerid") => {
1308 let raw_winner_id = val
1311 .as_str()
1312 .split_once(|a: char| !a.is_ascii_digit())
1313 .map_or(val.as_str(), |a| a.0);
1314 if let Ok(winner_id) = raw_winner_id.parse() {
1315 self.get_fight(x).winner_id = winner_id;
1316 } else {
1317 error!("Invalid winner id: {raw_winner_id}");
1318 }
1319 }
1320 "fightresult" => {
1321 let data: Vec<i64> = val.into_list("fight result")?;
1322 self.last_fight
1323 .get_or_insert_with(Default::default)
1324 .update_result(&data, server_time)?;
1325 }
1327 x if x.starts_with("fightheader") => {
1328 self.get_fight(x).update_fighters(val.as_str());
1329 }
1330 "fightgroups" => {
1331 let fight =
1332 self.last_fight.get_or_insert_with(Default::default);
1333 fight.update_groups(val.as_str());
1334 }
1335 "fightadditionalplayers" => {
1336 }
1339 "fightversion" => {
1340 }
1344 x if x.starts_with("fight") && x.len() <= 7 => {
1345 let fight_no = fight_no_from_header(x);
1346 let wkey = format!("winnerid{fight_no}");
1347 let version = if let Some(winner_id) =
1348 all_values.get(wkey.as_str())
1349 {
1350 winner_id.as_str().split_once("fightversion:").map(|a| a.1)
1354 } else {
1355 all_values.get("fightversion").map(|a| a.as_str())
1358 };
1359 let fight = self.get_fight(x);
1360 if let Some(version) = version.and_then(|a| a.parse().ok()) {
1361 fight.update_rounds(val.as_str(), version)?;
1362 } else {
1363 fight.actions.clear();
1364 }
1365 }
1366 "othergroupname" => {
1367 other_guild
1368 .get_or_insert_with(Default::default)
1369 .name
1370 .set(val.as_str());
1371 }
1372 "othergrouprank" => {
1373 other_guild.get_or_insert_with(Default::default).rank =
1374 val.into("other group rank")?;
1375 }
1376 "othergroupfightcost" => {
1377 other_guild.get_or_insert_with(Default::default).attack_cost =
1378 val.into("other group fighting cost")?;
1379 }
1380 "othergroupmember" => {
1381 let names: Vec<_> = val.as_str().split(',').collect();
1382 let og = other_guild.get_or_insert_with(Default::default);
1383 og.members.resize_with(names.len(), Default::default);
1384 for (m, n) in og.members.iter_mut().zip(names) {
1385 m.name.set(n);
1386 }
1387 }
1388 "othergroupdescription" => {
1389 let guild = other_guild.get_or_insert_with(Default::default);
1390 let (emblem, desc) =
1391 val.as_str().split_once('§').unwrap_or(("", val.as_str()));
1392
1393 guild.emblem.update(emblem);
1394 guild.description = from_sf_string(desc);
1395 }
1396 "othergroup" => {
1397 other_guild
1398 .get_or_insert_with(Default::default)
1399 .update(val.as_str(), server_time)?;
1400 }
1401 "reward" => {
1402 }
1405 "gtdailypoints" => {
1406 self.hellevator
1407 .active
1408 .get_or_insert_with(Default::default)
1409 .guild_points_today = val.into("gtdaily").unwrap_or(0);
1410 }
1411 "gtchest" => {
1412 }
1421 "gtraidparticipants" => {
1422 let all: Vec<_> = val.as_str().split('/').collect();
1423 let hellevator =
1424 self.hellevator.active.get_or_insert_with(Default::default);
1425
1426 for floor in &mut hellevator.guild_raid_floors {
1427 floor.today_assigned.clear();
1428 }
1429
1430 #[allow(clippy::indexing_slicing)]
1431 for part in all.chunks_exact(2) {
1432 let name = part[0];
1434 let val: usize = part
1436 .cget(1, "hell raid part")
1437 .ok()
1438 .and_then(|a| a.parse().ok())
1439 .unwrap_or(0);
1440 if val > 0 {
1441 if val > hellevator.guild_raid_floors.len() {
1442 hellevator
1443 .guild_raid_floors
1444 .resize_with(val, Default::default);
1445 }
1446 if let Some(floor) =
1447 hellevator.guild_raid_floors.get_mut(val - 1)
1448 {
1449 floor.today_assigned.push(name.to_string());
1450 }
1451 }
1452 }
1453 }
1454 "gtraidparticipantsyesterday" => {
1455 let all: Vec<_> = val.as_str().split('/').collect();
1456
1457 let hellevator =
1458 self.hellevator.active.get_or_insert_with(Default::default);
1459
1460 for floor in &mut hellevator.guild_raid_floors {
1461 floor.yesterday_assigned.clear();
1462 }
1463
1464 #[allow(clippy::indexing_slicing)]
1465 for part in all.chunks_exact(2) {
1466 let name = part[0];
1468 let val: usize = part
1470 .cget(1, "hell raid part yd")
1471 .ok()
1472 .and_then(|a| a.parse().ok())
1473 .unwrap_or(0);
1474 if val > 0 {
1475 if val > hellevator.guild_raid_floors.len() {
1476 hellevator
1477 .guild_raid_floors
1478 .resize_with(val, Default::default);
1479 }
1480 if let Some(floor) =
1481 hellevator.guild_raid_floors.get_mut(val - 1)
1482 {
1483 floor.yesterday_assigned.push(name.to_string());
1484 }
1485 }
1486 }
1487 }
1488 "gtrank" => {
1489 self.hellevator
1490 .active
1491 .get_or_insert_with(Default::default)
1492 .guild_rank = val.into("gt rank").unwrap_or(0);
1493 }
1494 "gtrankingmax" => {
1495 self.hall_of_fames.hellevator_total =
1496 val.into("gt rank max").ok();
1497 }
1498 "gtbracketlist" => {
1499 self.hellevator
1500 .active
1501 .get_or_insert_with(Default::default)
1502 .brackets =
1503 val.into_list("gtbracketlist").unwrap_or_default();
1504 }
1505 "gtraidfights" => {
1506 let data: Vec<i64> =
1507 val.into_list("gt raids").unwrap_or_default();
1508
1509 let hellevator =
1510 self.hellevator.active.get_or_insert_with(Default::default);
1511
1512 hellevator.guild_raid_signup_start = data
1513 .cstget(0, "h raid signup start", server_time)?
1514 .unwrap_or_default();
1515
1516 hellevator.guild_raid_start = data
1517 .cstget(1, "h raid next attack", server_time)?
1518 .unwrap_or_default();
1519
1520 let start = data.skip(2, "hellevator_fights")?;
1521
1522 let floor_count = start.len() / 5;
1523
1524 if floor_count > hellevator.guild_raid_floors.len() {
1525 hellevator
1526 .guild_raid_floors
1527 .resize_with(floor_count, Default::default);
1528 }
1529 #[allow(clippy::indexing_slicing)]
1530 for (data, floor) in
1531 start.chunks_exact(5).zip(&mut hellevator.guild_raid_floors)
1532 {
1533 floor.today = data[1];
1535 floor.yesterday = data[2];
1536 floor.point_reward =
1537 data.csiget(3, "floor t-reward", 0).unwrap_or(0);
1538 floor.silver_reward =
1539 data.csiget(4, "floor c-reward", 0).unwrap_or(0);
1540 }
1541 }
1542 "gtmonsterreward" => {
1543 let data: Vec<i64> =
1544 val.into_list("gt m reward").unwrap_or_default();
1545
1546 let hellevator =
1547 self.hellevator.active.get_or_insert_with(Default::default);
1548 hellevator.monster_rewards.clear();
1549
1550 for chunk in data.chunks_exact(3) {
1551 let raw_typ = chunk.cget(0, "gt monster reward typ")?;
1552 if raw_typ <= 0 {
1553 continue;
1554 }
1555 let one = chunk
1556 .csiget(1, "gt monster reward typ", 0)
1557 .unwrap_or(0);
1558 if one != 0 {
1559 warn!("hellevator monster t: {one}");
1560 }
1561 let typ = HellevatorMonsterRewardTyp::parse(raw_typ);
1562 let amount: u64 =
1563 chunk.csiget(2, "gt monster reward amount", 0)?;
1564 hellevator
1565 .monster_rewards
1566 .push(HellevatorMonsterReward { typ, amount });
1567 }
1568 }
1569 "gtdailyreward" => {
1570 self.hellevator
1571 .active
1572 .get_or_insert_with(Default::default)
1573 .rewards_today = HellevatorDailyReward::parse(
1574 &val.into_list("hdrtd").unwrap_or_default(),
1575 );
1576 }
1577 "gtdailyrewardnext" => {
1578 self.hellevator
1579 .active
1580 .get_or_insert_with(Default::default)
1581 .rewards_next = HellevatorDailyReward::parse(
1582 &val.into_list("hdrnd").unwrap_or_default(),
1583 );
1584 }
1585 "gtdailyrewardyesterday" => {
1586 self.hellevator
1587 .active
1588 .get_or_insert_with(Default::default)
1589 .rewards_yesterday = HellevatorDailyReward::parse(
1590 &val.into_list("hdryd").unwrap_or_default(),
1591 );
1592 }
1593 "gtdailyrewardclaimed" => {
1594 if let Some(hellevator) = self.hellevator.active.as_mut() {
1595 if !all_values.contains_key("gtdailyreward") {
1601 hellevator.rewards_yesterday = None;
1604 }
1605 }
1606 }
1607 "gtranking" => {
1608 self.hall_of_fames.hellevator = val
1609 .as_str()
1610 .split(';')
1611 .filter(|a| !a.is_empty())
1612 .map(|chunk| chunk.split(',').collect())
1613 .flat_map(|chunk: Vec<_>| -> Result<_, SFError> {
1614 Ok(HallOfFameHellevator {
1615 rank: chunk.cfsuget(0, "hh rank")?,
1616 name: chunk.cget(1, "hh name")?.to_string(),
1617 tokens: chunk.cfsuget(2, "hh tokens")?,
1618 })
1619 })
1620 .collect();
1621 }
1622 "gtpreviewreward" => {
1623 }
1643 "gtmonster" => {
1644 self.hellevator
1645 .active
1646 .get_or_insert_with(Default::default)
1647 .current_monster = HellevatorMonster::parse(
1648 &val.into_list("h monster").unwrap_or_default(),
1649 )
1650 .ok();
1651 }
1652 "gtbonus" => {
1653 self.hellevator
1654 .active
1655 .get_or_insert_with(Default::default)
1656 .daily_treat_bonus = val
1657 .into_list("gt bonus")
1658 .and_then(|a| HellevatorTreatBonus::parse(&a))
1659 .ok();
1660 }
1661 "pendingrewards" => {
1662 let vals: Vec<_> = val.as_str().split('/').collect();
1663 self.mail.claimables = vals
1664 .chunks_exact(6)
1665 .flat_map(|chunk| -> Result<ClaimableMail, SFError> {
1666 let start = chunk.cfsuget(4, "p reward start")?;
1667 let end = chunk.cfsuget(5, "p reward end")?;
1668
1669 let status = match chunk.cget(1, "p read")? {
1670 "0" => ClaimableStatus::Unread,
1671 "1" => ClaimableStatus::Read,
1672 "2" => ClaimableStatus::Claimed,
1673 x => {
1674 warn!("Unknown claimable status: {x}");
1675 ClaimableStatus::Claimed
1676 }
1677 };
1678
1679 Ok(ClaimableMail {
1680 typ: FromPrimitive::from_i64(
1681 chunk.cfsuget(2, "claimable typ")?,
1682 )
1683 .unwrap_or_default(),
1684 msg_id: chunk.cfsuget(0, "msg_id")?,
1685 status,
1686 name: chunk.cget(3, "reward code")?.to_string(),
1687 received: server_time
1688 .convert_to_local(start, "p start"),
1689 claimable_until: server_time
1690 .convert_to_local(end, "p end"),
1691 })
1692 })
1693 .collect();
1694 }
1695 "pendingrewardressources" => {
1696 let vals: Vec<i64> =
1697 val.into_list("pendingrewardressources")?;
1698
1699 self.mail
1700 .open_claimable
1701 .get_or_insert_with(Default::default)
1702 .resources = vals
1703 .chunks_exact(2)
1704 .flat_map(|chunk| -> Result<Reward, SFError> {
1705 Ok(Reward {
1706 typ: RewardType::parse(chunk.cget(0, "c typ")?),
1707 amount: chunk.csiget(1, "c amount", 1)?,
1708 })
1709 })
1710 .collect();
1711 }
1712 "pendingreward" => {
1713 let vals: Vec<i64> = val.into_list("pending item")?;
1714 self.mail
1715 .open_claimable
1716 .get_or_insert_with(Default::default)
1717 .items = vals
1718 .chunks_exact(ITEM_PARSE_LEN)
1719 .flat_map(|a|
1720 Item::parse(a, server_time))
1722 .flatten()
1723 .collect();
1724 }
1725 "fightablegroups" => {
1726 self.guild
1727 .get_or_insert_default()
1728 .update_fightable_targets(val.as_str())?;
1729 }
1730 "adventscalendar" => {
1731 let vals: Vec<i64> = val.into_list("advent door")?;
1732 self.specials.advent_calendar = match vals.first() {
1733 Some(0) | None => None,
1734 _ => Reward::parse(&vals).ok(),
1735 };
1736 }
1737 "fortresschances" => {
1738 }
1742 "deedsandtitlesplayersave" => {
1743 }
1747 "deedshelves" => {
1748 }
1751 "titleinfos" => {
1752 }
1765 "titleplayerinfos" => {
1766
1767 }
1769 "fortressstorage" => {
1770 self.fortress.get_or_insert_default().update_resources(
1771 &val.into_list("ft resources")?,
1772 server_time,
1773 )?;
1774 }
1775 "fortressunits" => {
1776 self.fortress
1777 .get_or_insert_default()
1778 .update_units(&val.into_list("ft units")?, server_time)?;
1779 }
1780 "fortress" => {
1781 self.fortress
1782 .get_or_insert_default()
1783 .update(&val.into_list("fortress")?, server_time)?;
1784 }
1785 "wheel" => {
1786 let data: Vec<i64> = val.into_list("wheel")?;
1787 self.specials.wheel.spins_today =
1789 data.csiget(1, "lucky turns", 0)?;
1790 self.specials.wheel.next_free_spin =
1791 data.cstget(2, "next lucky turn", server_time)?;
1792 }
1793 "dice" => {
1794 let data: Vec<i64> = val.into_list("dice")?;
1795 self.tavern.dice_game.next_free =
1796 data.cstget(0, "dice next", server_time)?;
1797 self.tavern.dice_game.remaining =
1798 data.csiget(1, "rem dice games", 0)?;
1799 }
1800 "charactergroup" => {
1801 let data: Vec<i64> = val.into_list("c group")?;
1802 let guild = self.guild.get_or_insert_with(Default::default);
1803 guild.own_treasure_skill =
1804 data.csiget(0, "own treasure skill", 0)?;
1805 guild.own_instructor_skill =
1806 data.csiget(1, "own instruction skill", 0)?;
1807 guild.hydra.next_battle =
1808 data.cstget(2, "pet battle", server_time)?;
1809 guild.hydra.remaining_fights =
1810 data.csiget(3, "remaining pet battles", 0)?;
1811 guild.own_pet_lvl = data.csiget(4, "own pet lvl", 0)?;
1812 guild.joined = data.cstget(5, "guild joined", server_time)?;
1813 }
1815 "arena" => {
1816 let data: Vec<i64> = val.into_list("arena")?;
1817 self.arena.next_free_fight =
1818 data.cstget(0, "next battle time", server_time)?;
1819 self.arena.fights_for_xp =
1820 data.csiget(1, "arena xp fights", 0)?;
1821 for (idx, val) in self.arena.enemy_ids.iter_mut().enumerate() {
1822 *val = data.csiget(2 + idx, "arena enemy id", 0)?;
1823 }
1824 }
1826 "ownplayersavepotions" => {
1827 let data: Vec<i64> = val.into_list("potions")?;
1828 self.character.active_potions =
1829 items::parse_active_potions(&data, server_time);
1830 }
1831 "arcanetoilet" => {
1832 let data: Vec<i64> = val.into_list("toilet")?;
1833
1834 let toilet_lvl = data.cget(0, "toilet lvl")?;
1836 if toilet_lvl > 0 {
1837 self.tavern
1838 .toilet
1839 .get_or_insert_with(Default::default)
1840 .update(&data, server_time)?;
1841 }
1842 }
1843 "vipstatus" => {
1844 other_player.get_or_insert_default().is_vip =
1845 val.as_str() != "0";
1846 }
1847 "characterstatus" => {
1848 let data: Vec<i64> = val.into_list("char status")?;
1849
1850 self.tavern.current_action = CurrentAction::parse(
1851 data.cget(1, "action id")?,
1852 data.cget(2, "action sec")?,
1853 data.cstget(3, "current action time", server_time)?,
1854 );
1855
1856 self.tavern.beer_max = data.csiget(5, "beer total", 0)?;
1858
1859 self.tavern.thirst_for_adventure_sec =
1860 data.csiget(6, "remaining ALU", 0)?;
1861 self.tavern.beer_drunk =
1862 data.csiget(7, "beer drunk count", 0)?;
1863 self.specials.calendar.collected =
1864 data.csiget(8, "calendar collected", 245)?;
1865 self.specials.calendar.next_possible =
1866 data.cstget(9, "calendar next", server_time)?;
1867 self.pets
1878 .get_or_insert_with(Default::default)
1879 .next_free_exploration =
1880 data.cstget(20, "pet next free exp", server_time)?;
1881 self.dungeons.next_free_fight =
1882 data.cstget(21, "dungeon timer", server_time)?;
1883 if let Some(start) =
1884 data.cstget(22, "dungeon timer", server_time)?
1885 {
1886 self.legendary_dungeon
1887 .active
1888 .get_or_insert_default()
1889 .healing_start = Some(start);
1890 }
1891 }
1901 "ownplayersavecharacter" => {
1902 let data: Vec<i64> = val.into_list("char save")?;
1903
1904 self.character.player_id = data.csiget(1, "player id", 0)?;
1906 self.character.level =
1908 data.csimget(3, "level", 0, |a| a & 0xFFFF)?;
1909 self.character.experience = data.csiget(4, "experience", 0)?;
1910 self.character.next_level_xp =
1911 data.csiget(5, "xp to next lvl", 0)?;
1912 self.character.honor = data.csiget(6, "honor", 0)?;
1913 self.character.rank = data.csiget(7, "rank", 0)?;
1914 self.character.portrait =
1915 Portrait::parse(data.skip(8, "portrait")?)
1916 .unwrap_or_default();
1917 self.character.race = data.cfpuget(18, "char race", |a| a)?;
1929 self.character.class =
1932 data.cfpuget(20, "character class", |a| a - 1)?;
1933 self.character.mount =
1934 data.cfpget(21, "character mount", |a| a & 0xFF)?;
1935 self.character.armor = data.csiget(23, "total armor", 0)?;
1937 self.character.min_damage = data.csiget(24, "min damage", 0)?;
1938 self.character.max_damage = data.csiget(25, "max damage", 0)?;
1939 self.guild
1940 .get_or_insert_with(Default::default)
1941 .portal
1942 .damage_bonus =
1943 data.cimget(26, "portal dmg bonus", |a| a)?;
1944 self.dungeons
1946 .portal
1947 .get_or_insert_with(Default::default)
1948 .player_hp_bonus =
1949 data.csimget(28, "portal hp bonus", 0, |a| a)?;
1950 self.character.mount_end =
1951 data.cstget(29, "mount end", server_time)?;
1952 update_enum_map(
1953 &mut self.character.attribute_basis,
1954 data.skip(30, "char attr basis")?,
1955 );
1956 update_enum_map(
1957 &mut self.character.attribute_additions,
1958 data.skip(35, "char attr adds")?,
1959 );
1960 update_enum_map(
1961 &mut self.character.attribute_times_bought,
1962 data.skip(40, "char attr tb")?,
1963 );
1964 }
1990 "adventure" => {
1991 let data: Vec<i64> = val.into_list("char save")?;
1992 for (slice, quest) in data
1995 .skip(2, "quests")?
1996 .chunks_exact(7)
1997 .zip(&mut self.tavern.quests)
1998 {
1999 quest.update(slice)?;
2000 }
2001 }
2002 "events" => {
2003 let data: Vec<i64> = val.into_list("events")?;
2004 if data.len() < 8 {
2005 return Ok(());
2006 }
2007 self.specials.events.active.clear();
2009 let flags = data.cget(1, "events")?;
2010 for (idx, event) in Event::iter().enumerate() {
2011 if (flags & (1 << idx)) > 0 {
2012 self.specials.events.active.insert(event);
2013 }
2014 }
2015 self.specials.events.ends =
2019 data.cstget(4, "event end", server_time)?;
2020
2021 }
2023 "tavernspecialend" | "tavernspecialsub" | "tavernspecial" => {
2024 }
2026 "subscriptionstatus" => {}
2027 "iadungeonchances" => {
2029 }
2031 "iadungeontime" => {
2032 let dungeons = &mut self.legendary_dungeon;
2033
2034 let vals: Vec<i64> = val.into_list("iadungeontime")?;
2035 dungeons.theme = vals.cfpget(0, "ld theme", |x| x)?;
2036 dungeons.start = vals.cstget(1, "ld start", server_time)?;
2037 dungeons.end = vals.cstget(2, "ld end", server_time)?;
2038 dungeons.close = vals.cstget(3, "ld closes", server_time)?;
2039 }
2040 "iadungeonstatstotal" => {
2041 let dungeons =
2042 self.legendary_dungeon.active.get_or_insert_default();
2043
2044 let data: Vec<i64> = val.into_list("iadungeonstatstotal")?;
2045 dungeons.total_stats = TotalStats::parse(&data)?;
2046 }
2047 "iadungeonstats" => {
2048 let dungeons =
2049 self.legendary_dungeon.active.get_or_insert_default();
2050
2051 let data = val.into_list("iadungeonstats")?;
2052 dungeons.stats = Stats::parse(&data).unwrap_or_default();
2053 }
2054 "iadungeon" => {
2055 let data: Vec<i64> = val.into_list("iadungeon")?;
2056 let dungeons =
2057 self.legendary_dungeon.active.get_or_insert_default();
2058 dungeons.update(&data)?;
2059 if !all_values.contains_key("iapendingitems") {
2060 dungeons.pending_items.clear();
2061 }
2062 }
2063 "iapendingitems" => {
2064 let dungeons =
2065 self.legendary_dungeon.active.get_or_insert_default();
2066 dungeons.pending_items.clear();
2067 let data: Vec<i64> = val.into_list("iapendingitems")?;
2068 let amount: i64 = data.cget(0, "pending amount")?;
2069 if amount < 1 {
2070 return Ok(());
2071 }
2072 for slice in
2073 data.skip(1, "ld items")?.chunks_exact(ITEM_PARSE_LEN)
2074 {
2075 let Some(item) = Item::parse(slice, server_time)? else {
2076 warn!("Could not parse pending ld item");
2077 continue;
2078 };
2079 dungeons.pending_items.push(item);
2080 }
2081 }
2082 "ialootitem" => {
2083 }
2085 "iamerchant" => {
2086 let data: Vec<i64> = val.into_list("iamerchant")?;
2087
2088 self.legendary_dungeon
2089 .active
2090 .get_or_insert_default()
2091 .merchant_offers = data
2092 .chunks_exact(3)
2093 .flat_map(MerchantOffer::parse)
2094 .flatten()
2095 .collect();
2096 }
2097 "iadungeon20cost" => {
2098 self.legendary_dungeon
2099 .active
2100 .get_or_insert_default()
2101 .heal_quarter_cost = val.into("iadungeon20cost")?;
2102 }
2103 "iadungeonsoulstones" => {
2104 let dungeons =
2105 self.legendary_dungeon.active.get_or_insert_default();
2106
2107 let data: Vec<i64> = val.into_list("iamerchant")?;
2108 let mut chunks = data.chunks_exact(6);
2109 dungeons.active_gems = chunks
2110 .by_ref()
2111 .take(3)
2112 .flat_map(GemOfFate::parse)
2113 .flatten()
2114 .collect();
2115
2116 dungeons.available_gems =
2117 chunks.flat_map(GemOfFate::parse).flatten().collect();
2118 }
2119 "iamap" => {
2120 }
2137 "otherplayerfortressinfo" => {
2138 other_player
2139 .get_or_insert_default()
2140 .update_fortress(&val.into_list("other ft")?)?;
2141 }
2142 x if x.contains("average") && x.ends_with("level") => {
2143 }
2145 x if x.contains("dungeonenemies") => {
2147 }
2149 x if x.starts_with("attbonus") => {
2150 }
2152 x => {
2153 warn!("Update ignored {x} -> {val:?}");
2154 }
2155 }
2156 Ok(())
2157 }
2158}
2159
2160fn fight_no_from_header(header_name: &str) -> usize {
2163 let number_str =
2164 header_name.trim_start_matches(|a: char| !a.is_ascii_digit());
2165 let id: usize = number_str.parse().unwrap_or(1);
2166 id.max(1)
2167}
2168
2169#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2174pub struct ServerTime(i64);
2175
2176impl ServerTime {
2177 #[must_use]
2179 pub(crate) fn convert_to_local(
2180 self,
2181 timestamp: i64,
2182 name: &str,
2183 ) -> Option<DateTime<Local>> {
2184 if matches!(timestamp, 0 | -1 | 1 | 11) {
2185 return None;
2187 }
2188
2189 if !(1_000_000_000..=3_000_000_000).contains(×tamp) {
2190 warn!("Weird time stamp: {timestamp} for {name}");
2191 return None;
2192 }
2193 DateTime::from_timestamp(timestamp - self.0, 0)?
2194 .naive_utc()
2195 .and_local_timezone(Local)
2196 .latest()
2197 }
2198
2199 #[must_use]
2204 pub fn current(&self) -> NaiveDateTime {
2205 Local::now().naive_local() + Duration::seconds(self.0)
2206 }
2207
2208 #[must_use]
2209 pub fn next_midnight(&self) -> std::time::Duration {
2210 let current = self.current();
2211 let tomorrow = current.date() + Duration::days(1);
2212 let tomorrow = NaiveDateTime::from(tomorrow);
2213 let sec_until_midnight =
2214 (tomorrow - current).to_std().unwrap_or_default().as_secs();
2215 std::time::Duration::from_secs(sec_until_midnight % (60 * 60 * 24))
2218 }
2219}
2220
2221trait StringSetExt {
2223 fn set(&mut self, s: &str);
2224}
2225
2226impl StringSetExt for String {
2227 fn set(&mut self, s: &str) {
2231 self.replace_range(.., s);
2232 }
2233}
2234
2235#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2237#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2238pub struct NormalCost {
2239 pub silver: u64,
2241 pub mushrooms: u16,
2243}