#![cfg_attr(not(feature = "std"), no_std)]
mod dim;
pub mod ffi;
mod flags;
mod fmt;
mod glue;
pub mod memstat;
mod motion;
mod music;
mod storage;
#[cfg(all(feature = "std", target_arch = "wasm32"))]
#[global_allocator]
static PIXEL8_ALLOC: memstat::TrackingAlloc = memstat::TrackingAlloc;
use crate::flags::bitflag_enum;
pub use crate::flags::{BitFlag, BitFlags, UnknownBits};
use core::ops::{Bound, RangeBounds};
pub use dim::{Dim, ZeroSize};
pub use glue::__internal;
pub use motion::Body;
pub use music::{Music, MusicBusy, PlayingMusic};
pub use storage::{StorageFull, StorageValue};
pub const SCREEN_WIDTH: u16 = 128;
pub const SCREEN_HEIGHT: u16 = 128;
pub const SPRITE_SHEET_WIDTH: u16 = 128;
pub const SPRITE_SHEET_HEIGHT: u16 = 128;
pub const MAP_WIDTH_TILES: u16 = 128;
pub const MAP_HEIGHT_TILES: u16 = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OutOfBounds;
pub const FPS: u32 = 60;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameRate {
Fps30,
Fps60,
}
impl FrameRate {
pub const fn fps(self) -> u32 {
match self {
FrameRate::Fps30 => 30,
FrameRate::Fps60 => 60,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Color(u8);
impl Color {
pub const BLACK: Color = Color(0);
pub const DARK_BLUE: Color = Color(1);
pub const DARK_PURPLE: Color = Color(2);
pub const DARK_GREEN: Color = Color(3);
pub const BROWN: Color = Color(4);
pub const DARK_GREY: Color = Color(5);
pub const LIGHT_GREY: Color = Color(6);
pub const WHITE: Color = Color(7);
pub const RED: Color = Color(8);
pub const ORANGE: Color = Color(9);
pub const YELLOW: Color = Color(10);
pub const GREEN: Color = Color(11);
pub const BLUE: Color = Color(12);
pub const LAVENDER: Color = Color(13);
pub const PINK: Color = Color(14);
pub const PEACH: Color = Color(15);
pub const fn new(i: u8) -> Option<Color> {
if i < 16 {
Some(Color(i))
} else {
None
}
}
pub const fn index(self) -> u8 {
self.0
}
pub(crate) const fn from_index(i: u8) -> Color {
Color(i & 0x0f)
}
}
bitflag_enum! {
pub enum Button {
Left = 1 << 0,
Right = 1 << 1,
Up = 1 << 2,
Down = 1 << 3,
O = 1 << 4,
X = 1 << 5,
}
}
impl Button {
pub const UP_LEFT: BitFlags<Button> =
unsafe { BitFlags::from_bits_unchecked(Button::Left as u8 | Button::Up as u8) };
pub const UP_RIGHT: BitFlags<Button> =
unsafe { BitFlags::from_bits_unchecked(Button::Right as u8 | Button::Up as u8) };
pub const DOWN_LEFT: BitFlags<Button> =
unsafe { BitFlags::from_bits_unchecked(Button::Left as u8 | Button::Down as u8) };
pub const DOWN_RIGHT: BitFlags<Button> =
unsafe { BitFlags::from_bits_unchecked(Button::Right as u8 | Button::Down as u8) };
}
const fn button_index(b: Button) -> u32 {
(b as u8).trailing_zeros()
}
bitflag_enum! {
pub enum SpriteFlag {
Flag0 = 1 << 0,
Flag1 = 1 << 1,
Flag2 = 1 << 2,
Flag3 = 1 << 3,
Flag4 = 1 << 4,
Flag5 = 1 << 5,
Flag6 = 1 << 6,
Flag7 = 1 << 7,
}
}
bitflag_enum! {
pub enum Channel {
Channel0 = 1 << 0,
Channel1 = 1 << 1,
Channel2 = 1 << 2,
Channel3 = 1 << 3,
}
}
const fn channel_index(c: Channel) -> u32 {
(c as u8).trailing_zeros()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SpriteId(pub u8);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SfxId(u8);
impl SfxId {
pub const fn new(n: u8) -> Option<SfxId> {
if n < 64 {
Some(SfxId(n))
} else {
None
}
}
pub const fn index(self) -> u8 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MusicId(u8);
impl MusicId {
pub const fn new(n: u8) -> Option<MusicId> {
if n < 64 {
Some(MusicId(n))
} else {
None
}
}
pub const fn index(self) -> u8 {
self.0
}
}
pub struct Context {
pub(crate) _private: (),
}
impl Context {
pub fn is_button_down(&self, b: Button) -> bool {
unsafe { ffi::is_button_down(button_index(b)) != 0 }
}
pub fn btn(&self, b: Button) -> bool {
self.is_button_down(b)
}
pub fn is_button_pressed(&self, b: Button) -> bool {
unsafe { ffi::is_button_pressed(button_index(b)) != 0 }
}
pub fn btnp(&self, b: Button) -> bool {
self.is_button_pressed(b)
}
pub fn buttons_down(&self) -> BitFlags<Button> {
BitFlags::from_bits(unsafe { ffi::buttons_down() } as u8)
.expect("buttons_down returned an unknown button bit (pixel8 host/SDK ABI mismatch)")
}
pub fn buttons_pressed(&self) -> BitFlags<Button> {
BitFlags::from_bits(unsafe { ffi::buttons_pressed() } as u8)
.expect("buttons_pressed returned an unknown button bit (pixel8 host/SDK ABI mismatch)")
}
pub fn map_tile(&self, x: i16, y: i16) -> Option<SpriteId> {
if !in_bounds(x, y, MAP_WIDTH_TILES, MAP_HEIGHT_TILES) {
return None;
}
Some(SpriteId(unsafe { ffi::map_tile(x as i32, y as i32) } as u8))
}
pub fn mget(&self, x: i16, y: i16) -> Option<SpriteId> {
self.map_tile(x, y)
}
pub fn set_map_tile(&mut self, x: i16, y: i16, sprite: SpriteId) -> Result<(), OutOfBounds> {
if !in_bounds(x, y, MAP_WIDTH_TILES, MAP_HEIGHT_TILES) {
return Err(OutOfBounds);
}
unsafe { ffi::set_map_tile(x as i32, y as i32, sprite.0 as u32) };
Ok(())
}
pub fn mset(&mut self, x: i16, y: i16, sprite: SpriteId) -> Result<(), OutOfBounds> {
self.set_map_tile(x, y, sprite)
}
pub fn sprite_pixel(&self, x: i16, y: i16) -> Option<Color> {
if !in_bounds(x, y, SPRITE_SHEET_WIDTH, SPRITE_SHEET_HEIGHT) {
return None;
}
Some(Color::from_index(
unsafe { ffi::sprite_pixel(x as i32, y as i32) } as u8,
))
}
pub fn sget(&self, x: i16, y: i16) -> Option<Color> {
self.sprite_pixel(x, y)
}
pub fn set_sprite_pixel(&mut self, x: i16, y: i16, color: Color) -> Result<(), OutOfBounds> {
if !in_bounds(x, y, SPRITE_SHEET_WIDTH, SPRITE_SHEET_HEIGHT) {
return Err(OutOfBounds);
}
unsafe { ffi::set_sprite_pixel(x as i32, y as i32, color.0 as i32) };
Ok(())
}
pub fn sset(&mut self, x: i16, y: i16, color: Color) -> Result<(), OutOfBounds> {
self.set_sprite_pixel(x, y, color)
}
pub fn sprite_flags(&self, sprite: SpriteId) -> BitFlags<SpriteFlag> {
BitFlags::from_bits(unsafe { ffi::sprite_flags(sprite.0 as u32) } as u8).expect(
"sprite_flags returned an unknown sprite-flag bit (pixel8 host/SDK ABI mismatch)",
)
}
pub fn fget(&self, sprite: SpriteId) -> BitFlags<SpriteFlag> {
self.sprite_flags(sprite)
}
pub fn has_sprite_flag(&self, sprite: SpriteId, flag: SpriteFlag) -> bool {
self.sprite_flags(sprite).contains(flag)
}
pub fn set_sprite_flags(&mut self, sprite: SpriteId, flags: impl Into<BitFlags<SpriteFlag>>) {
unsafe { ffi::set_sprite_flags(sprite.0 as u32, flags.into().bits() as u32) }
}
pub fn fset(&mut self, sprite: SpriteId, flags: impl Into<BitFlags<SpriteFlag>>) {
self.set_sprite_flags(sprite, flags)
}
pub fn sfx(&mut self, s: SfxId) {
unsafe { ffi::sfx(s.0 as i32, -1) }
}
pub fn sfx_on(&mut self, s: SfxId, channel: Channel) {
unsafe { ffi::sfx(s.0 as i32, channel_index(channel) as i32) }
}
pub fn sfx_stop(&mut self, channel: Channel) {
unsafe { ffi::sfx(-1, channel_index(channel) as i32) }
}
pub fn music(&mut self, m: MusicId) -> Music {
Music::new(m)
}
pub fn time(&self) -> f32 {
unsafe { ffi::time() }
}
pub fn random<R>(&mut self, range: R) -> f32
where
R: RangeBounds<f32>,
{
let (lo, hi) = f32_bounds(range);
sample_f32(lo, hi, unsafe { ffi::rnd() })
}
pub fn rnd(&mut self, max: f32) -> f32 {
self.random(0.0..max)
}
pub fn random_integer<R>(&mut self, range: R) -> i32
where
R: RangeBounds<i32>,
{
let (lo, count) = i32_bounds(range);
sample_i32(lo, count, unsafe { ffi::rnd() })
}
pub fn rndi(&mut self, max: i32) -> i32 {
self.random_integer(0..max)
}
pub fn seed_rng(&mut self, seed: u32) {
unsafe { ffi::seed_rng(seed) }
}
pub fn srand(&mut self, seed: u32) {
self.seed_rng(seed)
}
pub fn log(&mut self, msg: &str) {
unsafe { ffi::log(msg.as_ptr(), msg.len() as u32) }
}
pub fn cpu_update(&self) -> f32 {
unsafe { ffi::cpu_update() }
}
pub fn cpu_draw(&self) -> f32 {
unsafe { ffi::cpu_draw() }
}
pub fn mem(&self) -> f32 {
crate::memstat::used_fraction()
}
pub fn fps(&self) -> f32 {
unsafe { ffi::fps() }
}
}
pub struct Graphics {
pub(crate) _private: (),
}
impl Graphics {
pub fn clear(&mut self, color: Color) {
unsafe { ffi::clear(color.0 as i32) }
}
pub fn cls(&mut self, color: Color) {
self.clear(color)
}
pub fn camera(&mut self, x: i16, y: i16) {
unsafe { ffi::camera(x as i32, y as i32) }
}
pub fn clip(&mut self, x: i16, y: i16, w: impl Dim, h: impl Dim) -> Result<(), ZeroSize> {
let w = w.to_nonzero().ok_or(ZeroSize)?;
let h = h.to_nonzero().ok_or(ZeroSize)?;
unsafe { ffi::clip(x as i32, y as i32, w.get() as i32, h.get() as i32) };
Ok(())
}
pub fn clip_reset(&mut self) {
unsafe { ffi::clip(0, 0, SCREEN_WIDTH as i32, SCREEN_HEIGHT as i32) }
}
pub fn set_transparent_color(&mut self, color: Color, transparent: bool) {
unsafe { ffi::set_transparent_color(color.0 as i32, transparent as i32) }
}
pub fn palt(&mut self, color: Color, transparent: bool) {
self.set_transparent_color(color, transparent)
}
pub fn reset_transparency(&mut self) {
unsafe { ffi::reset_transparency() }
}
pub fn remap_color(&mut self, from: Color, to: Color) {
unsafe { ffi::remap_color(from.0 as i32, to.0 as i32, 0) }
}
pub fn pal(&mut self, from: Color, to: Color) {
self.remap_color(from, to)
}
pub fn remap_display_color(&mut self, from: Color, to: Color) {
unsafe { ffi::remap_color(from.0 as i32, to.0 as i32, 1) }
}
pub fn pal_display(&mut self, from: Color, to: Color) {
self.remap_display_color(from, to)
}
pub fn reset_palette(&mut self) {
unsafe { ffi::reset_palette() }
}
pub fn set_pixel(&mut self, x: i16, y: i16, color: Color) {
unsafe { ffi::set_pixel(x as i32, y as i32, color.0 as i32) }
}
pub fn pset(&mut self, x: i16, y: i16, color: Color) {
self.set_pixel(x, y, color)
}
pub fn pixel(&self, x: i16, y: i16) -> Option<Color> {
if !in_bounds(x, y, SCREEN_WIDTH, SCREEN_HEIGHT) {
return None;
}
Some(Color::from_index(
unsafe { ffi::pixel(x as i32, y as i32) } as u8
))
}
pub fn pget(&self, x: i16, y: i16) -> Option<Color> {
self.pixel(x, y)
}
pub fn line(&mut self, x0: i16, y0: i16, x1: i16, y1: i16, color: Color) {
unsafe { ffi::line(x0 as i32, y0 as i32, x1 as i32, y1 as i32, color.0 as i32) }
}
pub fn rect(
&mut self,
x: i16,
y: i16,
w: impl Dim,
h: impl Dim,
color: Color,
) -> Result<(), ZeroSize> {
let w = w.to_nonzero().ok_or(ZeroSize)?;
let h = h.to_nonzero().ok_or(ZeroSize)?;
unsafe {
ffi::rect(
x as i32,
y as i32,
x as i32 + w.get() as i32 - 1,
y as i32 + h.get() as i32 - 1,
color.0 as i32,
)
};
Ok(())
}
pub fn rect_fill(
&mut self,
x: i16,
y: i16,
w: impl Dim,
h: impl Dim,
color: Color,
) -> Result<(), ZeroSize> {
let w = w.to_nonzero().ok_or(ZeroSize)?;
let h = h.to_nonzero().ok_or(ZeroSize)?;
unsafe {
ffi::rect_fill(
x as i32,
y as i32,
x as i32 + w.get() as i32 - 1,
y as i32 + h.get() as i32 - 1,
color.0 as i32,
)
};
Ok(())
}
pub fn rectfill(
&mut self,
x: i16,
y: i16,
w: impl Dim,
h: impl Dim,
color: Color,
) -> Result<(), ZeroSize> {
self.rect_fill(x, y, w, h, color)
}
pub fn circle(&mut self, x: i16, y: i16, r: u16, color: Color) {
unsafe { ffi::circle(x as i32, y as i32, r as i32, color.0 as i32) }
}
pub fn circ(&mut self, x: i16, y: i16, r: u16, color: Color) {
self.circle(x, y, r, color)
}
pub fn circle_fill(&mut self, x: i16, y: i16, r: u16, color: Color) {
unsafe { ffi::circle_fill(x as i32, y as i32, r as i32, color.0 as i32) }
}
pub fn circfill(&mut self, x: i16, y: i16, r: u16, color: Color) {
self.circle_fill(x, y, r, color)
}
pub fn ellipse(
&mut self,
x: i16,
y: i16,
w: impl Dim,
h: impl Dim,
color: Color,
) -> Result<(), ZeroSize> {
let w = w.to_nonzero().ok_or(ZeroSize)?;
let h = h.to_nonzero().ok_or(ZeroSize)?;
unsafe {
ffi::ellipse(
x as i32,
y as i32,
x as i32 + w.get() as i32 - 1,
y as i32 + h.get() as i32 - 1,
color.0 as i32,
)
};
Ok(())
}
pub fn oval(
&mut self,
x: i16,
y: i16,
w: impl Dim,
h: impl Dim,
color: Color,
) -> Result<(), ZeroSize> {
self.ellipse(x, y, w, h, color)
}
pub fn ellipse_fill(
&mut self,
x: i16,
y: i16,
w: impl Dim,
h: impl Dim,
color: Color,
) -> Result<(), ZeroSize> {
let w = w.to_nonzero().ok_or(ZeroSize)?;
let h = h.to_nonzero().ok_or(ZeroSize)?;
unsafe {
ffi::ellipse_fill(
x as i32,
y as i32,
x as i32 + w.get() as i32 - 1,
y as i32 + h.get() as i32 - 1,
color.0 as i32,
)
};
Ok(())
}
pub fn ovalfill(
&mut self,
x: i16,
y: i16,
w: impl Dim,
h: impl Dim,
color: Color,
) -> Result<(), ZeroSize> {
self.ellipse_fill(x, y, w, h, color)
}
pub fn set_fill_pattern(&mut self, pattern: u16, secondary: Color) {
unsafe { ffi::set_fill_pattern(pattern as i32, secondary.0 as i32, 0) }
}
pub fn fillp(&mut self, pattern: u16) {
self.set_fill_pattern(pattern, Color::BLACK)
}
pub fn set_fill_pattern_transparent(&mut self, pattern: u16) {
unsafe { ffi::set_fill_pattern(pattern as i32, 0, 1) }
}
pub fn clear_fill_pattern(&mut self) {
unsafe { ffi::set_fill_pattern(0, 0, 0) }
}
pub fn print(&mut self, text: &str, x: i16, y: i16, color: Color) -> i16 {
unsafe {
ffi::print(
text.as_ptr(),
text.len() as u32,
x as i32,
y as i32,
color.0 as i32,
) as i16
}
}
pub fn set_pen_color(&mut self, color: Color) {
unsafe { ffi::set_pen_color(color.0 as i32) }
}
pub fn color(&mut self, color: Color) {
self.set_pen_color(color)
}
pub fn set_cursor(&mut self, x: i16, y: i16) {
unsafe { ffi::set_cursor(x as i32, y as i32) }
}
pub fn cursor(&mut self, x: i16, y: i16) {
self.set_cursor(x, y)
}
pub fn print_pen(&mut self, text: &str) -> i16 {
unsafe { ffi::print_pen(text.as_ptr(), text.len() as u32) as i16 }
}
pub fn sprite(&mut self, sprite: SpriteId, x: i16, y: i16) {
unsafe { ffi::sprite(sprite.0 as u32, x as i32, y as i32, 8, 8, 0, 0) }
}
pub fn spr(&mut self, sprite: SpriteId, x: i16, y: i16) {
self.sprite(sprite, x, y)
}
#[allow(clippy::too_many_arguments)]
pub fn sprite_ext(
&mut self,
sprite: SpriteId,
x: i16,
y: i16,
w: impl Dim,
h: impl Dim,
flip_x: bool,
flip_y: bool,
) -> Result<(), ZeroSize> {
let w = w.to_nonzero().ok_or(ZeroSize)?;
let h = h.to_nonzero().ok_or(ZeroSize)?;
unsafe {
ffi::sprite(
sprite.0 as u32,
x as i32,
y as i32,
w.get() as i32,
h.get() as i32,
flip_x as i32,
flip_y as i32,
)
};
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn sprite_stretch(
&mut self,
sx: i16,
sy: i16,
sw: impl Dim,
sh: impl Dim,
dx: i16,
dy: i16,
dw: impl Dim,
dh: impl Dim,
flip_x: bool,
flip_y: bool,
) -> Result<(), ZeroSize> {
let sw = sw.to_nonzero().ok_or(ZeroSize)?;
let sh = sh.to_nonzero().ok_or(ZeroSize)?;
let dw = dw.to_nonzero().ok_or(ZeroSize)?;
let dh = dh.to_nonzero().ok_or(ZeroSize)?;
unsafe {
ffi::sprite_stretch(
sx as i32,
sy as i32,
sw.get() as i32,
sh.get() as i32,
dx as i32,
dy as i32,
dw.get() as i32,
dh.get() as i32,
flip_x as i32,
flip_y as i32,
)
};
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn sspr(
&mut self,
sx: i16,
sy: i16,
sw: impl Dim,
sh: impl Dim,
dx: i16,
dy: i16,
dw: impl Dim,
dh: impl Dim,
flip_x: bool,
flip_y: bool,
) -> Result<(), ZeroSize> {
self.sprite_stretch(sx, sy, sw, sh, dx, dy, dw, dh, flip_x, flip_y)
}
#[allow(clippy::too_many_arguments)]
pub fn map(
&mut self,
cel_x: i16,
cel_y: i16,
sx: i16,
sy: i16,
cel_w: impl Dim,
cel_h: impl Dim,
layers: impl Into<BitFlags<SpriteFlag>>,
) -> Result<(), ZeroSize> {
let cel_w = cel_w.to_nonzero().ok_or(ZeroSize)?;
let cel_h = cel_h.to_nonzero().ok_or(ZeroSize)?;
let layers = layers.into().bits() as u32;
unsafe {
ffi::map(
cel_x as i32,
cel_y as i32,
sx as i32,
sy as i32,
cel_w.get() as i32,
cel_h.get() as i32,
layers,
)
};
Ok(())
}
}
pub trait Game {
const FRAME_RATE: FrameRate = FrameRate::Fps60;
fn update(&mut self, ctx: &mut Context);
fn draw(&self, gfx: &mut Graphics);
}
#[macro_export]
macro_rules! game {
($game:ty = $init:expr) => {
static GAME: $crate::__internal::Slot<$game> = $crate::__internal::Slot::new();
#[no_mangle]
pub extern "C" fn pixel8_init() {
GAME.init(|| $init);
}
#[no_mangle]
pub extern "C" fn pixel8_fps() -> u32 {
GAME.fps()
}
#[no_mangle]
pub extern "C" fn pixel8_mem_used() -> u32 {
$crate::memstat::used_bytes() as u32
}
#[no_mangle]
pub extern "C" fn pixel8_update() {
GAME.update();
}
#[no_mangle]
pub extern "C" fn pixel8_draw() {
GAME.draw();
}
};
($game:ident { $($field:tt)* }) => {
$crate::game!($game = $game { $($field)* });
};
($game:ident) => {
$crate::game!($game = <$game as ::core::default::Default>::default());
};
}
#[macro_export]
macro_rules! printf {
($cap:literal; $gfx:expr, $x:expr, $y:expr, $color:expr, $($arg:tt)*) => {{
let __buf = $crate::__internal::format_args_to_buf::<$cap>(::core::format_args!($($arg)*));
$gfx.print(__buf.as_str(), $x, $y, $color)
}};
($gfx:expr, $x:expr, $y:expr, $color:expr, $($arg:tt)*) => {{
let __buf = $crate::__internal::format_args_to_buf::<{ $crate::__internal::LINE_CAP }>(
::core::format_args!($($arg)*),
);
$gfx.print(__buf.as_str(), $x, $y, $color)
}};
}
#[macro_export]
macro_rules! logf {
($cap:literal; $ctx:expr, $($arg:tt)*) => {{
let __buf = $crate::__internal::format_args_to_buf::<$cap>(::core::format_args!($($arg)*));
$ctx.log(__buf.as_str());
}};
($ctx:expr, $($arg:tt)*) => {{
let __buf = $crate::__internal::format_args_to_buf::<{ $crate::__internal::LINE_CAP }>(
::core::format_args!($($arg)*),
);
$ctx.log(__buf.as_str());
}};
}
fn in_bounds(x: i16, y: i16, w: u16, h: u16) -> bool {
x >= 0 && y >= 0 && (x as u16) < w && (y as u16) < h
}
fn f32_bounds<R>(range: R) -> (f32, f32)
where
R: RangeBounds<f32>,
{
let lo = match range.start_bound() {
Bound::Included(&v) | Bound::Excluded(&v) => v,
Bound::Unbounded => f32::MIN,
};
let hi = match range.end_bound() {
Bound::Included(&v) | Bound::Excluded(&v) => v,
Bound::Unbounded => f32::MAX,
};
(lo, hi)
}
fn i32_bounds<R>(range: R) -> (i64, i64)
where
R: RangeBounds<i32>,
{
let lo = match range.start_bound() {
Bound::Included(&v) => v as i64,
Bound::Excluded(&v) => v as i64 + 1,
Bound::Unbounded => i32::MIN as i64,
};
let hi = match range.end_bound() {
Bound::Included(&v) => v as i64,
Bound::Excluded(&v) => v as i64 - 1,
Bound::Unbounded => i32::MAX as i64,
};
(lo, hi - lo + 1)
}
fn sample_f32(lo: f32, hi: f32, raw: f32) -> f32 {
let width = hi as f64 - lo as f64;
if width <= 0.0 {
lo
} else {
(lo as f64 + raw as f64 * width) as f32
}
}
fn sample_i32(lo: i64, count: i64, raw: f32) -> i32 {
if count <= 0 {
return lo as i32;
}
let idx = ((raw as f64 * count as f64) as i64).min(count - 1);
(lo + idx) as i32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn color_new_validates_the_palette_range() {
assert_eq!(Color::new(0), Some(Color::BLACK));
assert_eq!(Color::new(15), Some(Color::PEACH));
assert_eq!(Color::new(8).map(Color::index), Some(8));
assert_eq!(Color::new(16), None);
const ACCENT: Color = Color::new(8).unwrap();
assert_eq!(ACCENT, Color::RED);
}
#[test]
fn button_index_matches_abi_order() {
assert_eq!(button_index(Button::Left), 0);
assert_eq!(button_index(Button::Right), 1);
assert_eq!(button_index(Button::Up), 2);
assert_eq!(button_index(Button::Down), 3);
assert_eq!(button_index(Button::O), 4);
assert_eq!(button_index(Button::X), 5);
}
#[test]
fn button_aliases_match_primaries() {
let ctx = Context { _private: () };
for b in [
Button::Left,
Button::Right,
Button::Up,
Button::Down,
Button::O,
Button::X,
] {
assert_eq!(ctx.btn(b), ctx.is_button_down(b));
assert_eq!(ctx.btnp(b), ctx.is_button_pressed(b));
}
assert!(ctx.buttons_down().is_empty());
assert!(ctx.buttons_pressed().is_empty());
}
#[test]
fn sprite_flag_and_tile_helpers() {
let ctx = Context { _private: () };
assert!(ctx.sprite_flags(SpriteId(1)).is_empty());
assert_eq!(ctx.sprite_flags(SpriteId(1)), ctx.fget(SpriteId(1)));
assert!(!ctx.has_sprite_flag(SpriteId(1), SpriteFlag::Flag0));
assert_eq!(ctx.map_tile(0, 0), Some(SpriteId(0)));
assert_eq!(ctx.map_tile(0, 0), ctx.mget(0, 0));
assert_eq!(ctx.map_tile(-1, 0), None);
assert_eq!(ctx.map_tile(0, MAP_HEIGHT_TILES as i16), None);
}
#[test]
fn set_map_tile_is_bounds_checked() {
let mut ctx = Context { _private: () };
assert_eq!(ctx.set_map_tile(0, 0, SpriteId(3)), Ok(()));
assert_eq!(ctx.mset(0, 0, SpriteId(3)), Ok(()));
assert_eq!(
ctx.set_map_tile(MAP_WIDTH_TILES as i16, 0, SpriteId(3)),
Err(OutOfBounds)
);
assert_eq!(
ctx.mset(0, MAP_HEIGHT_TILES as i16, SpriteId(3)),
Err(OutOfBounds)
);
}
#[test]
fn button_mask_round_trips() {
let mask = BitFlags::<Button>::from_bits(0b10_1001).unwrap();
assert!(mask.contains(Button::Left));
assert!(mask.contains(Button::Down));
assert!(mask.contains(Button::X));
assert!(!mask.contains(Button::Right));
}
#[test]
fn diagonal_button_constants() {
assert_eq!(Button::UP_LEFT, Button::Left | Button::Up);
assert!(Button::UP_LEFT.contains(Button::Left));
assert!(Button::UP_LEFT.contains(Button::Up));
assert!(!Button::UP_LEFT.contains(Button::Right));
for (a, b) in [
(Button::UP_LEFT, Button::UP_RIGHT),
(Button::UP_LEFT, Button::DOWN_LEFT),
(Button::DOWN_RIGHT, Button::UP_LEFT),
] {
assert_ne!(a, b);
}
}
#[test]
fn map_accepts_flag_set_forms() {
let mut gfx = Graphics { _private: () };
gfx.map(0, 0, 0, 0, 16, 16, BitFlags::empty()).unwrap();
gfx.map(0, 0, 0, 0, 16, 16, SpriteFlag::Flag0).unwrap();
gfx.map(0, 0, 0, 0, 16, 16, SpriteFlag::Flag0 | SpriteFlag::Flag3)
.unwrap();
assert_eq!(gfx.map(0, 0, 0, 0, 0, 16, BitFlags::empty()), Err(ZeroSize));
}
#[test]
fn screen_pixel_read_is_bounds_checked() {
let gfx = Graphics { _private: () };
assert!(gfx.pixel(0, 0).is_some());
assert_eq!(gfx.pixel(1, 1), gfx.pget(1, 1));
assert_eq!(gfx.pixel(-1, 0), None);
assert_eq!(gfx.pixel(SCREEN_WIDTH as i16, 0), None);
assert_eq!(gfx.pixel(0, SCREEN_HEIGHT as i16), None);
}
#[test]
fn graphics_aliases_match_primaries() {
let mut gfx = Graphics { _private: () };
assert_eq!(gfx.pixel(1, 1), gfx.pget(1, 1));
gfx.set_pixel(0, 0, Color::RED);
gfx.pset(0, 0, Color::RED);
gfx.circle(0, 0, 4, Color::RED);
gfx.circ(0, 0, 4, Color::RED);
gfx.circle_fill(0, 0, 4, Color::RED);
gfx.circfill(0, 0, 4, Color::RED);
gfx.rect_fill(0, 0, 4, 4, Color::RED).unwrap();
gfx.rectfill(0, 0, 4, 4, Color::RED).unwrap();
gfx.sprite(SpriteId(0), 0, 0);
gfx.spr(SpriteId(0), 0, 0);
}
#[test]
fn printf_formats_and_returns_cursor() {
let mut gfx = Graphics { _private: () };
let cursor: i16 = printf!(gfx, 0, 0, Color::WHITE, "n={}", 3);
assert_eq!(cursor, 0);
let _: i16 = printf!(64; gfx, 0, 0, Color::WHITE, "{}-{}", 1, 2);
let _: i16 = printf!(gfx, 0, 0, Color::WHITE, "literal");
}
#[test]
fn logf_formats_and_runs() {
let mut ctx = Context { _private: () };
logf!(ctx, "frame {}", 9);
logf!(128; ctx, "{}-{}", 1, 2);
logf!(ctx, "literal");
}
#[test]
fn context_sheet_and_rng_aliases() {
let mut ctx = Context { _private: () };
ctx.seed_rng(1);
ctx.srand(1);
ctx.set_sprite_pixel(0, 0, Color::RED).unwrap();
ctx.sset(0, 0, Color::RED).unwrap();
assert_eq!(ctx.sprite_pixel(0, 0), Some(Color::from_index(0)));
assert_eq!(ctx.sprite_pixel(0, 0), ctx.sget(0, 0));
assert_eq!(ctx.sprite_pixel(-1, 0), None);
assert_eq!(ctx.sprite_pixel(SPRITE_SHEET_WIDTH as i16, 0), None);
assert_eq!(ctx.sprite_pixel(0, SPRITE_SHEET_HEIGHT as i16), None);
assert_eq!(
ctx.set_sprite_pixel(SPRITE_SHEET_WIDTH as i16, 0, Color::RED),
Err(OutOfBounds)
);
assert_eq!(ctx.set_sprite_pixel(5, 5, Color::RED), Ok(()));
}
#[test]
fn f32_bounds_fills_open_ends_with_extremes() {
assert_eq!(f32_bounds(2.0..5.0), (2.0, 5.0));
assert_eq!(f32_bounds(2.0..=5.0), (2.0, 5.0));
assert_eq!(f32_bounds(-5.0..-1.0), (-5.0, -1.0));
assert_eq!(f32_bounds(..44.0), (f32::MIN, 44.0));
assert_eq!(f32_bounds(0.0..), (0.0, f32::MAX));
assert_eq!(f32_bounds(..), (f32::MIN, f32::MAX));
}
#[test]
fn i32_bounds_counts_and_fills_open_ends() {
assert_eq!(i32_bounds(0..10), (0, 10));
assert_eq!(i32_bounds(1..=6), (1, 6));
assert_eq!(i32_bounds(-10..0), (-10, 10));
assert_eq!(i32_bounds(-5..=5), (-5, 11));
assert_eq!(i32_bounds(5..), (5, i32::MAX as i64 - 5 + 1));
assert_eq!(i32_bounds(..10), (i32::MIN as i64, 10 - i32::MIN as i64));
assert_eq!(
i32_bounds(..),
(i32::MIN as i64, i32::MAX as i64 - i32::MIN as i64 + 1)
);
let (a, b): (i32, i32) = (5, 2);
assert_eq!(i32_bounds(a..b), (5, -3));
assert_eq!(i32_bounds(5..5), (5, 0));
}
#[test]
fn sample_f32_maps_guards_and_stays_finite() {
assert_eq!(sample_f32(0.0, 10.0, 0.0), 0.0);
assert!((sample_f32(0.0, 10.0, 0.5) - 5.0).abs() < 1e-5);
assert_eq!(sample_f32(-5.0, 5.0, 0.0), -5.0);
assert!(sample_f32(-5.0, 5.0, 0.5).abs() < 1e-5);
assert_eq!(sample_f32(5.0, 2.0, 0.5), 5.0);
assert_eq!(sample_f32(3.0, 3.0, 0.5), 3.0);
let mid = sample_f32(f32::MIN, f32::MAX, 0.5);
assert!(mid.is_finite());
assert!(mid.abs() < 1e30);
}
#[test]
fn sample_i32_maps_clamps_and_guards() {
assert_eq!(sample_i32(0, 10, 0.0), 0);
assert_eq!(sample_i32(0, 10, 0.999_999), 9);
assert_eq!(sample_i32(0, 10, 0.55), 5);
assert_eq!(sample_i32(1, 6, 0.999_999), 6);
assert_eq!(sample_i32(-10, 10, 0.999_999), -1);
assert_eq!(sample_i32(5, -3, 0.5), 5);
assert_eq!(sample_i32(5, 0, 0.5), 5);
let full = i32::MAX as i64 - i32::MIN as i64 + 1;
assert_eq!(sample_i32(i32::MIN as i64, full, 0.0), i32::MIN);
assert_eq!(sample_i32(i32::MIN as i64, full, 0.5), 0);
}
#[test]
fn context_random_methods_forward() {
let mut ctx = Context { _private: () };
assert_eq!(ctx.random(2.0..5.0), 2.0);
assert_eq!(ctx.random(2.0..=5.0), 2.0);
assert_eq!(ctx.random(0.0..), 0.0);
assert_eq!(ctx.random(..44.0), f32::MIN);
assert_eq!(ctx.random_integer(3..9), 3);
assert_eq!(ctx.random_integer(3..=9), 3);
assert_eq!(ctx.random_integer(5..), 5);
assert_eq!(ctx.random_integer(..10), i32::MIN);
assert_eq!(ctx.rnd(5.0), 0.0);
assert_eq!(ctx.rndi(10), 0);
}
#[test]
fn context_exposes_resource_stats() {
let ctx = Context { _private: () };
assert_eq!(ctx.cpu_update(), 0.0);
assert_eq!(ctx.cpu_draw(), 0.0);
assert_eq!(ctx.mem(), 0.0);
assert_eq!(ctx.fps(), 0.0);
}
#[test]
fn graphics_parity_aliases_compile_and_forward() {
let mut gfx = Graphics { _private: () };
gfx.set_transparent_color(Color::BLACK, true);
gfx.palt(Color::BLACK, true);
gfx.reset_transparency();
gfx.remap_color(Color::RED, Color::BLUE);
gfx.pal(Color::RED, Color::BLUE);
gfx.remap_display_color(Color::RED, Color::BLUE);
gfx.pal_display(Color::RED, Color::BLUE);
gfx.reset_palette();
gfx.sprite_stretch(0, 0, 8, 8, 0, 0, 16, 16, false, false)
.unwrap();
gfx.sspr(0, 0, 8, 8, 0, 0, 16, 16, true, true).unwrap();
assert_eq!(
gfx.sspr(0, 0, 8, 8, 0, 0, 0, 16, false, false),
Err(ZeroSize)
);
gfx.ellipse(0, 0, 8, 6, Color::WHITE).unwrap();
gfx.oval(0, 0, 8, 6, Color::WHITE).unwrap();
gfx.ellipse_fill(0, 0, 8, 6, Color::WHITE).unwrap();
gfx.ovalfill(0, 0, 8, 6, Color::WHITE).unwrap();
gfx.set_fill_pattern(0b1010, Color::RED);
gfx.fillp(0b1010);
gfx.set_fill_pattern_transparent(0b1010);
gfx.clear_fill_pattern();
gfx.set_pen_color(Color::YELLOW);
gfx.color(Color::YELLOW);
gfx.set_cursor(4, 4);
gfx.cursor(4, 4);
let cursor: i16 = gfx.print_pen("hi");
assert_eq!(cursor, 0, "native print_pen stub returns 0");
}
#[test]
fn fallible_rect_and_ellipse() {
let mut gfx = Graphics { _private: () };
assert_eq!(gfx.rect(0, 0, 4, 4, Color::RED), Ok(()));
assert_eq!(gfx.rect_fill(0, 0, 4, 4, Color::RED), Ok(()));
assert_eq!(gfx.ellipse(0, 0, 8, 6, Color::WHITE), Ok(()));
assert_eq!(gfx.ellipse_fill(0, 0, 8, 6, Color::WHITE), Ok(()));
assert_eq!(gfx.rect_fill(0, 0, 0, 4, Color::RED), Err(ZeroSize));
assert_eq!(gfx.rect(0, 0, 4, -1, Color::RED), Err(ZeroSize));
let w = 10 - 4;
assert_eq!(gfx.rect(0, 0, w, 4, Color::RED), Ok(()));
}
#[test]
fn clip_is_fallible_and_reset_is_not() {
let mut gfx = Graphics { _private: () };
assert_eq!(gfx.clip(0, 0, 64, 64), Ok(()));
assert_eq!(gfx.clip(0, 0, 0, 64), Err(ZeroSize));
gfx.clip_reset(); }
#[test]
fn surface_dimensions_and_out_of_bounds_exist() {
assert_eq!((SPRITE_SHEET_WIDTH, SPRITE_SHEET_HEIGHT), (128, 128));
assert_eq!((MAP_WIDTH_TILES, MAP_HEIGHT_TILES), (128, 64));
assert_eq!(OutOfBounds, OutOfBounds);
}
#[test]
fn sprite_and_sprite_ext() {
let mut gfx = Graphics { _private: () };
gfx.sprite(SpriteId(0), 0, 0);
gfx.spr(SpriteId(0), 0, 0);
assert_eq!(
gfx.sprite_ext(SpriteId(0), 0, 0, 8, 8, false, false),
Ok(())
);
assert_eq!(gfx.sprite_ext(SpriteId(0), 0, 0, 4, 8, true, false), Ok(()));
assert_eq!(
gfx.sprite_ext(SpriteId(0), 0, 0, 0, 8, false, false),
Err(ZeroSize)
);
}
#[test]
fn channel_index_matches_abi_order() {
assert_eq!(channel_index(Channel::Channel0), 0);
assert_eq!(channel_index(Channel::Channel1), 1);
assert_eq!(channel_index(Channel::Channel2), 2);
assert_eq!(channel_index(Channel::Channel3), 3);
}
#[test]
fn sfx_channel_methods_take_exactly_one_channel() {
let mut ctx = Context { _private: () };
ctx.sfx(SfxId::new(0).unwrap());
ctx.sfx_on(SfxId::new(1).unwrap(), Channel::Channel2);
ctx.sfx_stop(Channel::Channel2);
}
#[test]
fn sfx_and_music_ids_validate_their_range() {
assert_eq!(SfxId::new(0).map(SfxId::index), Some(0));
assert_eq!(SfxId::new(63).map(SfxId::index), Some(63));
assert_eq!(SfxId::new(64), None);
assert_eq!(MusicId::new(63).map(MusicId::index), Some(63));
assert_eq!(MusicId::new(64), None);
const JUMP: SfxId = SfxId::new(5).unwrap();
assert_eq!(JUMP.index(), 5);
}
}