use super::{Bounds, Contacts, Kinetic, Velocity};
use crate::{BitFlags, Body, SpriteFlag, SpriteId};
pub const CAP: usize = 64;
pub const PROP: u8 = 1;
pub const CONFINED: u8 = 1 << 1;
pub const FLIP_X: u8 = 1 << 2;
pub const FLIP_Y: u8 = 1 << 3;
pub const HIDDEN: u8 = 1 << 4;
pub const UNWORN: u16 = u16::MAX;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct Record {
pub x: f32,
pub y: f32,
pub dx: f32,
pub dy: f32,
pub rx: i16,
pub ry: i16,
pub bx: i16,
pub by: i16,
pub bw: u16,
pub bh: u16,
pub cx: i16,
pub cy: i16,
pub cw: u16,
pub ch: u16,
pub sprite: u16,
pub solid: u8,
pub heeds: u8,
pub meta: u8,
pub sides: u8,
pub touched: u8,
pub span: u8,
}
pub const RECORD: usize = 44;
pub const VACANT: Record = Record {
meta: PROP,
..EMPTY
};
pub const EMPTY: Record = Record {
x: 0.0,
y: 0.0,
dx: 0.0,
dy: 0.0,
rx: 0,
ry: 0,
bx: 0,
by: 0,
bw: 0,
bh: 0,
cx: 0,
cy: 0,
cw: 0,
ch: 0,
sprite: UNWORN,
solid: 0,
heeds: 0,
meta: 0,
sides: 0,
touched: 0,
span: 0,
};
impl Record {
pub fn read(bytes: &[u8; RECORD]) -> Self {
#[inline]
fn f32_at(bytes: &[u8], at: usize) -> f32 {
f32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
}
#[inline]
fn i16_at(bytes: &[u8], at: usize) -> i16 {
i16::from_le_bytes([bytes[at], bytes[at + 1]])
}
Self {
x: f32_at(bytes, 0),
y: f32_at(bytes, 4),
dx: f32_at(bytes, 8),
dy: f32_at(bytes, 12),
rx: i16_at(bytes, 16),
ry: i16_at(bytes, 18),
bx: i16_at(bytes, 20),
by: i16_at(bytes, 22),
bw: i16_at(bytes, 24) as u16,
bh: i16_at(bytes, 26) as u16,
cx: i16_at(bytes, 28),
cy: i16_at(bytes, 30),
cw: i16_at(bytes, 32) as u16,
ch: i16_at(bytes, 34) as u16,
sprite: i16_at(bytes, 36) as u16,
solid: bytes[38],
heeds: bytes[39],
meta: bytes[40],
sides: bytes[41],
touched: bytes[42],
span: bytes[43],
}
}
pub fn write(&self, bytes: &mut [u8; RECORD]) {
bytes[0..4].copy_from_slice(&self.x.to_le_bytes());
bytes[4..8].copy_from_slice(&self.y.to_le_bytes());
bytes[8..12].copy_from_slice(&self.dx.to_le_bytes());
bytes[12..16].copy_from_slice(&self.dy.to_le_bytes());
bytes[16..18].copy_from_slice(&self.rx.to_le_bytes());
bytes[18..20].copy_from_slice(&self.ry.to_le_bytes());
bytes[41] = self.sides;
bytes[42] = self.touched;
}
pub fn look(&self) -> Option<Look> {
if self.sprite == UNWORN || self.meta & HIDDEN != 0 {
return None;
}
Some(Look {
sprite: SpriteId(self.sprite as u8),
x: self.rx,
y: self.ry,
width: (u16::from(self.span & 0x0f) + 1) * CELL,
height: (u16::from(self.span >> 4) + 1) * CELL,
flip_x: self.meta & FLIP_X != 0,
flip_y: self.meta & FLIP_Y != 0,
})
}
}
pub fn looks<'a, F>(
records: &'a [Record],
layers: BitFlags<SpriteFlag>,
carried: F,
) -> impl Iterator<Item = Look> + 'a
where
F: Fn(SpriteId) -> BitFlags<SpriteFlag> + 'a,
{
records
.iter()
.filter_map(Record::look)
.filter(move |look| layers.is_empty() || carried(look.sprite).intersects(layers))
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Look {
pub sprite: SpriteId,
pub x: i16,
pub y: i16,
pub width: u16,
pub height: u16,
pub flip_x: bool,
pub flip_y: bool,
}
pub struct Recast {
body: Body,
velocity: Velocity,
contacts: Contacts,
off_x: i32,
off_y: i32,
width: u16,
height: u16,
sprite: Option<SpriteId>,
solid: BitFlags<SpriteFlag>,
heeds: BitFlags<SpriteFlag>,
confines: Option<Bounds>,
prop: bool,
}
impl Recast {
pub fn of(record: &Record) -> Self {
let mismatch = "step_cast record carried an unknown flag bit (pixel8 host/SDK mismatch)";
Self {
body: Body::from_wire((record.x, record.y, record.rx, record.ry)),
velocity: Velocity::new(record.dx, record.dy),
contacts: Contacts::empty(),
off_x: record.bx as i32 - record.rx as i32,
off_y: record.by as i32 - record.ry as i32,
width: record.bw,
height: record.bh,
sprite: match record.sprite {
UNWORN => None,
id => Some(SpriteId(id as u8)),
},
solid: BitFlags::from_bits(record.solid).expect(mismatch),
heeds: BitFlags::from_bits(record.heeds).expect(mismatch),
confines: (record.meta & CONFINED != 0)
.then(|| Bounds::new(record.cx, record.cy, record.cw, record.ch)),
prop: record.meta & PROP != 0,
}
}
pub fn report(&self, record: &mut Record) {
(record.x, record.y, record.rx, record.ry) = self.body.wire();
record.dx = self.velocity.dx;
record.dy = self.velocity.dy;
(record.sides, record.touched) = self.contacts.wire();
}
}
impl Kinetic for Recast {
fn body(&self) -> &Body {
&self.body
}
fn body_mut(&mut self) -> &mut Body {
&mut self.body
}
fn velocity_mut(&mut self) -> &mut Velocity {
&mut self.velocity
}
fn contacts(&self) -> &Contacts {
&self.contacts
}
fn contacts_mut(&mut self) -> &mut Contacts {
&mut self.contacts
}
fn bounds(&self) -> Bounds {
let (x, y) = self.body.draw_pos();
Bounds::new(
(x as i32 + self.off_x).clamp(i16::MIN as i32, i16::MAX as i32) as i16,
(y as i32 + self.off_y).clamp(i16::MIN as i32, i16::MAX as i32) as i16,
self.width,
self.height,
)
}
fn solid(&self) -> Option<BitFlags<SpriteFlag>> {
Some(self.solid)
}
fn heeds(&self) -> BitFlags<SpriteFlag> {
self.heeds
}
fn sprite(&self) -> Option<SpriteId> {
self.sprite
}
fn confines(&self) -> Option<Bounds> {
self.confines
}
fn prop(&self) -> bool {
self.prop
}
}
const CELL: u16 = 8;
#[cfg(test)]
mod tests {
use super::*;
use core::mem::{offset_of, size_of};
#[test]
fn the_layout_is_the_one_written_down() {
assert_eq!(size_of::<Record>(), RECORD);
assert_eq!(offset_of!(Record, x), 0);
assert_eq!(offset_of!(Record, y), 4);
assert_eq!(offset_of!(Record, dx), 8);
assert_eq!(offset_of!(Record, dy), 12);
assert_eq!(offset_of!(Record, rx), 16);
assert_eq!(offset_of!(Record, ry), 18);
assert_eq!(offset_of!(Record, bx), 20);
assert_eq!(offset_of!(Record, by), 22);
assert_eq!(offset_of!(Record, bw), 24);
assert_eq!(offset_of!(Record, bh), 26);
assert_eq!(offset_of!(Record, cx), 28);
assert_eq!(offset_of!(Record, cy), 30);
assert_eq!(offset_of!(Record, cw), 32);
assert_eq!(offset_of!(Record, ch), 34);
assert_eq!(offset_of!(Record, sprite), 36);
assert_eq!(offset_of!(Record, solid), 38);
assert_eq!(offset_of!(Record, heeds), 39);
assert_eq!(offset_of!(Record, meta), 40);
assert_eq!(offset_of!(Record, sides), 41);
assert_eq!(offset_of!(Record, touched), 42);
assert_eq!(offset_of!(Record, span), 43);
}
#[test]
fn a_record_crosses_the_wire_and_comes_back_itself() {
let record = Record {
x: 12.75,
y: -3.5,
dx: 1.25,
dy: -0.5,
rx: 12,
ry: -4,
bx: 13,
by: -3,
bw: 6,
bh: 7,
cx: -8,
cy: 0,
cw: 144,
ch: 128,
sprite: 9,
solid: 0b0000_0101,
heeds: 0b0000_0010,
meta: CONFINED | FLIP_Y,
sides: 0,
touched: 0,
span: 0x21,
};
let bytes: [u8; RECORD] = unsafe { core::mem::transmute(record) };
let across = Record::read(&bytes);
assert_eq!(across.x, record.x);
assert_eq!(across.y, record.y);
assert_eq!((across.dx, across.dy), (record.dx, record.dy));
assert_eq!((across.rx, across.ry), (record.rx, record.ry));
assert_eq!((across.bx, across.by, across.bw, across.bh), (13, -3, 6, 7));
assert_eq!(
(across.cx, across.cy, across.cw, across.ch),
(-8, 0, 144, 128)
);
assert_eq!(across.sprite, 9);
assert_eq!(
(across.solid, across.heeds, across.meta),
(0b101, 0b10, CONFINED | FLIP_Y)
);
assert_eq!(across.span, 0x21);
let mut bytes = [0u8; RECORD];
let mut answered = across;
answered.sides = 0b1010;
answered.touched = 0b1;
answered.write(&mut bytes);
let back = Record::read(&bytes);
assert_eq!((back.x, back.y), (record.x, record.y));
assert_eq!((back.sides, back.touched), (0b1010, 0b1));
}
#[test]
fn a_step_s_answers_leave_the_look_alone() {
let record = Record {
meta: CONFINED | FLIP_X,
span: 0x12,
sprite: 3,
..EMPTY
};
let bytes: [u8; RECORD] = unsafe { core::mem::transmute(record) };
let mut answered = Record::read(&bytes);
(answered.x, answered.y) = (5.0, 6.0);
(answered.rx, answered.ry) = (5, 6);
(answered.sides, answered.touched) = (0b1, 0b10);
let mut bytes = bytes;
answered.write(&mut bytes);
let back = Record::read(&bytes);
assert_eq!(back.meta, CONFINED | FLIP_X, "write must not touch meta");
assert_eq!(back.span, 0x12, "write must not touch span");
assert_eq!((back.x, back.y), (5.0, 6.0), "the answered position landed");
assert_eq!(
(back.rx, back.ry),
(5, 6),
"the answered drawn pixel landed"
);
assert_eq!(
(back.sides, back.touched),
(0b1, 0b10),
"the answered contacts landed"
);
}
#[test]
fn nothing_shown_has_no_look() {
assert_eq!(EMPTY.look(), None, "an empty record wears nothing");
assert_eq!(VACANT.look(), None, "a vacant seat wears nothing");
let hidden = Record {
sprite: 3,
meta: HIDDEN,
..EMPTY
};
assert_eq!(hidden.look(), None, "a hidden record is never drawn");
}
#[test]
fn a_plain_record_looks_like_one_cell_at_the_drawn_pixel() {
let record = Record {
x: 12.75,
y: 30.5,
rx: -5,
ry: 20,
bx: 13,
by: 25,
sprite: 9,
..EMPTY
};
assert_eq!(
record.look(),
Some(Look {
sprite: SpriteId(9),
x: -5,
y: 20,
width: 8,
height: 8,
flip_x: false,
flip_y: false,
})
);
}
#[test]
fn span_and_flip_bits_size_and_mirror_the_look() {
let base = Record { sprite: 5, ..EMPTY };
let two_rows = Record { span: 0x10, ..base }.look().unwrap();
assert_eq!(
(two_rows.width, two_rows.height),
(8, 16),
"high nibble is cells down"
);
let two_cols = Record { span: 0x01, ..base }.look().unwrap();
assert_eq!(
(two_cols.width, two_cols.height),
(16, 8),
"low nibble is cells across"
);
let whole_sheet = Record { span: 0xff, ..base }.look().unwrap();
assert_eq!(
(whole_sheet.width, whole_sheet.height),
(128, 128),
"0xff spans the sheet"
);
let flipped = Record {
meta: FLIP_X | FLIP_Y,
..base
}
.look()
.unwrap();
assert!(flipped.flip_x && flipped.flip_y);
let still_plain = Record {
meta: PROP | CONFINED,
..base
}
.look()
.unwrap();
assert!(
!still_plain.flip_x && !still_plain.flip_y,
"PROP/CONFINED are not look bits"
);
}
#[test]
fn looks_keeps_order_and_skips_what_shows_nothing() {
let a = Record { sprite: 1, ..EMPTY };
let hidden_b = Record {
sprite: 2,
meta: HIDDEN,
..EMPTY
};
let c = Record { sprite: 3, ..EMPTY };
let records = [a, VACANT, hidden_b, c];
let shown: Vec<SpriteId> = looks(&records, BitFlags::empty(), |_| BitFlags::empty())
.map(|look| look.sprite)
.collect();
assert_eq!(shown, [SpriteId(1), SpriteId(3)]);
}
#[test]
fn layers_filter_by_what_the_worn_cell_carries() {
let plain = Record { sprite: 1, ..EMPTY }; let flagged = Record { sprite: 2, ..EMPTY }; let records = [plain, flagged];
let carried = |sprite: SpriteId| {
if sprite == SpriteId(2) {
BitFlags::from(SpriteFlag::Flag0)
} else {
BitFlags::empty()
}
};
let all: Vec<_> = looks(&records, BitFlags::empty(), carried)
.map(|look| look.sprite)
.collect();
assert_eq!(
all,
[SpriteId(1), SpriteId(2)],
"empty layers draws everything"
);
let picked: Vec<_> = looks(&records, BitFlags::from(SpriteFlag::Flag0), carried)
.map(|look| look.sprite)
.collect();
assert_eq!(
picked,
[SpriteId(2)],
"only the cell carrying the flag is picked"
);
let none: Vec<_> = looks(&records, BitFlags::from(SpriteFlag::Flag1), carried)
.map(|look| look.sprite)
.collect();
assert!(
none.is_empty(),
"a cell carrying no flags is never picked by a non-empty layer"
);
}
#[test]
fn a_recast_at_the_ends_of_the_coordinate_space_does_not_wrap() {
let mut record = EMPTY;
(record.x, record.y, record.rx, record.ry) = (-32768.0, 0.0, i16::MIN, 0);
(record.bx, record.by, record.bw, record.bh) = (i16::MAX - 8, 0, 8, 8);
let recast = Recast::of(&record);
assert_eq!(recast.bounds(), Bounds::new(i16::MAX - 8, 0, 8, 8));
}
#[test]
fn a_recast_stands_where_its_record_says_and_answers_as_it_answered() {
let mut record = EMPTY;
(record.x, record.y, record.rx, record.ry) = (20.5, 30.25, 20, 30);
(record.bx, record.by, record.bw, record.bh) = (22, 30, 4, 8);
record.sprite = 7;
record.solid = 0b1;
record.heeds = 0b11;
let mut recast = Recast::of(&record);
assert_eq!(recast.bounds(), Bounds::new(22, 30, 4, 8));
recast.body_mut().move_by(2.0, 0.0);
assert_eq!(recast.bounds(), Bounds::new(24, 30, 4, 8));
assert_eq!(recast.solid(), Some(BitFlags::from_bits(0b1).unwrap()));
assert!(!recast.prop());
assert_eq!(recast.confines(), None);
recast.report(&mut record);
assert_eq!((record.x, record.y), (22.5, 30.25));
assert_eq!((record.rx, record.ry), (22, 30));
}
#[test]
fn the_step_reads_nothing_of_the_look() {
let record = Record {
meta: FLIP_X | FLIP_Y | HIDDEN,
span: 0xff,
..EMPTY
};
let recast = Recast::of(&record);
assert!(!recast.prop(), "FLIP_X/FLIP_Y/HIDDEN are not PROP");
assert_eq!(
recast.confines(),
None,
"FLIP_X/FLIP_Y/HIDDEN are not CONFINED"
);
}
}