Skip to main content

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