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 "tavernspecialsub" => {
481 self.specials.events.active.clear();
482 let flags = val.into::<i32>("tavern special sub")?;
483 for (idx, event) in Event::iter().enumerate() {
484 if (flags & (1 << idx)) > 0 {
485 self.specials.events.active.insert(event);
486 }
487 }
488 }
489 "sfhomeid" => {}
490 "backpack" => {
491 let data: Vec<i64> = val.into_list("backpack")?;
492 self.character.inventory.backpack = data
493 .chunks_exact(ITEM_PARSE_LEN)
494 .map(|a| Item::parse(a, server_time))
495 .collect::<Result<Vec<_>, _>>()?;
496 }
497 "itemlevelshop" => {
498 self.shop_item_lvl = val.into("shop lvl")?;
499 }
500 "storeitemsshakes" => {
501 let data: Vec<i64> = val.into_list("weapon store")?;
502 *self.shops.get_mut(ShopType::Weapon) =
503 Shop::parse(&data, server_time, ShopType::Weapon)?;
504 }
505 "questofferitems" => {
506 for (chunk, quest) in val
507 .into_list("quest items")?
508 .chunks_exact(19)
509 .zip(&mut self.tavern.quests)
510 {
511 quest.item = Item::parse(chunk, server_time)?;
512 }
513 }
514 #[allow(
515 clippy::indexing_slicing,
516 clippy::cast_sign_loss,
517 clippy::cast_possible_truncation
518 )]
519 #[allow(deprecated)]
520 "toiletstate" => {
521 let vals: Vec<i64> = val.into_list("toilet state")?;
522 if vals.len() < 3 {
523 return Ok(());
524 }
525 let toilet = self.tavern.toilet.get_or_insert_default();
526 toilet.sacrifices_left = vals[2] as u32;
527 }
528 "companionequipment" => {
529 let data: Vec<i64> = val.into_list("quest items")?;
530 if data.is_empty() {
531 return Ok(());
532 }
533 for (idx, cmp) in self
534 .dungeons
535 .companions
536 .get_or_insert_with(Default::default)
537 .values_mut()
538 .enumerate()
539 {
540 let data = data.skip(
541 (19 * EquipmentSlot::COUNT) * idx,
542 "companion item",
543 )?;
544 cmp.equipment = Equipment::parse(data, server_time)?;
545 }
546 }
547 "storeitemsfidget" => {
548 let data: Vec<i64> = val.into_list("magic store")?;
549 *self.shops.get_mut(ShopType::Magic) =
550 Shop::parse(&data, server_time, ShopType::Magic)?;
551 }
552 "ownplayersaveequipment" => {
553 let data: Vec<i64> = val.into_list("player equipment")?;
554 self.character.equipment =
555 Equipment::parse(&data, server_time)?;
556 }
557 "systemmessagelist" => {}
558 "newslist" => {}
559 "dummieequipment" => {
560 let m: Vec<i64> = val.into_list("mannequin")?;
561 self.character.mannequin =
562 Some(Equipment::parse(&m, server_time)?);
563 }
564 "owntower" => {
565 let data = val.into_list("tower")?;
566 let companions = self
567 .dungeons
568 .companions
569 .get_or_insert_with(Default::default);
570
571 for (i, class) in CompanionClass::iter().enumerate() {
572 let comp_start = 3 + i * 148;
573 companions.get_mut(class).level =
574 data.cget(comp_start, "comp level")?;
575 update_enum_map(
576 &mut companions.get_mut(class).attributes,
577 data.skip(comp_start + 4, "comp attrs")?,
578 );
579 }
580 self.underworld
582 .get_or_insert_with(Default::default)
583 .update(&data, server_time)?;
584 }
585 "owngrouprank" => {
586 self.guild.get_or_insert_with(Default::default).rank =
587 val.into("group rank")?;
588 }
589 "owngroupattack" | "owngroupdefense" => {
590 }
592 "owngrouprequirement" | "othergrouprequirement" => {
593 }
595 "owngroupsave" => {
596 self.guild
597 .get_or_insert_with(Default::default)
598 .update_group_save(val.as_str(), server_time)?;
599 }
600 "owngroupmember" => self
601 .guild
602 .get_or_insert_with(Default::default)
603 .update_member_names(val.as_str()),
604 "owngrouppotion" => {
605 self.guild
606 .get_or_insert_with(Default::default)
607 .update_member_potions(val.as_str());
608 }
609 "unitprice" => {
610 self.fortress
611 .get_or_insert_with(Default::default)
612 .update_unit_prices(&val.into_list("fortress units")?)?;
613 }
614 "dicestatus" => {
615 let dices: Option<Vec<DiceType>> = val
616 .into_list("dice status")?
617 .into_iter()
618 .map(FromPrimitive::from_u8)
619 .collect();
620 self.tavern.dice_game.current_dice = dices.unwrap_or_default();
621 }
622 "dicereward" => {
623 let data: Vec<u32> = val.into_list("dice reward")?;
624 let win_typ: DiceType =
625 data.cfpuget(0, "dice reward", |a| a - 1)?;
626 self.tavern.dice_game.reward = Some(DiceReward {
627 win_typ,
628 amount: data.cget(1, "dice reward amount")?,
629 });
630 }
631 "chathistory" => {
632 self.guild.get_or_insert_with(Default::default).chat =
633 ChatMessage::parse_messages(val.as_str());
634 }
635 "chatwhisper" => {
636 self.guild.get_or_insert_with(Default::default).whispers =
637 ChatMessage::parse_messages(val.as_str());
638 }
639 "upgradeprice" => {
640 self.fortress
641 .get_or_insert_with(Default::default)
642 .update_unit_upgrade_info(
643 &val.into_list("fortress unit upgrade prices")?,
644 )?;
645 }
646 "unitlevel" => {
647 self.fortress
648 .get_or_insert_with(Default::default)
649 .update_levels(&val.into_list("fortress unit levels")?)?;
650 }
651 "fortressprice" => {
652 self.fortress
653 .get_or_insert_with(Default::default)
654 .update_prices(
655 &val.into_list("fortress upgrade prices")?,
656 )?;
657 }
658 "Arenarank" => {
659 if let Some(uw) = self.underworld.as_mut() {
660 uw.lure_suggestion =
661 val.as_str().parse::<u32>().ok().map(LureSuggestion);
662 }
663 }
664 "witch" => {
665 }
667 "witchshop" => {
668 self.witch
669 .get_or_insert_with(Default::default)
670 .update(&val.into_list("witch")?)?;
671 }
672 "underworldupgradeprice" => {
673 self.underworld
674 .get_or_insert_with(Default::default)
675 .update_underworld_unit_prices(
676 &val.into_list("underworld upgrade prices")?,
677 )?;
678 }
679 "unlockfeature" => {
680 self.pending_unlocks =
681 Unlockable::parse(&val.into_list("unlock")?)?;
682 }
683 "dungeonprogresslight" => self.dungeons.update_progress(
684 &val.into_list("dungeon progress light")?,
685 DungeonType::Light,
686 ),
687 "dungeonprogressshadow" => self.dungeons.update_progress(
688 &val.into_list("dungeon progress shadow")?,
689 DungeonType::Shadow,
690 ),
691 "portalprogress" => {
692 self.dungeons
693 .portal
694 .get_or_insert_with(Default::default)
695 .update(&val.into_list("portal progress")?, server_time)?;
696 }
697 "tavernspecialend" => {
698 self.specials.events.ends = server_time
699 .convert_to_local(val.into("event end")?, "event end");
700 }
701 "owntowerlevel" => {
702 }
704 "serverversion" => {
705 }
707 "stoneperhournextlevel" => {
708 self.fortress
709 .get_or_insert_with(Default::default)
710 .resources
711 .get_mut(FortressResourceType::Stone)
712 .production
713 .per_hour_next_lvl = val.into("stone next lvl")?;
714 }
715 "woodperhournextlevel" => {
716 self.fortress
717 .get_or_insert_with(Default::default)
718 .resources
719 .get_mut(FortressResourceType::Wood)
720 .production
721 .per_hour_next_lvl = val.into("wood next lvl")?;
722 }
723 "shadowlevel" | "dungeonlevel" => {
724 }
726 "gttime" => {
727 self.update_gttime(&val.into_list("gttime")?, server_time)?;
728 }
729 "gtsave" => {
730 self.hellevator
731 .active
732 .get_or_insert_with(Default::default)
733 .update(&val.into_list("gtsave")?, server_time)?;
734 }
735 "maxrank" => {
736 self.hall_of_fames.players_total = val.into("player count")?;
737 }
738 "achievement" => {
739 self.achievements.update(&val.into_list("achievements")?)?;
740 }
741 "groupskillprice" => {
742 self.guild
743 .get_or_insert_with(Default::default)
744 .update_group_prices(
745 &val.into_list("guild skill prices")?,
746 )?;
747 }
748 "soldieradvice" => {
749 }
751 "owngroupdescription" => self
752 .guild
753 .get_or_insert_with(Default::default)
754 .update_description_embed(val.as_str()),
755 "idle" => {
756 self.idle_game = IdleGame::parse_idle_game(
757 &val.into_list("idle game")?,
758 server_time,
759 );
760 }
761 "resources" => {
762 self.update_resources(&val.into_list("resources")?)?;
763 }
764 "chattime" => {
765 }
771 "maxpetlevel" => {
772 self.pets.get_or_insert_with(Default::default).max_pet_level =
773 val.into("max pet lvl")?;
774 }
775 "otherdescription" => {
776 other_player
777 .get_or_insert_with(Default::default)
778 .description = from_sf_string(val.as_str());
779 }
780 "otherplayergroupname" => {
781 let guild =
782 Some(val.as_str().to_string()).filter(|a| !a.is_empty());
783 other_player.get_or_insert_with(Default::default).guild = guild;
784 }
785 "otherplayername" => {
786 other_player
787 .get_or_insert_with(Default::default)
788 .name
789 .set(val.as_str());
790 }
791 "otherplayersaveequipment" => {
792 let data: Vec<i64> = val.into_list("other player equipment")?;
793 other_player.get_or_insert_with(Default::default).equipment =
794 Equipment::parse(&data, server_time)?;
795 }
796 "fortresspricereroll" => {
797 self.fortress
798 .get_or_insert_with(Default::default)
799 .opponent_reroll_price = val.into("fortress reroll")?;
800 }
801 "fortresswalllevel" => {
802 self.fortress
803 .get_or_insert_with(Default::default)
804 .wall_combat_lvl = val.into("fortress wall lvl")?;
805 }
806 "dragongoldbonus" => {
807 self.character.mount_dragon_refund = val.into("dragon gold")?;
808 }
809 "wheelresult" => {
810 let upgraded = self.character.level >= 95
813 && self.pets.is_some()
814 && self.underworld.is_some();
815 self.specials.wheel.result = Some(WheelReward::parse(
816 &val.into_list("wheel result")?,
817 upgraded,
818 )?);
819 }
820 "dailyreward" => {
821 }
823 "calenderreward" => {
824 }
826 "oktoberfest" => {
827 if !val.as_str().is_empty() {
830 warn!("oktoberfest response is not empty: {val}");
831 }
832 }
833 "usersettings" => {
834 let vals: Vec<_> = val.as_str().split('/').collect();
836 let v = match vals.as_slice().cget(4, "questing setting")? {
837 "a" => ExpeditionSetting::PreferExpeditions,
838 "0" | "b" => ExpeditionSetting::PreferQuests,
839 x => {
840 error!("Weird expedition settings: {x}");
841 ExpeditionSetting::PreferQuests
842 }
843 };
844 self.tavern.questing_preference = v;
845 }
846 "mailinvoice" => {
847 }
849 "calenderinfo" => {
850 let data: Vec<i64> = val.into_list("calendar")?;
853 self.specials.calendar.rewards.clear();
854 for p in data.chunks_exact(2) {
855 let reward = CalendarReward::parse(p)?;
856 self.specials.calendar.rewards.push(reward);
857 }
858 }
859 "othergroupattack" => {
860 other_guild.get_or_insert_with(Default::default).attacks =
861 Some(val.to_string());
862 }
863 "othergroupdefense" => {
864 other_guild
865 .get_or_insert_with(Default::default)
866 .defends_against = Some(val.to_string());
867 }
868 "inboxcapacity" => {
869 self.mail.inbox_capacity = val.into("inbox cap")?;
870 }
871 "magicregistration" => {
872 }
875 "Ranklistplayer" => {
876 self.hall_of_fames.players.clear();
877 for player in val.as_str().trim_matches(';').split(';') {
878 if player.ends_with(",,,0,0,0,") {
880 break;
881 }
882
883 match HallOfFamePlayer::parse(player) {
884 Ok(x) => {
885 self.hall_of_fames.players.push(x);
886 }
887 Err(err) => warn!("{err}"),
888 }
889 }
890 }
891 "ranklistgroup" => {
892 self.hall_of_fames.guilds.clear();
893 for guild in val.as_str().trim_matches(';').split(';') {
894 match HallOfFameGuild::parse(guild) {
895 Ok(x) => {
896 self.hall_of_fames.guilds.push(x);
897 }
898 Err(err) => warn!("{err}"),
899 }
900 }
901 }
902 "maxrankgroup" => {
903 self.hall_of_fames.guilds_total = Some(val.into("guild max")?);
904 }
905 "maxrankPets" => {
906 self.hall_of_fames.pets_total = Some(val.into("pet rank max")?);
907 }
908 "RanklistPets" => {
909 self.hall_of_fames.pets.clear();
910 for entry in val.as_str().trim_matches(';').split(';') {
911 match HallOfFamePets::parse(entry) {
912 Ok(x) => {
913 self.hall_of_fames.pets.push(x);
914 }
915 Err(err) => warn!("{err}"),
916 }
917 }
918 }
919 "ranklistfortress" | "Ranklistfortress" => {
920 self.hall_of_fames.fortresses.clear();
921 for guild in val.as_str().trim_matches(';').split(';') {
922 match HallOfFameFortress::parse(guild) {
923 Ok(x) => {
924 self.hall_of_fames.fortresses.push(x);
925 }
926 Err(err) => warn!("{err}"),
927 }
928 }
929 }
930 "ranklistunderworld" => {
931 self.hall_of_fames.underworlds.clear();
932 for entry in val.as_str().trim_matches(';').split(';') {
933 match HallOfFameUnderworld::parse(entry) {
934 Ok(x) => {
935 self.hall_of_fames.underworlds.push(x);
936 }
937 Err(err) => warn!("{err}"),
938 }
939 }
940 }
941 "gamblegoldvalue" => {
942 self.tavern.gamble_result =
943 Some(GambleResult::SilverChange(val.into("gold gamble")?));
944 }
945 "gamblecoinvalue" => {
946 self.tavern.gamble_result = Some(GambleResult::MushroomChange(
947 val.into("gold gamble")?,
948 ));
949 }
950 "maxrankFortress" => {
951 self.hall_of_fames.fortresses_total =
952 Some(val.into("fortress max")?);
953 }
954 "underworldprice" => self
955 .underworld
956 .get_or_insert_with(Default::default)
957 .update_building_prices(&val.into_list("ub prices")?)?,
958 "owngroupknights" => self
959 .guild
960 .get_or_insert_with(Default::default)
961 .update_group_knights(val.as_str()),
962 "friendlist" => self.updatete_relation_list(val.as_str()),
963 "legendaries" => {
964 if val.as_str().chars().any(|a| a != 'A') {
965 warn!("Found a legendaries value, that is not just AAA..");
966 }
967 }
968 "smith" => {
969 let data: Vec<i64> = val.into_list("smith")?;
970 let bs = self.blacksmith.get_or_insert_with(Default::default);
971
972 bs.dismantle_left = data.csiget(0, "dismantles left", 0)?;
973 bs.last_dismantled = data.cstget(1, "bs time", server_time)?;
974 }
975 "tavernspecial" => {
976 }
978 "fortressGroupPrice" => {
979 self.fortress
980 .get_or_insert_with(Default::default)
981 .hall_of_knights_upgrade_price = FortressCost::parse(
982 &val.into_list("hall of knights prices")?,
983 )?;
984 }
985 "goldperhournextlevel" => {
986 }
988 "underworldmaxsouls" => {
989 }
991 "dailytaskrewardpreview" => {
992 let vals: Vec<i64> =
993 val.into_list("event task reward preview")?;
994 self.specials.tasks.daily.rewards = parse_rewards(&vals);
995 }
996 "expeditionevent" => {
997 let data: Vec<i64> = val.into_list("exp event")?;
998 self.tavern.expeditions.start =
999 data.cstget(0, "expedition start", server_time)?;
1000 let end = data.cstget(1, "expedition end", server_time)?;
1001 self.tavern.expeditions.end = end;
1002 }
1003 "expeditions" => {
1004 let data: Vec<i64> = val.into_list("exp event")?;
1005
1006 if !data.len().is_multiple_of(8) {
1007 warn!(
1008 "Available expeditions have weird size: {data:?} {}",
1009 data.len()
1010 );
1011 }
1012 self.tavern.expeditions.available = data
1013 .chunks_exact(8)
1014 .map(|data| {
1015 Ok(AvailableExpedition {
1016 target: data
1017 .cfpget(0, "expedition typ", |a| a)?
1018 .unwrap_or_default(),
1019 location_1: data
1020 .cfpget(4, "exp loc 1", |a| a)?
1021 .unwrap_or_default(),
1022 location_2: data
1023 .cfpget(5, "exp loc 2", |a| a)?
1024 .unwrap_or_default(),
1025 thirst_for_adventure_sec: data
1026 .csiget(6, "exp alu", 600)?,
1027 special: data.cfpget(7, "exp special", |a| a)?,
1028 })
1029 })
1030 .collect::<Result<_, _>>()?;
1031 }
1032 "expeditionrewardresources" => {
1033 }
1036 "expeditionreward" => {
1037 }
1045 "expeditionmonster" => {
1046 let data: Vec<i64> = val.into_list("expedition monster")?;
1047 let exp = self
1048 .tavern
1049 .expeditions
1050 .active
1051 .get_or_insert_with(Default::default);
1052
1053 exp.boss = ExpeditionBoss {
1054 id: data
1055 .cfpget(0, "expedition monster", |a| -a)?
1056 .unwrap_or_default(),
1057 items: soft_into(
1058 data.get(1).copied().unwrap_or_default(),
1059 "exp monster items",
1060 3,
1061 ),
1062 };
1063 }
1064 "expeditionhalftime" => {
1065 let data: Vec<i64> = val.into_list("halftime exp")?;
1066 let exp = self
1067 .tavern
1068 .expeditions
1069 .active
1070 .get_or_insert_with(Default::default);
1071
1072 exp.halftime_for_boss_id =
1073 -data.cget(0, "halftime for boss id")?;
1074 exp.rewards = data
1075 .skip(1, "halftime choice")?
1076 .chunks_exact(2)
1077 .map(Reward::parse)
1078 .collect::<Result<_, _>>()?;
1079 }
1080 "expeditionstate" => {
1081 let data: Vec<i64> = val.into_list("exp state")?;
1082 let exp = self
1083 .tavern
1084 .expeditions
1085 .active
1086 .get_or_insert_with(Default::default);
1087 exp.floor_stage = data.cget(2, "floor stage")?;
1088
1089 exp.target_thing = data
1090 .cfpget(3, "expedition target", |a| a)?
1091 .unwrap_or_default();
1092 exp.target_current = data.csiget(7, "exp current", 100)?;
1093 exp.target_amount = data.csiget(8, "exp target", 100)?;
1094
1095 exp.current_floor = data.csiget(0, "clearing", 0)?;
1096 exp.heroism = data.csiget(13, "heroism", 0)?;
1097
1098 exp.busy_since = data.cstget(15, "exp start", server_time)?;
1099 exp.busy_until = data.cstget(16, "exp busy", server_time)?;
1100
1101 for (x, item) in data
1102 .skip(9, "exp items")?
1103 .iter()
1104 .copied()
1105 .zip(&mut exp.items)
1106 {
1107 *item = match FromPrimitive::from_i64(x) {
1108 None if x != 0 => {
1109 warn!("Unknown item: {x}");
1110 Some(ExpeditionThing::Unknown)
1111 }
1112 x => x,
1113 };
1114 }
1115 }
1116 "expeditioncrossroad" => {
1117 let data: Vec<i64> = val.into_list("cross")?;
1119 let exp = self
1120 .tavern
1121 .expeditions
1122 .active
1123 .get_or_insert_with(Default::default);
1124 exp.update_encounters(&data);
1125 }
1126 "eventtasklist" => {
1127 let data: Vec<i64> = val.into_list("etl")?;
1128 self.specials.tasks.event.tasks.clear();
1129 for c in data.chunks_exact(4) {
1130 let task = Task::parse(c)?;
1131 self.specials.tasks.event.tasks.push(task);
1132 }
1133 }
1134 "eventtaskrewardpreview" => {
1135 let vals: Vec<i64> =
1136 val.into_list("event task reward preview")?;
1137
1138 self.specials.tasks.event.rewards = parse_rewards(&vals);
1139 }
1140 "dailytasklist" => {
1141 let data: Vec<i64> = val.into_list("daily tasks list")?;
1142 self.specials.tasks.daily.tasks.clear();
1143
1144 for d in data.skip(1, "daily tasks")?.chunks_exact(4) {
1147 self.specials.tasks.daily.tasks.push(Task::parse(d)?);
1148 }
1149 }
1150 "eventtaskinfo" => {
1151 let data: Vec<i64> = val.into_list("eti")?;
1152 self.specials.tasks.event.theme = data
1153 .cfpget(2, "event task theme", |a| a)?
1154 .unwrap_or(EventTaskTheme::Unknown);
1155 self.specials.tasks.event.start =
1156 data.cstget(0, "event t start", server_time)?;
1157 self.specials.tasks.event.end =
1158 data.cstget(1, "event t end", server_time)?;
1159 }
1160 "scrapbook" => {
1161 self.character.scrapbook = ScrapBook::parse(val.as_str());
1162 }
1163 "dungeonfaces" | "shadowfaces" => {
1164 }
1168 "messagelist" => {
1169 let data = val.as_str();
1170 self.mail.inbox.clear();
1171 for msg in data.split(';').filter(|a| !a.trim().is_empty()) {
1172 match InboxEntry::parse(msg, server_time) {
1173 Ok(msg) => self.mail.inbox.push(msg),
1174 Err(e) => warn!("Invalid msg: {msg} {e}"),
1175 }
1176 }
1177 }
1178 "messagetext" => {
1179 self.mail.open_msg = Some(from_sf_string(val.as_str()));
1180 }
1181 "combatloglist" => {
1182 self.mail.combat_log.clear();
1183 for entry in val.as_str().split(';') {
1184 let parts = entry.split(',').collect::<Vec<_>>();
1185 if parts.iter().all(|a| a.is_empty()) {
1186 continue;
1187 }
1188 match CombatLogEntry::parse(&parts, server_time) {
1189 Ok(cle) => {
1190 self.mail.combat_log.push(cle);
1191 }
1192 Err(e) => {
1193 warn!(
1194 "Unable to parse combat log entry: {parts:?} \
1195 - {e}"
1196 );
1197 }
1198 }
1199 }
1200 }
1201 "maxupgradelevel" => {
1202 self.fortress
1203 .get_or_insert_with(Default::default)
1204 .building_max_lvl = val.into("max upgrade lvl")?;
1205 }
1206 "singleportalenemylevel" => {
1207 self.dungeons
1208 .portal
1209 .get_or_insert_with(Default::default)
1210 .enemy_level = val.into("portal lvl").unwrap_or(u32::MAX);
1211 }
1212 "ownpetsstats" => {
1213 self.pets
1214 .get_or_insert_with(Default::default)
1215 .update_pet_stat(&val.into_list("pet stats")?);
1216 }
1217 "ownpets" => {
1218 let data = val.into_list("own pets")?;
1219 self.pets
1220 .get_or_insert_with(Default::default)
1221 .update(&data, server_time)?;
1222 }
1223 "petsdefensetype" => {
1224 let pet_id = val.into("pet def typ")?;
1225 self.pets
1226 .get_or_insert_with(Default::default)
1227 .opponent
1228 .habitat = Some(HabitatType::from_typ_id(pet_id).ok_or(
1229 SFError::ParsingError("pet def typ", format!("{pet_id}")),
1230 )?);
1231 }
1232 "otherplayersavecharacter" => {
1233 other_player
1234 .get_or_insert_default()
1235 .update(&val.into_list("other player")?, server_time)?;
1236 }
1237 "otherplayersavepotions" => {
1238 other_player.get_or_insert_default().active_potions =
1239 items::parse_active_potions(
1240 &val.into_list("other potions")?,
1241 server_time,
1242 );
1243 }
1244 "otherplayer" => {
1245 let data: Vec<i64> = val.into_list("other player")?;
1246 #[allow(deprecated)]
1247 {
1248 other_player.get_or_insert_default().guild_joined =
1249 data.cstget(166, "other joined guild", server_time)?;
1250 }
1251 }
1252 "otherplayerfriendstatus" => {
1253 other_player
1254 .get_or_insert_with(Default::default)
1255 .relationship = warning_parse(
1256 val.into::<i32>("other friend")?,
1257 "other friend",
1258 FromPrimitive::from_i32,
1259 )
1260 .unwrap_or_default();
1261 }
1262 "otherplayerpetbonus" => {
1263 other_player
1264 .get_or_insert_with(Default::default)
1265 .update_pet_bonus(&val.into_list("o pet bonus")?)?;
1266 }
1267 "otherplayerunitlevel" => {
1268 let data: Vec<i64> =
1269 val.into_list("other player unit level")?;
1270 other_player
1273 .get_or_insert_with(Default::default)
1274 .wall_combat_lvl = data.csiget(0, "wall_lvl", 0)?;
1275 }
1276 "petsrank" => {
1277 self.pets.get_or_insert_with(Default::default).rank =
1278 val.into("pet rank")?;
1279 }
1280
1281 "maxrankUnderworld" => {
1282 self.hall_of_fames.underworlds_total =
1283 Some(val.into("mrank under")?);
1284 }
1285 "otherplayerfortressrank" => {
1286 match val.into::<i64>("other player fortress rank")? {
1287 ..=-1 => {}
1288 x => {
1289 let rank = x.try_into().unwrap_or(1);
1290 other_player
1291 .get_or_insert_default()
1292 .fortress
1293 .get_or_insert_default()
1294 .rank = rank;
1295 }
1296 }
1297 }
1298 "workreward" => {
1299 }
1301 x if x.starts_with("winnerid") => {
1302 let raw_winner_id = val
1305 .as_str()
1306 .split_once(|a: char| !a.is_ascii_digit())
1307 .map_or(val.as_str(), |a| a.0);
1308 if let Ok(winner_id) = raw_winner_id.parse() {
1309 self.get_fight(x).winner_id = winner_id;
1310 } else {
1311 error!("Invalid winner id: {raw_winner_id}");
1312 }
1313 }
1314 "fightresult" => {
1315 let data: Vec<i64> = val.into_list("fight result")?;
1316 self.last_fight
1317 .get_or_insert_with(Default::default)
1318 .update_result(&data, server_time)?;
1319 }
1321 x if x.starts_with("fightheader") => {
1322 self.get_fight(x).update_fighters(val.as_str());
1323 }
1324 "fightgroups" => {
1325 let fight =
1326 self.last_fight.get_or_insert_with(Default::default);
1327 fight.update_groups(val.as_str());
1328 }
1329 "fightadditionalplayers" => {
1330 }
1333 "fightversion" => {
1334 }
1338 x if x.starts_with("fight") && x.len() <= 7 => {
1339 let fight_no = fight_no_from_header(x);
1340 let wkey = format!("winnerid{fight_no}");
1341 let version = if let Some(winner_id) =
1342 all_values.get(wkey.as_str())
1343 {
1344 winner_id.as_str().split_once("fightversion:").map(|a| a.1)
1348 } else {
1349 all_values.get("fightversion").map(|a| a.as_str())
1352 };
1353 let fight = self.get_fight(x);
1354 if let Some(version) = version.and_then(|a| a.parse().ok()) {
1355 fight.update_rounds(val.as_str(), version)?;
1356 } else {
1357 fight.actions.clear();
1358 }
1359 }
1360 "othergroupname" => {
1361 other_guild
1362 .get_or_insert_with(Default::default)
1363 .name
1364 .set(val.as_str());
1365 }
1366 "othergrouprank" => {
1367 other_guild.get_or_insert_with(Default::default).rank =
1368 val.into("other group rank")?;
1369 }
1370 "othergroupfightcost" => {
1371 other_guild.get_or_insert_with(Default::default).attack_cost =
1372 val.into("other group fighting cost")?;
1373 }
1374 "othergroupmember" => {
1375 let names: Vec<_> = val.as_str().split(',').collect();
1376 let og = other_guild.get_or_insert_with(Default::default);
1377 og.members.resize_with(names.len(), Default::default);
1378 for (m, n) in og.members.iter_mut().zip(names) {
1379 m.name.set(n);
1380 }
1381 }
1382 "othergroupdescription" => {
1383 let guild = other_guild.get_or_insert_with(Default::default);
1384 let (emblem, desc) =
1385 val.as_str().split_once('§').unwrap_or(("", val.as_str()));
1386
1387 guild.emblem.update(emblem);
1388 guild.description = from_sf_string(desc);
1389 }
1390 "othergroup" => {
1391 other_guild
1392 .get_or_insert_with(Default::default)
1393 .update(val.as_str(), server_time)?;
1394 }
1395 "reward" => {
1396 }
1399 "gtdailypoints" => {
1400 self.hellevator
1401 .active
1402 .get_or_insert_with(Default::default)
1403 .guild_points_today = val.into("gtdaily").unwrap_or(0);
1404 }
1405 "gtchest" => {
1406 }
1415 "gtraidparticipants" => {
1416 let all: Vec<_> = val.as_str().split('/').collect();
1417 let hellevator =
1418 self.hellevator.active.get_or_insert_with(Default::default);
1419
1420 for floor in &mut hellevator.guild_raid_floors {
1421 floor.today_assigned.clear();
1422 }
1423
1424 #[allow(clippy::indexing_slicing)]
1425 for part in all.chunks_exact(2) {
1426 let name = part[0];
1428 let val: usize = part
1430 .cget(1, "hell raid part")
1431 .ok()
1432 .and_then(|a| a.parse().ok())
1433 .unwrap_or(0);
1434 if val > 0 {
1435 if val > hellevator.guild_raid_floors.len() {
1436 hellevator
1437 .guild_raid_floors
1438 .resize_with(val, Default::default);
1439 }
1440 if let Some(floor) =
1441 hellevator.guild_raid_floors.get_mut(val - 1)
1442 {
1443 floor.today_assigned.push(name.to_string());
1444 }
1445 }
1446 }
1447 }
1448 "gtraidparticipantsyesterday" => {
1449 let all: Vec<_> = val.as_str().split('/').collect();
1450
1451 let hellevator =
1452 self.hellevator.active.get_or_insert_with(Default::default);
1453
1454 for floor in &mut hellevator.guild_raid_floors {
1455 floor.yesterday_assigned.clear();
1456 }
1457
1458 #[allow(clippy::indexing_slicing)]
1459 for part in all.chunks_exact(2) {
1460 let name = part[0];
1462 let val: usize = part
1464 .cget(1, "hell raid part yd")
1465 .ok()
1466 .and_then(|a| a.parse().ok())
1467 .unwrap_or(0);
1468 if val > 0 {
1469 if val > hellevator.guild_raid_floors.len() {
1470 hellevator
1471 .guild_raid_floors
1472 .resize_with(val, Default::default);
1473 }
1474 if let Some(floor) =
1475 hellevator.guild_raid_floors.get_mut(val - 1)
1476 {
1477 floor.yesterday_assigned.push(name.to_string());
1478 }
1479 }
1480 }
1481 }
1482 "gtrank" => {
1483 self.hellevator
1484 .active
1485 .get_or_insert_with(Default::default)
1486 .guild_rank = val.into("gt rank").unwrap_or(0);
1487 }
1488 "gtrankingmax" => {
1489 self.hall_of_fames.hellevator_total =
1490 val.into("gt rank max").ok();
1491 }
1492 "gtbracketlist" => {
1493 self.hellevator
1494 .active
1495 .get_or_insert_with(Default::default)
1496 .brackets =
1497 val.into_list("gtbracketlist").unwrap_or_default();
1498 }
1499 "gtraidfights" => {
1500 let data: Vec<i64> =
1501 val.into_list("gt raids").unwrap_or_default();
1502
1503 let hellevator =
1504 self.hellevator.active.get_or_insert_with(Default::default);
1505
1506 hellevator.guild_raid_signup_start = data
1507 .cstget(0, "h raid signup start", server_time)?
1508 .unwrap_or_default();
1509
1510 hellevator.guild_raid_start = data
1511 .cstget(1, "h raid next attack", server_time)?
1512 .unwrap_or_default();
1513
1514 let start = data.skip(2, "hellevator_fights")?;
1515
1516 let floor_count = start.len() / 5;
1517
1518 if floor_count > hellevator.guild_raid_floors.len() {
1519 hellevator
1520 .guild_raid_floors
1521 .resize_with(floor_count, Default::default);
1522 }
1523 #[allow(clippy::indexing_slicing)]
1524 for (data, floor) in
1525 start.chunks_exact(5).zip(&mut hellevator.guild_raid_floors)
1526 {
1527 floor.today = data[1];
1529 floor.yesterday = data[2];
1530 floor.point_reward =
1531 data.csiget(3, "floor t-reward", 0).unwrap_or(0);
1532 floor.silver_reward =
1533 data.csiget(4, "floor c-reward", 0).unwrap_or(0);
1534 }
1535 }
1536 "gtmonsterreward" => {
1537 let data: Vec<i64> =
1538 val.into_list("gt m reward").unwrap_or_default();
1539
1540 let hellevator =
1541 self.hellevator.active.get_or_insert_with(Default::default);
1542 hellevator.monster_rewards.clear();
1543
1544 for chunk in data.chunks_exact(3) {
1545 let raw_typ = chunk.cget(0, "gt monster reward typ")?;
1546 if raw_typ <= 0 {
1547 continue;
1548 }
1549 let one = chunk
1550 .csiget(1, "gt monster reward typ", 0)
1551 .unwrap_or(0);
1552 if one != 0 {
1553 warn!("hellevator monster t: {one}");
1554 }
1555 let typ = HellevatorMonsterRewardTyp::parse(raw_typ);
1556 let amount: u64 =
1557 chunk.csiget(2, "gt monster reward amount", 0)?;
1558 hellevator
1559 .monster_rewards
1560 .push(HellevatorMonsterReward { typ, amount });
1561 }
1562 }
1563 "gtdailyreward" => {
1564 self.hellevator
1565 .active
1566 .get_or_insert_with(Default::default)
1567 .rewards_today = HellevatorDailyReward::parse(
1568 &val.into_list("hdrtd").unwrap_or_default(),
1569 );
1570 }
1571 "gtdailyrewardnext" => {
1572 self.hellevator
1573 .active
1574 .get_or_insert_with(Default::default)
1575 .rewards_next = HellevatorDailyReward::parse(
1576 &val.into_list("hdrnd").unwrap_or_default(),
1577 );
1578 }
1579 "gtdailyrewardyesterday" => {
1580 self.hellevator
1581 .active
1582 .get_or_insert_with(Default::default)
1583 .rewards_yesterday = HellevatorDailyReward::parse(
1584 &val.into_list("hdryd").unwrap_or_default(),
1585 );
1586 }
1587 "gtdailyrewardclaimed" => {
1588 if let Some(hellevator) = self.hellevator.active.as_mut() {
1589 if !all_values.contains_key("gtdailyreward") {
1595 hellevator.rewards_yesterday = None;
1598 }
1599 }
1600 }
1601 "gtranking" => {
1602 self.hall_of_fames.hellevator = val
1603 .as_str()
1604 .split(';')
1605 .filter(|a| !a.is_empty())
1606 .map(|chunk| chunk.split(',').collect())
1607 .flat_map(|chunk: Vec<_>| -> Result<_, SFError> {
1608 Ok(HallOfFameHellevator {
1609 rank: chunk.cfsuget(0, "hh rank")?,
1610 name: chunk.cget(1, "hh name")?.to_string(),
1611 tokens: chunk.cfsuget(2, "hh tokens")?,
1612 })
1613 })
1614 .collect();
1615 }
1616 "gtpreviewreward" => {
1617 }
1637 "gtmonster" => {
1638 self.hellevator
1639 .active
1640 .get_or_insert_with(Default::default)
1641 .current_monster = HellevatorMonster::parse(
1642 &val.into_list("h monster").unwrap_or_default(),
1643 )
1644 .ok();
1645 }
1646 "gtbonus" => {
1647 self.hellevator
1648 .active
1649 .get_or_insert_with(Default::default)
1650 .daily_treat_bonus = val
1651 .into_list("gt bonus")
1652 .and_then(|a| HellevatorTreatBonus::parse(&a))
1653 .ok();
1654 }
1655 "pendingrewards" => {
1656 let vals: Vec<_> = val.as_str().split('/').collect();
1657 self.mail.claimables = vals
1658 .chunks_exact(6)
1659 .flat_map(|chunk| -> Result<ClaimableMail, SFError> {
1660 let start = chunk.cfsuget(4, "p reward start")?;
1661 let end = chunk.cfsuget(5, "p reward end")?;
1662
1663 let status = match chunk.cget(1, "p read")? {
1664 "0" => ClaimableStatus::Unread,
1665 "1" => ClaimableStatus::Read,
1666 "2" => ClaimableStatus::Claimed,
1667 x => {
1668 warn!("Unknown claimable status: {x}");
1669 ClaimableStatus::Claimed
1670 }
1671 };
1672
1673 Ok(ClaimableMail {
1674 typ: FromPrimitive::from_i64(
1675 chunk.cfsuget(2, "claimable typ")?,
1676 )
1677 .unwrap_or_default(),
1678 msg_id: chunk.cfsuget(0, "msg_id")?,
1679 status,
1680 name: chunk.cget(3, "reward code")?.to_string(),
1681 received: server_time
1682 .convert_to_local(start, "p start"),
1683 claimable_until: server_time
1684 .convert_to_local(end, "p end"),
1685 })
1686 })
1687 .collect();
1688 }
1689 "pendingrewardressources" => {
1690 let vals: Vec<i64> =
1691 val.into_list("pendingrewardressources")?;
1692
1693 self.mail
1694 .open_claimable
1695 .get_or_insert_with(Default::default)
1696 .resources = vals
1697 .chunks_exact(2)
1698 .flat_map(|chunk| -> Result<Reward, SFError> {
1699 Ok(Reward {
1700 typ: RewardType::parse(chunk.cget(0, "c typ")?),
1701 amount: chunk.csiget(1, "c amount", 1)?,
1702 })
1703 })
1704 .collect();
1705 }
1706 "pendingreward" => {
1707 let vals: Vec<i64> = val.into_list("pending item")?;
1708 self.mail
1709 .open_claimable
1710 .get_or_insert_with(Default::default)
1711 .items = vals
1712 .chunks_exact(ITEM_PARSE_LEN)
1713 .flat_map(|a|
1714 Item::parse(a, server_time))
1716 .flatten()
1717 .collect();
1718 }
1719 "fightablegroups" => {
1720 self.guild
1721 .get_or_insert_default()
1722 .update_fightable_targets(val.as_str())?;
1723 }
1724 "adventscalendar" => {
1725 let vals: Vec<i64> = val.into_list("advent door")?;
1726 self.specials.advent_calendar = match vals.first() {
1727 Some(0) | None => None,
1728 _ => Reward::parse(&vals).ok(),
1729 };
1730 }
1731 "fortresschances" => {
1732 }
1736 "deedsandtitlesplayersave" => {
1737 }
1741 "deedshelves" => {
1742 }
1745 "fortressstorage" => {
1746 self.fortress.get_or_insert_default().update_resources(
1747 &val.into_list("ft resources")?,
1748 server_time,
1749 )?;
1750 }
1751 "fortressunits" => {
1752 self.fortress
1753 .get_or_insert_default()
1754 .update_units(&val.into_list("ft units")?, server_time)?;
1755 }
1756 "fortress" => {
1757 self.fortress
1758 .get_or_insert_default()
1759 .update(&val.into_list("fortress")?, server_time)?;
1760 }
1761 "wheel" => {
1762 let data: Vec<i64> = val.into_list("wheel")?;
1763 self.specials.wheel.spins_today =
1765 data.csiget(1, "lucky turns", 0)?;
1766 self.specials.wheel.next_free_spin =
1767 data.cstget(2, "next lucky turn", server_time)?;
1768 }
1769 "dice" => {
1770 let data: Vec<i64> = val.into_list("dice")?;
1771 self.tavern.dice_game.next_free =
1772 data.cstget(0, "dice next", server_time)?;
1773 self.tavern.dice_game.remaining =
1774 data.csiget(1, "rem dice games", 0)?;
1775 }
1776 "charactergroup" => {
1777 let data: Vec<i64> = val.into_list("c group")?;
1778 let guild = self.guild.get_or_insert_with(Default::default);
1779 guild.own_treasure_skill =
1780 data.csiget(0, "own treasure skill", 0)?;
1781 guild.own_instructor_skill =
1782 data.csiget(1, "own instruction skill", 0)?;
1783 guild.hydra.next_battle =
1784 data.cstget(2, "pet battle", server_time)?;
1785 guild.hydra.remaining_fights =
1786 data.csiget(3, "remaining pet battles", 0)?;
1787 guild.own_pet_lvl = data.csiget(4, "own pet lvl", 0)?;
1788 guild.joined = data.cstget(5, "guild joined", server_time)?;
1789 }
1791 "arena" => {
1792 let data: Vec<i64> = val.into_list("arena")?;
1793 self.arena.next_free_fight =
1794 data.cstget(0, "next battle time", server_time)?;
1795 self.arena.fights_for_xp =
1796 data.csiget(1, "arena xp fights", 0)?;
1797 for (idx, val) in self.arena.enemy_ids.iter_mut().enumerate() {
1798 *val = data.csiget(2 + idx, "arena enemy id", 0)?;
1799 }
1800 }
1802 "ownplayersavepotions" => {
1803 let data: Vec<i64> = val.into_list("potions")?;
1804 self.character.active_potions =
1805 items::parse_active_potions(&data, server_time);
1806 }
1807 "arcanetoilet" => {
1808 let data: Vec<i64> = val.into_list("toilet")?;
1809
1810 let toilet_lvl = data.cget(0, "toilet lvl")?;
1812 if toilet_lvl > 0 {
1813 self.tavern
1814 .toilet
1815 .get_or_insert_with(Default::default)
1816 .update(&data, server_time)?;
1817 }
1818 }
1819 "vipstatus" => {
1820 other_player.get_or_insert_default().is_vip =
1821 val.as_str() != "0";
1822 }
1823 "characterstatus" => {
1824 let data: Vec<i64> = val.into_list("char status")?;
1825
1826 self.tavern.current_action = CurrentAction::parse(
1827 data.cget(1, "action id")?,
1828 data.cget(2, "action sec")?,
1829 data.cstget(3, "current action time", server_time)?,
1830 );
1831
1832 self.tavern.beer_max = data.csiget(5, "beer total", 0)?;
1834
1835 self.tavern.thirst_for_adventure_sec =
1836 data.csiget(6, "remaining ALU", 0)?;
1837 self.tavern.beer_drunk =
1838 data.csiget(7, "beer drunk count", 0)?;
1839 self.specials.calendar.collected =
1840 data.csiget(8, "calendar collected", 245)?;
1841 self.specials.calendar.next_possible =
1842 data.cstget(9, "calendar next", server_time)?;
1843 self.pets
1854 .get_or_insert_with(Default::default)
1855 .next_free_exploration =
1856 data.cstget(20, "pet next free exp", server_time)?;
1857 self.dungeons.next_free_fight =
1858 data.cstget(21, "dungeon timer", server_time)?;
1859 if let Some(start) =
1860 data.cstget(22, "dungeon timer", server_time)?
1861 {
1862 self.legendary_dungeon
1863 .active
1864 .get_or_insert_default()
1865 .healing_start = Some(start);
1866 }
1867 }
1877 "ownplayersavecharacter" => {
1878 let data: Vec<i64> = val.into_list("char save")?;
1879
1880 self.character.player_id = data.csiget(1, "player id", 0)?;
1882 self.character.level =
1884 data.csimget(3, "level", 0, |a| a & 0xFFFF)?;
1885 self.character.experience = data.csiget(4, "experience", 0)?;
1886 self.character.next_level_xp =
1887 data.csiget(5, "xp to next lvl", 0)?;
1888 self.character.honor = data.csiget(6, "honor", 0)?;
1889 self.character.rank = data.csiget(7, "rank", 0)?;
1890 self.character.portrait =
1891 Portrait::parse(data.skip(8, "portrait")?)
1892 .unwrap_or_default();
1893 self.character.race = data.cfpuget(18, "char race", |a| a)?;
1905 self.character.class =
1908 data.cfpuget(20, "character class", |a| a - 1)?;
1909 self.character.mount =
1910 data.cfpget(21, "character mount", |a| a & 0xFF)?;
1911 self.character.armor = data.csiget(23, "total armor", 0)?;
1913 self.character.min_damage = data.csiget(24, "min damage", 0)?;
1914 self.character.max_damage = data.csiget(25, "max damage", 0)?;
1915 self.guild
1916 .get_or_insert_with(Default::default)
1917 .portal
1918 .damage_bonus =
1919 data.cimget(26, "portal dmg bonus", |a| a)?;
1920 self.dungeons
1922 .portal
1923 .get_or_insert_with(Default::default)
1924 .player_hp_bonus =
1925 data.csimget(28, "portal hp bonus", 0, |a| a)?;
1926 self.character.mount_end =
1927 data.cstget(29, "mount end", server_time)?;
1928 update_enum_map(
1929 &mut self.character.attribute_basis,
1930 data.skip(30, "char attr basis")?,
1931 );
1932 update_enum_map(
1933 &mut self.character.attribute_additions,
1934 data.skip(35, "char attr adds")?,
1935 );
1936 update_enum_map(
1937 &mut self.character.attribute_times_bought,
1938 data.skip(40, "char attr tb")?,
1939 );
1940 }
1966 "adventure" => {
1967 let data: Vec<i64> = val.into_list("char save")?;
1968 for (slice, quest) in data
1971 .skip(2, "quests")?
1972 .chunks_exact(7)
1973 .zip(&mut self.tavern.quests)
1974 {
1975 quest.update(slice)?;
1976 }
1977 }
1978 "events" => {
1979 }
1982
1983 "iadungeonchances" => {
1985 }
1987 "iadungeontime" => {
1988 let dungeons = &mut self.legendary_dungeon;
1989
1990 let vals: Vec<i64> = val.into_list("iadungeontime")?;
1991 dungeons.theme = vals.cfpget(0, "ld theme", |x| x)?;
1992 dungeons.start = vals.cstget(1, "ld start", server_time)?;
1993 dungeons.end = vals.cstget(2, "ld end", server_time)?;
1994 dungeons.close = vals.cstget(3, "ld closes", server_time)?;
1995 }
1996 "iadungeonstatstotal" => {
1997 let dungeons =
1998 self.legendary_dungeon.active.get_or_insert_default();
1999
2000 let data: Vec<i64> = val.into_list("iadungeonstatstotal")?;
2001 dungeons.total_stats = TotalStats::parse(&data)?;
2002 }
2003 "iadungeonstats" => {
2004 let dungeons =
2005 self.legendary_dungeon.active.get_or_insert_default();
2006
2007 let data = val.into_list("iadungeonstats")?;
2008 dungeons.stats = Stats::parse(&data).unwrap_or_default();
2009 }
2010 "iadungeon" => {
2011 let data: Vec<i64> = val.into_list("iadungeon")?;
2012 let dungeons =
2013 self.legendary_dungeon.active.get_or_insert_default();
2014 dungeons.update(&data)?;
2015 if !all_values.contains_key("iapendingitems") {
2016 dungeons.pending_items.clear();
2017 }
2018 }
2019 "iapendingitems" => {
2020 let dungeons =
2021 self.legendary_dungeon.active.get_or_insert_default();
2022 dungeons.pending_items.clear();
2023 let data: Vec<i64> = val.into_list("iapendingitems")?;
2024 let amount: i64 = data.cget(0, "pending amount")?;
2025 if amount < 1 {
2026 return Ok(());
2027 }
2028 for slice in
2029 data.skip(1, "ld items")?.chunks_exact(ITEM_PARSE_LEN)
2030 {
2031 let Some(item) = Item::parse(slice, server_time)? else {
2032 warn!("Could not parse pending ld item");
2033 continue;
2034 };
2035 dungeons.pending_items.push(item);
2036 }
2037 }
2038 "ialootitem" => {
2039 }
2041 "iamerchant" => {
2042 let data: Vec<i64> = val.into_list("iamerchant")?;
2043
2044 self.legendary_dungeon
2045 .active
2046 .get_or_insert_default()
2047 .merchant_offers = data
2048 .chunks_exact(3)
2049 .flat_map(MerchantOffer::parse)
2050 .flatten()
2051 .collect();
2052 }
2053 "iadungeon20cost" => {
2054 self.legendary_dungeon
2055 .active
2056 .get_or_insert_default()
2057 .heal_quarter_cost = val.into("iadungeon20cost")?;
2058 }
2059 "iadungeonsoulstones" => {
2060 let dungeons =
2061 self.legendary_dungeon.active.get_or_insert_default();
2062
2063 let data: Vec<i64> = val.into_list("iamerchant")?;
2064 let mut chunks = data.chunks_exact(6);
2065 dungeons.active_gems = chunks
2066 .by_ref()
2067 .take(3)
2068 .flat_map(GemOfFate::parse)
2069 .flatten()
2070 .collect();
2071
2072 dungeons.available_gems =
2073 chunks.flat_map(GemOfFate::parse).flatten().collect();
2074 }
2075 "iamap" => {
2076 }
2093 "otherplayerfortressinfo" => {
2094 other_player
2095 .get_or_insert_default()
2096 .update_fortress(&val.into_list("other ft")?)?;
2097 }
2098 x if x.contains("average") && x.ends_with("level") => {
2099 }
2101 x if x.contains("dungeonenemies") => {
2103 }
2105 x if x.starts_with("attbonus") => {
2106 }
2108 x => {
2109 warn!("Update ignored {x} -> {val:?}");
2110 }
2111 }
2112 Ok(())
2113 }
2114}
2115
2116fn fight_no_from_header(header_name: &str) -> usize {
2119 let number_str =
2120 header_name.trim_start_matches(|a: char| !a.is_ascii_digit());
2121 let id: usize = number_str.parse().unwrap_or(1);
2122 id.max(1)
2123}
2124
2125#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2130pub struct ServerTime(i64);
2131
2132impl ServerTime {
2133 #[must_use]
2135 pub(crate) fn convert_to_local(
2136 self,
2137 timestamp: i64,
2138 name: &str,
2139 ) -> Option<DateTime<Local>> {
2140 if matches!(timestamp, 0 | -1 | 1 | 11) {
2141 return None;
2143 }
2144
2145 if !(1_000_000_000..=3_000_000_000).contains(×tamp) {
2146 warn!("Weird time stamp: {timestamp} for {name}");
2147 return None;
2148 }
2149 DateTime::from_timestamp(timestamp - self.0, 0)?
2150 .naive_utc()
2151 .and_local_timezone(Local)
2152 .latest()
2153 }
2154
2155 #[must_use]
2160 pub fn current(&self) -> NaiveDateTime {
2161 Local::now().naive_local() + Duration::seconds(self.0)
2162 }
2163
2164 #[must_use]
2165 pub fn next_midnight(&self) -> std::time::Duration {
2166 let current = self.current();
2167 let tomorrow = current.date() + Duration::days(1);
2168 let tomorrow = NaiveDateTime::from(tomorrow);
2169 let sec_until_midnight =
2170 (tomorrow - current).to_std().unwrap_or_default().as_secs();
2171 std::time::Duration::from_secs(sec_until_midnight % (60 * 60 * 24))
2174 }
2175}
2176
2177trait StringSetExt {
2179 fn set(&mut self, s: &str);
2180}
2181
2182impl StringSetExt for String {
2183 fn set(&mut self, s: &str) {
2187 self.replace_range(.., s);
2188 }
2189}
2190
2191#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2193#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2194pub struct NormalCost {
2195 pub silver: u64,
2197 pub mushrooms: u16,
2199}