From 38e682644052ae4d5897459b8b2495d9519df0a9 Mon Sep 17 00:00:00 2001 From: gered Date: Sun, 15 May 2022 12:11:38 -0400 Subject: [PATCH] initial commit --- .gitignore | 3 + Cargo.toml | 5 + examples/.keep | 0 libretrogd/Cargo.toml | 32 + libretrogd/assets/vga.fnt | Bin 0 -> 2305 bytes libretrogd/assets/vga.pal | Bin 0 -> 768 bytes libretrogd/benches/bitmap.rs | 28 + libretrogd/benches/blit.rs | 62 + libretrogd/src/entities/mod.rs | 703 +++++++++ libretrogd/src/events/mod.rs | 291 ++++ libretrogd/src/graphics/bitmap/blit.rs | 342 +++++ libretrogd/src/graphics/bitmap/iff.rs | 620 ++++++++ libretrogd/src/graphics/bitmap/mod.rs | 584 ++++++++ libretrogd/src/graphics/bitmap/pcx.rs | 322 +++++ libretrogd/src/graphics/bitmap/primitives.rs | 378 +++++ libretrogd/src/graphics/bitmapatlas.rs | 178 +++ libretrogd/src/graphics/font.rs | 202 +++ libretrogd/src/graphics/mod.rs | 4 + libretrogd/src/graphics/palette.rs | 563 ++++++++ libretrogd/src/lib.rs | 68 + libretrogd/src/math/circle.rs | 113 ++ libretrogd/src/math/matrix3x3.rs | 420 ++++++ libretrogd/src/math/mod.rs | 272 ++++ libretrogd/src/math/rect.rs | 284 ++++ libretrogd/src/math/vector2.rs | 479 +++++++ libretrogd/src/states/mod.rs | 1264 +++++++++++++++++ .../src/system/input_devices/keyboard.rs | 93 ++ libretrogd/src/system/input_devices/mod.rs | 27 + libretrogd/src/system/input_devices/mouse.rs | 318 +++++ libretrogd/src/system/mod.rs | 400 ++++++ libretrogd/src/utils/bytes.rs | 12 + libretrogd/src/utils/mod.rs | 38 + libretrogd/src/utils/packbits.rs | 226 +++ libretrogd/test-assets/dp2.pal | Bin 0 -> 768 bytes libretrogd/test-assets/test-tiles.lbm | Bin 0 -> 28790 bytes libretrogd/test-assets/test.pcx | Bin 0 -> 1099 bytes .../test-assets/test_bmp_pixels_raw.bin | 1 + libretrogd/test-assets/test_ilbm.lbm | Bin 0 -> 1722 bytes libretrogd/test-assets/test_image.lbm | Bin 0 -> 50416 bytes libretrogd/test-assets/test_image.pcx | Bin 0 -> 17260 bytes libretrogd/test-assets/test_pbm.lbm | Bin 0 -> 1722 bytes 41 files changed, 8332 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 examples/.keep create mode 100644 libretrogd/Cargo.toml create mode 100644 libretrogd/assets/vga.fnt create mode 100644 libretrogd/assets/vga.pal create mode 100644 libretrogd/benches/bitmap.rs create mode 100644 libretrogd/benches/blit.rs create mode 100644 libretrogd/src/entities/mod.rs create mode 100644 libretrogd/src/events/mod.rs create mode 100644 libretrogd/src/graphics/bitmap/blit.rs create mode 100644 libretrogd/src/graphics/bitmap/iff.rs create mode 100644 libretrogd/src/graphics/bitmap/mod.rs create mode 100644 libretrogd/src/graphics/bitmap/pcx.rs create mode 100644 libretrogd/src/graphics/bitmap/primitives.rs create mode 100644 libretrogd/src/graphics/bitmapatlas.rs create mode 100644 libretrogd/src/graphics/font.rs create mode 100644 libretrogd/src/graphics/mod.rs create mode 100644 libretrogd/src/graphics/palette.rs create mode 100644 libretrogd/src/lib.rs create mode 100644 libretrogd/src/math/circle.rs create mode 100644 libretrogd/src/math/matrix3x3.rs create mode 100644 libretrogd/src/math/mod.rs create mode 100644 libretrogd/src/math/rect.rs create mode 100644 libretrogd/src/math/vector2.rs create mode 100644 libretrogd/src/states/mod.rs create mode 100644 libretrogd/src/system/input_devices/keyboard.rs create mode 100644 libretrogd/src/system/input_devices/mod.rs create mode 100644 libretrogd/src/system/input_devices/mouse.rs create mode 100644 libretrogd/src/system/mod.rs create mode 100644 libretrogd/src/utils/bytes.rs create mode 100644 libretrogd/src/utils/mod.rs create mode 100644 libretrogd/src/utils/packbits.rs create mode 100644 libretrogd/test-assets/dp2.pal create mode 100644 libretrogd/test-assets/test-tiles.lbm create mode 100755 libretrogd/test-assets/test.pcx create mode 100644 libretrogd/test-assets/test_bmp_pixels_raw.bin create mode 100755 libretrogd/test-assets/test_ilbm.lbm create mode 100644 libretrogd/test-assets/test_image.lbm create mode 100644 libretrogd/test-assets/test_image.pcx create mode 100755 libretrogd/test-assets/test_pbm.lbm diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16d5636 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +.DS_Store diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8e8530e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = [ + "libretrogd", + "examples/*", +] diff --git a/examples/.keep b/examples/.keep new file mode 100644 index 0000000..e69de29 diff --git a/libretrogd/Cargo.toml b/libretrogd/Cargo.toml new file mode 100644 index 0000000..8ecc1ed --- /dev/null +++ b/libretrogd/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "libretrogd" +description = "A 'retro'-like game development library, for funsies." +version = "0.1.0" +authors = ["Gered King "] +edition = "2021" + +[features] +low_res = [] +wide = [] + +[dependencies] +sdl2 = { version = "0.34.5", features = ["static-link", "bundled", "unsafe_textures" ] } +byte-slice-cast = "1.2.1" +byteorder = "1.4.3" +thiserror = "1.0.30" +rand = "0.8.5" +num-traits = "0.2.14" + +[dev-dependencies] +claim = "0.5.0" +criterion = "0.3.5" +anyhow = "1.0.55" +tempfile = "3.3.0" + +[[bench]] +name = "bitmap" +harness = false + +[[bench]] +name = "blit" +harness = false diff --git a/libretrogd/assets/vga.fnt b/libretrogd/assets/vga.fnt new file mode 100644 index 0000000000000000000000000000000000000000..b32b4e5ce15f3e32f975938092a649db0ac492ab GIT binary patch literal 2305 zcmd^A&1xe@5H1{0au{qM8!WsK_5m6k^lU)o*KM#4(Sk`L_;QHJM1BkuzrAC z@&vgUh6si|G~SzIkT5#QmyPjB9>Oq_@9P;Sd54skuCD6p`s=EaNDsxuFBeyr7e&!6 zyTyH1xVCNcbYJ$e{Q~lxWcJl7n@P90yt=wvbbFuf{TM>< znYA`kg*rJp(R%i{K07@-J^f}=UcS!m@3YsJ=j*zy*XOdcJ8h@CJJ4DvRb&d+tQ9L~ z%jFLT2M01eO`S`Fr?*8>L^+6cQC=xg0MKwNX%isPrZt+603Ys-U#3Bz5V073N}08Re8V#%Lw@6!s!E>oSWYts5Qf z$2|_D!Pf&0MhT?LbIGggyX__yJ=1_jd`MCgdYmBKappps97P1Hn+CHb0Tke&)VS~TrGt$ z-k4&*CZ1p0so;%qEr_T4+RI@AH{s95I`8ZTc_I{2J+$pjRY|qk-dzKyuBs}RMtjCQ z(*LmX<4E(K59`^OaYntGb^uL}@lv>b(>_2Slt35xaKTF(6Wa=$1$bfb$!JGB3q*O; zGlc(zPRHDk;gcNix;6&!uSkay_Qwg>;FE5Ur_`B8`YSSL#_P3la|?g9%wqhB^a)rS zCSVh}^h+c#0l+l_iTao)krgsdg;C!j=(&yhW8ulZ&b9u6@5gKO0|@b>AaB~1wTcA9 zkidsOc;|QeO(SEi*IJL+7x5zjmdqI%fr+SEynk8c$tagV#d8@CaTXWZXllO%d&tXx z5lhUI@TN~v$l1bVl#BUU4>%0@$bU-VLMH2huJ-e)#rwz7Z365yC@i+dS#lQ6WQuYR z;adD#C}Mu7n3#%z!SD019x(kz+zXX)(DY$I4%474F-lVR=Fx0eHy#byXk`Dd>O-~I zJy4law=={W<4FhqVghJyJm|0GHyA|ohL6oENaoyB3PBy9cyo*sSkmYdKvR}P(j*DT z#qH$V-{0;ns<1r%?!zaPiv#n>273f4tQKYP`Op3Dt6%%ya=ZL~S^V)QlJ*bOaaSwi z31EwlhT4dq`P&is&w$f`-i|o_$R}3Nf+C5~>s9JSC+SKth1N3rS IKl{JK-{Dcz;{X5v literal 0 HcmV?d00001 diff --git a/libretrogd/assets/vga.pal b/libretrogd/assets/vga.pal new file mode 100644 index 0000000000000000000000000000000000000000..8bbb15f496cb8334dfe0409a5c1403bab1af1d48 GIT binary patch literal 768 zcmZvaLCZoR5JhKEB>9@=fri8dTLeWQk6P5D&HVp=^*ODS&zXxO=cp@*YCUSx<6%o3 z(YDb_Ye!XEexq*d>Fsvwob%o@^E}TXgp?A1%jI&tUhnsNDdqKgt?T+buR@wh7pj+Q z7Nx8t^`TZYbS!e#Th=A(C#zaMnitJC?L|Y!q7;1=eHZ-}Rm)e)Tgy{>(a^CV!j8a% zNPs1R7_>t#8afutm>H3g7_cO81kdP2L&qWnoB~q7IRKV3LP9v97Y!W?BH55Ql8Hnt zlaLeING}>X7S74u$>fBch=s*X;`Y*uhK_~zGI=?8IeHPxQT##tB)w?pSlBoJSM`7X E0dY-B3IG5A literal 0 HcmV?d00001 diff --git a/libretrogd/benches/bitmap.rs b/libretrogd/benches/bitmap.rs new file mode 100644 index 0000000..2194052 --- /dev/null +++ b/libretrogd/benches/bitmap.rs @@ -0,0 +1,28 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +use libretrogd::Bitmap; +use libretrogd::Palette; +use libretrogd::{SCREEN_HEIGHT, SCREEN_WIDTH}; + +pub fn criterion_benchmark(c: &mut Criterion) { + let mut source = Bitmap::new(SCREEN_WIDTH, SCREEN_HEIGHT).unwrap(); + let mut dest = vec![0u32; (SCREEN_WIDTH * SCREEN_HEIGHT * 4) as usize].into_boxed_slice(); + let palette = Palette::new_vga_palette().unwrap(); + + c.bench_function("deindex_bitmap_pixels", |b| { + b.iter(|| source.copy_as_argb_to(&mut dest, &palette)) + }); + + c.bench_function("set_pixel", |b| { + b.iter(|| source.set_pixel(black_box(100), black_box(100), black_box(42))) + }); + + c.bench_function("set_pixel_unchecked", |b| { + b.iter(|| unsafe { + source.set_pixel_unchecked(black_box(100), black_box(100), black_box(42)) + }) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/libretrogd/benches/blit.rs b/libretrogd/benches/blit.rs new file mode 100644 index 0000000..63f74e0 --- /dev/null +++ b/libretrogd/benches/blit.rs @@ -0,0 +1,62 @@ +use std::path::Path; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +use libretrogd::{Bitmap, BlitMethod, Rect}; + +pub fn criterion_benchmark(c: &mut Criterion) { + let mut framebuffer = Bitmap::new(320, 240).unwrap(); + let (bmp, _) = Bitmap::load_iff_file(Path::new("./test-assets/test-tiles.lbm")).unwrap(); + + let mut solid_bmp = Bitmap::new(16, 16).unwrap(); + solid_bmp.blit_region(BlitMethod::Solid, &bmp, &Rect::new(16, 16, 16, 16), 0, 0); + let mut trans_bmp = Bitmap::new(16, 16).unwrap(); + trans_bmp.blit_region(BlitMethod::Solid, &bmp, &Rect::new(160, 0, 16, 16), 0, 0); + + c.bench_function("blit_single_checked_solid", |b| { + b.iter(|| { + framebuffer.blit( + black_box(BlitMethod::Solid), + black_box(&solid_bmp), + black_box(100), + black_box(100), + ) + }) + }); + + c.bench_function("blit_single_unchecked_solid", |b| { + b.iter(|| unsafe { + framebuffer.blit_unchecked( + black_box(BlitMethod::Solid), + black_box(&solid_bmp), + black_box(100), + black_box(100), + ) + }) + }); + + c.bench_function("blit_single_checked_transparent", |b| { + b.iter(|| { + framebuffer.blit( + black_box(BlitMethod::Transparent(0)), + black_box(&trans_bmp), + black_box(100), + black_box(100), + ) + }) + }); + + c.bench_function("blit_single_unchecked_transparent", |b| { + b.iter(|| unsafe { + framebuffer.blit_unchecked( + black_box(BlitMethod::Transparent(0)), + black_box(&trans_bmp), + black_box(100), + black_box(100), + ) + }) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/libretrogd/src/entities/mod.rs b/libretrogd/src/entities/mod.rs new file mode 100644 index 0000000..90ba3f8 --- /dev/null +++ b/libretrogd/src/entities/mod.rs @@ -0,0 +1,703 @@ +use std::any::{TypeId}; +use std::cell::{Ref, RefCell, RefMut}; +use std::collections::{HashMap, HashSet}; + +use crate::utils::AsAny; + +pub type EntityId = usize; + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// alias `Component` to always be `'static` ... +pub trait Component: 'static {} +impl Component for T {} + +pub type ComponentStore = RefCell>; +pub type RefComponents<'a, T> = Ref<'a, HashMap>; +pub type RefMutComponents<'a, T> = RefMut<'a, HashMap>; + +pub trait GenericComponentStore: AsAny { + fn has(&self, entity: EntityId) -> bool; + fn remove(&mut self, entity: EntityId) -> bool; + fn clear(&mut self); +} + +impl GenericComponentStore for ComponentStore { + #[inline] + fn has(&self, entity: EntityId) -> bool { + self.borrow().contains_key(&entity) + } + + #[inline] + fn remove(&mut self, entity: EntityId) -> bool { + self.get_mut().remove(&entity).is_some() + } + + #[inline] + fn clear(&mut self) { + self.get_mut().clear(); + } +} + +#[inline] +pub fn as_component_store( + collection: &dyn GenericComponentStore, +) -> &ComponentStore { + collection.as_any().downcast_ref().unwrap() +} + +#[inline] +pub fn as_component_store_mut( + collection: &mut dyn GenericComponentStore, +) -> &mut ComponentStore { + collection.as_any_mut().downcast_mut().unwrap() +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +pub struct Entities { + entities: HashSet, + component_stores: HashMap>, + next_id: EntityId, +} + +impl Entities { + pub fn new() -> Self { + Entities { + entities: HashSet::new(), + component_stores: HashMap::new(), + next_id: 0, + } + } + + #[inline] + fn has_component_store(&self) -> bool { + let type_id = TypeId::of::(); + self.component_stores.contains_key(&type_id) + } + + fn get_component_store(&self) -> Option<&ComponentStore> { + if !self.has_component_store::() { + None + } else { + let type_id = TypeId::of::(); + Some(as_component_store( + self.component_stores.get(&type_id).unwrap().as_ref(), + )) + } + } + + fn add_component_store(&mut self) -> &ComponentStore { + if self.has_component_store::() { + self.get_component_store().unwrap() + } else { + let component_store = ComponentStore::::new(HashMap::new()); + let type_id = TypeId::of::(); + self.component_stores + .insert(type_id, Box::new(component_store)); + as_component_store(self.component_stores.get(&type_id).unwrap().as_ref()) + } + } + + #[inline] + pub fn has_entity(&self, entity: EntityId) -> bool { + self.entities.contains(&entity) + } + + pub fn new_entity(&mut self) -> EntityId { + let new_entity_id = self.next_id; + self.next_id = self.next_id.wrapping_add(1); + self.entities.insert(new_entity_id); + new_entity_id + } + + pub fn remove_entity(&mut self, entity: EntityId) -> bool { + if !self.has_entity(entity) { + return false; + } + + self.entities.remove(&entity); + for (_, component_store) in self.component_stores.iter_mut() { + component_store.remove(entity); + } + true + } + + pub fn remove_all_entities(&mut self) { + self.entities.clear(); + for (_, component_store) in self.component_stores.iter_mut() { + component_store.clear(); + } + } + + pub fn has_component(&self, entity: EntityId) -> bool { + if !self.has_entity(entity) { + false + } else { + let type_id = TypeId::of::(); + if let Some(component_store) = self.component_stores.get(&type_id) { + component_store.has(entity) + } else { + false + } + } + } + + pub fn add_component(&mut self, entity: EntityId, component: T) -> bool { + if !self.has_entity(entity) { + false + } else { + if let Some(component_store) = self.get_component_store::() { + component_store.borrow_mut().insert(entity, component); + } else { + self.add_component_store::() + .borrow_mut() + .insert(entity, component); + } + true + } + } + + pub fn remove_component(&mut self, entity: EntityId) -> bool { + if !self.has_entity(entity) { + false + } else { + let type_id = TypeId::of::(); + if let Some(component_store) = self.component_stores.get_mut(&type_id) { + component_store.remove(entity) + } else { + false + } + } + } + + #[inline] + pub fn components(&self) -> Option> { + if let Some(component_store) = self.get_component_store() { + Some(component_store.borrow()) + } else { + None + } + } + + #[inline] + pub fn components_mut(&self) -> Option> { + if let Some(component_store) = self.get_component_store() { + Some(component_store.borrow_mut()) + } else { + None + } + } + + pub fn init_components(&mut self) { + if self.get_component_store::().is_none() { + self.add_component_store::(); + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// TODO: is there some fancy way to get rid of the impl duplication here ... ? + +pub trait ComponentStoreConvenience { + fn single(&self) -> Option<(&EntityId, &T)>; + fn get(&self, k: &EntityId) -> Option<&T>; +} + +pub trait ComponentStoreConvenienceMut { + fn single_mut(&mut self) -> Option<(&EntityId, &T)>; + fn get_mut(&mut self, k: &EntityId) -> Option<&mut T>; +} + +impl<'a, T: Component> ComponentStoreConvenience for Option> { + fn single(&self) -> Option<(&EntityId, &T)> { + if let Some(components) = self { + if let Some((entity_id, component)) = components.iter().next() { + return Some((entity_id, component)); + } + } + None + } + + fn get(&self, k: &EntityId) -> Option<&T> { + if let Some(components) = self { + components.get(k) + } else { + None + } + } +} + +impl<'a, T: Component> ComponentStoreConvenience for Option> { + fn single(&self) -> Option<(&EntityId, &T)> { + if let Some(components) = self { + if let Some((entity_id, component)) = components.iter().next() { + return Some((entity_id, component)); + } + } + None + } + + fn get(&self, k: &EntityId) -> Option<&T> { + if let Some(components) = self { + components.get(k) + } else { + None + } + } +} + +impl<'a, T: Component> ComponentStoreConvenienceMut for Option> { + fn single_mut(&mut self) -> Option<(&EntityId, &T)> { + if let Some(components) = self { + if let Some((entity_id, component)) = components.iter_mut().next() { + return Some((entity_id, component)); + } + } + None + } + + fn get_mut(&mut self, k: &EntityId) -> Option<&mut T> { + if let Some(components) = self { + components.get_mut(k) + } else { + None + } + } +} + +pub trait OptionComponentStore { + fn len(&self) -> usize; + fn is_empty(&self) -> bool; +} + +impl<'a, T: Component> OptionComponentStore for Option> { + fn len(&self) -> usize { + if let Some(components) = self { + components.len() + } else { + 0 + } + } + + fn is_empty(&self) -> bool { + if let Some(components) = self { + components.is_empty() + } else { + true + } + } +} + +impl<'a, T: Component> OptionComponentStore for Option> { + fn len(&self) -> usize { + if let Some(components) = self { + components.len() + } else { + 0 + } + } + + fn is_empty(&self) -> bool { + if let Some(components) = self { + components.is_empty() + } else { + true + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +pub type UpdateFn = fn(&mut Entities, &mut T); +pub type RenderFn = fn(&mut Entities, &mut T); + +pub struct ComponentSystems { + update_systems: Vec>, + render_systems: Vec>, +} + +impl ComponentSystems { + pub fn new() -> Self { + ComponentSystems { + update_systems: Vec::new(), + render_systems: Vec::new(), + } + } + + pub fn add_update_system(&mut self, f: UpdateFn) { + self.update_systems.push(f); + } + + pub fn add_render_system(&mut self, f: RenderFn) { + self.render_systems.push(f); + } + + pub fn reset(&mut self) { + self.update_systems.clear(); + self.render_systems.clear(); + } + + pub fn update(&mut self, entities: &mut Entities, context: &mut U) { + for f in self.update_systems.iter_mut() { + f(entities, context); + } + } + + pub fn render(&mut self, entities: &mut Entities, context: &mut R) { + for f in self.render_systems.iter_mut() { + f(entities, context); + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use claim::*; + + use super::*; + + #[derive(Debug, Eq, PartialEq, Hash, Clone)] + struct Name(&'static str); + #[derive(Debug, Eq, PartialEq, Hash, Clone)] + struct Position(i32, i32); + #[derive(Debug, Eq, PartialEq, Hash, Clone)] + struct Velocity(i32, i32); + #[derive(Debug, Eq, PartialEq, Hash, Clone)] + struct Health(u32); + #[derive(Debug, Eq, PartialEq, Hash, Clone)] + struct Counter(u32); + + #[test] + fn add_and_remove_entities() { + let mut em = Entities::new(); + + // add first entity + assert!(!em.has_entity(1)); + let a = em.new_entity(); + assert!(em.has_entity(a)); + + // add second entity with totally different id from first entity + let b = em.new_entity(); + assert!(em.has_entity(a)); + assert!(em.has_entity(b)); + assert_ne!(a, b); + + // remove first entity + assert!(em.remove_entity(a)); + assert!(!em.has_entity(a)); + assert!(em.has_entity(b)); + + // remove second entity + assert!(em.remove_entity(b)); + assert!(!em.has_entity(b)); + + // removing entities which don't exist shouldn't blow up + assert!(!em.remove_entity(a)); + assert!(!em.remove_entity(b)); + + // add third entity, will not re-use previous entity ids + let c = em.new_entity(); + assert!(em.has_entity(c)); + assert_ne!(a, c); + assert_ne!(b, c); + } + + #[test] + fn add_and_remove_entity_components() { + let mut em = Entities::new(); + + // create first entity + let a = em.new_entity(); + assert!(em.has_entity(a)); + + // add component + assert!(!em.has_component::(a)); + assert!(em.add_component(a, Name("Someone"))); + assert!(em.has_component::(a)); + + // verify the added component + { + let names = em.components::().unwrap(); + let name_a = names.get(&a).unwrap(); + assert_eq!("Someone", name_a.0); + } + + // create second entity + let b = em.new_entity(); + assert!(em.has_entity(b)); + + // add component to second entity + assert!(!em.has_component::(b)); + assert_none!(em.components::()); + assert!(em.add_component(b, Position(1, 2))); + assert!(em.has_component::(b)); + + // verify the added component + { + let positions = em.components::().unwrap(); + let position_b = positions.get(&b).unwrap(); + assert!(1 == position_b.0 && 2 == position_b.1); + } + + // verify current components for both entities are what we expect + assert!(em.has_component::(a)); + assert!(!em.has_component::(b)); + assert!(!em.has_component::(a)); + assert!(em.has_component::(b)); + + // add new component to first entity + assert!(em.add_component(a, Position(5, 3))); + assert!(em.has_component::(a)); + + // verify both position components for both entities + { + let positions = em.components::().unwrap(); + let position_a = positions.get(&a).unwrap(); + assert!(5 == position_a.0 && 3 == position_a.1); + let position_b = positions.get(&b).unwrap(); + assert!(1 == position_b.0 && 2 == position_b.1); + } + + // verify current components for both entities are what we expect + assert!(em.has_component::(a)); + assert!(!em.has_component::(b)); + assert!(em.has_component::(a)); + assert!(em.has_component::(b)); + + // remove position component from first entity + assert!(em.remove_component::(a)); + assert!(!em.has_component::(a)); + + // verify current components for both entities are what we expect + assert!(em.has_component::(a)); + assert!(!em.has_component::(b)); + assert!(!em.has_component::(a)); + assert!(em.has_component::(b)); + { + let positions = em.components::().unwrap(); + let position_b = positions.get(&b).unwrap(); + assert!(1 == position_b.0 && 2 == position_b.1); + } + } + + #[test] + fn modify_components() { + let mut em = Entities::new(); + + // create entities + let a = em.new_entity(); + em.add_component(a, Position(10, 20)); + let b = em.new_entity(); + em.add_component(b, Position(17, 5)); + + // change entity positions + { + let mut positions = em.components_mut::().unwrap(); + let position_a = positions.get_mut(&a).unwrap(); + assert_eq!(Position(10, 20), *position_a); + position_a.0 = 13; + position_a.1 = 23; + } + + { + let mut positions = em.components_mut::().unwrap(); + let position_b = positions.get_mut(&b).unwrap(); + assert_eq!(Position(17, 5), *position_b); + position_b.0 = 15; + position_b.1 = 8; + } + + // verify both entity position components + { + let positions = em.components::().unwrap(); + let position_a = positions.get(&a).unwrap(); + assert_eq!(Position(13, 23), *position_a); + let position_b = positions.get(&b).unwrap(); + assert_eq!(Position(15, 8), *position_b); + } + } + + #[test] + fn get_all_components_of_type() { + let mut em = Entities::new(); + + // create entities + let a = em.new_entity(); + em.add_component(a, Health(20)); + em.add_component(a, Position(10, 20)); + let b = em.new_entity(); + em.add_component(b, Health(30)); + em.add_component(b, Position(17, 5)); + + // verify initial entity positions + { + let positions = em.components::().unwrap(); + assert_eq!(2, positions.len()); + let positions: HashSet<&Position> = positions.values().collect(); + assert!(positions.contains(&Position(10, 20))); + assert!(positions.contains(&Position(17, 5))); + } + + // modify position components + { + let mut positions = em.components_mut::().unwrap(); + for mut component in positions.values_mut() { + component.0 += 5; + } + + assert_eq!(Position(15, 20), *positions.get(&a).unwrap()); + assert_eq!(Position(22, 5), *positions.get(&b).unwrap()); + } + + // modify health components + { + let mut healths = em.components_mut::().unwrap(); + for mut component in healths.values_mut() { + component.0 += 5; + } + assert_eq!(Health(25), *healths.get(&a).unwrap()); + assert_eq!(Health(35), *healths.get(&b).unwrap()); + } + } + + #[test] + fn get_all_entities_with_component() { + let mut em = Entities::new(); + + // create entities + let a = em.new_entity(); + em.add_component(a, Name("A")); + em.add_component(a, Health(20)); + em.add_component(a, Position(10, 20)); + let b = em.new_entity(); + em.add_component(b, Name("B")); + em.add_component(b, Position(17, 5)); + + // get entities with position components + { + let positions = em.components::().unwrap(); + let entities = positions.keys(); + assert_eq!(2, entities.len()); + let entities: HashSet<&EntityId> = entities.collect(); + assert!(entities.contains(&a)); + assert!(entities.contains(&b)); + } + + // + let names = em.components::().unwrap(); + for (entity, name) in names.iter() { + // just written this way to verify can grab two mutable components at once + // (since this wouldn't be an uncommon way to want to work with an entity) + let mut healths = em.components_mut::().unwrap(); + let mut positions = em.components_mut::().unwrap(); + + let health = healths.get_mut(&entity); + let position = positions.get_mut(&entity); + + println!( + "entity {}, health: {:?}, position: {:?}", + name.0, health, position + ); + + if let Some(mut health) = health { + health.0 += 5; + } + if let Some(mut position) = position { + position.0 += 5; + } + } + + let positions = em.components::().unwrap(); + assert_eq!(Position(15, 20), *positions.get(&a).unwrap()); + assert_eq!(Position(22, 5), *positions.get(&b).unwrap()); + let healths = em.components::().unwrap(); + assert_eq!(Health(25), *healths.get(&a).unwrap()); + assert!(healths.get(&b).is_none()); + } + + struct UpdateContext(f32); + struct RenderContext(f32); + + fn system_print_entity_positions(entities: &mut Entities, _context: &mut RenderContext) { + let positions = entities.components::().unwrap(); + for (entity, position) in positions.iter() { + println!("entity {} at x:{}, y:{}", entity, position.0, position.1) + } + } + + fn system_move_entities_forward(entities: &mut Entities, _context: &mut UpdateContext) { + let mut positions = entities.components_mut::().unwrap(); + let velocities = entities.components::().unwrap(); + for (entity, position) in positions.iter_mut() { + if let Some(velocity) = velocities.get(&entity) { + position.0 += velocity.0; + position.1 += velocity.1; + } + } + } + + fn system_increment_counter(entities: &mut Entities, _context: &mut UpdateContext) { + let mut counters = entities.components_mut::().unwrap(); + for (_entity, counter) in counters.iter_mut() { + counter.0 += 1; + } + } + + fn system_print_counter(entities: &mut Entities, _context: &mut RenderContext) { + let counters = entities.components::().unwrap(); + for (entity, counter) in counters.iter() { + println!("entity {} has counter {}", entity, counter.0); + } + } + + #[test] + pub fn component_systems() { + let mut em = Entities::new(); + + // create entities + let a = em.new_entity(); + em.add_component(a, Position(5, 6)); + em.add_component(a, Velocity(1, 1)); + em.add_component(a, Counter(0)); + let b = em.new_entity(); + em.add_component(b, Position(-3, 0)); + em.add_component(b, Velocity(1, 0)); + em.add_component(b, Counter(0)); + let c = em.new_entity(); + em.add_component(c, Position(2, 9)); + em.add_component(c, Counter(0)); + + // setup component systems + let mut cs = ComponentSystems::new(); + cs.add_update_system(system_move_entities_forward); + cs.add_update_system(system_increment_counter); + cs.add_render_system(system_print_entity_positions); + cs.add_render_system(system_print_counter); + + // run some update+render iterations + for _ in 0..5 { + let mut update_context = UpdateContext(0.0); + let mut render_context = RenderContext(0.0); + cs.update(&mut em, &mut update_context); + cs.render(&mut em, &mut render_context); + } + + // verify expected entity positions + let positions = em.components::().unwrap(); + let velocities = em.components::().unwrap(); + let counters = em.components::().unwrap(); + assert_eq!(Position(10, 11), *positions.get(&a).unwrap()); + assert_eq!(Velocity(1, 1), *velocities.get(&a).unwrap()); + assert_eq!(Counter(5), *counters.get(&a).unwrap()); + assert_eq!(Position(2, 0), *positions.get(&b).unwrap()); + assert_eq!(Velocity(1, 0), *velocities.get(&b).unwrap()); + assert_eq!(Counter(5), *counters.get(&b).unwrap()); + assert_eq!(Position(2, 9), *positions.get(&c).unwrap()); + assert_eq!(None, velocities.get(&c)); + assert_eq!(Counter(5), *counters.get(&c).unwrap()); + } +} diff --git a/libretrogd/src/events/mod.rs b/libretrogd/src/events/mod.rs new file mode 100644 index 0000000..a494e31 --- /dev/null +++ b/libretrogd/src/events/mod.rs @@ -0,0 +1,291 @@ +use std::collections::VecDeque; + +pub type ListenerFn = fn(event: &EventType, &mut ContextType) -> bool; + +pub struct EventPublisher { + queue: VecDeque, +} + +impl EventPublisher { + pub fn new() -> Self { + EventPublisher { + queue: VecDeque::new(), + } + } + + #[inline] + pub fn len(&self) -> usize { + self.queue.len() + } + + #[inline] + pub fn clear(&mut self) { + self.queue.clear(); + } + + #[inline] + pub fn queue(&mut self, event: EventType) { + self.queue.push_back(event); + } + + pub fn take_queue(&mut self, destination: &mut VecDeque) { + destination.clear(); + destination.append(&mut self.queue); + self.clear(); + } +} + +pub struct EventListeners { + listeners: Vec>, + dispatch_queue: VecDeque, +} + +impl EventListeners { + pub fn new() -> Self { + EventListeners { + listeners: Vec::new(), + dispatch_queue: VecDeque::new(), + } + } + + pub fn len(&self) -> usize { + self.listeners.len() + } + + pub fn clear(&mut self) { + self.listeners.clear(); + } + + pub fn add(&mut self, listener: ListenerFn) -> bool { + // HACK?: most advice i've seen right now for comparing function pointers suggests doing + // this, but i've also come across comments suggesting there are times where this + // might not be foolproof? (e.g. where generics or lifetimes come into play ... ) + if self.listeners.iter().any(|&l| l as usize == listener as usize) { + false // don't add a duplicate listener + } else { + self.listeners.push(listener); + true + } + } + + pub fn remove(&mut self, listener: ListenerFn) -> bool { + let before_size = self.listeners.len(); + // HACK?: comparing function pointers -- see above "HACK?" comment. same concern here. + self.listeners.retain(|&l| l as usize != listener as usize); + // return true if the listener was removed + return before_size != self.listeners.len() + } + + pub fn take_queue_from(&mut self, publisher: &mut EventPublisher) -> usize { + publisher.take_queue(&mut self.dispatch_queue); + self.dispatch_queue.len() + } + + pub fn dispatch_queue(&mut self, context: &mut ContextType) { + while let Some(event) = self.dispatch_queue.pop_front() { + for listener in &self.listeners { + if listener(&event, context) { + break; + } + } + } + } + +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, Eq, PartialEq, Copy, Clone)] + enum TestEvent { + Dummy, + Foobar(i32), + Message(&'static str), + } + + struct DummyContext; + + struct TestContext { + pub count: i32, + pub events: Vec, + } + + impl TestContext { + pub fn new() -> Self { + TestContext { count: 0, events: Vec::new() } + } + } + + fn dummy_listener(_event: &TestEvent, _context: &mut DummyContext) -> bool { + false + } + + fn other_dummy_listener(_event: &TestEvent, _context: &mut DummyContext) -> bool { + false + } + + fn event_logger(event: &TestEvent, context: &mut TestContext) -> bool { + context.events.push(*event); + false + } + + fn event_counter(_event: &TestEvent, context: &mut TestContext) -> bool { + context.count += 1; + false + } + + fn message_filter(event: &TestEvent, _context: &mut TestContext) -> bool { + match event { + TestEvent::Message(s) => { + if *s == "filter" { + true // means event was handled, and no subsequent listeners should be called + } else { + false + } + }, + _ => false + } + } + + #[test] + pub fn adding_and_removing_listeners() { + let mut listeners = EventListeners::::new(); + + // add and remove + assert_eq!(0, listeners.len()); + assert!(listeners.add(dummy_listener)); + assert_eq!(1, listeners.len()); + assert!(!listeners.add(dummy_listener)); + assert_eq!(1, listeners.len()); + assert!(listeners.remove(dummy_listener)); + assert_eq!(0, listeners.len()); + assert!(!listeners.remove(dummy_listener)); + assert_eq!(0, listeners.len()); + + // add and remove multiple + assert!(listeners.add(dummy_listener)); + assert_eq!(1, listeners.len()); + assert!(listeners.add(other_dummy_listener)); + assert_eq!(2, listeners.len()); + assert!(listeners.remove(dummy_listener)); + assert_eq!(1, listeners.len()); + assert!(!listeners.remove(dummy_listener)); + assert_eq!(1, listeners.len()); + assert!(listeners.remove(other_dummy_listener)); + assert_eq!(0, listeners.len()); + + // clear all + assert!(listeners.add(dummy_listener)); + assert!(listeners.add(other_dummy_listener)); + assert_eq!(2, listeners.len()); + listeners.clear(); + assert_eq!(0, listeners.len()); + } + + #[test] + pub fn queueing_events() { + use TestEvent::*; + + let mut publisher = EventPublisher::::new(); + assert_eq!(0, publisher.len()); + publisher.queue(Dummy); + assert_eq!(1, publisher.len()); + publisher.queue(Foobar(1)); + assert_eq!(2, publisher.len()); + publisher.queue(Foobar(2)); + assert_eq!(3, publisher.len()); + + let mut queue = VecDeque::::new(); + publisher.take_queue(&mut queue); + assert_eq!(0, publisher.len()); + assert_eq!(Dummy, queue.pop_front().unwrap()); + assert_eq!(Foobar(1), queue.pop_front().unwrap()); + assert_eq!(Foobar(2), queue.pop_front().unwrap()); + assert!(queue.pop_front().is_none()); + + publisher.queue(Dummy); + assert_eq!(1, publisher.len()); + publisher.clear(); + assert_eq!(0, publisher.len()); + let mut queue = VecDeque::::new(); + publisher.take_queue(&mut queue); + assert_eq!(0, publisher.len()); + assert_eq!(0, queue.len()); + } + + #[test] + pub fn listeners_receive_events() { + use TestEvent::*; + + let mut listeners = EventListeners::::new(); + assert!(listeners.add(event_logger)); + + let mut publisher = EventPublisher::::new(); + publisher.queue(Dummy); + publisher.queue(Foobar(1)); + publisher.queue(Dummy); + publisher.queue(Foobar(42)); + assert_eq!(4, listeners.take_queue_from(&mut publisher)); + + let mut context = TestContext::new(); + assert!(context.events.is_empty()); + assert_eq!(0, context.count); + listeners.dispatch_queue(&mut context); + assert!(!context.events.is_empty()); + assert_eq!(0, context.count); + assert_eq!( + vec![Dummy, Foobar(1), Dummy, Foobar(42)], + context.events + ); + + let mut context = TestContext::new(); + assert!(context.events.is_empty()); + assert_eq!(0, context.count); + listeners.dispatch_queue(&mut context); + assert!(context.events.is_empty()); + + assert!(listeners.add(event_counter)); + publisher.queue(Foobar(10)); + publisher.queue(Foobar(20)); + publisher.queue(Dummy); + listeners.take_queue_from(&mut publisher); + let mut context = TestContext::new(); + listeners.dispatch_queue(&mut context); + assert!(!context.events.is_empty()); + assert_eq!(3, context.count); + assert_eq!( + vec![Foobar(10), Foobar(20), Dummy], + context.events + ); + } + + #[test] + pub fn listener_filtering() { + use TestEvent::*; + + let mut listeners = EventListeners::::new(); + assert!(listeners.add(message_filter)); + assert!(listeners.add(event_logger)); + assert!(listeners.add(event_counter)); + + let mut publisher = EventPublisher::::new(); + publisher.queue(Message("hello")); + publisher.queue(Dummy); + publisher.queue(Message("filter")); + publisher.queue(Foobar(3)); + assert_eq!(4, listeners.take_queue_from(&mut publisher)); + + let mut context = TestContext::new(); + assert!(context.events.is_empty()); + assert_eq!(0, context.count); + listeners.dispatch_queue(&mut context); + assert!(!context.events.is_empty()); + assert_eq!(3, context.count); + assert_eq!( + vec![Message("hello"), Dummy, Foobar(3)], + context.events + ); + + } +} diff --git a/libretrogd/src/graphics/bitmap/blit.rs b/libretrogd/src/graphics/bitmap/blit.rs new file mode 100644 index 0000000..63e696c --- /dev/null +++ b/libretrogd/src/graphics/bitmap/blit.rs @@ -0,0 +1,342 @@ +use crate::{Bitmap, Rect}; + +pub enum BlitMethod { + Solid, + Transparent(u8), +} + +/// Clips the region for a source bitmap to be used in a subsequent blit operation. The source +/// region will be clipped against the clipping region given for the destination bitmap. The +/// top-left coordinates of the location to blit to on the destination bitmap are also adjusted +/// only if necessary based on the clipping performed. +/// +/// # Arguments +/// +/// * `dest_clip_region`: the clipping region for the destination bitmap +/// * `src_blit_region`: the region on the source bitmap that is to be blitted, which may be +/// clipped if necessary to at least partially fit into the destination clipping region given +/// * `dest_x`: the x (left) coordinate of the location on the destination bitmap to blit the +/// source to, which may be adjusted as necessary during clipping +/// * `dest_y`: the y (top) coordinate of the location on the destination bitmap to blit the source +/// to, which may be adjusted as necessary during clipping +/// +/// returns: true if the results of the clip is partially or entirely visible on the destination +/// bitmap, or false if the blit is entirely outside of the destination bitmap (and so no blit +/// needs to occur) +pub fn clip_blit( + dest_clip_region: &Rect, + src_blit_region: &mut Rect, + dest_x: &mut i32, + dest_y: &mut i32, +) -> bool { + // off the left edge? + if *dest_x < dest_clip_region.x { + // completely off the left edge? + if (*dest_x + src_blit_region.width as i32 - 1) < dest_clip_region.x { + return false; + } + + let offset = dest_clip_region.x - *dest_x; + src_blit_region.x += offset; + src_blit_region.width = (src_blit_region.width as i32 - offset) as u32; + *dest_x = dest_clip_region.x; + } + + // off the right edge? + if *dest_x > dest_clip_region.width as i32 - src_blit_region.width as i32 { + // completely off the right edge? + if *dest_x > dest_clip_region.right() { + return false; + } + + let offset = *dest_x + src_blit_region.width as i32 - dest_clip_region.width as i32; + src_blit_region.width = (src_blit_region.width as i32 - offset) as u32; + } + + // off the top edge? + if *dest_y < dest_clip_region.y { + // completely off the top edge? + if (*dest_y + src_blit_region.height as i32 - 1) < dest_clip_region.y { + return false; + } + + let offset = dest_clip_region.y - *dest_y; + src_blit_region.y += offset; + src_blit_region.height = (src_blit_region.height as i32 - offset) as u32; + *dest_y = dest_clip_region.y; + } + + // off the bottom edge? + if *dest_y > dest_clip_region.height as i32 - src_blit_region.height as i32 { + // completely off the bottom edge? + if *dest_y > dest_clip_region.bottom() { + return false; + } + + let offset = *dest_y + src_blit_region.height as i32 - dest_clip_region.height as i32; + src_blit_region.height = (src_blit_region.height as i32 - offset) as u32; + } + + true +} + +impl Bitmap { + pub unsafe fn solid_blit(&mut self, src: &Bitmap, src_region: &Rect, dest_x: i32, dest_y: i32) { + let src_row_length = src_region.width as usize; + let src_pitch = src.width as usize; + let dest_pitch = self.width as usize; + let mut src_pixels = src.pixels_at_ptr_unchecked(src_region.x, src_region.y); + let mut dest_pixels = self.pixels_at_mut_ptr_unchecked(dest_x, dest_y); + + for _ in 0..src_region.height { + dest_pixels.copy_from(src_pixels, src_row_length); + src_pixels = src_pixels.add(src_pitch); + dest_pixels = dest_pixels.add(dest_pitch); + } + } + + pub unsafe fn transparent_blit( + &mut self, + src: &Bitmap, + src_region: &Rect, + dest_x: i32, + dest_y: i32, + transparent_color: u8, + ) { + let src_next_row_inc = (src.width - src_region.width) as usize; + let dest_next_row_inc = (self.width - src_region.width) as usize; + let mut src_pixels = src.pixels_at_ptr_unchecked(src_region.x, src_region.y); + let mut dest_pixels = self.pixels_at_mut_ptr_unchecked(dest_x, dest_y); + + for _ in 0..src_region.height { + for _ in 0..src_region.width { + let pixel = *src_pixels; + if pixel != transparent_color { + *dest_pixels = pixel; + } + + src_pixels = src_pixels.add(1); + dest_pixels = dest_pixels.add(1); + } + + src_pixels = src_pixels.add(src_next_row_inc); + dest_pixels = dest_pixels.add(dest_next_row_inc); + } + } + + pub fn blit_region( + &mut self, + method: BlitMethod, + src: &Bitmap, + src_region: &Rect, + mut dest_x: i32, + mut dest_y: i32, + ) { + let mut src_region = *src_region; + if !clip_blit( + self.clip_region(), + &mut src_region, + &mut dest_x, + &mut dest_y, + ) { + return; + } + + unsafe { + self.blit_region_unchecked(method, src, &src_region, dest_x, dest_y); + }; + } + + #[inline] + pub unsafe fn blit_region_unchecked( + &mut self, + method: BlitMethod, + src: &Bitmap, + src_region: &Rect, + dest_x: i32, + dest_y: i32, + ) { + use BlitMethod::*; + match method { + Solid => self.solid_blit(src, src_region, dest_x, dest_y), + Transparent(transparent_color) => { + self.transparent_blit(src, src_region, dest_x, dest_y, transparent_color) + } + } + } + + #[inline] + pub fn blit(&mut self, method: BlitMethod, src: &Bitmap, x: i32, y: i32) { + let src_region = Rect::new(0, 0, src.width, src.height); + self.blit_region(method, src, &src_region, x, y); + } + + #[inline] + pub unsafe fn blit_unchecked(&mut self, method: BlitMethod, src: &Bitmap, x: i32, y: i32) { + let src_region = Rect::new(0, 0, src.width, src.height); + self.blit_region_unchecked(method, src, &src_region, x, y); + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + pub fn clip_blit_regions() { + let dest = Rect::new(0, 0, 320, 240); + + let mut src: Rect; + let mut x: i32; + let mut y: i32; + + src = Rect::new(0, 0, 16, 16); + x = 10; + y = 10; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 16, 16)); + assert_eq!(10, x); + assert_eq!(10, y); + + // left edge + + src = Rect::new(0, 0, 16, 16); + x = 0; + y = 10; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 16, 16)); + assert_eq!(0, x); + assert_eq!(10, y); + + src = Rect::new(0, 0, 16, 16); + x = -5; + y = 10; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(5, 0, 11, 16)); + assert_eq!(0, x); + assert_eq!(10, y); + + src = Rect::new(0, 0, 16, 16); + x = -16; + y = 10; + assert!(!clip_blit(&dest, &mut src, &mut x, &mut y)); + + // right edge + + src = Rect::new(0, 0, 16, 16); + x = 304; + y = 10; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 16, 16)); + assert_eq!(304, x); + assert_eq!(10, y); + + src = Rect::new(0, 0, 16, 16); + x = 310; + y = 10; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 10, 16)); + assert_eq!(310, x); + assert_eq!(10, y); + + src = Rect::new(0, 0, 16, 16); + x = 320; + y = 10; + assert!(!clip_blit(&dest, &mut src, &mut x, &mut y)); + + // top edge + + src = Rect::new(0, 0, 16, 16); + x = 10; + y = 0; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 16, 16)); + assert_eq!(10, x); + assert_eq!(0, y); + + src = Rect::new(0, 0, 16, 16); + x = 10; + y = -5; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 5, 16, 11)); + assert_eq!(10, x); + assert_eq!(0, y); + + src = Rect::new(0, 0, 16, 16); + x = 10; + y = -16; + assert!(!clip_blit(&dest, &mut src, &mut x, &mut y)); + + // bottom edge + + src = Rect::new(0, 0, 16, 16); + x = 10; + y = 224; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 16, 16)); + assert_eq!(10, x); + assert_eq!(224, y); + + src = Rect::new(0, 0, 16, 16); + x = 10; + y = 229; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 16, 11)); + assert_eq!(10, x); + assert_eq!(229, y); + + src = Rect::new(0, 0, 16, 16); + x = 10; + y = 240; + assert!(!clip_blit(&dest, &mut src, &mut x, &mut y)); + + src = Rect::new(16, 16, 16, 16); + x = -1; + y = 112; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(17, 16, 15, 16)); + assert_eq!(0, x); + assert_eq!(112, y); + } + + #[test] + pub fn clip_blit_regions_large_source() { + let dest = Rect::new(0, 0, 64, 64); + + let mut src: Rect; + let mut x: i32; + let mut y: i32; + + src = Rect::new(0, 0, 128, 128); + x = 0; + y = 0; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 64, 64)); + assert_eq!(0, x); + assert_eq!(0, y); + + src = Rect::new(0, 0, 128, 128); + x = -16; + y = -24; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(16, 24, 64, 64)); + assert_eq!(0, x); + assert_eq!(0, y); + + src = Rect::new(0, 0, 32, 128); + x = 10; + y = -20; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 20, 32, 64)); + assert_eq!(10, x); + assert_eq!(0, y); + + src = Rect::new(0, 0, 128, 32); + x = -20; + y = 10; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(20, 0, 64, 32)); + assert_eq!(0, x); + assert_eq!(10, y); + } +} diff --git a/libretrogd/src/graphics/bitmap/iff.rs b/libretrogd/src/graphics/bitmap/iff.rs new file mode 100644 index 0000000..12eb5b0 --- /dev/null +++ b/libretrogd/src/graphics/bitmap/iff.rs @@ -0,0 +1,620 @@ +use std::fs::File; +use std::io; +use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}; +use std::path::Path; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use thiserror::Error; + +use crate::utils::packbits::{pack_bits, unpack_bits, PackBitsError}; +use crate::{Bitmap, Palette, PaletteError, PaletteFormat}; + +#[derive(Error, Debug)] +pub enum IffError { + #[error("Bad or unsupported IFF file: {0}")] + BadFile(String), + + #[error("IFF palette data error")] + BadPalette(#[from] PaletteError), + + #[error("PackBits error")] + PackBitsError(#[from] PackBitsError), + + #[error("IFF I/O error")] + IOError(#[from] std::io::Error), +} + +pub enum IffFormat { + Pbm, + PbmUncompressed, + Ilbm, + IlbmUncompressed, +} + +impl IffFormat { + pub fn compressed(&self) -> bool { + use IffFormat::*; + match self { + Pbm | Ilbm => true, + PbmUncompressed | IlbmUncompressed => false, + } + } + + pub fn chunky(&self) -> bool { + use IffFormat::*; + match self { + Pbm | PbmUncompressed => true, + Ilbm | IlbmUncompressed => false, + } + } + + pub fn type_id(&self) -> [u8; 4] { + use IffFormat::*; + match self { + Pbm | PbmUncompressed => *b"PBM ", + Ilbm | IlbmUncompressed => *b"ILBM", + } + } +} + +#[derive(Debug, Copy, Clone)] +#[repr(packed)] +struct IffId { + id: [u8; 4], +} + +impl IffId { + pub fn read(reader: &mut T) -> Result { + let mut id = [0u8; 4]; + reader.read_exact(&mut id)?; + Ok(IffId { id }) + } + + pub fn write(&self, writer: &mut T) -> Result<(), IffError> { + writer.write_all(&self.id)?; + Ok(()) + } +} + +#[derive(Debug, Copy, Clone)] +#[repr(packed)] +struct FormChunkHeader { + chunk_id: IffId, + size: u32, + type_id: IffId, +} + +impl FormChunkHeader { + pub fn read(reader: &mut T) -> Result { + let chunk_id = IffId::read(reader)?; + let size = reader.read_u32::()?; + let type_id = IffId::read(reader)?; + Ok(FormChunkHeader { + chunk_id, + size, + type_id, + }) + } + + pub fn write(&self, writer: &mut T) -> Result<(), IffError> { + self.chunk_id.write(writer)?; + writer.write_u32::(self.size)?; + self.type_id.write(writer)?; + Ok(()) + } +} + +#[derive(Debug, Copy, Clone)] +#[repr(packed)] +struct SubChunkHeader { + chunk_id: IffId, + size: u32, +} + +impl SubChunkHeader { + pub fn read(reader: &mut T) -> Result { + let chunk_id = IffId::read(reader)?; + let mut size = reader.read_u32::()?; + if (size & 1) == 1 { + size += 1; // account for the padding byte + } + Ok(SubChunkHeader { chunk_id, size }) + } + + pub fn write(&self, writer: &mut T) -> Result<(), IffError> { + self.chunk_id.write(writer)?; + writer.write_u32::(self.size)?; + Ok(()) + } +} + +#[derive(Debug, Copy, Clone)] +#[repr(packed)] +struct BMHDChunk { + width: u16, + height: u16, + left: u16, + top: u16, + bitplanes: u8, + masking: u8, + compress: u8, + padding: u8, + transparency: u16, + x_aspect_ratio: u8, + y_aspect_ratio: u8, + page_width: u16, + page_height: u16, +} + +impl BMHDChunk { + pub fn read(reader: &mut T) -> Result { + Ok(BMHDChunk { + width: reader.read_u16::()?, + height: reader.read_u16::()?, + left: reader.read_u16::()?, + top: reader.read_u16::()?, + bitplanes: reader.read_u8()?, + masking: reader.read_u8()?, + compress: reader.read_u8()?, + padding: reader.read_u8()?, + transparency: reader.read_u16::()?, + x_aspect_ratio: reader.read_u8()?, + y_aspect_ratio: reader.read_u8()?, + page_width: reader.read_u16::()?, + page_height: reader.read_u16::()?, + }) + } + + pub fn write(&self, writer: &mut T) -> Result<(), IffError> { + writer.write_u16::(self.width)?; + writer.write_u16::(self.height)?; + writer.write_u16::(self.left)?; + writer.write_u16::(self.top)?; + writer.write_u8(self.bitplanes)?; + writer.write_u8(self.masking)?; + writer.write_u8(self.compress)?; + writer.write_u8(self.padding)?; + writer.write_u16::(self.transparency)?; + writer.write_u8(self.x_aspect_ratio)?; + writer.write_u8(self.y_aspect_ratio)?; + writer.write_u16::(self.page_width)?; + writer.write_u16::(self.page_height)?; + Ok(()) + } +} + +fn merge_bitplane(plane: u32, src: &[u8], dest: &mut [u8], row_size: usize) { + let bitmask = 1 << plane; + for x in 0..row_size { + let data = src[x]; + if (data & 128) > 0 { + dest[x * 8] |= bitmask; + } + if (data & 64) > 0 { + dest[(x * 8) + 1] |= bitmask; + } + if (data & 32) > 0 { + dest[(x * 8) + 2] |= bitmask; + } + if (data & 16) > 0 { + dest[(x * 8) + 3] |= bitmask; + } + if (data & 8) > 0 { + dest[(x * 8) + 4] |= bitmask; + } + if (data & 4) > 0 { + dest[(x * 8) + 5] |= bitmask; + } + if (data & 2) > 0 { + dest[(x * 8) + 6] |= bitmask; + } + if (data & 1) > 0 { + dest[(x * 8) + 7] |= bitmask; + } + } +} + +fn extract_bitplane(plane: u32, src: &[u8], dest: &mut [u8], row_size: usize) { + let bitmask = 1 << plane; + let mut src_base_index = 0; + for x in 0..row_size { + let mut data = 0; + if src[src_base_index] & bitmask != 0 { + data |= 128; + } + if src[src_base_index + 1] & bitmask != 0 { + data |= 64; + } + if src[src_base_index + 2] & bitmask != 0 { + data |= 32; + } + if src[src_base_index + 3] & bitmask != 0 { + data |= 16; + } + if src[src_base_index + 4] & bitmask != 0 { + data |= 8; + } + if src[src_base_index + 5] & bitmask != 0 { + data |= 4; + } + if src[src_base_index + 6] & bitmask != 0 { + data |= 2; + } + if src[src_base_index + 7] & bitmask != 0 { + data |= 1; + } + + src_base_index += 8; + dest[x] = data; + } +} + +fn load_planar_body(reader: &mut T, bmhd: &BMHDChunk) -> Result { + let mut bitmap = Bitmap::new(bmhd.width as u32, bmhd.height as u32).unwrap(); + + let row_bytes = (((bmhd.width + 15) >> 4) << 1) as usize; + let mut buffer = vec![0u8; row_bytes]; + + for y in 0..bmhd.height { + // planar data is stored for each bitplane in sequence for the scanline. + // that is, ALL of bitplane1, followed by ALL of bitplane2, etc, NOT + // alternating after each pixel. if compression is enabled, it does NOT + // cross bitplane boundaries. each bitplane is compressed individually. + // bitplanes also do NOT cross the scanline boundary. basically, each + // scanline of pixel data, and within that, each of the bitplanes of + // pixel data found in each scanline can all be treated as they are all + // their own self-contained bit of data as far as this loading process + // is concerned (well, except that we merge all of the scanline's + // bitplanes together at the end of each line) + + // read all the bitplane rows per scanline + for plane in 0..(bmhd.bitplanes as u32) { + if bmhd.compress == 1 { + // decompress packed line for this bitplane only + buffer.clear(); + unpack_bits(reader, &mut buffer, row_bytes)? + } else { + // TODO: check this. maybe row_bytes calculation is wrong? either way, i don't + // think that DP2 or Grafx2 ever output uncompressed interleaved files ... + // just read all this bitplane's line data in as-is + reader.read_exact(&mut buffer)?; + } + + // merge this bitplane data into the final destination. after all of + // the bitplanes have been loaded and merged in this way for this + // scanline, the destination pointer will contain VGA-friendly + // "chunky pixel"-format pixel data + merge_bitplane( + plane, + &buffer, + bitmap.pixels_at_mut(0, y as i32).unwrap(), + row_bytes, + ); + } + } + + Ok(bitmap) +} + +fn load_chunky_body(reader: &mut T, bmhd: &BMHDChunk) -> Result { + let mut bitmap = Bitmap::new(bmhd.width as u32, bmhd.height as u32).unwrap(); + + for y in 0..bmhd.height { + if bmhd.compress == 1 { + // for compression-enabled, read row of pixels using PackBits + let mut writer = bitmap.pixels_at_mut(0, y as i32).unwrap(); + unpack_bits(reader, &mut writer, bmhd.width as usize)? + } else { + // for uncompressed, read row of pixels literally + let dest = &mut bitmap.pixels_at_mut(0, y as i32).unwrap()[0..bmhd.width as usize]; + reader.read_exact(dest)?; + } + } + + Ok(bitmap) +} + +fn write_planar_body( + writer: &mut T, + bitmap: &Bitmap, + bmhd: &BMHDChunk, +) -> Result<(), IffError> { + let row_bytes = (((bitmap.width() + 15) >> 4) << 1) as usize; + let mut buffer = vec![0u8; row_bytes]; + + for y in 0..bitmap.height() { + for plane in 0..(bmhd.bitplanes as u32) { + extract_bitplane( + plane, + bitmap.pixels_at(0, y as i32).unwrap(), + &mut buffer, + row_bytes, + ); + + if bmhd.compress == 1 { + // for compression-enabled, write this plane's pixels using PackBits + pack_bits(&mut buffer.as_slice(), writer, row_bytes)?; + } else { + // TODO: check this. maybe row_bytes calculation is wrong? either way, i don't + // think that DP2 or Grafx2 ever output uncompressed interleaved files ... + // for uncompressed, write this plane's pixels literally + writer.write_all(&buffer)?; + } + } + } + + Ok(()) +} + +fn write_chunky_body( + writer: &mut T, + bitmap: &Bitmap, + bmhd: &BMHDChunk, +) -> Result<(), IffError> { + for y in 0..bitmap.height() { + if bmhd.compress == 1 { + // for compression-enabled, write row of pixels using PackBits + let mut reader = bitmap.pixels_at(0, y as i32).unwrap(); + pack_bits(&mut reader, writer, bitmap.width() as usize)?; + } else { + // for uncompressed, write out the row of pixels literally + let src = &bitmap.pixels_at(0, y as i32).unwrap()[0..bitmap.width() as usize]; + writer.write_all(src)?; + } + } + + Ok(()) +} + +impl Bitmap { + pub fn load_iff_bytes( + reader: &mut T, + ) -> Result<(Bitmap, Palette), IffError> { + let form_chunk = FormChunkHeader::read(reader)?; + if form_chunk.chunk_id.id != *b"FORM" { + return Err(IffError::BadFile(String::from( + "Unexpected form chunk ID, probably not an IFF file", + ))); + } + if form_chunk.type_id.id != *b"ILBM" && form_chunk.type_id.id != *b"PBM " { + return Err(IffError::BadFile(String::from( + "Only ILBM or PBM formats are supported", + ))); + } + + let mut bmhd: Option = None; + let mut palette: Option = None; + let mut bitmap: Option = None; + + loop { + let header = match SubChunkHeader::read(reader) { + Ok(header) => header, + Err(IffError::IOError(io_error)) + if io_error.kind() == io::ErrorKind::UnexpectedEof => + { + break + } + Err(err) => return Err(err), + }; + let chunk_data_position = reader.stream_position()?; + + // todo: process chunk here + if header.chunk_id.id == *b"BMHD" { + bmhd = Some(BMHDChunk::read(reader)?); + if bmhd.as_ref().unwrap().bitplanes != 8 { + return Err(IffError::BadFile(String::from( + "Only 8bpp files are supported", + ))); + } + if bmhd.as_ref().unwrap().masking == 1 { + return Err(IffError::BadFile(String::from("Masking is not supported"))); + } + } else if header.chunk_id.id == *b"CMAP" { + if header.size != 768 { + return Err(IffError::BadFile(String::from( + "Only 256 color files are supported", + ))); + } + palette = Some(Palette::load_from_bytes(reader, PaletteFormat::Normal)?) + } else if header.chunk_id.id == *b"BODY" { + if let Some(bmhd) = &bmhd { + if form_chunk.type_id.id == *b"PBM " { + bitmap = Some(load_chunky_body(reader, bmhd)?); + } else { + bitmap = Some(load_planar_body(reader, bmhd)?); + } + } else { + // TODO: does this ever occur in practice? and if so, we can probably make some + // changes to allow for it ... + return Err(IffError::BadFile(String::from( + "BODY chunk occurs before BMHD chunk, or no BMHD chunk exists", + ))); + } + } + + reader.seek(SeekFrom::Start(chunk_data_position + header.size as u64))?; + } + + if bitmap.is_none() { + return Err(IffError::BadFile(String::from("No BODY chunk was found"))); + } + // TODO: we can probably make this optional ... + if palette.is_none() { + return Err(IffError::BadFile(String::from("No CMAP chunk was found"))); + } + + Ok((bitmap.unwrap(), palette.unwrap())) + } + + pub fn load_iff_file(path: &Path) -> Result<(Bitmap, Palette), IffError> { + let f = File::open(path)?; + let mut reader = BufReader::new(f); + Self::load_iff_bytes(&mut reader) + } + + pub fn to_iff_bytes( + &self, + writer: &mut T, + palette: &Palette, + format: IffFormat, + ) -> Result<(), IffError> { + let form_chunk_position = writer.stream_position()?; + + let mut form_chunk = FormChunkHeader { + chunk_id: IffId { id: *b"FORM" }, + type_id: IffId { + id: format.type_id(), + }, + size: 0, // filled in later once we know the size + }; + + // skip over the form chunk for now. will come back here and write it out later once we + // know what the final size is + writer.seek(SeekFrom::Current( + std::mem::size_of::() as i64 + ))?; + + let bmhd_chunk_header = SubChunkHeader { + chunk_id: IffId { id: *b"BMHD" }, + size: std::mem::size_of::() as u32, + }; + let bmhd = BMHDChunk { + width: self.width() as u16, + height: self.height() as u16, + left: 0, + top: 0, + bitplanes: 8, + masking: 0, + compress: if format.compressed() { 1 } else { 0 }, + padding: 0, + transparency: 0, + // the following values are based on what DP2 writes out in 320x200 modes. good enough. + x_aspect_ratio: 5, + y_aspect_ratio: 6, + page_width: 320, + page_height: 200, + }; + bmhd_chunk_header.write(writer)?; + bmhd.write(writer)?; + + let cmap_chunk_header = SubChunkHeader { + chunk_id: IffId { id: *b"CMAP" }, + size: 768, + }; + cmap_chunk_header.write(writer)?; + palette.to_bytes(writer, PaletteFormat::Normal)?; + + let body_position = writer.stream_position()?; + + let mut body_chunk_header = SubChunkHeader { + chunk_id: IffId { id: *b"BODY" }, + size: 0, // filled in later once we know the size + }; + + // skip over the body chunk header for now. we will again come back here and write it out + // later once we know what the final size again. + writer.seek(SeekFrom::Current( + std::mem::size_of::() as i64 + ))?; + + if format.chunky() { + write_chunky_body(writer, self, &bmhd)?; + } else { + write_planar_body(writer, self, &bmhd)?; + } + + // add a padding byte (only if necessary) to the body we just finished writing + let mut eof_pos = writer.stream_position()?; + if (eof_pos - body_position) & 1 == 1 { + writer.write_u8(0)?; + eof_pos += 1; + } + + // go back and write out the form chunk header now that we know the final file size + form_chunk.size = (eof_pos - (std::mem::size_of::() as u64 * 2)) as u32; + writer.seek(SeekFrom::Start(form_chunk_position))?; + form_chunk.write(writer)?; + + // and then write out the body chunk header since we now know the size of that too + body_chunk_header.size = eof_pos as u32 - std::mem::size_of::() as u32; + writer.seek(SeekFrom::Start(body_position))?; + body_chunk_header.write(writer)?; + + // and then go back to eof + writer.seek(SeekFrom::Start(eof_pos))?; + + Ok(()) + } + + pub fn to_iff_file( + &self, + path: &Path, + palette: &Palette, + format: IffFormat, + ) -> Result<(), IffError> { + let f = File::create(path)?; + let mut writer = BufWriter::new(f); + self.to_iff_bytes(&mut writer, palette, format) + } +} + +#[cfg(test)] +mod tests { + use tempfile::TempDir; + + use super::*; + + pub static TEST_BMP_PIXELS_RAW: &[u8] = + include_bytes!("../../../test-assets/test_bmp_pixels_raw.bin"); + + #[test] + pub fn load_and_save() -> Result<(), IffError> { + let dp2_palette = + Palette::load_from_file(Path::new("./test-assets/dp2.pal"), PaletteFormat::Normal) + .unwrap(); + let tmp_dir = TempDir::new()?; + + // ILBM format + + let (bmp, palette) = Bitmap::load_iff_file(Path::new("./test-assets/test_ilbm.lbm"))?; + assert_eq!(16, bmp.width()); + assert_eq!(16, bmp.height()); + assert_eq!(bmp.pixels(), TEST_BMP_PIXELS_RAW); + assert_eq!(palette, dp2_palette); + + let save_path = tmp_dir.path().join("test_save_ilbm.lbm"); + bmp.to_iff_file(&save_path, &palette, IffFormat::Ilbm)?; + let (reloaded_bmp, reloaded_palette) = Bitmap::load_iff_file(&save_path)?; + assert_eq!(16, reloaded_bmp.width()); + assert_eq!(16, reloaded_bmp.height()); + assert_eq!(reloaded_bmp.pixels(), TEST_BMP_PIXELS_RAW); + assert_eq!(reloaded_palette, dp2_palette); + + // PBM format + + let (bmp, palette) = Bitmap::load_iff_file(Path::new("./test-assets/test_pbm.lbm"))?; + assert_eq!(16, bmp.width()); + assert_eq!(16, bmp.height()); + assert_eq!(bmp.pixels(), TEST_BMP_PIXELS_RAW); + assert_eq!(palette, dp2_palette); + + let save_path = tmp_dir.path().join("test_save_pbm.lbm"); + bmp.to_iff_file(&save_path, &palette, IffFormat::Pbm)?; + let (reloaded_bmp, reloaded_palette) = Bitmap::load_iff_file(&save_path)?; + assert_eq!(16, reloaded_bmp.width()); + assert_eq!(16, reloaded_bmp.height()); + assert_eq!(reloaded_bmp.pixels(), TEST_BMP_PIXELS_RAW); + assert_eq!(reloaded_palette, dp2_palette); + + Ok(()) + } + + #[test] + pub fn load_larger_image() -> Result<(), IffError> { + let (bmp, _palette) = Bitmap::load_iff_file(Path::new("./test-assets/test_image.lbm"))?; + assert_eq!(320, bmp.width()); + assert_eq!(200, bmp.height()); + + Ok(()) + } +} diff --git a/libretrogd/src/graphics/bitmap/mod.rs b/libretrogd/src/graphics/bitmap/mod.rs new file mode 100644 index 0000000..6d09b5d --- /dev/null +++ b/libretrogd/src/graphics/bitmap/mod.rs @@ -0,0 +1,584 @@ +use std::path::Path; +use std::slice; + +use thiserror::Error; + +pub use crate::blit::*; +pub use crate::iff::*; +pub use crate::pcx::*; +pub use crate::primitives::*; +use crate::{Palette, Rect}; + +pub mod blit; +pub mod iff; +pub mod pcx; +pub mod primitives; + +#[derive(Error, Debug)] +pub enum BitmapError { + #[error("Invalid bitmap dimensions")] + InvalidDimensions, + + #[error("Region is not fully within bitmap boundaries")] + OutOfBounds, + + #[error("Unknown bitmap file type: {0}")] + UnknownFileType(String), + + #[error("Bitmap IFF file error")] + IffError(#[from] iff::IffError), + + #[error("Bitmap PCX file error")] + PcxError(#[from] pcx::PcxError), +} + +/// Container for 256 color 2D pixel/image data that can be rendered to the screen. Pixel data +/// is stored as contiguous bytes, where each pixel is an index into a separate 256 color palette +/// stored independently of the bitmap. The pixel data is not padded in any way, so the stride from +/// one row to the next is always exactly equal to the bitmap width. Rendering operations provided +/// here are done with respect to the bitmaps clipping region, where rendering outside of the +/// clipping region is simply not performed / stops at the clipping boundary. +#[derive(Debug, Clone)] +pub struct Bitmap { + width: u32, + height: u32, + pixels: Box<[u8]>, + clip_region: Rect, +} + +impl Bitmap { + /// Creates a new Bitmap with the specified dimensions. + /// + /// # Arguments + /// + /// * `width`: the width of the bitmap in pixels + /// * `height`: the height of the bitmap in pixels + /// + /// returns: `Result` + pub fn new(width: u32, height: u32) -> Result { + if width == 0 || height == 0 { + return Err(BitmapError::InvalidDimensions); + } + + Ok(Bitmap { + width, + height, + pixels: vec![0u8; (width * height) as usize].into_boxed_slice(), + clip_region: Rect { + x: 0, + y: 0, + width, + height, + }, + }) + } + + /// Creates a new Bitmap, copying the pixel data from a sub-region of another source Bitmap. + /// The resulting bitmap will have dimensions equal to that of the region specified. + /// + /// # Arguments + /// + /// * `source`: the source bitmap to copy from + /// * `region`: the region on the source bitmap to copy from + /// + /// returns: `Result` + pub fn from(source: &Bitmap, region: &Rect) -> Result { + if !source.full_bounds().contains_rect(region) { + return Err(BitmapError::OutOfBounds); + } + + let mut bmp = Bitmap::new(region.width, region.height)?; + unsafe { bmp.solid_blit(source, region, 0, 0) }; + Ok(bmp) + } + + pub fn load_file(path: &Path) -> Result<(Bitmap, Palette), BitmapError> { + if let Some(extension) = path.extension() { + let extension = extension.to_ascii_lowercase(); + match extension.to_str() { + Some("pcx") => Ok(Self::load_pcx_file(path)?), + Some("iff") | Some("lbm") | Some("pbm") | Some("bbm") => { + Ok(Self::load_iff_file(path)?) + } + _ => Err(BitmapError::UnknownFileType(String::from( + "Unrecognized file extension", + ))), + } + } else { + Err(BitmapError::UnknownFileType(String::from( + "No file extension", + ))) + } + } + + /// Returns the width of the bitmap in pixels. + #[inline] + pub fn width(&self) -> u32 { + self.width + } + + /// Returns the height of the bitmap in pixels. + #[inline] + pub fn height(&self) -> u32 { + self.height + } + + /// Returns the right x coordinate of the bitmap. + #[inline] + pub fn right(&self) -> u32 { + self.width - 1 + } + + /// Returns the bottom x coordinate of the bitmap. + #[inline] + pub fn bottom(&self) -> u32 { + self.height - 1 + } + + /// Returns the current clipping region set on this bitmap. + #[inline] + pub fn clip_region(&self) -> &Rect { + &self.clip_region + } + + /// Returns a rect representing the full bitmap boundaries, ignoring the current clipping + /// region set on this bitmap. + #[inline] + pub fn full_bounds(&self) -> Rect { + Rect { + x: 0, + y: 0, + width: self.width, + height: self.height, + } + } + + /// Sets a new clipping region on this bitmap. The region will be automatically clamped to + /// the maximum bitmap boundaries if the supplied region extends beyond it. + /// + /// # Arguments + /// + /// * `region`: the new clipping region + pub fn set_clip_region(&mut self, region: &Rect) { + self.clip_region = *region; + self.clip_region.clamp_to(&self.full_bounds()); + } + + /// Resets the bitmaps clipping region back to the default (full boundaries of the bitmap). + pub fn reset_clip_region(&mut self) { + self.clip_region = self.full_bounds(); + } + + /// Returns a reference to the raw pixels in this bitmap. + #[inline] + pub fn pixels(&self) -> &[u8] { + &self.pixels + } + + /// Returns a mutable reference to the raw pixels in this bitmap. + #[inline] + pub fn pixels_mut(&mut self) -> &mut [u8] { + &mut self.pixels + } + + /// Returns a reference to the subset of the raw pixels in this bitmap beginning at the + /// given coordinates and extending to the end of the bitmap. If the coordinates given are + /// outside the bitmap's current clipping region, None is returned. + #[inline] + pub fn pixels_at(&self, x: i32, y: i32) -> Option<&[u8]> { + if self.is_xy_visible(x, y) { + let offset = self.get_offset_to_xy(x, y); + Some(&self.pixels[offset..]) + } else { + None + } + } + + /// Returns a mutable reference to the subset of the raw pixels in this bitmap beginning at the + /// given coordinates and extending to the end of the bitmap. If the coordinates given are + /// outside the bitmap's current clipping region, None is returned. + #[inline] + pub fn pixels_at_mut(&mut self, x: i32, y: i32) -> Option<&mut [u8]> { + if self.is_xy_visible(x, y) { + let offset = self.get_offset_to_xy(x, y); + Some(&mut self.pixels[offset..]) + } else { + None + } + } + + /// Returns an unsafe reference to the subset of the raw pixels in this bitmap beginning at the + /// given coordinates and extending to the end of the bitmap. The coordinates are not checked + /// for validity, so it is up to you to ensure they lie within the bounds of the bitmap. + #[inline] + pub unsafe fn pixels_at_unchecked(&self, x: i32, y: i32) -> &[u8] { + let offset = self.get_offset_to_xy(x, y); + slice::from_raw_parts(self.pixels.as_ptr().add(offset), self.pixels.len() - offset) + } + + /// Returns a mutable unsafe reference to the subset of the raw pixels in this bitmap beginning + /// at the given coordinates and extending to the end of the bitmap. The coordinates are not + /// checked for validity, so it is up to you to ensure they lie within the bounds of the bitmap. + #[inline] + pub unsafe fn pixels_at_mut_unchecked(&mut self, x: i32, y: i32) -> &mut [u8] { + let offset = self.get_offset_to_xy(x, y); + slice::from_raw_parts_mut( + self.pixels.as_mut_ptr().add(offset), + self.pixels.len() - offset, + ) + } + + /// Returns a pointer to the subset of the raw pixels in this bitmap beginning at the given + /// coordinates. If the coordinates given are outside the bitmap's current clipping region, + /// None is returned. + #[inline] + pub unsafe fn pixels_at_ptr(&self, x: i32, y: i32) -> Option<*const u8> { + if self.is_xy_visible(x, y) { + let offset = self.get_offset_to_xy(x, y); + Some(self.pixels.as_ptr().add(offset)) + } else { + None + } + } + + /// Returns a mutable pointer to the subset of the raw pixels in this bitmap beginning at the + /// given coordinates. If the coordinates given are outside the bitmap's current clipping + /// region, None is returned. + #[inline] + pub unsafe fn pixels_at_mut_ptr(&mut self, x: i32, y: i32) -> Option<*mut u8> { + if self.is_xy_visible(x, y) { + let offset = self.get_offset_to_xy(x, y); + Some(self.pixels.as_mut_ptr().add(offset)) + } else { + None + } + } + + /// Returns an unsafe pointer to the subset of the raw pixels in this bitmap beginning at the + /// given coordinates. The coordinates are not checked for validity, so it is up to you to + /// ensure they lie within the bounds of the bitmap. + #[inline] + pub unsafe fn pixels_at_ptr_unchecked(&self, x: i32, y: i32) -> *const u8 { + let offset = self.get_offset_to_xy(x, y); + self.pixels.as_ptr().add(offset) + } + + /// Returns a mutable unsafe pointer to the subset of the raw pixels in this bitmap beginning + /// at the given coordinates. The coordinates are not checked for validity, so it is up to you + /// to ensure they lie within the bounds of the bitmap. + #[inline] + pub unsafe fn pixels_at_mut_ptr_unchecked(&mut self, x: i32, y: i32) -> *mut u8 { + let offset = self.get_offset_to_xy(x, y); + self.pixels.as_mut_ptr().add(offset) + } + + /// Returns an offset corresponding to the coordinates of the pixel given on this bitmap that + /// can be used with a reference to the raw pixels in this bitmap to access that pixel. The + /// coordinates given are not checked for validity. + #[inline] + pub fn get_offset_to_xy(&self, x: i32, y: i32) -> usize { + ((y * self.width as i32) + x) as usize + } + + /// Returns true if the coordinates given lie within the bitmaps clipping region. + #[inline] + pub fn is_xy_visible(&self, x: i32, y: i32) -> bool { + (x >= self.clip_region.x) + && (y >= self.clip_region.y) + && (x <= self.clip_region.right()) + && (y <= self.clip_region.bottom()) + } + + /// Copies and converts the entire pixel data from this bitmap to a destination expecting + /// 32-bit ARGB-format pixel data. This can be used to display the contents of the bitmap + /// on-screen by using an SDL Surface, OpenGL texture, etc as the destination. + /// + /// # Arguments + /// + /// * `dest`: destination 32-bit ARGB pixel buffer to copy converted pixels to + /// * `palette`: the 256 colour palette to use during pixel conversion + pub fn copy_as_argb_to(&self, dest: &mut [u32], palette: &Palette) { + for (src, dest) in self.pixels().iter().zip(dest.iter_mut()) { + *dest = palette[*src]; + } + } +} + +#[cfg(test)] +pub mod tests { + use claim::assert_matches; + + use super::*; + + #[rustfmt::skip] + static RAW_BMP_PIXELS: &[u8] = &[ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 2, + ]; + + #[rustfmt::skip] + static RAW_BMP_PIXELS_SUBSET: &[u8] = &[ + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 2, + ]; + + #[test] + pub fn creation_and_sizing() { + assert_matches!(Bitmap::new(0, 0), Err(BitmapError::InvalidDimensions)); + assert_matches!(Bitmap::new(16, 0), Err(BitmapError::InvalidDimensions)); + assert_matches!(Bitmap::new(0, 32), Err(BitmapError::InvalidDimensions)); + let bmp = Bitmap::new(16, 32).unwrap(); + assert_eq!(16, bmp.width()); + assert_eq!(32, bmp.height()); + assert_eq!(15, bmp.right()); + assert_eq!(31, bmp.bottom()); + assert_eq!( + Rect { + x: 0, + y: 0, + width: 16, + height: 32 + }, + bmp.full_bounds() + ); + assert_eq!( + Rect { + x: 0, + y: 0, + width: 16, + height: 32 + }, + *bmp.clip_region() + ); + } + + #[test] + pub fn copy_from() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + assert_matches!( + Bitmap::from(&bmp, &Rect::new(0, 0, 16, 16)), + Err(BitmapError::OutOfBounds) + ); + + let copy = Bitmap::from(&bmp, &Rect::new(0, 0, 8, 8)).unwrap(); + assert_eq!(bmp.pixels(), copy.pixels()); + + let copy = Bitmap::from(&bmp, &Rect::new(4, 4, 4, 4)).unwrap(); + assert_eq!(RAW_BMP_PIXELS_SUBSET, copy.pixels()); + } + + #[test] + pub fn xy_offset_calculation() { + let bmp = Bitmap::new(20, 15).unwrap(); + assert_eq!(0, bmp.get_offset_to_xy(0, 0)); + assert_eq!(19, bmp.get_offset_to_xy(19, 0)); + assert_eq!(20, bmp.get_offset_to_xy(0, 1)); + assert_eq!(280, bmp.get_offset_to_xy(0, 14)); + assert_eq!(299, bmp.get_offset_to_xy(19, 14)); + assert_eq!(227, bmp.get_offset_to_xy(7, 11)); + } + + #[test] + pub fn bounds_testing_and_clip_region() { + let mut bmp = Bitmap::new(16, 8).unwrap(); + assert!(bmp.is_xy_visible(0, 0)); + assert!(bmp.is_xy_visible(15, 0)); + assert!(bmp.is_xy_visible(0, 7)); + assert!(bmp.is_xy_visible(15, 7)); + assert!(!bmp.is_xy_visible(-1, -1)); + assert!(!bmp.is_xy_visible(16, 8)); + assert!(!bmp.is_xy_visible(4, -2)); + assert!(!bmp.is_xy_visible(11, 8)); + assert!(!bmp.is_xy_visible(-1, 3)); + assert!(!bmp.is_xy_visible(16, 6)); + + let new_clip_region = Rect::from_coords(4, 2, 12, 6); + bmp.set_clip_region(&new_clip_region); + assert_eq!( + Rect { + x: 0, + y: 0, + width: 16, + height: 8 + }, + bmp.full_bounds() + ); + assert_eq!(new_clip_region, *bmp.clip_region()); + assert!(bmp.is_xy_visible(4, 2)); + assert!(bmp.is_xy_visible(12, 2)); + assert!(bmp.is_xy_visible(4, 6)); + assert!(bmp.is_xy_visible(12, 6)); + assert!(!bmp.is_xy_visible(3, 1)); + assert!(!bmp.is_xy_visible(13, 7)); + assert!(!bmp.is_xy_visible(5, 1)); + assert!(!bmp.is_xy_visible(10, 7)); + assert!(!bmp.is_xy_visible(3, 4)); + assert!(!bmp.is_xy_visible(13, 5)); + + assert!(!bmp.is_xy_visible(0, 0)); + assert!(!bmp.is_xy_visible(15, 0)); + assert!(!bmp.is_xy_visible(0, 7)); + assert!(!bmp.is_xy_visible(15, 7)); + bmp.reset_clip_region(); + assert!(bmp.is_xy_visible(0, 0)); + assert!(bmp.is_xy_visible(15, 0)); + assert!(bmp.is_xy_visible(0, 7)); + assert!(bmp.is_xy_visible(15, 7)); + assert!(!bmp.is_xy_visible(-1, -1)); + assert!(!bmp.is_xy_visible(16, 8)); + } + + #[test] + pub fn pixels_at() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + assert_eq!(None, bmp.pixels_at(-1, -1)); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = bmp.pixels_at(0, 0).unwrap(); + assert_eq!(64, pixels.len()); + assert_eq!(0, pixels[0]); + assert_eq!(1, pixels[offset]); + assert_eq!(2, pixels[63]); + + let pixels = bmp.pixels_at(1, 1).unwrap(); + assert_eq!(55, pixels.len()); + assert_eq!(1, pixels[0]); + assert_eq!(2, pixels[54]); + } + + #[test] + pub fn pixels_at_mut() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + assert_eq!(None, bmp.pixels_at_mut(-1, -1)); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = bmp.pixels_at_mut(0, 0).unwrap(); + assert_eq!(64, pixels.len()); + assert_eq!(0, pixels[0]); + assert_eq!(1, pixels[offset]); + assert_eq!(2, pixels[63]); + + let pixels = bmp.pixels_at_mut(1, 1).unwrap(); + assert_eq!(55, pixels.len()); + assert_eq!(1, pixels[0]); + assert_eq!(2, pixels[54]); + } + + #[test] + pub fn pixels_at_unchecked() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = unsafe { bmp.pixels_at_unchecked(0, 0) }; + assert_eq!(64, pixels.len()); + assert_eq!(0, pixels[0]); + assert_eq!(1, pixels[offset]); + assert_eq!(2, pixels[63]); + + let pixels = unsafe { bmp.pixels_at_unchecked(1, 1) }; + assert_eq!(55, pixels.len()); + assert_eq!(1, pixels[0]); + assert_eq!(2, pixels[54]); + } + + #[test] + pub fn pixels_at_mut_unchecked() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = unsafe { bmp.pixels_at_mut_unchecked(0, 0) }; + assert_eq!(64, pixels.len()); + assert_eq!(0, pixels[0]); + assert_eq!(1, pixels[offset]); + assert_eq!(2, pixels[63]); + + let pixels = unsafe { bmp.pixels_at_mut_unchecked(1, 1) }; + assert_eq!(55, pixels.len()); + assert_eq!(1, pixels[0]); + assert_eq!(2, pixels[54]); + } + + #[test] + pub fn pixels_at_ptr() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + assert_eq!(None, unsafe { bmp.pixels_at_ptr(-1, -1) }); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = unsafe { bmp.pixels_at_ptr(0, 0).unwrap() }; + assert_eq!(0, unsafe { *pixels }); + assert_eq!(1, unsafe { *(pixels.add(offset)) }); + assert_eq!(2, unsafe { *(pixels.add(63)) }); + + let pixels = unsafe { bmp.pixels_at_ptr(1, 1).unwrap() }; + assert_eq!(1, unsafe { *pixels }); + assert_eq!(2, unsafe { *(pixels.add(54)) }); + } + + #[test] + pub fn pixels_at_mut_ptr() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + assert_eq!(None, unsafe { bmp.pixels_at_mut_ptr(-1, -1) }); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = unsafe { bmp.pixels_at_mut_ptr(0, 0).unwrap() }; + assert_eq!(0, unsafe { *pixels }); + assert_eq!(1, unsafe { *(pixels.add(offset)) }); + assert_eq!(2, unsafe { *(pixels.add(63)) }); + + let pixels = unsafe { bmp.pixels_at_mut_ptr(1, 1).unwrap() }; + assert_eq!(1, unsafe { *pixels }); + assert_eq!(2, unsafe { *(pixels.add(54)) }); + } + + #[test] + pub fn pixels_at_ptr_unchecked() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = unsafe { bmp.pixels_at_ptr_unchecked(0, 0) }; + assert_eq!(0, unsafe { *pixels }); + assert_eq!(1, unsafe { *(pixels.add(offset)) }); + assert_eq!(2, unsafe { *(pixels.add(63)) }); + + let pixels = unsafe { bmp.pixels_at_ptr_unchecked(1, 1) }; + assert_eq!(1, unsafe { *pixels }); + assert_eq!(2, unsafe { *(pixels.add(54)) }); + } + + #[test] + pub fn pixels_at_mut_ptr_unchecked() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = unsafe { bmp.pixels_at_mut_ptr_unchecked(0, 0) }; + assert_eq!(0, unsafe { *pixels }); + assert_eq!(1, unsafe { *(pixels.add(offset)) }); + assert_eq!(2, unsafe { *(pixels.add(63)) }); + + let pixels = unsafe { bmp.pixels_at_mut_ptr_unchecked(1, 1) }; + assert_eq!(1, unsafe { *pixels }); + assert_eq!(2, unsafe { *(pixels.add(54)) }); + } +} diff --git a/libretrogd/src/graphics/bitmap/pcx.rs b/libretrogd/src/graphics/bitmap/pcx.rs new file mode 100644 index 0000000..bc281e4 --- /dev/null +++ b/libretrogd/src/graphics/bitmap/pcx.rs @@ -0,0 +1,322 @@ +use std::fs::File; +use std::io::{BufReader, BufWriter, Cursor, Seek, SeekFrom}; +use std::path::Path; + +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use thiserror::Error; + +use crate::utils::bytes::ReadFixedLengthByteArray; +use crate::{from_rgb32, Bitmap, Palette, PaletteError, PaletteFormat}; + +#[derive(Error, Debug)] +pub enum PcxError { + #[error("Bad or unsupported PCX file: {0}")] + BadFile(String), + + #[error("PCX palette data error")] + BadPalette(#[from] PaletteError), + + #[error("PCX I/O error")] + IOError(#[from] std::io::Error), +} + +#[derive(Debug, Copy, Clone)] +#[repr(packed)] +struct PcxHeader { + manufacturer: u8, + version: u8, + encoding: u8, + bpp: u8, + x1: u16, + y1: u16, + x2: u16, + y2: u16, + horizontal_dpi: u16, + vertical_dpi: u16, + ega_palette: [u8; 48], + reserved: u8, + num_color_planes: u8, + bytes_per_line: u16, + palette_type: u16, + horizontal_size: u16, + vertical_size: u16, + padding: [u8; 54], +} + +impl PcxHeader { + pub fn read(reader: &mut T) -> Result { + Ok(PcxHeader { + manufacturer: reader.read_u8()?, + version: reader.read_u8()?, + encoding: reader.read_u8()?, + bpp: reader.read_u8()?, + x1: reader.read_u16::()?, + y1: reader.read_u16::()?, + x2: reader.read_u16::()?, + y2: reader.read_u16::()?, + horizontal_dpi: reader.read_u16::()?, + vertical_dpi: reader.read_u16::()?, + ega_palette: reader.read_bytes()?, + reserved: reader.read_u8()?, + num_color_planes: reader.read_u8()?, + bytes_per_line: reader.read_u16::()?, + palette_type: reader.read_u16::()?, + horizontal_size: reader.read_u16::()?, + vertical_size: reader.read_u16::()?, + padding: reader.read_bytes()?, + }) + } + + pub fn write(&self, writer: &mut T) -> Result<(), PcxError> { + writer.write_u8(self.manufacturer)?; + writer.write_u8(self.version)?; + writer.write_u8(self.encoding)?; + writer.write_u8(self.bpp)?; + writer.write_u16::(self.x1)?; + writer.write_u16::(self.y1)?; + writer.write_u16::(self.x2)?; + writer.write_u16::(self.y2)?; + writer.write_u16::(self.horizontal_dpi)?; + writer.write_u16::(self.vertical_dpi)?; + writer.write_all(&self.ega_palette)?; + writer.write_u8(self.reserved)?; + writer.write_u8(self.num_color_planes)?; + writer.write_u16::(self.bytes_per_line)?; + writer.write_u16::(self.palette_type)?; + writer.write_u16::(self.horizontal_size)?; + writer.write_u16::(self.vertical_size)?; + writer.write_all(&self.padding)?; + Ok(()) + } +} + +fn write_pcx_data( + writer: &mut T, + run_count: u8, + pixel: u8, +) -> Result<(), PcxError> { + if (run_count > 1) || ((pixel & 0xc0) == 0xc0) { + writer.write_u8(0xc0 | run_count)?; + } + writer.write_u8(pixel)?; + Ok(()) +} + +impl Bitmap { + pub fn load_pcx_bytes( + reader: &mut T, + ) -> Result<(Bitmap, Palette), PcxError> { + let header = PcxHeader::read(reader)?; + + if header.manufacturer != 10 { + return Err(PcxError::BadFile(String::from( + "Unexpected header.manufacturer value, probably not a PCX file", + ))); + } + if header.version != 5 { + return Err(PcxError::BadFile(String::from( + "Only version 5 PCX files are supported", + ))); + } + if header.encoding != 1 { + return Err(PcxError::BadFile(String::from( + "Only RLE-compressed PCX files are supported", + ))); + } + if header.bpp != 8 { + return Err(PcxError::BadFile(String::from( + "Only 8-bit indexed (256 color palette) PCX files are supported", + ))); + } + if header.x2 == 0 || header.y2 == 0 { + return Err(PcxError::BadFile(String::from( + "Invalid PCX image dimensions", + ))); + } + + // read the PCX file's pixel data into a bitmap + + let width = (header.x2 + 1) as u32; + let height = (header.y2 + 1) as u32; + let mut bmp = Bitmap::new(width, height).unwrap(); + let mut writer = Cursor::new(bmp.pixels_mut()); + + for _y in 0..height { + // read the next scanline's worth of pixels from the PCX file + let mut x: u32 = 0; + while x < (header.bytes_per_line as u32) { + let mut data: u8; + let mut count: u32; + + // read pixel or RLE count + data = reader.read_u8()?; + + if (data & 0xc0) == 0xc0 { + // it was an RLE count, actual pixel is the next byte ... + count = (data & 0x3f) as u32; + data = reader.read_u8()?; + } else { + // it was just a single pixel + count = 1; + } + + // write the current pixel value 'data' to the bitmap 'count' number of times + while count > 0 { + if x <= width { + writer.write_u8(data)?; + } else { + writer.seek(SeekFrom::Current(1))?; + } + + x += 1; + count -= 1; + } + } + } + + // now read the palette data located at the end of the PCX file + // palette data should be for 256 colors, 3 bytes per color = 768 bytes + // the palette is preceded by a single byte, 0x0c, which we will also validate + + reader.seek(SeekFrom::End(-769))?; + + let palette_marker = reader.read_u8()?; + if palette_marker != 0x0c { + return Err(PcxError::BadFile(String::from( + "Palette not found at end of file", + ))); + } + + let palette = Palette::load_from_bytes(reader, PaletteFormat::Normal)?; + + Ok((bmp, palette)) + } + + pub fn load_pcx_file(path: &Path) -> Result<(Bitmap, Palette), PcxError> { + let f = File::open(path)?; + let mut reader = BufReader::new(f); + Self::load_pcx_bytes(&mut reader) + } + + pub fn to_pcx_bytes( + &self, + writer: &mut T, + palette: &Palette, + ) -> Result<(), PcxError> { + let header = PcxHeader { + manufacturer: 10, + version: 5, + encoding: 1, + bpp: 8, + x1: 0, + y1: 0, + x2: self.right() as u16, + y2: self.bottom() as u16, + horizontal_dpi: 320, + vertical_dpi: 200, + ega_palette: [0u8; 48], + reserved: 0, + num_color_planes: 1, + bytes_per_line: self.width() as u16, + palette_type: 1, + horizontal_size: self.width() as u16, + vertical_size: self.height() as u16, + padding: [0u8; 54], + }; + header.write(writer)?; + + let pixels = self.pixels(); + let mut i = 0; + + for _y in 0..=self.bottom() { + // write one scanline at a time. breaking runs that could have continued across + // scanlines in the process, as per the pcx standard + + let mut run_count = 0; + let mut run_pixel = 0; + + for _x in 0..=self.right() { + let pixel = pixels[i]; + i += 1; + + if run_count == 0 { + run_count = 1; + run_pixel = pixel; + } else { + if (pixel != run_pixel) || (run_count >= 63) { + write_pcx_data(writer, run_count, run_pixel)?; + run_count = 1; + run_pixel = pixel; + } else { + run_count += 1; + } + } + } + + // end the scanline, writing out whatever run we might have had going + write_pcx_data(writer, run_count, run_pixel)?; + } + + // marker for beginning of palette data + writer.write_u8(0xc)?; + + for i in 0..=255 { + let argb = palette[i]; + let (r, g, b) = from_rgb32(argb); + writer.write_u8(r)?; + writer.write_u8(g)?; + writer.write_u8(b)?; + } + + Ok(()) + } + + pub fn to_pcx_file(&self, path: &Path, palette: &Palette) -> Result<(), PcxError> { + let f = File::create(path)?; + let mut writer = BufWriter::new(f); + self.to_pcx_bytes(&mut writer, palette) + } +} + +#[cfg(test)] +pub mod tests { + use tempfile::TempDir; + + use super::*; + + pub static TEST_BMP_PIXELS_RAW: &[u8] = + include_bytes!("../../../test-assets/test_bmp_pixels_raw.bin"); + + #[test] + pub fn load_and_save() -> Result<(), PcxError> { + let dp2_palette = + Palette::load_from_file(Path::new("./test-assets/dp2.pal"), PaletteFormat::Normal) + .unwrap(); + let tmp_dir = TempDir::new()?; + + let (bmp, palette) = Bitmap::load_pcx_file(Path::new("./test-assets/test.pcx"))?; + assert_eq!(16, bmp.width()); + assert_eq!(16, bmp.height()); + assert_eq!(bmp.pixels(), TEST_BMP_PIXELS_RAW); + assert_eq!(palette, dp2_palette); + + let save_path = tmp_dir.path().join("test_save.pcx"); + bmp.to_pcx_file(&save_path, &palette)?; + let (reloaded_bmp, reloaded_palette) = Bitmap::load_pcx_file(&save_path)?; + assert_eq!(16, reloaded_bmp.width()); + assert_eq!(16, reloaded_bmp.height()); + assert_eq!(reloaded_bmp.pixels(), TEST_BMP_PIXELS_RAW); + assert_eq!(reloaded_palette, dp2_palette); + + Ok(()) + } + + #[test] + pub fn load_larger_image() -> Result<(), PcxError> { + let (bmp, _palette) = Bitmap::load_pcx_file(Path::new("./test-assets/test_image.pcx"))?; + assert_eq!(320, bmp.width()); + assert_eq!(200, bmp.height()); + + Ok(()) + } +} diff --git a/libretrogd/src/graphics/bitmap/primitives.rs b/libretrogd/src/graphics/bitmap/primitives.rs new file mode 100644 index 0000000..1093d94 --- /dev/null +++ b/libretrogd/src/graphics/bitmap/primitives.rs @@ -0,0 +1,378 @@ +use std::mem::swap; + +use crate::{Bitmap, Character, Font, FontRenderOpts, Rect}; + +impl Bitmap { + /// Fills the entire bitmap with the given color. + pub fn clear(&mut self, color: u8) { + self.pixels.fill(color); + } + + /// Sets the pixel at the given coordinates to the color specified. If the coordinates lie + /// outside of the bitmaps clipping region, no pixels will be changed. + #[inline] + pub fn set_pixel(&mut self, x: i32, y: i32, color: u8) { + if let Some(pixels) = self.pixels_at_mut(x, y) { + pixels[0] = color; + } + } + + /// Sets the pixel at the given coordinates to the color specified. The coordinates are not + /// checked for validity, so it is up to you to ensure they lie within the bounds of the + /// bitmap. + #[inline] + pub unsafe fn set_pixel_unchecked(&mut self, x: i32, y: i32, color: u8) { + let p = self.pixels_at_mut_ptr_unchecked(x, y); + *p = color; + } + + /// Gets the pixel at the given coordinates. If the coordinates lie outside of the bitmaps + /// clipping region, None is returned. + #[inline] + pub fn get_pixel(&self, x: i32, y: i32) -> Option { + if let Some(pixels) = self.pixels_at(x, y) { + Some(pixels[0]) + } else { + None + } + } + + /// Gets the pixel at the given coordinates. The coordinates are not checked for validity, so + /// it is up to you to ensure they lie within the bounds of the bitmap. + #[inline] + pub unsafe fn get_pixel_unchecked(&self, x: i32, y: i32) -> u8 { + *(self.pixels_at_ptr_unchecked(x, y)) + } + + /// Renders a single character using the font given. + #[inline] + pub fn print_char(&mut self, ch: char, x: i32, y: i32, color: u8, font: &T) { + font.character(ch) + .draw(self, x, y, FontRenderOpts::Color(color)); + } + + /// Renders the string of text using the font given. + pub fn print_string(&mut self, text: &str, x: i32, y: i32, color: u8, font: &T) { + let mut current_x = x; + let mut current_y = y; + for ch in text.chars() { + match ch { + ' ' => current_x += font.space_width() as i32, + '\n' => { + current_x = x; + current_y += font.line_height() as i32 + } + '\r' => (), + otherwise => { + self.print_char(otherwise, current_x, current_y, color, font); + current_x += font.character(otherwise).bounds().width as i32; + } + } + } + } + + /// Draws a line from x1,y1 to x2,y2. + pub fn line(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, color: u8) { + let mut dx = x1; + let mut dy = y1; + let delta_x = x2 - x1; + let delta_y = y2 - y1; + let delta_x_abs = delta_x.abs(); + let delta_y_abs = delta_y.abs(); + let delta_x_sign = delta_x.signum(); + let delta_y_sign = delta_y.signum(); + let mut x = delta_x_abs / 2; + let mut y = delta_y_abs / 2; + let offset_x_inc = delta_x_sign; + let offset_y_inc = delta_y_sign * self.width as i32; + + unsafe { + // safety: while we are blindly getting a pointer to this x/y coordinate, we don't + // write to it unless we know the coordinates are in bounds. + // TODO: should be ok ... ? or am i making too many assumptions about memory layout? + let mut dest = self.pixels_at_mut_ptr_unchecked(x1, y1); + + if self.is_xy_visible(dx, dy) { + *dest = color; + } + + if delta_x_abs >= delta_y_abs { + for _ in 0..delta_x_abs { + y += delta_y_abs; + + if y >= delta_x_abs { + y -= delta_x_abs; + dy += delta_y_sign; + dest = dest.offset(offset_y_inc as isize); + } + + dx += delta_x_sign; + dest = dest.offset(offset_x_inc as isize); + + if self.is_xy_visible(dx, dy) { + *dest = color; + } + } + } else { + for _ in 0..delta_y_abs { + x += delta_x_abs; + + if x >= delta_y_abs { + x -= delta_y_abs; + dx += delta_x_sign; + dest = dest.offset(offset_x_inc as isize); + } + + dy += delta_y_sign; + dest = dest.offset(offset_y_inc as isize); + + if self.is_xy_visible(dx, dy) { + *dest = color; + } + } + } + } + } + + /// Draws a horizontal line from x1,y to x2,y. + pub fn horiz_line(&mut self, x1: i32, x2: i32, y: i32, color: u8) { + let mut region = Rect::from_coords(x1, y, x2, y); + if region.clamp_to(&self.clip_region) { + unsafe { + let dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y); + dest.write_bytes(color, region.width as usize); + } + } + } + + /// Draws a vertical line from x,y1 to x,y2. + pub fn vert_line(&mut self, x: i32, y1: i32, y2: i32, color: u8) { + let mut region = Rect::from_coords(x, y1, x, y2); + if region.clamp_to(&self.clip_region) { + unsafe { + let mut dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y); + for _ in 0..region.height { + *dest = color; + dest = dest.add(self.width as usize); + } + } + } + } + + /// Draws an empty box (rectangle) using the points x1,y1 and x2,y2 to form the box to be + /// drawn, assuming they are specifying the top-left and bottom-right corners respectively. + pub fn rect(&mut self, mut x1: i32, mut y1: i32, mut x2: i32, mut y2: i32, color: u8) { + // note: need to manually do all this instead of just relying on Rect::from_coords (which + // could otherwise figure all this out for us) mainly just because we need the post-swap + // x1,y1,x2,y2 values for post-region-clamping comparison purposes ... + if x2 < x1 { + swap(&mut x1, &mut x2); + } + if y2 < y1 { + swap(&mut y1, &mut y2); + } + let mut region = Rect { + x: x1, + y: y1, + width: (x2 - x1 + 1) as u32, + height: (y2 - y1 + 1) as u32, + }; + if !region.clamp_to(&self.clip_region) { + return; + } + + // top line, only if y1 was originally within bounds + if y1 == region.y { + unsafe { + let dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y); + dest.write_bytes(color, region.width as usize); + } + } + + // bottom line, only if y2 was originally within bounds + if y2 == region.bottom() { + unsafe { + let dest = self.pixels_at_mut_ptr_unchecked(region.x, region.bottom()); + dest.write_bytes(color, region.width as usize); + } + } + + // left line, only if x1 was originally within bounds + if x1 == region.x { + unsafe { + let mut dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y); + for _ in 0..region.height { + *dest = color; + dest = dest.add(self.width as usize); + } + } + } + + // right line, only if x2 was originally within bounds + if x2 == region.right() { + unsafe { + let mut dest = self.pixels_at_mut_ptr_unchecked(region.right(), region.y); + for _ in 0..region.height { + *dest = color; + dest = dest.add(self.width as usize); + } + } + } + } + + /// Draws a filled box (rectangle) using the points x1,y1 and x2,y2 to form the box to be + /// drawn, assuming they are specifying the top-left and bottom-right corners respectively. + pub fn filled_rect(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, color: u8) { + let mut region = Rect::from_coords(x1, y1, x2, y2); + if region.clamp_to(&self.clip_region) { + unsafe { + let mut dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y); + for _ in 0..region.height { + dest.write_bytes(color, region.width as usize); + dest = dest.add(self.width as usize); + } + } + } + } + + /// Draws the outline of a circle formed by the center point and radius given. + pub fn circle(&mut self, center_x: i32, center_y: i32, radius: u32, color: u8) { + // TODO: optimize + let mut x = 0; + let mut y = radius as i32; + let mut m = 5 - 4 * radius as i32; + + while x <= y { + self.set_pixel(center_x + x, center_y + y, color); + self.set_pixel(center_x + x, center_y - y, color); + self.set_pixel(center_x - x, center_y + y, color); + self.set_pixel(center_x - x, center_y - y, color); + self.set_pixel(center_x + y, center_y + x, color); + self.set_pixel(center_x + y, center_y - x, color); + self.set_pixel(center_x - y, center_y + x, color); + self.set_pixel(center_x - y, center_y - x, color); + + if m > 0 { + y -= 1; + m -= 8 * y; + } + + x += 1; + m += 8 * x + 4; + } + } + + /// Draws a filled circle formed by the center point and radius given. + pub fn filled_circle(&mut self, center_x: i32, center_y: i32, radius: u32, color: u8) { + // TODO: optimize + let mut x = 0; + let mut y = radius as i32; + let mut m = 5 - 4 * radius as i32; + + while x <= y { + self.horiz_line(center_x - x, center_x + x, center_y - y, color); + self.horiz_line(center_x - y, center_x + y, center_y - x, color); + self.horiz_line(center_x - y, center_x + y, center_y + x, color); + self.horiz_line(center_x - x, center_x + x, center_y + y, color); + + if m > 0 { + y -= 1; + m -= 8 * y; + } + + x += 1; + m += 8 * x + 4; + } + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[rustfmt::skip] + #[test] + pub fn set_and_get_pixel() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + + assert_eq!(None, bmp.get_pixel(-1, -1)); + + assert_eq!(0, bmp.get_pixel(0, 0).unwrap()); + bmp.set_pixel(0, 0, 7); + assert_eq!(7, bmp.get_pixel(0, 0).unwrap()); + + assert_eq!( + bmp.pixels(), + &[ + 7, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ] + ); + + assert_eq!(0, bmp.get_pixel(2, 4).unwrap()); + bmp.set_pixel(2, 4, 5); + assert_eq!(5, bmp.get_pixel(2, 4).unwrap()); + + assert_eq!( + bmp.pixels(), + &[ + 7, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 5, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ] + ); + } + + #[rustfmt::skip] + #[test] + pub fn set_and_get_pixel_unchecked() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + + assert_eq!(0, unsafe { bmp.get_pixel_unchecked(0, 0) }); + unsafe { bmp.set_pixel_unchecked(0, 0, 7) }; + assert_eq!(7, unsafe { bmp.get_pixel_unchecked(0, 0) }); + + assert_eq!( + bmp.pixels(), + &[ + 7, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ] + ); + + assert_eq!(0, unsafe { bmp.get_pixel_unchecked(2, 4) }); + unsafe { bmp.set_pixel_unchecked(2, 4, 5) }; + assert_eq!(5, unsafe { bmp.get_pixel_unchecked(2, 4) }); + + assert_eq!( + bmp.pixels(), + &[ + 7, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 5, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ] + ); + } +} diff --git a/libretrogd/src/graphics/bitmapatlas.rs b/libretrogd/src/graphics/bitmapatlas.rs new file mode 100644 index 0000000..423b1f7 --- /dev/null +++ b/libretrogd/src/graphics/bitmapatlas.rs @@ -0,0 +1,178 @@ +use std::ops::Index; + +use thiserror::Error; + +use crate::{Bitmap, Rect}; + +#[derive(Error, Debug)] +pub enum BitmapAtlasError { + #[error("Region is out of bounds for the Bitmap used by the BitmapAtlas")] + OutOfBounds, +} + +#[derive(Debug)] +pub struct BitmapAtlas { + bitmap: Bitmap, + bounds: Rect, + tiles: Vec, +} + +impl BitmapAtlas { + pub fn new(bitmap: Bitmap) -> BitmapAtlas { + let bounds = bitmap.full_bounds(); + BitmapAtlas { + bitmap, + bounds, + tiles: Vec::new(), + } + } + + pub fn add(&mut self, rect: Rect) -> Result { + if !self.bounds.contains_rect(&rect) { + return Err(BitmapAtlasError::OutOfBounds); + } + + self.tiles.push(rect); + Ok(self.tiles.len() - 1) + } + + pub fn add_grid( + &mut self, + tile_width: u32, + tile_height: u32, + ) -> Result { + if self.bounds.width < tile_width || self.bounds.height < tile_height { + return Err(BitmapAtlasError::OutOfBounds); + } + + for yt in 0..(self.bounds.height / tile_height) { + for xt in 0..(self.bounds.width) / tile_width { + let x = xt * tile_width; + let y = yt * tile_height; + let rect = Rect::new(x as i32, y as i32, tile_width, tile_height); + self.tiles.push(rect); + } + } + + Ok(self.tiles.len() - 1) + } + + pub fn add_custom_grid( + &mut self, + start_x: u32, + start_y: u32, + tile_width: u32, + tile_height: u32, + x_tiles: u32, + y_tiles: u32, + border: u32, + ) -> Result { + // figure out of the grid properties given would result in us creating any + // rects that lie out of the bounds of this bitmap + let grid_region = Rect::new( + start_x as i32, + start_y as i32, + (tile_width + border) * x_tiles + border, + (tile_height + border) * y_tiles + border, + ); + if !self.bounds.contains_rect(&grid_region) { + return Err(BitmapAtlasError::OutOfBounds); + } + + // all good! now create all the tiles needed for the grid specified + for yt in 0..y_tiles { + for xt in 0..x_tiles { + let x = start_x + (tile_width + border) * xt; + let y = start_y + (tile_height + border) * yt; + let rect = Rect::new(x as i32, y as i32, tile_width, tile_height); + self.tiles.push(rect); + } + } + + Ok(self.tiles.len() - 1) + } + + pub fn clear(&mut self) { + self.tiles.clear() + } + + pub fn len(&self) -> usize { + self.tiles.len() + } + + pub fn bitmap(&self) -> &Bitmap { + &self.bitmap + } +} + +impl Index for BitmapAtlas { + type Output = Rect; + + fn index(&self, index: usize) -> &Self::Output { + &self.tiles[index] + } +} + +#[cfg(test)] +pub mod tests { + use claim::assert_matches; + + use super::*; + + #[test] + pub fn adding_rects() { + let bmp = Bitmap::new(64, 64).unwrap(); + let mut atlas = BitmapAtlas::new(bmp); + + let rect = Rect::new(0, 0, 16, 16); + assert_eq!(0, atlas.add(rect.clone()).unwrap()); + assert_eq!(rect, atlas[0]); + assert_eq!(1, atlas.len()); + + let rect = Rect::new(16, 0, 16, 16); + assert_eq!(1, atlas.add(rect.clone()).unwrap()); + assert_eq!(rect, atlas[1]); + assert_eq!(2, atlas.len()); + + assert_matches!( + atlas.add(Rect::new(56, 0, 16, 16)), + Err(BitmapAtlasError::OutOfBounds) + ); + assert_eq!(2, atlas.len()); + + assert_matches!( + atlas.add(Rect::new(-8, 4, 16, 16)), + Err(BitmapAtlasError::OutOfBounds) + ); + assert_eq!(2, atlas.len()); + + assert_matches!( + atlas.add(Rect::new(0, 0, 128, 128)), + Err(BitmapAtlasError::OutOfBounds) + ); + assert_eq!(2, atlas.len()); + } + + #[test] + pub fn adding_grid() { + let bmp = Bitmap::new(64, 64).unwrap(); + let mut atlas = BitmapAtlas::new(bmp); + + assert_eq!(3, atlas.add_custom_grid(0, 0, 8, 8, 2, 2, 0).unwrap()); + assert_eq!(4, atlas.len()); + assert_eq!(Rect::new(0, 0, 8, 8), atlas[0]); + assert_eq!(Rect::new(8, 0, 8, 8), atlas[1]); + assert_eq!(Rect::new(0, 8, 8, 8), atlas[2]); + assert_eq!(Rect::new(8, 8, 8, 8), atlas[3]); + + atlas.clear(); + assert_eq!(0, atlas.len()); + + assert_eq!(3, atlas.add_custom_grid(0, 0, 4, 8, 2, 2, 1).unwrap()); + assert_eq!(4, atlas.len()); + assert_eq!(Rect::new(0, 0, 4, 8), atlas[0]); + assert_eq!(Rect::new(5, 0, 4, 8), atlas[1]); + assert_eq!(Rect::new(0, 9, 4, 8), atlas[2]); + assert_eq!(Rect::new(5, 9, 4, 8), atlas[3]); + } +} diff --git a/libretrogd/src/graphics/font.rs b/libretrogd/src/graphics/font.rs new file mode 100644 index 0000000..47dc987 --- /dev/null +++ b/libretrogd/src/graphics/font.rs @@ -0,0 +1,202 @@ +use std::fs::File; +use std::io::{BufReader, BufWriter, Cursor}; +use std::path::Path; + +use byteorder::{ReadBytesExt, WriteBytesExt}; +use thiserror::Error; + +use crate::{Bitmap, Rect}; + +pub static VGA_FONT_BYTES: &[u8] = include_bytes!("../../assets/vga.fnt"); + +pub const NUM_CHARS: usize = 256; +pub const CHAR_HEIGHT: usize = 8; +pub const CHAR_FIXED_WIDTH: usize = 8; + +#[derive(Error, Debug)] +pub enum FontError { + #[error("Invalid font file: {0}")] + InvalidFile(String), + + #[error("Font I/O error")] + IOError(#[from] std::io::Error), +} + +#[derive(Debug)] +pub enum FontRenderOpts { + Color(u8), + None, +} + +pub trait Character { + fn bounds(&self) -> &Rect; + fn draw(&self, dest: &mut Bitmap, x: i32, y: i32, opts: FontRenderOpts); +} + +pub trait Font { + type CharacterType: Character; + + fn character(&self, ch: char) -> &Self::CharacterType; + fn space_width(&self) -> u8; + fn line_height(&self) -> u8; +} + +#[derive(Debug)] +pub struct BitmaskCharacter { + bytes: [u8; CHAR_HEIGHT], + bounds: Rect, +} + +impl Character for BitmaskCharacter { + #[inline] + fn bounds(&self) -> &Rect { + &self.bounds + } + + fn draw(&self, dest: &mut Bitmap, x: i32, y: i32, opts: FontRenderOpts) { + // out of bounds check + if ((x + self.bounds.width as i32) < dest.clip_region().x) + || ((y + self.bounds.height as i32) < dest.clip_region().y) + || (x >= dest.clip_region().right()) + || (y >= dest.clip_region().bottom()) + { + return; + } + + let color = match opts { + FontRenderOpts::Color(color) => color, + _ => 0, + }; + + // TODO: i'm sure this can be optimized, lol + for char_y in 0..self.bounds.height as usize { + let mut bit_mask = 0x80; + for char_x in 0..self.bounds.width as usize { + if self.bytes[char_y] & bit_mask > 0 { + dest.set_pixel(x + char_x as i32, y + char_y as i32, color); + } + bit_mask >>= 1; + } + } + } +} + +#[derive(Debug)] +pub struct BitmaskFont { + characters: Box<[BitmaskCharacter]>, + line_height: u8, + space_width: u8, +} + +impl BitmaskFont { + pub fn new_vga_font() -> Result { + BitmaskFont::load_from_bytes(&mut Cursor::new(VGA_FONT_BYTES)) + } + + pub fn load_from_file(path: &Path) -> Result { + let f = File::open(path)?; + let mut reader = BufReader::new(f); + + BitmaskFont::load_from_bytes(&mut reader) + } + + pub fn load_from_bytes(reader: &mut T) -> Result { + let mut characters: Vec = Vec::with_capacity(NUM_CHARS); + + // read character bitmap data + for _ in 0..NUM_CHARS { + let mut buffer = [0u8; CHAR_HEIGHT]; + reader.read_exact(&mut buffer)?; + let character = BitmaskCharacter { + bytes: buffer, + // bounds are filled in below. ugh. + bounds: Rect { + x: 0, + y: 0, + width: 0, + height: 0, + }, + }; + characters.push(character); + } + + // read character widths (used for rendering) + for i in 0..NUM_CHARS { + characters[i].bounds.width = reader.read_u8()? as u32; + } + + // read global font height (used for rendering) + let line_height = reader.read_u8()?; + for i in 0..NUM_CHARS { + characters[i].bounds.height = line_height as u32; + } + + let space_width = characters[' ' as usize].bounds.width as u8; + + Ok(BitmaskFont { + characters: characters.into_boxed_slice(), + line_height, + space_width, + }) + } + + pub fn to_file(&self, path: &Path) -> Result<(), FontError> { + let f = File::create(path)?; + let mut writer = BufWriter::new(f); + self.to_bytes(&mut writer) + } + + pub fn to_bytes(&self, writer: &mut T) -> Result<(), FontError> { + // write character bitmap data + for i in 0..NUM_CHARS { + writer.write_all(&self.characters[i].bytes)?; + } + + // write character widths + for i in 0..NUM_CHARS { + writer.write_u8(self.characters[i].bounds.width as u8)?; + } + + // write global font height + writer.write_u8(self.line_height)?; + + Ok(()) + } +} + +impl Font for BitmaskFont { + type CharacterType = BitmaskCharacter; + + #[inline] + fn character(&self, ch: char) -> &Self::CharacterType { + &self.characters[ch as usize] + } + + #[inline] + fn space_width(&self) -> u8 { + self.space_width + } + + #[inline] + fn line_height(&self) -> u8 { + self.line_height + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + pub fn load_font() -> Result<(), FontError> { + let font = BitmaskFont::load_from_file(Path::new("./assets/vga.fnt"))?; + assert_eq!(256, font.characters.len()); + assert_eq!(CHAR_FIXED_WIDTH as u8, font.space_width); + for character in font.characters.iter() { + assert_eq!(CHAR_FIXED_WIDTH as u8, character.bounds.width as u8); + assert_eq!(CHAR_HEIGHT, character.bytes.len()); + } + + Ok(()) + } +} diff --git a/libretrogd/src/graphics/mod.rs b/libretrogd/src/graphics/mod.rs new file mode 100644 index 0000000..631f704 --- /dev/null +++ b/libretrogd/src/graphics/mod.rs @@ -0,0 +1,4 @@ +pub mod bitmap; +pub mod bitmapatlas; +pub mod font; +pub mod palette; diff --git a/libretrogd/src/graphics/palette.rs b/libretrogd/src/graphics/palette.rs new file mode 100644 index 0000000..d9b5a59 --- /dev/null +++ b/libretrogd/src/graphics/palette.rs @@ -0,0 +1,563 @@ +use std::cmp::min; +use std::fs::File; +use std::io::{BufReader, BufWriter, Cursor}; +use std::ops::{Bound, Index, IndexMut, RangeBounds}; +use std::path::Path; + +use byteorder::{ReadBytesExt, WriteBytesExt}; +use thiserror::Error; + +use crate::utils::abs_diff; +use crate::NUM_COLORS; + +// silly "hack" (???) which allows us to alias the generic constraint `RangeBounds + Iterator` to `ColorRange` +pub trait ColorRange: RangeBounds + Iterator {} +impl ColorRange for T where T: RangeBounds + Iterator {} + +pub static VGA_PALETTE_BYTES: &[u8] = include_bytes!("../../assets/vga.pal"); + +/// Converts a set of individual ARGB components to a combined 32-bit color value, packed into +/// the format 0xAARRGGBB +/// +/// # Arguments +/// +/// * `a`: the alpha component (0-255) +/// * `r`: the red component (0-255) +/// * `g`: the green component (0-255) +/// * `b`: the blue component (0-255) +/// +/// returns: the u32 packed color +#[inline] +pub fn to_argb32(a: u8, r: u8, g: u8, b: u8) -> u32 { + (b as u32) + ((g as u32) << 8) + ((r as u32) << 16) + ((a as u32) << 24) +} + +/// Extracts the individual ARGB components out of a combined 32-bit color value which is in the +/// format 0xAARRGGBB +/// +/// # Arguments +/// +/// * `argb`: the 32-bit packed color +/// +/// returns: the individual ARGB color components (0-255 each) in order: alpha, red, green, blue +#[inline] +pub fn from_argb32(argb: u32) -> (u8, u8, u8, u8) { + let a = ((argb & 0xff000000) >> 24) as u8; + let r = ((argb & 0x00ff0000) >> 16) as u8; + let g = ((argb & 0x0000ff00) >> 8) as u8; + let b = (argb & 0x000000ff) as u8; + (a, r, g, b) +} + +/// Converts a set of individual RGB components to a combined 32-bit color value, packed into +/// the format 0xAARRGGBB. Substitutes a value of 255 for the missing alpha component. +/// +/// # Arguments +/// +/// * `r`: the red component (0-255) +/// * `g`: the green component (0-255) +/// * `b`: the blue component (0-255) +/// +/// returns: the u32 packed color +#[inline] +pub fn to_rgb32(r: u8, g: u8, b: u8) -> u32 { + to_argb32(255, r, g, b) +} + +/// Extracts the individual RGB components out of a combined 32-bit color value which is in the +/// format 0xAARRGGBB. Ignores the alpha component. +/// +/// # Arguments +/// +/// * `argb`: the 32-bit packed color +/// +/// returns: the individual ARGB color components (0-255 each) in order: red, green, blue +#[inline] +pub fn from_rgb32(rgb: u32) -> (u8, u8, u8) { + // ignore alpha component at 0xff000000 ... + let r = ((rgb & 0x00ff0000) >> 16) as u8; + let g = ((rgb & 0x0000ff00) >> 8) as u8; + let b = (rgb & 0x000000ff) as u8; + (r, g, b) +} + +/// Linearly interpolates between two 32-bit packed colors in the format 0xAARRGGBB. +/// +/// # Arguments +/// +/// * `a`: the first 32-bit packed color +/// * `b`: the second 32-bit packed color +/// * `t`: the amount to interpolate between the two values, specified as a fraction. +#[inline] +pub fn lerp_argb32(a: u32, b: u32, t: f32) -> u32 { + let (a1, r1, g1, b1) = from_argb32(a); + let (a2, r2, g2, b2) = from_argb32(b); + to_argb32( + ((a1 as f32) + ((a2 as f32) - (a1 as f32)) * t) as u8, + ((r1 as f32) + ((r2 as f32) - (r1 as f32)) * t) as u8, + ((g1 as f32) + ((g2 as f32) - (g1 as f32)) * t) as u8, + ((b1 as f32) + ((b2 as f32) - (b1 as f32)) * t) as u8, + ) +} + +/// Linearly interpolates between two 32-bit packed colors in the format 0xAARRGGBB. Ignores the +/// alpha component, which will always be set to 255 in the return value. +/// +/// # Arguments +/// +/// * `a`: the first 32-bit packed color +/// * `b`: the second 32-bit packed color +/// * `t`: the amount to interpolate between the two values, specified as a fraction. +#[inline] +pub fn lerp_rgb32(a: u32, b: u32, t: f32) -> u32 { + let (r1, g1, b1) = from_rgb32(a); + let (r2, g2, b2) = from_rgb32(b); + to_rgb32( + ((r1 as f32) + ((r2 as f32) - (r1 as f32)) * t) as u8, + ((g1 as f32) + ((g2 as f32) - (g1 as f32)) * t) as u8, + ((b1 as f32) + ((b2 as f32) - (b1 as f32)) * t) as u8, + ) +} + +// vga bios (0-63) format +fn read_256color_6bit_palette( + reader: &mut T, +) -> Result<[u32; NUM_COLORS], PaletteError> { + let mut colors = [0u32; NUM_COLORS]; + for color in colors.iter_mut() { + let r = reader.read_u8()?; + let g = reader.read_u8()?; + let b = reader.read_u8()?; + *color = to_rgb32(r * 4, g * 4, b * 4); + } + Ok(colors) +} + +fn write_256color_6bit_palette( + writer: &mut T, + colors: &[u32; NUM_COLORS], +) -> Result<(), PaletteError> { + for color in colors.iter() { + let (r, g, b) = from_rgb32(*color); + writer.write_u8(r / 4)?; + writer.write_u8(g / 4)?; + writer.write_u8(b / 4)?; + } + Ok(()) +} + +// normal (0-255) format +fn read_256color_8bit_palette( + reader: &mut T, +) -> Result<[u32; NUM_COLORS], PaletteError> { + let mut colors = [0u32; NUM_COLORS]; + for color in colors.iter_mut() { + let r = reader.read_u8()?; + let g = reader.read_u8()?; + let b = reader.read_u8()?; + *color = to_rgb32(r, g, b); + } + Ok(colors) +} + +fn write_256color_8bit_palette( + writer: &mut T, + colors: &[u32; NUM_COLORS], +) -> Result<(), PaletteError> { + for color in colors.iter() { + let (r, g, b) = from_rgb32(*color); + writer.write_u8(r)?; + writer.write_u8(g)?; + writer.write_u8(b)?; + } + Ok(()) +} + +#[derive(Error, Debug)] +pub enum PaletteError { + #[error("Palette I/O error")] + IOError(#[from] std::io::Error), +} + +pub enum PaletteFormat { + /// Individual RGB components in 6-bits (0-63) for VGA BIOS compatibility + Vga, + /// Individual RGB components in 8-bits (0-255) + Normal, +} + +/// Contains a 256 color palette, and provides methods useful for working with palettes. The +/// colors are all stored individually as 32-bit packed values in the format 0xAARRGGBB. +#[derive(Debug, Clone)] +pub struct Palette { + colors: [u32; NUM_COLORS], +} + +impl Palette { + /// Creates a new Palette with all black colors. + pub fn new() -> Palette { + Palette { + colors: [0; NUM_COLORS], + } + } + + /// Creates a new Palette, pre-loaded with the default VGA BIOS colors. + pub fn new_vga_palette() -> Result { + Palette::load_from_bytes(&mut Cursor::new(VGA_PALETTE_BYTES), PaletteFormat::Vga) + } + + /// Loads and returns a Palette from a palette file on disk. + /// + /// # Arguments + /// + /// * `path`: the path of the palette file to be loaded + /// * `format`: the format that the palette data is expected to be in + pub fn load_from_file(path: &Path, format: PaletteFormat) -> Result { + let f = File::open(path)?; + let mut reader = BufReader::new(f); + Self::load_from_bytes(&mut reader, format) + } + + /// Loads and returns a Palette from a reader. The data being loaded is expected to be the same + /// as if the palette was being loaded from a file on disk. + /// + /// # Arguments + /// + /// * `reader`: the reader to load the palette from + /// * `format`: the format that the palette data is expected to be in + pub fn load_from_bytes( + reader: &mut T, + format: PaletteFormat, + ) -> Result { + let colors = match format { + PaletteFormat::Vga => read_256color_6bit_palette(reader)?, + PaletteFormat::Normal => read_256color_8bit_palette(reader)?, + }; + Ok(Palette { colors }) + } + + /// Writes the palette to a file on disk. If the file already exists, it will be overwritten. + /// + /// # Arguments + /// + /// * `path`: the path of the file to save the palette to + /// * `format`: the format to write the palette data in + pub fn to_file(&self, path: &Path, format: PaletteFormat) -> Result<(), PaletteError> { + let f = File::create(path)?; + let mut writer = BufWriter::new(f); + self.to_bytes(&mut writer, format) + } + + /// Writes the palette to a writer, in the same format as if it was writing to a file on disk. + /// + /// # Arguments + /// + /// * `writer`: the writer to write palette data to + /// * `format`: the format to write the palette data in + pub fn to_bytes( + &self, + writer: &mut T, + format: PaletteFormat, + ) -> Result<(), PaletteError> { + match format { + PaletteFormat::Vga => write_256color_6bit_palette(writer, &self.colors), + PaletteFormat::Normal => write_256color_8bit_palette(writer, &self.colors), + } + } + + /// Fades a single color in the palette from its current RGB values towards the given RGB + /// values by up to the step amount given. This function is intended to be run many times + /// over a number of frames where each run completes a small step towards the complete fade. + /// + /// # Arguments + /// + /// * `color`: the color index to fade + /// * `target_r`: the target red component (0-255) to fade towards + /// * `target_g`: the target green component (0-255) to fade towards + /// * `target_b`: the target blue component (0-255) to fade towards + /// * `step`: the amount to "step" by towards the target RGB values + /// + /// returns: true if the color has reached the target RGB values, false otherwise + pub fn fade_color_toward_rgb( + &mut self, + color: u8, + target_r: u8, + target_g: u8, + target_b: u8, + step: u8, + ) -> bool { + let mut modified = false; + + let (mut r, mut g, mut b) = from_rgb32(self.colors[color as usize]); + + if r != target_r { + modified = true; + let diff_r = r.overflowing_sub(target_r).0; + if r > target_r { + r -= min(step, diff_r); + } else { + r += min(step, diff_r); + } + } + + if g != target_g { + modified = true; + let diff_g = g.overflowing_sub(target_g).0; + if g > target_g { + g -= min(step, diff_g); + } else { + g += min(step, diff_g); + } + } + + if b != target_b { + modified = true; + let diff_b = b.overflowing_sub(target_b).0; + if b > target_b { + b -= min(step, diff_b); + } else { + b += min(step, diff_b); + } + } + + if modified { + self.colors[color as usize] = to_rgb32(r, g, b); + } + + (target_r == r) && (target_g == g) && (target_b == b) + } + + /// Fades a range of colors in the palette from their current RGB values all towards the given + /// RGB values by up to the step amount given. This function is intended to be run many times + /// over a number of frames where each run completes a small step towards the complete fade. + /// + /// # Arguments + /// + /// * `colors`: the range of colors to be faded + /// * `target_r`: the target red component (0-255) to fade towards + /// * `target_g`: the target green component (0-255) to fade towards + /// * `target_b`: the target blue component (0-255) to fade towards + /// * `step`: the amount to "step" by towards the target RGB values + /// + /// returns: true if all of the colors in the range have reached the target RGB values, false + /// otherwise + pub fn fade_colors_toward_rgb( + &mut self, + colors: T, + target_r: u8, + target_g: u8, + target_b: u8, + step: u8, + ) -> bool { + let mut all_faded = true; + for color in colors { + if !self.fade_color_toward_rgb(color, target_r, target_g, target_b, step) { + all_faded = false; + } + } + all_faded + } + + /// Fades a range of colors in the palette from their current RGB values all towards the RGB + /// values in the other palette specified, by up to the step amount given. This function is + /// intended to be run many times over a number of frames where each run completes a small step + /// towards the complete fade. + /// + /// # Arguments + /// + /// * `colors`: the range of colors to be faded + /// * `palette`: the other palette to use as the target to fade towards + /// * `step`: the amount to "step" by towards the target RGB values + /// + /// returns: true if all of the colors in the range have reached the RGB values from the other + /// target palette, false otherwise + pub fn fade_colors_toward_palette( + &mut self, + colors: T, + palette: &Palette, + step: u8, + ) -> bool { + let mut all_faded = true; + for color in colors { + let (r, g, b) = from_rgb32(palette[color]); + if !self.fade_color_toward_rgb(color, r, g, b, step) { + all_faded = false; + } + } + all_faded + } + + /// Linearly interpolates between the specified colors in two palettes, storing the + /// interpolation results in this palette. + /// + /// # Arguments + /// + /// * `colors`: the range of colors to be interpolated + /// * `a`: the first palette + /// * `b`: the second palette + /// * `t`: the amount to interpolate between the two palettes, specified as a fraction + pub fn lerp(&mut self, colors: T, a: &Palette, b: &Palette, t: f32) { + for color in colors { + self[color] = lerp_rgb32(a[color], b[color], t); + } + } + + /// Rotates a range of colors in the palette by a given amount. + /// + /// # Arguments + /// + /// * `colors`: the range of colors to be rotated + /// * `step`: the number of positions (and direction) to rotate all colors by + pub fn rotate_colors(&mut self, colors: T, step: i8) { + use Bound::*; + let start = match colors.start_bound() { + Excluded(&start) => start + 1, + Included(&start) => start, + Unbounded => 0, + } as usize; + let end = match colors.end_bound() { + Excluded(&end) => end - 1, + Included(&end) => end, + Unbounded => 255, + } as usize; + let subset = &mut self.colors[start..=end]; + match step.signum() { + -1 => subset.rotate_left(step.abs() as usize), + 1 => subset.rotate_right(step.abs() as usize), + _ => {} + } + } + + /// Finds and returns the index of the closest color in this palette to the RGB values provided. + /// This will not always return great results. It depends largely on the palette and the RGB + /// values being searched (for example, searching for bright green 0,255,0 in a palette which + /// contains no green hues at all is not likely to return a useful result). + pub fn find_color(&self, r: u8, g: u8, b: u8) -> u8 { + let mut closest_distance = 255 * 3; + let mut closest = 0; + + for (index, color) in self.colors.iter().enumerate() { + let (this_r, this_g, this_b) = from_rgb32(*color); + + // this comparison method is using the sRGB Euclidean formula described here: + // https://en.wikipedia.org/wiki/Color_difference + + let distance = abs_diff(this_r, r) as u32 + + abs_diff(this_g, g) as u32 + + abs_diff(this_b, b) as u32; + + if distance < closest_distance { + closest = index as u8; + closest_distance = distance; + } + } + + closest + } +} + +impl Index for Palette { + type Output = u32; + + #[inline] + fn index(&self, index: u8) -> &Self::Output { + &self.colors[index as usize] + } +} + +impl IndexMut for Palette { + #[inline] + fn index_mut(&mut self, index: u8) -> &mut Self::Output { + &mut self.colors[index as usize] + } +} + +impl PartialEq for Palette { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.colors == other.colors + } +} + +#[cfg(test)] +mod tests { + use tempfile::TempDir; + + use super::*; + + #[test] + fn argb_conversions() { + let argb = to_argb32(0x11, 0x22, 0x33, 0x44); + assert_eq!(argb, 0x11223344); + + let argb = to_rgb32(0x22, 0x33, 0x44); + assert_eq!(argb, 0xff223344); + + let (a, r, g, b) = from_argb32(0x11223344); + assert_eq!(0x11, a); + assert_eq!(0x22, r); + assert_eq!(0x33, g); + assert_eq!(0x44, b); + + let (r, g, b) = from_rgb32(0x11223344); + assert_eq!(0x22, r); + assert_eq!(0x33, g); + assert_eq!(0x44, b); + } + + #[test] + fn get_and_set_colors() { + let mut palette = Palette::new(); + assert_eq!(0, palette[0]); + assert_eq!(0, palette[1]); + palette[0] = 0x11223344; + assert_eq!(0x11223344, palette[0]); + assert_eq!(0, palette[1]); + } + + fn assert_vga_palette(palette: &Palette) { + assert_eq!(0xff000000, palette[0]); + assert_eq!(0xff0000a8, palette[1]); + assert_eq!(0xff00a800, palette[2]); + assert_eq!(0xff00a8a8, palette[3]); + assert_eq!(0xffa80000, palette[4]); + assert_eq!(0xffa800a8, palette[5]); + assert_eq!(0xffa85400, palette[6]); + assert_eq!(0xffa8a8a8, palette[7]); + assert_eq!(0xff545454, palette[8]); + assert_eq!(0xff5454fc, palette[9]); + assert_eq!(0xff54fc54, palette[10]); + assert_eq!(0xff54fcfc, palette[11]); + assert_eq!(0xfffc5454, palette[12]); + assert_eq!(0xfffc54fc, palette[13]); + assert_eq!(0xfffcfc54, palette[14]); + assert_eq!(0xfffcfcfc, palette[15]); + } + + #[test] + fn load_and_save() -> Result<(), PaletteError> { + let tmp_dir = TempDir::new()?; + + // vga format + + let palette = Palette::load_from_file(Path::new("./assets/vga.pal"), PaletteFormat::Vga)?; + assert_vga_palette(&palette); + + let save_path = tmp_dir.path().join("test_save_vga_format.pal"); + palette.to_file(&save_path, PaletteFormat::Vga)?; + let reloaded_palette = Palette::load_from_file(&save_path, PaletteFormat::Vga)?; + assert_eq!(palette, reloaded_palette); + + // normal format + + let palette = + Palette::load_from_file(Path::new("./test-assets/dp2.pal"), PaletteFormat::Normal)?; + + let save_path = tmp_dir.path().join("test_save_normal_format.pal"); + palette.to_file(&save_path, PaletteFormat::Normal)?; + let reloaded_palette = Palette::load_from_file(&save_path, PaletteFormat::Normal)?; + assert_eq!(palette, reloaded_palette); + + Ok(()) + } +} diff --git a/libretrogd/src/lib.rs b/libretrogd/src/lib.rs new file mode 100644 index 0000000..89da3f5 --- /dev/null +++ b/libretrogd/src/lib.rs @@ -0,0 +1,68 @@ +extern crate core; +extern crate sdl2; + +pub use crate::graphics::bitmap::*; +pub use crate::graphics::bitmapatlas::*; +pub use crate::graphics::font::*; +pub use crate::graphics::palette::*; +pub use crate::graphics::*; +pub use crate::math::circle::*; +pub use crate::math::matrix3x3::*; +pub use crate::math::rect::*; +pub use crate::math::vector2::*; +pub use crate::math::*; +pub use crate::system::input_devices::keyboard::*; +pub use crate::system::input_devices::mouse::*; +pub use crate::system::input_devices::*; +pub use crate::system::*; + +pub mod entities; +pub mod events; +pub mod graphics; +pub mod math; +pub mod states; +pub mod system; +pub mod utils; + +pub const LOW_RES: bool = if cfg!(feature = "low_res") { + true +} else { + false +}; +pub const WIDE_SCREEN: bool = if cfg!(feature = "wide") { + true +} else { + false +}; + +pub const SCREEN_WIDTH: u32 = if cfg!(feature = "low_res") { + if cfg!(feature = "wide") { + 214 + } else { + 160 + } +} else { + if cfg!(feature = "wide") { + 428 + } else { + 320 + } +}; +pub const SCREEN_HEIGHT: u32 = if cfg!(feature = "low_res") { + 120 +} else { + 240 +}; + +pub const SCREEN_TOP: u32 = 0; +pub const SCREEN_LEFT: u32 = 0; +pub const SCREEN_RIGHT: u32 = SCREEN_WIDTH - 1; +pub const SCREEN_BOTTOM: u32 = SCREEN_HEIGHT - 1; + +pub const DEFAULT_SCALE_FACTOR: u32 = if cfg!(feature = "low_res") { + 6 +} else { + 3 +}; + +pub const NUM_COLORS: usize = 256; // i mean ... the number of colors is really defined by the size of u8 ... diff --git a/libretrogd/src/math/circle.rs b/libretrogd/src/math/circle.rs new file mode 100644 index 0000000..f892887 --- /dev/null +++ b/libretrogd/src/math/circle.rs @@ -0,0 +1,113 @@ +use crate::{distance_between, distance_squared_between, Vector2}; + +/// Represents a 2D circle, using integer center coordinates and radius. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Circle { + pub x: i32, + pub y: i32, + pub radius: u32, +} + +impl Circle { + #[inline] + pub fn new(x: i32, y: i32, radius: u32) -> Self { + Circle { x, y, radius } + } + + pub fn new_encapsulating(points: &[Vector2]) -> Option { + if points.is_empty() { + return None; + } + + let mut min_x = points[0].x; + let mut min_y = points[0].y; + let mut max_x = min_x; + let mut max_y = min_y; + + for i in 0..points.len() { + let point = &points[i]; + min_x = point.x.min(min_x); + min_y = point.y.min(min_y); + max_x = point.x.max(max_x); + max_y = point.y.max(max_y); + } + + let radius = distance_between(min_x, min_y, max_x, max_y) * 0.5; + let center_x = (max_x - min_x) / 2.0; + let center_y = (max_y - min_y) / 2.0; + + Some(Circle { + x: center_x as i32, + y: center_y as i32, + radius: radius as u32, + }) + } + + /// Calculates the diameter of the circle. + #[inline] + pub fn diameter(&self) -> u32 { + self.radius * 2 + } + + /// Returns true if the given point is contained within the bounds of this circle. + pub fn contains_point(&self, x: i32, y: i32) -> bool { + let distance_squared = + distance_squared_between(self.x as f32, self.y as f32, x as f32, y as f32); + let radius_squared = (self.radius * self.radius) as f32; + distance_squared <= radius_squared + } + + /// Returns true if the given circle at least partially overlaps the bounds of this circle. + pub fn overlaps(&self, other: &Circle) -> bool { + let distance_squared = + distance_squared_between(self.x as f32, self.y as f32, other.x as f32, other.y as f32); + let minimum_distance_squared = + ((self.radius + other.radius) * (self.radius + other.radius)) as f32; + distance_squared <= minimum_distance_squared + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + pub fn test_new() { + let c = Circle::new(1, 2, 4); + assert_eq!(1, c.x); + assert_eq!(2, c.y); + assert_eq!(4, c.radius); + } + + #[test] + pub fn test_diameter() { + let c = Circle::new(4, 4, 3); + assert_eq!(6, c.diameter()); + } + + #[test] + pub fn test_contains_point() { + let c = Circle::new(1, 1, 6); + assert!(c.contains_point(4, 4)); + assert!(!c.contains_point(8, 4)); + + let c = Circle::new(0, 1, 2); + assert!(!c.contains_point(3, 3)); + assert!(c.contains_point(0, 0)); + } + + #[test] + pub fn test_overlaps() { + let a = Circle::new(3, 4, 5); + let b = Circle::new(14, 18, 8); + assert!(!a.overlaps(&b)); + + let a = Circle::new(2, 3, 12); + let b = Circle::new(15, 28, 10); + assert!(!a.overlaps(&b)); + + let a = Circle::new(-10, 8, 30); + let b = Circle::new(14, -24, 10); + assert!(a.overlaps(&b)); + } +} diff --git a/libretrogd/src/math/matrix3x3.rs b/libretrogd/src/math/matrix3x3.rs new file mode 100644 index 0000000..7cb2f98 --- /dev/null +++ b/libretrogd/src/math/matrix3x3.rs @@ -0,0 +1,420 @@ +use std::ops::{Mul, MulAssign}; + +use crate::{nearly_equal, Vector2}; + +/// Represents a 3x3 column-major matrix and provides common methods for matrix math. +#[derive(Debug, Copy, Clone)] +pub struct Matrix3x3 { + pub m: [f32; 9], +} + +impl Matrix3x3 { + pub const M11: usize = 0; + pub const M12: usize = 3; + pub const M13: usize = 6; + pub const M21: usize = 1; + pub const M22: usize = 4; + pub const M23: usize = 7; + pub const M31: usize = 2; + pub const M32: usize = 5; + pub const M33: usize = 8; + + pub const IDENTITY: Matrix3x3 = Matrix3x3 { + m: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], + }; + + /// Returns a new identity matrix. + #[inline] + pub fn identity() -> Matrix3x3 { + Matrix3x3::IDENTITY + } + + /// Creates a new matrix with the specified elements. + #[rustfmt::skip] + #[inline] + pub fn new( + m11: f32, m12: f32, m13: f32, + m21: f32, m22: f32, m23: f32, + m31: f32, m32: f32, m33: f32, + ) -> Matrix3x3 { + Matrix3x3 { + m: [ + m11, m21, m31, + m12, m22, m32, + m13, m23, m33 + ], + } + } + + /// Creates a new rotation matrix from a set of euler angles. + /// + /// # Arguments + /// + /// * `x`: the x angle (in radians) + /// * `y`: the y angle (in radians) + /// * `z`: the z angle (in radians) + pub fn from_euler_angles(x: f32, y: f32, z: f32) -> Matrix3x3 { + let rotate_z = Matrix3x3::new_rotation_z(z); + let rotate_y = Matrix3x3::new_rotation_y(y); + let rotate_x = Matrix3x3::new_rotation_x(x); + + // "right-to-left" column-major matrix concatenation + rotate_z * rotate_y * rotate_x + } + + /// Creates a new rotation matrix for rotation around the x axis. + /// + /// # Arguments + /// + /// * `radians`: angle to rotate the x axis around (in radians) + #[rustfmt::skip] + #[inline] + pub fn new_rotation_x(radians: f32) -> Matrix3x3 { + let (s, c) = radians.sin_cos(); + Matrix3x3::new( + 1.0, 0.0, 0.0, + 0.0, c, -s, + 0.0, s, c + ) + } + + /// Creates a new rotation matrix for rotation around the y axis. + /// + /// # Arguments + /// + /// * `radians`: angle to rotate the y axis around (in radians) + #[rustfmt::skip] + #[inline] + pub fn new_rotation_y(radians: f32) -> Matrix3x3 { + let (s, c) = radians.sin_cos(); + Matrix3x3::new( + c, 0.0, s, + 0.0, 1.0, 0.0, + -s, 0.0, c + ) + } + + /// Creates a new rotation matrix for rotation around the z axis. + /// + /// # Arguments + /// + /// * `radians`: angle to rotate the z axis around (in radians) + #[rustfmt::skip] + #[inline] + pub fn new_rotation_z(radians: f32) -> Matrix3x3 { + let (s, c) = radians.sin_cos(); + Matrix3x3::new( + c, -s, 0.0, + s, c, 0.0, + 0.0, 0.0, 1.0 + ) + } + + /// Creates a translation matrix. For use with 2D coordinates only. + /// + /// # Arguments + /// + /// * `x`: the amount to translate on the x axis + /// * `y`: the amount to translate on the y axis + #[rustfmt::skip] + #[inline] + pub fn new_2d_translation(x: f32, y: f32) -> Matrix3x3 { + Matrix3x3::new( + 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + x, y, 1.0 + ) + } + + /// Creates a scaling matrix from scaling factors for each axis. For use with 2D coordinates + /// only. + /// + /// # Arguments + /// + /// * `x`: the scale factor for the x axis + /// * `y`: the scale factor for the y axis + #[rustfmt::skip] + #[inline] + pub fn new_2d_scaling(x: f32, y: f32) -> Matrix3x3 { + Matrix3x3::new( + x, 0.0, 0.0, + 0.0, y, 0.0, + 0.0, 0.0, 1.0 + ) + } + + /// Creates a new rotation matrix. For use with 2D coordinates only. + /// + /// # Arguments + /// + /// * `radians`: angle to rotate by (in radians) + #[inline(always)] + pub fn new_2d_rotation(radians: f32) -> Matrix3x3 { + Matrix3x3::new_rotation_z(radians) + } + + /// Calculates the determinant of this matrix. + #[rustfmt::skip] + #[inline] + pub fn determinant(&self) -> f32 { + self.m[Matrix3x3::M11] * self.m[Matrix3x3::M22] * self.m[Matrix3x3::M33] + + self.m[Matrix3x3::M12] * self.m[Matrix3x3::M23] * self.m[Matrix3x3::M31] + + self.m[Matrix3x3::M13] * self.m[Matrix3x3::M21] * self.m[Matrix3x3::M32] - + self.m[Matrix3x3::M11] * self.m[Matrix3x3::M23] * self.m[Matrix3x3::M32] - + self.m[Matrix3x3::M12] * self.m[Matrix3x3::M21] * self.m[Matrix3x3::M33] - + self.m[Matrix3x3::M13] * self.m[Matrix3x3::M22] * self.m[Matrix3x3::M31] + } + + /// Calculates the inverse of this matrix. + #[rustfmt::skip] + pub fn invert(&self) -> Matrix3x3 { + let d = self.determinant(); + if nearly_equal(d, 0.0, 0.000001) { + Matrix3x3::IDENTITY + } else { + let d = 1.0 / d; + Matrix3x3 { + m: [ + d * (self.m[Matrix3x3::M22] * self.m[Matrix3x3::M33] - self.m[Matrix3x3::M32] * self.m[Matrix3x3::M23]), + d * (self.m[Matrix3x3::M31] * self.m[Matrix3x3::M23] - self.m[Matrix3x3::M21] * self.m[Matrix3x3::M33]), + d * (self.m[Matrix3x3::M21] * self.m[Matrix3x3::M32] - self.m[Matrix3x3::M31] * self.m[Matrix3x3::M22]), + d * (self.m[Matrix3x3::M32] * self.m[Matrix3x3::M13] - self.m[Matrix3x3::M12] * self.m[Matrix3x3::M33]), + d * (self.m[Matrix3x3::M11] * self.m[Matrix3x3::M33] - self.m[Matrix3x3::M31] * self.m[Matrix3x3::M13]), + d * (self.m[Matrix3x3::M31] * self.m[Matrix3x3::M12] - self.m[Matrix3x3::M11] * self.m[Matrix3x3::M32]), + d * (self.m[Matrix3x3::M12] * self.m[Matrix3x3::M23] - self.m[Matrix3x3::M22] * self.m[Matrix3x3::M13]), + d * (self.m[Matrix3x3::M21] * self.m[Matrix3x3::M13] - self.m[Matrix3x3::M11] * self.m[Matrix3x3::M23]), + d * (self.m[Matrix3x3::M11] * self.m[Matrix3x3::M22] - self.m[Matrix3x3::M21] * self.m[Matrix3x3::M12]), + ] + } + } + } + + /// Calculates the transpose of this matrix. + #[inline] + pub fn transpose(&self) -> Matrix3x3 { + Matrix3x3::new( + self.m[Matrix3x3::M11], + self.m[Matrix3x3::M21], + self.m[Matrix3x3::M31], + self.m[Matrix3x3::M12], + self.m[Matrix3x3::M22], + self.m[Matrix3x3::M32], + self.m[Matrix3x3::M13], + self.m[Matrix3x3::M23], + self.m[Matrix3x3::M33], + ) + } + + /// Sets all of the elements of this matrix. + #[inline] + pub fn set( + &mut self, + m11: f32, + m12: f32, + m13: f32, + m21: f32, + m22: f32, + m23: f32, + m31: f32, + m32: f32, + m33: f32, + ) { + self.m[Matrix3x3::M11] = m11; + self.m[Matrix3x3::M12] = m12; + self.m[Matrix3x3::M13] = m13; + self.m[Matrix3x3::M21] = m21; + self.m[Matrix3x3::M22] = m22; + self.m[Matrix3x3::M23] = m23; + self.m[Matrix3x3::M31] = m31; + self.m[Matrix3x3::M32] = m32; + self.m[Matrix3x3::M33] = m33; + } +} + +impl Mul for Matrix3x3 { + type Output = Self; + + #[rustfmt::skip] + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Matrix3x3::new( + self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M31], + self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M32], + self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M33], + self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M31], + self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M32], + self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M33], + self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M31], + self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M32], + self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M33] + ) + } +} + +impl MulAssign for Matrix3x3 { + #[rustfmt::skip] + #[inline] + fn mul_assign(&mut self, rhs: Self) { + self.set( + self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M31], + self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M32], + self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M33], + self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M31], + self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M32], + self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M33], + self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M31], + self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M32], + self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M33] + ) + } +} + +impl Mul for Matrix3x3 { + type Output = Vector2; + + #[rustfmt::skip] + #[inline] + fn mul(self, rhs: Vector2) -> Self::Output { + Vector2 { + x: rhs.x * self.m[Matrix3x3::M11] + rhs.y * self.m[Matrix3x3::M12] + self.m[Matrix3x3::M13] + self.m[Matrix3x3::M31], + y: rhs.x * self.m[Matrix3x3::M21] + rhs.y * self.m[Matrix3x3::M22] + self.m[Matrix3x3::M23] + self.m[Matrix3x3::M32] + } + } +} + +#[cfg(test)] +pub mod tests { + use crate::math::{RADIANS_180, RADIANS_90}; + + use super::*; + + #[test] + pub fn test_new() { + let m = Matrix3x3::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0); + assert_eq!(1.0, m.m[Matrix3x3::M11]); + assert_eq!(2.0, m.m[Matrix3x3::M12]); + assert_eq!(3.0, m.m[Matrix3x3::M13]); + assert_eq!(4.0, m.m[Matrix3x3::M21]); + assert_eq!(5.0, m.m[Matrix3x3::M22]); + assert_eq!(6.0, m.m[Matrix3x3::M23]); + assert_eq!(7.0, m.m[Matrix3x3::M31]); + assert_eq!(8.0, m.m[Matrix3x3::M32]); + assert_eq!(9.0, m.m[Matrix3x3::M33]); + } + + #[test] + pub fn test_set() { + let mut m = Matrix3x3 { m: [0.0; 9] }; + m.set(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0); + assert_eq!(1.0, m.m[Matrix3x3::M11]); + assert_eq!(2.0, m.m[Matrix3x3::M12]); + assert_eq!(3.0, m.m[Matrix3x3::M13]); + assert_eq!(4.0, m.m[Matrix3x3::M21]); + assert_eq!(5.0, m.m[Matrix3x3::M22]); + assert_eq!(6.0, m.m[Matrix3x3::M23]); + assert_eq!(7.0, m.m[Matrix3x3::M31]); + assert_eq!(8.0, m.m[Matrix3x3::M32]); + assert_eq!(9.0, m.m[Matrix3x3::M33]); + } + + #[test] + pub fn test_identity() { + let m = Matrix3x3::identity(); + assert_eq!(1.0, m.m[Matrix3x3::M11]); + assert_eq!(0.0, m.m[Matrix3x3::M12]); + assert_eq!(0.0, m.m[Matrix3x3::M13]); + assert_eq!(0.0, m.m[Matrix3x3::M21]); + assert_eq!(1.0, m.m[Matrix3x3::M22]); + assert_eq!(0.0, m.m[Matrix3x3::M23]); + assert_eq!(0.0, m.m[Matrix3x3::M31]); + assert_eq!(0.0, m.m[Matrix3x3::M32]); + assert_eq!(1.0, m.m[Matrix3x3::M33]); + } + + #[rustfmt::skip] + #[test] + pub fn test_transpose() { + let m = Matrix3x3::new( + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, + 7.0, 8.0, 9.0 + ); + let t = m.transpose(); + assert_eq!(1.0, t.m[Matrix3x3::M11]); + assert_eq!(4.0, t.m[Matrix3x3::M12]); + assert_eq!(7.0, t.m[Matrix3x3::M13]); + assert_eq!(2.0, t.m[Matrix3x3::M21]); + assert_eq!(5.0, t.m[Matrix3x3::M22]); + assert_eq!(8.0, t.m[Matrix3x3::M23]); + assert_eq!(3.0, t.m[Matrix3x3::M31]); + assert_eq!(6.0, t.m[Matrix3x3::M32]); + assert_eq!(9.0, t.m[Matrix3x3::M33]); + } + + #[test] + pub fn test_mul() { + let a = Matrix3x3::new(12.0, 8.0, 4.0, 3.0, 17.0, 14.0, 9.0, 8.0, 10.0); + let b = Matrix3x3::new(5.0, 19.0, 3.0, 6.0, 15.0, 9.0, 7.0, 8.0, 16.0); + let c = a * b; + assert!(nearly_equal(136.0, c.m[Matrix3x3::M11], 0.001)); + assert!(nearly_equal(380.0, c.m[Matrix3x3::M12], 0.001)); + assert!(nearly_equal(172.0, c.m[Matrix3x3::M13], 0.001)); + assert!(nearly_equal(215.0, c.m[Matrix3x3::M21], 0.001)); + assert!(nearly_equal(424.0, c.m[Matrix3x3::M22], 0.001)); + assert!(nearly_equal(386.0, c.m[Matrix3x3::M23], 0.001)); + assert!(nearly_equal(163.0, c.m[Matrix3x3::M31], 0.001)); + assert!(nearly_equal(371.0, c.m[Matrix3x3::M32], 0.001)); + assert!(nearly_equal(259.0, c.m[Matrix3x3::M33], 0.001)); + + let mut a = Matrix3x3::new(12.0, 8.0, 4.0, 3.0, 17.0, 14.0, 9.0, 8.0, 10.0); + let b = Matrix3x3::new(5.0, 19.0, 3.0, 6.0, 15.0, 9.0, 7.0, 8.0, 16.0); + a *= b; + assert!(nearly_equal(136.0, a.m[Matrix3x3::M11], 0.001)); + assert!(nearly_equal(380.0, a.m[Matrix3x3::M12], 0.001)); + assert!(nearly_equal(172.0, a.m[Matrix3x3::M13], 0.001)); + assert!(nearly_equal(215.0, a.m[Matrix3x3::M21], 0.001)); + assert!(nearly_equal(424.0, a.m[Matrix3x3::M22], 0.001)); + assert!(nearly_equal(386.0, a.m[Matrix3x3::M23], 0.001)); + assert!(nearly_equal(163.0, a.m[Matrix3x3::M31], 0.001)); + assert!(nearly_equal(371.0, a.m[Matrix3x3::M32], 0.001)); + assert!(nearly_equal(259.0, a.m[Matrix3x3::M33], 0.001)); + } + + #[test] + pub fn test_2d_translation() { + let v = Vector2::new(10.2, 5.7); + let m = Matrix3x3::new_2d_translation(2.0, 3.0); + let t = m * v; + assert!(nearly_equal(12.2, t.x, 0.001)); + assert!(nearly_equal(8.7, t.y, 0.001)); + } + + #[test] + pub fn test_2d_scaling() { + let v = Vector2::new(10.2, 5.7); + let m = Matrix3x3::new_2d_scaling(3.0, 4.0); + let t = m * v; + assert!(nearly_equal(30.6, t.x, 0.001)); + assert!(nearly_equal(22.8, t.y, 0.001)); + } + + #[test] + pub fn test_2d_rotation() { + let v = Vector2::new(0.0, 5.0); + let m = Matrix3x3::new_2d_rotation(RADIANS_90); + let t = m * v; + assert!(nearly_equal(-5.0, t.x, 0.001)); + assert!(nearly_equal(0.0, t.y, 0.001)); + } + + #[test] + pub fn test_2d_combined_transform() { + let a = Matrix3x3::new_2d_translation(-2.0, 0.0); + let b = Matrix3x3::new_2d_rotation(RADIANS_180); + let m = a * b; + let v = Vector2::new(0.0, 5.0); + let t = m * v; + assert!(nearly_equal(2.0, t.x, 0.001)); + assert!(nearly_equal(-5.0, t.y, 0.001)); + } +} diff --git a/libretrogd/src/math/mod.rs b/libretrogd/src/math/mod.rs new file mode 100644 index 0000000..800a914 --- /dev/null +++ b/libretrogd/src/math/mod.rs @@ -0,0 +1,272 @@ +use std::ops::{Add, Div, Mul, Sub}; + +pub mod circle; +pub mod matrix3x3; +pub mod rect; +pub mod vector2; + +pub const PI: f32 = std::f32::consts::PI; // 180 degrees +pub const HALF_PI: f32 = PI / 2.0; // 90 degrees +pub const QUARTER_PI: f32 = PI / 4.0; // 45 degrees +pub const TWO_PI: f32 = PI * 2.0; // 360 degrees + +pub const RADIANS_0: f32 = 0.0; +pub const RADIANS_45: f32 = PI / 4.0; +pub const RADIANS_90: f32 = PI / 2.0; +pub const RADIANS_135: f32 = (3.0 * PI) / 4.0; +pub const RADIANS_180: f32 = PI; +pub const RADIANS_225: f32 = (5.0 * PI) / 4.0; +pub const RADIANS_270: f32 = (3.0 * PI) / 2.0; +pub const RADIANS_315: f32 = (7.0 * PI) / 4.0; +pub const RADIANS_360: f32 = PI * 2.0; + +pub const PI_OVER_180: f32 = PI / 180.0; +pub const INVERSE_PI_OVER_180: f32 = 180.0 / PI; + +// 2d directions. intended to be usable in a 2d screen-space where the origin 0,0 is at the +// top-left of the screen, where moving up is accomplished by decrementing Y. +// TODO: this is not a perfect solution and does pose some problems in various math calculations ... +pub const UP: f32 = -RADIANS_90; +pub const DOWN: f32 = RADIANS_90; +pub const LEFT: f32 = RADIANS_180; +pub const RIGHT: f32 = RADIANS_0; + +/// Returns true if the two f32 values are "close enough" to be considered equal using the +/// precision of the provided epsilon value. +#[inline] +pub fn nearly_equal(a: f32, b: f32, epsilon: f32) -> bool { + // HACK/TODO: this is a shitty way to do this. but it's "good enough" for me ... + (a - b).abs() <= epsilon +} + +/// Linearly interpolates between two values. +/// +/// # Arguments +/// +/// * `a`: first value (low end of range) +/// * `b`: second value (high end of range) +/// * `t`: the amount to interpolate between the two values, specified as a fraction +#[inline] +pub fn lerp(a: N, b: N, t: f32) -> N +where + N: Copy + Add + Sub + Mul, +{ + a + (b - a) * t +} + +/// Given a linearly interpolated value and the original range (high and low) of the linear +/// interpolation, this will return the original interpolation factor (as a fraction) +/// +/// # Arguments +/// +/// * `a`: first value (low end of range) +/// * `b`: second value (high end of range) +/// * `lerp_result`: the interpolated value between the range given +#[inline] +pub fn inverse_lerp(a: N, b: N, lerp_result: N) -> f32 +where + N: Copy + Sub + Div, +{ + (lerp_result - a) / (b - a) +} + +/// Interpolates between two values using a cubic equation. +/// +/// # Arguments +/// +/// * `a`: first value (low end of range) +/// * `b`: second value (high end of range) +/// * `t`: the amount to interpolate between the two values, specified as a fraction +#[inline] +pub fn smooth_lerp(a: N, b: N, t: f32) -> N +where + N: Copy + Add + Sub + Mul, +{ + let t = t.clamp(0.0, 1.0); + lerp(a, b, (t * t) * (3.0 - (2.0 * t))) +} + +/// Re-scales a given value from an old min/max range to a new and different min/max range such +/// that the returned value is approximately at the same relative position within the new min/max +/// range. +/// +/// # Arguments +/// +/// * `value`: the value to be re-scaled which is currently between `old_min` and `old_max` +/// * `old_min`: original min value (low end of range) +/// * `old_max`: original max value (high end of range) +/// * `new_min`: new min value (low end of range) +/// * `new_max`: new max value (high end of range) +#[inline] +pub fn scale_range(value: N, old_min: N, old_max: N, new_min: N, new_max: N) -> N +where + N: Copy + Add + Sub + Mul + Div, +{ + (new_max - new_min) * (value - old_min) / (old_max - old_min) + new_min +} + +/// Calculates the angle (in radians) between the two points. +#[inline] +pub fn angle_between(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 { + let delta_x = x2 - x1; + let delta_y = y2 - y1; + delta_y.atan2(delta_x) +} + +/// Returns the X and Y point of a normalized 2D vector that points in the same direction as +/// the given angle. +#[inline] +pub fn angle_to_direction(radians: f32) -> (f32, f32) { + let x = radians.cos(); + let y = radians.sin(); + (x, y) +} + +/// Calculates the squared distance between two 2D points. +#[inline] +pub fn distance_squared_between(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 { + (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) +} + +/// Calculates the distance between two 2D points. +#[inline] +pub fn distance_between(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 { + distance_squared_between(x1, y1, x2, y2).sqrt() +} + +pub trait NearlyEqual { + type Output; + + /// Returns true if the two f32 values are "close enough" to be considered equal using the + /// precision of the provided epsilon value. + fn nearly_equal(self, other: Self::Output, epsilon: f32) -> bool; +} + +impl NearlyEqual for f32 { + type Output = f32; + + #[inline(always)] + fn nearly_equal(self, other: Self::Output, epsilon: f32) -> bool { + nearly_equal(self, other, epsilon) + } +} + +pub trait WrappingRadians { + type Type; + + /// Adds two angle values in radians together returning the result. The addition will wrap + /// around so that the returned result always lies within 0 -> 2Ï€ radians (0 -> 360 degrees). + fn wrapping_radians_add(self, other: Self::Type) -> Self::Type; + + /// Subtracts two angle values in radians returning the result. The subtraction will wrap + /// around so that the returned result always lies within 0 -> 2Ï€ radians (0 -> 360 degrees). + fn wrapping_radians_sub(self, other: Self::Type) -> Self::Type; +} + +impl WrappingRadians for f32 { + type Type = f32; + + fn wrapping_radians_add(self, other: Self::Type) -> Self::Type { + let result = self + other; + if result < RADIANS_0 { + result + RADIANS_360 + } else if result >= RADIANS_360 { + result - RADIANS_360 + } else { + result + } + } + + fn wrapping_radians_sub(self, other: Self::Type) -> Self::Type { + let result = self - other; + if result < RADIANS_0 { + result + RADIANS_360 + } else if result >= RADIANS_360 { + result - RADIANS_360 + } else { + result + } + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + pub fn test_nearly_equal() { + assert!(nearly_equal(4.0, 4.0, 0.1)); + assert!(4.0f32.nearly_equal(4.0, 0.1)); + assert!(nearly_equal(0.1 + 0.2, 0.3, 0.01)); + assert!(!nearly_equal(1.0001, 1.0005, 0.0001)); + } + + #[test] + pub fn test_lerp() { + assert!(nearly_equal(15.0, lerp(10.0, 20.0, 0.5), 0.0001)); + assert!(nearly_equal(10.0, lerp(10.0, 20.0, 0.0), 0.0001)); + assert!(nearly_equal(20.0, lerp(10.0, 20.0, 1.0), 0.0001)); + } + + #[test] + pub fn test_inverse_lerp() { + assert_eq!(0.5, inverse_lerp(10.0, 20.0, 15.0f32)) + } + + #[test] + pub fn test_angle_between() { + let angle = angle_between(20.0, 20.0, 10.0, 10.0); + assert!(nearly_equal(-RADIANS_135, angle, 0.0001)); + let angle = angle_between(0.0, 0.0, 10.0, 10.0); + assert!(nearly_equal(RADIANS_45, angle, 0.0001)); + let angle = angle_between(5.0, 5.0, 5.0, 5.0); + assert!(nearly_equal(0.0, angle, 0.0001)); + } + + #[test] + pub fn test_angle_to_direction() { + let (x, y) = angle_to_direction(RADIANS_0); + assert!(nearly_equal(x, 1.0, 0.000001)); + assert!(nearly_equal(y, 0.0, 0.000001)); + let (x, y) = angle_to_direction(RADIANS_45); + assert!(nearly_equal(x, 0.707106, 0.000001)); + assert!(nearly_equal(y, 0.707106, 0.000001)); + let (x, y) = angle_to_direction(RADIANS_225); + assert!(nearly_equal(x, -0.707106, 0.000001)); + assert!(nearly_equal(y, -0.707106, 0.000001)); + + let (x, y) = angle_to_direction(UP); + assert!(nearly_equal(x, 0.0, 0.000001)); + assert!(nearly_equal(y, -1.0, 0.000001)); + let (x, y) = angle_to_direction(DOWN); + assert!(nearly_equal(x, 0.0, 0.000001)); + assert!(nearly_equal(y, 1.0, 0.000001)); + let (x, y) = angle_to_direction(LEFT); + assert!(nearly_equal(x, -1.0, 0.000001)); + assert!(nearly_equal(y, 0.0, 0.000001)); + let (x, y) = angle_to_direction(RIGHT); + assert!(nearly_equal(x, 1.0, 0.000001)); + assert!(nearly_equal(y, 0.0, 0.000001)); + } + + #[test] + pub fn test_distance_between() { + let x1 = -2.0; + let y1 = 1.0; + let x2 = 4.0; + let y2 = 3.0; + let distance_squared = distance_squared_between(x1, y1, x2, y2); + let distance = distance_between(x1, y1, x2, y2); + assert!(nearly_equal(distance_squared, 40.0000, 0.0001)); + assert!(nearly_equal(distance, 6.3245, 0.0001)); + } + + #[test] + pub fn test_wrapping_radians() { + assert!(nearly_equal(RADIANS_90, RADIANS_45.wrapping_radians_add(RADIANS_45), 0.0001)); + assert!(nearly_equal(RADIANS_90, RADIANS_180.wrapping_radians_sub(RADIANS_90), 0.0001)); + + assert!(nearly_equal(RADIANS_45, RADIANS_315.wrapping_radians_add(RADIANS_90), 0.0001)); + assert!(nearly_equal(RADIANS_315, RADIANS_90.wrapping_radians_sub(RADIANS_135), 0.0001)); + } +} diff --git a/libretrogd/src/math/rect.rs b/libretrogd/src/math/rect.rs new file mode 100644 index 0000000..1180007 --- /dev/null +++ b/libretrogd/src/math/rect.rs @@ -0,0 +1,284 @@ +/// Represents a 2D rectangle, using integer coordinates and dimensions. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Rect { + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, +} + +impl Rect { + #[inline] + pub fn new(x: i32, y: i32, width: u32, height: u32) -> Rect { + Rect { + x, + y, + width, + height, + } + } + + /// Creates a new rect from the specified coordinates. Automatically determines if the + /// coordinates provided are swapped (where the right/bottom coordinate is provided before the + /// left/top). All of the coordinates are inclusive. + /// + /// # Arguments + /// + /// * `left`: the left x coordinate + /// * `top`: the top y coordinate + /// * `right`: the right x coordinate + /// * `bottom`: the bottom y coordinate + pub fn from_coords(left: i32, top: i32, right: i32, bottom: i32) -> Rect { + let x; + let y; + let width; + let height; + + if left <= right { + x = left; + width = (right - left).abs() + 1; + } else { + x = right; + width = (left - right).abs() + 1; + } + + if top <= bottom { + y = top; + height = (bottom - top).abs() + 1; + } else { + y = bottom; + height = (top - bottom).abs() + 1; + } + + Rect { + x, + y, + width: width as u32, + height: height as u32, + } + } + + pub fn set_from_coords(&mut self, left: i32, top: i32, right: i32, bottom: i32) { + if left <= right { + self.x = left; + self.width = ((right - left).abs() + 1) as u32; + } else { + self.x = right; + self.width = ((left - right).abs() + 1) as u32; + } + + if top <= bottom { + self.y = top; + self.height = ((bottom - top).abs() + 1) as u32; + } else { + self.y = bottom; + self.height = ((top - bottom).abs() + 1) as u32; + } + } + + /// Calculates the right-most x coordinate contained by this rect. + #[inline] + pub fn right(&self) -> i32 { + if self.width > 0 { + self.x + self.width as i32 - 1 + } else { + self.x + } + } + + /// Calculates the bottom-most y coordinate contained by this rect. + #[inline] + pub fn bottom(&self) -> i32 { + if self.height > 0 { + self.y + self.height as i32 - 1 + } else { + self.y + } + } + + /// Returns true if the given point is contained within the bounds of this rect. + pub fn contains_point(&self, x: i32, y: i32) -> bool { + (self.x <= x) && (self.right() >= x) && (self.y <= y) && (self.bottom() >= y) + } + + /// Returns true if the given rect is contained completely within the bounds of this rect. + pub fn contains_rect(&self, other: &Rect) -> bool { + (other.x >= self.x && other.x < self.right()) + && (other.right() > self.x && other.right() <= self.right()) + && (other.y >= self.y && other.y < self.bottom()) + && (other.bottom() > self.y && other.bottom() <= self.bottom()) + } + + /// Returns true if the given rect at least partially overlaps the bounds of this rect. + pub fn overlaps(&self, other: &Rect) -> bool { + (self.x <= other.right()) + && (self.right() >= other.x) + && (self.y <= other.bottom()) + && (self.bottom() >= other.y) + } + + pub fn clamp_to(&mut self, other: &Rect) -> bool { + if !self.overlaps(other) { + // not possible to clamp this rect to the other rect as they do not overlap at all + false + } else { + // the rects at least partially overlap, so we will clamp this rect to the overlapping + // region of the other rect + let mut x1 = self.x; + let mut y1 = self.y; + let mut x2 = self.right(); + let mut y2 = self.bottom(); + if y1 < other.y { + y1 = other.y; + } + if y1 > other.bottom() { + y1 = other.bottom(); + } + if y2 < other.y { + y2 = other.y; + } + if y2 > other.bottom() { + y2 = other.bottom(); + } + if x1 < other.x { + x1 = other.x; + } + if x1 > other.right() { + x1 = other.right(); + } + if x2 < other.x { + x2 = other.x; + } + if x2 > other.right() { + x2 = other.right(); + } + + self.set_from_coords(x1, y1, x2, y2); + true + } + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + pub fn right_and_left() { + let rect = Rect { + x: 5, + y: 6, + width: 16, + height: 12, + }; + assert_eq!(20, rect.right()); + assert_eq!(17, rect.bottom()); + + let rect = Rect { + x: -11, + y: -25, + width: 16, + height: 12, + }; + assert_eq!(4, rect.right()); + assert_eq!(-14, rect.bottom()); + } + + #[test] + pub fn create_from_coords() { + let rect = Rect::from_coords(10, 15, 20, 30); + assert_eq!(10, rect.x); + assert_eq!(15, rect.y); + assert_eq!(11, rect.width); + assert_eq!(16, rect.height); + assert_eq!(20, rect.right()); + assert_eq!(30, rect.bottom()); + + let rect = Rect::from_coords(-5, -13, 6, -2); + assert_eq!(-5, rect.x); + assert_eq!(-13, rect.y); + assert_eq!(12, rect.width); + assert_eq!(12, rect.height); + assert_eq!(6, rect.right()); + assert_eq!(-2, rect.bottom()); + } + + #[test] + pub fn create_from_coords_with_swapped_order() { + let rect = Rect::from_coords(20, 30, 10, 15); + assert_eq!(10, rect.x); + assert_eq!(15, rect.y); + assert_eq!(11, rect.width); + assert_eq!(16, rect.height); + assert_eq!(20, rect.right()); + assert_eq!(30, rect.bottom()); + + let rect = Rect::from_coords(6, -2, -5, -13); + assert_eq!(-5, rect.x); + assert_eq!(-13, rect.y); + assert_eq!(12, rect.width); + assert_eq!(12, rect.height); + assert_eq!(6, rect.right()); + assert_eq!(-2, rect.bottom()); + } + + #[test] + pub fn test_contains_point() { + let r = Rect::from_coords(10, 10, 20, 20); + + assert!(r.contains_point(10, 10)); + assert!(r.contains_point(15, 15)); + assert!(r.contains_point(20, 20)); + + assert!(!r.contains_point(12, 30)); + assert!(!r.contains_point(8, 12)); + assert!(!r.contains_point(25, 16)); + assert!(!r.contains_point(17, 4)); + } + + #[test] + pub fn test_contains_rect() { + let r = Rect::from_coords(10, 10, 20, 20); + + assert!(r.contains_rect(&Rect::from_coords(12, 12, 15, 15))); + assert!(r.contains_rect(&Rect::from_coords(10, 10, 15, 15))); + assert!(r.contains_rect(&Rect::from_coords(15, 15, 20, 20))); + assert!(r.contains_rect(&Rect::from_coords(10, 12, 20, 15))); + assert!(r.contains_rect(&Rect::from_coords(12, 10, 15, 20))); + + assert!(!r.contains_rect(&Rect::from_coords(5, 5, 15, 15))); + assert!(!r.contains_rect(&Rect::from_coords(15, 15, 25, 25))); + + assert!(!r.contains_rect(&Rect::from_coords(2, 2, 8, 4))); + assert!(!r.contains_rect(&Rect::from_coords(12, 21, 18, 25))); + assert!(!r.contains_rect(&Rect::from_coords(22, 12, 32, 17))); + } + + #[test] + pub fn test_overlaps() { + let r = Rect::from_coords(10, 10, 20, 20); + + assert!(r.overlaps(&Rect::from_coords(12, 12, 15, 15))); + assert!(r.overlaps(&Rect::from_coords(10, 10, 15, 15))); + assert!(r.overlaps(&Rect::from_coords(15, 15, 20, 20))); + assert!(r.overlaps(&Rect::from_coords(10, 12, 20, 15))); + assert!(r.overlaps(&Rect::from_coords(12, 10, 15, 20))); + + assert!(r.overlaps(&Rect::from_coords(12, 5, 18, 10))); + assert!(r.overlaps(&Rect::from_coords(13, 20, 16, 25))); + assert!(r.overlaps(&Rect::from_coords(5, 12, 10, 18))); + assert!(r.overlaps(&Rect::from_coords(20, 13, 25, 16))); + + assert!(r.overlaps(&Rect::from_coords(5, 5, 15, 15))); + assert!(r.overlaps(&Rect::from_coords(15, 15, 25, 25))); + + assert!(!r.overlaps(&Rect::from_coords(2, 2, 8, 4))); + assert!(!r.overlaps(&Rect::from_coords(12, 21, 18, 25))); + assert!(!r.overlaps(&Rect::from_coords(22, 12, 32, 17))); + + assert!(!r.overlaps(&Rect::from_coords(12, 5, 18, 9))); + assert!(!r.overlaps(&Rect::from_coords(13, 21, 16, 25))); + assert!(!r.overlaps(&Rect::from_coords(5, 12, 9, 18))); + assert!(!r.overlaps(&Rect::from_coords(21, 13, 25, 16))); + } +} diff --git a/libretrogd/src/math/vector2.rs b/libretrogd/src/math/vector2.rs new file mode 100644 index 0000000..f64cddb --- /dev/null +++ b/libretrogd/src/math/vector2.rs @@ -0,0 +1,479 @@ +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +use crate::Matrix3x3; +use crate::{angle_between, angle_to_direction, nearly_equal, NearlyEqual}; + +/// Represents a 2D vector and provides common methods for vector math. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Vector2 { + pub x: f32, + pub y: f32, +} + +impl Vector2 { + pub const ZERO: Vector2 = Vector2 { x: 0.0, y: 0.0 }; + + pub const UP: Vector2 = Vector2 { x: 0.0, y: -1.0 }; + pub const DOWN: Vector2 = Vector2 { x: 0.0, y: 1.0 }; + pub const LEFT: Vector2 = Vector2 { x: -1.0, y: 0.0 }; + pub const RIGHT: Vector2 = Vector2 { x: 1.0, y: 0.0 }; + + pub const X_AXIS: Vector2 = Vector2 { x: 1.0, y: 0.0 }; + pub const Y_AXIS: Vector2 = Vector2 { x: 0.0, y: 1.0 }; + + /// Creates a vector with the specified X and Y components. + #[inline] + pub fn new(x: f32, y: f32) -> Vector2 { + Vector2 { x, y } + } + + /// Creates a normalized vector that points in the same direction as the given angle. + #[inline] + pub fn from_angle(radians: f32) -> Vector2 { + let (x, y) = angle_to_direction(radians); + Vector2 { x, y } + } + + /// Calculates the distance between this and another vector. + #[inline] + pub fn distance(&self, other: &Vector2) -> f32 { + self.distance_squared(other).sqrt() + } + + /// Calculates the squared distance between this and another vector. + #[inline] + pub fn distance_squared(&self, other: &Vector2) -> f32 { + (other.x - self.x) * (other.x - self.x) + (other.y - self.y) * (other.y - self.y) + } + + /// Calculates the dot product of this and another vector. + #[inline] + pub fn dot(&self, other: &Vector2) -> f32 { + (self.x * other.x) + (self.y * other.y) + } + + /// Calculates the length (a.k.a. magnitude) of this vector. + #[inline] + pub fn length(&self) -> f32 { + self.length_squared().sqrt() + } + + /// Calculates the squared length of this vector. + #[inline] + pub fn length_squared(&self) -> f32 { + (self.x * self.x) + (self.y * self.y) + } + + /// Returns a normalized vector from this vector. + pub fn normalize(&self) -> Vector2 { + let inverse_length = 1.0 / self.length(); + Vector2 { + x: self.x * inverse_length, + y: self.y * inverse_length, + } + } + + /// Returns an extended (or shrunk) vector from this vector, where the returned vector will + /// have a length exactly matching the specified length, but will retain the same direction. + pub fn extend(&self, length: f32) -> Vector2 { + *self * (length / self.length()) + } + + /// Returns the angle (in radians) equivalent to the direction of this vector. + #[inline] + pub fn angle(&self) -> f32 { + self.y.atan2(self.x) + } + + /// Calculates the angle (in radians) between the this and another vector. + #[inline] + pub fn angle_between(&self, other: &Vector2) -> f32 { + angle_between(self.x, self.y, other.x, other.y) + } +} + +impl Neg for Vector2 { + type Output = Self; + + #[inline] + fn neg(self) -> Self::Output { + Vector2 { + x: -self.x, + y: -self.y, + } + } +} + +impl Add for Vector2 { + type Output = Self; + + #[inline] + fn add(self, rhs: Self) -> Self::Output { + Vector2 { + x: self.x + rhs.x, + y: self.y + rhs.y, + } + } +} + +impl AddAssign for Vector2 { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.x += rhs.x; + self.y += rhs.y; + } +} + +impl Sub for Vector2 { + type Output = Self; + + #[inline] + fn sub(self, rhs: Self) -> Self::Output { + Vector2 { + x: self.x - rhs.x, + y: self.y - rhs.y, + } + } +} + +impl SubAssign for Vector2 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.x -= rhs.x; + self.y -= rhs.y; + } +} + +impl Mul for Vector2 { + type Output = Self; + + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Vector2 { + x: self.x * rhs.x, + y: self.y * rhs.y, + } + } +} + +impl MulAssign for Vector2 { + #[inline] + fn mul_assign(&mut self, rhs: Self) { + self.x *= rhs.x; + self.y *= rhs.y; + } +} + +impl Div for Vector2 { + type Output = Self; + + #[inline] + fn div(self, rhs: Self) -> Self::Output { + Vector2 { + x: self.x / rhs.x, + y: self.y / rhs.y, + } + } +} + +impl DivAssign for Vector2 { + #[inline] + fn div_assign(&mut self, rhs: Self) { + self.x /= rhs.x; + self.y /= rhs.y; + } +} + +impl Mul for Vector2 { + type Output = Self; + + #[inline] + fn mul(self, rhs: f32) -> Self::Output { + Vector2 { + x: self.x * rhs, + y: self.y * rhs, + } + } +} + +impl MulAssign for Vector2 { + #[inline] + fn mul_assign(&mut self, rhs: f32) { + self.x *= rhs; + self.y *= rhs; + } +} + +impl Div for Vector2 { + type Output = Self; + + #[inline] + fn div(self, rhs: f32) -> Self::Output { + Vector2 { + x: self.x / rhs, + y: self.y / rhs, + } + } +} + +impl DivAssign for Vector2 { + #[inline] + fn div_assign(&mut self, rhs: f32) { + self.x /= rhs; + self.y /= rhs; + } +} + +impl NearlyEqual for Vector2 { + type Output = Self; + + #[inline(always)] + fn nearly_equal(self, other: Self::Output, epsilon: f32) -> bool { + nearly_equal(self.x, other.x, epsilon) && nearly_equal(self.y, other.y, epsilon) + } +} + +impl MulAssign for Vector2 { + #[rustfmt::skip] + #[inline] + fn mul_assign(&mut self, rhs: Matrix3x3) { + let x = self.x * rhs.m[Matrix3x3::M11] + self.y * rhs.m[Matrix3x3::M12] + rhs.m[Matrix3x3::M13] + rhs.m[Matrix3x3::M31]; + let y = self.x * rhs.m[Matrix3x3::M21] + self.y * rhs.m[Matrix3x3::M22] + rhs.m[Matrix3x3::M23] + rhs.m[Matrix3x3::M32]; + self.x = x; + self.y = y; + } +} + +#[cfg(test)] +pub mod tests { + use crate::math::*; + + use super::*; + + #[test] + pub fn test_new() { + let v = Vector2::new(3.0, 7.0); + assert!(nearly_equal(v.x, 3.0, 0.0001)); + assert!(nearly_equal(v.y, 7.0, 0.0001)); + } + + #[test] + pub fn test_neg() { + let v = Vector2 { x: 1.0, y: 2.0 }; + let neg = -v; + assert!(nearly_equal(neg.x, -1.0, 0.0001)); + assert!(nearly_equal(neg.y, -2.0, 0.0001)); + } + + #[test] + pub fn test_add() { + let a = Vector2 { x: 3.0, y: 4.0 }; + let b = Vector2 { x: 1.0, y: 2.0 }; + let c = a + b; + assert!(nearly_equal(c.x, 4.0, 0.0001)); + assert!(nearly_equal(c.y, 6.0, 0.0001)); + + let mut a = Vector2 { x: 3.0, y: 4.0 }; + let b = Vector2 { x: 1.0, y: 2.0 }; + a += b; + assert!(nearly_equal(a.x, 4.0, 0.0001)); + assert!(nearly_equal(a.y, 6.0, 0.0001)); + } + + #[test] + pub fn test_sub() { + let a = Vector2 { x: 3.0, y: 4.0 }; + let b = Vector2 { x: 1.0, y: 2.0 }; + let c = a - b; + assert!(nearly_equal(c.x, 2.0, 0.0001)); + assert!(nearly_equal(c.y, 2.0, 0.0001)); + + let mut a = Vector2 { x: 3.0, y: 4.0 }; + let b = Vector2 { x: 1.0, y: 2.0 }; + a -= b; + assert!(nearly_equal(a.x, 2.0, 0.0001)); + assert!(nearly_equal(a.y, 2.0, 0.0001)); + } + + #[test] + pub fn test_mul() { + let a = Vector2 { x: 2.5, y: 6.0 }; + let b = Vector2 { x: 1.25, y: 2.0 }; + let c = a * b; + assert!(nearly_equal(c.x, 3.125, 0.0001)); + assert!(nearly_equal(c.y, 12.0, 0.0001)); + + let mut a = Vector2 { x: 2.5, y: 6.0 }; + let b = Vector2 { x: 1.25, y: 2.0 }; + a *= b; + assert!(nearly_equal(a.x, 3.125, 0.0001)); + assert!(nearly_equal(a.y, 12.0, 0.0001)); + } + + #[test] + pub fn test_div() { + let a = Vector2 { x: 2.5, y: 6.0 }; + let b = Vector2 { x: 1.25, y: 2.0 }; + let c = a / b; + assert!(nearly_equal(c.x, 2.0, 0.0001)); + assert!(nearly_equal(c.y, 3.0, 0.0001)); + + let mut a = Vector2 { x: 2.5, y: 6.0 }; + let b = Vector2 { x: 1.25, y: 2.0 }; + a /= b; + assert!(nearly_equal(a.x, 2.0, 0.0001)); + assert!(nearly_equal(a.y, 3.0, 0.0001)); + } + + #[test] + pub fn test_scalar_mul() { + let a = Vector2 { x: 1.0, y: 2.0 }; + let b = a * 2.0; + assert!(nearly_equal(b.x, 2.0, 0.0001)); + assert!(nearly_equal(b.y, 4.0, 0.0001)); + + let mut a = Vector2 { x: 1.0, y: 2.0 }; + a *= 2.0; + assert!(nearly_equal(b.x, 2.0, 0.0001)); + assert!(nearly_equal(b.y, 4.0, 0.0001)); + } + + #[test] + pub fn test_scalar_div() { + let a = Vector2 { x: 1.0, y: 2.0 }; + let b = a / 2.0; + assert!(nearly_equal(b.x, 0.5, 0.0001)); + assert!(nearly_equal(b.y, 1.0, 0.0001)); + + let mut a = Vector2 { x: 1.0, y: 2.0 }; + a /= 2.0; + assert!(nearly_equal(b.x, 0.5, 0.0001)); + assert!(nearly_equal(b.y, 1.0, 0.0001)); + } + + #[test] + pub fn test_nearly_equal() { + let a = Vector2 { x: 3.4, y: -7.1 }; + let b = Vector2 { x: 3.5, y: -7.1 }; + assert!(!a.nearly_equal(b, 0.0001)); + + let a = Vector2 { x: 2.0, y: 4.0 }; + let b = Vector2 { x: 2.0, y: 4.0 }; + assert!(a.nearly_equal(b, 0.0001)); + } + + #[test] + pub fn test_length() { + let v = Vector2 { x: 6.0, y: 8.0 }; + let length_squared = v.length_squared(); + let length = v.length(); + assert!(nearly_equal(length_squared, 100.0, 0.0001)); + assert!(nearly_equal(length, 10.0, 0.0001)); + } + + #[test] + pub fn test_dot() { + let a = Vector2 { x: -6.0, y: 8.0 }; + let b = Vector2 { x: 5.0, y: 12.0 }; + let dot = a.dot(&b); + assert!(nearly_equal(dot, 66.0, 0.0001)); + + let a = Vector2 { x: -12.0, y: 16.0 }; + let b = Vector2 { x: 12.0, y: 9.0 }; + let dot = a.dot(&b); + assert!(nearly_equal(dot, 0.0, 0.0001)); + } + + #[test] + pub fn test_distance() { + let a = Vector2 { x: 1.0, y: 1.0 }; + let b = Vector2 { x: 1.0, y: 3.0 }; + let distance_squared = a.distance_squared(&b); + let distance = a.distance(&b); + assert!(nearly_equal(distance_squared, 4.0, 0.0001)); + assert!(nearly_equal(distance, 2.0, 0.0001)); + } + + #[test] + pub fn test_normalize() { + let v = Vector2 { x: 3.0, y: 4.0 }; + let normalized = v.normalize(); + assert!(nearly_equal(normalized.x, 0.6, 0.0001)); + assert!(nearly_equal(normalized.y, 0.8, 0.0001)); + } + + #[test] + pub fn test_extend() { + let v = Vector2 { x: 10.0, y: 1.0 }; + let extended = v.extend(2.0); + assert!(nearly_equal(extended.x, 1.990, 0.0001)); + assert!(nearly_equal(extended.y, 0.199, 0.0001)); + } + + #[test] + #[rustfmt::skip] + pub fn test_angle() { + assert!(nearly_equal(RADIANS_0, Vector2::new(5.0, 0.0).angle(), 0.0001)); + assert!(nearly_equal(RADIANS_45, Vector2::new(5.0, 5.0).angle(), 0.0001)); + assert!(nearly_equal(RADIANS_90, Vector2::new(0.0, 5.0).angle(), 0.0001)); + assert!(nearly_equal(RADIANS_135, Vector2::new(-5.0, 5.0).angle(), 0.0001)); + assert!(nearly_equal(RADIANS_180, Vector2::new(-5.0, 0.0).angle(), 0.0001)); + assert!(nearly_equal(-RADIANS_135, Vector2::new(-5.0, -5.0).angle(), 0.0001)); + assert!(nearly_equal(-RADIANS_90, Vector2::new(0.0, -5.0).angle(), 0.0001)); + assert!(nearly_equal(-RADIANS_45, Vector2::new(5.0, -5.0).angle(), 0.0001)); + + assert!(nearly_equal(crate::math::UP, Vector2::UP.angle(), 0.0001)); + assert!(nearly_equal(crate::math::DOWN, Vector2::DOWN.angle(), 0.0001)); + assert!(nearly_equal(crate::math::LEFT, Vector2::LEFT.angle(), 0.0001)); + assert!(nearly_equal(crate::math::RIGHT, Vector2::RIGHT.angle(), 0.0001)); + } + + #[test] + pub fn test_from_angle() { + let v = Vector2::from_angle(RADIANS_0); + assert!(nearly_equal(v.x, 1.0, 0.000001)); + assert!(nearly_equal(v.y, 0.0, 0.000001)); + let v = Vector2::from_angle(RADIANS_45); + assert!(nearly_equal(v.x, 0.707106, 0.000001)); + assert!(nearly_equal(v.y, 0.707106, 0.000001)); + let v = Vector2::from_angle(RADIANS_225); + assert!(nearly_equal(v.x, -0.707106, 0.000001)); + assert!(nearly_equal(v.y, -0.707106, 0.000001)); + + let v = Vector2::from_angle(UP); + assert!(v.nearly_equal(Vector2::UP, 0.000001)); + let v = Vector2::from_angle(DOWN); + assert!(v.nearly_equal(Vector2::DOWN, 0.000001)); + let v = Vector2::from_angle(LEFT); + assert!(v.nearly_equal(Vector2::LEFT, 0.000001)); + let v = Vector2::from_angle(RIGHT); + assert!(v.nearly_equal(Vector2::RIGHT, 0.000001)); + } + + #[test] + pub fn test_angle_between() { + let a = Vector2::new(20.0, 20.0); + let b = Vector2::new(10.0, 10.0); + let angle = a.angle_between(&b); + assert!(nearly_equal(-RADIANS_135, angle, 0.0001)); + + let a = Vector2::new(0.0, 0.0); + let b = Vector2::new(10.0, 10.0); + let angle = a.angle_between(&b); + assert!(nearly_equal(RADIANS_45, angle, 0.0001)); + + let a = Vector2::new(5.0, 5.0); + let b = Vector2::new(5.0, 5.0); + let angle = a.angle_between(&b); + assert!(nearly_equal(0.0, angle, 0.0001)); + } + + #[test] + pub fn test_lerp() { + let a = Vector2 { x: 5.0, y: 1.0 }; + let b = Vector2 { x: 10.0, y: 2.0 }; + let c = lerp(a, b, 0.5); + assert!(nearly_equal(c.x, 7.5, 0.0001)); + assert!(nearly_equal(c.y, 1.5, 0.0001)); + } +} diff --git a/libretrogd/src/states/mod.rs b/libretrogd/src/states/mod.rs new file mode 100644 index 0000000..68b7958 --- /dev/null +++ b/libretrogd/src/states/mod.rs @@ -0,0 +1,1264 @@ +use std::collections::VecDeque; +use std::ops::DerefMut; + +use thiserror::Error; + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum TransitionTo { + Paused, + Dead, +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum TransitionDirection { + In, + Out +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum State { + Pending, + Active, + Paused, + TransitionIn, + TransitionOut(TransitionTo), + Dead, +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +pub enum StateChange { + Push(Box>), + Pop, +} + +pub trait GameState { + fn update(&mut self, state: State, context: &mut ContextType) -> Option>; + fn render(&mut self, state: State, context: &mut ContextType); + fn transition(&mut self, state: State, context: &mut ContextType) -> bool; + fn state_change(&mut self, new_state: State, old_state: State, context: &mut ContextType); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#[derive(Error, Debug)] +pub enum StateError { + #[error("Operation cannot currently be performed because there is already a pending state change.")] + HasPendingStateChange, + + #[error("Operation cannot currently be performed because the GameState's current state ({0:?}) does not allow it.")] + GameStateInvalidState(State), +} + +struct StateContainer { + current_state: State, + pending_state_change: Option, + state: Box>, +} + +impl StateContainer { + pub fn new(state: Box>) -> Self { + StateContainer { + current_state: State::Dead, + pending_state_change: None, + state, + } + } + + #[inline] + pub fn current_state(&self) -> State { + self.current_state + } + + #[inline] + pub fn pending_state_change(&mut self) -> Option { + self.pending_state_change.take() + } + + #[inline] + pub fn change_state(&mut self, new_state: State, context: &mut ContextType) { + let old_state = self.current_state; + self.current_state = new_state; + self.state.state_change(self.current_state, old_state, context); + } + + #[inline] + pub fn state(&mut self) -> &mut dyn GameState { + self.state.deref_mut() + } + + pub fn transition_out(&mut self, to: TransitionTo, context: &mut ContextType) -> Result<(), StateError> { + if self.current_state == State::Active { + self.change_state(State::TransitionOut(to), context); + Ok(()) + } else { + Err(StateError::GameStateInvalidState(self.current_state)) + } + } + + #[inline] + pub fn pending_transition_out(&mut self, to: TransitionTo) { + self.pending_state_change = Some(State::TransitionOut(to)); + } + + pub fn transition_in(&mut self, context: &mut ContextType) -> Result<(), StateError> { + match self.current_state { + State::Pending | State::Paused => { + self.change_state(State::TransitionIn, context); + Ok(()) + }, + _ => { + Err(StateError::GameStateInvalidState(self.current_state)) + } + } + } + + #[inline] + pub fn pending_transition_in(&mut self) { + self.pending_state_change = Some(State::TransitionIn); + } + + pub fn activate(&mut self, context: &mut ContextType) -> Result<(), StateError> { + self.change_state(State::Active, context); + Ok(()) + } + + #[inline] + pub fn pending_activate(&mut self) { + self.pending_state_change = Some(State::Active); + } + + pub fn pause(&mut self, context: &mut ContextType) -> Result<(), StateError> { + self.change_state(State::Paused, context); + Ok(()) + } + + #[inline] + pub fn pending_pause(&mut self) { + self.pending_state_change = Some(State::Paused); + } + + pub fn kill(&mut self, context: &mut ContextType) -> Result<(), StateError> { + self.change_state(State::Dead, context); + Ok(()) + } + + #[inline] + pub fn pending_kill(&mut self) { + self.pending_state_change = Some(State::Dead); + } +} + +pub struct States { + states: VecDeque>, + command: Option>, + pending_state: Option>>, +} + +impl States { + pub fn new() -> Self { + States { + states: VecDeque::new(), + command: None, + pending_state: None, + } + } + + fn can_push_or_pop(&self) -> bool { + if let Some(state) = self.states.front() { + if state.current_state != State::Active { + return false; + } + } + if self.pending_state.is_some() { + return false; + } + + true + } + + fn push_boxed_state(&mut self, boxed_state: Box>) -> Result<(), StateError> { + if !self.can_push_or_pop() { + Err(StateError::HasPendingStateChange) + } else { + self.command = Some(StateChange::Push(boxed_state)); + Ok(()) + } + } + + pub fn push(&mut self, state: impl GameState + 'static) -> Result<(), StateError> { + self.push_boxed_state(Box::new(state)) + } + + pub fn pop(&mut self) -> Result<(), StateError> { + if !self.can_push_or_pop() { + Err(StateError::HasPendingStateChange) + } else { + if !self.states.is_empty() { + self.command = Some(StateChange::Pop); + } + Ok(()) + } + } + + fn state_of_front_state(&self) -> Option { + if let Some(state) = self.states.front() { + Some(state.current_state()) + } else { + None + } + } + + fn process_state_changes(&mut self, context: &mut ContextType) -> Result<(), StateError> { + if let Some(command) = self.command.take() { + match command { + StateChange::Push(new_state) => { + self.pending_state = Some(new_state); + } + StateChange::Pop => { + if let Some(state) = self.states.front_mut() { + state.pending_transition_out(TransitionTo::Dead); + } + } + } + } + + if self.pending_state.is_some() { + if self.states.is_empty() { + // special case to bootstrap the stack of states when e.g. the system is first set + // up with the very first state pushed to it. + let mut new_state = StateContainer::new(self.pending_state.take().unwrap()); + new_state.change_state(State::Pending, context); + self.states.push_front(new_state); + } else if self.state_of_front_state() == Some(State::Active) { + // if the current state is active and there is a pending state waiting to be added, + // we need to start transitioning out the active state towards a 'paused' state + let state = self.states.front_mut().unwrap(); + state.pending_transition_out(TransitionTo::Paused); + } + } + + // handle any pending state change queued from the previous frame, so that we can + // process the state as necessary below ... + if let Some(state) = self.states.front_mut() { + if let Some(pending_state_change) = state.pending_state_change() { + match pending_state_change { + State::Dead => state.kill(context)?, + State::Paused => state.pause(context)?, + State::Active => state.activate(context)?, + State::TransitionOut(to) => state.transition_out(to, context)?, + State::TransitionIn => state.transition_in(context)?, + _ => {}, + } + } + } + + + // now figure out what state change processing is needed based on the current state ... + match self.state_of_front_state() { + Some(State::Pending) => { + // top state is just sitting there pending, lets start it up ... + let state = self.states.front_mut().unwrap(); + state.pending_transition_in(); + }, + Some(State::Paused) => { + if self.pending_state.is_some() { + // top state is paused and we have a new state waiting to be added. + // add the new state + let mut new_state = StateContainer::new(self.pending_state.take().unwrap()); + new_state.change_state(State::Pending, context); + self.states.push_front(new_state); + } + }, + Some(State::Dead) => { + // remove the dead state + self.states.pop_front(); + + if self.pending_state.is_some() { + // if there is a new pending state waiting, we can add it here right now + let mut new_state = StateContainer::new(self.pending_state.take().unwrap()); + new_state.change_state(State::Pending, context); + self.states.push_front(new_state); + } else if self.state_of_front_state() == Some(State::Paused) { + // otherwise, we're probably waking up a state that was paused and needs to + // be resumed since it's once again on top + let state = self.states.front_mut().unwrap(); + state.pending_transition_in(); + } + }, + Some(State::TransitionIn) => { + let state = self.states.front_mut().unwrap(); + if state.state().transition(State::TransitionIn, context) { + // state has indicated it is done transitioning, so we can switch it to active + state.pending_activate(); + } + }, + Some(State::TransitionOut(to)) => { + let state = self.states.front_mut().unwrap(); + if state.state().transition(State::TransitionOut(to), context) { + // state has indicated it is done transitioning, so we can switch it to whatever + // it was transitioning to + match to { + TransitionTo::Paused => { state.pending_pause(); }, + TransitionTo::Dead => { state.pending_kill(); } + } + } + }, + _ => {} + } + + Ok(()) + } + + pub fn update(&mut self, context: &mut ContextType) -> Result<(), StateError> { + self.process_state_changes(context)?; + if let Some(state) = self.states.front_mut() { + let current_state = state.current_state(); + match current_state { + State::Active | State::TransitionIn | State::TransitionOut(_) => { + if let Some(state_change) = state.state().update(current_state, context) { + match state_change { + StateChange::Push(state) => self.push_boxed_state(state)?, + StateChange::Pop => self.pop()?, + } + } + } + _ => {} + } + } + Ok(()) + } + + pub fn render(&mut self, context: &mut ContextType) { + if let Some(state) = self.states.front_mut() { + let current_state = state.current_state(); + match current_state { + State::Active | State::TransitionIn | State::TransitionOut(_) => { + state.state().render(current_state, context); + }, + _ => {} + } + } + } +} + + +#[cfg(test)] +mod tests { + use claim::*; + + use super::*; + + #[derive(Debug, Eq, PartialEq, Copy, Clone)] + enum LogEntry { + Update(u32, State), + Render(u32, State), + Transition(u32, State), + StateChange(u32, State, State), + } + + struct TestContext { + pub log: Vec, + } + + impl TestContext { + pub fn new() -> Self { + TestContext { + log: Vec::new() + } + } + + pub fn log(&mut self, entry: LogEntry) { + self.log.push(entry); + } + + pub fn take_log(&mut self) -> Vec { + let taken = self.log.to_vec(); + self.log.clear(); + taken + } + } + + struct TestState { + id: u32, + counter: u32, + transition_length: u32, + } + + impl TestState { + pub fn new(id: u32) -> Self { + TestState { + id, + counter: 0, + transition_length: 0, + } + } + + pub fn new_with_transition_length(id: u32, transition_length: u32) -> Self { + TestState { + id, + counter: 0, + transition_length, + } + } + } + + impl GameState for TestState { + fn update(&mut self, state: State, context: &mut TestContext) -> Option> { + context.log(LogEntry::Update(self.id, state)); + None + } + + fn render(&mut self, state: State, context: &mut TestContext) { + context.log(LogEntry::Render(self.id, state)); + } + + fn transition(&mut self, state: State, context: &mut TestContext) -> bool { + context.log(LogEntry::Transition(self.id, state)); + if self.counter > 0 { + self.counter -= 1; + } + if self.counter == 0 { + true + } else { + false + } + } + + fn state_change(&mut self, new_state: State, old_state: State, context: &mut TestContext) { + context.log(LogEntry::StateChange(self.id, new_state, old_state)); + match new_state { + State::TransitionIn | State::TransitionOut(_) => { + self.counter = self.transition_length; + }, + _ => {} + } + } + } + + fn tick(states: &mut States, context: &mut ContextType) -> Result<(), StateError> { + states.update(context)?; + states.render(context); + Ok(()) + } + + #[test] + fn push_and_pop_state() -> Result<(), StateError> { + use LogEntry::*; + use State::*; + + const FOO: u32 = 1; + + let mut states = States::::new(); + let mut context = TestContext::new(); + + states.push(TestState::new(FOO))?; + assert_eq!(context.take_log(), vec![]); + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FOO, Pending, Dead)]); + // state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, TransitionIn, Pending), + Transition(FOO, TransitionIn), + Update(FOO, TransitionIn), + Render(FOO, TransitionIn) + ] + ); + // state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, Active, TransitionIn), + Update(FOO, Active), + Render(FOO, Active) + ] + ); + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + // state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, TransitionOut(TransitionTo::Dead), Active), + Transition(FOO, TransitionOut(TransitionTo::Dead)), + Update(FOO, TransitionOut(TransitionTo::Dead)), + Render(FOO, TransitionOut(TransitionTo::Dead)) + ] + ); + // state finished transitioning out, now dies + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FOO, Dead, TransitionOut(TransitionTo::Dead))]); + + // nothing! no states anymore! + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + + Ok(()) + } + + #[test] + fn push_and_pop_state_with_longer_transition() -> Result<(), StateError> { + use LogEntry::*; + use State::*; + + const FOO: u32 = 1; + + let mut states = States::::new(); + let mut context = TestContext::new(); + + states.push(TestState::new_with_transition_length(FOO, 5))?; + assert_eq!(context.take_log(), vec![]); + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FOO, Pending, Dead)]); + // state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, TransitionIn, Pending), + Transition(FOO, TransitionIn), + Update(FOO, TransitionIn), + Render(FOO, TransitionIn) + ] + ); + // wait for transition to finish + for _ in 0..4 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(FOO, TransitionIn), + Update(FOO, TransitionIn), + Render(FOO, TransitionIn) + ] + ); + } + // state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, Active, TransitionIn), + Update(FOO, Active), + Render(FOO, Active) + ] + ); + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + // state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, TransitionOut(TransitionTo::Dead), Active), + Transition(FOO, TransitionOut(TransitionTo::Dead)), + Update(FOO, TransitionOut(TransitionTo::Dead)), + Render(FOO, TransitionOut(TransitionTo::Dead)) + ] + ); + // wait for transition to finish + for _ in 0..4 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(FOO, TransitionOut(TransitionTo::Dead)), + Update(FOO, TransitionOut(TransitionTo::Dead)), + Render(FOO, TransitionOut(TransitionTo::Dead)) + ] + ); + } + // state finished transitioning out, now dies + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FOO, Dead, TransitionOut(TransitionTo::Dead))]); + + // nothing! no states anymore! + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + + Ok(()) + } + + #[test] + fn push_and_pop_multiple_states() -> Result<(), StateError> { + use LogEntry::*; + use State::*; + + const FIRST: u32 = 1; + const SECOND: u32 = 2; + + let mut states = States::::new(); + let mut context = TestContext::new(); + + // push first state + + states.push(TestState::new(FIRST))?; + assert_eq!(context.take_log(), vec![]); + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FIRST, Pending, Dead)]); + // first state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionIn, Pending), + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + // first state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Active, TransitionIn), + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + + // push second state + + states.push(TestState::new(SECOND))?; + assert_eq!(context.take_log(), vec![]); + // first state begins to transition out to 'paused' state + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionOut(TransitionTo::Paused), Active), + Transition(FIRST, TransitionOut(TransitionTo::Paused)), + Update(FIRST, TransitionOut(TransitionTo::Paused)), + Render(FIRST, TransitionOut(TransitionTo::Paused)) + ] + ); + // state finished transitioning out, now is paused + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Paused, TransitionOut(TransitionTo::Paused)), + StateChange(SECOND, Pending, Dead), + ] + ); + // second state starts up + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + // second state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, TransitionIn, Pending), + Transition(SECOND, TransitionIn), + Update(SECOND, TransitionIn), + Render(SECOND, TransitionIn) + ] + ); + // second state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, Active, TransitionIn), + Update(SECOND, Active), + Render(SECOND, Active) + ] + ); + + // pop second state + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + // second state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, TransitionOut(TransitionTo::Dead), Active), + Transition(SECOND, TransitionOut(TransitionTo::Dead)), + Update(SECOND, TransitionOut(TransitionTo::Dead)), + Render(SECOND, TransitionOut(TransitionTo::Dead)) + ] + ); + // second state finished transitioning out, now dies. first state wakes up again. + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(SECOND, Dead, TransitionOut(TransitionTo::Dead))]); + // first state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionIn, Paused), + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + // first state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Active, TransitionIn), + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + + // pop first state + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + // first state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionOut(TransitionTo::Dead), Active), + Transition(FIRST, TransitionOut(TransitionTo::Dead)), + Update(FIRST, TransitionOut(TransitionTo::Dead)), + Render(FIRST, TransitionOut(TransitionTo::Dead)) + ] + ); + // first state finished transitioning out, now dies + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FIRST, Dead, TransitionOut(TransitionTo::Dead))]); + + // nothing! no states anymore! + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + + Ok(()) + } + + #[test] + fn push_and_pop_multiple_states_with_longer_transitions() -> Result<(), StateError> { + use LogEntry::*; + use State::*; + + const FIRST: u32 = 1; + const SECOND: u32 = 2; + + let mut states = States::::new(); + let mut context = TestContext::new(); + + // push first state + + states.push(TestState::new_with_transition_length(FIRST, 3))?; + assert_eq!(context.take_log(), vec![]); + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FIRST, Pending, Dead)]); + // first state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionIn, Pending), + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + // wait for transition to finish + for _ in 0..2 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + } + // first state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Active, TransitionIn), + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + + // push second state + + states.push(TestState::new_with_transition_length(SECOND, 5))?; + assert_eq!(context.take_log(), vec![]); + // first state begins to transition out to 'paused' state + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionOut(TransitionTo::Paused), Active), + Transition(FIRST, TransitionOut(TransitionTo::Paused)), + Update(FIRST, TransitionOut(TransitionTo::Paused)), + Render(FIRST, TransitionOut(TransitionTo::Paused)) + ] + ); + // wait for transition to finish + for _ in 0..2 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(FIRST, TransitionOut(TransitionTo::Paused)), + Update(FIRST, TransitionOut(TransitionTo::Paused)), + Render(FIRST, TransitionOut(TransitionTo::Paused)) + ] + ); + } + // first state finished transitioning out, now is paused + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Paused, TransitionOut(TransitionTo::Paused)), + StateChange(SECOND, Pending, Dead) + ] + ); + // second state starts up + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + // second state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, TransitionIn, Pending), + Transition(SECOND, TransitionIn), + Update(SECOND, TransitionIn), + Render(SECOND, TransitionIn) + ] + ); + // wait for transition to finish + for _ in 0..4 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(SECOND, TransitionIn), + Update(SECOND, TransitionIn), + Render(SECOND, TransitionIn) + ] + ); + } + // second state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, Active, TransitionIn), + Update(SECOND, Active), + Render(SECOND, Active) + ] + ); + + // pop second state + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + // second state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, TransitionOut(TransitionTo::Dead), Active), + Transition(SECOND, TransitionOut(TransitionTo::Dead)), + Update(SECOND, TransitionOut(TransitionTo::Dead)), + Render(SECOND, TransitionOut(TransitionTo::Dead)) + ] + ); + // wait for transition to finish + for _ in 0..4 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(SECOND, TransitionOut(TransitionTo::Dead)), + Update(SECOND, TransitionOut(TransitionTo::Dead)), + Render(SECOND, TransitionOut(TransitionTo::Dead)) + ] + ); + } + // second state finished transitioning out, now dies. first state wakes up again. + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(SECOND, Dead, TransitionOut(TransitionTo::Dead))]); + // first state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionIn, Paused), + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + // wait for transition to finish + for _ in 0..2 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + } + // first state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Active, TransitionIn), + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + + // pop first state + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + // first state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionOut(TransitionTo::Dead), Active), + Transition(FIRST, TransitionOut(TransitionTo::Dead)), + Update(FIRST, TransitionOut(TransitionTo::Dead)), + Render(FIRST, TransitionOut(TransitionTo::Dead)) + ] + ); + // wait for transition to finish + for _ in 0..2 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(FIRST, TransitionOut(TransitionTo::Dead)), + Update(FIRST, TransitionOut(TransitionTo::Dead)), + Render(FIRST, TransitionOut(TransitionTo::Dead)) + ] + ); + } + // first state finished transitioning out, now dies + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FIRST, Dead, TransitionOut(TransitionTo::Dead))]); + + // nothing! no states anymore! + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + + Ok(()) + } + + struct SelfPushPopState { + id: u32, + counter: u32, + push_after: Option, + pop_after: u32, + } + + impl SelfPushPopState { + pub fn new(id: u32, push_after: Option, pop_after: u32) -> Self { + SelfPushPopState { + id, + counter: 0, + push_after, + pop_after + } + } + } + + impl GameState for SelfPushPopState { + fn update(&mut self, state: State, context: &mut TestContext) -> Option> { + context.log(LogEntry::Update(self.id, state)); + if state == State::Active { + self.counter += 1; + if self.push_after == Some(self.counter) { + return Some(StateChange::Push(Box::new(SelfPushPopState::new(self.id + 1, None, self.pop_after)))); + } else if self.pop_after == self.counter { + return Some(StateChange::Pop); + } + } + None + } + + fn render(&mut self, state: State, context: &mut TestContext) { + context.log(LogEntry::Render(self.id, state)); + } + + fn transition(&mut self, state: State, context: &mut TestContext) -> bool { + context.log(LogEntry::Transition(self.id, state)); + true + } + + fn state_change(&mut self, new_state: State, old_state: State, context: &mut TestContext) { + context.log(LogEntry::StateChange(self.id, new_state, old_state)); + } + } + + + #[test] + fn state_can_push_and_pop_states_itself() -> Result<(), StateError> { + use LogEntry::*; + use State::*; + + const FIRST: u32 = 1; + const SECOND: u32 = 2; + + let mut states = States::::new(); + let mut context = TestContext::new(); + + // pop first state. it will do the rest this time ... + + states.push(SelfPushPopState::new(FIRST, Some(5), 10))?; + assert_eq!(context.take_log(), vec![]); + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FIRST, Pending, Dead)]); + // first state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionIn, Pending), + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + // first state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Active, TransitionIn), + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + // wait for first state's counter to count up to where it should push the second state + for _ in 0..4 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + } + + // first state begins to transition out to 'paused' state because it pushed the second state + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionOut(TransitionTo::Paused), Active), + Transition(FIRST, TransitionOut(TransitionTo::Paused)), + Update(FIRST, TransitionOut(TransitionTo::Paused)), + Render(FIRST, TransitionOut(TransitionTo::Paused)) + ] + ); + // first state finished transitioning out, now is paused + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Paused, TransitionOut(TransitionTo::Paused)), + StateChange(SECOND, Pending, Dead) + ] + ); + // second state starts up + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + // second state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, TransitionIn, Pending), + Transition(SECOND, TransitionIn), + Update(SECOND, TransitionIn), + Render(SECOND, TransitionIn) + ] + ); + // second state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, Active, TransitionIn), + Update(SECOND, Active), + Render(SECOND, Active) + ] + ); + // wait for second state's counter to count up to where it should pop itself + for _ in 0..9 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Update(SECOND, Active), + Render(SECOND, Active) + ] + ); + } + + // second state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, TransitionOut(TransitionTo::Dead), Active), + Transition(SECOND, TransitionOut(TransitionTo::Dead)), + Update(SECOND, TransitionOut(TransitionTo::Dead)), + Render(SECOND, TransitionOut(TransitionTo::Dead)) + ] + ); + // second state finished transitioning out, now dies. first state wakes up again. + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(SECOND, Dead, TransitionOut(TransitionTo::Dead))]); + // first state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionIn, Paused), + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + // first state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Active, TransitionIn), + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + // wait for first state's counter to count up to where it should pop itself + for _ in 0..4 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + } + + // first state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionOut(TransitionTo::Dead), Active), + Transition(FIRST, TransitionOut(TransitionTo::Dead)), + Update(FIRST, TransitionOut(TransitionTo::Dead)), + Render(FIRST, TransitionOut(TransitionTo::Dead)) + ] + ); + // first state finished transitioning out, now dies + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FIRST, Dead, TransitionOut(TransitionTo::Dead))]); + + // nothing! no states anymore! + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + + Ok(()) + } + + #[test] + fn cannot_push_or_pop_states_when_current_state_not_active() -> Result<(), StateError> { + use LogEntry::*; + use State::*; + + const FOO: u32 = 1; + + let mut states = States::::new(); + let mut context = TestContext::new(); + + states.push(TestState::new(FOO))?; + assert_eq!(context.take_log(), vec![]); + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FOO, Pending, Dead)]); + + assert_matches!(states.push(TestState::new(123)), Err(StateError::HasPendingStateChange)); + assert_matches!(states.pop(), Err(StateError::HasPendingStateChange)); + + // state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, TransitionIn, Pending), + Transition(FOO, TransitionIn), + Update(FOO, TransitionIn), + Render(FOO, TransitionIn) + ] + ); + + assert_matches!(states.push(TestState::new(123)), Err(StateError::HasPendingStateChange)); + assert_matches!(states.pop(), Err(StateError::HasPendingStateChange)); + + // state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, Active, TransitionIn), + Update(FOO, Active), + Render(FOO, Active) + ] + ); + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + // state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, TransitionOut(TransitionTo::Dead), Active), + Transition(FOO, TransitionOut(TransitionTo::Dead)), + Update(FOO, TransitionOut(TransitionTo::Dead)), + Render(FOO, TransitionOut(TransitionTo::Dead)) + ] + ); + + assert_matches!(states.push(TestState::new(123)), Err(StateError::HasPendingStateChange)); + assert_matches!(states.pop(), Err(StateError::HasPendingStateChange)); + + // state finished transitioning out, now dies + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FOO, Dead, TransitionOut(TransitionTo::Dead))]); + + states.pop()?; + + // nothing! no states anymore! + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + + Ok(()) + } +} diff --git a/libretrogd/src/system/input_devices/keyboard.rs b/libretrogd/src/system/input_devices/keyboard.rs new file mode 100644 index 0000000..63119fc --- /dev/null +++ b/libretrogd/src/system/input_devices/keyboard.rs @@ -0,0 +1,93 @@ +use sdl2::event::Event; +use sdl2::keyboard::Scancode; + +use super::{ButtonState, InputDevice}; + +const MAX_KEYS: usize = 256; + +/// Holds the current state of the keyboard. +/// +/// Must be explicitly updated each frame by calling `handle_event` each frame for all SDL2 events +/// received, as well as calling `do_events` once each frame. Usually, you would accomplish all +/// this house-keeping by simply calling [`System`]'s `do_events` method once per frame. +/// +/// [`System`]: crate::System +pub struct Keyboard { + keyboard: [ButtonState; MAX_KEYS], // Box<[ButtonState]>, +} + +impl Keyboard { + pub fn new() -> Keyboard { + Keyboard { + keyboard: [ButtonState::Idle; MAX_KEYS], + } + /* + Keyboard { + keyboard: vec![ButtonState::Idle; 256].into_boxed_slice(), + } + */ + } + + /// Returns true if the given key was just pressed or is being held down. + #[inline] + pub fn is_key_down(&self, scancode: Scancode) -> bool { + matches!( + self.keyboard[scancode as usize], + ButtonState::Pressed | ButtonState::Held + ) + } + + /// Returns true if the given key was not just pressed and is not being held down. + #[inline] + pub fn is_key_up(&self, scancode: Scancode) -> bool { + matches!( + self.keyboard[scancode as usize], + ButtonState::Released | ButtonState::Idle + ) + } + + /// Returns true if the given key was just pressed (not being held down, yet). + #[inline] + pub fn is_key_pressed(&self, scancode: Scancode) -> bool { + self.keyboard[scancode as usize] == ButtonState::Pressed + } + + /// Returns true if the given key was just released. + #[inline] + pub fn is_key_released(&self, scancode: Scancode) -> bool { + self.keyboard[scancode as usize] == ButtonState::Released + } +} + +impl InputDevice for Keyboard { + fn update(&mut self) { + for state in self.keyboard.iter_mut() { + *state = match *state { + ButtonState::Pressed => ButtonState::Held, + ButtonState::Released => ButtonState::Idle, + otherwise => otherwise, + }; + } + } + + fn handle_event(&mut self, event: &Event) { + match event { + Event::KeyDown { scancode, .. } => { + if let Some(scancode) = scancode { + let state = &mut self.keyboard[*scancode as usize]; + *state = match *state { + ButtonState::Pressed => ButtonState::Held, + ButtonState::Held => ButtonState::Held, + _ => ButtonState::Pressed, + }; + } + } + Event::KeyUp { scancode, .. } => { + if let Some(scancode) = scancode { + self.keyboard[*scancode as usize] = ButtonState::Released; + } + } + _ => (), + } + } +} diff --git a/libretrogd/src/system/input_devices/mod.rs b/libretrogd/src/system/input_devices/mod.rs new file mode 100644 index 0000000..613177e --- /dev/null +++ b/libretrogd/src/system/input_devices/mod.rs @@ -0,0 +1,27 @@ +use sdl2::event::Event; + +pub mod keyboard; +pub mod mouse; + +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub enum ButtonState { + Idle, + Pressed, + Held, + Released, +} + +/// Common trait for input device implementations. +pub trait InputDevice { + /// Performs internal house-keeping necessary for properly reporting the current state of this + /// input device. Normally this should be called on the device after all of this frame's + /// input events have been processed via `handle_event`. + fn update(&mut self); + + /// Processes the data from the given [`Event`] if it is relevant for this input device. You + /// should pass in all events received every frame and let the input device figure out if it + /// is relevant to it or not. + /// + /// [`Event`]: sdl2::event::Event + fn handle_event(&mut self, event: &Event); +} diff --git a/libretrogd/src/system/input_devices/mouse.rs b/libretrogd/src/system/input_devices/mouse.rs new file mode 100644 index 0000000..3967755 --- /dev/null +++ b/libretrogd/src/system/input_devices/mouse.rs @@ -0,0 +1,318 @@ +use sdl2::event::Event; + +use crate::{Bitmap, BlitMethod, Rect}; + +use super::{ButtonState, InputDevice}; + +const MAX_BUTTONS: usize = 32; + +const DEFAULT_MOUSE_CURSOR_HOTSPOT_X: u32 = 0; +const DEFAULT_MOUSE_CURSOR_HOTSPOT_Y: u32 = 0; +const DEFAULT_MOUSE_CURSOR_WIDTH: usize = 16; +const DEFAULT_MOUSE_CURSOR_HEIGHT: usize = 16; + +#[rustfmt::skip] +const DEFAULT_MOUSE_CURSOR: [u8; DEFAULT_MOUSE_CURSOR_WIDTH * DEFAULT_MOUSE_CURSOR_HEIGHT] = [ + 0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x00,0x00,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x00,0xff,0xff,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff +]; + +/// Holds the current state of the mouse. +/// +/// Must be explicitly updated each frame by calling `handle_event` each frame for all SDL2 events +/// received, as well as calling `do_events` once each frame. Usually, you would accomplish all +/// this house-keeping by simply calling [`System`]'s `do_events` method once per frame. +/// +/// [`System`]: crate::System +pub struct Mouse { + x: i32, + y: i32, + x_delta: i32, + y_delta: i32, + buttons: [ButtonState; MAX_BUTTONS], + cursor: Bitmap, + cursor_background: Bitmap, + cursor_hotspot_x: u32, + cursor_hotspot_y: u32, + cursor_enabled: bool, +} + +impl Mouse { + pub fn new() -> Mouse { + let (cursor, cursor_background, cursor_hotspot_x, cursor_hotspot_y) = + Self::get_default_mouse_cursor(); + + Mouse { + x: 0, + y: 0, + x_delta: 0, + y_delta: 0, + buttons: [ButtonState::Idle; MAX_BUTTONS], + cursor, + cursor_background, + cursor_hotspot_x, + cursor_hotspot_y, + cursor_enabled: false, + } + } + + /// Returns the current x coordinate of the mouse cursor. + #[inline] + pub fn x(&self) -> i32 { + self.x + } + + /// Returns the current y coordinate of the mouse cursor. + #[inline] + pub fn y(&self) -> i32 { + self.y + } + + /// Returns the amount of pixels along the x-axis that the mouse cursor moved since the last + /// time that the mouse state was updated. + #[inline] + pub fn x_delta(&self) -> i32 { + self.x_delta + } + + /// Returns the amount of pixels along the y-axis that the mouse cursor moved since the last + /// time that the mouse state was updated. + #[inline] + pub fn y_delta(&self) -> i32 { + self.y_delta + } + + /// Returns true if the given button was just pressed or is being held down. + #[inline] + pub fn is_button_down(&self, button: usize) -> bool { + matches!( + self.buttons[button], + ButtonState::Pressed | ButtonState::Held + ) + } + + /// Returns true if the given button was not just pressed and is not being held down. + #[inline] + pub fn is_button_up(&self, button: usize) -> bool { + matches!( + self.buttons[button], + ButtonState::Released | ButtonState::Idle + ) + } + + /// Returns true if the given button was just pressed (not being held down, yet). + #[inline] + pub fn is_button_pressed(&self, button: usize) -> bool { + self.buttons[button] == ButtonState::Pressed + } + + /// Returns true if the given button was just released. + #[inline] + pub fn is_button_released(&self, button: usize) -> bool { + self.buttons[button] == ButtonState::Released + } + + /// Returns a reference to the current mouse cursor bitmap. + #[inline] + pub fn cursor_bitmap(&self) -> &Bitmap { + &self.cursor + } + + /// Returns the current mouse cursor's "hotspot" x coordinate. + #[inline] + pub fn cursor_hotspot_x(&self) -> u32 { + self.cursor_hotspot_x + } + + /// Returns the current mouse cursor's "hotspot" y coordinate. + #[inline] + pub fn cursor_hotspot_y(&self) -> u32 { + self.cursor_hotspot_y + } + + /// Returns true if mouse cursor bitmap rendering is enabled. + #[inline] + pub fn is_cursor_enabled(&self) -> bool { + self.cursor_enabled + } + + /// Enables or disables mouse cursor bitmap rendering. + #[inline] + pub fn enable_cursor(&mut self, enable: bool) { + self.cursor_enabled = enable; + } + + /// Sets the [`Bitmap`] used to display the mouse cursor and the "hotspot" coordinate. The + /// bitmap provided here should be set up to use color 255 as the transparent color. + /// + /// # Arguments + /// + /// * `cursor`: the bitmap to be used to display the mouse cursor on screen + /// * `hotspot_x`: the "hotspot" x coordinate + /// * `hotspot_y`: the "hotspot" y coordinate. + pub fn set_mouse_cursor(&mut self, cursor: Bitmap, hotspot_x: u32, hotspot_y: u32) { + self.cursor = cursor; + self.cursor_background = Bitmap::new(self.cursor.width(), self.cursor.height()).unwrap(); + self.cursor_hotspot_x = hotspot_x; + self.cursor_hotspot_y = hotspot_y; + } + + /// Resets the mouse cursor bitmap and "hotspot" coordinate back to the default settings. + pub fn set_default_mouse_cursor(&mut self) { + let (cursor, background, hotspot_x, hotspot_y) = Self::get_default_mouse_cursor(); + self.cursor = cursor; + self.cursor_background = background; + self.cursor_hotspot_x = hotspot_x; + self.cursor_hotspot_y = hotspot_y; + } + + fn get_default_mouse_cursor() -> (Bitmap, Bitmap, u32, u32) { + let mut cursor = Bitmap::new( + DEFAULT_MOUSE_CURSOR_WIDTH as u32, + DEFAULT_MOUSE_CURSOR_HEIGHT as u32, + ) + .unwrap(); + cursor.pixels_mut().copy_from_slice(&DEFAULT_MOUSE_CURSOR); + + let cursor_background = Bitmap::new(cursor.width(), cursor.height()).unwrap(); + + ( + cursor, + cursor_background, + DEFAULT_MOUSE_CURSOR_HOTSPOT_X, + DEFAULT_MOUSE_CURSOR_HOTSPOT_Y, + ) + } + + #[inline] + fn get_cursor_render_position(&self) -> (i32, i32) { + ( + self.x - self.cursor_hotspot_x as i32, + self.y - self.cursor_hotspot_y as i32, + ) + } + + /// Renders the mouse cursor bitmap onto the destination bitmap at the mouse's current + /// position. The destination bitmap specified is assumed to be the [`System`]'s video + /// backbuffer bitmap. The background on the destination bitmap is saved internally and a + /// subsequent call to [`Mouse::hide_cursor`] will restore the background. + /// + /// If mouse cursor rendering is not currently enabled, this method does nothing. + /// + /// Applications will not normally need to call this method, as if mouse cursor rendering is + /// enabled, this will be automatically handled by [`System::display`]. + /// + /// [`System`]: crate::System + /// [`System::display`]: crate::System::display + pub fn render_cursor(&mut self, dest: &mut Bitmap) { + if !self.cursor_enabled { + return; + } + + let (x, y) = self.get_cursor_render_position(); + + // preserve existing background first + self.cursor_background.blit_region( + BlitMethod::Solid, + &dest, + &Rect::new(x, y, self.cursor.width(), self.cursor.height()), + 0, + 0, + ); + + dest.blit(BlitMethod::Transparent(255), &self.cursor, x, y); + } + + /// Restores the original destination bitmap contents where the mouse cursor bitmap was + /// rendered to during the previous call to [`Mouse::render_cursor`]. The destination bitmap + /// specified is assumed to be the [`System`]'s video backbuffer bitmap. + /// + /// If mouse cursor rendering is not currently enabled, this method does nothing. + /// + /// Applications will not normally need to call this method, as if mouse cursor rendering is + /// enabled, this will be automatically handled by [`System::display`]. + /// + /// [`System`]: crate::System + /// [`System::display`]: crate::System::display + pub fn hide_cursor(&mut self, dest: &mut Bitmap) { + if !self.cursor_enabled { + return; + } + + let (x, y) = self.get_cursor_render_position(); + dest.blit(BlitMethod::Solid, &self.cursor_background, x, y); + } + + fn update_button_state(&mut self, button: u32, is_pressed: bool) { + let button_state = &mut self.buttons[button as usize]; + *button_state = if is_pressed { + match *button_state { + ButtonState::Pressed => ButtonState::Held, + ButtonState::Held => ButtonState::Held, + _ => ButtonState::Pressed, + } + } else { + match *button_state { + ButtonState::Pressed | ButtonState::Held => ButtonState::Released, + ButtonState::Released => ButtonState::Idle, + ButtonState::Idle => ButtonState::Idle, + } + } + } +} + +impl InputDevice for Mouse { + fn update(&mut self) { + self.x_delta = 0; + self.y_delta = 0; + for state in self.buttons.iter_mut() { + *state = match *state { + ButtonState::Pressed => ButtonState::Held, + ButtonState::Released => ButtonState::Idle, + otherwise => otherwise, + } + } + } + + fn handle_event(&mut self, event: &Event) { + match event { + Event::MouseMotion { + mousestate, + x, + y, + xrel, + yrel, + .. + } => { + self.x = *x; + self.y = *y; + self.x_delta = *xrel; + self.y_delta = *yrel; + for (button, is_pressed) in mousestate.mouse_buttons() { + self.update_button_state(button as u32, is_pressed); + } + } + Event::MouseButtonDown { mouse_btn, .. } => { + self.update_button_state(*mouse_btn as u32, true); + } + Event::MouseButtonUp { mouse_btn, .. } => { + self.update_button_state(*mouse_btn as u32, false); + } + _ => (), + } + } +} diff --git a/libretrogd/src/system/mod.rs b/libretrogd/src/system/mod.rs new file mode 100644 index 0000000..269bec2 --- /dev/null +++ b/libretrogd/src/system/mod.rs @@ -0,0 +1,400 @@ +use byte_slice_cast::AsByteSlice; +use sdl2::event::Event; +use sdl2::pixels::PixelFormatEnum; +use sdl2::render::{Texture, WindowCanvas}; +use sdl2::{EventPump, Sdl, TimerSubsystem, VideoSubsystem}; +use thiserror::Error; + +use crate::{Bitmap, BitmaskFont, InputDevice, Keyboard, Mouse, Palette}; +use crate::{DEFAULT_SCALE_FACTOR, SCREEN_HEIGHT, SCREEN_WIDTH}; + +pub mod input_devices; + +#[derive(Error, Debug)] +pub enum SystemError { + #[error("System init error: {0}")] + InitError(String), + + #[error("System display error: {0}")] + DisplayError(String), +} + +/// Builder for configuring and constructing an instance of [`System`]. +#[derive(Debug)] +pub struct SystemBuilder { + window_title: String, + vsync: bool, + target_framerate: Option, + initial_scale_factor: u32, + resizable: bool, + show_mouse: bool, + relative_mouse_scaling: bool, + integer_scaling: bool, +} + +impl SystemBuilder { + /// Returns a new [`SystemBuilder`] with a default configuration. + pub fn new() -> SystemBuilder { + SystemBuilder { + window_title: String::new(), + vsync: false, + target_framerate: None, + initial_scale_factor: DEFAULT_SCALE_FACTOR, + resizable: true, + show_mouse: false, + relative_mouse_scaling: true, + integer_scaling: false, + } + } + + /// Set the window title for the [`System`] to be built. + pub fn window_title(&mut self, window_title: &str) -> &mut SystemBuilder { + self.window_title = window_title.to_string(); + self + } + + /// Enables or disables V-Sync for the [`System`] to be built. Enabling V-sync automatically + /// disables `target_framerate`. + pub fn vsync(&mut self, enable: bool) -> &mut SystemBuilder { + self.vsync = enable; + self.target_framerate = None; + self + } + + /// Sets a target framerate for the [`System`] being built to run at. This is intended to be + /// used when V-sync is not desired, so setting a target framerate automatically disables + /// `vsync`. + pub fn target_framerate(&mut self, target_framerate: u32) -> &mut SystemBuilder { + self.target_framerate = Some(target_framerate); + self.vsync = false; + self + } + + /// Sets an integer scaling factor for the [`System`] being built to up-scale the virtual + /// framebuffer to when displaying it on screen. + pub fn scale_factor(&mut self, scale_factor: u32) -> &mut SystemBuilder { + self.initial_scale_factor = scale_factor; + self + } + + /// Sets whether the window will be resizable by the user for the [`System`] being built. + pub fn resizable(&mut self, enable: bool) -> &mut SystemBuilder { + self.resizable = enable; + self + } + + /// Enables or disables mouse cursor display by the operating system when the cursor is over + /// the window for the [`System`] being built. Disable this if you intend to render your own + /// custom mouse cursor. + pub fn show_mouse(&mut self, enable: bool) -> &mut SystemBuilder { + self.show_mouse = enable; + self + } + + /// Enables or disables automatic DPI scaling of mouse relative movement values (delta values) + /// available via the [`Mouse`] input device. + pub fn relative_mouse_scaling(&mut self, enable: bool) -> &mut SystemBuilder { + self.relative_mouse_scaling = enable; + self + } + + /// Enables or disables restricting the final rendered output to always be integer scaled, + /// even if that result will not fully fill the area of the window. + pub fn integer_scaling(&mut self, enable: bool) -> &mut SystemBuilder { + self.integer_scaling = enable; + self + } + + /// Builds and returns a [`System`] from the current configuration. + pub fn build(&self) -> Result { + // todo: maybe let this be customized in the future, or at least halved so a 160x120 mode can be available ... ? + let screen_width = SCREEN_WIDTH; + let screen_height = SCREEN_HEIGHT; + let texture_pixel_size = 4; // 32-bit ARGB format + + sdl2::hint::set( + "SDL_MOUSE_RELATIVE_SCALING", + if self.relative_mouse_scaling { + "1" + } else { + "0" + }, + ); + + // build all the individual SDL subsystems + + let sdl_context = match sdl2::init() { + Ok(sdl_context) => sdl_context, + Err(message) => return Err(SystemError::InitError(message)), + }; + + let sdl_timer_subsystem = match sdl_context.timer() { + Ok(timer_subsystem) => timer_subsystem, + Err(message) => return Err(SystemError::InitError(message)), + }; + + let sdl_video_subsystem = match sdl_context.video() { + Ok(video_subsystem) => video_subsystem, + Err(message) => return Err(SystemError::InitError(message)), + }; + + let sdl_event_pump = match sdl_context.event_pump() { + Ok(event_pump) => event_pump, + Err(message) => return Err(SystemError::InitError(message)), + }; + + // create the window + + let window_width = screen_width * self.initial_scale_factor; + let window_height = screen_height * self.initial_scale_factor; + let mut window_builder = &mut (sdl_video_subsystem.window( + self.window_title.as_str(), + window_width, + window_height, + )); + if self.resizable { + window_builder = window_builder.resizable(); + } + let sdl_window = match window_builder.build() { + Ok(window) => window, + Err(error) => return Err(SystemError::InitError(error.to_string())), + }; + + sdl_context.mouse().show_cursor(self.show_mouse); + + // turn the window into a canvas (under the hood, an SDL Renderer that owns the window) + + let mut canvas_builder = sdl_window.into_canvas(); + if self.vsync { + canvas_builder = canvas_builder.present_vsync(); + } + let mut sdl_canvas = match canvas_builder.build() { + Ok(canvas) => canvas, + Err(error) => return Err(SystemError::InitError(error.to_string())), + }; + if let Err(error) = sdl_canvas.set_logical_size(screen_width, screen_height) { + return Err(SystemError::InitError(error.to_string())); + }; + + // TODO: newer versions of rust-sdl2 support this directly off the WindowCanvas struct + unsafe { + sdl2::sys::SDL_RenderSetIntegerScale( + sdl_canvas.raw(), + if self.integer_scaling { + sdl2::sys::SDL_bool::SDL_TRUE + } else { + sdl2::sys::SDL_bool::SDL_FALSE + }, + ); + } + + // create an SDL texture which we will be uploading to every frame to display the + // application's framebuffer + + let sdl_texture = match sdl_canvas.create_texture_streaming( + Some(PixelFormatEnum::ARGB8888), + screen_width, + screen_height, + ) { + Ok(texture) => texture, + Err(error) => return Err(SystemError::InitError(error.to_string())), + }; + let sdl_texture_pitch = (sdl_texture.query().width * texture_pixel_size) as usize; + + // create a raw 32-bit RGBA buffer that will be used as the temporary source for + // SDL texture uploads each frame. necessary as applications are dealing with 8-bit indexed + // bitmaps, not 32-bit RGBA pixels, so this temporary buffer is where we convert the final + // application framebuffer to 32-bit RGBA pixels before it is uploaded to the SDL texture + let texture_pixels_size = (screen_width * screen_height * texture_pixel_size) as usize; + let texture_pixels = vec![0u32; texture_pixels_size].into_boxed_slice(); + + // create the Bitmap object that will be exposed to the application acting as the system + // backbuffer + + let framebuffer = match Bitmap::new(SCREEN_WIDTH, SCREEN_HEIGHT) { + Ok(bmp) => bmp, + Err(error) => return Err(SystemError::InitError(error.to_string())), + }; + + // create the default palette, initialized to the VGA default palette. also exposed to the + // application for manipulation + + let palette = match Palette::new_vga_palette() { + Ok(palette) => palette, + Err(error) => return Err(SystemError::InitError(error.to_string())), + }; + + // create the default font, initialized to the VGA BIOS default font. + + let font = match BitmaskFont::new_vga_font() { + Ok(font) => font, + Err(error) => return Err(SystemError::InitError(error.to_string())), + }; + + // create input device objects, exposed to the application + + let keyboard = Keyboard::new(); + let mouse = Mouse::new(); + + Ok(System { + sdl_context, + sdl_video_subsystem, + sdl_timer_subsystem, + sdl_canvas, + sdl_texture, + sdl_texture_pitch, + sdl_event_pump, + texture_pixels, + video: framebuffer, + palette, + font, + keyboard, + mouse, + target_framerate: self.target_framerate, + target_framerate_delta: None, + next_tick: 0, + }) + } +} + +/// Holds all primary structures necessary for interacting with the operating system and for +/// applications to render to the display, react to input device events, etc. through the +/// "virtual machine" exposed by this library. +#[allow(dead_code)] +pub struct System { + sdl_context: Sdl, + sdl_video_subsystem: VideoSubsystem, + sdl_timer_subsystem: TimerSubsystem, + sdl_canvas: WindowCanvas, + sdl_texture: Texture, + sdl_texture_pitch: usize, + sdl_event_pump: EventPump, + + texture_pixels: Box<[u32]>, + + target_framerate: Option, + target_framerate_delta: Option, + next_tick: i64, + + /// The primary backbuffer [`Bitmap`] that will be rendered to the screen whenever + /// [`System::display`] is called. Regardless of the actual window size, this bitmap is always + /// [`SCREEN_WIDTH`]x[`SCREEN_HEIGHT`] pixels in size. + pub video: Bitmap, + + /// The [`Palette`] that will be used in conjunction with the `video` backbuffer to + /// render the final output to the screen whenever [`System::display`] is called. + pub palette: Palette, + + /// A pre-loaded [`Font`] that can be used for text rendering. + pub font: BitmaskFont, + + /// The current keyboard state. To ensure it is updated each frame, you should call + /// [`System::do_events`] or [`System::do_events_with`] each frame. + pub keyboard: Keyboard, + + /// The current mouse state. To ensure it is updated each frame, you should call + /// [`System::do_events`] or [`System::do_events_with`] each frame. + pub mouse: Mouse, +} + +impl System { + /// Takes the `video` backbuffer bitmap and `palette` and renders it to the window, up-scaled + /// to fill the window (preserving aspect ratio of course). If V-sync is enabled, this method + /// will block to wait for V-sync. Otherwise, if a target framerate was configured a delay + /// might be used to try to meet that framerate. + pub fn display(&mut self) -> Result<(), SystemError> { + self.mouse.render_cursor(&mut self.video); + + // convert application framebuffer to 32-bit RGBA pixels, and then upload it to the SDL + // texture so it will be displayed on screen + + self.video + .copy_as_argb_to(&mut self.texture_pixels, &self.palette); + + let texture_pixels = self.texture_pixels.as_byte_slice(); + if let Err(error) = self + .sdl_texture + .update(None, texture_pixels, self.sdl_texture_pitch) + { + return Err(SystemError::DisplayError(error.to_string())); + } + self.sdl_canvas.clear(); + if let Err(error) = self.sdl_canvas.copy(&self.sdl_texture, None, None) { + return Err(SystemError::DisplayError(error)); + } + self.sdl_canvas.present(); + + self.mouse.hide_cursor(&mut self.video); + + // if a specific target framerate is desired, apply some loop timing/delay to achieve it + // TODO: do this better. delaying when running faster like this is a poor way to do this.. + + if let Some(target_framerate) = self.target_framerate { + if self.target_framerate_delta.is_some() { + // normal path for every other loop iteration except the first + let delay = self.next_tick - self.ticks() as i64; + if delay < 0 { + // this loop iteration took too long, no need to delay + self.next_tick -= delay; + } else { + // this loop iteration completed before next_tick time, delay by the remainder + // time period so we're running at about the desired framerate + self.delay(((delay * 1000) / self.tick_frequency() as i64) as u32); + } + } else { + // this branch will occur on the first main loop iteration. we use the fact that + // target_framerate_delta was not yet set to avoid doing any delay on the first + // loop, just in case there was some other processing between the System struct + // being created and the actual beginning of the first loop ... + self.target_framerate_delta = + Some((self.tick_frequency() / target_framerate as u64) as i64); + } + + // expected time for the next display() call to happen by + self.next_tick = (self.ticks() as i64) + self.target_framerate_delta.unwrap(); + } + + Ok(()) + } + + /// Checks for and responds to all SDL2 events waiting in the queue. Each event is passed to + /// all [`InputDevice`]'s automatically to ensure input device state is up to date. + pub fn do_events(&mut self) { + self.do_events_with(|_event| {}); + } + + /// Same as [`System::do_events`] but also takes a function which will be called for each + /// SDL2 event being processed (after everything else has already processed it), allowing + /// your application to also react to any events received. + pub fn do_events_with(&mut self, mut f: F) + where + F: FnMut(&Event), + { + self.keyboard.update(); + self.mouse.update(); + self.sdl_event_pump.pump_events(); + for event in self.sdl_event_pump.poll_iter() { + self.keyboard.handle_event(&event); + self.mouse.handle_event(&event); + f(&event); + } + } + + pub fn ticks(&self) -> u64 { + self.sdl_timer_subsystem.performance_counter() + } + + pub fn tick_frequency(&self) -> u64 { + self.sdl_timer_subsystem.performance_frequency() + } + + /// Returns the number of milliseconds elapsed since SDL was initialized. + pub fn millis(&self) -> u32 { + self.sdl_timer_subsystem.ticks() + } + + /// Delays (blocks) for about the number of milliseconds specified. + pub fn delay(&mut self, millis: u32) { + self.sdl_timer_subsystem.delay(millis); + } +} diff --git a/libretrogd/src/utils/bytes.rs b/libretrogd/src/utils/bytes.rs new file mode 100644 index 0000000..5e954db --- /dev/null +++ b/libretrogd/src/utils/bytes.rs @@ -0,0 +1,12 @@ +pub trait ReadFixedLengthByteArray { + fn read_bytes(&mut self) -> Result<[u8; N], std::io::Error>; +} + +impl ReadFixedLengthByteArray for T { + fn read_bytes(&mut self) -> Result<[u8; N], std::io::Error> { + assert_ne!(N, 0); + let mut array = [0u8; N]; + self.read_exact(&mut array)?; + Ok(array) + } +} diff --git a/libretrogd/src/utils/mod.rs b/libretrogd/src/utils/mod.rs new file mode 100644 index 0000000..07f93e9 --- /dev/null +++ b/libretrogd/src/utils/mod.rs @@ -0,0 +1,38 @@ +use std::any::Any; + +use num_traits::Unsigned; +use rand::distributions::uniform::SampleUniform; +use rand::Rng; + +pub mod bytes; +pub mod packbits; + +pub fn rnd_value(low: N, high: N) -> N { + rand::thread_rng().gen_range(low..=high) +} + +/// Returns the absolute difference between two unsigned values. This is just here as a temporary +/// alternative to the `abs_diff` method currently provided by Rust but that is marked unstable. +#[inline] +pub fn abs_diff(a: N, b: N) -> N { + if a < b { + b - a + } else { + a - b + } +} + +pub trait AsAny { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; +} + +impl AsAny for A { + fn as_any(&self) -> &dyn Any { + self as &dyn Any + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self as &mut dyn Any + } +} \ No newline at end of file diff --git a/libretrogd/src/utils/packbits.rs b/libretrogd/src/utils/packbits.rs new file mode 100644 index 0000000..1141e05 --- /dev/null +++ b/libretrogd/src/utils/packbits.rs @@ -0,0 +1,226 @@ +use byteorder::{ReadBytesExt, WriteBytesExt}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum PackBitsError { + #[error("PackBits I/O error")] + IOError(#[from] std::io::Error), +} + +enum PackMode { + Dump, + Run, +} + +pub fn pack_bits(src: &mut S, dest: &mut D, src_length: usize) -> Result<(), PackBitsError> +where + S: ReadBytesExt, + D: WriteBytesExt, +{ + const MIN_RUN: usize = 3; + const MAX_RUN: usize = 128; + const MAX_BUFFER: usize = 128; + + if src_length == 0 { + return Ok(()); + } + let mut bytes_left = src_length; + + let mut buffer = [0u8; (MAX_RUN * 2)]; + + // read the first byte from the source, just to start things off before we get into the loop + buffer[0] = src.read_u8()?; + bytes_left -= 1; + + let mut mode = PackMode::Dump; + let mut run_end = 1; // 1 because we already read the first byte into the buffer + let mut run_start = 0; + let mut previous_byte = buffer[0]; + + while bytes_left > 0 { + let byte = src.read_u8()?; + + buffer[run_end] = byte; + run_end += 1; + + match mode { + // "dump" mode. keep collecting any bytes and write them as-is until we detect the + // start of a run of identical bytes + PackMode::Dump => { + if run_end > MAX_BUFFER { + // we need to flush the temp buffer to the destination + dest.write_u8((run_end - 2) as u8)?; + dest.write_all(&buffer[0..run_end])?; + + buffer[0] = byte; + run_end = 1; + run_start = 0; + } else if byte == previous_byte { + // detected the start of a run of identical bytes + if (run_end - run_start) >= MIN_RUN { + if run_start > 0 { + // we've found a run, flush the buffer we have currently so we can + // start tracking the length of this run + dest.write_u8((run_start - 1) as u8)?; + dest.write_all(&buffer[0..run_start])?; + } + mode = PackMode::Run; + } else if run_start == 0 { + mode = PackMode::Run; + } + } else { + run_start = run_end - 1; + } + } + // "run" mode. keep counting up bytes as long as they are identical to each other. when + // we find the end of a run, write out the run info and switch back to dump mode + PackMode::Run => { + // check for the end of a run of identical bytes + if (byte != previous_byte) || ((run_end - run_start) > MAX_RUN) { + // the identical byte run has ended, write it out to the destination + // (this is just two bytes, the count and the actual byte) + dest.write_i8(-((run_end - run_start - 2) as i8))?; + dest.write_u8(previous_byte)?; + + // clear the temp buffer for our switch back to "dump" mode + buffer[0] = byte; + run_end = 1; + run_start = 0; + mode = PackMode::Dump; + } + } + }; + + previous_byte = byte; + bytes_left -= 1; + } + + // the source bytes have all been read, but we still might have to flush the temp buffer + // out to the destination, or finish writing out a run of identical bytes that was at the very + // end of the source + match mode { + PackMode::Dump => { + dest.write_u8((run_end - 1) as u8)?; + dest.write_all(&buffer[0..run_end])?; + } + PackMode::Run => { + dest.write_i8(-((run_end - run_start - 1) as i8))?; + dest.write_u8(previous_byte)?; + } + }; + + Ok(()) +} + +pub fn unpack_bits( + src: &mut S, + dest: &mut D, + unpacked_length: usize, +) -> Result<(), PackBitsError> +where + S: ReadBytesExt, + D: WriteBytesExt, +{ + let mut buffer = [0u8; 128]; + let mut bytes_written = 0; + + while bytes_written < unpacked_length { + // read the next "code" byte that determines how to process the subsequent byte(s) + let byte = src.read_u8()?; + + if byte > 128 { + // 129-255 = repeat the next byte 257-n times + let run_length = (257 - byte as u32) as usize; + + // read the next byte from the source and repeat it the specified number of times + let byte = src.read_u8()?; + buffer.fill(byte); + dest.write_all(&buffer[0..run_length])?; + bytes_written += run_length; + } else if byte < 128 { + // 0-128 = copy next n-1 bytes from src to dest as-is + let run_length = (byte + 1) as usize; + + src.read_exact(&mut buffer[0..run_length])?; + dest.write_all(&buffer[0..run_length])?; + bytes_written += run_length; + } + + // note that byte == 128 is a no-op (does it even appear in any files ???) + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use super::*; + + struct TestData<'a> { + packed: &'a [u8], + unpacked: &'a [u8], + } + + static TEST_DATA: &[TestData] = &[ + TestData { + packed: &[ + 0xfe, 0xaa, 0x02, 0x80, 0x00, 0x2a, 0xfd, 0xaa, 0x03, 0x80, 0x00, 0x2a, 0x22, 0xf7, + 0xaa, + ], + unpacked: &[ + 0xaa, 0xaa, 0xaa, 0x80, 0x00, 0x2a, 0xaa, 0xaa, 0xaa, 0xaa, 0x80, 0x00, 0x2a, 0x22, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + ], + }, + TestData { + packed: &[0x00, 0xaa], + unpacked: &[0xaa], + }, + TestData { + packed: &[0xf9, 0xaa], + unpacked: &[0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa], + }, + TestData { + packed: &[0xf9, 0xaa, 0x00, 0xbb], + unpacked: &[0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xbb], + }, + TestData { + packed: &[0x07, 0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8], + unpacked: &[0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8], + }, + TestData { + packed: &[0x08, 0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa8], + unpacked: &[0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa8], + }, + TestData { + packed: &[0x06, 0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xfe, 0xa8], + unpacked: &[0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa8, 0xa8], + }, + ]; + + #[test] + fn packs() -> Result<(), PackBitsError> { + for TestData { packed, unpacked } in TEST_DATA { + let mut src = Cursor::new(*unpacked); + let mut dest = vec![0u8; 0]; + pack_bits(&mut src, &mut dest, unpacked.len())?; + assert_eq!(dest, *packed); + } + + Ok(()) + } + + #[test] + fn unpacks() -> Result<(), PackBitsError> { + for TestData { packed, unpacked } in TEST_DATA { + let mut src = Cursor::new(*packed); + let mut dest = vec![0u8; 0]; + unpack_bits(&mut src, &mut dest, unpacked.len())?; + assert_eq!(dest, *unpacked); + } + + Ok(()) + } +} diff --git a/libretrogd/test-assets/dp2.pal b/libretrogd/test-assets/dp2.pal new file mode 100644 index 0000000000000000000000000000000000000000..47099d771641a9936713318ffd88a595b53bffa1 GIT binary patch literal 768 zcma))F-Tic6o&ty<&B-Xxb%X!c!vnZB@W)jMGFyXh$MmcmJ%p}fmAF-cn_+yXdx~I zX&yWmQIbLhUx)YPDLWQYn|quIrXcr9z>Q$z*0`W+o;k z5{bm{@Gt`d;3x0{I0qu&0N4d8zy{y~C7=N0fE1tr?smD^KxIbPrgSh4&@aoK>}oe{w`ph!-`-Xz}kgXfwck4g;j!8fR%%l z!Xt$)^8*@EM1sgrqTffWg)saBRK)ze0m>hd+9$C`@Cl##SZQG)MBa~Wi0*Ch*mmx1 z7-2j%G!KCO!_nI=UpKiMaNFfGnhI+&$&*5nt@{=?+H$H#d3TXg*WQU_9N8ECnzWh#DsF10Q7hSUvN)6&)QrA|nlllp_y2l4DTE#GUI JF|+2Le*?R?=l+*F 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 literal 0 HcmV?d00001 diff --git a/libretrogd/test-assets/test.pcx b/libretrogd/test-assets/test.pcx new file mode 100755 index 0000000000000000000000000000000000000000..af81243bf38aafcf986c5094b13687dd26ad4dc5 GIT binary patch literal 1099 zcmbVMKWGzC9R3Q*i3sQ7;^IMZ=^G+Qmv-n|Tv{Oo4J~QlvPeJ*2BO#`ms~)LVuf}Q z#2lQ9D5+qf2`xbgxJAHEC z6!bDRlpaxxBcz=(=tulS!pgQ&UqD6BDDOqmf8taBz@;ec%i53HSgsfK}inPzDwN7svo9 zUvi^hc3rk@mXa+?e}rBo=&7WK7R{kIi&hfV>+r6EXqRZ6$Rl#-dx8!m z9awaL-Y!}jsJ@2h;r@N3XEA*pQld?X4U$!|o)eaOD7A3Ailm2K+I>sw9(g5ll9Z4n zEwVsb(60>`A7C_KtipH+qYPsKh6^JDBL!myMieI`I?Tt^kR-B*v`X}PXthwS9tlcg zdRhxgyd}9p)+>TXc-({2!fX`@FSMc2x1q##vTwsh#!0Q_n4rIK^jnAb>fG_U?edn* z4aqf&E8u6?D`EQ%YDxH$n7@MbFycdytT#|+_L*H~n^`hj`oQpK>}_GYhMEt59`m=5 zjw3Gh{MRmjZgXdoKQ?%Kg*RVt!{_=Euax*jn*HRrC-&FHc=u7PU7Fl-qmA5nEq$eO zQ$D{j<|jv%?BPf8OQp%ko#@c5@r$;!Vq+J^N6yPj=SL#vhAi_>CsYZcqC(vj$`?ur tUx~X)M8oc7p=_ZRgjy5JO1Mf~QxTzNgnA&8wiF-;+=~?|N_yIzk*suTq literal 0 HcmV?d00001 diff --git a/libretrogd/test-assets/test_bmp_pixels_raw.bin b/libretrogd/test-assets/test_bmp_pixels_raw.bin new file mode 100644 index 0000000..a864fe4 --- /dev/null +++ b/libretrogd/test-assets/test_bmp_pixels_raw.bin @@ -0,0 +1 @@ +~~~~~~~~~~~~~}}~}~}~}~~~}}~}~}~}}}|}}}|}|||}}||}|}|}}|{|||{|{|||{|{|{|{|||{z{z{z{z{z{zz{{z{{z{z{z{zyzzzyzzzzzzyzyzyzyzyzyyyyxyyyxyyyxyyyxyyxyxyxyxyxyxyxyxxxxxxxxxxxxxxxxx \ No newline at end of file diff --git a/libretrogd/test-assets/test_ilbm.lbm b/libretrogd/test-assets/test_ilbm.lbm new file mode 100755 index 0000000000000000000000000000000000000000..94d7cd2092ab57c2c534aee4795840bb25811884 GIT binary patch literal 1722 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 literal 0 HcmV?d00001 diff --git a/libretrogd/test-assets/test_image.pcx b/libretrogd/test-assets/test_image.pcx new file mode 100644 index 0000000000000000000000000000000000000000..41ca0b6b7f24dd8e2ddd644cf0b00f86e96a8e80 GIT binary patch literal 17260 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 literal 0 HcmV?d00001 diff --git a/libretrogd/test-assets/test_pbm.lbm b/libretrogd/test-assets/test_pbm.lbm new file mode 100755 index 0000000000000000000000000000000000000000..f2029a0c7796e0d34d472c874c16fae19b6e4473 GIT binary patch literal 1722 zcmds1L1-IS5PfcwY;NkIz2wpblZ*GD(2|RN$gkrxsa;1Rv9K!(q)SmLrWU9nvb6FF z8q*RN>`S3k3!6(wkP9`ngo6m^;DlcsfeohA=%PU7OD@jAhaLu|vn$j}A%$Ms|M)ZW z-puaKA9nY{$c<^>%+JyBX>)w~+5~{N@s>c=S>N#KGeZi$T%P_Q3cTqnKt+H`#W&(& zei-u?3u4T^37mw-$H(n<`{3YUXJ==7dwXMJqg*bpt*tqZlg(z+>GbUE?9|lM*w|P& z93CDXX5bk39rz7+1~h;z-~mtuRsjdd0%>3#hyY63YqhvuXU}8D;bfAPV$+nH(8C0M zujm_-a_B6enL>32?#CcHBzi>T64~@ef=(2jm~?{9A(~H6{SmHyM!k zjG@HIR4|6~jMLIhUqk<5qAy!~ROgn*`ws6WxuLjbas~VYj!M|Og<1+;6f0Mdxrq1( z6w3kX%pSADoMcwaCNuasj&`wEL(PM?f|W&N;)pA$f6?aW``miUzczSplb_z_hR5}F z-YoI`411}U-z8tH#oKpd`=#iv6KUipYndyR&(*z8FL|la_2k97@e8GB_*P_Oaq@gp znXyanPL7^a7tW1_-x)DO!Tb3dqb*BEEPdP3o~5Wd?$k1ni|CNXuf+?Ba?l1bMc<7bH zYxw0G*RM*|+4q9doYUjoE6wmAPc07eb#IUZ|201I(e+Pc!l(Q{ll(ab*0Qr4lI4i+ zLfmp7i7IhXTyePzUlE}w>FzETi?(oq9V*&lY!SpsUP3=*7XrCp7eaw