diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index b033f31c..23c1a9a5 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -53,6 +53,7 @@ add_executable(${PROJECT_NAME} WIN32 target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CMAKE_CURRENT_SOURCE_DIR}/src/Viewport ${imgui_SOURCE_DIR} ${imgui_SOURCE_DIR}/backends ) @@ -79,3 +80,9 @@ set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "XCEngine" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bin" ) + +add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll + $/assimp-vc143-mt.dll +) diff --git a/editor/src/Managers/ProjectManager.cpp b/editor/src/Managers/ProjectManager.cpp index a8ff7d0b..11b75f32 100644 --- a/editor/src/Managers/ProjectManager.cpp +++ b/editor/src/Managers/ProjectManager.cpp @@ -1,6 +1,7 @@ #include "ProjectManager.h" #include #include +#include #include #include #include @@ -12,6 +13,24 @@ namespace Editor { namespace { +std::wstring Utf8PathToWstring(const std::string& str) { + if (str.empty()) return L""; + int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, nullptr, 0); + if (len <= 0) return L""; + std::wstring result(len - 1, 0); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, &result[0], len); + return result; +} + +std::string WstringPathToUtf8(const std::wstring& wstr) { + if (wstr.empty()) return ""; + int len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, nullptr, 0, nullptr, nullptr); + if (len <= 0) return ""; + std::string result(len - 1, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &result[0], len, nullptr, nullptr); + return result; +} + std::wstring MakePathKey(const fs::path& path) { std::wstring key = path.lexically_normal().generic_wstring(); std::transform(key.begin(), key.end(), key.begin(), ::towlower); @@ -34,6 +53,92 @@ bool IsSameOrDescendantPath(const fs::path& path, const fs::path& ancestor) { return pathKey.rfind(ancestorKey, 0) == 0; } +std::string TrimAssetName(const std::string& name) { + const auto first = std::find_if_not(name.begin(), name.end(), [](unsigned char ch) { + return std::isspace(ch) != 0; + }); + if (first == name.end()) { + return {}; + } + + const auto last = std::find_if_not(name.rbegin(), name.rend(), [](unsigned char ch) { + return std::isspace(ch) != 0; + }).base(); + return std::string(first, last); +} + +bool HasInvalidAssetName(const std::string& name) { + if (name.empty() || name == "." || name == "..") { + return true; + } + + return name.find_first_of("\\/:*?\"<>|") != std::string::npos; +} + +fs::path MakeUniqueFolderPath(const fs::path& parentPath, const std::string& preferredName) { + fs::path candidatePath = parentPath / Utf8PathToWstring(preferredName); + if (!fs::exists(candidatePath)) { + return candidatePath; + } + + for (size_t suffix = 1;; ++suffix) { + candidatePath = parentPath / Utf8PathToWstring(preferredName + " " + std::to_string(suffix)); + if (!fs::exists(candidatePath)) { + return candidatePath; + } + } +} + +std::string BuildRenamedEntryName(const fs::path& sourcePath, const std::string& requestedName) { + if (fs::is_directory(sourcePath)) { + return requestedName; + } + + const fs::path requestedPath = Utf8PathToWstring(requestedName); + if (requestedPath.has_extension()) { + return WstringPathToUtf8(requestedPath.filename().wstring()); + } + + return requestedName + WstringPathToUtf8(sourcePath.extension().wstring()); +} + +fs::path MakeCaseOnlyRenameTempPath(const fs::path& sourcePath) { + const fs::path parentPath = sourcePath.parent_path(); + const std::wstring sourceStem = sourcePath.filename().wstring(); + + for (size_t suffix = 0;; ++suffix) { + std::wstring tempName = sourceStem + L".xc_tmp_rename"; + if (suffix > 0) { + tempName += std::to_wstring(suffix); + } + + const fs::path tempPath = parentPath / tempName; + if (!fs::exists(tempPath)) { + return tempPath; + } + } +} + +bool RenamePathCaseAware(const fs::path& sourcePath, const fs::path& destPath) { + if (MakePathKey(sourcePath) != MakePathKey(destPath)) { + if (fs::exists(destPath)) { + return false; + } + + fs::rename(sourcePath, destPath); + return true; + } + + if (sourcePath.filename() == destPath.filename()) { + return true; + } + + const fs::path tempPath = MakeCaseOnlyRenameTempPath(sourcePath); + fs::rename(sourcePath, tempPath); + fs::rename(tempPath, destPath); + return true; +} + } // namespace const std::vector& ProjectManager::GetCurrentItems() const { @@ -134,28 +239,10 @@ std::string ProjectManager::GetPathName(size_t index) const { return m_path[index]->name; } -static std::wstring Utf8ToWstring(const std::string& str) { - if (str.empty()) return L""; - int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, nullptr, 0); - if (len <= 0) return L""; - std::wstring result(len - 1, 0); - MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, &result[0], len); - return result; -} - -static std::string WstringToUtf8(const std::wstring& wstr) { - if (wstr.empty()) return ""; - int len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, nullptr, 0, nullptr, nullptr); - if (len <= 0) return ""; - std::string result(len - 1, 0); - WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &result[0], len, nullptr, nullptr); - return result; -} - void ProjectManager::Initialize(const std::string& projectPath) { m_projectPath = projectPath; - std::wstring projectPathW = Utf8ToWstring(projectPath); + std::wstring projectPathW = Utf8PathToWstring(projectPath); fs::path assetsPath = fs::path(projectPathW) / L"Assets"; try { @@ -166,7 +253,7 @@ void ProjectManager::Initialize(const std::string& projectPath) { m_rootFolder = ScanDirectory(assetsPath.wstring()); m_rootFolder->name = "Assets"; - m_rootFolder->fullPath = WstringToUtf8(assetsPath.wstring()); + m_rootFolder->fullPath = WstringPathToUtf8(assetsPath.wstring()); m_path.clear(); m_path.push_back(m_rootFolder); @@ -176,7 +263,7 @@ void ProjectManager::Initialize(const std::string& projectPath) { m_rootFolder->name = "Assets"; m_rootFolder->isFolder = true; m_rootFolder->type = "Folder"; - m_rootFolder->fullPath = WstringToUtf8(assetsPath.wstring()); + m_rootFolder->fullPath = WstringPathToUtf8(assetsPath.wstring()); m_path.clear(); m_path.push_back(m_rootFolder); ClearSelection(); @@ -185,10 +272,10 @@ void ProjectManager::Initialize(const std::string& projectPath) { std::wstring ProjectManager::GetCurrentFullPathW() const { if (AssetItemPtr currentFolder = GetCurrentFolder()) { - return Utf8ToWstring(currentFolder->fullPath); + return Utf8PathToWstring(currentFolder->fullPath); } - return Utf8ToWstring(m_projectPath); + return Utf8PathToWstring(m_projectPath); } void ProjectManager::RefreshCurrentFolder() { @@ -200,13 +287,31 @@ void ProjectManager::RefreshCurrentFolder() { } } -void ProjectManager::CreateFolder(const std::string& name) { +AssetItemPtr ProjectManager::CreateFolder(const std::string& name) { + if (!m_rootFolder) { + return nullptr; + } + try { - std::wstring fullPath = GetCurrentFullPathW(); - fs::path newFolderPath = fs::path(fullPath) / Utf8ToWstring(name); - fs::create_directory(newFolderPath); + const std::string trimmedName = TrimAssetName(name); + if (HasInvalidAssetName(trimmedName)) { + return nullptr; + } + + const fs::path currentFolderPath = GetCurrentFullPathW(); + const fs::path newFolderPath = MakeUniqueFolderPath(currentFolderPath, trimmedName); + if (!fs::create_directory(newFolderPath)) { + return nullptr; + } + RefreshCurrentFolder(); + const AssetItemPtr createdFolder = FindCurrentItemByPath(WstringPathToUtf8(newFolderPath.wstring())); + if (createdFolder) { + SetSelectedItem(createdFolder); + } + return createdFolder; } catch (...) { + return nullptr; } } @@ -216,8 +321,8 @@ bool ProjectManager::DeleteItem(const std::string& fullPath) { } try { - const fs::path itemPath = Utf8ToWstring(fullPath); - const fs::path rootPath = Utf8ToWstring(m_rootFolder->fullPath); + const fs::path itemPath = Utf8PathToWstring(fullPath); + const fs::path rootPath = Utf8PathToWstring(m_rootFolder->fullPath); if (!fs::exists(itemPath) || !IsSameOrDescendantPath(itemPath, rootPath)) { return false; } @@ -242,9 +347,9 @@ bool ProjectManager::MoveItem(const std::string& sourceFullPath, const std::stri } try { - const fs::path sourcePath = Utf8ToWstring(sourceFullPath); - const fs::path destFolderPath = Utf8ToWstring(destFolderFullPath); - const fs::path rootPath = Utf8ToWstring(m_rootFolder->fullPath); + const fs::path sourcePath = Utf8PathToWstring(sourceFullPath); + const fs::path destFolderPath = Utf8PathToWstring(destFolderFullPath); + const fs::path rootPath = Utf8PathToWstring(m_rootFolder->fullPath); if (!fs::exists(sourcePath) || !fs::exists(destFolderPath) || !fs::is_directory(destFolderPath)) { return false; @@ -272,6 +377,49 @@ bool ProjectManager::MoveItem(const std::string& sourceFullPath, const std::stri } } +bool ProjectManager::RenameItem(const std::string& sourceFullPath, const std::string& newName) { + if (sourceFullPath.empty() || !m_rootFolder) { + return false; + } + + try { + const std::string trimmedName = TrimAssetName(newName); + if (HasInvalidAssetName(trimmedName)) { + return false; + } + + const fs::path sourcePath = Utf8PathToWstring(sourceFullPath); + const fs::path rootPath = Utf8PathToWstring(m_rootFolder->fullPath); + if (!fs::exists(sourcePath) || !IsSameOrDescendantPath(sourcePath, rootPath)) { + return false; + } + if (MakePathKey(sourcePath) == MakePathKey(rootPath)) { + return false; + } + + const std::string targetName = BuildRenamedEntryName(sourcePath, trimmedName); + if (HasInvalidAssetName(targetName)) { + return false; + } + + const fs::path destPath = sourcePath.parent_path() / Utf8PathToWstring(targetName); + if (!RenamePathCaseAware(sourcePath, destPath)) { + return false; + } + + if (!m_selectedItemPath.empty() && + MakePathKey(Utf8PathToWstring(m_selectedItemPath)) == MakePathKey(sourcePath)) + { + m_selectedItemPath = WstringPathToUtf8(destPath.wstring()); + } + + RefreshCurrentFolder(); + return true; + } catch (...) { + return false; + } +} + AssetItemPtr ProjectManager::FindCurrentItemByPath(const std::string& fullPath) const { const int index = FindCurrentItemIndex(fullPath); if (index < 0) { @@ -289,10 +437,10 @@ void ProjectManager::SyncSelection() { AssetItemPtr ProjectManager::ScanDirectory(const std::wstring& path) { auto folder = std::make_shared(); - folder->name = WstringToUtf8(fs::path(path).filename().wstring()); + folder->name = WstringPathToUtf8(fs::path(path).filename().wstring()); folder->isFolder = true; folder->type = "Folder"; - folder->fullPath = WstringToUtf8(path); + folder->fullPath = WstringPathToUtf8(path); if (!fs::exists(path)) return folder; @@ -352,10 +500,10 @@ void ProjectManager::RebuildTreePreservingPath() { } } - const fs::path assetsPath = fs::path(Utf8ToWstring(m_projectPath)) / L"Assets"; + const fs::path assetsPath = fs::path(Utf8PathToWstring(m_projectPath)) / L"Assets"; m_rootFolder = ScanDirectory(assetsPath.wstring()); m_rootFolder->name = "Assets"; - m_rootFolder->fullPath = WstringToUtf8(assetsPath.wstring()); + m_rootFolder->fullPath = WstringPathToUtf8(assetsPath.wstring()); m_path.clear(); m_path.push_back(m_rootFolder); @@ -373,9 +521,9 @@ void ProjectManager::RebuildTreePreservingPath() { AssetItemPtr ProjectManager::CreateAssetItem(const std::wstring& path, const std::wstring& nameW, bool isFolder) { auto item = std::make_shared(); - item->name = WstringToUtf8(nameW); + item->name = WstringPathToUtf8(nameW); item->isFolder = isFolder; - item->fullPath = WstringToUtf8(path); + item->fullPath = WstringPathToUtf8(path); if (isFolder) { item->type = "Folder"; @@ -388,7 +536,7 @@ AssetItemPtr ProjectManager::CreateAssetItem(const std::wstring& path, const std std::wstring ext = fs::path(path).extension().wstring(); std::transform(ext.begin(), ext.end(), ext.begin(), ::towlower); - if (ext == L".png" || ext == L".jpg" || ext == L".tga" || ext == L".bmp") { + if (ext == L".png" || ext == L".jpg" || ext == L".jpeg" || ext == L".tga" || ext == L".bmp") { item->type = "Texture"; } else if (ext == L".fbx" || ext == L".obj" || ext == L".gltf" || ext == L".glb") { item->type = "Model"; diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index e4c2f11a..f089c62c 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -435,6 +435,11 @@ target_link_libraries(XCEngine PUBLIC Vulkan::Vulkan ) +if(MSVC) + target_link_libraries(XCEngine PUBLIC delayimp) + target_link_options(XCEngine INTERFACE "/DELAYLOAD:assimp-vc143-mt.dll") +endif() + if(XCENGINE_ENABLE_MONO_SCRIPTING) set(XCENGINE_MONO_INCLUDE_DIR "${XCENGINE_MONO_ROOT_DIR}/include") set(XCENGINE_MONO_STATIC_LIBRARY "${XCENGINE_MONO_ROOT_DIR}/lib/libmono-static-sgen.lib") diff --git a/engine/src/Core/Asset/ResourceManager.cpp b/engine/src/Core/Asset/ResourceManager.cpp index 725a6c89..a240da41 100644 --- a/engine/src/Core/Asset/ResourceManager.cpp +++ b/engine/src/Core/Asset/ResourceManager.cpp @@ -2,7 +2,9 @@ #include #include #include +#include #include +#include namespace XCEngine { namespace Resources { @@ -17,7 +19,9 @@ void RegisterBuiltinLoader(ResourceManager& manager, TLoader& loader) { } MaterialLoader g_materialLoader; +MeshLoader g_meshLoader; ShaderLoader g_shaderLoader; +TextureLoader g_textureLoader; } // namespace @@ -31,7 +35,9 @@ void ResourceManager::Initialize() { m_asyncLoader->Initialize(2); RegisterBuiltinLoader(*this, g_materialLoader); + RegisterBuiltinLoader(*this, g_meshLoader); RegisterBuiltinLoader(*this, g_shaderLoader); + RegisterBuiltinLoader(*this, g_textureLoader); } void ResourceManager::Shutdown() { diff --git a/project/Assets/Models/backpack/ao.jpg b/project/Assets/Models/backpack/ao.jpg new file mode 100644 index 00000000..11a2aeec Binary files /dev/null and b/project/Assets/Models/backpack/ao.jpg differ diff --git a/project/Assets/Models/backpack/backpack.mtl b/project/Assets/Models/backpack/backpack.mtl new file mode 100644 index 00000000..f8b974ca --- /dev/null +++ b/project/Assets/Models/backpack/backpack.mtl @@ -0,0 +1,16 @@ +# Blender MTL File: 'None' +# Material Count: 1 + +newmtl Scene_-_Root +Ns 225.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.800000 0.800000 0.800000 +Ks 0.500000 0.500000 0.500000 +Ke 0.0 0.0 0.0 +Ni 1.450000 +d 1.000000 +illum 2 +map_Kd diffuse.jpg +map_Bump normal.png +map_Ks specular.jpg + diff --git a/project/Assets/Models/backpack/diffuse.jpg b/project/Assets/Models/backpack/diffuse.jpg new file mode 100644 index 00000000..e971a331 Binary files /dev/null and b/project/Assets/Models/backpack/diffuse.jpg differ diff --git a/project/Assets/Models/backpack/normal.png b/project/Assets/Models/backpack/normal.png new file mode 100644 index 00000000..3a8a1030 Binary files /dev/null and b/project/Assets/Models/backpack/normal.png differ diff --git a/project/Assets/Models/backpack/roughness.jpg b/project/Assets/Models/backpack/roughness.jpg new file mode 100644 index 00000000..9f50e674 Binary files /dev/null and b/project/Assets/Models/backpack/roughness.jpg differ diff --git a/project/Assets/Models/backpack/source_attribution.txt b/project/Assets/Models/backpack/source_attribution.txt new file mode 100644 index 00000000..a704b4ae --- /dev/null +++ b/project/Assets/Models/backpack/source_attribution.txt @@ -0,0 +1,3 @@ +Model by Berk Gedik, from: https://sketchfab.com/3d-models/survival-guitar-backpack-low-poly-799f8c4511f84fab8c3f12887f7e6b36 + +Modified material assignment (Joey de Vries) for easier load in OpenGL model loading chapter, and renamed albedo to diffuse and metallic to specular to match non-PBR lighting setup. \ No newline at end of file diff --git a/project/Assets/Models/backpack/specular.jpg b/project/Assets/Models/backpack/specular.jpg new file mode 100644 index 00000000..1fd675a6 Binary files /dev/null and b/project/Assets/Models/backpack/specular.jpg differ diff --git a/project/Assets/Scenes/Main.xc b/project/Assets/Scenes/Main.xc new file mode 100644 index 00000000..e2a9f9c0 --- /dev/null +++ b/project/Assets/Scenes/Main.xc @@ -0,0 +1,61 @@ +# XCEngine Scene File +scene=Main Scene +active=1 + +gameobject_begin +id=1 +uuid=11806343893082442755 +name=Main Camera +active=1 +parent=0 +transform=position=0,0,0;rotation=0,0,0,1;scale=1,1,1; +component=Camera;projection=0;fov=45;orthoSize=5;near=0.1;far=100;depth=0;primary=1;clearColor=0.05,0.05,0.08,1; +gameobject_end + +gameobject_begin +id=2 +uuid=1695101543211549096 +name=BackpackRoot +active=1 +parent=0 +transform=position=0,0.08,3;rotation=0,0,0,1;scale=1,1,1; +gameobject_end + +gameobject_begin +id=3 +uuid=9855370671540411784 +name=BackpackRotateY +active=1 +parent=2 +transform=position=0,0,0;rotation=0,-0.174108138,0,0.984726539;scale=1,1,1; +gameobject_end + +gameobject_begin +id=4 +uuid=14568936532392398358 +name=BackpackRotateX +active=1 +parent=3 +transform=position=0,0,0;rotation=-0.0898785492,0,0,0.995952733;scale=1,1,1; +gameobject_end + +gameobject_begin +id=5 +uuid=7319598685716776031 +name=BackpackScale +active=1 +parent=4 +transform=position=0,0,0;rotation=0,0,0,1;scale=0.389120452,0.389120452,0.389120452; +gameobject_end + +gameobject_begin +id=6 +uuid=14495577888798577643 +name=BackpackMesh +active=1 +parent=5 +transform=position=0.048938,-0.5718905,-0.943127;rotation=0,0,0,1;scale=1,1,1; +component=MeshFilter;mesh=Assets/Models/backpack/backpack.obj; +component=MeshRenderer;materials=;castShadows=1;receiveShadows=1;renderLayer=0; +gameobject_end + diff --git a/tests/Rendering/unit/test_render_scene_extractor.cpp b/tests/Rendering/unit/test_render_scene_extractor.cpp index 6f9b83e6..485bc644 100644 --- a/tests/Rendering/unit/test_render_scene_extractor.cpp +++ b/tests/Rendering/unit/test_render_scene_extractor.cpp @@ -247,6 +247,40 @@ TEST(RenderSceneExtractor_Test, SortsOpaqueFrontToBackAndTransparentBackToFront) delete transparentFarMesh; } +TEST(RenderSceneExtractor_Test, FallsBackToEmbeddedMeshMaterialsWhenRendererHasNoExplicitSlots) { + Scene scene("EmbeddedMaterialScene"); + + GameObject* cameraObject = scene.CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + camera->SetPrimary(true); + + GameObject* renderObject = scene.CreateGameObject("EmbeddedBackpack"); + auto* meshFilter = renderObject->AddComponent(); + auto* meshRenderer = renderObject->AddComponent(); + + Mesh* mesh = CreateSectionedTestMesh("Meshes/embedded.mesh", { 0u }); + Material* embeddedMaterial = CreateTestMaterial( + "Materials/embedded.mat", + static_cast(MaterialRenderQueue::Transparent), + "ForwardLit", + "ForwardBase"); + mesh->AddMaterial(embeddedMaterial); + meshFilter->SetMesh(mesh); + meshRenderer->ClearMaterials(); + + RenderSceneExtractor extractor; + const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600); + + ASSERT_EQ(sceneData.visibleItems.size(), 1u); + EXPECT_EQ(sceneData.visibleItems[0].material, embeddedMaterial); + EXPECT_EQ(sceneData.visibleItems[0].renderQueue, static_cast(MaterialRenderQueue::Transparent)); + EXPECT_EQ(ResolveMaterial(sceneData.visibleItems[0]), embeddedMaterial); + + meshFilter->ClearMesh(); + delete embeddedMaterial; + delete mesh; +} + TEST(RenderMaterialUtility_Test, MatchesBuiltinForwardPassMetadata) { Material forwardMaterial; forwardMaterial.SetShaderPass("ForwardLit"); diff --git a/tests/Resources/Mesh/test_mesh_loader.cpp b/tests/Resources/Mesh/test_mesh_loader.cpp index 62861042..373b49fc 100644 --- a/tests/Resources/Mesh/test_mesh_loader.cpp +++ b/tests/Resources/Mesh/test_mesh_loader.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -43,6 +44,16 @@ TEST(MeshLoader, LoadInvalidPath) { EXPECT_FALSE(result); } +TEST(MeshLoader, ResourceManagerRegistersMeshAndTextureLoaders) { + ResourceManager& manager = ResourceManager::Get(); + manager.Initialize(); + + EXPECT_NE(manager.GetLoader(ResourceType::Mesh), nullptr); + EXPECT_NE(manager.GetLoader(ResourceType::Texture), nullptr); + + manager.Shutdown(); +} + TEST(MeshLoader, LoadValidObjMesh) { MeshLoader loader; const std::string path = GetMeshFixturePath("triangle.obj"); diff --git a/tests/Scene/test_scene.cpp b/tests/Scene/test_scene.cpp index ff0ee75e..07ae459f 100644 --- a/tests/Scene/test_scene.cpp +++ b/tests/Scene/test_scene.cpp @@ -5,17 +5,32 @@ #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(); +} + class TestComponent : public Component { public: TestComponent() = default; @@ -505,4 +520,118 @@ TEST_F(SceneTest, Save_ContainsHierarchyAndComponentEntries) { 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, MainSceneLoadsBackpackMeshAsset) { + 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 mainScenePath = projectRoot / "Assets" / "Scenes" / "Main.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"; + + ASSERT_TRUE(fs::exists(mainScenePath)); + 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); + + 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(mainScenePath.string()); + + GameObject* backpackObject = loadedScene.Find("BackpackMesh"); + 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"); +} + } // namespace diff --git a/tests/Scene/test_scene_runtime.cpp b/tests/Scene/test_scene_runtime.cpp index 7fbdf8d2..15a1790e 100644 --- a/tests/Scene/test_scene_runtime.cpp +++ b/tests/Scene/test_scene_runtime.cpp @@ -97,6 +97,18 @@ public: return false; } + bool TryGetClassFieldDefaultValues( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className, + std::vector& outFields) const override { + (void)assemblyName; + (void)namespaceName; + (void)className; + outFields.clear(); + return false; + } + bool TrySetManagedFieldValue( const ScriptRuntimeContext& context, const std::string& fieldName,