diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 34abf95..963a8dd 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,14 +3,34 @@ windows_subsystem = "windows" )] +mod nes_parser; + use std::io::Read; use tauri::Manager; +use nes_parser::INesVersion; #[derive(Clone, serde::Serialize)] struct Payload { message: String, } +#[derive(Clone, serde::Serialize)] +struct INesData { + filename: String, + prg_rom_size: u32, + chr_rom_size: u32, + mirroring_type: bool, + has_battery: bool, + has_trainer: bool, + has_four_screen_vram: bool, + is_vs_unisystem: bool, + is_playchoice_10: bool, + ines_version_identifier: u8, + mapper: u8, + prg_ram_size: u8, + tv_system: bool, +} + // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command #[tauri::command] fn select_file(app: tauri::AppHandle) { @@ -39,7 +59,57 @@ fn select_file(app: tauri::AppHandle) { app.emit_all("error", Payload { message: "Unable to read file".into() }).unwrap(); return; } - println!("{}", contents.len()); + let version = nes_parser::parse_version(&contents); + if version.is_err() { + return; + } + match version { + Ok(INesVersion::INes20) => { + let header = nes_parser::INes20Header::from_rom(&contents); + match header { + Ok(header) => { + println!("PRG ROM size: {}", header.get_prg_rom_size()); + }, + Err(error) => { + app.emit_all("error", Payload { message: nes_parser::get_error_message(error).into() }).unwrap(); + return; + } + } + }, + Ok(INesVersion::INes) => { + let header = nes_parser::INesHeader::from_rom(&contents); + match header { + Ok(header) => { + app.emit_all("loaded_ines", INesData { + filename: path_buf.file_name().unwrap().to_os_string().into_string().unwrap(), + prg_rom_size: header.prg_rom_size(), + chr_rom_size: header.chr_rom_size(), + mirroring_type: header.mirroring_type(), + has_battery: header.has_battery(), + has_trainer: header.has_trainer(), + has_four_screen_vram: header.has_four_screen_vram(), + is_vs_unisystem: header.is_vs_unisystem(), + is_playchoice_10: header.is_playchoice_10(), + ines_version_identifier: header.ines_version_identifier(), + mapper: header.mapper(), + prg_ram_size: header.prg_ram_size(), + tv_system: header.tv_system(), + }).unwrap(); + return; + }, + Err(error) => { + app.emit_all("error", Payload { message: nes_parser::get_error_message(error).into() }).unwrap(); + return; + }, + } + }, + Ok(INesVersion::INesArchaic) => { + app.emit_all("error", Payload { message: "Gotta parse an old iNES version huh".into() }).unwrap(); + }, + Err(error) => { + app.emit_all("error", Payload { message: nes_parser::get_error_message(error).into() }).unwrap(); + }, + } } None => {} }); diff --git a/src-tauri/src/nes_parser.rs b/src-tauri/src/nes_parser.rs new file mode 100644 index 0000000..e37b1f7 --- /dev/null +++ b/src-tauri/src/nes_parser.rs @@ -0,0 +1,180 @@ +#[derive(Debug)] +pub enum INesVersion { + INes, + INes20, + INesArchaic, +} + +#[derive(Debug)] +pub enum INesError { + NotAnINesFile, +} + +pub struct INesHeader { + prg_rom_size: u8, + chr_rom_size: u8, + mirroring_type: bool, + has_battery: bool, + has_trainer: bool, + has_four_screen_vram: bool, + is_vs_unisystem: bool, + is_playchoice_10: bool, + ines_version_identifier: u8, + mapper: u8, + prg_ram_size: u8, + tv_system: bool, +} + +impl INesHeader { + + pub fn from_rom(rom_bytes: &Vec) -> Result { + if rom_bytes[0] != 0x4e || rom_bytes[1] != 0x45 || rom_bytes[2] != 0x53 || rom_bytes[3] != 0x1a { + return Err(INesError::NotAnINesFile); + } + Ok( + INesHeader { + prg_rom_size: rom_bytes[4], + chr_rom_size: rom_bytes[5], + mirroring_type: rom_bytes[6] & 0x01 != 0, + has_battery: rom_bytes[6] & 0x02 != 0, + has_trainer: rom_bytes[6] & 0x04 != 0, + has_four_screen_vram: rom_bytes[6] & 0x08 != 0, + is_vs_unisystem: rom_bytes[7] & 0x01 != 0, + is_playchoice_10: rom_bytes[7] & 0x02 != 0, + ines_version_identifier: (rom_bytes[7] & 0x0c) >> 2, + mapper: + ((rom_bytes[6] & 0xf0) >> 4) | + (rom_bytes[7] & 0xf0), + prg_ram_size: rom_bytes[8], + tv_system: rom_bytes[9] & 0x01 != 0, + } + ) + } + + pub fn prg_rom_size(&self) -> u32 { + (self.prg_rom_size as u32) * 16384 + } + + pub fn chr_rom_size(&self) -> u32 { + (self.chr_rom_size as u32) * 8192 + } + + pub fn mirroring_type(&self) -> bool { + self.mirroring_type + } + + pub fn has_battery(&self) -> bool { + self.has_battery + } + + pub fn has_trainer(&self) -> bool { + self.has_trainer + } + + pub fn has_four_screen_vram(&self) -> bool { + self.has_four_screen_vram + } + + pub fn is_vs_unisystem(&self) -> bool { + self.is_vs_unisystem + } + + pub fn is_playchoice_10(&self) -> bool { + self.is_playchoice_10 + } + + pub fn ines_version_identifier(&self) -> u8 { + self.ines_version_identifier + } + + pub fn mapper(&self) -> u8 { + self.mapper + } + + pub fn prg_ram_size(&self) -> u8 { + self.prg_ram_size + } + + pub fn tv_system(&self) -> bool { + self.tv_system + } + +} + +pub struct INes20Header { + prg_rom_size: u16, + chr_rom_size: u16, + mirroring_type: bool, + has_battery: bool, + has_trainer: bool, + has_four_screen_vram: bool, + console_type: u8, + mapper: u16, + submapper: u8, + prg_ram_shift_count: u8, + prg_nvram_shift_count: u8, + chr_ram_shift_count: u8, + chr_nvram_shift_count: u8, + cpu_type: u8, + console_specific_type: u8, + miscellaneous_roms: u8, + default_expansion_device: u8, +} + +impl INes20Header { + + pub fn from_rom(rom_bytes: &Vec) -> Result { + if rom_bytes[0] != 0x4e || rom_bytes[1] != 0x45 || rom_bytes[2] != 0x53 || rom_bytes[3] != 0x1a { + return Err(INesError::NotAnINesFile); + } + Ok( + INes20Header { + prg_rom_size: (rom_bytes[4] as u16) | (((rom_bytes[9] as u16) << 8) & 0x0f00), + chr_rom_size: (rom_bytes[5] as u16) | (((rom_bytes[9] as u16) << 4) & 0x0f00), + mirroring_type: (rom_bytes[6] & 0x01) != 0, + has_battery: (rom_bytes[6] & 0x02) != 0, + has_trainer: (rom_bytes[6] & 0x04) != 0, + has_four_screen_vram: (rom_bytes[6] & 0x08) != 0, + console_type: rom_bytes[7] & 0x03, + mapper: + (((rom_bytes[6] as u16) >> 4) & 0x000f) | + (((rom_bytes[7] as u16) ) & 0x00f0) | + (((rom_bytes[8] as u16) << 8) & 0x0f00), + submapper: (rom_bytes[8] >> 4) & 0x0f, + prg_ram_shift_count: rom_bytes[10] & 0x0f, + prg_nvram_shift_count: (rom_bytes[10] >> 4) & 0x0f, + chr_ram_shift_count: rom_bytes[11] & 0x0f, + chr_nvram_shift_count: (rom_bytes[11] >> 4) & 0x0f, + cpu_type: rom_bytes[12] & 0x03, + console_specific_type: rom_bytes[13], + miscellaneous_roms: rom_bytes[14] & 0x03, + default_expansion_device: rom_bytes[16] & 0x3f, + } + ) + } + + pub fn get_prg_rom_size(self) -> u16 { + self.prg_rom_size + } + +} + +pub fn get_error_message(error: INesError) -> &'static str { + match error { + INesError::NotAnINesFile => "Not an iNES file", + } +} + +pub fn parse_version(rom_bytes: &Vec) -> Result { + if rom_bytes[0] != 0x4e || rom_bytes[1] != 0x45 || rom_bytes[2] != 0x53 || rom_bytes[3] != 0x1a { + return Err(INesError::NotAnINesFile); + } + let version: u8 = rom_bytes[7] & 0x0c; + if version == 0x08 { + Ok(INesVersion::INes20) + } else if version == 0x04 { + Ok(INesVersion::INesArchaic) + } else { + Ok(INesVersion::INes) + } +} \ No newline at end of file diff --git a/src/index.html b/src/index.html index 6f22633..6c9ab46 100644 --- a/src/index.html +++ b/src/index.html @@ -19,7 +19,10 @@

Loading...

- +
    +
    +
    +

    diff --git a/src/script.js b/src/script.js index 7ffb83d..5a54ae4 100644 --- a/src/script.js +++ b/src/script.js @@ -2,15 +2,55 @@ const { listen } = window.__TAURI__.event; const { invoke } = window.__TAURI__.tauri; window.addEventListener("DOMContentLoaded", (e) => { - console.log(fileUploader); fileUploader.addEventListener("click", (e) => { invoke("select_file"); }); }); -listen("loading", e => { - document.body.classList.remove([ +listen("loaded_ines", e => { + console.log(e); + document.body.classList.remove( "no-file-selected", - ]); - document.body.classList.add(["is-loading"]); + "is-loading", + "has-error", + ); + document.body.classList.add("file-loaded"); + fileProperties.innerHTML = ""; + [ + ["File name", e.payload.filename], + ["PRG ROM size", e.payload.prg_rom_size+" bytes"], + ["CHR ROM size", e.payload.chr_rom_size+" bytes"], + ["Mirroring Type", e.payload.mirroring_type ? "Vertical" : "Horizontal"], + ["Has persistent memory", e.payload.has_battery ? "Yes" : "No"], + ["Has trainer", e.payload.has_trainer ? "Yes" : "No"], + ["Has four-screen VRAM", e.payload.has_four_screen_vram ? "Yes" : "No"], + ["Is a VS Unisystem ROM", e.payload.is_vs_unisystem ? "Yes" : "No"], + ["Is a PlayChoice-10 ROM", e.payload.is_playchoice_10 ? "Yes" : "No"], + ["iNES version identifier", e.payload.ines_version_identifier], + ["Mapper", e.payload.mapper], + ["PRG RAM size", e.payload.prg_ram_size], + ["TV system", e.payload.tv_system ? "PAL" : "NTSC"], + ].forEach(el => { + let domElement = document.createElement("li"); + domElement.innerHTML = ""+el[0]+": "+el[1]; + fileProperties.appendChild(domElement); + }); +}); + +listen("loading", e => { + document.body.classList.remove( + "no-file-selected", + "has-error", + "file-loaded", + ); + document.body.classList.add("is-loading"); + }); + + listen("error", e => { + document.body.classList.remove( + "is-loading", + "file-loaded", + ); + document.body.classList.add("no-file-selected", "has-error"); + errorMessage.innerText = e.payload.message; }); \ No newline at end of file diff --git a/src/style.css b/src/style.css index 5ad5bf6..8226946 100644 --- a/src/style.css +++ b/src/style.css @@ -14,7 +14,7 @@ h1 { } .select-file-wrapper { - display: none; + display: flex; justify-content: center; align-items: center; width: 100%; @@ -24,8 +24,8 @@ h1 { cursor: pointer; } -body.no-file-selected .select-file-wrapper { - display: flex; +body.file-loaded .select-file-wrapper { + height: 100px; } .loader { @@ -39,4 +39,18 @@ body.is-loading .loader { main { flex-grow: 1; +} + +.error-wrapper { + display: none; + background-color: #eb8c95; + color: #66121a; + border: 1px solid #66121a; + border-radius: 0.5rem; + padding: 0.5rem 2rem; + margin-top: 1rem; +} + +body.has-error .error-wrapper { + display: block; } \ No newline at end of file