#![deny(clippy::print_stdout, clippy::print_stderr, clippy::float_arithmetic)]
mod entity;
mod store;
pub use entity::{Entities, Entity};
pub use store::Store;
pub fn join<'a, A, B>(
left: &'a Store<A>,
right: &'a Store<B>,
) -> impl Iterator<Item = (u32, &'a A, &'a B)> {
if walks_left(left, right) {
Join::FromLeft(
left.iter()
.filter_map(move |(slot, value)| Some((slot, value, right.get(slot)?))),
)
} else {
Join::FromRight(
right
.iter()
.filter_map(move |(slot, value)| Some((slot, left.get(slot)?, value))),
)
}
}
fn walks_left<A, B>(left: &Store<A>, right: &Store<B>) -> bool {
left.scan_len() <= right.scan_len()
}
enum Join<L, R> {
FromLeft(L),
FromRight(R),
}
impl<'a, A: 'a, B: 'a, L, R> Iterator for Join<L, R>
where
L: Iterator<Item = (u32, &'a A, &'a B)>,
R: Iterator<Item = (u32, &'a A, &'a B)>,
{
type Item = (u32, &'a A, &'a B);
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::FromLeft(iter) => iter.next(),
Self::FromRight(iter) => iter.next(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_join_yields_only_entities_in_both_stores() {
let mut names: Store<&str> = Store::new();
let mut scores: Store<u32> = Store::new();
names.insert(0, "zero");
names.insert(1, "one");
names.insert(2, "two");
scores.insert(1, 10);
scores.insert(2, 20);
scores.insert(9, 90);
let found: Vec<(u32, &str, u32)> = join(&names, &scores)
.map(|(slot, name, score)| (slot, *name, *score))
.collect();
assert_eq!(found, vec![(1, "one", 10), (2, "two", 20)]);
}
#[test]
fn a_join_with_nothing_in_common_is_empty() {
let mut left: Store<u8> = Store::new();
let mut right: Store<u8> = Store::new();
left.insert(0, 1);
right.insert(1, 2);
assert_eq!(join(&left, &right).count(), 0);
}
#[test]
fn a_join_is_in_slot_order_after_churn() {
let mut left: Store<u32> = Store::new();
let mut right: Store<u32> = Store::new();
for slot in [7u32, 2, 5, 1, 9] {
left.insert(slot, slot);
right.insert(slot, slot);
}
left.remove(5);
right.remove(9);
left.insert(3, 3);
right.insert(3, 3);
let slots: Vec<u32> = join(&left, &right).map(|(slot, _, _)| slot).collect();
assert_eq!(slots, vec![1, 2, 3, 7]);
}
#[test]
fn a_join_yields_the_same_sequence_from_either_side() {
let mut wide: Store<u32> = Store::new();
let mut narrow: Store<u32> = Store::new();
for slot in [0u32, 3, 40_000] {
wide.insert(slot, slot);
}
for slot in [0u32, 3] {
narrow.insert(slot, slot * 10);
}
assert!(narrow.scan_len() < wide.scan_len());
assert!(!walks_left(&wide, &narrow));
assert!(walks_left(&narrow, &wide));
let forward: Vec<(u32, u32, u32)> = join(&wide, &narrow)
.map(|(slot, a, b)| (slot, *a, *b))
.collect();
let backward: Vec<(u32, u32, u32)> = join(&narrow, &wide)
.map(|(slot, a, b)| (slot, *b, *a))
.collect();
assert_eq!(forward, vec![(0, 0, 0), (3, 3, 30)]);
assert_eq!(forward, backward);
}
#[test]
fn scan_cost_follows_slot_span_not_component_count() {
let mut few_but_scattered: Store<u32> = Store::new();
few_but_scattered.insert(0, 0);
few_but_scattered.insert(50_000, 1);
let mut many_but_packed: Store<u32> = Store::new();
for slot in 0..1_000u32 {
many_but_packed.insert(slot, slot);
}
assert!(few_but_scattered.len() < many_but_packed.len());
assert!(few_but_scattered.scan_len() > many_but_packed.scan_len());
}
#[test]
fn a_despawned_entity_can_be_filtered_out_of_a_query() {
let mut entities = Entities::new();
let mut store: Store<u32> = Store::new();
let keep = entities.spawn();
let drop = entities.spawn();
store.insert(keep.index(), 1);
store.insert(drop.index(), 2);
entities.despawn(drop);
assert_eq!(store.len(), 2);
let live: Vec<u32> = entities
.iter()
.filter_map(|entity| store.get(entity.index()).map(|_| entity.index()))
.collect();
assert_eq!(live, vec![keep.index()]);
}
}