├── .gitignore ├── libsm64-sys ├── .gitignore ├── lib.rs ├── Cargo.toml └── build.rs ├── .gitmodules ├── Cargo.toml ├── LICENSE ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | *.z64 4 | -------------------------------------------------------------------------------- /libsm64-sys/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libsm64-sys/libsm64"] 2 | path = libsm64-sys/libsm64 3 | url = https://github.com/libsm64/libsm64.git 4 | -------------------------------------------------------------------------------- /libsm64-sys/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | 5 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libsm64" 3 | version = "0.3.0" 4 | edition = "2024" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | libsm64-sys = {path = "./libsm64-sys" } 10 | sha = "1.0.3" 11 | once_cell = "1.21.3" 12 | -------------------------------------------------------------------------------- /libsm64-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libsm64-sys" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | path = "lib.rs" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [build-dependencies] 12 | cc = "1.2" 13 | bindgen="0.72" 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nick Massey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libsm64 - Rust Bindings 2 | 3 | [Documentation](https://nickmass.com/doc/libsm64/index.html) 4 | 5 | This is a thin layer of rust bindings over the very excellent [libsm64 project](https://github.com/libsm64/libsm64). 6 | 7 | ## Usage 8 | 9 | ```rust 10 | use std::fs::File; 11 | use libsm64::*; 12 | 13 | const ROM_PATH: &str = "./baserom.us.z64"; 14 | let rom = File::open(ROM_PATH).unwrap(); 15 | 16 | let mut sm64 = Sm64::new(rom).unwrap(); 17 | 18 | // Convert your existing level geometry into LevelTriangles 19 | let level_collision_geometry = create_level_geometry(); 20 | 21 | // Load the geometry into sm64 to be used for collision detection 22 | sm64.load_level_geometry(&level_collision_geometry); 23 | 24 | // Create a new Mario and provide his starting position 25 | let mut mario = sm64.create_mario(0.0, 0.0, 0.0).unwrap(); 26 | 27 | let input = MarioInput { 28 | stick_x: 0.5, 29 | button_a: true, 30 | ..MarioInput::default() 31 | }; 32 | 33 | // For each iteration of your gameloop, tick Mario's state 34 | let state = mario.tick(input); 35 | 36 | println!("Mario's current health: {}", state.health); 37 | 38 | // Mario's geometry will be updated to his new position and animation 39 | for triangle in mario.geometry().triangles() { 40 | draw_triangle(&triangle, sm64.texture()); 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /libsm64-sys/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | use std::process::Command; 4 | 5 | const C_FILES: &[&str] = &[ 6 | "libsm64/src/debug_print.c", 7 | "libsm64/src/decomp/audio/copt/seq_channel_layer_process_script_copt.inc.c", 8 | "libsm64/src/decomp/audio/data.c", 9 | "libsm64/src/decomp/audio/effects.c", 10 | "libsm64/src/decomp/audio/external.c", 11 | "libsm64/src/decomp/audio/globals_start.c", 12 | "libsm64/src/decomp/audio/heap.c", 13 | "libsm64/src/decomp/audio/load.c", 14 | "libsm64/src/decomp/audio/load_dat.c", 15 | "libsm64/src/decomp/audio/load_sh.c", 16 | "libsm64/src/decomp/audio/playback.c", 17 | "libsm64/src/decomp/audio/port_eu.c", 18 | "libsm64/src/decomp/audio/port_sh.c", 19 | "libsm64/src/decomp/audio/seqplayer.c", 20 | "libsm64/src/decomp/audio/shindou_debug_prints.c", 21 | "libsm64/src/decomp/audio/synthesis.c", 22 | "libsm64/src/decomp/audio/synthesis_sh.c", 23 | "libsm64/src/decomp/engine/geo_layout.c", 24 | "libsm64/src/decomp/engine/graph_node.c", 25 | "libsm64/src/decomp/engine/graph_node_manager.c", 26 | "libsm64/src/decomp/engine/guMtxF2L.c", 27 | "libsm64/src/decomp/engine/math_util.c", 28 | "libsm64/src/decomp/engine/surface_collision.c", 29 | "libsm64/src/decomp/game/behavior_actions.c", 30 | "libsm64/src/decomp/game/interaction.c", 31 | "libsm64/src/decomp/game/mario_actions_airborne.c", 32 | "libsm64/src/decomp/game/mario_actions_automatic.c", 33 | "libsm64/src/decomp/game/mario_actions_cutscene.c", 34 | "libsm64/src/decomp/game/mario_actions_moving.c", 35 | "libsm64/src/decomp/game/mario_actions_object.c", 36 | "libsm64/src/decomp/game/mario_actions_stationary.c", 37 | "libsm64/src/decomp/game/mario_actions_submerged.c", 38 | "libsm64/src/decomp/game/mario.c", 39 | "libsm64/src/decomp/game/mario_misc.c", 40 | "libsm64/src/decomp/game/mario_step.c", 41 | "libsm64/src/decomp/game/object_stuff.c", 42 | "libsm64/src/decomp/game/platform_displacement.c", 43 | "libsm64/src/decomp/game/rendering_graph_node.c", 44 | "libsm64/src/decomp/game/sound_init.c", 45 | "libsm64/src/decomp/global_state.c", 46 | "libsm64/src/decomp/mario/geo.inc.c", 47 | "libsm64/src/decomp/mario/model.inc.c", 48 | "libsm64/src/decomp/memory.c", 49 | "libsm64/src/decomp/pc/alBnkfNew.c", 50 | "libsm64/src/decomp/pc/mixer.c", 51 | "libsm64/src/decomp/pc/ultra_reimplementation.c", 52 | "libsm64/src/decomp/tools/convUtils.c", 53 | "libsm64/src/decomp/tools/libmio0.c", 54 | "libsm64/src/decomp/tools/n64graphics.c", 55 | "libsm64/src/decomp/tools/utils.c", 56 | "libsm64/src/fake_interaction.c", 57 | "libsm64/src/gfx_adapter.c", 58 | "libsm64/src/libsm64.c", 59 | "libsm64/src/load_anim_data.c", 60 | "libsm64/src/load_audio_data.c", 61 | "libsm64/src/load_surfaces.c", 62 | "libsm64/src/load_tex_data.c", 63 | "libsm64/src/obj_pool.c", 64 | "libsm64/src/play_sound.c", 65 | ]; 66 | 67 | const MARIO_GEO: &str = "libsm64/src/decomp/mario/geo.inc.c"; 68 | 69 | fn main() { 70 | if !PathBuf::from(MARIO_GEO).exists() { 71 | Command::new("python3") 72 | .arg("import-mario-geo.py") 73 | .current_dir("libsm64") 74 | .output() 75 | .expect("Unable to download mario geometry"); 76 | } 77 | 78 | cc::Build::new() 79 | .flags(["-fno-strict-aliasing", "-fPIC", "-fvisibility=hidden"]) 80 | .warnings(false) 81 | .define("GBI_FLOATS", None) 82 | .define("VERSION_US", None) 83 | .define("NO_SEGMENTED_MEMORY", None) 84 | .files(C_FILES) 85 | .include("libsm64/src/decomp/include") 86 | .compile("sm64"); 87 | 88 | let bindings = bindgen::Builder::default() 89 | .header("libsm64/src/libsm64.h") 90 | .generate() 91 | .expect("Unable to generate libsm64 bindings"); 92 | 93 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 94 | bindings 95 | .write_to_file(out_path.join("bindings.rs")) 96 | .expect("Could not write C bindings"); 97 | } 98 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This crate provides bindings and a rust friendly wrapper around the C API of [libsm64](https://github.com/libsm64/libsm64). 3 | libsm64 extracts the logic for the movement and control of Mario from the Super Mario 4 | 64 ROM providing a interface to implement your own Mario in your own 3D engine. 5 | 6 | **Note:** You will be required to provide your own copy of a Super Mario 64 (USA) ROM, 7 | the correct ROM has a SHA1 hash of '9bef1128717f958171a4afac3ed78ee2bb4e86ce'. 8 | 9 | # Usage: 10 | 11 | ```rust 12 | use std::fs::File; 13 | use libsm64::*; 14 | 15 | const ROM_PATH: &str = "./baserom.us.z64"; 16 | let rom = File::open(ROM_PATH).unwrap(); 17 | 18 | let mut sm64 = Sm64::new(rom).unwrap(); 19 | 20 | // Convert your existing level geometry into LevelTriangles 21 | let level_collision_geometry = create_level_geometry(); 22 | 23 | // Load the geometry into sm64 to be used for collision detection 24 | sm64.load_level_geometry(&level_collision_geometry); 25 | 26 | // Create a new Mario and provide his starting position 27 | let mut mario = sm64.create_mario(0.0, 0.0, 0.0).unwrap(); 28 | 29 | let input = MarioInput { 30 | stick_x: 0.5, 31 | button_a: true, 32 | ..MarioInput::default() 33 | }; 34 | 35 | // For each iteration of your gameloop, tick Mario's state 36 | let state = mario.tick(input); 37 | 38 | println!("Mario's current health: {}", state.health); 39 | 40 | // Mario's geometry will be updated to his new position and animation 41 | for triangle in mario.geometry().triangles() { 42 | draw_triangle(&triangle, sm64.texture()); 43 | } 44 | 45 | # fn draw_triangle(_triangle: &(MarioVertex, MarioVertex, MarioVertex), _texture: Texture) {} 46 | # fn create_level_geometry() -> Vec { 47 | # let tri = LevelTriangle { 48 | # kind: Surface::Default, 49 | # force: 0, 50 | # terrain: Terrain::Grass, 51 | # vertices: (Point3{x: 10, y: -1, z: 10}, Point3{x: 10, y: -1, z: -10}, Point3{x: -10, y: -1, z: -10}), 52 | # }; 53 | # let mut level = Vec::new(); 54 | # level.push(tri); 55 | # level 56 | # } 57 | ``` 58 | */ 59 | 60 | use std::io::{BufReader, Read}; 61 | 62 | use once_cell::sync::OnceCell; 63 | use sha::sha1; 64 | use sha::utils::{Digest, DigestExt}; 65 | 66 | const VALID_HASH: &str = "9bef1128717f958171a4afac3ed78ee2bb4e86ce"; 67 | 68 | static SM64: once_cell::sync::OnceCell = OnceCell::new(); 69 | 70 | /// An error that can occur 71 | #[derive(Debug)] 72 | pub enum Error { 73 | /// An IO error 74 | Io(std::io::Error), 75 | /// When creating Mario he must be positioned above a surface 76 | InvalidMarioPosition, 77 | /// The rom proivided must be Super Mario 64 (USA), with a SHA1 hash of '9bef1128717f958171a4afac3ed78ee2bb4e86ce' 78 | InvalidRom(String), 79 | } 80 | 81 | impl std::fmt::Display for Error { 82 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 83 | match self { 84 | Error::Io(err) => write!(f, "{}", err), 85 | Error::InvalidMarioPosition => write!( 86 | f, 87 | "Invalid Mario position, ensure coordinates are above ground" 88 | ), 89 | Error::InvalidRom(hash) => write!( 90 | f, 91 | "Invalid Super Mario 64 rom: found hash '{}', expected hash '{}'", 92 | hash, VALID_HASH 93 | ), 94 | } 95 | } 96 | } 97 | 98 | impl From for Error { 99 | fn from(err: std::io::Error) -> Self { 100 | Error::Io(err) 101 | } 102 | } 103 | 104 | struct Sm64Inner { 105 | texture_data: Vec, 106 | #[allow(dead_code)] 107 | rom_data: Vec, 108 | } 109 | 110 | /// The core interface to libsm64 111 | pub struct Sm64; 112 | 113 | impl Sm64 { 114 | /// Create a new instance of Sm64, requires a Super Mario 64 rom to extra Mario's texture and animation data from 115 | pub fn new(rom: R) -> Result { 116 | let mut rom_file = BufReader::new(rom); 117 | let mut rom_data = Vec::new(); 118 | rom_file.read_to_end(&mut rom_data)?; 119 | 120 | let rom_hash = sha1::Sha1::default().digest(&*rom_data).to_hex(); 121 | 122 | if rom_hash != VALID_HASH { 123 | return Err(Error::InvalidRom(rom_hash)); 124 | } 125 | 126 | let _sm64 = SM64.get_or_init(|| { 127 | let mut texture_data = vec![ 128 | 0; 129 | (libsm64_sys::SM64_TEXTURE_WIDTH * libsm64_sys::SM64_TEXTURE_HEIGHT) 130 | as usize 131 | * 4 132 | ]; 133 | 134 | unsafe { 135 | libsm64_sys::sm64_global_init(rom_data.as_mut_ptr(), texture_data.as_mut_ptr()); 136 | } 137 | 138 | Sm64Inner { 139 | texture_data, 140 | rom_data, 141 | } 142 | }); 143 | 144 | Ok(Self) 145 | } 146 | 147 | /// A texture atlas that can be applied to the Mario geometry 148 | pub fn texture(&self) -> Texture { 149 | let texture_data = &SM64 150 | .get() 151 | .expect("Sm64::new() must of been called") 152 | .texture_data; 153 | 154 | Texture { 155 | data: texture_data.as_slice(), 156 | width: libsm64_sys::SM64_TEXTURE_WIDTH, 157 | height: libsm64_sys::SM64_TEXTURE_HEIGHT, 158 | } 159 | } 160 | 161 | /// Create a new instancec of Mario that spawns at the point indicated by x/y/z, he must be placed above a surface or an error will be returned 162 | pub fn create_mario(&mut self, x: f32, y: f32, z: f32) -> Result { 163 | let mario_id = unsafe { libsm64_sys::sm64_mario_create(x, y, z) }; 164 | 165 | if mario_id < 0 { 166 | Err(Error::InvalidMarioPosition) 167 | } else { 168 | Ok(Mario::new(mario_id)) 169 | } 170 | } 171 | 172 | /// Create a dynamic surface that can have its position and rotation updated at runtime, good for moving platforms 173 | pub fn create_dynamic_surface( 174 | &mut self, 175 | geometry: &[LevelTriangle], 176 | transform: SurfaceTransform, 177 | ) -> DynamicSurface { 178 | let id = unsafe { 179 | let surface_object = libsm64_sys::SM64SurfaceObject { 180 | transform: transform.into(), 181 | surfaceCount: geometry.len() as u32, 182 | surfaces: geometry.as_ptr() as *mut _, 183 | }; 184 | libsm64_sys::sm64_surface_object_create(&surface_object as *const _) 185 | }; 186 | 187 | DynamicSurface::new(id) 188 | } 189 | 190 | /// Load the static level geometry, used for collision detection 191 | pub fn load_level_geometry(&mut self, geometry: &[LevelTriangle]) { 192 | unsafe { 193 | libsm64_sys::sm64_static_surfaces_load( 194 | geometry.as_ptr() as *const _, 195 | geometry.len() as u32, 196 | ) 197 | } 198 | } 199 | } 200 | 201 | /// A instance of Mario that can be controlled 202 | pub struct Mario { 203 | id: i32, 204 | geometry: MarioGeometry, 205 | } 206 | 207 | impl Mario { 208 | fn new(id: i32) -> Self { 209 | let geometry = MarioGeometry::new(); 210 | Self { id, geometry } 211 | } 212 | 213 | /// Advance the Mario simulation ahead by 1 frame, should be called 30 times per second 214 | pub fn tick(&mut self, input: MarioInput) -> MarioState { 215 | let input = input.into(); 216 | let mut state = libsm64_sys::SM64MarioState { 217 | position: [0.0, 0.0, 0.0], 218 | velocity: [0.0, 0.0, 0.0], 219 | faceAngle: 0.0, 220 | health: 0, 221 | forwardVelocity: 0.0, 222 | action: 0, 223 | animID: 0, 224 | animFrame: 0, 225 | flags: 0, 226 | particleFlags: 0, 227 | invincTimer: 0, 228 | }; 229 | 230 | let tris = unsafe { 231 | let mut geometry: libsm64_sys::SM64MarioGeometryBuffers = (&mut self.geometry).into(); 232 | libsm64_sys::sm64_mario_tick( 233 | self.id, 234 | &input as *const _, 235 | &mut state as *mut _, 236 | &mut geometry as *mut _, 237 | ); 238 | geometry.numTrianglesUsed 239 | }; 240 | 241 | self.geometry.num_triangles = tris as usize; 242 | 243 | state.into() 244 | } 245 | 246 | /// Mario's geometry as of the current tick 247 | pub fn geometry(&self) -> &MarioGeometry { 248 | &self.geometry 249 | } 250 | } 251 | 252 | impl Drop for Mario { 253 | fn drop(&mut self) { 254 | unsafe { libsm64_sys::sm64_mario_delete(self.id) } 255 | } 256 | } 257 | 258 | /// A dynamic surface that can have its position and rotation updated at runtime, good for moving platforms 259 | pub struct DynamicSurface { 260 | id: u32, 261 | } 262 | 263 | impl DynamicSurface { 264 | fn new(id: u32) -> Self { 265 | Self { id } 266 | } 267 | 268 | /// Reposition or rotate the surface 269 | pub fn transform(&mut self, transform: SurfaceTransform) { 270 | unsafe { 271 | let transform = transform.into(); 272 | libsm64_sys::sm64_surface_object_move(self.id, &transform as *const _) 273 | } 274 | } 275 | } 276 | 277 | impl Drop for DynamicSurface { 278 | fn drop(&mut self) { 279 | unsafe { libsm64_sys::sm64_surface_object_delete(self.id) } 280 | } 281 | } 282 | 283 | /// Representions a transform that can be applied to a dynamic surface 284 | #[derive(Copy, Clone, Debug)] 285 | pub struct SurfaceTransform { 286 | /// The x/y/z coordinates of the surface 287 | pub position: Point3, 288 | /// The rotation of the surface on each axis, the units should be in degrees 289 | pub euler_rotation: Point3, 290 | } 291 | 292 | impl From for libsm64_sys::SM64ObjectTransform { 293 | fn from(transform: SurfaceTransform) -> Self { 294 | Self { 295 | position: [ 296 | transform.position.x, 297 | transform.position.y, 298 | transform.position.z, 299 | ], 300 | eulerRotation: [ 301 | transform.euler_rotation.x, 302 | transform.euler_rotation.y, 303 | transform.euler_rotation.z, 304 | ], 305 | } 306 | } 307 | } 308 | 309 | /// A texture atlas that can be applied to the Mario geometry 310 | pub struct Texture { 311 | /// 8-bit RGBA values 312 | pub data: &'static [u8], 313 | /// The width of the texture 314 | pub width: u32, 315 | /// The height of the texture 316 | pub height: u32, 317 | } 318 | 319 | /// A point in 3D space 320 | #[repr(C)] 321 | #[derive(Debug, Default, Copy, Clone)] 322 | pub struct Point3 323 | where 324 | T: Copy, 325 | { 326 | pub x: T, 327 | pub y: T, 328 | pub z: T, 329 | } 330 | 331 | /// A point in 2D space 332 | #[repr(C)] 333 | #[derive(Debug, Default, Copy, Clone)] 334 | pub struct Point2 335 | where 336 | T: Copy, 337 | { 338 | pub x: T, 339 | pub y: T, 340 | } 341 | 342 | /// A color 343 | #[repr(C)] 344 | #[derive(Debug, Default, Copy, Clone)] 345 | pub struct Color { 346 | pub r: f32, 347 | pub g: f32, 348 | pub b: f32, 349 | } 350 | 351 | /// A level triangle, the main building block of the collision geometry 352 | #[repr(C)] 353 | #[derive(Copy, Clone, Debug)] 354 | pub struct LevelTriangle { 355 | /// The type of surface 356 | pub kind: Surface, 357 | pub force: i16, 358 | /// The type of terrain 359 | pub terrain: Terrain, 360 | /// The verticies of the triangle. Super Mario 64 using integer math for its collision detection expect to have to scale your vertexes appropriately to use them for level geometry 361 | /// 362 | /// **Note:** The order of the verticies is important, Mario will only collide with the front face of the geometry 363 | pub vertices: (Point3, Point3, Point3), 364 | } 365 | 366 | /// The input for a frame of Mario's logic 367 | #[derive(Copy, Clone, Debug, Default)] 368 | pub struct MarioInput { 369 | /// The position of the camera on the x-axis, used to adjust the movement of mario based on his postion relative to the camera 370 | pub cam_look_x: f32, 371 | /// The position of the camera on the z-axis, used to adjust the movement of mario based on his postion relative to the camera 372 | pub cam_look_z: f32, 373 | /// The input of the analog control on the x-axis (-1.0..1.0) 374 | pub stick_x: f32, 375 | /// The input of the analog control on the y-axis (-1.0..1.0) 376 | pub stick_y: f32, 377 | /// Is the A button pressed 378 | pub button_a: bool, 379 | /// Is the B button pressed 380 | pub button_b: bool, 381 | /// Is the Z button pressed 382 | pub button_z: bool, 383 | } 384 | 385 | impl From for libsm64_sys::SM64MarioInputs { 386 | fn from(input: MarioInput) -> Self { 387 | libsm64_sys::SM64MarioInputs { 388 | camLookX: input.cam_look_x, 389 | camLookZ: input.cam_look_z, 390 | stickX: input.stick_x, 391 | stickY: input.stick_y, 392 | buttonA: input.button_a as u8, 393 | buttonB: input.button_b as u8, 394 | buttonZ: input.button_z as u8, 395 | } 396 | } 397 | } 398 | 399 | /// Mario's state after a tick of logic 400 | #[derive(Debug, Default, Copy, Clone)] 401 | pub struct MarioState { 402 | /// The position of Mario in 3D space 403 | pub position: Point3, 404 | /// The velocity of Mario on each axis 405 | pub velocity: Point3, 406 | /// The direction Mario is facing 407 | pub face_angle: f32, 408 | /// Mario's current health 409 | pub health: i16, 410 | } 411 | 412 | impl From for MarioState { 413 | fn from(state: libsm64_sys::SM64MarioState) -> Self { 414 | let position = Point3 { 415 | x: state.position[0], 416 | y: state.position[1], 417 | z: state.position[2], 418 | }; 419 | let velocity = Point3 { 420 | x: state.velocity[0], 421 | y: state.velocity[1], 422 | z: state.velocity[2], 423 | }; 424 | MarioState { 425 | position, 426 | velocity, 427 | face_angle: state.faceAngle, 428 | health: state.health, 429 | } 430 | } 431 | } 432 | 433 | /// Mario's geometry 434 | pub struct MarioGeometry { 435 | position: Vec>, 436 | normal: Vec>, 437 | color: Vec, 438 | uv: Vec>, 439 | num_triangles: usize, 440 | } 441 | 442 | impl MarioGeometry { 443 | fn new() -> Self { 444 | Self { 445 | position: vec![Point3::default(); libsm64_sys::SM64_GEO_MAX_TRIANGLES as usize * 3], 446 | normal: vec![Point3::default(); libsm64_sys::SM64_GEO_MAX_TRIANGLES as usize * 3], 447 | color: vec![Color::default(); libsm64_sys::SM64_GEO_MAX_TRIANGLES as usize * 3], 448 | uv: vec![Point2::default(); libsm64_sys::SM64_GEO_MAX_TRIANGLES as usize * 3], 449 | num_triangles: 0, 450 | } 451 | } 452 | 453 | /// The geometry represented as a series of vertices, every 3 verticies is a new triangle. Includes position, normal, color, and texture coordinates 454 | pub fn vertices(&self) -> impl Iterator + '_ { 455 | let positions = self.position.iter().copied(); 456 | let normals = self.normal.iter().copied(); 457 | let color = self.color.iter().copied(); 458 | let uv = self.uv.iter().copied(); 459 | 460 | positions 461 | .zip(normals) 462 | .zip(color) 463 | .zip(uv) 464 | .take(self.num_triangles * 3) 465 | .map(|(((position, normal), color), uv)| MarioVertex { 466 | position, 467 | normal, 468 | color, 469 | uv, 470 | }) 471 | } 472 | 473 | /// The geometry represented as a series of triangles. Includes position, normal, color, and texture coordinates 474 | pub fn triangles(&self) -> impl Iterator + '_ { 475 | let positions = self.position.chunks_exact(3); 476 | let normals = self.normal.chunks_exact(3); 477 | let color = self.color.chunks_exact(3); 478 | let uv = self.uv.chunks_exact(3); 479 | 480 | positions 481 | .zip(normals) 482 | .zip(color) 483 | .zip(uv) 484 | .take(self.num_triangles) 485 | .map(|(((positions, normals), colors), uvs)| { 486 | let a = MarioVertex { 487 | position: positions[0], 488 | normal: normals[0], 489 | color: colors[0], 490 | uv: uvs[0], 491 | }; 492 | let b = MarioVertex { 493 | position: positions[1], 494 | normal: normals[1], 495 | color: colors[1], 496 | uv: uvs[1], 497 | }; 498 | let c = MarioVertex { 499 | position: positions[2], 500 | normal: normals[2], 501 | color: colors[2], 502 | uv: uvs[2], 503 | }; 504 | (a, b, c) 505 | }) 506 | } 507 | 508 | /// The position elements of Mario's verticies 509 | pub fn positions(&self) -> &[Point3] { 510 | &self.position[0..self.num_triangles * 3] 511 | } 512 | 513 | /// The normal elements of Mario's verticies 514 | pub fn normals(&self) -> &[Point3] { 515 | &self.normal[0..self.num_triangles * 3] 516 | } 517 | 518 | /// The color elements of Mario's verticies 519 | pub fn colors(&self) -> &[Color] { 520 | &self.color[0..self.num_triangles * 3] 521 | } 522 | 523 | /// The texture coordinate elements of Mario's verticies 524 | pub fn uvs(&self) -> &[Point2] { 525 | &self.uv[0..self.num_triangles * 3] 526 | } 527 | } 528 | 529 | impl<'a> From<&'a mut MarioGeometry> for libsm64_sys::SM64MarioGeometryBuffers { 530 | fn from(geo: &'a mut MarioGeometry) -> libsm64_sys::SM64MarioGeometryBuffers { 531 | libsm64_sys::SM64MarioGeometryBuffers { 532 | position: geo.position.as_mut_ptr() as *mut _, 533 | normal: geo.normal.as_mut_ptr() as *mut _, 534 | color: geo.color.as_mut_ptr() as *mut _, 535 | uv: geo.uv.as_mut_ptr() as *mut _, 536 | numTrianglesUsed: geo.position.len() as u16 / 3, 537 | } 538 | } 539 | } 540 | 541 | /// A vertex that makes up Mario's model 542 | #[derive(Debug, Copy, Clone)] 543 | pub struct MarioVertex { 544 | /// The position of the vertex 545 | pub position: Point3, 546 | /// The normal of the vertex 547 | pub normal: Point3, 548 | /// The color of the vertex 549 | pub color: Color, 550 | /// The texture coordinate of the vertex 551 | pub uv: Point2, 552 | } 553 | 554 | /// The surface terrain of a triangle 555 | #[repr(u16)] 556 | #[derive(Copy, Clone, Debug)] 557 | pub enum Terrain { 558 | Grass = 0x0000, 559 | Stone = 0x0001, 560 | Snow = 0x0002, 561 | Sand = 0x0003, 562 | Spooky = 0x0004, 563 | Water = 0x0005, 564 | Slide = 0x0006, 565 | Mask = 0x0007, 566 | } 567 | 568 | /// The surface type of a triangle 569 | #[repr(u16)] 570 | #[derive(Copy, Clone, Debug)] 571 | pub enum Surface { 572 | Default = 0x0000, 573 | Burning = 0x0001, 574 | _0004 = 0x0004, 575 | Hangable = 0x0005, 576 | Slow = 0x0009, 577 | DeathPlane = 0x000A, 578 | CloseCamera = 0x000B, 579 | Water = 0x000D, 580 | FlowingWater = 0x000E, 581 | Intangible = 0x0012, 582 | VerySlippery = 0x0013, 583 | Slippery = 0x0014, 584 | NotSlippery = 0x0015, 585 | TtmVines = 0x0016, 586 | MgrMusic = 0x001A, 587 | InstantWarp1b = 0x001B, 588 | InstantWarp1c = 0x001C, 589 | InstantWarp1d = 0x001D, 590 | InstantWarp1e = 0x001E, 591 | ShallowQuicksand = 0x0021, 592 | DeepQuicksand = 0x0022, 593 | InstantQuicksand = 0x0023, 594 | DeepMovingQuicksand = 0x0024, 595 | ShallowMovingQuicksand = 0x0025, 596 | Quicksand = 0x0026, 597 | MovingQuicksand = 0x0027, 598 | WallMisc = 0x0028, 599 | NoiseDefault = 0x0029, 600 | NoiseSlippery = 0x002A, 601 | HorizontalWind = 0x002C, 602 | InstantMovingQuicksand = 0x002D, 603 | Ice = 0x002E, 604 | LookUpWarp = 0x002F, 605 | Hard = 0x0030, 606 | Warp = 0x0032, 607 | TimerStart = 0x0033, 608 | TimerEnd = 0x0034, 609 | HardSlippery = 0x0035, 610 | HardVerySlippery = 0x0036, 611 | HardNotSlippery = 0x0037, 612 | VerticalWind = 0x0038, 613 | BossFightCamera = 0x0065, 614 | CameraFreeRoam = 0x0066, 615 | Thi3Wallkick = 0x0068, 616 | CameraPlatform = 0x0069, 617 | CameraMiddle = 0x006E, 618 | CameraRotateRight = 0x006F, 619 | CameraRotateLeft = 0x0070, 620 | CameraBoundary = 0x0072, 621 | NoiseVerySlippery73 = 0x0073, 622 | NoiseVerySlippery74 = 0x0074, 623 | NoiseVerySlippery = 0x0075, 624 | NoCamCollision = 0x0076, 625 | NoCamCollision77 = 0x0077, 626 | NoCamColVerySlippery = 0x0078, 627 | NoCamColSlippery = 0x0079, 628 | Switch = 0x007A, 629 | VanishCapWalls = 0x007B, 630 | PaintingWobbleA6 = 0x00A6, 631 | PaintingWobbleA7 = 0x00A7, 632 | PaintingWobbleA8 = 0x00A8, 633 | PaintingWobbleA9 = 0x00A9, 634 | PaintingWobbleAA = 0x00AA, 635 | PaintingWobbleAB = 0x00AB, 636 | PaintingWobbleAC = 0x00AC, 637 | PaintingWobbleAD = 0x00AD, 638 | PaintingWobbleAE = 0x00AE, 639 | PaintingWobbleAF = 0x00AF, 640 | PaintingWobbleB0 = 0x00B0, 641 | PaintingWobbleB1 = 0x00B1, 642 | PaintingWobbleB2 = 0x00B2, 643 | PaintingWobbleB3 = 0x00B3, 644 | PaintingWobbleB4 = 0x00B4, 645 | PaintingWobbleB5 = 0x00B5, 646 | PaintingWobbleB6 = 0x00B6, 647 | PaintingWobbleB7 = 0x00B7, 648 | PaintingWobbleB8 = 0x00B8, 649 | PaintingWobbleB9 = 0x00B9, 650 | PaintingWobbleBA = 0x00BA, 651 | PaintingWobbleBB = 0x00BB, 652 | PaintingWobbleBC = 0x00BC, 653 | PaintingWobbleBD = 0x00BD, 654 | PaintingWobbleBE = 0x00BE, 655 | PaintingWobbleBF = 0x00BF, 656 | PaintingWobbleC0 = 0x00C0, 657 | PaintingWobbleC1 = 0x00C1, 658 | PaintingWobbleC2 = 0x00C2, 659 | PaintingWobbleC3 = 0x00C3, 660 | PaintingWobbleC4 = 0x00C4, 661 | PaintingWobbleC5 = 0x00C5, 662 | PaintingWobbleC6 = 0x00C6, 663 | PaintingWobbleC7 = 0x00C7, 664 | PaintingWobbleC8 = 0x00C8, 665 | PaintingWobbleC9 = 0x00C9, 666 | PaintingWobbleCA = 0x00CA, 667 | PaintingWobbleCB = 0x00CB, 668 | PaintingWobbleCC = 0x00CC, 669 | PaintingWobbleCD = 0x00CD, 670 | PaintingWobbleCE = 0x00CE, 671 | PaintingWobbleCF = 0x00CF, 672 | PaintingWobbleD0 = 0x00D0, 673 | PaintingWobbleD1 = 0x00D1, 674 | PaintingWobbleD2 = 0x00D2, 675 | PaintingWarpD3 = 0x00D3, 676 | PaintingWarpD4 = 0x00D4, 677 | PaintingWarpD5 = 0x00D5, 678 | PaintingWarpD6 = 0x00D6, 679 | PaintingWarpD7 = 0x00D7, 680 | PaintingWarpD8 = 0x00D8, 681 | PaintingWarpD9 = 0x00D9, 682 | PaintingWarpDA = 0x00DA, 683 | PaintingWarpDB = 0x00DB, 684 | PaintingWarpDC = 0x00DC, 685 | PaintingWarpDD = 0x00DD, 686 | PaintingWarpDE = 0x00DE, 687 | PaintingWarpDF = 0x00DF, 688 | PaintingWarpE0 = 0x00E0, 689 | PaintingWarpE1 = 0x00E1, 690 | PaintingWarpE2 = 0x00E2, 691 | PaintingWarpE3 = 0x00E3, 692 | PaintingWarpE4 = 0x00E4, 693 | PaintingWarpE5 = 0x00E5, 694 | PaintingWarpE6 = 0x00E6, 695 | PaintingWarpE7 = 0x00E7, 696 | PaintingWarpE8 = 0x00E8, 697 | PaintingWarpE9 = 0x00E9, 698 | PaintingWarpEA = 0x00EA, 699 | PaintingWarpEB = 0x00EB, 700 | PaintingWarpEC = 0x00EC, 701 | PaintingWarpED = 0x00ED, 702 | PaintingWarpEE = 0x00EE, 703 | PaintingWarpEF = 0x00EF, 704 | PaintingWarpF0 = 0x00F0, 705 | PaintingWarpF1 = 0x00F1, 706 | PaintingWarpF2 = 0x00F2, 707 | PaintingWarpF3 = 0x00F3, 708 | TtcPainting1 = 0x00F4, 709 | TtcPainting2 = 0x00F5, 710 | TtcPainting3 = 0x00F6, 711 | PaintingWarpF7 = 0x00F7, 712 | PaintingWarpF8 = 0x00F8, 713 | PaintingWarpF9 = 0x00F9, 714 | PaintingWarpFA = 0x00FA, 715 | PaintingWarpFB = 0x00FB, 716 | PaintingWarpFC = 0x00FC, 717 | WobblingWarp = 0x00FD, 718 | Trapdoor = 0x00FF, 719 | } 720 | 721 | #[test] 722 | fn basic_loading() { 723 | let rom = std::env::var("SM64_ROM_PATH") 724 | .expect("Path to SM64 rom must be proivided in 'SM64_ROM_PATH' env var"); 725 | let rom = std::fs::File::open(rom).unwrap(); 726 | let mut sm64 = Sm64::new(rom).unwrap(); 727 | let mario = sm64.create_mario(1.0, 2.0, 3.0); 728 | 729 | match mario { 730 | Err(Error::InvalidMarioPosition) => (), 731 | _ => panic!("Expected InvalidMarioPosition error"), 732 | } 733 | } 734 | 735 | #[test] 736 | fn correct_repr() { 737 | assert_eq!( 738 | std::mem::size_of::(), 739 | std::mem::size_of::() 740 | ); 741 | 742 | let tri = LevelTriangle { 743 | kind: Surface::Default, 744 | force: 333, 745 | terrain: Terrain::Grass, 746 | vertices: ( 747 | Point3 { x: 1, y: 2, z: 3 }, 748 | Point3 { x: 4, y: 5, z: 6 }, 749 | Point3 { x: 7, y: 8, z: 9 }, 750 | ), 751 | }; 752 | 753 | let c_tri = libsm64_sys::SM64Surface { 754 | type_: 0, 755 | force: 333, 756 | terrain: 0, 757 | vertices: [[1, 2, 3], [4, 5, 6], [7, 8, 9]], 758 | }; 759 | 760 | let my_c_tri = unsafe { std::mem::transmute::<_, libsm64_sys::SM64Surface>(tri) }; 761 | 762 | assert_eq!(c_tri.type_, my_c_tri.type_); 763 | assert_eq!(c_tri.force, my_c_tri.force); 764 | assert_eq!(c_tri.terrain, my_c_tri.terrain); 765 | assert_eq!(c_tri.vertices, my_c_tri.vertices); 766 | } 767 | --------------------------------------------------------------------------------