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