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