bfbb/game_interface/
mod.rs

1//! Allows performing actions on or reading information about a running instance of BfBB.
2
3use std::{
4    collections::HashMap,
5    ops::{Index, IndexMut},
6};
7
8use thiserror::Error;
9
10use crate::{
11    game_state::{GameMode, GameOstrich, GameState},
12    Level, Spatula,
13};
14
15use self::game_var::{GameVar, GameVarMut, InterfaceBackend};
16
17pub mod dolphin;
18pub mod game_var;
19pub mod mock;
20
21/// Interact with BfBB in an abstract way.
22///
23//// This struct allows accessing variables existing with a running instance of *Battle for Bikini Bottom*
24/// and performing actions on that instance in a way that is generic over a backend (e.g Dolphin and Xemu)
25///
26/// NOTE: To a see a list of supported backends see the `Implementors` list of [`InterfaceBackend`]
27///
28/// This is the key struct of the [`game_interface`](self) module and enables interacting with the game.
29#[non_exhaustive]
30pub struct GameInterface<F: InterfaceBackend> {
31    /// True while on loading screens
32    pub is_loading: F::Var<bool>,
33    /// Location of the [`GameState`] enum.
34    pub game_state: F::Mut<GameState>,
35    /// Location of the [`GameState`] enum.
36    pub game_mode: F::Mut<GameMode>,
37    /// Location of the [`GameOstrich`] enum.
38    ///
39    /// Not recommended to mutate this, but the option is available if you wish, it's probably not what you want to do.
40    pub game_ostrich: F::Mut<GameOstrich>,
41    /// [`Hans`]
42    pub hans: Hans<F>,
43    /// [`PowerUps`]
44    pub powers: PowerUps<F>,
45    /// Location of the ID for the current scene. Can be converted to a [`Level`](crate::Level) via [`TryFrom`].
46    ///
47    /// # Examples
48    /// ```
49    /// use std::error::Error;
50    ///
51    /// use bfbb::game_interface::GameInterface;
52    /// use bfbb::game_interface::game_var::{GameVar, InterfaceBackend};
53    /// use bfbb::Level;
54    ///
55    /// fn get_current_level<V: InterfaceBackend>(
56    ///     interface: &mut GameInterface<V>,
57    /// ) -> Result<Level, Box<dyn Error>> {
58    ///     Ok(interface.scene_id.get()?.try_into()?)
59    /// }
60    /// ```
61    pub scene_id: F::Var<[u8; 4]>,
62    /// Location of the spatula counter
63    pub spatula_count: F::Mut<u32>,
64    /// [`Tasks`]
65    pub tasks: Tasks<F>,
66
67    // TODO: This value is on the heap, it shouldn't be global like this
68    lab_door_cost: F::Mut<u32>,
69}
70
71/// A collection of [`Task`]s. Can be indexed by [`Spatula`]
72///
73/// # Examples
74///
75/// ```
76/// use bfbb::game_interface::game_var::{GameVar, GameVarMut, InterfaceBackend};
77/// use bfbb::game_interface::{InterfaceResult, Tasks};
78/// use bfbb::Spatula;
79///
80/// fn unlock_pinapple<V: InterfaceBackend>(tasks: &mut Tasks<V>) -> InterfaceResult<()> {
81///     tasks[Spatula::OnTopOfThePineapple].menu_count.set(1)
82/// }
83/// ```
84pub struct Tasks<F: InterfaceBackend> {
85    pub(crate) arr: HashMap<Spatula, Task<F>>,
86}
87
88/// Contains [`GameVar`]s for a [`Spatula`]'s pause-menu counter and game object state.
89#[non_exhaustive]
90pub struct Task<F: InterfaceBackend> {
91    /// The `count` field of this task's `_xCounter` struct.
92    ///
93    /// Notable values are:
94    /// - `0` => Task is "locked", will be a question mark in the menu.
95    /// - `1` => Task is "incomplete", will be a silver spatula in the menu.
96    /// - `2` => Task is "complete", will be a golden spatula in the menu.
97    /// - `3` => Task is also silver in the menu, this appears to only be used by [`Spatula::InfestationAtTheKrustyKrab`],
98    ///          which uses this value for after clearing the robots, but before you've collected the spatula.
99    /// - `_` => No icon will appear for this task in the menu, just an empty bubble. You can not warp to it and
100    ///          attempting to will put the menu into an invalid state until a different unlocked task is selected.
101    pub menu_count: F::Mut<i16>,
102    /// A bitfield of flags for a spatula entity. The first bit determines if the entity is enabled or not.
103    pub flags: Option<F::Mut<u8>>,
104    /// Another bitfield for a spatula entity.
105    pub state: Option<F::Mut<u32>>,
106}
107/// [`GameVar`]s related to the bubble bowl and cruise-missile
108#[non_exhaustive]
109pub struct PowerUps<F: InterfaceBackend> {
110    /// Whether the bubble bowl is currently unlocked
111    pub bubble_bowl: F::Mut<bool>,
112    /// Whether the cruise bubble is currently unlocked
113    pub cruise_bubble: F::Mut<bool>,
114    /// Whether new games should begin with the bubble bowl unlocked.
115    pub initial_bubble_bowl: F::Mut<bool>,
116    /// Whether new games should begin with the cruise missile
117    pub initial_cruise_bubble: F::Mut<bool>,
118}
119
120/// Implements methods for interacting with Hans' state.
121#[non_exhaustive]
122pub struct Hans<F: InterfaceBackend> {
123    flags: F::Mut<u8>,
124}
125
126impl<F: InterfaceBackend> GameInterface<F> {
127    /// Will start a new game when called. Only works when the player is on the main menu and not in the demo cutscene.
128    ///
129    /// # Errors
130    ///
131    /// Will return an [`InterfaceError`] if the implementation is unable to access the game.
132    pub fn start_new_game(&mut self) -> InterfaceResult<()> {
133        self.game_mode.set(GameMode::Game)
134    }
135
136    /// Get the level that the player is currently in
137    ///
138    /// # Errors
139    ///
140    /// Will return an [`InterfaceError::DataUnavailable`] sometimes when the game is loading due to the scene pointer being null
141    pub fn get_current_level(&self) -> InterfaceResult<Level> {
142        // TODO: It would be great to eventually allow self.scene_id to be of type `F::Var<Level>` directly and be able
143        //       to blanket impl `GameVar<T=Level>` for all backends by using an actual `GameVar<T=[u8;4]>` internally.
144        self.scene_id
145            .get()?
146            .try_into()
147            .map_err(|_| InterfaceError::DataUnavailable)
148    }
149
150    /// Marks a task as available (Silver). This will not update an already unlocked task.
151    ///
152    /// # Errors
153    ///
154    /// Will return an [`InterfaceError`] if the implementation is unable to access the game.
155    pub fn unlock_task(&mut self, spatula: Spatula) -> InterfaceResult<()> {
156        let task = &mut self.tasks[spatula];
157        let curr = task.menu_count.get()?;
158        if curr == 0 {
159            task.menu_count.set(1)?;
160        }
161        Ok(())
162    }
163
164    /// Marks a spatula as "completed" in the pause menu. This has the effect of giving the player access to the task warp.
165    ///
166    /// # Errors
167    ///
168    /// Will return an [`InterfaceError`] if the implementation is unable to access the game.
169    pub fn mark_task_complete(&mut self, spatula: Spatula) -> InterfaceResult<()> {
170        self.tasks[spatula].menu_count.set(2)
171    }
172
173    /// True when `spatula` is shown as gold in the pause menu.
174    ///
175    /// # Errors
176    ///
177    /// Will return an [`InterfaceError`] if the implementation is unable to access the game.
178    pub fn is_task_complete(&self, spatula: Spatula) -> InterfaceResult<bool> {
179        Ok(self.tasks[spatula].menu_count.get()? == 2)
180    }
181
182    /// Collect a spatula in the world. This only removes the entity, it will not complete the task or increment the spatula
183    /// counter.
184    ///
185    /// *NOTE*: Will always return `Ok(false)` when `spatula` is located in a level other than `current_level` or when
186    /// `spatula` is [Kah-Rah-Tae](Spatula::KahRahTae) or [The Small Shall Rule... Or Not](Spatula::TheSmallShallRuleOrNot)
187    /// without writing memory.
188    ///
189    /// # Errors
190    ///
191    /// Will return an [`InterfaceError`] if the implementation is unable to access the game.
192    pub fn collect_spatula(
193        &mut self,
194        spatula: Spatula,
195        current_level: Option<Level>,
196    ) -> InterfaceResult<()> {
197        if current_level != Some(spatula.get_level()) {
198            return Ok(());
199        }
200
201        let task = &mut self.tasks[spatula];
202        let (flags, state) = match (&mut task.flags, &mut task.state) {
203            (Some(flags), Some(state)) => (flags, state),
204            _ => return Ok(()),
205        };
206
207        let mut new_flags = flags.get()?;
208        new_flags &= !1; // Disable the entity
209
210        // Set some model flags
211        let mut new_state = state.get()?;
212        new_state |= 8;
213        new_state &= !4;
214        new_state &= !2;
215
216        flags.set(new_flags)?;
217        state.set(new_state)?;
218        Ok(())
219    }
220
221    /// True when `spatula`'s collected animation is playing
222    ///
223    /// *NOTE*: Will always return `Ok(false)` when `spatula` is located in a level other than `current_level` or when
224    /// `spatula` is [Kah-Rah-Tae](Spatula::KahRahTae) or [The Small Shall Rule... Or Not](Spatula::TheSmallShallRuleOrNot)
225    ///
226    /// # Errors
227    ///
228    /// Will return an [`InterfaceError`] if the implementation is unable to access the game.
229    pub fn is_spatula_being_collected(
230        &self,
231        spatula: Spatula,
232        current_level: Option<Level>,
233    ) -> InterfaceResult<bool> {
234        if current_level != Some(spatula.get_level()) {
235            return Ok(false);
236        }
237
238        let state = match &self.tasks[spatula].state {
239            Some(x) => x,
240            None => return Ok(false),
241        };
242
243        Ok(state.get()? & 4 != 0)
244    }
245
246    /// Changes the number of spatulas required to enter the Chum Bucket Lab.
247    ///
248    /// *NOTE*: This function requires that the current level is the Chum Bucket an will therefore always return `Ok(())`
249    /// if `current_level` is not `Some(Level::ChumBucket)`
250    ///
251    /// # Errors
252    ///
253    /// Will return an [`InterfaceError`] if the implementation is unable to access the game.
254    pub fn set_lab_door(
255        &mut self,
256        value: u32,
257        current_level: Option<Level>,
258    ) -> InterfaceResult<()> {
259        if current_level != Some(Level::ChumBucket) {
260            return Ok(());
261        }
262
263        // The game uses a greater than check so we need to subtract by one
264        let cost = value - 1;
265        self.lab_door_cost.set(cost)?;
266        Ok(())
267    }
268}
269
270impl<V: InterfaceBackend> Index<Spatula> for Tasks<V> {
271    type Output = Task<V>;
272
273    fn index(&self, index: Spatula) -> &Self::Output {
274        &self.arr[&index]
275    }
276}
277
278impl<T: InterfaceBackend> IndexMut<Spatula> for Tasks<T> {
279    fn index_mut(&mut self, index: Spatula) -> &mut Self::Output {
280        self.arr.get_mut(&index).unwrap()
281    }
282}
283
284impl<F: InterfaceBackend> PowerUps<F> {
285    /// Set whether a new game should start with powers or not (New Game+)
286    ///
287    /// # Errors
288    ///
289    /// Will return a [`InterfaceError`] if the implementation is unable to access the game.
290    pub fn start_with_powers(&mut self, value: bool) -> InterfaceResult<()> {
291        self.initial_bubble_bowl.set(value)?;
292        self.initial_cruise_bubble.set(value)
293    }
294
295    /// Unlock the Bubble Bowl and Cruise Bubble for the current game.
296    ///
297    /// # Errors
298    ///
299    /// Will return a [`InterfaceError`] if the implementation is unable to access the game.
300    pub fn unlock_powers(&mut self) -> InterfaceResult<()> {
301        self.bubble_bowl.set(true)?;
302        self.cruise_bubble.set(true)
303    }
304}
305
306impl<F: InterfaceBackend> Hans<F> {
307    /// Whether or not Hans is currently enabled.
308    ///
309    /// # Errors
310    ///
311    /// Will return a [`InterfaceError`] if the implementation is unable to access the game.
312    pub fn is_enabled(&mut self) -> InterfaceResult<bool> {
313        Ok(self.flags.get()? & 4 == 0)
314    }
315
316    /// Sets Hans' enabled status to `value`
317    ///
318    /// # Errors
319    ///
320    /// Will return a [`InterfaceError`] if the implementation is unable to access the game.
321    pub fn set_enabled(&mut self, value: bool) -> InterfaceResult<()> {
322        let new = match value {
323            true => self.flags.get()? & !4,
324            false => self.flags.get()? | 4,
325        };
326        self.flags.set(new)
327    }
328
329    /// Toggles whether Hans is enabled or not
330    ///
331    /// If successful, returns an `Ok(bool)` specifying what the new state of hans is.
332    ///
333    /// # Errors
334    ///
335    /// Will return a [`InterfaceError`] if the implementation is unable to access the game.
336    pub fn toggle_enabled(&mut self) -> InterfaceResult<bool> {
337        let new = self.flags.get()? ^ 4;
338        self.flags.set(new)?;
339        Ok(new & 4 == 0)
340    }
341}
342
343/// A type that is capable of providing a [`GameInterface`], generic over some backend.
344///
345/// # Examples
346/// ```
347/// use bfbb::game_interface::mock::MockInterface;
348/// use bfbb::game_interface::InterfaceProvider;
349/// use bfbb::game_interface::game_var::GameVar;
350/// use bfbb::game_interface::InterfaceResult;
351///
352/// fn main() -> InterfaceResult<()> {
353///     let mut provider = MockInterface::default();
354///     provider.do_with_interface(|interface| {
355///         let count = interface.spatula_count.get()?;
356///         println!("You have {count} spatulas");
357///         Ok(())
358///     })
359/// }
360/// ```
361pub trait InterfaceProvider: Default {
362    /// Backend implementation for this provider.
363    type Backend: InterfaceBackend;
364    /// Interface with the backend.
365    ///
366    /// This function will first attempt to hook the backend if necessary. If the hooking process is sucessful then the provided function
367    /// will be called with a reference to the [`GameInterface`]. If that function returns a [`InterfaceError::Unhooked`] error then
368    /// the [`InterfaceProvider`] will transition back to an unhooked state.
369    ///
370    /// # Errors
371    ///
372    /// If a hooking attempt is made and fails then an [`InterfaceError::Unhooked`] will be returned. Otherwise the result of the
373    /// provided function will be returned as-is.
374    fn do_with_interface<T>(
375        &mut self,
376        fun: impl FnOnce(&mut GameInterface<Self::Backend>) -> InterfaceResult<T>,
377    ) -> InterfaceResult<T>;
378
379    /// Check if this interface is currently available
380    ///
381    /// For most interfaces, this is the same thing as being "hooked".
382    ///
383    /// *NOTE*: A currently available interface may become unavaiable in the future and vice versa.
384    /// For example: The user closes Dolphin, making it unavailable, but then opens it again later.
385    fn is_available(&mut self) -> bool;
386}
387
388/// Result type for [`GameInterface`] actions.
389pub type InterfaceResult<T> = std::result::Result<T, InterfaceError>;
390
391/// Error type for failed [`GameInterface`] actions.
392///
393/// This list is non-exhaustive and may grow over time.
394#[derive(Debug, Error)]
395#[non_exhaustive]
396pub enum InterfaceError {
397    /// Error for when interface is still properly hooked, but the requested operation can not be completed at this time
398    /// (e.g. the game is loading and some heap data isn't available yet.)
399    #[error("Data temporarily unavailable")]
400    DataUnavailable,
401    /// Error for when a previously hooked interface has become unhooked.
402    #[error("Interface became unhooked")]
403    Unhooked,
404    /// Error for when an interface's target emulator is not running.
405    #[error("Target emulator process could not be found")]
406    ProcessNotFound,
407    /// Error for when an interface's target emulator is found, but it is not currently running a game.
408    #[error("Target emulator is found but no emulation is started")]
409    EmulationNotRunning,
410    /// Error for when an emulated game is found, but it is not BfBB
411    #[error("A game other than SpongeBob SquarePants: Battle for Bikini Bottom is running.")]
412    IncorrectGame,
413    /// Error for when I/O with the interface fails.
414    #[error("Unexpected I/O error")]
415    Io(std::io::Error),
416}
417
418impl From<std::io::Error> for InterfaceError {
419    fn from(e: std::io::Error) -> Self {
420        // For now, treat any error other than InvalidData as being unhooked
421        if e.kind() == std::io::ErrorKind::InvalidData {
422            return Self::Io(e);
423        }
424        Self::Unhooked
425    }
426}