#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "TestUtils.h" using namespace test; namespace { class TestComponent final : public Component { public: explicit TestComponent(GameObject& owner) : Component(owner, "TestComponent") {} void Update(float) override {} std::string GetTypeName() const override { return "TestComponent"; } }; class RemovingListener final : public EventListener { public: explicit RemovingListener(std::unique_ptr* victim) : victim(victim) {} void OnEvent(int) { if (victim != nullptr) { victim->reset(); victim = nullptr; } ++calls; } int calls{0}; private: std::unique_ptr* victim; }; void testTransforms() { GameObject parent{"parent"}; GameObject child{"child"}; parent.GetTransform().SetLocalPosition({10.f, 2.f, -4.f}); parent.GetTransform().SetLocalRotation({0.f, 0.f, 90.f}); parent.GetTransform().SetLocalScale({2.f, 2.f, 2.f}); child.GetTransform().SetLocalPosition({1.f, 0.f, 0.f}); child.GetTransform().SetParent(&parent.GetTransform(), true); const auto worldPosition = child.GetTransform().GetWorldPosition(); check(close(worldPosition.x, 1.f) && close(worldPosition.y, 0.f), "reparenting must preserve world position"); child.GetTransform().SetWorldPosition({20.f, 10.f, 0.f}); const auto movedPosition = child.GetTransform().GetWorldPosition(); check(close(movedPosition.x, 20.f) && close(movedPosition.y, 10.f), "world position must account for parent rotation and scale"); parent.Destroy(); check(parent.IsBeingDestroyed() && child.IsBeingDestroyed(), "destroying a parent must mark descendants"); } void testSceneFindGameObject() { Scene& scene = SceneManager::GetInstance().CreateScene("find-object"); GameObject* pending = scene.CreateGameObject("pending"); check(scene.FindGameObject(pending->GetId()) == pending, "objects must be found while still pending"); scene.CommitPendingAdditions(); GameObject* committed = scene.CreateGameObject("committed"); scene.CommitPendingAdditions(); check(scene.FindGameObject(committed->GetId()) == committed, "committed objects must be found by id"); check(scene.FindGameObject(InvalidObjectId) == nullptr, "the invalid id must never match"); const ObjectId destroyedId = committed->GetId(); committed->Destroy(); check(scene.FindGameObject(destroyedId) == nullptr, "objects being destroyed must not be found"); SceneManager::GetInstance().Destroy(); } void testEventMutation() { Event event; int calls = 0; event.AddListener([&](int) { ++calls; }); event.AddListener([&](int) { ++calls; event.AddListener([&](int) { ++calls; }); }); event.Invoke(1); check(calls == 2, "event mutation must not invalidate the current invocation"); event.Invoke(1); check(calls == 5, "new event listeners must run on subsequent invocations"); } void testEventRemoval() { Event event; std::unique_ptr victim; RemovingListener remover{&victim}; victim = std::make_unique(nullptr); event.AddListener(&remover, &RemovingListener::OnEvent); event.AddListener(victim.get(), &RemovingListener::OnEvent); event.Invoke(1); check(remover.calls == 1, "event remover must run once"); check(victim == nullptr, "event listener must be removable during invocation"); } void testComponentRemoval() { Scene& scene = SceneManager::GetInstance().CreateScene("component-removal"); GameObject* object = scene.CreateGameObject("object"); object->AddComponent(); scene.CommitPendingAdditions(); check(object->GetComponent() != nullptr, "test component must be discoverable before removal"); check(object->DestroyComponent() != nullptr, "destroying a component must find it"); scene.CleanupDestroyedGameObjects(); check(object->GetComponent() == nullptr, "destroyed components must be removed at the scene boundary"); SceneManager::GetInstance().Destroy(); } void testEngineComponentRegistration() { RegisterEngineComponents(); const char* canonicalNames[] = { "MeshRendererComponent", "LightComponent", "Rotator", "Spinner", "OrbitAndSpin", "Animator", "Rigidbody", "BoxCollider", "SphereCollider", "CapsuleCollider", "Canvas", "RectTransform", "UIImage", "UIText" }; for (const char* name : canonicalNames) { check(ComponentFactory::IsRegistered(name), "all canonical engine component names must be registered"); } check(!ComponentFactory::IsRegistered("MeshRenderer"), "MeshRenderer legacy alias must not be registered"); check(!ComponentFactory::IsRegistered("RigidBody"), "RigidBody legacy alias must not be registered"); } void testSceneCurrentCamera() { Scene& scene = SceneManager::GetInstance().CreateScene("cameras"); check(scene.GetCurrentCamera() == nullptr, "a scene without cameras must have no current camera"); GameObject* first = scene.CreateGameObject("first camera"); auto* firstCamera = first->AddComponent(); scene.CommitPendingAdditions(); check(scene.GetCurrentCamera() == firstCamera, "the only camera in a scene must be the current camera"); GameObject* second = scene.CreateGameObject("second camera"); auto* secondCamera = second->AddComponent(); secondCamera->SetFieldOfView(60.0f); second->GetTransform().SetWorldPosition({1.0f, 2.0f, 3.0f}); scene.CommitPendingAdditions(); secondCamera->MakeCurrent(); check(scene.GetCurrentCamera() == secondCamera && !firstCamera->IsCurrent(), "the selected camera must be the current camera"); const Camera& camera = secondCamera->GetCamera(16.0f / 9.0f); check(camera.GetPosition() == glm::vec3{1.0f, 2.0f, 3.0f} && close(camera.GetForward().z, -1.0f) && camera.GetFOVY() == 60.0f, "a camera component must place its camera at its transform, looking down -Z"); const auto path = std::filesystem::temp_directory_path() / "destrum_camera_scene_test.json"; check(SceneSerializer::Save(scene, path), "scene with cameras must save"); check(SceneSerializer::Load(scene, path), "scene with cameras must load"); std::filesystem::remove(path); const CameraComponent* loadedCamera = scene.GetCurrentCamera(); check(loadedCamera != nullptr && loadedCamera->GetGameObject()->GetName() == "second camera" && loadedCamera->GetFieldOfView() == 60.0f, "scene round-trip must preserve the current camera"); scene.GetCurrentCamera()->GetGameObject()->SetActive(false); check(scene.GetCurrentCamera() != nullptr && scene.GetCurrentCamera()->GetGameObject()->GetName() == "first camera", "an inactive current camera must fall back to another camera"); SceneManager::GetInstance().Destroy(); } void testHierarchyMoves() { Scene& scene = SceneManager::GetInstance().CreateScene("hierarchy"); GameObject* parent = scene.CreateGameObject("parent"); GameObject* a = scene.CreateGameObject("a"); GameObject* b = scene.CreateGameObject("b"); GameObject* c = scene.CreateGameObject("c"); GameObject* other = scene.CreateGameObject("other"); for (GameObject* child : {a, b, c}) { child->GetTransform().SetParent(&parent->GetTransform(), false); } scene.CommitPendingAdditions(); const auto childNames = [](const GameObject& object) { std::string names; for (const Transform* child : object.GetTransform().GetChildren()) { names += child->GetOwner()->GetName(); } return names; }; check(scene.MoveInHierarchy(*c, parent, a) && childNames(*parent) == "cab", "moving before a sibling must reorder the children"); check(scene.MoveInHierarchy(*c, parent) && childNames(*parent) == "abc", "moving without a sibling must put the object last"); check(scene.MoveInHierarchy(*a, other) && childNames(*parent) == "bc" && childNames(*other) == "a", "moving onto another object must reparent it"); check(!scene.MoveInHierarchy(*other, a) && a->GetTransform().GetParent() == &other->GetTransform(), "an object must not be moved under its own child"); check(scene.MoveInHierarchy(*a, nullptr, parent) && a->GetTransform().GetParent() == nullptr && scene.GetObjects().front().get() == a, "moving to the root must unparent and order the roots"); check(scene.MoveInHierarchy(*c, parent, b) && childNames(*parent) == "cb", "reordering must still work"); const auto path = std::filesystem::temp_directory_path() / "destrum_hierarchy_scene.json"; check(SceneSerializer::Save(scene, path), "the hierarchy scene must save"); Scene& copy = SceneManager::GetInstance().CreateScene("hierarchy copy"); check(SceneSerializer::Load(copy, path), "the hierarchy scene must load"); std::filesystem::remove(path); copy.CommitPendingAdditions(); const GameObject* loadedParent = findByName(copy, "parent"); check(loadedParent != nullptr && childNames(*loadedParent) == "cb", "sibling order must survive a save"); check(copy.GetObjects().front()->GetName() == "a", "root order must survive a save"); SceneManager::GetInstance().Destroy(); } } void RunCoreTests() { testTransforms(); testSceneFindGameObject(); testEventMutation(); testEventRemoval(); testComponentRemoval(); testEngineComponentRegistration(); testSceneCurrentCamera(); testHierarchyMoves(); }