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 }
738 "owngroupdescription" => self
739 .guild
740 .get_or_insert_with(Default::default)
741 .update_description_embed(val.as_str()),
742 "idle" => {
743 self.idle_game = IdleGame::parse_idle_game(
744 &val.into_list("idle game")?,
745 server_time,
746 );
747 }
748 "resources" => {
749 self.update_resources(&val.into_list("resources")?)?;
750 }
751 "chattime" => {
752 }
758 "maxpetlevel" => {
759 self.pets.get_or_insert_with(Default::default).max_pet_level =
760 val.into("max pet lvl")?;
761 }
762 "otherdescription" => {
763 other_player
764 .get_or_insert_with(Default::default)
765 .description = from_sf_string(val.as_str());
766 }
767 "otherplayergroupname" => {
768 let guild =
769 Some(val.as_str().to_string()).filter(|a| !a.is_empty());
770 other_player.get_or_insert_with(Default::default).guild = guild;
771 }
772 "otherplayername" => {
773 other_player
774 .get_or_insert_with(Default::default)
775 .name
776 .set(val.as_str());
777 }
778 "otherplayersaveequipment" => {
779 let data: Vec<i64> = val.into_list("other player equipment")?;
780 other_player.get_or_insert_with(Default::default).equipment =
781 Equipment::parse(&data, server_time)?;
782 }
783 "fortresspricereroll" => {
784 self.fortress
785 .get_or_insert_with(Default::default)
786 .opponent_reroll_price = val.into("fortress reroll")?;
787 }
788 "fortresswalllevel" => {
789 self.fortress
790 .get_or_insert_with(Default::default)
791 .wall_combat_lvl = val.into("fortress wall lvl")?;
792 }
793 "dragongoldbonus" => {
794 self.character.mount_dragon_refund = val.into("dragon gold")?;
795 }
796 "wheelresult" => {
797 let upgraded = self.character.level >= 95
800 && self.pets.is_some()
801 && self.underworld.is_some();
802 self.specials.wheel.result = Some(WheelReward::parse(
803 &val.into_list("wheel result")?,
804 upgraded,
805 )?);
806 }
807 "dailyreward" => {
808 }
810 "calenderreward" => {
811 }
813 "oktoberfest" => {
814 if !val.as_str().is_empty() {
817 warn!("oktoberfest response is not empty: {val}");
818 }
819 }
820 "usersettings" => {
821 let vals: Vec<_> = val.as_str().split('/').collect();
823 let v = match vals.as_slice().cget(4, "questing setting")? {
824 "a" => ExpeditionSetting::PreferExpeditions,
825 "0" | "b" => ExpeditionSetting::PreferQuests,
826 x => {
827 error!("Weird expedition settings: {x}");
828 ExpeditionSetting::PreferQuests
829 }
830 };
831 self.tavern.questing_preference = v;
832 }
833 "mailinvoice" => {
834 }
836 "calenderinfo" => {
837 let data: Vec<i64> = val.into_list("calendar")?;
840 self.specials.calendar.rewards.clear();
841 for p in data.chunks_exact(2) {
842 let reward = CalendarReward::parse(p)?;
843 self.specials.calendar.rewards.push(reward);
844 }
845 }
846 "othergroupattack" => {
847 other_guild.get_or_insert_with(Default::default).attacks =
848 Some(val.to_string());
849 }
850 "othergroupdefense" => {
851 other_guild
852 .get_or_insert_with(Default::default)
853 .defends_against = Some(val.to_string());
854 }
855 "inboxcapacity" => {
856 self.mail.inbox_capacity = val.into("inbox cap")?;
857 }
858 "magicregistration" => {
859 }
862 "Ranklistplayer" => {
863 self.hall_of_fames.players.clear();
864 for player in val.as_str().trim_matches(';').split(';') {
865 if player.ends_with(",,,0,0,0,") {
867 break;
868 }
869
870 match HallOfFamePlayer::parse(player) {
871 Ok(x) => {
872 self.hall_of_fames.players.push(x);
873 }
874 Err(err) => warn!("{err}"),
875 }
876 }
877 }
878 "ranklistgroup" => {
879 self.hall_of_fames.guilds.clear();
880 for guild in val.as_str().trim_matches(';').split(';') {
881 match HallOfFameGuild::parse(guild) {
882 Ok(x) => {
883 self.hall_of_fames.guilds.push(x);
884 }
885 Err(err) => warn!("{err}"),
886 }
887 }
888 }
889 "maxrankgroup" => {
890 self.hall_of_fames.guilds_total = Some(val.into("guild max")?);
891 }
892 "maxrankPets" => {
893 self.hall_of_fames.pets_total = Some(val.into("pet rank max")?);
894 }
895 "RanklistPets" => {
896 self.hall_of_fames.pets.clear();
897 for entry in val.as_str().trim_matches(';').split(';') {
898 match HallOfFamePets::parse(entry) {
899 Ok(x) => {
900 self.hall_of_fames.pets.push(x);
901 }
902 Err(err) => warn!("{err}"),
903 }
904 }
905 }
906 "ranklistfortress" | "Ranklistfortress" => {
907 self.hall_of_fames.fortresses.clear();
908 for guild in val.as_str().trim_matches(';').split(';') {
909 match HallOfFameFortress::parse(guild) {
910 Ok(x) => {
911 self.hall_of_fames.fortresses.push(x);
912 }
913 Err(err) => warn!("{err}"),
914 }
915 }
916 }
917 "ranklistunderworld" => {
918 self.hall_of_fames.underworlds.clear();
919 for entry in val.as_str().trim_matches(';').split(';') {
920 match HallOfFameUnderworld::parse(entry) {
921 Ok(x) => {
922 self.hall_of_fames.underworlds.push(x);
923 }
924 Err(err) => warn!("{err}"),
925 }
926 }
927 }
928 "gamblegoldvalue" => {
929 self.tavern.gamble_result =
930 Some(GambleResult::SilverChange(val.into("gold gamble")?));
931 }
932 "gamblecoinvalue" => {
933 self.tavern.gamble_result = Some(GambleResult::MushroomChange(
934 val.into("gold gamble")?,
935 ));
936 }
937 "maxrankFortress" => {
938 self.hall_of_fames.fortresses_total =
939 Some(val.into("fortress max")?);
940 }
941 "underworldprice" => self
942 .underworld
943 .get_or_insert_with(Default::default)
944 .update_building_prices(&val.into_list("ub prices")?)?,
945 "owngroupknights" => self
946 .guild
947 .get_or_insert_with(Default::default)
948 .update_group_knights(val.as_str()),
949 "friendlist" => self.updatete_relation_list(val.as_str()),
950 "legendaries" => {
951 if val.as_str().chars().any(|a| a != 'A') {
952 warn!("Found a legendaries value, that is not just AAA..");
953 }
954 }
955 "smith" => {
956 let data: Vec<i64> = val.into_list("smith")?;
957 let bs = self.blacksmith.get_or_insert_with(Default::default);
958
959 bs.dismantle_left = data.csiget(0, "dismantles left", 0)?;
960 bs.last_dismantled = data.cstget(1, "bs time", server_time)?;
961 }
962 "fortressGroupPrice" => {
963 self.fortress
964 .get_or_insert_with(Default::default)
965 .hall_of_knights_upgrade_price = FortressCost::parse(
966 &val.into_list("hall of knights prices")?,
967 )?;
968 }
969 "goldperhournextlevel" => {
970 }
972 "underworldmaxsouls" => {
973 }
975 "dailytaskrewardpreview" => {
976 let vals: Vec<i64> =
977 val.into_list("event task reward preview")?;
978 self.specials.tasks.daily.rewards = parse_rewards(&vals);
979 }
980 "expeditionevent" => {
981 let data: Vec<i64> = val.into_list("exp event")?;
982 self.tavern.expeditions.start =
983 data.cstget(0, "expedition start", server_time)?;
984 let end = data.cstget(1, "expedition end", server_time)?;
985 self.tavern.expeditions.end = end;
986 }
987 "expeditions" => {
988 let data: Vec<i64> = val.into_list("exp event")?;
989
990 if !data.len().is_multiple_of(8) {
991 warn!(
992 "Available expeditions have weird size: {data:?} {}",
993 data.len()
994 );
995 }
996 self.tavern.expeditions.available = data
997 .chunks_exact(8)
998 .map(|data| {
999 Ok(AvailableExpedition {
1000 target: data
1001 .cfpget(0, "expedition typ", |a| a)?
1002 .unwrap_or_default(),
1003 location_1: data
1004 .cfpget(4, "exp loc 1", |a| a)?
1005 .unwrap_or_default(),
1006 location_2: data
1007 .cfpget(5, "exp loc 2", |a| a)?
1008 .unwrap_or_default(),
1009 thirst_for_adventure_sec: data
1010 .csiget(6, "exp alu", 600)?,
1011 special: data.cfpget(7, "exp special", |a| a)?,
1012 })
1013 })
1014 .collect::<Result<_, _>>()?;
1015 }
1016 "expeditionrewardresources" => {
1017 }
1020 "expeditionreward" => {
1021 }
1029 "expeditionmonster" => {
1030 let data: Vec<i64> = val.into_list("expedition monster")?;
1031 let exp = self
1032 .tavern
1033 .expeditions
1034 .active
1035 .get_or_insert_with(Default::default);
1036
1037 exp.boss = ExpeditionBoss {
1038 id: data
1039 .cfpget(0, "expedition monster", |a| -a)?
1040 .unwrap_or_default(),
1041 items: soft_into(
1042 data.get(1).copied().unwrap_or_default(),
1043 "exp monster items",
1044 3,
1045 ),
1046 };
1047 }
1048 "expeditionhalftime" => {
1049 let data: Vec<i64> = val.into_list("halftime exp")?;
1050 let exp = self
1051 .tavern
1052 .expeditions
1053 .active
1054 .get_or_insert_with(Default::default);
1055
1056 exp.halftime_for_boss_id =
1057 -data.cget(0, "halftime for boss id")?;
1058 exp.rewards = data
1059 .skip(1, "halftime choice")?
1060 .chunks_exact(2)
1061 .map(Reward::parse)
1062 .collect::<Result<_, _>>()?;
1063 }
1064 "expeditionstate" => {
1065 let data: Vec<i64> = val.into_list("exp state")?;
1066 let exp = self
1067 .tavern
1068 .expeditions
1069 .active
1070 .get_or_insert_with(Default::default);
1071 exp.floor_stage = data.cget(2, "floor stage")?;
1072
1073 exp.target_thing = data
1074 .cfpget(3, "expedition target", |a| a)?
1075 .unwrap_or_default();
1076 exp.target_current = data.csiget(7, "exp current", 100)?;
1077 exp.target_amount = data.csiget(8, "exp target", 100)?;
1078
1079 exp.current_floor = data.csiget(0, "clearing", 0)?;
1080 exp.heroism = data.csiget(13, "heroism", 0)?;
1081
1082 exp.busy_since = data.cstget(15, "exp start", server_time)?;
1083 exp.busy_until = data.cstget(16, "exp busy", server_time)?;
1084
1085 for (x, item) in data
1086 .skip(9, "exp items")?
1087 .iter()
1088 .copied()
1089 .zip(&mut exp.items)
1090 {
1091 *item = match FromPrimitive::from_i64(x) {
1092 None if x != 0 => {
1093 warn!("Unknown item: {x}");
1094 Some(ExpeditionThing::Unknown)
1095 }
1096 x => x,
1097 };
1098 }
1099 }
1100 "expeditioncrossroad" => {
1101 let data: Vec<i64> = val.into_list("cross")?;
1103 let exp = self
1104 .tavern
1105 .expeditions
1106 .active
1107 .get_or_insert_with(Default::default);
1108 exp.update_encounters(&data);
1109 }
1110 "eventtasklist" => {
1111 let data: Vec<i64> = val.into_list("etl")?;
1112 self.specials.tasks.event.tasks.clear();
1113 for c in data.chunks_exact(4) {
1114 let task = Task::parse(c)?;
1115 self.specials.tasks.event.tasks.push(task);
1116 }
1117 }
1118 "eventtaskrewardpreview" => {
1119 let vals: Vec<i64> =
1120 val.into_list("event task reward preview")?;
1121
1122 self.specials.tasks.event.rewards = parse_rewards(&vals);
1123 }
1124 "dailytasklist" => {
1125 let data: Vec<i64> = val.into_list("daily tasks list")?;
1126 self.specials.tasks.daily.tasks.clear();
1127
1128 for d in data.skip(1, "daily tasks")?.chunks_exact(4) {
1131 self.specials.tasks.daily.tasks.push(Task::parse(d)?);
1132 }
1133 }
1134 "eventtaskinfo" => {
1135 let data: Vec<i64> = val.into_list("eti")?;
1136 self.specials.tasks.event.theme = data
1137 .cfpget(2, "event task theme", |a| a)?
1138 .unwrap_or(EventTaskTheme::Unknown);
1139 self.specials.tasks.event.start =
1140 data.cstget(0, "event t start", server_time)?;
1141 self.specials.tasks.event.end =
1142 data.cstget(1, "event t end", server_time)?;
1143 }
1144 "scrapbook" => {
1145 self.character.scrapbook = ScrapBook::parse(val.as_str());
1146 }
1147 "dungeonfaces" | "shadowfaces" => {
1148 }
1152 "messagelist" => {
1153 let data = val.as_str();
1154 self.mail.inbox.clear();
1155 for msg in data.split(';').filter(|a| !a.trim().is_empty()) {
1156 match InboxEntry::parse(msg, server_time) {
1157 Ok(msg) => self.mail.inbox.push(msg),
1158 Err(e) => warn!("Invalid msg: {msg} {e}"),
1159 }
1160 }
1161 }
1162 "messagetext" => {
1163 self.mail.open_msg = Some(from_sf_string(val.as_str()));
1164 }
1165 "combatloglist" => {
1166 self.mail.combat_log.clear();
1167 for entry in val.as_str().split(';') {
1168 let parts = entry.split(',').collect::<Vec<_>>();
1169 if parts.iter().all(|a| a.is_empty()) {
1170 continue;
1171 }
1172 match CombatLogEntry::parse(&parts, server_time) {
1173 Ok(cle) => {
1174 self.mail.combat_log.push(cle);
1175 }
1176 Err(e) => {
1177 warn!(
1178 "Unable to parse combat log entry: {parts:?} \
1179 - {e}"
1180 );
1181 }
1182 }
1183 }
1184 }
1185 "maxupgradelevel" => {
1186 self.fortress
1187 .get_or_insert_with(Default::default)
1188 .building_max_lvl = val.into("max upgrade lvl")?;
1189 }
1190 "singleportalenemylevel" => {
1191 self.dungeons
1192 .portal
1193 .get_or_insert_with(Default::default)
1194 .enemy_level = val.into("portal lvl").unwrap_or(u32::MAX);
1195 }
1196 "ownpetsstats" => {
1197 self.pets
1198 .get_or_insert_with(Default::default)
1199 .update_pet_stat(&val.into_list("pet stats")?);
1200 }
1201 "ownpets" => {
1202 let data = val.into_list("own pets")?;
1203 self.pets
1204 .get_or_insert_with(Default::default)
1205 .update(&data, server_time)?;
1206 }
1207 "petsdefensetype" => {
1208 let pet_id = val.into("pet def typ")?;
1209 self.pets
1210 .get_or_insert_with(Default::default)
1211 .opponent
1212 .habitat = Some(HabitatType::from_typ_id(pet_id).ok_or(
1213 SFError::ParsingError("pet def typ", format!("{pet_id}")),
1214 )?);
1215 }
1216 "otherplayersavecharacter" => {
1217 other_player
1218 .get_or_insert_default()
1219 .update(&val.into_list("other player")?, server_time)?;
1220 }
1221 "otherplayersavepotions" => {
1222 other_player.get_or_insert_default().active_potions =
1223 items::parse_active_potions(
1224 &val.into_list("other potions")?,
1225 server_time,
1226 );
1227 }
1228 "otherplayer" => {
1229 let data: Vec<i64> = val.into_list("other player")?;
1230 #[allow(deprecated)]
1231 {
1232 other_player.get_or_insert_default().guild_joined =
1233 data.cstget(166, "other joined guild", server_time)?;
1234 }
1235 }
1236 "otherplayerfriendstatus" => {
1237 other_player
1238 .get_or_insert_with(Default::default)
1239 .relationship = warning_parse(
1240 val.into::<i32>("other friend")?,
1241 "other friend",
1242 FromPrimitive::from_i32,
1243 )
1244 .unwrap_or_default();
1245 }
1246 "otherplayerpetbonus" => {
1247 other_player
1248 .get_or_insert_with(Default::default)
1249 .update_pet_bonus(&val.into_list("o pet bonus")?)?;
1250 }
1251 "otherplayerunitlevel" => {
1252 let data: Vec<i64> =
1253 val.into_list("other player unit level")?;
1254 other_player
1257 .get_or_insert_with(Default::default)
1258 .wall_combat_lvl = data.csiget(0, "wall_lvl", 0)?;
1259 }
1260 "petsrank" => {
1261 self.pets.get_or_insert_with(Default::default).rank =
1262 val.into("pet rank")?;
1263 }
1264
1265 "maxrankUnderworld" => {
1266 self.hall_of_fames.underworlds_total =
1267 Some(val.into("mrank under")?);
1268 }
1269 "otherplayerfortressrank" => {
1270 match val.into::<i64>("other player fortress rank")? {
1271 ..=-1 => {}
1272 x => {
1273 let rank = x.try_into().unwrap_or(1);
1274 other_player
1275 .get_or_insert_default()
1276 .fortress
1277 .get_or_insert_default()
1278 .rank = rank;
1279 }
1280 }
1281 }
1282 "workreward" => {
1283 }
1285 x if x.starts_with("winnerid") => {
1286 let raw_winner_id = val
1289 .as_str()
1290 .split_once(|a: char| !a.is_ascii_digit())
1291 .map_or(val.as_str(), |a| a.0);
1292 if let Ok(winner_id) = raw_winner_id.parse() {
1293 self.get_fight(x).winner_id = winner_id;
1294 } else {
1295 error!("Invalid winner id: {raw_winner_id}");
1296 }
1297 }
1298 "fightresult" => {
1299 let data: Vec<i64> = val.into_list("fight result")?;
1300 self.last_fight
1301 .get_or_insert_with(Default::default)
1302 .update_result(&data, server_time)?;
1303 }
1305 x if x.starts_with("fightheader") => {
1306 self.get_fight(x).update_fighters(val.as_str());
1307 }
1308 "fightgroups" => {
1309 let fight =
1310 self.last_fight.get_or_insert_with(Default::default);
1311 fight.update_groups(val.as_str());
1312 }
1313 "fightadditionalplayers" => {
1314 }
1317 "fightversion" => {
1318 }
1322 x if x.starts_with("fight") && x.len() <= 7 => {
1323 let fight_no = fight_no_from_header(x);
1324 let wkey = format!("winnerid{fight_no}");
1325 let version = if let Some(winner_id) =
1326 all_values.get(wkey.as_str())
1327 {
1328 winner_id.as_str().split_once("fightversion:").map(|a| a.1)
1332 } else {
1333 all_values.get("fightversion").map(|a| a.as_str())
1336 };
1337 let fight = self.get_fight(x);
1338 if let Some(version) = version.and_then(|a| a.parse().ok()) {
1339 fight.update_rounds(val.as_str(), version)?;
1340 } else {
1341 fight.actions.clear();
1342 }
1343 }
1344 "othergroupname" => {
1345 other_guild
1346 .get_or_insert_with(Default::default)
1347 .name
1348 .set(val.as_str());
1349 }
1350 "othergrouprank" => {
1351 other_guild.get_or_insert_with(Default::default).rank =
1352 val.into("other group rank")?;
1353 }
1354 "othergroupfightcost" => {
1355 other_guild.get_or_insert_with(Default::default).attack_cost =
1356 val.into("other group fighting cost")?;
1357 }
1358 "othergroupmember" => {
1359 let names: Vec<_> = val.as_str().split(',').collect();
1360 let og = other_guild.get_or_insert_with(Default::default);
1361 og.members.resize_with(names.len(), Default::default);
1362 for (m, n) in og.members.iter_mut().zip(names) {
1363 m.name.set(n);
1364 }
1365 }
1366 "othergroupdescription" => {
1367 let guild = other_guild.get_or_insert_with(Default::default);
1368 let (emblem, desc) =
1369 val.as_str().split_once('§').unwrap_or(("", val.as_str()));
1370
1371 guild.emblem.update(emblem);
1372 guild.description = from_sf_string(desc);
1373 }
1374 "othergroup" => {
1375 other_guild
1376 .get_or_insert_with(Default::default)
1377 .update(val.as_str(), server_time)?;
1378 }
1379 "reward" => {
1380 }
1383 "gtdailypoints" => {
1384 self.hellevator
1385 .active
1386 .get_or_insert_with(Default::default)
1387 .guild_points_today = val.into("gtdaily").unwrap_or(0);
1388 }
1389 "gtchest" => {
1390 }
1399 "gtraidparticipants" => {
1400 let all: Vec<_> = val.as_str().split('/').collect();
1401 let hellevator =
1402 self.hellevator.active.get_or_insert_with(Default::default);
1403
1404 for floor in &mut hellevator.guild_raid_floors {
1405 floor.today_assigned.clear();
1406 }
1407
1408 #[allow(clippy::indexing_slicing)]
1409 for part in all.chunks_exact(2) {
1410 let name = part[0];
1412 let val: usize = part
1414 .cget(1, "hell raid part")
1415 .ok()
1416 .and_then(|a| a.parse().ok())
1417 .unwrap_or(0);
1418 if val > 0 {
1419 if val > hellevator.guild_raid_floors.len() {
1420 hellevator
1421 .guild_raid_floors
1422 .resize_with(val, Default::default);
1423 }
1424 if let Some(floor) =
1425 hellevator.guild_raid_floors.get_mut(val - 1)
1426 {
1427 floor.today_assigned.push(name.to_string());
1428 }
1429 }
1430 }
1431 }
1432 "gtraidparticipantsyesterday" => {
1433 let all: Vec<_> = val.as_str().split('/').collect();
1434
1435 let hellevator =
1436 self.hellevator.active.get_or_insert_with(Default::default);
1437
1438 for floor in &mut hellevator.guild_raid_floors {
1439 floor.yesterday_assigned.clear();
1440 }
1441
1442 #[allow(clippy::indexing_slicing)]
1443 for part in all.chunks_exact(2) {
1444 let name = part[0];
1446 let val: usize = part
1448 .cget(1, "hell raid part yd")
1449 .ok()
1450 .and_then(|a| a.parse().ok())
1451 .unwrap_or(0);
1452 if val > 0 {
1453 if val > hellevator.guild_raid_floors.len() {
1454 hellevator
1455 .guild_raid_floors
1456 .resize_with(val, Default::default);
1457 }
1458 if let Some(floor) =
1459 hellevator.guild_raid_floors.get_mut(val - 1)
1460 {
1461 floor.yesterday_assigned.push(name.to_string());
1462 }
1463 }
1464 }
1465 }
1466 "gtrank" => {
1467 self.hellevator
1468 .active
1469 .get_or_insert_with(Default::default)
1470 .guild_rank = val.into("gt rank").unwrap_or(0);
1471 }
1472 "gtrankingmax" => {
1473 self.hall_of_fames.hellevator_total =
1474 val.into("gt rank max").ok();
1475 }
1476 "gtbracketlist" => {
1477 self.hellevator
1478 .active
1479 .get_or_insert_with(Default::default)
1480 .brackets =
1481 val.into_list("gtbracketlist").unwrap_or_default();
1482 }
1483 "gtraidfights" => {
1484 let data: Vec<i64> =
1485 val.into_list("gt raids").unwrap_or_default();
1486
1487 let hellevator =
1488 self.hellevator.active.get_or_insert_with(Default::default);
1489
1490 hellevator.guild_raid_signup_start = data
1491 .cstget(0, "h raid signup start", server_time)?
1492 .unwrap_or_default();
1493
1494 hellevator.guild_raid_start = data
1495 .cstget(1, "h raid next attack", server_time)?
1496 .unwrap_or_default();
1497
1498 let start = data.skip(2, "hellevator_fights")?;
1499
1500 let floor_count = start.len() / 5;
1501
1502 if floor_count > hellevator.guild_raid_floors.len() {
1503 hellevator
1504 .guild_raid_floors
1505 .resize_with(floor_count, Default::default);
1506 }
1507 #[allow(clippy::indexing_slicing)]
1508 for (data, floor) in
1509 start.chunks_exact(5).zip(&mut hellevator.guild_raid_floors)
1510 {
1511 floor.today = data[1];
1513 floor.yesterday = data[2];
1514 floor.point_reward =
1515 data.csiget(3, "floor t-reward", 0).unwrap_or(0);
1516 floor.silver_reward =
1517 data.csiget(4, "floor c-reward", 0).unwrap_or(0);
1518 }
1519 }
1520 "gtmonsterreward" => {
1521 let data: Vec<i64> =
1522 val.into_list("gt m reward").unwrap_or_default();
1523
1524 let hellevator =
1525 self.hellevator.active.get_or_insert_with(Default::default);
1526 hellevator.monster_rewards.clear();
1527
1528 for chunk in data.chunks_exact(3) {
1529 let raw_typ = chunk.cget(0, "gt monster reward typ")?;
1530 if raw_typ <= 0 {
1531 continue;
1532 }
1533 let one = chunk
1534 .csiget(1, "gt monster reward typ", 0)
1535 .unwrap_or(0);
1536 if one != 0 {
1537 warn!("hellevator monster t: {one}");
1538 }
1539 let typ = HellevatorMonsterRewardTyp::parse(raw_typ);
1540 let amount: u64 =
1541 chunk.csiget(2, "gt monster reward amount", 0)?;
1542 hellevator
1543 .monster_rewards
1544 .push(HellevatorMonsterReward { typ, amount });
1545 }
1546 }
1547 "gtdailyreward" => {
1548 self.hellevator
1549 .active
1550 .get_or_insert_with(Default::default)
1551 .rewards_today = HellevatorDailyReward::parse(
1552 &val.into_list("hdrtd").unwrap_or_default(),
1553 );
1554 }
1555 "gtdailyrewardnext" => {
1556 self.hellevator
1557 .active
1558 .get_or_insert_with(Default::default)
1559 .rewards_next = HellevatorDailyReward::parse(
1560 &val.into_list("hdrnd").unwrap_or_default(),
1561 );
1562 }
1563 "gtdailyrewardyesterday" => {
1564 self.hellevator
1565 .active
1566 .get_or_insert_with(Default::default)
1567 .rewards_yesterday = HellevatorDailyReward::parse(
1568 &val.into_list("hdryd").unwrap_or_default(),
1569 );
1570 }
1571 "gtdailyrewardclaimed" => {
1572 if let Some(hellevator) = self.hellevator.active.as_mut() {
1573 if !all_values.contains_key("gtdailyreward") {
1579 hellevator.rewards_yesterday = None;
1582 }
1583 }
1584 }
1585 "gtranking" => {
1586 self.hall_of_fames.hellevator = val
1587 .as_str()
1588 .split(';')
1589 .filter(|a| !a.is_empty())
1590 .map(|chunk| chunk.split(',').collect())
1591 .flat_map(|chunk: Vec<_>| -> Result<_, SFError> {
1592 Ok(HallOfFameHellevator {
1593 rank: chunk.cfsuget(0, "hh rank")?,
1594 name: chunk.cget(1, "hh name")?.to_string(),
1595 tokens: chunk.cfsuget(2, "hh tokens")?,
1596 })
1597 })
1598 .collect();
1599 }
1600 "gtpreviewreward" => {
1601 }
1621 "gtmonster" => {
1622 self.hellevator
1623 .active
1624 .get_or_insert_with(Default::default)
1625 .current_monster = HellevatorMonster::parse(
1626 &val.into_list("h monster").unwrap_or_default(),
1627 )
1628 .ok();
1629 }
1630 "gtbonus" => {
1631 self.hellevator
1632 .active
1633 .get_or_insert_with(Default::default)
1634 .daily_treat_bonus = val
1635 .into_list("gt bonus")
1636 .and_then(|a| HellevatorTreatBonus::parse(&a))
1637 .ok();
1638 }
1639 "pendingrewards" => {
1640 let vals: Vec<_> = val.as_str().split('/').collect();
1641 self.mail.claimables = vals
1642 .chunks_exact(6)
1643 .flat_map(|chunk| -> Result<ClaimableMail, SFError> {
1644 let start = chunk.cfsuget(4, "p reward start")?;
1645 let end = chunk.cfsuget(5, "p reward end")?;
1646
1647 let status = match chunk.cget(1, "p read")? {
1648 "0" => ClaimableStatus::Unread,
1649 "1" => ClaimableStatus::Read,
1650 "2" => ClaimableStatus::Claimed,
1651 x => {
1652 warn!("Unknown claimable status: {x}");
1653 ClaimableStatus::Claimed
1654 }
1655 };
1656
1657 Ok(ClaimableMail {
1658 typ: FromPrimitive::from_i64(
1659 chunk.cfsuget(2, "claimable typ")?,
1660 )
1661 .unwrap_or_default(),
1662 msg_id: chunk.cfsuget(0, "msg_id")?,
1663 status,
1664 name: chunk.cget(3, "reward code")?.to_string(),
1665 received: server_time
1666 .convert_to_local(start, "p start"),
1667 claimable_until: server_time
1668 .convert_to_local(end, "p end"),
1669 })
1670 })
1671 .collect();
1672 }
1673 "pendingrewardressources" => {
1674 let vals: Vec<i64> =
1675 val.into_list("pendingrewardressources")?;
1676
1677 self.mail
1678 .open_claimable
1679 .get_or_insert_with(Default::default)
1680 .resources = vals
1681 .chunks_exact(2)
1682 .flat_map(|chunk| -> Result<Reward, SFError> {
1683 Ok(Reward {
1684 typ: RewardType::parse(chunk.cget(0, "c typ")?),
1685 amount: chunk.csiget(1, "c amount", 1)?,
1686 })
1687 })
1688 .collect();
1689 }
1690 "pendingreward" => {
1691 let vals: Vec<i64> = val.into_list("pending item")?;
1692 self.mail
1693 .open_claimable
1694 .get_or_insert_with(Default::default)
1695 .items = vals
1696 .chunks_exact(ITEM_PARSE_LEN)
1697 .flat_map(|a|
1698 Item::parse(a, server_time))
1700 .flatten()
1701 .collect();
1702 }
1703 "fightablegroups" => {
1704 self.guild
1705 .get_or_insert_default()
1706 .update_fightable_targets(val.as_str())?;
1707 }
1708 "adventscalendar" => {
1709 let vals: Vec<i64> = val.into_list("advent door")?;
1710 self.specials.advent_calendar = match vals.first() {
1711 Some(0) | None => None,
1712 _ => Reward::parse(&vals).ok(),
1713 };
1714 }
1715 "fortresschances" => {
1716 }
1720 "deedsandtitlesplayersave" => {
1721 }
1725 "deedshelves" => {
1726 }
1729 "fortressstorage" => {
1730 self.fortress.get_or_insert_default().update_resources(
1731 &val.into_list("ft resources")?,
1732 server_time,
1733 )?;
1734 }
1735 "fortressunits" => {
1736 self.fortress
1737 .get_or_insert_default()
1738 .update_units(&val.into_list("ft units")?, server_time)?;
1739 }
1740 "fortress" => {
1741 self.fortress
1742 .get_or_insert_default()
1743 .update(&val.into_list("fortress")?, server_time)?;
1744 }
1745 "wheel" => {
1746 let data: Vec<i64> = val.into_list("wheel")?;
1747 self.specials.wheel.spins_today =
1749 data.csiget(1, "lucky turns", 0)?;
1750 self.specials.wheel.next_free_spin =
1751 data.cstget(2, "next lucky turn", server_time)?;
1752 }
1753 "dice" => {
1754 let data: Vec<i64> = val.into_list("dice")?;
1755 self.tavern.dice_game.next_free =
1756 data.cstget(0, "dice next", server_time)?;
1757 self.tavern.dice_game.remaining =
1758 data.csiget(1, "rem dice games", 0)?;
1759 }
1760 "charactergroup" => {
1761 let data: Vec<i64> = val.into_list("c group")?;
1762 let guild = self.guild.get_or_insert_with(Default::default);
1763 guild.own_treasure_skill =
1764 data.csiget(0, "own treasure skill", 0)?;
1765 guild.own_instructor_skill =
1766 data.csiget(1, "own instruction skill", 0)?;
1767 guild.hydra.next_battle =
1768 data.cstget(2, "pet battle", server_time)?;
1769 guild.hydra.remaining_fights =
1770 data.csiget(3, "remaining pet battles", 0)?;
1771 guild.own_pet_lvl = data.csiget(4, "own pet lvl", 0)?;
1772 guild.joined = data.cstget(5, "guild joined", server_time)?;
1773 }
1775 "arena" => {
1776 let data: Vec<i64> = val.into_list("arena")?;
1777 self.arena.next_free_fight =
1778 data.cstget(0, "next battle time", server_time)?;
1779 self.arena.fights_for_xp =
1780 data.csiget(1, "arena xp fights", 0)?;
1781 for (idx, val) in self.arena.enemy_ids.iter_mut().enumerate() {
1782 *val = data.csiget(2 + idx, "arena enemy id", 0)?;
1783 }
1784 }
1786 "ownplayersavepotions" => {
1787 let data: Vec<i64> = val.into_list("potions")?;
1788 self.character.active_potions =
1789 items::parse_active_potions(&data, server_time);
1790 }
1791 "arcanetoilet" => {
1792 let data: Vec<i64> = val.into_list("toilet")?;
1793
1794 let toilet_lvl = data.cget(0, "toilet lvl")?;
1796 if toilet_lvl > 0 {
1797 self.tavern
1798 .toilet
1799 .get_or_insert_with(Default::default)
1800 .update(&data, server_time)?;
1801 }
1802 }
1803 "vipstatus" => {
1804 other_player.get_or_insert_default().is_vip =
1805 val.as_str() != "0";
1806 }
1807 "characterstatus" => {
1808 let data: Vec<i64> = val.into_list("char status")?;
1809
1810 self.tavern.current_action = CurrentAction::parse(
1811 data.cget(1, "action id")?,
1812 data.cget(2, "action sec")?,
1813 data.cstget(3, "current action time", server_time)?,
1814 );
1815
1816 self.tavern.beer_max = data.csiget(5, "beer total", 0)?;
1818
1819 self.tavern.thirst_for_adventure_sec =
1820 data.csiget(6, "remaining ALU", 0)?;
1821 self.tavern.beer_drunk =
1822 data.csiget(7, "beer drunk count", 0)?;
1823 self.specials.calendar.collected =
1824 data.csiget(8, "calendar collected", 245)?;
1825 self.specials.calendar.next_possible =
1826 data.cstget(9, "calendar next", server_time)?;
1827 self.pets
1838 .get_or_insert_with(Default::default)
1839 .next_free_exploration =
1840 data.cstget(20, "pet next free exp", server_time)?;
1841 self.dungeons.next_free_fight =
1842 data.cstget(21, "dungeon timer", server_time)?;
1843 if let Some(start) =
1844 data.cstget(22, "dungeon timer", server_time)?
1845 {
1846 self.legendary_dungeon
1847 .active
1848 .get_or_insert_default()
1849 .healing_start = Some(start);
1850 }
1851 }
1861 "ownplayersavecharacter" => {
1862 let data: Vec<i64> = val.into_list("char save")?;
1863
1864 self.character.player_id = data.csiget(1, "player id", 0)?;
1866 self.character.level =
1868 data.csimget(3, "level", 0, |a| a & 0xFFFF)?;
1869 self.character.experience = data.csiget(4, "experience", 0)?;
1870 self.character.next_level_xp =
1871 data.csiget(5, "xp to next lvl", 0)?;
1872 self.character.honor = data.csiget(6, "honor", 0)?;
1873 self.character.rank = data.csiget(7, "rank", 0)?;
1874 self.character.portrait =
1875 Portrait::parse(data.skip(8, "portrait")?)
1876 .unwrap_or_default();
1877 self.character.race = data.cfpuget(18, "char race", |a| a)?;
1889 self.character.class =
1892 data.cfpuget(20, "character class", |a| a - 1)?;
1893 self.character.mount =
1894 data.cfpget(21, "character mount", |a| a & 0xFF)?;
1895 self.character.armor = data.csiget(23, "total armor", 0)?;
1897 self.character.min_damage = data.csiget(24, "min damage", 0)?;
1898 self.character.max_damage = data.csiget(25, "max damage", 0)?;
1899 self.guild
1900 .get_or_insert_with(Default::default)
1901 .portal
1902 .damage_bonus =
1903 data.cimget(26, "portal dmg bonus", |a| a)?;
1904 self.dungeons
1906 .portal
1907 .get_or_insert_with(Default::default)
1908 .player_hp_bonus =
1909 data.csimget(28, "portal hp bonus", 0, |a| a)?;
1910 self.character.mount_end =
1911 data.cstget(29, "mount end", server_time)?;
1912 update_enum_map(
1913 &mut self.character.attribute_basis,
1914 data.skip(30, "char attr basis")?,
1915 );
1916 update_enum_map(
1917 &mut self.character.attribute_additions,
1918 data.skip(35, "char attr adds")?,
1919 );
1920 update_enum_map(
1921 &mut self.character.attribute_times_bought,
1922 data.skip(40, "char attr tb")?,
1923 );
1924 }
1950 "adventure" => {
1951 let data: Vec<i64> = val.into_list("char save")?;
1952 for (slice, quest) in data
1955 .skip(2, "quests")?
1956 .chunks_exact(7)
1957 .zip(&mut self.tavern.quests)
1958 {
1959 quest.update(slice)?;
1960 }
1961 }
1962 "events" => {
1963 let data: Vec<i64> = val.into_list("events")?;
1964 if data.len() < 8 {
1965 return Ok(());
1966 }
1967 self.specials.events.active.clear();
1969 let flags = data.cget(1, "events")?;
1970 for (idx, event) in Event::iter().enumerate() {
1971 if (flags & (1 << idx)) > 0 {
1972 self.specials.events.active.insert(event);
1973 }
1974 }
1975 self.specials.events.ends =
1979 data.cstget(4, "event end", server_time)?;
1980
1981 }
1983 "tavernspecialend" | "tavernspecialsub" | "tavernspecial" => {
1984 }
1986 "subscriptionstatus" => {}
1987 "iadungeonchances" => {
1989 }
1991 "iadungeontime" => {
1992 let dungeons = &mut self.legendary_dungeon;
1993
1994 let vals: Vec<i64> = val.into_list("iadungeontime")?;
1995 dungeons.theme = vals.cfpget(0, "ld theme", |x| x)?;
1996 dungeons.start = vals.cstget(1, "ld start", server_time)?;
1997 dungeons.end = vals.cstget(2, "ld end", server_time)?;
1998 dungeons.close = vals.cstget(3, "ld closes", server_time)?;
1999 }
2000 "iadungeonstatstotal" => {
2001 let dungeons =
2002 self.legendary_dungeon.active.get_or_insert_default();
2003
2004 let data: Vec<i64> = val.into_list("iadungeonstatstotal")?;
2005 dungeons.total_stats = TotalStats::parse(&data)?;
2006 }
2007 "iadungeonstats" => {
2008 let dungeons =
2009 self.legendary_dungeon.active.get_or_insert_default();
2010
2011 let data = val.into_list("iadungeonstats")?;
2012 dungeons.stats = Stats::parse(&data).unwrap_or_default();
2013 }
2014 "iadungeon" => {
2015 let data: Vec<i64> = val.into_list("iadungeon")?;
2016 let dungeons =
2017 self.legendary_dungeon.active.get_or_insert_default();
2018 dungeons.update(&data)?;
2019 if !all_values.contains_key("iapendingitems") {
2020 dungeons.pending_items.clear();
2021 }
2022 }
2023 "iapendingitems" => {
2024 let dungeons =
2025 self.legendary_dungeon.active.get_or_insert_default();
2026 dungeons.pending_items.clear();
2027 let data: Vec<i64> = val.into_list("iapendingitems")?;
2028 let amount: i64 = data.cget(0, "pending amount")?;
2029 if amount < 1 {
2030 return Ok(());
2031 }
2032 for slice in
2033 data.skip(1, "ld items")?.chunks_exact(ITEM_PARSE_LEN)
2034 {
2035 let Some(item) = Item::parse(slice, server_time)? else {
2036 warn!("Could not parse pending ld item");
2037 continue;
2038 };
2039 dungeons.pending_items.push(item);
2040 }
2041 }
2042 "ialootitem" => {
2043 }
2045 "iamerchant" => {
2046 let data: Vec<i64> = val.into_list("iamerchant")?;
2047
2048 self.legendary_dungeon
2049 .active
2050 .get_or_insert_default()
2051 .merchant_offers = data
2052 .chunks_exact(3)
2053 .flat_map(MerchantOffer::parse)
2054 .flatten()
2055 .collect();
2056 }
2057 "iadungeon20cost" => {
2058 self.legendary_dungeon
2059 .active
2060 .get_or_insert_default()
2061 .heal_quarter_cost = val.into("iadungeon20cost")?;
2062 }
2063 "iadungeonsoulstones" => {
2064 let dungeons =
2065 self.legendary_dungeon.active.get_or_insert_default();
2066
2067 let data: Vec<i64> = val.into_list("iamerchant")?;
2068 let mut chunks = data.chunks_exact(6);
2069 dungeons.active_gems = chunks
2070 .by_ref()
2071 .take(3)
2072 .flat_map(GemOfFate::parse)
2073 .flatten()
2074 .collect();
2075
2076 dungeons.available_gems =
2077 chunks.flat_map(GemOfFate::parse).flatten().collect();
2078 }
2079 "iamap" => {
2080 }
2097 "otherplayerfortressinfo" => {
2098 other_player
2099 .get_or_insert_default()
2100 .update_fortress(&val.into_list("other ft")?)?;
2101 }
2102 x if x.contains("average") && x.ends_with("level") => {
2103 }
2105 x if x.contains("dungeonenemies") => {
2107 }
2109 x if x.starts_with("attbonus") => {
2110 }
2112 x => {
2113 warn!("Update ignored {x} -> {val:?}");
2114 }
2115 }
2116 Ok(())
2117 }
2118}
2119
2120fn fight_no_from_header(header_name: &str) -> usize {
2123 let number_str =
2124 header_name.trim_start_matches(|a: char| !a.is_ascii_digit());
2125 let id: usize = number_str.parse().unwrap_or(1);
2126 id.max(1)
2127}
2128
2129#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2134pub struct ServerTime(i64);
2135
2136impl ServerTime {
2137 #[must_use]
2139 pub(crate) fn convert_to_local(
2140 self,
2141 timestamp: i64,
2142 name: &str,
2143 ) -> Option<DateTime<Local>> {
2144 if matches!(timestamp, 0 | -1 | 1 | 11) {
2145 return None;
2147 }
2148
2149 if !(1_000_000_000..=3_000_000_000).contains(×tamp) {
2150 warn!("Weird time stamp: {timestamp} for {name}");
2151 return None;
2152 }
2153 DateTime::from_timestamp(timestamp - self.0, 0)?
2154 .naive_utc()
2155 .and_local_timezone(Local)
2156 .latest()
2157 }
2158
2159 #[must_use]
2164 pub fn current(&self) -> NaiveDateTime {
2165 Local::now().naive_local() + Duration::seconds(self.0)
2166 }
2167
2168 #[must_use]
2169 pub fn next_midnight(&self) -> std::time::Duration {
2170 let current = self.current();
2171 let tomorrow = current.date() + Duration::days(1);
2172 let tomorrow = NaiveDateTime::from(tomorrow);
2173 let sec_until_midnight =
2174 (tomorrow - current).to_std().unwrap_or_default().as_secs();
2175 std::time::Duration::from_secs(sec_until_midnight % (60 * 60 * 24))
2178 }
2179}
2180
2181trait StringSetExt {
2183 fn set(&mut self, s: &str);
2184}
2185
2186impl StringSetExt for String {
2187 fn set(&mut self, s: &str) {
2191 self.replace_range(.., s);
2192 }
2193}
2194
2195#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2197#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2198pub struct NormalCost {
2199 pub silver: u64,
2201 pub mushrooms: u16,
2203}