sf_api/gamestate/
tavern.rs

1use chrono::{DateTime, Local};
2use log::{error, warn};
3use num_derive::FromPrimitive;
4use num_traits::FromPrimitive;
5
6use super::{
7    ArrSkip, CCGet, CFPGet, CGet, CSTGet, ExpeditionSetting, SFError,
8    ServerTime, items::Item,
9};
10use crate::{
11    command::{DiceReward, DiceType},
12    gamestate::rewards::Reward,
13    misc::soft_into,
14};
15
16/// Anything related to things you can do in the tavern
17#[derive(Debug, Clone, Default)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub struct Tavern {
20    /// All the available quests
21    pub quests: [Quest; 3],
22    /// How many seconds the character still has left to do adventures
23    #[doc(alias = "alu")]
24    pub thirst_for_adventure_sec: u32,
25    /// Whether or not skipping with mushrooms is allowed
26    pub mushroom_skip_allowed: bool,
27    /// The amount of beers we already drank today
28    pub beer_drunk: u8,
29    /// The amount of quicksand glasses we have and can use to skip quests
30    pub quicksand_glasses: u32,
31    /// The thing the player is currently doing (either questing or working)
32    pub current_action: CurrentAction,
33    /// The amount of silver earned per hour working the guard jobs
34    pub guard_wage: u64,
35    /// The toilet, if it has been unlocked
36    pub toilet: Option<Toilet>,
37    /// The dice game you can play with the weird guy in the tavern
38    pub dice_game: DiceGame,
39    /// Information about everything related to expeditions
40    pub expeditions: ExpeditionsEvent,
41    /// Decides if you can on on expeditions, or quests, when this event is
42    /// currently ongoing
43    pub questing_preference: ExpeditionSetting,
44    /// The result of playing the shell game
45    pub gamble_result: Option<GambleResult>,
46    /// Total amount of beers you can drink today
47    pub beer_max: u8,
48}
49
50/// Information about everything related to expeditions
51#[derive(Debug, Clone, Default)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53pub struct ExpeditionsEvent {
54    /// The time the expeditions mechanic was enabled at
55    pub start: Option<DateTime<Local>>,
56    /// The time until which expeditions will be available
57    pub end: Option<DateTime<Local>>,
58    /// The expeditions available to do
59    pub available: Vec<AvailableExpedition>,
60    /// The expedition the player is currently doing. Accessible via the
61    /// `active()` method.
62    pub(crate) active: Option<Expedition>,
63}
64
65impl ExpeditionsEvent {
66    /// Checks if the event has started and not yet ended compared to the
67    /// current time
68    #[must_use]
69    pub fn is_event_ongoing(&self) -> bool {
70        let now = Local::now();
71        matches!((self.start, self.end), (Some(start), Some(end)) if end > now && start < now)
72    }
73
74    /// Expeditions finish after the last timer elapses. That means, this can
75    /// happen without any new requests. To make sure you do not access an
76    /// expedition, that has elapsed, you access expeditions with this
77    #[must_use]
78    pub fn active(&self) -> Option<&Expedition> {
79        self.active.as_ref().filter(|a| !a.is_finished())
80    }
81
82    /// Expeditions finish after the last timer elapses. That means, this can
83    /// happen without any new requests. To make sure you do not access an
84    /// expedition, that has elapsed, you access expeditions with this
85    #[must_use]
86    pub fn active_mut(&mut self) -> Option<&mut Expedition> {
87        self.active.as_mut().filter(|a| !a.is_finished())
88    }
89}
90
91/// Information about the current state of the dice game
92#[derive(Debug, Clone, Default)]
93#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
94pub struct DiceGame {
95    /// The amount of dice games you can still play today
96    pub remaining: u8,
97    /// The next free dice game can be played at this point in time
98    pub next_free: Option<DateTime<Local>>,
99    /// These are the dices, that are laying on the table after the first
100    /// round. The ones you can select to keep from
101    pub current_dice: Vec<DiceType>,
102    /// Whatever we won in the dice game
103    pub reward: Option<DiceReward>,
104}
105
106/// The tasks you will presented with, when clicking the person in the tavern.
107/// Make sure you are not currently busy and have enough ALU/thirst of adventure
108/// before trying to start them
109#[derive(Debug, Clone)]
110#[allow(missing_docs)]
111pub enum AvailableTasks<'a> {
112    Quests(&'a [Quest; 3]),
113    Expeditions(&'a [AvailableExpedition]),
114}
115
116impl Tavern {
117    /// Checks if the player is currently doing anything. Note that this may
118    /// change between calls, as expeditions finish without sending any collect
119    /// commands. In most cases you should match on the `current_action`
120    /// yourself and collect/wait, if necessary, but if you want a quick sanity
121    /// check somewhere, to make sure you are idle, this is the function for you
122    #[must_use]
123    pub fn is_idle(&self) -> bool {
124        match self.current_action {
125            CurrentAction::Idle => true,
126            CurrentAction::Expedition => self.expeditions.active.is_none(),
127            _ => false,
128        }
129    }
130
131    /// Gives you the same tasks, that the person in the tavern would present
132    /// you with. When expeditions are available and they are not disabled by
133    /// the `questing_preference`, they will be shown. Otherwise you will get
134    /// quests
135    #[must_use]
136    pub fn available_tasks(&self) -> AvailableTasks<'_> {
137        if self.questing_preference == ExpeditionSetting::PreferExpeditions
138            && self.expeditions.is_event_ongoing()
139        {
140            AvailableTasks::Expeditions(&self.expeditions.available)
141        } else {
142            AvailableTasks::Quests(&self.quests)
143        }
144    }
145
146    /// The expedition/questing setting can only be changed, before any
147    /// alu/thirst for adventure is used that day
148    #[must_use]
149    pub fn can_change_questing_preference(&self) -> bool {
150        self.thirst_for_adventure_sec == 6000 && self.beer_drunk == 0
151    }
152
153    pub(crate) fn update(
154        &mut self,
155        data: &[i64],
156        server_time: ServerTime,
157    ) -> Result<(), SFError> {
158        self.current_action = CurrentAction::parse(
159            data.cget(45, "action id")? & 0xFF,
160            data.cget(46, "action sec")? & 0xFF,
161            data.cstget(47, "current action time", server_time)?,
162        );
163        self.thirst_for_adventure_sec = data.csiget(456, "remaining ALU", 0)?;
164        self.beer_drunk = data.csiget(457, "beer drunk count", 0)?;
165        self.beer_max = data.csiget(13, "beer total", 0)?;
166
167        for (qidx, quest) in self.quests.iter_mut().enumerate() {
168            let quest_start = data.skip(235 + qidx, "tavern quest")?;
169            quest.update(quest_start)?;
170        }
171        Ok(())
172    }
173}
174
175/// One of the three possible quests in the tavern
176#[derive(Debug, Default, Clone, PartialEq, Eq)]
177#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
178pub struct Quest {
179    /// The length of this quest in sec (without item enchantment)
180    pub base_length: u32,
181    /// The silver reward for this quest (without item enchantment)
182    pub base_silver: u32,
183    /// The xp reward for this quest (without item enchantment)
184    pub base_experience: u32,
185    /// The item reward for this quest
186    pub item: Option<Item>,
187    /// The place where this quest takes place. Useful for the scrapbook
188    pub location_id: Location,
189    /// The enemy you fight in this quest. Useful for the scrapbook
190    pub monster_id: u16,
191}
192
193/// The background/location for a quest, or another activity
194#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, FromPrimitive, Hash)]
195#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
196#[allow(missing_docs)]
197pub enum Location {
198    #[default]
199    SprawlingJungle = 1,
200    SkullIsland,
201    EvernightForest,
202    StumbleSteppe,
203    ShadowrockMountain,
204    SplitCanyon,
205    BlackWaterSwamp,
206    FloodedCaldwell,
207    TuskMountain,
208    MoldyForest,
209    Nevermoor,
210    BustedLands,
211    Erogenion,
212    Magmaron,
213    SunburnDesert,
214    Gnarogrim,
215    Northrunt,
216    BlackForest,
217    Maerwynn,
218    PlainsOfOzKorr,
219    RottenLands,
220}
221
222impl Quest {
223    /// Checks if this is a red quest, which means a special enemy + extra
224    /// rewards
225    #[must_use]
226    pub fn is_red(&self) -> bool {
227        matches!(self.monster_id, 139 | 145 | 148 | 152 | 155 | 157)
228    }
229
230    pub(crate) fn update(&mut self, data: &[i64]) -> Result<(), SFError> {
231        self.base_length = data.csiget(6, "quest length", 100_000)?;
232        self.base_silver = data.csiget(48, "quest silver", 0)?;
233        self.base_experience = data.csiget(45, "quest xp", 0)?;
234        self.location_id = data
235            .cfpget(3, "quest location id", |a| a)?
236            .unwrap_or_default();
237        self.monster_id = data.csimget(0, "quest monster id", 0, |a| -a)?;
238        Ok(())
239    }
240}
241
242/// The thing the player is currently doing
243#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
244#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
245pub enum CurrentAction {
246    /// The character is not doing anything and can basically do anything
247    #[default]
248    Idle,
249    /// The character is working on guard duty right now. If `busy_until <
250    /// Local::now()`, you can send a `WorkFinish` command
251    CityGuard {
252        /// The total amount of hours the character has decided to work
253        hours: u8,
254        /// The time at which the guard job will be over
255        busy_until: DateTime<Local>,
256    },
257    /// The character is doing a quest right now. If `busy_until <
258    /// Local::now()` you can send a `FinishQuest` command
259    Quest {
260        /// 0-2 index into tavern quest array
261        quest_idx: u8,
262        /// The time, at which the quest can be finished
263        busy_until: DateTime<Local>,
264    },
265    /// The player is currently doing an expedition. This can be wrong, if the
266    /// last expedition timer elapsed since sending the last request
267    Expedition,
268    /// The character is not able to do something, but we do not know what.
269    /// Most likely something from a new update
270    Unknown(Option<DateTime<Local>>),
271}
272
273impl CurrentAction {
274    pub(crate) fn parse(
275        id: i64,
276        sec: i64,
277        busy: Option<DateTime<Local>>,
278    ) -> Self {
279        match (id, busy) {
280            (0, None) => CurrentAction::Idle,
281            (1, Some(busy_until)) => CurrentAction::CityGuard {
282                hours: soft_into(sec, "city guard time", 10),
283                busy_until,
284            },
285            (2, Some(busy_until)) => CurrentAction::Quest {
286                quest_idx: soft_into(sec, "quest index", 0),
287                busy_until,
288            },
289            (4, None) => CurrentAction::Expedition,
290            _ => {
291                error!("Unknown action id combination: {id}, {busy:?}");
292                CurrentAction::Unknown(busy)
293            }
294        }
295    }
296}
297
298/// The unlocked toilet, that you can throw items into
299#[derive(Debug, Clone, Default, Copy)]
300#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
301pub struct Toilet {
302    // Checks if all sacrifices today have been used up
303    #[deprecated(note = "You should use sacrifices_left instead")]
304    pub used: bool,
305    /// The level the aura is at currently
306    pub aura: u32,
307    /// The amount of mana currently in the toilet
308    pub mana_currently: u32,
309    /// The total amount of mana you have to collect to flush the toilet
310    pub mana_total: u32,
311    // The amount of sacrifices you have left today
312    pub sacrifices_left: u32,
313}
314
315impl Toilet {
316    pub(crate) fn update(&mut self, data: &[i64]) -> Result<(), SFError> {
317        self.aura = data.csiget(491, "aura level", 0)?;
318        self.mana_currently = data.csiget(492, "mana now", 0)?;
319        self.mana_total = data.csiget(515, "mana missing", 1000)?;
320        Ok(())
321    }
322}
323
324/// The state of an ongoing expedition
325#[derive(Debug, Clone, Default)]
326#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
327pub struct Expedition {
328    /// The items collected durign the expedition
329    pub items: [Option<ExpeditionThing>; 4],
330
331    /// The thing, that we are searching on this expedition
332    pub target_thing: ExpeditionThing,
333    /// The amount of the target item we have found
334    pub target_current: u8,
335    /// The amount of the target item that we are supposed to find
336    pub target_amount: u8,
337
338    /// The level we are currently clearing. Starts at 1
339    pub current_floor: u8,
340    ///  The heroism we have collected so far
341    pub heroism: i32,
342
343    pub(crate) adjusted_bounty_heroism: bool,
344
345    pub(crate) floor_stage: i64,
346
347    /// Choose one of these rewards
348    pub(crate) rewards: Vec<Reward>,
349    pub(crate) halftime_for_boss_id: i64,
350    /// If we encountered a boss, this will contain information about it
351    pub(crate) boss: ExpeditionBoss,
352    /// The different encounters, that you can choose between. Should be 3
353    pub(crate) encounters: Vec<ExpeditionEncounter>,
354    pub(crate) busy_until: Option<DateTime<Local>>,
355}
356
357impl Expedition {
358    pub(crate) fn adjust_bounty_heroism(&mut self) {
359        if self.adjusted_bounty_heroism {
360            return;
361        }
362
363        for ExpeditionEncounter { typ, heroism } in &mut self.encounters {
364            if let Some(possible_bounty) = typ.required_bounty()
365                && self.items.iter().flatten().any(|a| a == &possible_bounty)
366            {
367                *heroism += 10;
368            }
369        }
370        self.adjusted_bounty_heroism = true;
371    }
372
373    pub(crate) fn update_encounters(&mut self, data: &[i64]) {
374        if !data.len().is_multiple_of(2) {
375            warn!("weird encounters: {data:?}");
376        }
377        let default_ecp = |ci| {
378            warn!("Unknown encounter: {ci}");
379            ExpeditionThing::Unknown
380        };
381        self.encounters = data
382            .chunks_exact(2)
383            .filter_map(|ci| {
384                let raw = *ci.first()?;
385                let typ = FromPrimitive::from_i64(raw)
386                    .unwrap_or_else(|| default_ecp(raw));
387                let heroism = soft_into(*ci.get(1)?, "e heroism", 0);
388                Some(ExpeditionEncounter { typ, heroism })
389            })
390            .collect();
391    }
392
393    /// Returns the current stage the player is doing. This is dependent on
394    /// time, because the timers are lazily evaluated. That means it might
395    /// flip from Waiting->Encounters/Finished between calls
396    #[must_use]
397    pub fn current_stage(&self) -> ExpeditionStage {
398        let cross_roads =
399            || ExpeditionStage::Encounters(self.encounters.clone());
400
401        match self.floor_stage {
402            1 => cross_roads(),
403            2 => ExpeditionStage::Boss(self.boss),
404            3 => ExpeditionStage::Rewards(self.rewards.clone()),
405            4 => match self.busy_until {
406                Some(x) if x > Local::now() => ExpeditionStage::Waiting(x),
407                _ if self.current_floor == 10 => ExpeditionStage::Finished,
408                _ => cross_roads(),
409            },
410            _ => ExpeditionStage::Unknown,
411        }
412    }
413
414    /// Checks, if the last timer of this expedition has run out
415    #[must_use]
416    pub fn is_finished(&self) -> bool {
417        matches!(self.current_stage(), ExpeditionStage::Finished)
418    }
419}
420
421/// The current thing, that would be on screen, when using the web client
422#[derive(Debug, Clone)]
423#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
424pub enum ExpeditionStage {
425    /// Choose one of these rewards after winning against the boss
426    Rewards(Vec<Reward>),
427    /// If we encountered a boss, this will contain information about it
428    Boss(ExpeditionBoss),
429    /// The different encounters, that you can choose between. Should be <= 3
430    Encounters(Vec<ExpeditionEncounter>),
431    /// We have to wait until the specified time to continue in the expedition.
432    /// When this is `< Local::now()`, you can send the update command to
433    /// update the expedition stage, which will make `current_stage()`
434    /// yield the new encounters
435    Waiting(DateTime<Local>),
436    /// The expedition has finished and you can choose another one
437    Finished,
438    /// Something strange happened and the current stage is not known. Feel
439    /// free to try anything from logging in again to just continuing
440    Unknown,
441}
442
443impl Default for ExpeditionStage {
444    fn default() -> Self {
445        ExpeditionStage::Encounters(Vec::new())
446    }
447}
448
449/// The monster you fight after 5 and 10 expedition encounters
450#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
451#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
452pub struct ExpeditionBoss {
453    /// The monster id of this boss
454    pub id: i64,
455    /// The amount of items this boss is supposed to drop
456    pub items: u8,
457}
458
459/// One of up to three encounters you can find. In comparison to
460/// `ExpeditionThing`, this also includes the expected heroism
461#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
462#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
463pub struct ExpeditionEncounter {
464    /// The type of thing you engage, or find on this path
465    pub typ: ExpeditionThing,
466    /// The heroism you get from picking this encounter. This contains the
467    /// bonus from bounties, but no further boni from
468    pub heroism: i32,
469}
470
471/// The type of something you can encounter on the expedition. Can also be found
472/// as the target, or in the items section
473#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, Default)]
474#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
475#[allow(missing_docs, clippy::doc_markdown)]
476pub enum ExpeditionThing {
477    #[default]
478    Unknown = 0,
479
480    Dummy1 = 1,
481    Dummy2 = 2,
482    Dumy3 = 3,
483
484    ToiletPaper = 11,
485
486    Bait = 21,
487    /// New name: `DragonTaming`
488    Dragon = 22,
489
490    CampFire = 31,
491    Phoenix = 32,
492    /// New name: `ExtinguishedCampfire`
493    BurntCampfire = 33,
494
495    UnicornHorn = 41,
496    Donkey = 42,
497    Rainbow = 43,
498    /// New name: `UnicornWhisperer`
499    Unicorn = 44,
500
501    CupCake = 51,
502    /// New name: `SucklingPig`
503    Cake = 61,
504
505    SmallHurdle = 71,
506    BigHurdle = 72,
507    /// New name: `PodiumClimber`
508    WinnersPodium = 73,
509
510    Socks = 81,
511    ClothPile = 82,
512    /// New name: `RevealingLady`
513    RevealingCouple = 83,
514
515    SwordInStone = 91,
516    BentSword = 92,
517    BrokenSword = 93,
518
519    Well = 101,
520    Girl = 102,
521    /// New name: `BewitchedStew`
522    Balloons = 103,
523
524    Prince = 111,
525    /// New name: `ToxicFountainCure`
526    RoyalFrog = 112,
527
528    Hand = 121,
529    Feet = 122,
530    Body = 123,
531    // New name: BuildAFriend
532    Klaus = 124,
533
534    Key = 131,
535    Suitcase = 132,
536
537    // Dont know if they all exist tbh
538    DummyBounty = 1000,
539    ToiletPaperBounty = 1001,
540    DragonBounty = 1002,
541    BurntCampfireBounty = 1003,
542    UnicornBounty = 1004,
543    WinnerPodiumBounty = 1007,
544    RevealingCoupleBounty = 1008,
545    BrokenSwordBounty = 1009,
546    BaloonBounty = 1010,
547    FrogBounty = 1011,
548    KlausBounty = 1012,
549}
550
551impl ExpeditionThing {
552    /// Returns the associated bounty item required to get a +10 bonus for
553    /// picking up this item
554    #[must_use]
555    #[allow(clippy::enum_glob_use)]
556    pub fn required_bounty(&self) -> Option<ExpeditionThing> {
557        use ExpeditionThing::*;
558        Some(match self {
559            Dummy1 | Dummy2 | Dumy3 => DummyBounty,
560            ToiletPaper => ToiletPaperBounty,
561            Dragon => DragonBounty,
562            BurntCampfire => BurntCampfireBounty,
563            Unicorn => UnicornBounty,
564            WinnersPodium => WinnerPodiumBounty,
565            RevealingCouple => RevealingCoupleBounty,
566            BrokenSword => BrokenSwordBounty,
567            Balloons => BaloonBounty,
568            RoyalFrog => FrogBounty,
569            Klaus => KlausBounty,
570            _ => return None,
571        })
572    }
573
574    /// If the thing is a bounty, this will contain all the things, that receive
575    /// a bonus
576    #[must_use]
577    #[allow(clippy::enum_glob_use)]
578    pub fn is_bounty_for(&self) -> Option<&'static [ExpeditionThing]> {
579        use ExpeditionThing::*;
580        Some(match self {
581            DummyBounty => &[Dummy1, Dummy2, Dumy3],
582            ToiletPaperBounty => &[ToiletPaper],
583            DragonBounty => &[Dragon],
584            BurntCampfireBounty => &[BurntCampfire],
585            UnicornBounty => &[Unicorn],
586            WinnerPodiumBounty => &[WinnersPodium],
587            RevealingCoupleBounty => &[RevealingCouple],
588            BrokenSwordBounty => &[BrokenSword],
589            BaloonBounty => &[Balloons],
590            FrogBounty => &[RoyalFrog],
591            KlausBounty => &[Klaus],
592            _ => return None,
593        })
594    }
595}
596
597/// Information about a possible expedition, that you could start
598#[derive(Debug, Clone, Copy, PartialEq, Eq)]
599#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
600pub struct AvailableExpedition {
601    /// The target, that will be collected during the expedition
602    pub target: ExpeditionThing,
603    /// The amount of thirst for adventure, that selecting this expedition
604    /// costs and also the expected time the two waiting periods take
605    pub thirst_for_adventure_sec: u32,
606    /// The first location, that you visit during the expedition. Might
607    /// influence the haltime monsters type
608    pub location_1: Location,
609    /// The second location, that you visit during the expedition. Might
610    /// influence the final monsters type
611    pub location_2: Location,
612}
613
614/// The amount, that you either won or lost gambling. If the value is negative,
615/// you lost
616#[derive(Debug, Clone, Copy, PartialEq, Eq)]
617#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
618#[allow(missing_docs)]
619pub enum GambleResult {
620    SilverChange(i64),
621    MushroomChange(i32),
622}