diff --git a/Cargo.lock b/Cargo.lock index afbb6ef..2e76f54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,6 +427,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + [[package]] name = "backtrace" version = "0.3.74" @@ -451,7 +457,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -834,6 +840,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "cursor-icon" version = "1.1.0" @@ -1166,6 +1178,19 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fixed" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c6e0b89bf864acd20590dbdbad56f69aeb898abfc9443008fd7bd48b2cc85a" +dependencies = [ + "az", + "bytemuck", + "half", + "num-traits", + "typenum", +] + [[package]] name = "flate2" version = "1.0.35" @@ -1443,6 +1468,16 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -1691,6 +1726,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.14" @@ -1779,10 +1823,11 @@ dependencies = [ "egui-wgpu", "egui-winit", "egui_extras", + "fixed", "gilrs", "hex", "image", - "itertools", + "itertools 0.14.0", "num-derive", "num-traits", "oneshot", @@ -3372,6 +3417,12 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "uds_windows" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index cd55b09..64eab6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,10 +21,11 @@ egui_extras = { version = "0.30", features = ["image"] } egui-toast = { git = "https://github.com/urholaukkarinen/egui-toast.git", rev = "d0bcf97" } egui-winit = "0.30" egui-wgpu = { version = "0.30", features = ["winit"] } +fixed = { version = "1.28", features = ["num-traits"] } gilrs = { version = "0.11", features = ["serde-serialize"] } hex = "0.4" image = { version = "0.25", default-features = false, features = ["png"] } -itertools = "0.13" +itertools = "0.14" num-derive = "0.4" num-traits = "0.2" oneshot = "0.1" diff --git a/src/window/utils.rs b/src/window/utils.rs index d53f6b4..7aab7b7 100644 --- a/src/window/utils.rs +++ b/src/window/utils.rs @@ -9,7 +9,7 @@ use egui::{ Response, RichText, Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText, }; -use num_traits::PrimInt; +use num_traits::{CheckedAdd, CheckedSub, One}; pub trait UiExt { fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)); @@ -97,12 +97,20 @@ enum Direction { Down, } -pub trait Number: PrimInt + Display + FromStr + Send + Sync + 'static {} -impl Number for T {} +pub trait Number: + Copy + One + CheckedAdd + CheckedSub + Eq + Ord + Display + FromStr + Send + Sync + 'static +{ +} +impl< + T: Copy + One + CheckedAdd + CheckedSub + Eq + Ord + Display + FromStr + Send + Sync + 'static, + > Number for T +{ +} pub struct NumberEdit<'a, T: Number> { value: &'a mut T, increment: T, + precision: usize, min: Option, max: Option, } @@ -112,11 +120,16 @@ impl<'a, T: Number> NumberEdit<'a, T> { Self { value, increment: T::one(), + precision: 3, min: None, max: None, } } + pub fn precision(self, precision: usize) -> Self { + Self { precision, ..self } + } + pub fn range(self, range: impl RangeBounds) -> Self { let min = match range.start_bound() { Bound::Unbounded => None, @@ -135,18 +148,19 @@ impl<'a, T: Number> NumberEdit<'a, T> { impl Widget for NumberEdit<'_, T> { fn ui(self, ui: &mut Ui) -> Response { let id = ui.id(); + let to_string = |val: &T| format!("{val:.0$}", self.precision); let (last_value, mut str, focus) = ui.memory(|m| { let (lv, s) = m .data .get_temp(id) - .unwrap_or((*self.value, self.value.to_string())); + .unwrap_or((*self.value, to_string(self.value))); let focus = m.has_focus(id); (lv, s, focus) }); let mut stale = false; if *self.value != last_value { - str = self.value.to_string(); + str = to_string(self.value); stale = true; } let valid = str.parse().is_ok_and(|v: T| v == *self.value); @@ -236,7 +250,7 @@ impl Widget for NumberEdit<'_, T> { if let Some(new_value) = value.filter(in_range) { *self.value = new_value; } - str = self.value.to_string(); + str = to_string(self.value); stale = true; } else if res.changed { if let Some(new_value) = str.parse().ok().filter(in_range) { diff --git a/src/window/vram/world.rs b/src/window/vram/world.rs index f9e9a61..4c0df76 100644 --- a/src/window/vram/world.rs +++ b/src/window/vram/world.rs @@ -5,6 +5,10 @@ use egui::{ TextureOptions, Ui, ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; +use fixed::{ + types::extra::{U3, U9}, + FixedI32, +}; use num_derive::{FromPrimitive, ToPrimitive}; use num_traits::FromPrimitive; @@ -277,10 +281,11 @@ impl WorldWindow { ui.label("Index"); }); row.col(|ui| { - ui.add(NumberEdit::new(&mut self.param_index).range(0..32)); + let max = world.height.max(8) as usize; + ui.add(NumberEdit::new(&mut self.param_index).range(0..max)); }); }); - let base = world.param_base + self.param_index * 2; + let base = (world.param_base + self.param_index * 2) & 0x1ffff; let mut param = HBiasParam::load(&self.bgmaps.borrow(), base); body.row(row_height, |mut row| { row.col(|ui| { @@ -314,6 +319,79 @@ impl WorldWindow { }); }); }); + } else if world.header.mode == WorldMode::Affine { + ui.section("Affine", |ui| { + TableBuilder::new(ui) + .column(Column::remainder()) + .column(Column::remainder()) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Index"); + }); + row.col(|ui| { + let max = world.height.max(1) as usize; + ui.add(NumberEdit::new(&mut self.param_index).range(0..max)); + }); + }); + let base = (world.param_base + self.param_index * 8) & 0x1ffff; + let mut param = AffineParam::load(&self.bgmaps.borrow(), base); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Address"); + }); + row.col(|ui| { + let address = 0x00020000 + base * 2; + let mut address_str = format!("{address:08x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut address_str) + .horizontal_align(Align::Max), + ); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Src X"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut param.src_x).precision(3)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Src Y"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut param.src_y).precision(3)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Src parallax"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut param.src_parallax)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Delta X"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut param.dx).precision(9)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Delta Y"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut param.dy).precision(9)); + }); + }); + }); + }); } else { self.param_index = 0; } @@ -812,14 +890,19 @@ impl<'a> SourceCoordCalculator<'a> { fn left(&mut self, x: i16, y: i16) -> (i16, i16) { self.update_param(y); match &self.param { + SourceParam::Normal => { + let sx = x + self.world.src_x - self.world.src_parallax; + let sy = y + self.world.src_y; + (sx, sy) + } SourceParam::HBias(HBiasParam { left, .. }) => { let sx = x + self.world.src_x - self.world.src_parallax + *left; let sy = y + self.world.src_y; (sx, sy) } - SourceParam::Normal => { - let sx = x + self.world.src_x - self.world.src_parallax; - let sy = y + self.world.src_y; + SourceParam::Affine(affine) => { + let sx = affine_coord(affine.src_x, x, affine.dx, affine.src_parallax.min(0).abs()); + let sy = affine_coord(affine.src_y, x, affine.dy, affine.src_parallax.min(0).abs()); (sx, sy) } } @@ -828,14 +911,19 @@ impl<'a> SourceCoordCalculator<'a> { fn right(&mut self, x: i16, y: i16) -> (i16, i16) { self.update_param(y); match &self.param { + SourceParam::Normal => { + let sx = x + self.world.src_x + self.world.src_parallax; + let sy = y + self.world.src_y; + (sx, sy) + } SourceParam::HBias(HBiasParam { right, .. }) => { let sx = x + self.world.src_x + self.world.src_parallax + *right; let sy = y + self.world.src_y; (sx, sy) } - SourceParam::Normal => { - let sx = x + self.world.src_x + self.world.src_parallax; - let sy = y + self.world.src_y; + SourceParam::Affine(affine) => { + let sx = affine_coord(affine.src_x, x, affine.dx, -affine.src_parallax.max(0)); + let sy = affine_coord(affine.src_y, x, affine.dy, -affine.src_parallax.max(0)); (sx, sy) } } @@ -849,6 +937,10 @@ impl<'a> SourceCoordCalculator<'a> { let base = self.world.param_base + (2 * y as usize); self.param = SourceParam::HBias(HBiasParam::load(self.params, base)); } + if self.world.header.mode == WorldMode::Affine { + let base = self.world.param_base + (8 * y as usize); + self.param = SourceParam::Affine(AffineParam::load(self.params, base)); + } self.y = y; } } @@ -856,6 +948,7 @@ impl<'a> SourceCoordCalculator<'a> { enum SourceParam { Normal, HBias(HBiasParam), + Affine(AffineParam), } struct HBiasParam { @@ -870,3 +963,45 @@ impl HBiasParam { Self { left, right } } } + +struct AffineParam { + src_x: FixedI32, + src_parallax: i16, + src_y: FixedI32, + dx: FixedI32, + dy: FixedI32, +} + +impl AffineParam { + fn load(params: &MemoryRef, index: usize) -> Self { + let src_x = params.read::(index & 0x1ffff); + let src_x = FixedI32::from_bits(src_x as i32); + + let src_parallax = params.read::((index + 1) & 0x1ffff); + + let src_y = params.read::((index + 2) & 0x1ffff); + let src_y = FixedI32::from_bits(src_y as i32); + + let dx = params.read::((index + 3) & 0x1ffff); + let dx = FixedI32::from_bits(dx as i32); + + let dy = params.read::((index + 4) & 0x1ffff); + let dy = FixedI32::from_bits(dy as i32); + + AffineParam { + src_x, + src_parallax, + src_y, + dx, + dy, + } + } +} + +fn affine_coord(start: FixedI32, distance: i16, delta: FixedI32, parallax: i16) -> i16 { + let start = FixedI32::::from_num(start); + let distance = FixedI32::::from_num(distance); + let parallax = FixedI32::::from_num(parallax); + let coord = start + ((distance + parallax) * delta); + coord.to_num::() as i16 +}