use std::sync::Arc; use egui::{ Align, CentralPanel, Checkbox, Color32, ComboBox, Image, ScrollArea, Slider, TextEdit, TextureOptions, Ui, ViewportBuilder, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use serde::{Deserialize, Serialize}; use crate::{ emulator::SimId, images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader}, memory::{MemoryClient, MemoryView}, window::{ AppWindow, utils::{NumberEdit, UiData, UiExt as _}, }, }; use super::utils::{self, Object}; #[derive(Clone, PartialEq, Serialize, Deserialize)] struct State { index: usize, generic_palette: bool, scale: f32, } impl Default for State { fn default() -> Self { Self { index: 0, generic_palette: false, scale: 1.0, } } } pub struct ObjectWindow { sim_id: SimId, memory: Arc, objects: MemoryView, params: ImageParams, state: UiData, } impl ObjectWindow { pub fn new(sim_id: SimId, memory: &Arc, images: &ImageTextureLoader) -> Self { let state: UiData = UiData::new(); let renderer = ObjectRenderer::new(sim_id, memory); let params = images.add( sim_id, renderer, ObjectParams { index: state.index, generic_palette: state.generic_palette, left_color: Color32::from_rgb(0xff, 0x00, 0x00), right_color: Color32::from_rgb(0x00, 0xc6, 0xf0), }, ); Self { sim_id, memory: memory.clone(), objects: memory.watch(sim_id, 0x0003e000, 0x2000), params, state, } } 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.state.index).range(0..1024)); }); }); body.row(row_height, |mut row| { row.col(|ui| { ui.label("Address"); }); row.col(|ui| { let address = 0x3e000 + self.state.index * 8; let mut address_str = format!("{address:08x}"); ui.add_enabled( false, TextEdit::singleline(&mut address_str).horizontal_align(Align::Max), ); }); }); }); let image = Image::new(self.image_url("object-zoom")) .maintain_aspect_ratio(true) .texture_options(TextureOptions::NEAREST); ui.add(image); ui.section("Properties", |ui| { let mut object = self.objects.borrow().read::<[u16; 4]>(self.state.index); let mut obj = Object::parse(object); TableBuilder::new(ui) .column(Column::remainder()) .column(Column::remainder()) .body(|mut body| { body.row(row_height, |mut row| { row.col(|ui| { ui.label("Character"); }); row.col(|ui| { ui.add(NumberEdit::new(&mut obj.data.char_index).range(0..2048)); }); }); body.row(row_height, |mut row| { row.col(|ui| { ui.label("Palette"); }); row.col(|ui| { ComboBox::from_id_salt("palette") .selected_text(format!("OBJ {}", obj.data.palette_index)) .width(ui.available_width()) .show_ui(ui, |ui| { for palette in 0..4 { ui.selectable_value( &mut obj.data.palette_index, palette, format!("OBJ {palette}"), ); } }); }); }); body.row(row_height, |mut row| { row.col(|ui| { ui.label("X"); }); row.col(|ui| { ui.add(NumberEdit::new(&mut obj.x).range(-512..512)); }); }); body.row(row_height, |mut row| { row.col(|ui| { ui.label("Y"); }); row.col(|ui| { ui.add(NumberEdit::new(&mut obj.y).range(-8..=224)); }); }); body.row(row_height, |mut row| { row.col(|ui| { ui.label("Parallax"); }); row.col(|ui| { ui.add(NumberEdit::new(&mut obj.parallax).range(-512..512)); }); }); body.row(row_height, |mut row| { row.col(|ui| { ui.add(Checkbox::new(&mut obj.data.hflip, "H-flip")); }); row.col(|ui| { ui.add(Checkbox::new(&mut obj.data.vflip, "V-flip")); }); }); body.row(row_height, |mut row| { row.col(|ui| { ui.add(Checkbox::new(&mut obj.lon, "Left")); }); row.col(|ui| { ui.add(Checkbox::new(&mut obj.ron, "Right")); }); }); }); if obj.update(&mut object) { let address = 0x3e000 + self.state.index * 8; self.memory.write(self.sim_id, address as u32, &object); } }); ui.section("Display", |ui| { ui.horizontal(|ui| { ui.label("Scale"); ui.spacing_mut().slider_width = ui.available_width(); let slider = Slider::new(&mut self.state.scale, 1.0..=10.0) .step_by(1.0) .show_value(false); ui.add(slider); }); ui.checkbox(&mut self.state.generic_palette, "Generic palette"); }); }); self.params.write(ObjectParams { index: self.state.index, generic_palette: self.state.generic_palette, ..*self.params }); } fn show_object(&mut self, ui: &mut Ui) { let image = Image::new(self.image_url("object-full")) .fit_to_original_size(self.state.scale) .texture_options(TextureOptions::NEAREST); ui.add(image); } } impl AppWindow for ObjectWindow { fn sim_id(&self) -> SimId { self.sim_id } fn initial_viewport(&self) -> ViewportBuilder { ViewportBuilder::default() .with_title(format!("Object Data ({})", self.sim_id)) .with_inner_size((640.0, 500.0)) } fn show(&mut self, ui: &mut Ui) { self.state.load(ui); CentralPanel::default().show_inside(ui, |ui| { ui.horizontal_top(|ui| { StripBuilder::new(ui) .size(Size::relative(0.3).at_most(200.0)) .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_object(ui)); }); }); }); }); self.state.save(ui); } } #[derive(Clone, PartialEq, Eq)] struct ObjectParams { index: usize, generic_palette: bool, left_color: Color32, right_color: Color32, } enum Eye { Left, Right, } struct ObjectRenderer { chardata: MemoryView, objects: MemoryView, brightness: MemoryView, palettes: MemoryView, } impl ObjectRenderer { pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self { Self { chardata: memory.watch(sim_id, 0x00078000, 0x8000), objects: memory.watch(sim_id, 0x0003e000, 0x2000), brightness: memory.watch(sim_id, 0x0005f824, 8), palettes: memory.watch(sim_id, 0x0005f860, 16), } } fn render_object( &self, image: &mut ImageBuffer, params: &ObjectParams, use_pos: bool, eye: Eye, ) { let chardata = self.chardata.borrow(); let objects = self.objects.borrow(); let brightness = self.brightness.borrow(); let palettes = self.palettes.borrow(); let object: [u16; 4] = objects.read(params.index); let obj = Object::parse(object); if match eye { Eye::Left => !obj.lon, Eye::Right => !obj.ron, } { return; } let brts = brightness.read::<[u8; 8]>(0); let (x, y) = if use_pos { let x = match eye { Eye::Left => obj.x - obj.parallax, Eye::Right => obj.x + obj.parallax, }; (x, obj.y) } else { (0, 0) }; let color = match eye { Eye::Left => params.left_color, Eye::Right => params.right_color, }; let char = chardata.read::<[u16; 8]>(obj.data.char_index); let palette = if params.generic_palette { utils::generic_palette(color) } else { utils::palette_colors(palettes.read(8 + obj.data.palette_index * 2), &brts, color) }; for row in 0..8 { let real_y = y + row as i16; if !(0..224).contains(&real_y) { continue; } for (col, pixel) in utils::read_char_row(&char, obj.data.hflip, obj.data.vflip, row).enumerate() { let real_x = x + col as i16; if !(0..384).contains(&real_x) { continue; } image.add((real_x as usize, real_y as usize), palette[pixel as usize]); } } } } impl ImageRenderer<2> for ObjectRenderer { type Params = ObjectParams; fn names(&self) -> [&str; 2] { ["object-zoom", "object-full"] } fn sizes(&self) -> [[usize; 2]; 2] { [[8, 8], [384, 224]] } fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 2]) { images[0].clear(); self.render_object(&mut images[0], params, false, Eye::Left); self.render_object(&mut images[0], params, false, Eye::Right); images[1].clear(); self.render_object(&mut images[1], params, true, Eye::Left); self.render_object(&mut images[1], params, true, Eye::Right); } }