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}