moirai-for-games 0.1.0

A small deterministic no_std ECS for constrained and headless games
Documentation
use core::any::TypeId;
use core::mem::{align_of, size_of};

use super::*;
use crate::component::{ComponentOptions, StorageKind};
use crate::world::WorldOwner;
use rstest::rstest;

struct Marker;
struct Position {
    _x: i32,
    _y: i32,
}
struct DroppingTag(#[allow(dead_code)] String);

#[rstest]
fn first_registration_succeeds() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    let id = registry
        .register_typed::<Position>(&owner, None, ComponentOptions::sparse())
        .expect("first registration");
    assert_eq!(id.index(), 0);
}

#[rstest]
fn find_conflict_exact_repeat_returns_none() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_typed::<Position>(&owner, Some("pos"), ComponentOptions::sparse())
        .expect("first");
    assert!(!registry.find_conflict_for_test(
        Some(TypeId::of::<Position>()),
        "pos",
        ComponentOptions::sparse(),
        size_of::<Position>(),
        align_of::<Position>(),
    ));
}

#[rstest]
fn exact_repeat_is_idempotent() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    let first = registry
        .register_typed::<Position>(&owner, None, ComponentOptions::sparse())
        .expect("first");
    let second = registry
        .register_typed::<Position>(&owner, None, ComponentOptions::sparse())
        .expect("repeat");
    assert_eq!(first, second);
    assert_eq!(registry.len(), 1);
}

#[rstest]
fn conflicting_options_fail_without_mutation() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_typed::<Position>(&owner, None, ComponentOptions::sparse())
        .expect("first");
    let err = registry
        .register_typed::<Position>(&owner, None, ComponentOptions::table())
        .expect_err("conflict");
    assert!(matches!(err, RegistrationError::TypeConflict { .. }));
    assert_eq!(registry.len(), 1);
}

#[rstest]
fn same_name_different_type_fails() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_typed::<Position>(&owner, Some("shared"), ComponentOptions::sparse())
        .expect("first");
    let err = registry
        .register_typed::<Marker>(&owner, Some("shared"), ComponentOptions::tag())
        .expect_err("name conflict");
    assert!(matches!(err, RegistrationError::NameConflict { .. }));
}

#[rstest]
fn typed_non_zst_tag_fails() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    let err = registry
        .register_typed::<Position>(&owner, None, ComponentOptions::tag())
        .expect_err("invalid tag");
    assert!(matches!(err, RegistrationError::InvalidTag { .. }));
}

#[rstest]
fn untyped_registration_requires_tag_options() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    let err = registry
        .register_untyped(&owner, "marker", ComponentOptions::sparse())
        .expect_err("untyped sparse");
    assert!(matches!(err, RegistrationError::LayoutConflict { .. }));
}

#[rstest]
fn dropping_tag_fails() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    let err = registry
        .register_typed::<DroppingTag>(&owner, None, ComponentOptions::tag())
        .expect_err("dropping tag");
    assert!(matches!(err, RegistrationError::InvalidTag { .. }));
}

#[rstest]
fn component_id_rejects_foreign_owner() {
    let owner_a = WorldOwner::new();
    let owner_b = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    let id = registry
        .register_typed::<Marker>(&owner_a, None, ComponentOptions::tag())
        .expect("register");
    assert!(matches!(
        id.validate_owner(&owner_b),
        Err(RegistrationError::LayoutConflict { .. })
    ));
}

#[rstest]
fn storage_kind_and_lookup_helpers() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    let sparse = registry
        .register_typed::<Position>(&owner, Some("pos"), ComponentOptions::sparse())
        .expect("sparse");
    let tag = registry
        .register_typed::<Marker>(&owner, Some("marker"), ComponentOptions::tag())
        .expect("tag");

    assert_eq!(registry.storage_kind(&sparse), Some(StorageKind::Sparse));
    assert_eq!(registry.storage_kind(&tag), Some(StorageKind::Sparse));
    assert_eq!(registry.is_tag(&tag), Some(true));
    assert!(registry.entry_is_tag(tag.index()));
    assert_eq!(
        registry.index_of_type(core::any::TypeId::of::<Position>()),
        Some(sparse.index())
    );
    assert_eq!(registry.id_of::<Position>(&owner), Some(sparse.clone()));
    assert_eq!(registry.component_name(&sparse), "pos");
}

#[rstest]
fn tag_table_storage_is_rejected() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    let err = registry
        .register_typed::<Marker>(&owner, None, ComponentOptions::test_tag_table())
        .expect_err("tag table");
    assert!(matches!(err, RegistrationError::UnsupportedStorage { .. }));
}

#[rstest]
fn untyped_tag_table_storage_is_rejected() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    let err = registry
        .register_untyped(&owner, "marker", ComponentOptions::test_tag_table())
        .expect_err("untyped tag table");
    assert!(matches!(err, RegistrationError::UnsupportedStorage { .. }));
}

#[rstest]
fn layout_conflict_reports_detail() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_typed::<Position>(&owner, Some("pos"), ComponentOptions::sparse())
        .expect("first");
    let err = registry
        .register_typed::<Position>(&owner, Some("other"), ComponentOptions::sparse())
        .expect_err("layout");
    assert!(matches!(err, RegistrationError::TypeConflict { .. }));
}

#[rstest]
fn layout_conflict_reports_existing_and_requested_layout() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_typed::<Position>(&owner, Some("pos"), ComponentOptions::sparse())
        .expect("first");
    let err = registry
        .register_inner(
            &owner,
            "pos-alias".into(),
            Some(TypeId::of::<Position>()),
            ComponentOptions::sparse(),
            size_of::<Position>() + 1,
            align_of::<Position>(),
        )
        .expect_err("layout");
    assert!(matches!(err, RegistrationError::LayoutConflict { .. }));
}

#[rstest]
fn type_conflict_detects_same_type_different_name() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_typed::<Position>(&owner, Some("first"), ComponentOptions::sparse())
        .expect("first");
    let err = registry
        .register_typed::<Position>(&owner, Some("second"), ComponentOptions::sparse())
        .expect_err("type");
    assert!(matches!(err, RegistrationError::TypeConflict { .. }));
}

#[rstest]
fn name_conflict_detects_same_name_different_type() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_typed::<Position>(&owner, Some("shared"), ComponentOptions::sparse())
        .expect("first");
    let err = registry
        .register_typed::<Marker>(&owner, Some("shared"), ComponentOptions::tag())
        .expect_err("name");
    assert!(matches!(err, RegistrationError::NameConflict { .. }));
}

#[rstest]
fn type_conflict_with_renamed_repeat() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_typed::<Position>(&owner, Some("a"), ComponentOptions::sparse())
        .expect("first");
    let err = registry
        .register_typed::<Position>(&owner, Some("b"), ComponentOptions::sparse())
        .expect_err("rename");
    assert!(matches!(err, RegistrationError::TypeConflict { .. }));
}

#[rstest]
fn name_conflict_detects_matching_name_with_different_tag_kind() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_typed::<Position>(&owner, Some("shared"), ComponentOptions::sparse())
        .expect("sparse");
    let err = registry
        .register_inner(
            &owner,
            "shared".into(),
            Some(TypeId::of::<Marker>()),
            ComponentOptions::tag(),
            0,
            1,
        )
        .expect_err("tag");
    assert!(matches!(err, RegistrationError::NameConflict { .. }));
}

#[rstest]
fn type_conflict_same_type_different_storage_kind() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_typed::<Position>(&owner, Some("velocity"), ComponentOptions::sparse())
        .expect("sparse");
    let err = registry
        .register_inner(
            &owner,
            "velocity".into(),
            Some(TypeId::of::<Position>()),
            ComponentOptions::table(),
            size_of::<Position>(),
            align_of::<Position>(),
        )
        .expect_err("storage");
    assert!(matches!(err, RegistrationError::TypeConflict { .. }));
}

#[rstest]
fn name_conflict_untyped_same_tag_kind_different_size() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_inner(&owner, "shared".into(), None, ComponentOptions::tag(), 0, 1)
        .expect("tag");
    let err = registry
        .register_inner(&owner, "shared".into(), None, ComponentOptions::tag(), 8, 8)
        .expect_err("size");
    assert!(matches!(err, RegistrationError::NameConflict { .. }));
}

#[rstest]
fn name_conflict_untyped_same_tag_kind_different_align() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_inner(&owner, "shared".into(), None, ComponentOptions::tag(), 0, 1)
        .expect("tag");
    let err = registry
        .register_inner(&owner, "shared".into(), None, ComponentOptions::tag(), 0, 4)
        .expect_err("align");
    assert!(matches!(err, RegistrationError::NameConflict { .. }));
}

#[rstest]
fn name_conflict_untyped_sparse_and_tag_share_name() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_inner(&owner, "shared".into(), None, ComponentOptions::tag(), 0, 1)
        .expect("tag");
    let err = registry
        .register_inner(
            &owner,
            "shared".into(),
            None,
            ComponentOptions::sparse(),
            0,
            1,
        )
        .expect_err("sparse");
    assert!(matches!(err, RegistrationError::NameConflict { .. }));
}

#[rstest]
fn name_conflict_same_name_different_storage_on_name_branch() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_typed::<Position>(&owner, Some("shared"), ComponentOptions::sparse())
        .expect("sparse");
    let err = registry
        .register_inner(
            &owner,
            "shared".into(),
            Some(TypeId::of::<Marker>()),
            ComponentOptions::table(),
            size_of::<Marker>(),
            align_of::<Marker>(),
        )
        .expect_err("storage");
    assert!(matches!(err, RegistrationError::NameConflict { .. }));
}

#[rstest]
fn name_conflict_detects_matching_name_with_different_layout() {
    let owner = WorldOwner::new();
    let mut registry = ComponentRegistry::new();
    registry
        .register_typed::<Position>(&owner, Some("shared"), ComponentOptions::sparse())
        .expect("sparse");
    let err = registry
        .register_inner(
            &owner,
            "shared".into(),
            Some(TypeId::of::<Marker>()),
            ComponentOptions::sparse(),
            size_of::<Marker>(),
            align_of::<Marker>(),
        )
        .expect_err("layout");
    assert!(matches!(err, RegistrationError::NameConflict { .. }));
}