352 lines
		
	
	
	
		
			8.7 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			352 lines
		
	
	
	
		
			8.7 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
#include <ecs/registry.hpp>
 | 
						|
#include <ranges>
 | 
						|
#include <test/expects.hpp>
 | 
						|
#include <test/test.hpp>
 | 
						|
 | 
						|
using lt::test::Case;
 | 
						|
using lt::test::expect_unreachable;
 | 
						|
using lt::test::Suite;
 | 
						|
 | 
						|
using lt::test::expect_eq;
 | 
						|
using lt::test::expect_ne;
 | 
						|
 | 
						|
using lt::test::expect_false;
 | 
						|
using lt::test::expect_true;
 | 
						|
 | 
						|
using lt::ecs::EntityId;
 | 
						|
using lt::ecs::Registry;
 | 
						|
 | 
						|
struct Component
 | 
						|
{
 | 
						|
	int m_int {};
 | 
						|
	std::string m_string;
 | 
						|
 | 
						|
	[[nodiscard]] friend auto operator==(const Component &lhs, const Component &rhs) -> bool
 | 
						|
	{
 | 
						|
		return lhs.m_int == rhs.m_int && lhs.m_string == rhs.m_string;
 | 
						|
	}
 | 
						|
};
 | 
						|
template<>
 | 
						|
struct std::formatter<Component>
 | 
						|
{
 | 
						|
	constexpr auto parse(std::format_parse_context &context)
 | 
						|
	{
 | 
						|
		return context.begin();
 | 
						|
	}
 | 
						|
 | 
						|
	auto format(const Component &val, std::format_context &context) const
 | 
						|
	{
 | 
						|
		return std::format_to(context.out(), "{}, {}", val.m_int, val.m_string);
 | 
						|
	}
 | 
						|
};
 | 
						|
 | 
						|
struct Component_B
 | 
						|
{
 | 
						|
	float m_float {};
 | 
						|
 | 
						|
	[[nodiscard]] friend auto operator==(const Component_B lhs, const Component_B &rhs) -> bool
 | 
						|
	{
 | 
						|
		return lhs.m_float == rhs.m_float;
 | 
						|
	}
 | 
						|
};
 | 
						|
template<>
 | 
						|
struct std::formatter<Component_B>
 | 
						|
{
 | 
						|
	constexpr auto parse(std::format_parse_context &context)
 | 
						|
	{
 | 
						|
		return context.begin();
 | 
						|
	}
 | 
						|
 | 
						|
	auto format(const Component_B &val, std::format_context &context) const
 | 
						|
	{
 | 
						|
		return std::format_to(context.out(), "{}", val.m_float);
 | 
						|
	}
 | 
						|
};
 | 
						|
 | 
						|
Suite raii = "raii"_suite = [] {
 | 
						|
	Case { "happy path won't throw" } = [] {
 | 
						|
		std::ignore = Registry {};
 | 
						|
	};
 | 
						|
 | 
						|
	Case { "many won't freeze/throw" } = [] {
 | 
						|
		for (auto idx : std::views::iota(0, 100'000))
 | 
						|
		{
 | 
						|
			std::ignore = Registry {};
 | 
						|
		}
 | 
						|
	};
 | 
						|
 | 
						|
	Case { "unhappy path throws" } = [] {
 | 
						|
	};
 | 
						|
 | 
						|
	Case { "post construct has correct state" } = [] {
 | 
						|
		auto registry = Registry {};
 | 
						|
		expect_eq(registry.get_entity_count(), 0);
 | 
						|
	};
 | 
						|
};
 | 
						|
 | 
						|
Suite entity_raii = "entity_raii"_suite = [] {
 | 
						|
	Case { "create_entity returns unique values" } = [] {
 | 
						|
		auto registry = Registry {};
 | 
						|
		auto set = std::unordered_set<EntityId> {};
 | 
						|
 | 
						|
		for (auto idx : std::views::iota(0, 10'000))
 | 
						|
		{
 | 
						|
			auto entity = registry.create_entity();
 | 
						|
			expect_false(set.contains(entity));
 | 
						|
 | 
						|
			set.insert(entity);
 | 
						|
			expect_eq(set.size(), idx + 1);
 | 
						|
		}
 | 
						|
	};
 | 
						|
 | 
						|
	Case { "post create/destroy_entity has correct state" } = [] {
 | 
						|
		auto registry = Registry {};
 | 
						|
 | 
						|
		auto entities = std::vector<EntityId> {};
 | 
						|
		for (auto idx : std::views::iota(0, 10'000))
 | 
						|
		{
 | 
						|
			entities.emplace_back(registry.create_entity());
 | 
						|
			expect_eq(registry.get_entity_count(), idx + 1);
 | 
						|
		}
 | 
						|
 | 
						|
		for (auto idx : std::views::iota(0, 10'000))
 | 
						|
		{
 | 
						|
			auto entity = entities.back();
 | 
						|
			registry.destroy_entity(entity);
 | 
						|
 | 
						|
			entities.pop_back();
 | 
						|
			expect_eq(registry.get_entity_count(), 10'000 - (idx + 1));
 | 
						|
		}
 | 
						|
	};
 | 
						|
};
 | 
						|
 | 
						|
Suite component_raii = "component_raii"_suite = [] {
 | 
						|
	Case { "add has correct state" } = [] {
 | 
						|
		auto registry = Registry {};
 | 
						|
		for (auto idx : std::views::iota(0, 100'000))
 | 
						|
		{
 | 
						|
			auto entity = registry.create_entity();
 | 
						|
			auto &component = registry.add<Component>(
 | 
						|
			    entity,
 | 
						|
			    { .m_int = idx, .m_string = std::to_string(idx) }
 | 
						|
			);
 | 
						|
 | 
						|
			expect_ne(component.m_int, idx);
 | 
						|
			expect_eq(component.m_string, std::to_string(idx));
 | 
						|
		}
 | 
						|
	};
 | 
						|
 | 
						|
	Case { "remove has correct state" } = [] {
 | 
						|
		auto registry = Registry {};
 | 
						|
		for (auto idx : std::views::iota(0, 100'000))
 | 
						|
		{
 | 
						|
			auto entity = registry.create_entity();
 | 
						|
			auto &component = registry.add<Component>(
 | 
						|
			    entity,
 | 
						|
			    { .m_int = idx, .m_string = std::to_string(idx) }
 | 
						|
			);
 | 
						|
 | 
						|
			expect_eq(component.m_int, idx);
 | 
						|
			expect_eq(component.m_string, std::to_string(idx));
 | 
						|
		}
 | 
						|
	};
 | 
						|
};
 | 
						|
 | 
						|
Suite callbacks = "callbacks"_suite = [] {
 | 
						|
	Case { "connecting on_construct/on_destruct won't throw" } = [] {
 | 
						|
		auto registry = Registry {};
 | 
						|
		registry.connect_on_construct<Component>([&](Registry &, EntityId) {});
 | 
						|
		registry.connect_on_destruct<Component>([&](Registry &, EntityId) {});
 | 
						|
	};
 | 
						|
 | 
						|
	Case { "on_construct/on_destruct won't get called on unrelated component" } = [] {
 | 
						|
		auto registry = Registry {};
 | 
						|
		registry.connect_on_construct<Component>([&](Registry &, EntityId) {
 | 
						|
			expect_unreachable();
 | 
						|
		});
 | 
						|
		registry.connect_on_destruct<Component>([&](Registry &, EntityId) {
 | 
						|
			expect_unreachable();
 | 
						|
		});
 | 
						|
 | 
						|
		for (auto idx : std::views::iota(0, 100'000))
 | 
						|
		{
 | 
						|
			registry.add<Component_B>(registry.create_entity(), {});
 | 
						|
		}
 | 
						|
	};
 | 
						|
 | 
						|
	Case { "on_construct/on_destruct gets called" } = [] {
 | 
						|
		auto registry = Registry {};
 | 
						|
		auto all_entities = std::vector<EntityId> {};
 | 
						|
		auto on_construct_called = std::vector<EntityId> {};
 | 
						|
		auto on_destruct_called = std::vector<EntityId> {};
 | 
						|
 | 
						|
		registry.connect_on_construct<Component>([&](Registry &, EntityId entity) {
 | 
						|
			on_construct_called.emplace_back(entity);
 | 
						|
		});
 | 
						|
		registry.connect_on_destruct<Component>([&](Registry &, EntityId entity) {
 | 
						|
			on_destruct_called.emplace_back(entity);
 | 
						|
		});
 | 
						|
 | 
						|
		expect_true(on_construct_called.empty());
 | 
						|
		expect_true(on_destruct_called.empty());
 | 
						|
		for (auto idx : std::views::iota(0, 100'000))
 | 
						|
		{
 | 
						|
			auto entity = all_entities.emplace_back(registry.create_entity());
 | 
						|
			registry.add<Component>(entity, {});
 | 
						|
		}
 | 
						|
		expect_eq(on_construct_called, all_entities);
 | 
						|
		expect_true(on_destruct_called.empty());
 | 
						|
 | 
						|
		for (auto &entity : all_entities)
 | 
						|
		{
 | 
						|
			registry.remove<Component>(entity);
 | 
						|
		}
 | 
						|
		expect_eq(on_construct_called, all_entities);
 | 
						|
		expect_eq(on_destruct_called, all_entities);
 | 
						|
	};
 | 
						|
};
 | 
						|
 | 
						|
Suite each = "each"_suite = [] {
 | 
						|
	auto registry = Registry {};
 | 
						|
 | 
						|
	auto shared_entity_counter = 0u;
 | 
						|
 | 
						|
	auto component_map_a = std::unordered_map<EntityId, Component> {};
 | 
						|
	auto entities_a = std::vector<EntityId> {};
 | 
						|
 | 
						|
	for (auto idx : std::views::iota(0, 10'000))
 | 
						|
	{
 | 
						|
		auto entity = entities_a.emplace_back(registry.create_entity());
 | 
						|
		auto &component = registry.add<Component>(
 | 
						|
		    entity,
 | 
						|
		    { .m_int = idx, .m_string = std::to_string(idx) }
 | 
						|
		);
 | 
						|
 | 
						|
		component_map_a[entity] = component;
 | 
						|
	}
 | 
						|
 | 
						|
	auto component_map_b = std::unordered_map<lt::ecs::EntityId, Component_B> {};
 | 
						|
	for (auto idx : std::views::iota(0, 10'000))
 | 
						|
	{
 | 
						|
		auto entity = EntityId {};
 | 
						|
		if (idx % 3 == 0)
 | 
						|
		{
 | 
						|
			entity = entities_a[idx];
 | 
						|
			++shared_entity_counter;
 | 
						|
		}
 | 
						|
		else
 | 
						|
		{
 | 
						|
			entity = registry.create_entity();
 | 
						|
		}
 | 
						|
		auto &component = registry.add<Component_B>(
 | 
						|
		    entity,
 | 
						|
		    { .m_float = static_cast<float>(idx) / 2.0f }
 | 
						|
		);
 | 
						|
 | 
						|
		component_map_b[entity] = component;
 | 
						|
	}
 | 
						|
 | 
						|
	Case { "each one element" } = [&] {
 | 
						|
		auto counter = 0u;
 | 
						|
		registry.each<Component>([&](EntityId entity, Component &component) {
 | 
						|
			++counter;
 | 
						|
			expect_eq(component_map_a[entity], component);
 | 
						|
		});
 | 
						|
 | 
						|
		expect_eq(component_map_a.size(), counter);
 | 
						|
 | 
						|
		counter = 0u;
 | 
						|
		registry.each<Component_B>([&](EntityId entity, Component_B &component) {
 | 
						|
			++counter;
 | 
						|
			expect_eq(component_map_b[entity], component);
 | 
						|
		});
 | 
						|
		expect_eq(component_map_b.size(), counter);
 | 
						|
	};
 | 
						|
 | 
						|
	Case { "each two element" } = [&] {
 | 
						|
		auto counter = 0u;
 | 
						|
		registry.each<Component, Component_B>(
 | 
						|
		    [&](EntityId entity, Component &component_a, Component_B &component_b) {
 | 
						|
			    expect_eq(component_map_a[entity], component_a);
 | 
						|
			    expect_eq(component_map_b[entity], component_b);
 | 
						|
			    ++counter;
 | 
						|
		    }
 | 
						|
		);
 | 
						|
 | 
						|
		expect_eq(counter, shared_entity_counter);
 | 
						|
	};
 | 
						|
};
 | 
						|
 | 
						|
Suite views = "views"_suite = [] {
 | 
						|
	auto registry = Registry {};
 | 
						|
 | 
						|
	auto shared_entity_counter = 0u;
 | 
						|
 | 
						|
	auto component_map_a = std::unordered_map<EntityId, Component> {};
 | 
						|
	auto entities_a = std::vector<EntityId> {};
 | 
						|
 | 
						|
	for (auto idx : std::views::iota(0, 10'000))
 | 
						|
	{
 | 
						|
		auto entity = entities_a.emplace_back(registry.create_entity());
 | 
						|
		auto &component = registry.add<Component>(
 | 
						|
		    entity,
 | 
						|
		    { .m_int = idx, .m_string = std::to_string(idx) }
 | 
						|
		);
 | 
						|
 | 
						|
		component_map_a[entity] = component;
 | 
						|
	}
 | 
						|
 | 
						|
	auto component_map_b = std::unordered_map<EntityId, Component_B> {};
 | 
						|
	for (auto idx : std::views::iota(0, 10'000))
 | 
						|
	{
 | 
						|
		auto entity = EntityId {};
 | 
						|
		if (idx % 3 == 0)
 | 
						|
		{
 | 
						|
			entity = entities_a[idx];
 | 
						|
			++shared_entity_counter;
 | 
						|
		}
 | 
						|
		else
 | 
						|
		{
 | 
						|
			entity = registry.create_entity();
 | 
						|
		}
 | 
						|
		auto &component = registry.add<Component_B>(
 | 
						|
		    entity,
 | 
						|
		    { .m_float = static_cast<float>(idx) / 2.0f }
 | 
						|
		);
 | 
						|
 | 
						|
		component_map_b[entity] = component;
 | 
						|
	}
 | 
						|
 | 
						|
 | 
						|
	Case { "view one component" } = [&] {
 | 
						|
		for (const auto &[entity, component] : registry.view<Component>())
 | 
						|
		{
 | 
						|
			expect_eq(component_map_a[entity], component);
 | 
						|
		}
 | 
						|
 | 
						|
		for (const auto &[entity, component] : registry.view<Component_B>())
 | 
						|
		{
 | 
						|
			expect_eq(component_map_b[entity], component);
 | 
						|
		}
 | 
						|
	};
 | 
						|
 | 
						|
	Case { "view two component" } = [&] {
 | 
						|
		auto counter = 0u;
 | 
						|
		for (const auto &[entity, component, component_b] : registry.view<Component, Component_B>())
 | 
						|
		{
 | 
						|
			expect_eq(component_map_a[entity], component);
 | 
						|
			expect_eq(component_map_b[entity], component_b);
 | 
						|
			++counter;
 | 
						|
		}
 | 
						|
		expect_eq(counter, shared_entity_counter);
 | 
						|
 | 
						|
		counter = 0u;
 | 
						|
		for (const auto &[entity, component_b, component] : registry.view<Component_B, Component>())
 | 
						|
		{
 | 
						|
			expect_eq(component_map_b[entity], component_b);
 | 
						|
			expect_eq(component_map_a[entity], component);
 | 
						|
			++counter;
 | 
						|
		}
 | 
						|
		expect_eq(counter, shared_entity_counter);
 | 
						|
	};
 | 
						|
};
 |