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