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