bfbb 0.3.0

Library for interacting with SpongeBob SquarePants: Battle for Bikini Bottom
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
//! Allows performing actions on or reading information about a running instance of BfBB.

use std::{
    collections::HashMap,
    ops::{Index, IndexMut},
};

use thiserror::Error;

use crate::{
    game_state::{GameMode, GameOstrich, GameState},
    Level, Spatula,
};

use self::game_var::{GameVar, GameVarMut, InterfaceBackend};

pub mod dolphin;
pub mod game_var;
pub mod mock;

/// Interact with BfBB in an abstract way.
///
//// This struct allows accessing variables existing with a running instance of *Battle for Bikini Bottom*
/// and performing actions on that instance in a way that is generic over a backend (e.g Dolphin and Xemu)
///
/// NOTE: To a see a list of supported backends see the `Implementors` list of [`InterfaceBackend`]
///
/// This is the key struct of the [`game_interface`](self) module and enables interacting with the game.
#[non_exhaustive]
pub struct GameInterface<F: InterfaceBackend> {
    /// True while on loading screens
    pub is_loading: F::Var<bool>,
    /// Location of the [`GameState`] enum.
    pub game_state: F::Mut<GameState>,
    /// Location of the [`GameState`] enum.
    pub game_mode: F::Mut<GameMode>,
    /// Location of the [`GameOstrich`] enum.
    ///
    /// Not recommended to mutate this, but the option is available if you wish, it's probably not what you want to do.
    pub game_ostrich: F::Mut<GameOstrich>,
    /// [`Hans`]
    pub hans: Hans<F>,
    /// [`PowerUps`]
    pub powers: PowerUps<F>,
    /// Location of the ID for the current scene. Can be converted to a [`Level`](crate::Level) via [`TryFrom`].
    ///
    /// # Examples
    /// ```
    /// use std::error::Error;
    ///
    /// use bfbb::game_interface::GameInterface;
    /// use bfbb::game_interface::game_var::{GameVar, InterfaceBackend};
    /// use bfbb::Level;
    ///
    /// fn get_current_level<V: InterfaceBackend>(
    ///     interface: &mut GameInterface<V>,
    /// ) -> Result<Level, Box<dyn Error>> {
    ///     Ok(interface.scene_id.get()?.try_into()?)
    /// }
    /// ```
    pub scene_id: F::Var<[u8; 4]>,
    /// Location of the spatula counter
    pub spatula_count: F::Mut<u32>,
    /// [`Tasks`]
    pub tasks: Tasks<F>,

    // TODO: This value is on the heap, it shouldn't be global like this
    lab_door_cost: F::Mut<u32>,
}

/// A collection of [`Task`]s. Can be indexed by [`Spatula`]
///
/// # Examples
///
/// ```
/// use bfbb::game_interface::game_var::{GameVar, GameVarMut, InterfaceBackend};
/// use bfbb::game_interface::{InterfaceResult, Tasks};
/// use bfbb::Spatula;
///
/// fn unlock_pinapple<V: InterfaceBackend>(tasks: &mut Tasks<V>) -> InterfaceResult<()> {
///     tasks[Spatula::OnTopOfThePineapple].menu_count.set(1)
/// }
/// ```
pub struct Tasks<F: InterfaceBackend> {
    pub(crate) arr: HashMap<Spatula, Task<F>>,
}

/// Contains [`GameVar`]s for a [`Spatula`]'s pause-menu counter and game object state.
#[non_exhaustive]
pub struct Task<F: InterfaceBackend> {
    /// The `count` field of this task's `_xCounter` struct.
    ///
    /// Notable values are:
    /// - `0` => Task is "locked", will be a question mark in the menu.
    /// - `1` => Task is "incomplete", will be a silver spatula in the menu.
    /// - `2` => Task is "complete", will be a golden spatula in the menu.
    /// - `3` => Task is also silver in the menu, this appears to only be used by [`Spatula::InfestationAtTheKrustyKrab`],
    ///          which uses this value for after clearing the robots, but before you've collected the spatula.
    /// - `_` => No icon will appear for this task in the menu, just an empty bubble. You can not warp to it and
    ///          attempting to will put the menu into an invalid state until a different unlocked task is selected.
    pub menu_count: F::Mut<i16>,
    /// A bitfield of flags for a spatula entity. The first bit determines if the entity is enabled or not.
    pub flags: Option<F::Mut<u8>>,
    /// Another bitfield for a spatula entity.
    pub state: Option<F::Mut<u32>>,
}
/// [`GameVar`]s related to the bubble bowl and cruise-missile
#[non_exhaustive]
pub struct PowerUps<F: InterfaceBackend> {
    /// Whether the bubble bowl is currently unlocked
    pub bubble_bowl: F::Mut<bool>,
    /// Whether the cruise bubble is currently unlocked
    pub cruise_bubble: F::Mut<bool>,
    /// Whether new games should begin with the bubble bowl unlocked.
    pub initial_bubble_bowl: F::Mut<bool>,
    /// Whether new games should begin with the cruise missile
    pub initial_cruise_bubble: F::Mut<bool>,
}

/// Implements methods for interacting with Hans' state.
#[non_exhaustive]
pub struct Hans<F: InterfaceBackend> {
    flags: F::Mut<u8>,
}

impl<F: InterfaceBackend> GameInterface<F> {
    /// Will start a new game when called. Only works when the player is on the main menu and not in the demo cutscene.
    ///
    /// # Errors
    ///
    /// Will return an [`InterfaceError`] if the implementation is unable to access the game.
    pub fn start_new_game(&mut self) -> InterfaceResult<()> {
        self.game_mode.set(GameMode::Game)
    }

    /// Get the level that the player is currently in
    ///
    /// # Errors
    ///
    /// Will return an [`InterfaceError::DataUnavailable`] sometimes when the game is loading due to the scene pointer being null
    pub fn get_current_level(&self) -> InterfaceResult<Level> {
        // TODO: It would be great to eventually allow self.scene_id to be of type `F::Var<Level>` directly and be able
        //       to blanket impl `GameVar<T=Level>` for all backends by using an actual `GameVar<T=[u8;4]>` internally.
        self.scene_id
            .get()?
            .try_into()
            .map_err(|_| InterfaceError::DataUnavailable)
    }

    /// Marks a task as available (Silver). This will not update an already unlocked task.
    ///
    /// # Errors
    ///
    /// Will return an [`InterfaceError`] if the implementation is unable to access the game.
    pub fn unlock_task(&mut self, spatula: Spatula) -> InterfaceResult<()> {
        let task = &mut self.tasks[spatula];
        let curr = task.menu_count.get()?;
        if curr == 0 {
            task.menu_count.set(1)?;
        }
        Ok(())
    }

    /// Marks a spatula as "completed" in the pause menu. This has the effect of giving the player access to the task warp.
    ///
    /// # Errors
    ///
    /// Will return an [`InterfaceError`] if the implementation is unable to access the game.
    pub fn mark_task_complete(&mut self, spatula: Spatula) -> InterfaceResult<()> {
        self.tasks[spatula].menu_count.set(2)
    }

    /// True when `spatula` is shown as gold in the pause menu.
    ///
    /// # Errors
    ///
    /// Will return an [`InterfaceError`] if the implementation is unable to access the game.
    pub fn is_task_complete(&self, spatula: Spatula) -> InterfaceResult<bool> {
        Ok(self.tasks[spatula].menu_count.get()? == 2)
    }

    /// Collect a spatula in the world. This only removes the entity, it will not complete the task or increment the spatula
    /// counter.
    ///
    /// *NOTE*: Will always return `Ok(false)` when `spatula` is located in a level other than `current_level` or when
    /// `spatula` is [Kah-Rah-Tae](Spatula::KahRahTae) or [The Small Shall Rule... Or Not](Spatula::TheSmallShallRuleOrNot)
    /// without writing memory.
    ///
    /// # Errors
    ///
    /// Will return an [`InterfaceError`] if the implementation is unable to access the game.
    pub fn collect_spatula(
        &mut self,
        spatula: Spatula,
        current_level: Option<Level>,
    ) -> InterfaceResult<()> {
        if current_level != Some(spatula.get_level()) {
            return Ok(());
        }

        let task = &mut self.tasks[spatula];
        let (flags, state) = match (&mut task.flags, &mut task.state) {
            (Some(flags), Some(state)) => (flags, state),
            _ => return Ok(()),
        };

        let mut new_flags = flags.get()?;
        new_flags &= !1; // Disable the entity

        // Set some model flags
        let mut new_state = state.get()?;
        new_state |= 8;
        new_state &= !4;
        new_state &= !2;

        flags.set(new_flags)?;
        state.set(new_state)?;
        Ok(())
    }

    /// True when `spatula`'s collected animation is playing
    ///
    /// *NOTE*: Will always return `Ok(false)` when `spatula` is located in a level other than `current_level` or when
    /// `spatula` is [Kah-Rah-Tae](Spatula::KahRahTae) or [The Small Shall Rule... Or Not](Spatula::TheSmallShallRuleOrNot)
    ///
    /// # Errors
    ///
    /// Will return an [`InterfaceError`] if the implementation is unable to access the game.
    pub fn is_spatula_being_collected(
        &self,
        spatula: Spatula,
        current_level: Option<Level>,
    ) -> InterfaceResult<bool> {
        if current_level != Some(spatula.get_level()) {
            return Ok(false);
        }

        let state = match &self.tasks[spatula].state {
            Some(x) => x,
            None => return Ok(false),
        };

        Ok(state.get()? & 4 != 0)
    }

    /// Changes the number of spatulas required to enter the Chum Bucket Lab.
    ///
    /// *NOTE*: This function requires that the current level is the Chum Bucket an will therefore always return `Ok(())`
    /// if `current_level` is not `Some(Level::ChumBucket)`
    ///
    /// # Errors
    ///
    /// Will return an [`InterfaceError`] if the implementation is unable to access the game.
    pub fn set_lab_door(
        &mut self,
        value: u32,
        current_level: Option<Level>,
    ) -> InterfaceResult<()> {
        if current_level != Some(Level::ChumBucket) {
            return Ok(());
        }

        // The game uses a greater than check so we need to subtract by one
        let cost = value - 1;
        self.lab_door_cost.set(cost)?;
        Ok(())
    }
}

impl<V: InterfaceBackend> Index<Spatula> for Tasks<V> {
    type Output = Task<V>;

    fn index(&self, index: Spatula) -> &Self::Output {
        &self.arr[&index]
    }
}

impl<T: InterfaceBackend> IndexMut<Spatula> for Tasks<T> {
    fn index_mut(&mut self, index: Spatula) -> &mut Self::Output {
        self.arr.get_mut(&index).unwrap()
    }
}

impl<F: InterfaceBackend> PowerUps<F> {
    /// Set whether a new game should start with powers or not (New Game+)
    ///
    /// # Errors
    ///
    /// Will return a [`InterfaceError`] if the implementation is unable to access the game.
    pub fn start_with_powers(&mut self, value: bool) -> InterfaceResult<()> {
        self.initial_bubble_bowl.set(value)?;
        self.initial_cruise_bubble.set(value)
    }

    /// Unlock the Bubble Bowl and Cruise Bubble for the current game.
    ///
    /// # Errors
    ///
    /// Will return a [`InterfaceError`] if the implementation is unable to access the game.
    pub fn unlock_powers(&mut self) -> InterfaceResult<()> {
        self.bubble_bowl.set(true)?;
        self.cruise_bubble.set(true)
    }
}

impl<F: InterfaceBackend> Hans<F> {
    /// Whether or not Hans is currently enabled.
    ///
    /// # Errors
    ///
    /// Will return a [`InterfaceError`] if the implementation is unable to access the game.
    pub fn is_enabled(&mut self) -> InterfaceResult<bool> {
        Ok(self.flags.get()? & 4 == 0)
    }

    /// Sets Hans' enabled status to `value`
    ///
    /// # Errors
    ///
    /// Will return a [`InterfaceError`] if the implementation is unable to access the game.
    pub fn set_enabled(&mut self, value: bool) -> InterfaceResult<()> {
        let new = match value {
            true => self.flags.get()? & !4,
            false => self.flags.get()? | 4,
        };
        self.flags.set(new)
    }

    /// Toggles whether Hans is enabled or not
    ///
    /// If successful, returns an `Ok(bool)` specifying what the new state of hans is.
    ///
    /// # Errors
    ///
    /// Will return a [`InterfaceError`] if the implementation is unable to access the game.
    pub fn toggle_enabled(&mut self) -> InterfaceResult<bool> {
        let new = self.flags.get()? ^ 4;
        self.flags.set(new)?;
        Ok(new & 4 == 0)
    }
}

/// A type that is capable of providing a [`GameInterface`], generic over some backend.
///
/// # Examples
/// ```
/// use bfbb::game_interface::mock::MockInterface;
/// use bfbb::game_interface::InterfaceProvider;
/// use bfbb::game_interface::game_var::GameVar;
/// use bfbb::game_interface::InterfaceResult;
///
/// fn main() -> InterfaceResult<()> {
///     let mut provider = MockInterface::default();
///     provider.do_with_interface(|interface| {
///         let count = interface.spatula_count.get()?;
///         println!("You have {count} spatulas");
///         Ok(())
///     })
/// }
/// ```
pub trait InterfaceProvider: Default {
    /// Backend implementation for this provider.
    type Backend: InterfaceBackend;
    /// Interface with the backend.
    ///
    /// This function will first attempt to hook the backend if necessary. If the hooking process is sucessful then the provided function
    /// will be called with a reference to the [`GameInterface`]. If that function returns a [`InterfaceError::Unhooked`] error then
    /// the [`InterfaceProvider`] will transition back to an unhooked state.
    ///
    /// # Errors
    ///
    /// If a hooking attempt is made and fails then an [`InterfaceError::Unhooked`] will be returned. Otherwise the result of the
    /// provided function will be returned as-is.
    fn do_with_interface<T>(
        &mut self,
        fun: impl FnOnce(&mut GameInterface<Self::Backend>) -> InterfaceResult<T>,
    ) -> InterfaceResult<T>;

    /// Check if this interface is currently available
    ///
    /// For most interfaces, this is the same thing as being "hooked".
    ///
    /// *NOTE*: A currently available interface may become unavaiable in the future and vice versa.
    /// For example: The user closes Dolphin, making it unavailable, but then opens it again later.
    fn is_available(&mut self) -> bool;
}

/// Result type for [`GameInterface`] actions.
pub type InterfaceResult<T> = std::result::Result<T, InterfaceError>;

/// Error type for failed [`GameInterface`] actions.
///
/// This list is non-exhaustive and may grow over time.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum InterfaceError {
    /// Error for when interface is still properly hooked, but the requested operation can not be completed at this time
    /// (e.g. the game is loading and some heap data isn't available yet.)
    #[error("Data temporarily unavailable")]
    DataUnavailable,
    /// Error for when a previously hooked interface has become unhooked.
    #[error("Interface became unhooked")]
    Unhooked,
    /// Error for when an interface's target emulator is not running.
    #[error("Target emulator process could not be found")]
    ProcessNotFound,
    /// Error for when an interface's target emulator is found, but it is not currently running a game.
    #[error("Target emulator is found but no emulation is started")]
    EmulationNotRunning,
    /// Error for when an emulated game is found, but it is not BfBB
    #[error("A game other than SpongeBob SquarePants: Battle for Bikini Bottom is running.")]
    IncorrectGame,
    /// Error for when I/O with the interface fails.
    #[error("Unexpected I/O error")]
    Io(std::io::Error),
}

impl From<std::io::Error> for InterfaceError {
    fn from(e: std::io::Error) -> Self {
        // For now, treat any error other than InvalidData as being unhooked
        if e.kind() == std::io::ErrorKind::InvalidData {
            return Self::Io(e);
        }
        Self::Unhooked
    }
}