#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif #include #endif using namespace XCEngine::Components; using namespace XCEngine::Math; namespace { std::filesystem::path GetRepositoryRoot() { return std::filesystem::path(__FILE__).parent_path().parent_path().parent_path(); } bool PumpAsyncLoadsUntilIdle(XCEngine::Resources::ResourceManager& manager, std::chrono::milliseconds timeout = std::chrono::milliseconds(4000)) { const auto deadline = std::chrono::steady_clock::now() + timeout; while (manager.IsAsyncLoading() && std::chrono::steady_clock::now() < deadline) { manager.UpdateAsyncLoads(); std::this_thread::sleep_for(std::chrono::milliseconds(5)); } manager.UpdateAsyncLoads(); return !manager.IsAsyncLoading(); } std::vector FindGameObjectsByMeshPath(Scene& scene, const std::string& meshPath) { std::vector matches; const std::vector meshFilters = scene.FindObjectsOfType(); for (MeshFilterComponent* meshFilter : meshFilters) { if (meshFilter == nullptr || meshFilter->GetGameObject() == nullptr) { continue; } if (meshFilter->GetMeshPath() == meshPath) { matches.push_back(meshFilter->GetGameObject()); } } std::sort(matches.begin(), matches.end(), [](const GameObject* lhs, const GameObject* rhs) { return lhs->GetID() < rhs->GetID(); }); return matches; } class TestComponent : public Component { public: TestComponent() = default; explicit TestComponent(const std::string& name) : m_customName(name) {} std::string GetName() const override { return m_customName.empty() ? "TestComponent" : m_customName; } bool m_awakeCalled = false; bool m_updateCalled = false; bool m_onDestroyCalled = false; int m_startCount = 0; int m_updateCount = 0; int m_onDestroyCount = 0; int* m_externalOnDestroyCount = nullptr; void Awake() override { m_awakeCalled = true; } void Start() override { ++m_startCount; } void Update(float deltaTime) override { m_updateCalled = true; ++m_updateCount; } void OnDestroy() override { m_onDestroyCalled = true; ++m_onDestroyCount; if (m_externalOnDestroyCount) { ++(*m_externalOnDestroyCount); } } private: std::string m_customName; }; class SceneTest : public ::testing::Test { protected: void SetUp() override { testScene = std::make_unique("TestScene"); } std::filesystem::path GetTempScenePath(const char* fileName) const { return std::filesystem::temp_directory_path() / fileName; } std::unique_ptr testScene; }; TEST(Scene_Test, DefaultConstructor_DefaultName) { Scene s; EXPECT_EQ(s.GetName(), "Untitled"); } TEST(Scene_Test, NamedConstructor) { Scene s("MyScene"); EXPECT_EQ(s.GetName(), "MyScene"); } TEST_F(SceneTest, CreateGameObject_Simple) { GameObject* go = testScene->CreateGameObject("TestObject"); EXPECT_NE(go, nullptr); EXPECT_EQ(go->GetName(), "TestObject"); } TEST_F(SceneTest, CreateGameObject_WithParent) { GameObject* parent = testScene->CreateGameObject("Parent"); GameObject* child = testScene->CreateGameObject("Child", parent); EXPECT_NE(child, nullptr); EXPECT_EQ(child->GetParent(), parent); } TEST_F(SceneTest, CreateGameObject_AssignsID) { GameObject* go1 = testScene->CreateGameObject("Object1"); GameObject* go2 = testScene->CreateGameObject("Object2"); EXPECT_NE(go1->GetID(), GameObject::INVALID_ID); EXPECT_NE(go2->GetID(), GameObject::INVALID_ID); EXPECT_NE(go1->GetID(), go2->GetID()); } TEST_F(SceneTest, CreateGameObject_AddsToRoot) { GameObject* go = testScene->CreateGameObject("RootObject"); auto roots = testScene->GetRootGameObjects(); EXPECT_EQ(roots.size(), 1u); EXPECT_EQ(roots[0], go); } TEST_F(SceneTest, DestroyGameObject_Simple) { GameObject* go = testScene->CreateGameObject("TestObject"); testScene->DestroyGameObject(go); EXPECT_EQ(testScene->Find("TestObject"), nullptr); } TEST_F(SceneTest, DestroyGameObject_WithChildren) { GameObject* parent = testScene->CreateGameObject("Parent"); testScene->CreateGameObject("Child", parent); testScene->DestroyGameObject(parent); EXPECT_EQ(testScene->Find("Parent"), nullptr); EXPECT_EQ(testScene->Find("Child"), nullptr); } TEST_F(SceneTest, DestroyGameObject_CallsOnDestroyOnce) { GameObject* go = testScene->CreateGameObject("DestroyMe"); TestComponent* comp = go->AddComponent(); int destroyCount = 0; comp->m_externalOnDestroyCount = &destroyCount; testScene->DestroyGameObject(go); EXPECT_EQ(destroyCount, 1); } TEST_F(SceneTest, Find_Exists) { testScene->CreateGameObject("FindMe"); GameObject* found = testScene->Find("FindMe"); EXPECT_NE(found, nullptr); EXPECT_EQ(found->GetName(), "FindMe"); } TEST_F(SceneTest, Find_NotExists) { GameObject* found = testScene->Find("NonExistent"); EXPECT_EQ(found, nullptr); } TEST_F(SceneTest, GetRootGameObjects_ReturnsTopLevel) { GameObject* parent = testScene->CreateGameObject("Parent"); testScene->CreateGameObject("Child", parent); auto roots = testScene->GetRootGameObjects(); EXPECT_EQ(roots.size(), 1u); EXPECT_EQ(roots[0], parent); } TEST(Scene_Test, GetRootGameObjects_EmptyScene) { Scene emptyScene; auto roots = emptyScene.GetRootGameObjects(); EXPECT_EQ(roots.size(), 0u); } TEST_F(SceneTest, Update_UpdatesActiveObjects) { GameObject* go = testScene->CreateGameObject("TestObject"); TestComponent* comp = go->AddComponent(); testScene->Update(0.016f); EXPECT_TRUE(comp->m_updateCalled); } TEST_F(SceneTest, Update_SkipsInactiveObjects) { GameObject* go = testScene->CreateGameObject("InactiveObject"); TestComponent* comp = go->AddComponent(); go->SetActive(false); testScene->Update(0.016f); EXPECT_FALSE(comp->m_updateCalled); } TEST_F(SceneTest, Update_RecursivelyUpdatesChildObjects) { GameObject* parent = testScene->CreateGameObject("Parent"); GameObject* child = testScene->CreateGameObject("Child", parent); TestComponent* comp = child->AddComponent(); testScene->Update(0.016f); EXPECT_TRUE(comp->m_updateCalled); EXPECT_EQ(comp->m_updateCount, 1); } TEST_F(SceneTest, Update_CallsStartOnlyOnceBeforeUpdate) { GameObject* go = testScene->CreateGameObject("TestObject"); TestComponent* comp = go->AddComponent(); testScene->Update(0.016f); testScene->Update(0.016f); EXPECT_EQ(comp->m_startCount, 1); EXPECT_EQ(comp->m_updateCount, 2); } TEST(Scene_Test, IsActive_DefaultTrue) { Scene s; EXPECT_TRUE(s.IsActive()); } TEST(Scene_Test, SetActive) { Scene s; s.SetActive(false); EXPECT_FALSE(s.IsActive()); } TEST_F(SceneTest, FindObjectOfType) { GameObject* go = testScene->CreateGameObject("TestObject"); go->AddComponent(); TestComponent* found = testScene->FindObjectOfType(); EXPECT_NE(found, nullptr); } TEST_F(SceneTest, FindObjectsOfType) { GameObject* go1 = testScene->CreateGameObject("Object1"); GameObject* go2 = testScene->CreateGameObject("Object2"); go1->AddComponent(); go2->AddComponent(); auto found = testScene->FindObjectsOfType(); EXPECT_EQ(found.size(), 2u); } TEST_F(SceneTest, FindGameObjectWithTag) { GameObject* go = testScene->CreateGameObject("MyTag"); GameObject* found = testScene->FindGameObjectWithTag("MyTag"); EXPECT_NE(found, nullptr); } TEST_F(SceneTest, OnGameObjectCreated_Event) { bool eventFired = false; testScene->OnGameObjectCreated().Subscribe([&eventFired](GameObject*) { eventFired = true; }); testScene->CreateGameObject("TestObject"); EXPECT_TRUE(eventFired); } TEST_F(SceneTest, OnGameObjectDestroyed_Event) { bool eventFired = false; testScene->OnGameObjectDestroyed().Subscribe([&eventFired](GameObject*) { eventFired = true; }); GameObject* go = testScene->CreateGameObject("TestObject"); testScene->DestroyGameObject(go); EXPECT_TRUE(eventFired); } TEST_F(SceneTest, Save_And_Load) { GameObject* go = testScene->CreateGameObject("SavedObject"); go->GetTransform()->SetLocalPosition(XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f)); go->SetActive(true); const std::filesystem::path scenePath = GetTempScenePath("test_scene.xc"); testScene->Save(scenePath.string()); Scene loadedScene; loadedScene.Load(scenePath.string()); GameObject* loadedGo = loadedScene.Find("SavedObject"); EXPECT_NE(loadedGo, nullptr); EXPECT_EQ(loadedGo->GetTransform()->GetLocalPosition(), XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f)); std::filesystem::remove(scenePath); } TEST_F(SceneTest, Save_ContainsGameObjectData) { testScene->CreateGameObject("Player"); testScene->CreateGameObject("Enemy"); const std::filesystem::path scenePath = GetTempScenePath("test_scene_multi.xc"); testScene->Save(scenePath.string()); std::ifstream file(scenePath); std::stringstream buffer; buffer << file.rdbuf(); std::string content = buffer.str(); file.close(); EXPECT_TRUE(content.find("Player") != std::string::npos); EXPECT_TRUE(content.find("Enemy") != std::string::npos); std::filesystem::remove(scenePath); } TEST_F(SceneTest, Save_And_Load_PreservesAudioComponents) { GameObject* listenerObject = testScene->CreateGameObject("Listener"); GameObject* sourceObject = testScene->CreateGameObject("Source"); listenerObject->AddComponent(); sourceObject->AddComponent(); const std::filesystem::path scenePath = GetTempScenePath("test_scene_audio_components.xc"); testScene->Save(scenePath.string()); Scene loadedScene; loadedScene.Load(scenePath.string()); GameObject* loadedListenerObject = loadedScene.Find("Listener"); GameObject* loadedSourceObject = loadedScene.Find("Source"); ASSERT_NE(loadedListenerObject, nullptr); ASSERT_NE(loadedSourceObject, nullptr); EXPECT_NE(loadedListenerObject->GetComponent(), nullptr); EXPECT_NE(loadedSourceObject->GetComponent(), nullptr); std::filesystem::remove(scenePath); } TEST_F(SceneTest, Save_And_Load_PreservesHierarchyAndComponents) { testScene->SetName("Serialized Scene"); testScene->SetActive(false); GameObject* parent = testScene->CreateGameObject("Rig Root"); parent->GetTransform()->SetLocalPosition(Vector3(5.0f, 0.0f, 0.0f)); parent->SetActive(false); auto* light = parent->AddComponent(); light->SetLightType(LightType::Spot); light->SetIntensity(3.5f); light->SetRange(12.0f); light->SetSpotAngle(45.0f); light->SetCastsShadows(true); GameObject* child = testScene->CreateGameObject("Main Camera", parent); child->GetTransform()->SetLocalPosition(Vector3(1.0f, 2.0f, 3.0f)); child->GetTransform()->SetLocalScale(Vector3(2.0f, 2.0f, 2.0f)); auto* camera = child->AddComponent(); camera->SetProjectionType(CameraProjectionType::Orthographic); camera->SetOrthographicSize(7.5f); camera->SetNearClipPlane(0.5f); camera->SetFarClipPlane(250.0f); camera->SetDepth(2.0f); camera->SetPrimary(false); const std::filesystem::path scenePath = GetTempScenePath("test_scene_hierarchy_components.xc"); testScene->Save(scenePath.string()); Scene loadedScene; loadedScene.Load(scenePath.string()); EXPECT_EQ(loadedScene.GetName(), "Serialized Scene"); EXPECT_FALSE(loadedScene.IsActive()); GameObject* loadedParent = loadedScene.Find("Rig Root"); GameObject* loadedChild = loadedScene.Find("Main Camera"); ASSERT_NE(loadedParent, nullptr); ASSERT_NE(loadedChild, nullptr); EXPECT_EQ(loadedChild->GetParent(), loadedParent); EXPECT_EQ(loadedScene.GetRootGameObjects().size(), 1u); EXPECT_FALSE(loadedParent->IsActive()); EXPECT_EQ(loadedParent->GetTransform()->GetLocalPosition(), Vector3(5.0f, 0.0f, 0.0f)); EXPECT_EQ(loadedChild->GetTransform()->GetLocalPosition(), Vector3(1.0f, 2.0f, 3.0f)); EXPECT_EQ(loadedChild->GetTransform()->GetPosition(), Vector3(6.0f, 2.0f, 3.0f)); auto* loadedLight = loadedParent->GetComponent(); ASSERT_NE(loadedLight, nullptr); EXPECT_EQ(loadedLight->GetLightType(), LightType::Spot); EXPECT_FLOAT_EQ(loadedLight->GetIntensity(), 3.5f); EXPECT_FLOAT_EQ(loadedLight->GetRange(), 12.0f); EXPECT_FLOAT_EQ(loadedLight->GetSpotAngle(), 45.0f); EXPECT_TRUE(loadedLight->GetCastsShadows()); auto* loadedCamera = loadedChild->GetComponent(); ASSERT_NE(loadedCamera, nullptr); EXPECT_EQ(loadedCamera->GetProjectionType(), CameraProjectionType::Orthographic); EXPECT_FLOAT_EQ(loadedCamera->GetOrthographicSize(), 7.5f); EXPECT_FLOAT_EQ(loadedCamera->GetNearClipPlane(), 0.5f); EXPECT_FLOAT_EQ(loadedCamera->GetFarClipPlane(), 250.0f); EXPECT_FLOAT_EQ(loadedCamera->GetDepth(), 2.0f); EXPECT_FALSE(loadedCamera->IsPrimary()); std::filesystem::remove(scenePath); } TEST_F(SceneTest, SerializeToString_And_DeserializeFromString_PreservesHierarchyAndComponents) { testScene->SetName("Round Trip Scene"); testScene->SetActive(false); GameObject* parent = testScene->CreateGameObject("Parent"); parent->GetTransform()->SetLocalPosition(Vector3(2.0f, 4.0f, 6.0f)); parent->GetTransform()->SetLocalScale(Vector3(1.5f, 1.5f, 1.5f)); auto* light = parent->AddComponent(); light->SetLightType(LightType::Point); light->SetIntensity(4.0f); light->SetRange(20.0f); light->SetCastsShadows(true); GameObject* child = testScene->CreateGameObject("Child Camera", parent); child->GetTransform()->SetLocalPosition(Vector3(1.0f, 0.0f, -3.0f)); auto* camera = child->AddComponent(); camera->SetProjectionType(CameraProjectionType::Perspective); camera->SetFieldOfView(72.0f); camera->SetNearClipPlane(0.2f); camera->SetFarClipPlane(512.0f); camera->SetPrimary(true); const std::string serialized = testScene->SerializeToString(); Scene loadedScene; loadedScene.DeserializeFromString(serialized); EXPECT_EQ(loadedScene.GetName(), "Round Trip Scene"); EXPECT_FALSE(loadedScene.IsActive()); EXPECT_EQ(loadedScene.GetRootGameObjects().size(), 1u); GameObject* loadedParent = loadedScene.Find("Parent"); GameObject* loadedChild = loadedScene.Find("Child Camera"); ASSERT_NE(loadedParent, nullptr); ASSERT_NE(loadedChild, nullptr); EXPECT_EQ(loadedChild->GetParent(), loadedParent); EXPECT_EQ(loadedParent->GetTransform()->GetLocalPosition(), Vector3(2.0f, 4.0f, 6.0f)); EXPECT_EQ(loadedParent->GetTransform()->GetLocalScale(), Vector3(1.5f, 1.5f, 1.5f)); EXPECT_EQ(loadedChild->GetTransform()->GetPosition(), Vector3(3.5f, 4.0f, 1.5f)); auto* loadedLight = loadedParent->GetComponent(); ASSERT_NE(loadedLight, nullptr); EXPECT_EQ(loadedLight->GetLightType(), LightType::Point); EXPECT_FLOAT_EQ(loadedLight->GetIntensity(), 4.0f); EXPECT_FLOAT_EQ(loadedLight->GetRange(), 20.0f); EXPECT_TRUE(loadedLight->GetCastsShadows()); auto* loadedCamera = loadedChild->GetComponent(); ASSERT_NE(loadedCamera, nullptr); EXPECT_EQ(loadedCamera->GetProjectionType(), CameraProjectionType::Perspective); EXPECT_FLOAT_EQ(loadedCamera->GetFieldOfView(), 72.0f); EXPECT_FLOAT_EQ(loadedCamera->GetNearClipPlane(), 0.2f); EXPECT_FLOAT_EQ(loadedCamera->GetFarClipPlane(), 512.0f); EXPECT_TRUE(loadedCamera->IsPrimary()); } TEST_F(SceneTest, SerializeToString_And_DeserializeFromString_PreservesUUIDs) { GameObject* parent = testScene->CreateGameObject("Parent"); GameObject* child = testScene->CreateGameObject("Child", parent); const uint64_t parentUUID = parent->GetUUID(); const uint64_t childUUID = child->GetUUID(); const std::string serialized = testScene->SerializeToString(); Scene loadedScene; loadedScene.DeserializeFromString(serialized); GameObject* loadedParent = loadedScene.Find("Parent"); GameObject* loadedChild = loadedScene.Find("Child"); ASSERT_NE(loadedParent, nullptr); ASSERT_NE(loadedChild, nullptr); EXPECT_EQ(loadedParent->GetUUID(), parentUUID); EXPECT_EQ(loadedChild->GetUUID(), childUUID); } TEST_F(SceneTest, Save_ContainsHierarchyAndComponentEntries) { GameObject* parent = testScene->CreateGameObject("Parent"); GameObject* child = testScene->CreateGameObject("Child", parent); child->AddComponent(); parent->AddComponent(); const std::filesystem::path scenePath = GetTempScenePath("test_scene_format.xc"); testScene->Save(scenePath.string()); std::ifstream file(scenePath); std::stringstream buffer; buffer << file.rdbuf(); const std::string content = buffer.str(); file.close(); EXPECT_TRUE(content.find("gameobject_begin") != std::string::npos); EXPECT_TRUE(content.find("parent=" + std::to_string(parent->GetID())) != std::string::npos); EXPECT_TRUE(content.find("component=Camera;") != std::string::npos); EXPECT_TRUE(content.find("component=Light;") != std::string::npos); std::filesystem::remove(scenePath); } TEST_F(SceneTest, SaveAndLoad_PreservesMeshComponentPaths) { GameObject* meshObject = testScene->CreateGameObject("Backpack"); auto* meshFilter = meshObject->AddComponent(); auto* meshRenderer = meshObject->AddComponent(); meshFilter->SetMeshPath("Assets/Models/backpack/backpack.obj"); meshRenderer->SetMaterialPath(0, "Assets/Materials/backpack.mat"); meshRenderer->SetCastShadows(false); meshRenderer->SetReceiveShadows(true); meshRenderer->SetRenderLayer(4); const std::filesystem::path scenePath = GetTempScenePath("test_scene_mesh_components.xc"); testScene->Save(scenePath.string()); Scene loadedScene; loadedScene.Load(scenePath.string()); GameObject* loadedObject = loadedScene.Find("Backpack"); ASSERT_NE(loadedObject, nullptr); auto* loadedMeshFilter = loadedObject->GetComponent(); auto* loadedMeshRenderer = loadedObject->GetComponent(); ASSERT_NE(loadedMeshFilter, nullptr); ASSERT_NE(loadedMeshRenderer, nullptr); EXPECT_EQ(loadedMeshFilter->GetMeshPath(), "Assets/Models/backpack/backpack.obj"); ASSERT_EQ(loadedMeshRenderer->GetMaterialCount(), 1u); EXPECT_EQ(loadedMeshRenderer->GetMaterialPath(0), "Assets/Materials/backpack.mat"); EXPECT_FALSE(loadedMeshRenderer->GetCastShadows()); EXPECT_TRUE(loadedMeshRenderer->GetReceiveShadows()); EXPECT_EQ(loadedMeshRenderer->GetRenderLayer(), 4u); std::filesystem::remove(scenePath); } TEST(Scene_ProjectSample, BackpackSceneLoadsBackpackMeshAsset) { namespace fs = std::filesystem; XCEngine::Resources::ResourceManager& resourceManager = XCEngine::Resources::ResourceManager::Get(); resourceManager.Initialize(); struct ResourceManagerGuard { XCEngine::Resources::ResourceManager* manager = nullptr; ~ResourceManagerGuard() { if (manager != nullptr) { manager->Shutdown(); } } } resourceManagerGuard{ &resourceManager }; struct CurrentPathGuard { fs::path previousPath; ~CurrentPathGuard() { if (!previousPath.empty()) { fs::current_path(previousPath); } } } currentPathGuard{ fs::current_path() }; const fs::path repositoryRoot = GetRepositoryRoot(); const fs::path projectRoot = repositoryRoot / "project"; const fs::path backpackScenePath = projectRoot / "Assets" / "Scenes" / "Backpack.xc"; const fs::path assimpDllPath = repositoryRoot / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll"; const fs::path backpackMeshPath = projectRoot / "Assets" / "Models" / "backpack" / "backpack.obj"; if (!fs::exists(backpackScenePath)) { GTEST_SKIP() << "Backpack sample scene is not available in the local project fixture."; } ASSERT_TRUE(fs::exists(assimpDllPath)); ASSERT_TRUE(fs::exists(backpackMeshPath)); #ifdef _WIN32 struct DllGuard { HMODULE module = nullptr; ~DllGuard() { if (module != nullptr) { FreeLibrary(module); } } } dllGuard; dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str()); ASSERT_NE(dllGuard.module, nullptr); #endif fs::current_path(projectRoot); resourceManager.SetResourceRoot(projectRoot.string().c_str()); ASSERT_NE(resourceManager.GetLoader(XCEngine::Resources::ResourceType::Mesh), nullptr); XCEngine::Resources::MeshLoader meshLoader; const XCEngine::Resources::LoadResult directMeshLoadResult = meshLoader.Load("Assets/Models/backpack/backpack.obj"); ASSERT_TRUE(directMeshLoadResult) << "MeshLoader failed: " << directMeshLoadResult.errorMessage.CStr(); const auto directMeshHandle = resourceManager.Load("Assets/Models/backpack/backpack.obj"); ASSERT_NE(directMeshHandle.Get(), nullptr); ASSERT_TRUE(directMeshHandle->IsValid()); ASSERT_GT(directMeshHandle->GetVertexCount(), 0u); Scene loadedScene; loadedScene.Load(backpackScenePath.string()); std::vector backpackObjects = FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj"); ASSERT_EQ(backpackObjects.size(), 2u); for (GameObject* backpackObject : backpackObjects) { ASSERT_NE(backpackObject, nullptr); auto* meshFilter = backpackObject->GetComponent(); auto* meshRenderer = backpackObject->GetComponent(); ASSERT_NE(meshFilter, nullptr); ASSERT_NE(meshRenderer, nullptr); ASSERT_NE(meshFilter->GetMesh(), nullptr); EXPECT_TRUE(meshFilter->GetMesh()->IsValid()); EXPECT_GT(meshFilter->GetMesh()->GetVertexCount(), 0u); EXPECT_GT(meshFilter->GetMesh()->GetSections().Size(), 0u); EXPECT_GT(meshFilter->GetMesh()->GetMaterials().Size(), 0u); EXPECT_EQ(meshFilter->GetMeshPath(), "Assets/Models/backpack/backpack.obj"); } } TEST(Scene_ProjectSample, MainSceneStaysLightweightForEditorStartup) { namespace fs = std::filesystem; const fs::path repositoryRoot = GetRepositoryRoot(); const fs::path mainScenePath = repositoryRoot / "project" / "Assets" / "Scenes" / "Main.xc"; ASSERT_TRUE(fs::exists(mainScenePath)); Scene loadedScene; loadedScene.Load(mainScenePath.string()); EXPECT_NE(loadedScene.Find("Camera"), nullptr); EXPECT_NE(loadedScene.Find("Light"), nullptr); EXPECT_NE(loadedScene.Find("Cube"), nullptr); EXPECT_EQ(loadedScene.Find("BackpackMesh"), nullptr); EXPECT_EQ(FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj").size(), 0u); } TEST(Scene_ProjectSample, AsyncLoadBackpackMeshArtifactCompletes) { namespace fs = std::filesystem; XCEngine::Resources::ResourceManager& resourceManager = XCEngine::Resources::ResourceManager::Get(); resourceManager.Initialize(); struct ResourceManagerGuard { XCEngine::Resources::ResourceManager* manager = nullptr; ~ResourceManagerGuard() { if (manager != nullptr) { manager->Shutdown(); } } } resourceManagerGuard{ &resourceManager }; struct CurrentPathGuard { fs::path previousPath; ~CurrentPathGuard() { if (!previousPath.empty()) { fs::current_path(previousPath); } } } currentPathGuard{ fs::current_path() }; const fs::path repositoryRoot = GetRepositoryRoot(); const fs::path projectRoot = repositoryRoot / "project"; const fs::path assimpDllPath = repositoryRoot / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll"; ASSERT_TRUE(fs::exists(assimpDllPath)); #ifdef _WIN32 struct DllGuard { HMODULE module = nullptr; ~DllGuard() { if (module != nullptr) { FreeLibrary(module); } } } dllGuard; dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str()); ASSERT_NE(dllGuard.module, nullptr); #endif fs::current_path(projectRoot); resourceManager.SetResourceRoot(projectRoot.string().c_str()); bool callbackInvoked = false; XCEngine::Resources::LoadResult completedResult; resourceManager.LoadAsync( "Assets/Models/backpack/backpack.obj", XCEngine::Resources::ResourceType::Mesh, [&](XCEngine::Resources::LoadResult result) { callbackInvoked = true; completedResult = std::move(result); }); EXPECT_GT(resourceManager.GetAsyncPendingCount(), 0u); ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager, std::chrono::milliseconds(10000))); EXPECT_TRUE(callbackInvoked); ASSERT_TRUE(completedResult); ASSERT_NE(completedResult.resource, nullptr); auto* mesh = static_cast(completedResult.resource); ASSERT_NE(mesh, nullptr); EXPECT_TRUE(mesh->IsValid()); EXPECT_GT(mesh->GetVertexCount(), 0u); EXPECT_GT(mesh->GetSections().Size(), 0u); } TEST(Scene_ProjectSample, AsyncLoadBuiltinMeshAndMaterialComplete) { namespace fs = std::filesystem; XCEngine::Resources::ResourceManager& resourceManager = XCEngine::Resources::ResourceManager::Get(); resourceManager.Initialize(); struct ResourceManagerGuard { XCEngine::Resources::ResourceManager* manager = nullptr; ~ResourceManagerGuard() { if (manager != nullptr) { manager->Shutdown(); } } } resourceManagerGuard{ &resourceManager }; const fs::path repositoryRoot = GetRepositoryRoot(); const fs::path projectRoot = repositoryRoot / "project"; resourceManager.SetResourceRoot(projectRoot.string().c_str()); bool meshCallbackInvoked = false; bool materialCallbackInvoked = false; XCEngine::Resources::LoadResult meshResult; XCEngine::Resources::LoadResult materialResult; resourceManager.LoadAsync( "builtin://meshes/cube", XCEngine::Resources::ResourceType::Mesh, [&](XCEngine::Resources::LoadResult result) { meshCallbackInvoked = true; meshResult = std::move(result); }); resourceManager.LoadAsync( "builtin://materials/default-primitive", XCEngine::Resources::ResourceType::Material, [&](XCEngine::Resources::LoadResult result) { materialCallbackInvoked = true; materialResult = std::move(result); }); EXPECT_GE(resourceManager.GetAsyncPendingCount(), 2u); ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager, std::chrono::milliseconds(10000))); EXPECT_TRUE(meshCallbackInvoked); EXPECT_TRUE(materialCallbackInvoked); ASSERT_TRUE(meshResult); ASSERT_TRUE(materialResult); ASSERT_NE(meshResult.resource, nullptr); ASSERT_NE(materialResult.resource, nullptr); } TEST(Scene_ProjectSample, AsyncLoadBuiltinMeshCompletes) { namespace fs = std::filesystem; XCEngine::Resources::ResourceManager& resourceManager = XCEngine::Resources::ResourceManager::Get(); resourceManager.Initialize(); struct ResourceManagerGuard { XCEngine::Resources::ResourceManager* manager = nullptr; ~ResourceManagerGuard() { if (manager != nullptr) { manager->Shutdown(); } } } resourceManagerGuard{ &resourceManager }; const fs::path repositoryRoot = GetRepositoryRoot(); const fs::path projectRoot = repositoryRoot / "project"; resourceManager.SetResourceRoot(projectRoot.string().c_str()); bool callbackInvoked = false; XCEngine::Resources::LoadResult result; resourceManager.LoadAsync( "builtin://meshes/cube", XCEngine::Resources::ResourceType::Mesh, [&](XCEngine::Resources::LoadResult loadResult) { callbackInvoked = true; result = std::move(loadResult); }); EXPECT_GT(resourceManager.GetAsyncPendingCount(), 0u); ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager, std::chrono::milliseconds(10000))); EXPECT_TRUE(callbackInvoked); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); } TEST(Scene_ProjectSample, AsyncLoadBuiltinMaterialCompletes) { namespace fs = std::filesystem; XCEngine::Resources::ResourceManager& resourceManager = XCEngine::Resources::ResourceManager::Get(); resourceManager.Initialize(); struct ResourceManagerGuard { XCEngine::Resources::ResourceManager* manager = nullptr; ~ResourceManagerGuard() { if (manager != nullptr) { manager->Shutdown(); } } } resourceManagerGuard{ &resourceManager }; const fs::path repositoryRoot = GetRepositoryRoot(); const fs::path projectRoot = repositoryRoot / "project"; resourceManager.SetResourceRoot(projectRoot.string().c_str()); bool callbackInvoked = false; XCEngine::Resources::LoadResult result; resourceManager.LoadAsync( "builtin://materials/default-primitive", XCEngine::Resources::ResourceType::Material, [&](XCEngine::Resources::LoadResult loadResult) { callbackInvoked = true; result = std::move(loadResult); }); EXPECT_GT(resourceManager.GetAsyncPendingCount(), 0u); ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager, std::chrono::milliseconds(10000))); EXPECT_TRUE(callbackInvoked); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); } TEST(Scene_ProjectSample, DeferredLoadBackpackSceneEventuallyRestoresBackpackMesh) { namespace fs = std::filesystem; XCEngine::Resources::ResourceManager& resourceManager = XCEngine::Resources::ResourceManager::Get(); resourceManager.Initialize(); struct ResourceManagerGuard { XCEngine::Resources::ResourceManager* manager = nullptr; ~ResourceManagerGuard() { if (manager != nullptr) { manager->Shutdown(); } } } resourceManagerGuard{ &resourceManager }; struct CurrentPathGuard { fs::path previousPath; ~CurrentPathGuard() { if (!previousPath.empty()) { fs::current_path(previousPath); } } } currentPathGuard{ fs::current_path() }; const fs::path repositoryRoot = GetRepositoryRoot(); const fs::path projectRoot = repositoryRoot / "project"; const fs::path backpackScenePath = projectRoot / "Assets" / "Scenes" / "Backpack.xc"; const fs::path assimpDllPath = repositoryRoot / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll"; if (!fs::exists(backpackScenePath)) { GTEST_SKIP() << "Backpack sample scene is not available in the local project fixture."; } ASSERT_TRUE(fs::exists(assimpDllPath)); #ifdef _WIN32 struct DllGuard { HMODULE module = nullptr; ~DllGuard() { if (module != nullptr) { FreeLibrary(module); } } } dllGuard; dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str()); ASSERT_NE(dllGuard.module, nullptr); #endif fs::current_path(projectRoot); resourceManager.SetResourceRoot(projectRoot.string().c_str()); Scene loadedScene; { XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope; loadedScene.Load(backpackScenePath.string()); } std::vector backpackObjects = FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj"); ASSERT_EQ(backpackObjects.size(), 2u); std::vector backpackMeshFilters; std::vector backpackMeshRenderers; for (GameObject* backpackObject : backpackObjects) { ASSERT_NE(backpackObject, nullptr); auto* meshFilter = backpackObject->GetComponent(); auto* meshRenderer = backpackObject->GetComponent(); ASSERT_NE(meshFilter, nullptr); ASSERT_NE(meshRenderer, nullptr); EXPECT_EQ(meshFilter->GetMeshPath(), "Assets/Models/backpack/backpack.obj"); EXPECT_EQ(meshFilter->GetMesh(), nullptr); backpackMeshFilters.push_back(meshFilter); backpackMeshRenderers.push_back(meshRenderer); } EXPECT_GT(resourceManager.GetAsyncPendingCount(), 0u); ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager)); EXPECT_EQ(resourceManager.GetAsyncPendingCount(), 0u); ASSERT_EQ(backpackMeshFilters.size(), 2u); ASSERT_EQ(backpackMeshRenderers.size(), 2u); ASSERT_NE(backpackMeshFilters[0]->GetMesh(), nullptr); ASSERT_NE(backpackMeshFilters[1]->GetMesh(), nullptr); EXPECT_EQ(backpackMeshFilters[0]->GetMesh(), backpackMeshFilters[1]->GetMesh()); EXPECT_TRUE(backpackMeshFilters[0]->GetMesh()->IsValid()); EXPECT_TRUE(backpackMeshFilters[1]->GetMesh()->IsValid()); EXPECT_GT(backpackMeshFilters[0]->GetMesh()->GetVertexCount(), 0u); EXPECT_GT(backpackMeshFilters[1]->GetMesh()->GetVertexCount(), 0u); EXPECT_EQ(backpackMeshRenderers[0]->GetMaterialCount(), 0u); EXPECT_EQ(backpackMeshRenderers[1]->GetMaterialCount(), 0u); } TEST(Scene_ProjectSample, DeferredLoadBackpackSceneEventuallyProducesVisibleRenderItems) { namespace fs = std::filesystem; XCEngine::Resources::ResourceManager& resourceManager = XCEngine::Resources::ResourceManager::Get(); resourceManager.Initialize(); struct ResourceManagerGuard { XCEngine::Resources::ResourceManager* manager = nullptr; ~ResourceManagerGuard() { if (manager != nullptr) { manager->Shutdown(); } } } resourceManagerGuard{ &resourceManager }; struct CurrentPathGuard { fs::path previousPath; ~CurrentPathGuard() { if (!previousPath.empty()) { fs::current_path(previousPath); } } } currentPathGuard{ fs::current_path() }; const fs::path repositoryRoot = GetRepositoryRoot(); const fs::path projectRoot = repositoryRoot / "project"; const fs::path backpackScenePath = projectRoot / "Assets" / "Scenes" / "Backpack.xc"; const fs::path assimpDllPath = repositoryRoot / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll"; if (!fs::exists(backpackScenePath)) { GTEST_SKIP() << "Backpack sample scene is not available in the local project fixture."; } ASSERT_TRUE(fs::exists(assimpDllPath)); #ifdef _WIN32 struct DllGuard { HMODULE module = nullptr; ~DllGuard() { if (module != nullptr) { FreeLibrary(module); } } } dllGuard; dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str()); ASSERT_NE(dllGuard.module, nullptr); #endif fs::current_path(projectRoot); resourceManager.SetResourceRoot(projectRoot.string().c_str()); Scene loadedScene; { XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope; loadedScene.Load(backpackScenePath.string()); } const std::vector backpackObjects = FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj"); ASSERT_EQ(backpackObjects.size(), 2u); XCEngine::Rendering::RenderSceneExtractor extractor; const XCEngine::Rendering::RenderSceneData initialRenderScene = extractor.Extract(loadedScene, nullptr, 1280u, 720u); ASSERT_TRUE(initialRenderScene.HasCamera()); EXPECT_GT(resourceManager.GetAsyncPendingCount(), 0u); ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager, std::chrono::milliseconds(10000))); std::vector backpackMeshFilters; for (GameObject* backpackObject : backpackObjects) { ASSERT_NE(backpackObject, nullptr); auto* meshFilter = backpackObject->GetComponent(); ASSERT_NE(meshFilter, nullptr); ASSERT_NE(meshFilter->GetMesh(), nullptr); backpackMeshFilters.push_back(meshFilter); } const XCEngine::Rendering::RenderSceneData renderScene = extractor.Extract(loadedScene, nullptr, 1280u, 720u); ASSERT_TRUE(renderScene.HasCamera()); ASSERT_FALSE(renderScene.visibleItems.empty()); std::vector foundVisibleBackpack(backpackObjects.size(), false); for (const auto& visibleItem : renderScene.visibleItems) { for (size_t index = 0; index < backpackObjects.size(); ++index) { if (visibleItem.gameObject == backpackObjects[index] && visibleItem.mesh == backpackMeshFilters[index]->GetMesh()) { foundVisibleBackpack[index] = true; } } } EXPECT_TRUE(foundVisibleBackpack[0]); EXPECT_TRUE(foundVisibleBackpack[1]); } } // namespace