diff --git a/src/app.rs b/src/app.rs index 7566d91..61408a1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -23,7 +23,7 @@ use crate::{ vram::VramProcessor, window::{ AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, GameWindow, GdbServerWindow, - InputWindow, ObjectWindow, + InputWindow, ObjectWindow, WorldWindow, }, }; @@ -225,6 +225,10 @@ impl ApplicationHandler for Application { let objects = ObjectWindow::new(sim_id, &self.memory, &mut self.vram); self.open(event_loop, Box::new(objects)); } + UserEvent::OpenWorlds(sim_id) => { + let world = WorldWindow::new(sim_id, &self.memory, &mut self.vram); + self.open(event_loop, Box::new(world)); + } UserEvent::OpenDebugger(sim_id) => { let debugger = GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone()); @@ -486,6 +490,7 @@ pub enum UserEvent { OpenCharacterData(SimId), OpenBgMap(SimId), OpenObjects(SimId), + OpenWorlds(SimId), OpenDebugger(SimId), OpenInput, OpenPlayer2, diff --git a/src/memory.rs b/src/memory.rs index 769326f..d679ad0 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -1,6 +1,7 @@ use std::{ collections::HashMap, fmt::Debug, + iter::FusedIterator, sync::{atomic::AtomicU64, Arc, Mutex, RwLock, RwLockReadGuard, TryLockError, Weak}, }; @@ -117,7 +118,6 @@ impl MemoryValue for [T; N] { pub struct MemoryIter<'a, T> { bytes: &'a [u8], - index: usize, _phantom: std::marker::PhantomData, } @@ -125,7 +125,6 @@ impl<'a, T> MemoryIter<'a, T> { fn new(bytes: &'a [u8]) -> Self { Self { bytes, - index: 0, _phantom: std::marker::PhantomData, } } @@ -136,15 +135,30 @@ impl Iterator for MemoryIter<'_, T> { #[inline] fn next(&mut self) -> Option { - if self.index >= self.bytes.len() { - return None; - } - let bytes = &self.bytes[self.index..self.index + std::mem::size_of::()]; - self.index += std::mem::size_of::(); + let (bytes, rest) = self.bytes.split_at_checked(std::mem::size_of::())?; + self.bytes = rest; + Some(T::from_bytes(bytes)) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let size = self.bytes.len() / std::mem::size_of::(); + (size, Some(size)) + } +} + +impl DoubleEndedIterator for MemoryIter<'_, T> { + fn next_back(&mut self) -> Option { + let mid = self.bytes.len().checked_sub(std::mem::size_of::())?; + // SAFETY: the checked_sub above is effectively a bounds check + let (rest, bytes) = unsafe { self.bytes.split_at_unchecked(mid) }; + self.bytes = rest; Some(T::from_bytes(bytes)) } } +impl FusedIterator for MemoryIter<'_, T> {} + impl MemoryRef<'_> { pub fn read(&self, index: usize) -> T { let from = index * size_of::(); diff --git a/src/vram.rs b/src/vram.rs index be6ee1d..88e5e89 100644 --- a/src/vram.rs +++ b/src/vram.rs @@ -39,6 +39,7 @@ impl VramProcessor { pub fn add + 'static>( &self, renderer: R, + params: R::Params, ) -> ([VramImageHandle; N], VramParams) { let states = renderer.sizes().map(VramRenderImageState::new); let handles = states.clone().map(|state| VramImageHandle { @@ -48,7 +49,7 @@ impl VramProcessor { let images = renderer .sizes() .map(|[width, height]| VramImage::new(width, height)); - let sink = Arc::new(Mutex::new(R::Params::default())); + let sink = Arc::new(Mutex::new(params.clone())); let _ = self.sender.send(Box::new(VramRendererWrapper { renderer, params: Arc::downgrade(&sink), @@ -56,7 +57,7 @@ impl VramProcessor { states, })); let params = VramParams { - value: R::Params::default(), + value: params, sink, }; (handles, params) @@ -100,32 +101,32 @@ impl VramProcessorWorker { } pub struct VramImage { - size: [usize; 2], - shades: Vec, + pub size: [usize; 2], + pub pixels: Vec, } impl VramImage { pub fn new(width: usize, height: usize) -> Self { Self { size: [width, height], - shades: vec![Color32::BLACK; width * height], + pixels: vec![Color32::BLACK; width * height], } } pub fn clear(&mut self) { - for shade in self.shades.iter_mut() { - *shade = Color32::BLACK; + for pixel in self.pixels.iter_mut() { + *pixel = Color32::BLACK; } } pub fn write(&mut self, coords: (usize, usize), pixel: Color32) { - self.shades[coords.1 * self.size[0] + coords.0] = pixel; + self.pixels[coords.1 * self.size[0] + coords.0] = pixel; } pub fn add(&mut self, coords: (usize, usize), pixel: Color32) { let index = coords.1 * self.size[0] + coords.0; - let old = self.shades[index]; - self.shades[index] = Color32::from_rgb( + let old = self.pixels[index]; + self.pixels[index] = Color32::from_rgb( old.r() + pixel.r(), old.g() + pixel.g(), old.b() + pixel.b(), @@ -133,11 +134,11 @@ impl VramImage { } pub fn changed(&self, image: &ColorImage) -> bool { - image.pixels.iter().zip(&self.shades).any(|(a, b)| a != b) + image.pixels.iter().zip(&self.pixels).any(|(a, b)| a != b) } pub fn read(&self, image: &mut ColorImage) { - image.pixels.copy_from_slice(&self.shades); + image.pixels.copy_from_slice(&self.pixels); } } @@ -176,7 +177,7 @@ impl VramParams { } pub trait VramRenderer: Send { - type Params: Clone + Default + Send; + type Params: Clone + Send; fn sizes(&self) -> [[usize; 2]; N]; fn render(&mut self, params: &Self::Params, images: &mut [VramImage; N]); } diff --git a/src/window.rs b/src/window.rs index fd7cfe1..80d5179 100644 --- a/src/window.rs +++ b/src/window.rs @@ -3,7 +3,7 @@ use egui::{Context, ViewportBuilder, ViewportId}; pub use game::GameWindow; pub use gdb::GdbServerWindow; pub use input::InputWindow; -pub use vram::{BgMapWindow, CharacterDataWindow, ObjectWindow}; +pub use vram::{BgMapWindow, CharacterDataWindow, ObjectWindow, WorldWindow}; use winit::event::KeyEvent; use crate::emulator::SimId; diff --git a/src/window/game.rs b/src/window/game.rs index 256fc78..36ac27f 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -150,6 +150,12 @@ impl GameWindow { .unwrap(); ui.close_menu(); } + if ui.button("Worlds").clicked() { + self.proxy + .send_event(UserEvent::OpenWorlds(self.sim_id)) + .unwrap(); + ui.close_menu(); + } }); ui.menu_button("Help", |ui| { if ui.button("About").clicked() { diff --git a/src/window/vram.rs b/src/window/vram.rs index ad93bef..f1acc3a 100644 --- a/src/window/vram.rs +++ b/src/window/vram.rs @@ -2,7 +2,9 @@ mod bgmap; mod chardata; mod object; mod utils; +mod world; pub use bgmap::*; pub use chardata::*; pub use object::*; +pub use world::*; diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 8760347..47fa87e 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -33,7 +33,7 @@ pub struct BgMapWindow { impl BgMapWindow { pub fn new(sim_id: SimId, memory: &Arc, vram: &mut VramProcessor) -> Self { let renderer = BgMapRenderer::new(sim_id, memory); - let ([cell, bgmap], params) = vram.add(renderer); + let ([cell, bgmap], params) = vram.add(renderer, BgMapParams::default()); let loader = VramTextureLoader::new([("vram://cell".into(), cell), ("vram://bgmap".into(), bgmap)]); Self { @@ -41,8 +41,8 @@ impl BgMapWindow { loader: Arc::new(loader), memory: memory.clone(), bgmaps: memory.watch(sim_id, 0x00020000, 0x1d800), - cell_index: 0, - generic_palette: false, + cell_index: params.cell_index, + generic_palette: params.generic_palette, params, scale: 1.0, show_grid: false, @@ -243,7 +243,7 @@ impl BgMapRenderer { let colors = if generic_palette { [utils::generic_palette(Color32::RED); 4] } else { - [0, 2, 4, 6].map(|i| utils::parse_palette(palettes.read(i), &brts, Color32::RED)) + [0, 2, 4, 6].map(|i| utils::palette_colors(palettes.read(i), &brts, Color32::RED)) }; for (i, cell) in bgmaps.range::(bgmap_index * 4096, 4096).enumerate() { @@ -286,7 +286,7 @@ impl BgMapRenderer { let palette = if generic_palette { utils::generic_palette(Color32::RED) } else { - utils::parse_palette(palettes.read(palette_index * 2), &brts, Color32::RED) + utils::palette_colors(palettes.read(palette_index * 2), &brts, Color32::RED) }; for row in 0..8 { diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index 1cde8d4..10c14d7 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -94,7 +94,7 @@ pub struct CharacterDataWindow { impl CharacterDataWindow { pub fn new(sim_id: SimId, memory: &MemoryClient, vram: &mut VramProcessor) -> Self { let renderer = CharDataRenderer::new(sim_id, memory); - let ([char, chardata], params) = vram.add(renderer); + let ([char, chardata], params) = vram.add(renderer, CharDataParams::default()); let loader = VramTextureLoader::new([ ("vram://char".into(), char), ("vram://chardata".into(), chardata), @@ -222,7 +222,7 @@ impl CharacterDataWindow { let palette = self.palettes.borrow().read(offset); let brightnesses = self.brightness.borrow(); let brts = brightnesses.read(0); - utils::parse_palette(palette, &brts, Color32::RED) + utils::palette_colors(palette, &brts, Color32::RED) } fn show_chardata(&mut self, ui: &mut Ui) { @@ -351,6 +351,6 @@ impl CharDataRenderer { let palette = self.palettes.borrow().read(offset); let brightnesses = self.brightness.borrow(); let brts = brightnesses.read(0); - utils::parse_palette(palette, &brts, Color32::RED) + utils::palette_colors(palette, &brts, Color32::RED) } } diff --git a/src/window/vram/object.rs b/src/window/vram/object.rs index 8c2aae3..90c200e 100644 --- a/src/window/vram/object.rs +++ b/src/window/vram/object.rs @@ -31,8 +31,14 @@ pub struct ObjectWindow { impl ObjectWindow { pub fn new(sim_id: SimId, memory: &Arc, vram: &mut VramProcessor) -> Self { + let initial_params = ObjectParams { + index: 0, + generic_palette: false, + left_color: Color32::from_rgb(0xff, 0x00, 0x00), + right_color: Color32::from_rgb(0x00, 0xc6, 0xf0), + }; let renderer = ObjectRenderer::new(sim_id, memory); - let ([zoom, full], params) = vram.add(renderer); + let ([zoom, full], params) = vram.add(renderer, initial_params); let loader = VramTextureLoader::new([("vram://zoom".into(), zoom), ("vram://full".into(), full)]); Self { @@ -40,8 +46,8 @@ impl ObjectWindow { loader: Arc::new(loader), memory: memory.clone(), objects: memory.watch(sim_id, 0x0003e000, 0x2000), - index: 0, - generic_palette: false, + index: params.index, + generic_palette: params.generic_palette, params, scale: 1.0, } @@ -175,7 +181,7 @@ impl ObjectWindow { self.params.write(ObjectParams { index: self.index, generic_palette: self.generic_palette, - ..ObjectParams::default() + ..*self.params }); } @@ -233,17 +239,6 @@ struct ObjectParams { right_color: Color32, } -impl Default for ObjectParams { - fn default() -> Self { - Self { - index: 0, - generic_palette: false, - left_color: Color32::from_rgb(0xff, 0x00, 0x00), - right_color: Color32::from_rgb(0x00, 0xc6, 0xf0), - } - } -} - enum Eye { Left, Right, @@ -302,7 +297,7 @@ impl ObjectRenderer { let palette = if params.generic_palette { utils::generic_palette(color) } else { - utils::parse_palette(palettes.read(8 + obj.data.palette_index * 2), &brts, color) + utils::palette_colors(palettes.read(8 + obj.data.palette_index * 2), &brts, color) }; for row in 0..8 { diff --git a/src/window/vram/utils.rs b/src/window/vram/utils.rs index d95319c..9cbfd4f 100644 --- a/src/window/vram/utils.rs +++ b/src/window/vram/utils.rs @@ -10,24 +10,29 @@ pub fn generic_palette(color: Color32) -> [Color32; 4] { GENERIC_PALETTE.map(|brt| shade(brt, color)) } -pub fn parse_palette(palette: u8, brts: &[u8; 8], color: Color32) -> [Color32; 4] { - let shades = [ - Color32::BLACK, - shade(brts[0], color), - shade(brts[2], color), - shade( - brts[0].saturating_add(brts[2]).saturating_add(brts[4]), - color, - ), - ]; +pub const fn parse_palette(palette: u8) -> [u8; 4] { [ - Color32::BLACK, - shades[(palette >> 2) as usize & 0x03], - shades[(palette >> 4) as usize & 0x03], - shades[(palette >> 6) as usize & 0x03], + 0, + (palette >> 2) & 0x03, + (palette >> 4) & 0x03, + (palette >> 6) & 0x03, ] } +pub const fn parse_shades(brts: &[u8; 8]) -> [u8; 4] { + [ + 0, + brts[0], + brts[2], + brts[0].saturating_add(brts[2]).saturating_add(brts[4]), + ] +} + +pub fn palette_colors(palette: u8, brts: &[u8; 8], color: Color32) -> [Color32; 4] { + let colors = parse_shades(brts).map(|s| shade(s, color)); + parse_palette(palette).map(|p| colors[p as usize]) +} + pub struct Object { pub x: i16, pub lon: bool, diff --git a/src/window/vram/world.rs b/src/window/vram/world.rs new file mode 100644 index 0000000..6bf78e6 --- /dev/null +++ b/src/window/vram/world.rs @@ -0,0 +1,409 @@ +use std::{fmt::Display, sync::Arc}; + +use egui::{ + CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextureOptions, + Ui, ViewportBuilder, ViewportId, +}; +use egui_extras::{Column, Size, StripBuilder, TableBuilder}; +use num_derive::{FromPrimitive, ToPrimitive}; +use num_traits::FromPrimitive; + +use crate::{ + emulator::SimId, + memory::{MemoryClient, MemoryView}, + vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, + window::{ + utils::{NumberEdit, UiExt as _}, + AppWindow, + }, +}; + +use super::utils::{self, shade, Object}; + +pub struct WorldWindow { + sim_id: SimId, + loader: Arc, + worlds: MemoryView, + index: usize, + generic_palette: bool, + params: VramParams, + scale: f32, +} + +impl WorldWindow { + pub fn new(sim_id: SimId, memory: &MemoryClient, vram: &mut VramProcessor) -> Self { + let initial_params = WorldParams { + index: 31, + generic_palette: false, + left_color: Color32::from_rgb(0xff, 0x00, 0x00), + right_color: Color32::from_rgb(0x00, 0xc6, 0xf0), + }; + let renderer = WorldRenderer::new(sim_id, memory); + let ([world], params) = vram.add(renderer, initial_params); + let loader = VramTextureLoader::new([("vram://world".into(), world)]); + Self { + sim_id, + loader: Arc::new(loader), + worlds: memory.watch(sim_id, 0x3d800, 0x400), + index: params.index, + generic_palette: params.generic_palette, + params, + scale: 1.0, + } + } + + fn show_form(&mut self, ui: &mut Ui) { + let row_height = ui.spacing().interact_size.y; + ui.vertical(|ui| { + TableBuilder::new(ui) + .column(Column::auto()) + .column(Column::remainder()) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Index"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut self.index).range(0..32)); + }); + }); + }); + let data = { + let worlds = self.worlds.borrow(); + worlds.read(self.index) + }; + let mut world = World::parse(&data); + ui.section("Properties", |ui| { + TableBuilder::new(ui) + .column(Column::remainder()) + .column(Column::remainder()) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Mode"); + }); + row.col(|ui| { + ComboBox::from_id_salt("mode") + .selected_text(world.header.mode.to_string()) + .width(ui.available_width()) + .show_ui(ui, |ui| { + for mode in WorldMode::values() { + ui.selectable_value( + &mut world.header.mode, + mode, + mode.to_string(), + ); + } + }); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add(Checkbox::new(&mut world.header.lon, "Left")); + }); + row.col(|ui| { + ui.add(Checkbox::new(&mut world.header.ron, "Right")); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add(Checkbox::new(&mut world.header.end, "End")); + }); + row.col(|ui| { + ui.add(Checkbox::new(&mut world.header.over, "Overplane")); + }); + }); + }); + }); + ui.section("Display", |ui| { + ui.horizontal(|ui| { + ui.label("Scale"); + ui.spacing_mut().slider_width = ui.available_width(); + let slider = Slider::new(&mut self.scale, 1.0..=10.0) + .step_by(1.0) + .show_value(false); + ui.add(slider); + }); + ui.checkbox(&mut self.generic_palette, "Generic palette"); + }); + }); + self.params.write(WorldParams { + index: self.index, + generic_palette: self.generic_palette, + ..*self.params + }); + } + + fn show_world(&mut self, ui: &mut Ui) { + let image = Image::new("vram://world") + .fit_to_original_size(self.scale) + .texture_options(TextureOptions::NEAREST); + ui.add(image); + } +} + +impl AppWindow for WorldWindow { + fn viewport_id(&self) -> ViewportId { + ViewportId::from_hash_of(format!("world-{}", self.sim_id)) + } + + fn sim_id(&self) -> SimId { + self.sim_id + } + + fn initial_viewport(&self) -> ViewportBuilder { + ViewportBuilder::default() + .with_title(format!("Worlds ({})", self.sim_id)) + .with_inner_size((640.0, 480.0)) + } + + fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) { + ctx.add_texture_loader(self.loader.clone()); + } + + fn show(&mut self, ctx: &Context) { + CentralPanel::default().show(ctx, |ui| { + ui.horizontal_top(|ui| { + StripBuilder::new(ui) + .size(Size::relative(0.3)) + .size(Size::remainder()) + .horizontal(|mut strip| { + strip.cell(|ui| { + ScrollArea::vertical().show(ui, |ui| self.show_form(ui)); + }); + strip.cell(|ui| { + ScrollArea::both().show(ui, |ui| self.show_world(ui)); + }); + }); + }); + }); + } +} + +#[derive(Clone, PartialEq, Eq)] +struct WorldParams { + index: usize, + generic_palette: bool, + left_color: Color32, + right_color: Color32, +} + +struct WorldRenderer { + chardata: MemoryView, + objects: MemoryView, + worlds: MemoryView, + brightness: MemoryView, + palettes: MemoryView, + scr: MemoryView, + // an object world could update the same pixel more than once, + // so we can't add the left/right eye color to the output buffer directly. + buffer: Box<[[u8; 2]; 384 * 224]>, +} + +impl WorldRenderer { + pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self { + Self { + chardata: memory.watch(sim_id, 0x00078000, 0x8000), + objects: memory.watch(sim_id, 0x0003e000, 0x2000), + worlds: memory.watch(sim_id, 0x0003d800, 0x400), + brightness: memory.watch(sim_id, 0x0005f824, 8), + palettes: memory.watch(sim_id, 0x0005f860, 16), + scr: memory.watch(sim_id, 0x0005f848, 8), + + buffer: vec![[0, 0]; 384 * 224] + .into_boxed_slice() + .try_into() + .unwrap(), + } + } + + fn render_object_world(&mut self, group: usize, params: &WorldParams, image: &mut VramImage) { + for cell in self.buffer.iter_mut() { + *cell = [0, 0]; + } + + let palettes = { + let palettes = self.palettes.borrow().read::<[u8; 8]>(1); + [ + utils::parse_palette(palettes[0]), + utils::parse_palette(palettes[2]), + utils::parse_palette(palettes[4]), + utils::parse_palette(palettes[6]), + ] + }; + let chardata = self.chardata.borrow(); + let objects = self.objects.borrow(); + + let (first_range, second_range) = { + let scr = self.scr.borrow(); + let start = if group == 0 { + 0 + } else { + scr.read::(group - 1).wrapping_add(1) as usize & 0x03ff + }; + let end = scr.read::(group).wrapping_add(1) as usize & 0x03ff; + if start > end { + ((start, 1024 - start), (end != 0).then_some((0, end))) + } else { + ((start, end - start), None) + } + }; + + // Fill the buffer + for object in std::iter::once(first_range) + .chain(second_range) + .flat_map(|(start, count)| objects.range(start, count)) + .rev() + { + let obj = Object::parse(object); + if !obj.lon && !obj.ron { + continue; + } + + let char = chardata.read::<[u16; 8]>(obj.data.char_index); + let palette = &palettes[obj.data.palette_index]; + + for row in 0..8 { + let y = obj.y + row as i16; + if !(0..224).contains(&y) { + continue; + } + for (col, pixel) in + utils::read_char_row(&char, obj.data.hflip, obj.data.vflip, row).enumerate() + { + if pixel == 0 { + // transparent + continue; + } + let lx = obj.x + col as i16 - obj.parallax; + if obj.lon && (0..384).contains(&lx) { + let index = (y as usize) * 384 + lx as usize; + self.buffer[index][0] = palette[pixel as usize]; + } + let rx = obj.x + col as i16 + obj.parallax; + if obj.ron & (0..384).contains(&rx) { + let index = (y as usize) * 384 + rx as usize; + self.buffer[index][1] = palette[pixel as usize]; + } + } + } + } + + let colors = if params.generic_palette { + [ + utils::generic_palette(params.left_color), + utils::generic_palette(params.right_color), + ] + } else { + let brts = self.brightness.borrow().read::<[u8; 8]>(0); + let shades = utils::parse_shades(&brts); + [ + shades.map(|s| shade(s, params.left_color)), + shades.map(|s| shade(s, params.right_color)), + ] + }; + + for (dst, shades) in image.pixels.iter_mut().zip(self.buffer.iter()) { + let left = colors[0][shades[0] as usize]; + let right = colors[1][shades[1] as usize]; + *dst = Color32::from_rgb( + left.r() + right.r(), + left.g() + right.g(), + left.b() + right.b(), + ) + } + } +} + +impl VramRenderer<1> for WorldRenderer { + type Params = WorldParams; + + fn sizes(&self) -> [[usize; 2]; 1] { + [[384, 224]] + } + + fn render(&mut self, params: &Self::Params, images: &mut [VramImage; 1]) { + let image = &mut images[0]; + + let worlds = self.worlds.borrow(); + let header = WorldHeader::parse(worlds.read(params.index * 16)); + if header.end || (!header.lon && header.ron) { + image.clear(); + return; + } + + if header.mode == WorldMode::Object { + let mut group = 3usize; + for world in params.index + 1..32 { + let header = WorldHeader::parse(worlds.read(world * 16)); + if header.mode == WorldMode::Object && (header.lon || header.ron) { + group = group.checked_sub(1).unwrap_or(3); + } + } + drop(worlds); + self.render_object_world(group, params, image); + } + } +} + +struct World { + header: WorldHeader, +} + +impl World { + pub fn parse(data: &[u16; 16]) -> Self { + Self { + header: WorldHeader::parse(data[0]), + } + } +} + +struct WorldHeader { + lon: bool, + ron: bool, + mode: WorldMode, + over: bool, + end: bool, +} + +impl WorldHeader { + fn parse(data: u16) -> Self { + let lon = data & 0x8000 != 0; + let ron = data & 0x4000 != 0; + let mode = WorldMode::from_u16((data >> 12) & 0x3).unwrap(); + let over = data & 0x0080 != 0; + let end = data & 0x0040 != 0; + Self { + lon, + ron, + mode, + over, + end, + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] +enum WorldMode { + Normal = 0, + HBias = 1, + Affine = 2, + Object = 3, +} + +impl WorldMode { + fn values() -> [Self; 4] { + [Self::Normal, Self::HBias, Self::Affine, Self::Object] + } +} + +impl Display for WorldMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Normal => "Normal", + Self::HBias => "H-bias", + Self::Affine => "Affine", + Self::Object => "Object", + }) + } +}