#include "MetaCoreEditor/MetaCoreBuiltinModules.h" #include "MetaCoreEditor/MetaCoreEditorAssetTypes.h" #include "MetaCoreEditor/MetaCoreEditorContext.h" #include "MetaCoreEditor/MetaCoreEditorModule.h" #include "MetaCoreEditor/MetaCoreEditorServices.h" #include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" #include "MetaCoreFoundation/MetaCoreLogService.h" #include "MetaCorePlatform/MetaCoreWindow.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataDispatcher.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataSource.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" #include "MetaCoreScene/MetaCoreScenePackage.h" #include "MetaCoreScene/MetaCoreScene.h" #include #include #include #include #include #include #include #include #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #ifndef NOMINMAX #define NOMINMAX #endif #include #include namespace { void MetaCoreExpect(bool condition, const char* message) { if (!condition) { std::cerr << "MetaCoreSmokeTests failed: " << message << '\n'; std::exit(1); } } void MetaCoreExpectVec3Near(const glm::vec3& actual, const glm::vec3& expected, const char* message) { const auto nearlyEqual = [](float lhs, float rhs) { return std::abs(lhs - rhs) <= 0.0001F; }; if (!nearlyEqual(actual.x, expected.x) || !nearlyEqual(actual.y, expected.y) || !nearlyEqual(actual.z, expected.z)) { std::cerr << "MetaCoreSmokeTests failed: " << message << " (actual=" << actual.x << "," << actual.y << "," << actual.z << " expected=" << expected.x << "," << expected.y << "," << expected.z << ")\n"; std::exit(1); } } class MetaCoreDummyPanelProvider final : public MetaCore::MetaCoreIEditorPanelProvider { public: std::string GetPanelId() const override { return "Dummy"; } std::string GetPanelTitle() const override { return "Dummy"; } bool IsOpenByDefault() const override { return false; } void DrawPanel(MetaCore::MetaCoreEditorContext&) override {} }; void MetaCoreTestSceneEditApi() { MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); const std::size_t originalCount = scene.GetGameObjects().size(); const auto roots = scene.GetRootObjectIds(); MetaCoreExpect(!roots.empty(), "默认场景应有根对象"); const std::vector duplicateRoots = scene.DuplicateGameObjects({roots.front()}); MetaCoreExpect(!duplicateRoots.empty(), "复制应返回新根对象"); MetaCoreExpect(scene.GetGameObjects().size() > originalCount, "复制后对象数量应增加"); const MetaCore::MetaCoreId duplicatedRootId = duplicateRoots.front(); MetaCoreExpect(scene.FindGameObject(duplicatedRootId) != nullptr, "复制后的对象应可被查找到"); const bool reparented = scene.ReparentGameObjects({duplicatedRootId}, 0, true); MetaCoreExpect(reparented, "重挂接操作应成功"); const std::vector deletedIds = scene.DeleteGameObjects({duplicatedRootId}); MetaCoreExpect(!deletedIds.empty(), "删除应返回被删除对象"); MetaCoreExpect(scene.FindGameObject(duplicatedRootId) == nullptr, "删除后对象不应存在"); } void MetaCoreTestSelectionStateMachine() { MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene; MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; const MetaCore::MetaCoreId objectAId = scene.CreateGameObject("A").Id; const MetaCore::MetaCoreId objectBId = scene.CreateGameObject("B").Id; const MetaCore::MetaCoreId objectCId = scene.CreateGameObject("C").Id; MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); editorContext.SelectOnly(objectAId); MetaCoreExpect(editorContext.GetActiveObjectId() == objectAId, "单选后 Active 应为 A"); MetaCoreExpect(editorContext.GetSelectedObjectIds().size() == 1, "单选后应仅包含一个对象"); editorContext.ToggleSelection(objectBId); MetaCoreExpect(editorContext.IsObjectSelected(objectAId), "Toggle 后应保留已选对象 A"); MetaCoreExpect(editorContext.IsObjectSelected(objectBId), "Toggle 后应包含对象 B"); MetaCoreExpect(editorContext.GetActiveObjectId() == objectBId, "Toggle 新增对象后 Active 应为 B"); editorContext.SetSelectionAnchorId(objectAId); editorContext.SelectRangeByOrderedIds(scene.BuildHierarchyPreorder(), objectCId, false); MetaCoreExpect(editorContext.IsObjectSelected(objectAId), "范围选择应包含起点 A"); MetaCoreExpect(editorContext.IsObjectSelected(objectBId), "范围选择应包含中间对象 B"); MetaCoreExpect(editorContext.IsObjectSelected(objectCId), "范围选择应包含终点 C"); MetaCoreExpect(editorContext.GetActiveObjectId() == objectCId, "范围选择后 Active 应为终点 C"); } void MetaCoreTestUndoRedo() { MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); const std::size_t beforeCreateCount = scene.GetGameObjects().size(); const bool created = editorContext.ExecuteSnapshotCommand("创建对象", [&]() { MetaCore::MetaCoreGameObject& object = scene.CreateGameObject("UndoRedoObject"); editorContext.SelectOnly(object.Id); return true; }); MetaCoreExpect(created, "创建快照命令应执行成功"); MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount + 1, "执行命令后对象数量应增加"); MetaCoreExpect(editorContext.GetCommandService().CanUndo(), "执行命令后应可撤销"); const bool undoSucceeded = editorContext.UndoCommand(); MetaCoreExpect(undoSucceeded, "Undo 应成功"); MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount, "Undo 后对象数量应恢复"); MetaCoreExpect(editorContext.GetCommandService().CanRedo(), "Undo 后应可重做"); const bool redoSucceeded = editorContext.RedoCommand(); MetaCoreExpect(redoSucceeded, "Redo 应成功"); MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount + 1, "Redo 后对象数量应再次增加"); } void MetaCoreTestBuiltinModuleComposition() { MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); auto editorViewsModule = MetaCore::MetaCoreCreateBuiltinEditorViewsModule(); coreServicesModule->Startup(moduleRegistry); editorViewsModule->Startup(moduleRegistry); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ReflectionRegistry"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 PackageService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 AssetDatabaseService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ImportPipelineService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 CookService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ScenePersistenceService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 SelectionService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ClipboardService"); MetaCoreExpect(!moduleRegistry.GetPanelProviders().empty(), "应注册至少一个面板提供者"); editorViewsModule->Shutdown(moduleRegistry); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); } void MetaCoreTestScenePackageProjectStartupLoad() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreScenePackageSmoke"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Scenes"); MetaCore::MetaCoreSceneDocument sceneDocument; sceneDocument.Name = "Main"; const MetaCore::MetaCoreScene sourceScene = MetaCore::MetaCoreCreateDefaultScene(); sceneDocument.GameObjects = sourceScene.GetGameObjects(); const std::filesystem::path scenePath = tempProjectRoot / "Scenes" / "Main.mcscene"; MetaCoreExpect(MetaCore::MetaCoreWriteScenePackage(scenePath, sceneDocument), "应能写入二进制场景包"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); MetaCoreExpect(projectFile.is_open(), "应能写入项目文件"); projectFile << "{\n" << " \"name\": \"SmokeProject\",\n" << " \"scenes\": [\n" << " \"Scenes/Main.mcscene\"\n" << " ],\n" << " \"startup_scene\": \"Scenes/Main.mcscene\",\n" << " \"version\": \"0.1.0\"\n" << "}\n"; } const auto loadedSceneDocument = MetaCore::MetaCoreLoadStartupSceneDocument(tempProjectRoot / "MetaCore.project.json"); MetaCoreExpect(loadedSceneDocument.has_value(), "应能从项目文件加载 startup scene"); MetaCoreExpect(loadedSceneDocument->GameObjects.size() == sourceScene.GetGameObjects().size(), "startup scene 对象数量应一致"); MetaCoreExpect(!loadedSceneDocument->GameObjects.empty(), "startup scene 应包含对象"); MetaCoreExpect(loadedSceneDocument->GameObjects.front().Name == "Main Camera", "startup scene 首个对象应为 Main Camera"); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestComponentRegistryDescriptors() { MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); coreServicesModule->Startup(moduleRegistry); const auto componentRegistry = moduleRegistry.ResolveService(); MetaCoreExpect(componentRegistry != nullptr, "应解析到 ComponentTypeRegistry"); const auto* transformDescriptor = componentRegistry->FindDescriptor("Transform"); const auto* cameraDescriptor = componentRegistry->FindDescriptor("Camera"); const auto* lightDescriptor = componentRegistry->FindDescriptor("Light"); const auto* meshRendererDescriptor = componentRegistry->FindDescriptor("MeshRenderer"); MetaCoreExpect(transformDescriptor != nullptr, "Transform descriptor 应存在"); MetaCoreExpect(cameraDescriptor != nullptr, "Camera descriptor 应存在"); MetaCoreExpect(lightDescriptor != nullptr, "Light descriptor 应存在"); MetaCoreExpect(meshRendererDescriptor != nullptr, "MeshRenderer descriptor 应存在"); MetaCoreExpect(!transformDescriptor->DrawInspector, "Transform 应继续由独立 InspectorDrawer 处理"); MetaCoreExpect(static_cast(cameraDescriptor->DrawInspector), "Camera descriptor 应提供 drawer"); MetaCoreExpect(static_cast(lightDescriptor->DrawInspector), "Light descriptor 应提供 drawer"); MetaCoreExpect(static_cast(meshRendererDescriptor->DrawInspector), "MeshRenderer descriptor 应提供 drawer"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); } void MetaCoreTestScenePersistenceRoundTrip() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreSceneRoundTripProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Library"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); projectFile << "{\n" << " \"name\": \"RoundTripProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); coreServicesModule->Startup(moduleRegistry); MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); const auto scenePersistenceService = moduleRegistry.ResolveService(); MetaCoreExpect(scenePersistenceService != nullptr, "应解析到 ScenePersistenceService"); MetaCore::MetaCoreId cubeId = 0; for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) { if (object.Name == "Cube") { cubeId = object.Id; break; } } MetaCoreExpect(cubeId != 0, "默认场景应包含 Cube"); editorContext.SelectOnly(cubeId); MetaCoreExpect(scenePersistenceService->SaveSceneAs(editorContext, std::filesystem::path("Scenes") / "Main.mcscene"), "应能保存场景"); MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "保存后场景不应为 dirty"); const bool renamed = editorContext.ExecuteSnapshotCommand("重命名对象", [&]() { return scene.RenameGameObject(cubeId, "RoundTripCube"); }); MetaCoreExpect(renamed, "应能修改场景对象"); MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "修改后场景应变为 dirty"); MetaCoreExpect(scenePersistenceService->SaveCurrentScene(editorContext), "应能再次保存当前场景"); MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "再次保存后 dirty 应清除"); scene.RestoreSnapshot(MetaCore::MetaCoreSceneSnapshot{}); editorContext.ClearSelection(); MetaCoreExpect(scene.GetGameObjects().empty(), "清空快照后场景应为空"); MetaCoreExpect(scenePersistenceService->LoadScene(editorContext, std::filesystem::path("Scenes") / "Main.mcscene"), "应能重新加载场景"); MetaCoreExpect(scene.GetGameObjects().size() == 6, "重新加载后对象数量应恢复"); MetaCoreExpect(editorContext.GetSelectedObjectId() != 0, "重新加载后应恢复选中对象"); bool foundRenamedCube = false; for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) { if (object.Name == "RoundTripCube") { foundRenamedCube = true; break; } } MetaCoreExpect(foundRenamedCube, "重新加载后应保留保存过的对象名称"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestImportPipelineAndCook() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreImportCookProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Library"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); projectFile << "{\n" << " \"name\": \"ImportProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } { std::ofstream rawAsset(tempProjectRoot / "Assets" / "Texture.png", std::ios::binary | std::ios::trunc); rawAsset << "PNGDATA_V1"; } _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); coreServicesModule->Startup(moduleRegistry); const auto assetDatabase = moduleRegistry.ResolveService(); const auto cookService = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(cookService != nullptr, "应解析到 CookService"); const std::filesystem::path metaPath = tempProjectRoot / "Assets" / "Texture.png.mcmeta"; const std::filesystem::path packagePath = tempProjectRoot / "Assets" / "Texture.png.mcasset"; MetaCoreExpect(std::filesystem::exists(metaPath), "导入后应生成 mcmeta"); MetaCoreExpect(std::filesystem::exists(packagePath), "导入后应生成 mcasset"); const auto sourceRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Texture.png"); MetaCoreExpect(sourceRecord.has_value(), "应能找到原始资源记录"); MetaCoreExpect(sourceRecord->Guid.IsValid(), "原始资源应有稳定 GUID"); MetaCoreExpect(sourceRecord->PackagePath == std::filesystem::path("Assets") / "Texture.png.mcasset", "原始资源应关联包路径"); const std::filesystem::path cookedPath = tempProjectRoot / cookService->GetCookedPathForAsset(sourceRecord->Guid); MetaCoreExpect(std::filesystem::exists(cookedPath), "导入后应生成 cooked 结果"); const MetaCore::MetaCoreAssetGuid initialGuid = sourceRecord->Guid; { std::ofstream rawAsset(tempProjectRoot / "Assets" / "Texture.png", std::ios::binary | std::ios::trunc); rawAsset << "PNGDATA_V2"; } MetaCoreExpect(assetDatabase->Refresh(), "修改原始资源后应能重新刷新资产数据库"); const auto refreshedSourceRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Texture.png"); MetaCoreExpect(refreshedSourceRecord.has_value(), "刷新后应仍能找到原始资源记录"); MetaCoreExpect(refreshedSourceRecord->Guid == initialGuid, "重新导入后 GUID 应保持稳定"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestProjectDescriptorAndGltfImporterSkeleton() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreProjectGltfImporterProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "ConfigRuntime"); std::filesystem::create_directories(tempProjectRoot / "UiDocuments"); std::filesystem::create_directories(tempProjectRoot / "BuildOutput"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); projectFile << "{\n" << " \"name\": \"ImporterProject\",\n" << " \"version\": \"0.2.0\",\n" << " \"runtime_directory\": \"ConfigRuntime\",\n" << " \"ui_directory\": \"UiDocuments\",\n" << " \"build_directory\": \"BuildOutput\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } { std::ofstream gltfFile(tempProjectRoot / "Assets" / "Valve.gltf", std::ios::trunc); gltfFile << "{\n" << " \"meshes\": [{\"name\": \"ValveMesh\"}],\n" << " \"materials\": [{\"name\": \"ValveMaterial\"}],\n" << " \"images\": [{\"uri\": \"Textures/ValveBaseColor.png\"}]\n" << "}\n"; } { std::ofstream glbFile(tempProjectRoot / "Assets" / "Tank.glb", std::ios::binary | std::ios::trunc); glbFile << "glTF_BINARY_PLACEHOLDER"; } _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); coreServicesModule->Startup(moduleRegistry); const auto assetDatabase = moduleRegistry.ResolveService(); const auto packageService = moduleRegistry.ResolveService(); const auto reflectionRegistry = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(packageService != nullptr, "应解析到 PackageService"); MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry"); const auto& project = assetDatabase->GetProjectDescriptor(); MetaCoreExpect(project.RuntimePath.filename() == "ConfigRuntime", "项目应读取 runtime_directory"); MetaCoreExpect(project.UiPath.filename() == "UiDocuments", "项目应读取 ui_directory"); MetaCoreExpect(project.BuildPath.filename() == "BuildOutput", "项目应读取 build_directory"); const auto gltfRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Valve.gltf"); MetaCoreExpect(gltfRecord.has_value(), "应能找到 glTF 资源记录"); MetaCoreExpect(gltfRecord->ImporterId == "GltfModelImporter", "glTF 应使用 GltfModelImporter"); MetaCoreExpect(gltfRecord->Type == "model", "glTF 资源类型应为 model"); const auto gltfPackage = packageService->ReadPackage(tempProjectRoot / gltfRecord->PackagePath); MetaCoreExpect(gltfPackage.has_value(), "glTF 应生成包文件"); MetaCore::MetaCoreImportedGltfAssetDocument gltfDocument; MetaCoreExpect( !gltfPackage->PayloadSections.empty() && MetaCore::MetaCoreDeserializeFromBytes( gltfPackage->PayloadSections.front(), gltfDocument, reflectionRegistry->GetTypeRegistry() ), "glTF 导入包应能反序列化为专用导入文档" ); MetaCoreExpect(gltfDocument.SourceFormat == "gltf", "glTF 导入文档应标记源格式"); MetaCoreExpect(!gltfDocument.Nodes.empty() && gltfDocument.Nodes.front().Name == "Valve", "glTF 骨架文档应写入默认根节点名"); MetaCoreExpect(!gltfDocument.Meshes.empty() && gltfDocument.Meshes.front().Name == "ValveMesh", "glTF 导入文档应提取 mesh 名称"); MetaCoreExpect(gltfDocument.Meshes.front().PrimitiveCount == 1, "glTF mesh 骨架应写入 primitive 数"); MetaCoreExpect(!gltfDocument.Materials.empty() && gltfDocument.Materials.front().Name == "ValveMaterial", "glTF 导入文档应提取材质名称"); MetaCoreExpect(!gltfDocument.Textures.empty(), "glTF 导入文档应记录引用贴图"); MetaCoreExpect(gltfDocument.Textures.front().SourcePath == std::filesystem::path("Textures") / "ValveBaseColor.png", "glTF 导入文档应保存贴图路径"); MetaCoreExpect(gltfDocument.GeneratedMeshAssets.size() == gltfDocument.Meshes.size(), "glTF 导入文档应生成 mesh 资源骨架"); MetaCoreExpect(gltfDocument.GeneratedMaterialAssets.size() == gltfDocument.Materials.size(), "glTF 导入文档应生成材质资源骨架"); MetaCoreExpect(gltfDocument.GeneratedTextureAssets.size() == gltfDocument.Textures.size(), "glTF 导入文档应生成纹理资源骨架"); MetaCoreExpect(gltfDocument.GeneratedMeshAssets.front().AssetGuid.IsValid(), "生成的 mesh 资源应有 GUID"); MetaCoreExpect(gltfDocument.GeneratedMaterialAssets.front().AssetGuid.IsValid(), "生成的材质资源应有 GUID"); MetaCoreExpect(gltfDocument.GeneratedTextureAssets.front().AssetGuid.IsValid(), "生成的纹理资源应有 GUID"); MetaCoreExpect(gltfDocument.GeneratedMaterialAssets.front().BaseColorTexture.IsValid(), "材质资源应引用生成的贴图资源"); const auto glbRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Tank.glb"); MetaCoreExpect(glbRecord.has_value(), "应能找到 glb 资源记录"); MetaCoreExpect(glbRecord->ImporterId == "GltfModelImporter", "glb 应使用 GltfModelImporter"); const auto glbPackage = packageService->ReadPackage(tempProjectRoot / glbRecord->PackagePath); MetaCoreExpect(glbPackage.has_value(), "glb 应生成包文件"); MetaCore::MetaCoreImportedGltfAssetDocument glbDocument; MetaCoreExpect( !glbPackage->PayloadSections.empty() && MetaCore::MetaCoreDeserializeFromBytes( glbPackage->PayloadSections.front(), glbDocument, reflectionRegistry->GetTypeRegistry() ), "glb 导入包应能反序列化为专用导入文档" ); MetaCoreExpect(glbDocument.SourceFormat == "glb", "glb 导入文档应标记源格式"); MetaCoreExpect(!glbDocument.Meshes.empty() && glbDocument.Meshes.front().Name == "Tank", "glb 骨架文档应生成默认 mesh 描述"); MetaCoreExpect(!glbDocument.GeneratedMeshAssets.empty(), "glb 导入文档应生成默认 mesh 资源骨架"); MetaCoreExpect(!glbDocument.GeneratedMaterialAssets.empty(), "glb 导入文档应生成默认材质资源骨架"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestInstantiateImportedModelAssetIntoScene() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreInstantiateModelProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Library"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); projectFile << "{\n" << " \"name\": \"InstantiateProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } { std::ofstream gltfFile(tempProjectRoot / "Assets" / "Pump.gltf", std::ios::trunc); gltfFile << "{\n" << " \"meshes\": [{\"name\": \"PumpMesh\"}],\n" << " \"materials\": [{\"name\": \"PumpMaterial\"}],\n" << " \"images\": [{\"uri\": \"Textures/PumpBaseColor.png\"}]\n" << "}\n"; } _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene; MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); coreServicesModule->Startup(moduleRegistry); MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); const auto assetDatabase = moduleRegistry.ResolveService(); const auto sceneEditingService = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(sceneEditingService != nullptr, "应解析到 SceneEditingService"); const auto modelRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Pump.gltf"); MetaCoreExpect(modelRecord.has_value(), "应能找到导入后的模型资源"); const auto instantiatedRootId = sceneEditingService->InstantiateModelAsset(editorContext, modelRecord->Guid, std::nullopt); MetaCoreExpect(instantiatedRootId.has_value(), "应能将导入模型实例化到场景"); const MetaCore::MetaCoreGameObject* instantiatedRoot = scene.FindGameObject(*instantiatedRootId); MetaCoreExpect(instantiatedRoot != nullptr, "实例化后应能找到根对象"); MetaCoreExpect(instantiatedRoot->Name == "Pump", "实例化根对象名称应来自导入节点"); MetaCoreExpect(instantiatedRoot->MeshRenderer.has_value(), "导入模型实例应带 MeshRenderer"); MetaCoreExpect(instantiatedRoot->MeshRenderer->MeshSource == MetaCore::MetaCoreMeshSourceKind::Asset, "导入模型实例应使用 Asset MeshSource"); MetaCoreExpect(instantiatedRoot->MeshRenderer->MeshAssetGuid.IsValid(), "导入模型实例应绑定 MeshAssetGuid"); MetaCoreExpect(!instantiatedRoot->MeshRenderer->MaterialAssetGuids.empty(), "导入模型实例应绑定材质资源"); MetaCoreExpect(editorContext.GetActiveObjectId() == *instantiatedRootId, "实例化后应选中新对象"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestBootstrapStartupSceneCreation() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreBootstrapSceneProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Library"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); projectFile << "{\n" << " \"name\": \"BootstrapProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); coreServicesModule->Startup(moduleRegistry); MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); const auto scenePersistenceService = moduleRegistry.ResolveService(); const auto assetDatabaseService = moduleRegistry.ResolveService(); MetaCoreExpect(scenePersistenceService != nullptr, "应解析到 ScenePersistenceService"); MetaCoreExpect(assetDatabaseService != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(!scenePersistenceService->LoadStartupScene(editorContext), "空项目初始时不应加载到启动场景"); const std::filesystem::path bootstrapScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; MetaCoreExpect(scenePersistenceService->SaveSceneAs(editorContext, bootstrapScenePath), "应能为默认场景创建二进制启动场景"); MetaCoreExpect(assetDatabaseService->SetStartupScenePath(bootstrapScenePath), "应能设置 startup scene"); MetaCoreExpect(assetDatabaseService->Refresh(), "刷新资产数据库应成功"); MetaCoreExpect(std::filesystem::exists(tempProjectRoot / bootstrapScenePath), "应生成 Main.mcscene"); MetaCoreExpect(scenePersistenceService->LoadStartupScene(editorContext), "设置后应能加载 startup scene"); MetaCoreExpect(scenePersistenceService->GetCurrentScenePath() == bootstrapScenePath, "startup scene 路径应正确"); MetaCoreExpect(scene.GetGameObjects().size() == 6, "bootstrap scene 应保存默认场景内容"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestPrefabWorkflow() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCorePrefabWorkflowProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets" / "Prefabs"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Library"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); projectFile << "{\n" << " \"name\": \"PrefabProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene; MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); coreServicesModule->Startup(moduleRegistry); MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); MetaCore::MetaCoreGameObject& root = scene.CreateGameObject("PrefabRoot"); root.MeshRenderer = MetaCore::MetaCoreMeshRendererComponent{}; root.Transform.Position = glm::vec3(1.0F, 2.0F, 3.0F); const MetaCore::MetaCoreId rootId = root.Id; MetaCore::MetaCoreGameObject& child = scene.CreateGameObject("PrefabChild", rootId); child.Light = MetaCore::MetaCoreLightComponent{}; editorContext.SelectOnly(rootId); const auto prefabService = moduleRegistry.ResolveService(); const auto assetDatabaseService = moduleRegistry.ResolveService(); const auto packageService = moduleRegistry.ResolveService(); const auto reflectionRegistry = moduleRegistry.ResolveService(); MetaCoreExpect(prefabService != nullptr, "应解析到 PrefabService"); MetaCoreExpect(prefabService->SupportsPrefabWorkflows(), "PrefabService 应支持工作流"); MetaCoreExpect(assetDatabaseService != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(packageService != nullptr, "应解析到 PackageService"); MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry"); const std::filesystem::path prefabPath = std::filesystem::path("Assets") / "Prefabs" / "PrefabRoot.mcprefab"; const auto createdPrefabPath = prefabService->CreatePrefabFromSelection(editorContext, prefabPath); MetaCoreExpect(createdPrefabPath.has_value(), "应能从选中对象创建 prefab"); MetaCoreExpect(std::filesystem::exists(tempProjectRoot / prefabPath), "应写出 mcprefab 文件"); MetaCoreExpect(assetDatabaseService->Refresh(), "创建 prefab 后应能刷新资产数据库"); const auto prefabRecord = assetDatabaseService->FindAssetByRelativePath(prefabPath); MetaCoreExpect(prefabRecord.has_value(), "应能找到 prefab 资产记录"); MetaCoreExpect(prefabRecord->Type == "prefab", "prefab 资产类型应正确"); const auto instantiatedRootId = prefabService->InstantiatePrefab(editorContext, prefabRecord->Guid, std::nullopt); MetaCoreExpect(instantiatedRootId.has_value(), "应能实例化 prefab"); MetaCore::MetaCoreGameObject* instantiatedRoot = scene.FindGameObject(*instantiatedRootId); MetaCoreExpect(instantiatedRoot != nullptr, "实例化后应能找到根对象"); MetaCoreExpect(instantiatedRoot->PrefabInstance.has_value(), "实例根应带有 prefab 元数据"); instantiatedRoot->Transform.Position = glm::vec3(8.0F, 9.0F, 10.0F); editorContext.SelectOnly(instantiatedRoot->Id); MetaCoreExpect(prefabService->ApplySelectedPrefabInstance(editorContext), "应能应用 prefab 实例"); const auto prefabPackage = packageService->ReadPackage(tempProjectRoot / prefabPath); MetaCoreExpect(prefabPackage.has_value(), "Apply 后应能读取 prefab 包"); MetaCore::MetaCorePrefabDocument appliedPrefabDocument; MetaCoreExpect( !prefabPackage->PayloadSections.empty() && MetaCore::MetaCoreDeserializeFromBytes( prefabPackage->PayloadSections.front(), appliedPrefabDocument, reflectionRegistry->GetTypeRegistry() ), "Apply 后 prefab payload 应可反序列化" ); MetaCoreExpect(!appliedPrefabDocument.GameObjects.empty(), "Apply 后 prefab 应保留对象数据"); MetaCoreExpectVec3Near( appliedPrefabDocument.GameObjects.front().Transform.Position, glm::vec3(8.0F, 9.0F, 10.0F), "Apply 后 prefab 资源应写入修改过的 transform" ); const auto secondInstanceRootId = prefabService->InstantiatePrefab(editorContext, prefabRecord->Guid, std::nullopt); MetaCoreExpect(secondInstanceRootId.has_value(), "应用后应仍能再次实例化 prefab"); MetaCore::MetaCoreGameObject* secondInstanceRoot = scene.FindGameObject(*secondInstanceRootId); MetaCoreExpect(secondInstanceRoot != nullptr, "第二个实例应存在"); MetaCoreExpectVec3Near(secondInstanceRoot->Transform.Position, glm::vec3(8.0F, 9.0F, 10.0F), "Apply 后新实例应继承修改过的 transform"); secondInstanceRoot->Transform.Position = glm::vec3(-1.0F, -2.0F, -3.0F); editorContext.SelectOnly(secondInstanceRoot->Id); MetaCoreExpect(prefabService->RevertSelectedPrefabInstance(editorContext), "应能还原 prefab 实例"); MetaCore::MetaCoreGameObject* revertedRoot = scene.FindGameObject(editorContext.GetActiveObjectId()); MetaCoreExpect(revertedRoot != nullptr, "还原后应重新选中实例根"); MetaCoreExpectVec3Near(revertedRoot->Transform.Position, glm::vec3(8.0F, 9.0F, 10.0F), "Revert 后实例应恢复为 prefab 内容"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestComponentRegistryOperations() { MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene; MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); coreServicesModule->Startup(moduleRegistry); MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); MetaCore::MetaCoreGameObject& object = scene.CreateGameObject("RegistryObject"); editorContext.SelectOnly(object.Id); const auto componentRegistry = moduleRegistry.ResolveService(); MetaCoreExpect(componentRegistry != nullptr, "应解析到 ComponentTypeRegistry"); const auto* cameraDescriptor = componentRegistry->FindDescriptor("Camera"); const auto* lightDescriptor = componentRegistry->FindDescriptor("Light"); MetaCoreExpect(cameraDescriptor != nullptr, "应能找到 Camera 描述符"); MetaCoreExpect(lightDescriptor != nullptr, "应能找到 Light 描述符"); MetaCoreExpect(!cameraDescriptor->HasComponent(object), "初始时不应有 Camera"); const bool addedCamera = editorContext.ExecuteSnapshotCommand("添加 Camera", [&]() { return cameraDescriptor->AddComponent(object); }); MetaCoreExpect(addedCamera, "应能通过注册表添加 Camera"); MetaCoreExpect(object.Camera.has_value(), "添加后对象应拥有 Camera"); const bool addedLight = editorContext.ExecuteSnapshotCommand("添加 Light", [&]() { return lightDescriptor->AddComponent(object); }); MetaCoreExpect(addedLight, "应能通过注册表添加 Light"); MetaCoreExpect(object.Light.has_value(), "添加后对象应拥有 Light"); const bool removedCamera = editorContext.ExecuteSnapshotCommand("移除 Camera", [&]() { return cameraDescriptor->RemoveComponent(object); }); MetaCoreExpect(removedCamera, "应能通过注册表移除 Camera"); MetaCoreExpect(!object.Camera.has_value(), "移除后对象不应再拥有 Camera"); const bool readdedCamera = editorContext.ExecuteSnapshotCommand("重新添加 Camera", [&]() { return cameraDescriptor->AddComponent(object); }); MetaCoreExpect(readdedCamera, "应能重新添加 Camera"); object.Camera->FieldOfViewDegrees = 77.0F; object.Camera->NearClip = 0.25F; object.Camera->FarClip = 250.0F; object.Camera->IsPrimary = true; MetaCoreExpect(componentRegistry->CopyComponent("Camera", object), "应能复制 Camera 组件值"); MetaCore::MetaCoreGameObject pasteTarget = scene.CreateGameObject("PasteTarget"); MetaCore::MetaCoreGameObject secondPasteTarget = scene.CreateGameObject("SecondPasteTarget"); MetaCoreExpect(!pasteTarget.Camera.has_value(), "新对象初始不应拥有 Camera"); MetaCoreExpect(!secondPasteTarget.Camera.has_value(), "第二个新对象初始不应拥有 Camera"); MetaCoreExpect(componentRegistry->CanPasteComponent("Camera"), "复制后应可粘贴 Camera"); MetaCoreExpect(componentRegistry->PasteComponent("Camera", pasteTarget), "应能粘贴 Camera 组件值"); MetaCoreExpect(componentRegistry->PasteComponent("Camera", secondPasteTarget), "应能向第二个对象粘贴 Camera 组件值"); MetaCoreExpect(pasteTarget.Camera.has_value(), "粘贴后目标对象应拥有 Camera"); MetaCoreExpect(secondPasteTarget.Camera.has_value(), "第二个目标对象粘贴后应拥有 Camera"); MetaCoreExpect(std::abs(pasteTarget.Camera->FieldOfViewDegrees - 77.0F) <= 0.0001F, "粘贴后 FOV 应一致"); MetaCoreExpect(std::abs(pasteTarget.Camera->NearClip - 0.25F) <= 0.0001F, "粘贴后 NearClip 应一致"); MetaCoreExpect(std::abs(pasteTarget.Camera->FarClip - 250.0F) <= 0.0001F, "粘贴后 FarClip 应一致"); MetaCoreExpect(pasteTarget.Camera->IsPrimary, "粘贴后 IsPrimary 应一致"); MetaCoreExpect(std::abs(secondPasteTarget.Camera->FieldOfViewDegrees - 77.0F) <= 0.0001F, "第二个对象粘贴后 FOV 应一致"); MetaCoreExpect(cameraDescriptor->ResetComponent != nullptr, "Camera descriptor 应提供 Reset"); pasteTarget.Camera->FieldOfViewDegrees = 10.0F; MetaCoreExpect(cameraDescriptor->ResetComponent(pasteTarget), "应能重置 Camera"); MetaCoreExpect(std::abs(pasteTarget.Camera->FieldOfViewDegrees - 60.0F) <= 0.0001F, "重置后 Camera 应恢复默认值"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); } void MetaCoreTestRuntimeDataTypeSerialization() { MetaCore::MetaCoreTypeRegistry registry; MetaCoreRegisterRuntimeDataGeneratedTypes(registry); MetaCore::MetaCoreRuntimeDataValue value; value.Type = MetaCore::MetaCoreRuntimeValueType::Vec3; value.Vec3Value = glm::vec3(1.0F, 2.0F, 3.0F); value.SourceTimestamp = 42; const auto serialized = MetaCore::MetaCoreSerializeToBytes(value, registry); MetaCoreExpect(serialized.has_value(), "RuntimeDataValue 应可序列化"); MetaCore::MetaCoreRuntimeDataValue roundTripValue; MetaCoreExpect( MetaCore::MetaCoreDeserializeFromBytes(*serialized, roundTripValue, registry), "RuntimeDataValue 应可反序列化" ); MetaCoreExpect(roundTripValue.Type == MetaCore::MetaCoreRuntimeValueType::Vec3, "RuntimeDataValue 类型应保留"); MetaCoreExpectVec3Near(roundTripValue.Vec3Value, glm::vec3(1.0F, 2.0F, 3.0F), "RuntimeDataValue Vec3 应保留"); MetaCoreExpect(roundTripValue.SourceTimestamp == 42, "RuntimeDataValue 时间戳应保留"); } void MetaCoreTestRuntimeDataProjectDocumentSerialization() { MetaCore::MetaCoreTypeRegistry registry; MetaCoreRegisterRuntimeDataGeneratedTypes(registry); MetaCore::MetaCoreRuntimeDataSourcesDocument sourcesDocument; sourcesDocument.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ "mock-source", "mock", "Mock Source", {MetaCore::MetaCoreDataSourceSetting{"seed", "demo"}}, true, 1000 }); sourcesDocument.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ "pump.position", "mock-source", "pump.position", MetaCore::MetaCoreRuntimeValueType::Vec3 }); const auto serialized = MetaCore::MetaCoreSerializeToBytes(sourcesDocument, registry); MetaCoreExpect(serialized.has_value(), "RuntimeDataSourcesDocument 应可序列化"); MetaCore::MetaCoreRuntimeDataSourcesDocument roundTripDocument; MetaCoreExpect( MetaCore::MetaCoreDeserializeFromBytes(*serialized, roundTripDocument, registry), "RuntimeDataSourcesDocument 应可反序列化" ); MetaCoreExpect(roundTripDocument.Sources.size() == 1, "RuntimeDataSourcesDocument 应保留 source"); MetaCoreExpect(roundTripDocument.DataPoints.size() == 1, "RuntimeDataSourcesDocument 应保留 data point"); MetaCoreExpect(roundTripDocument.Sources.front().Id == "mock-source", "RuntimeDataSourcesDocument source id 应保留"); } void MetaCoreTestMeshRendererResourceSerialization() { MetaCore::MetaCoreTypeRegistry registry; MetaCoreRegisterFoundationGeneratedTypes(registry); MetaCoreRegisterSceneGeneratedTypes(registry); MetaCore::MetaCoreMeshRendererComponent component; component.MeshSource = MetaCore::MetaCoreMeshSourceKind::Asset; component.MeshAssetGuid = MetaCore::MetaCoreAssetGuid::Generate(); component.MaterialAssetGuids.push_back(MetaCore::MetaCoreAssetGuid::Generate()); component.MaterialAssetGuids.push_back(MetaCore::MetaCoreAssetGuid::Generate()); component.Visible = false; const auto serialized = MetaCore::MetaCoreSerializeToBytes(component, registry); MetaCoreExpect(serialized.has_value(), "MeshRenderer 资源化结构应可序列化"); MetaCore::MetaCoreMeshRendererComponent roundTripComponent; MetaCoreExpect( MetaCore::MetaCoreDeserializeFromBytes(*serialized, roundTripComponent, registry), "MeshRenderer 资源化结构应可反序列化" ); MetaCoreExpect(roundTripComponent.MeshSource == MetaCore::MetaCoreMeshSourceKind::Asset, "MeshSource 应保留"); MetaCoreExpect(roundTripComponent.MeshAssetGuid == component.MeshAssetGuid, "MeshAssetGuid 应保留"); MetaCoreExpect(roundTripComponent.MaterialAssetGuids.size() == 2, "材质槽引用数量应保留"); MetaCoreExpect(roundTripComponent.MaterialAssetGuids.front() == component.MaterialAssetGuids.front(), "第一个材质引用应保留"); MetaCoreExpect(!roundTripComponent.Visible, "Visible 应保留"); } void MetaCoreTestRuntimeDataBinaryDocumentIo() { MetaCore::MetaCoreTypeRegistry registry; MetaCoreRegisterRuntimeDataGeneratedTypes(registry); const std::filesystem::path tempDirectory = std::filesystem::temp_directory_path() / "MetaCoreRuntimeDataIo"; std::filesystem::remove_all(tempDirectory); std::filesystem::create_directories(tempDirectory); const std::filesystem::path sourcesPath = tempDirectory / "DataSources.mcruntime"; const std::filesystem::path bindingsPath = tempDirectory / "Bindings.mcruntime"; MetaCore::MetaCoreRuntimeDataSourcesDocument sourcesDocument; sourcesDocument.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ "mock-source", "mock", "Mock Source", {}, true, 1000 }); sourcesDocument.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ "cube.position", "mock-source", "cube.position", MetaCore::MetaCoreRuntimeValueType::Vec3 }); MetaCore::MetaCoreRuntimeBindingsDocument bindingsDocument; bindingsDocument.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ "binding.cube.position", "cube.position", 3, MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }); MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeDataSourcesDocument(sourcesPath, sourcesDocument, registry), "RuntimeDataSourcesDocument 应可写出二进制文件" ); MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeBindingsDocument(bindingsPath, bindingsDocument, registry), "RuntimeBindingsDocument 应可写出二进制文件" ); const auto loadedSources = MetaCore::MetaCoreReadRuntimeDataSourcesDocument(sourcesPath, registry); const auto loadedBindings = MetaCore::MetaCoreReadRuntimeBindingsDocument(bindingsPath, registry); MetaCoreExpect(loadedSources.has_value(), "RuntimeDataSourcesDocument 应可读回二进制文件"); MetaCoreExpect(loadedBindings.has_value(), "RuntimeBindingsDocument 应可读回二进制文件"); MetaCoreExpect(loadedSources->Sources.size() == 1, "读回的 RuntimeDataSourcesDocument 应保留 source"); MetaCoreExpect(loadedBindings->Bindings.size() == 1, "读回的 RuntimeBindingsDocument 应保留 binding"); std::filesystem::remove_all(tempDirectory); } void MetaCoreTestRuntimeProjectDocumentIo() { MetaCore::MetaCoreTypeRegistry registry; MetaCore::MetaCoreRegisterRuntimeDataGeneratedTypes(registry); const std::filesystem::path tempPath = std::filesystem::temp_directory_path() / "MetaCoreRuntimeProjectDocument.mcruntimecfg"; MetaCore::MetaCoreRuntimeProjectDocument writtenDocument; writtenDocument.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; writtenDocument.DataSourcesPath = std::filesystem::path("Runtime") / "DataSources.mcruntime"; writtenDocument.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; writtenDocument.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; MetaCoreExpect(MetaCore::MetaCoreWriteRuntimeProjectDocument(tempPath, writtenDocument, registry), "应能写入 RuntimeProject 文档"); const auto loadedDocument = MetaCore::MetaCoreReadRuntimeProjectDocument(tempPath, registry); MetaCoreExpect(loadedDocument.has_value(), "应能读回 RuntimeProject 文档"); MetaCoreExpect(loadedDocument->StartupScenePath == writtenDocument.StartupScenePath, "StartupScenePath 应一致"); MetaCoreExpect(loadedDocument->DiagnosticsPath == writtenDocument.DiagnosticsPath, "DiagnosticsPath 应一致"); std::filesystem::remove(tempPath); } void MetaCoreTestRuntimeDiagnosticsSnapshotIo() { MetaCore::MetaCoreTypeRegistry registry; MetaCore::MetaCoreRegisterRuntimeDataGeneratedTypes(registry); const std::filesystem::path tempPath = std::filesystem::temp_directory_path() / "MetaCoreRuntimeDiagnostics.mcruntimestate"; MetaCore::MetaCoreRuntimeDiagnosticsSnapshot writtenSnapshot; writtenSnapshot.SourceStatuses.push_back(MetaCore::MetaCoreRuntimeDataSourceStatus{ "tcp-source", MetaCore::MetaCoreRuntimeDataSourceState::Connected, 10, 20, {} }); writtenSnapshot.BindingStatuses.push_back(MetaCore::MetaCoreRuntimeBindingStatus{ "binding.cube.position", false, true, 100, 1000, "stale" }); writtenSnapshot.HasFaults = true; MetaCoreExpect(MetaCore::MetaCoreWriteRuntimeDiagnosticsSnapshot(tempPath, writtenSnapshot, registry), "应能写入 Runtime diagnostics"); const auto loadedSnapshot = MetaCore::MetaCoreReadRuntimeDiagnosticsSnapshot(tempPath, registry); MetaCoreExpect(loadedSnapshot.has_value(), "应能读回 Runtime diagnostics"); MetaCoreExpect(loadedSnapshot->HasFaults, "Runtime diagnostics fault 状态应保留"); MetaCoreExpect(loadedSnapshot->SourceStatuses.size() == 1, "应包含一个 source status"); MetaCoreExpect(loadedSnapshot->BindingStatuses.size() == 1, "应包含一个 binding status"); std::filesystem::remove(tempPath); } void MetaCoreTestRuntimeDataDispatcherConstruction() { MetaCore::MetaCoreScene scene; scene.CreateGameObject("Pump"); MetaCore::MetaCoreRuntimeDataDispatcher dispatcher(scene); dispatcher.SetDataPointDefinitions({ MetaCore::MetaCoreDataPointDefinition{ "pump.position", "mock-source", "pump.position", MetaCore::MetaCoreRuntimeValueType::Vec3 } }); dispatcher.SetBindingDefinitions({ MetaCore::MetaCoreSceneBindingDefinition{ "binding.pump.position", "pump.position", scene.GetGameObjects().front().Id, MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue } }); MetaCoreExpect(dispatcher.GetBindingStatuses().size() == 1, "Dispatcher 应构建一个 binding status"); MetaCoreExpect(dispatcher.GetBindingStatuses().front().Healthy, "初始 binding status 应为 healthy"); } void MetaCoreTestRuntimeDataDispatcherAppliesUpdates() { MetaCore::MetaCoreScene scene; MetaCore::MetaCoreGameObject& cube = scene.CreateGameObject("Cube"); cube.MeshRenderer = MetaCore::MetaCoreMeshRendererComponent{}; MetaCore::MetaCoreRuntimeDataDispatcher dispatcher(scene); dispatcher.SetDataPointDefinitions({ MetaCore::MetaCoreDataPointDefinition{ "cube.position", "mock-source", "cube.position", MetaCore::MetaCoreRuntimeValueType::Vec3 }, MetaCore::MetaCoreDataPointDefinition{ "cube.visible", "mock-source", "cube.visible", MetaCore::MetaCoreRuntimeValueType::Bool } }); dispatcher.SetBindingDefinitions({ MetaCore::MetaCoreSceneBindingDefinition{ "binding.position", "cube.position", cube.Id, MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }, MetaCore::MetaCoreSceneBindingDefinition{ "binding.visible", "cube.visible", cube.Id, MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue } }); dispatcher.ApplyUpdates({ MetaCore::MetaCoreRuntimeDataUpdate{ "cube.position", MetaCore::MetaCoreRuntimeDataValue{ MetaCore::MetaCoreRuntimeValueType::Vec3, false, 0, 0.0, {}, glm::vec3(4.0F, 5.0F, 6.0F), 100, MetaCore::MetaCoreRuntimeDataQuality::Good }, 1 }, MetaCore::MetaCoreRuntimeDataUpdate{ "cube.visible", MetaCore::MetaCoreRuntimeDataValue{ MetaCore::MetaCoreRuntimeValueType::Bool, false, 0, 0.0, {}, glm::vec3(0.0F, 0.0F, 0.0F), 100, MetaCore::MetaCoreRuntimeDataQuality::Good }, 2 } }); MetaCoreExpectVec3Near(cube.Transform.Position, glm::vec3(4.0F, 5.0F, 6.0F), "Dispatcher 应更新 Transform.Position"); MetaCoreExpect(!cube.MeshRenderer->Visible, "Dispatcher 应更新 MeshRenderer.Visible"); } void MetaCoreTestMockRuntimeDataSourceAdapterEmitsUpdates() { MetaCore::MetaCoreMockRuntimeDataSourceAdapter adapter; MetaCoreExpect(adapter.Configure(MetaCore::MetaCoreDataSourceDefinition{ "mock-source", "mock", "Mock Source", {}, true, 1000 }), "Mock adapter 应能配置"); MetaCoreExpect(adapter.Connect(), "Mock adapter 应能连接"); adapter.Tick(0.25); const auto updates = adapter.PollUpdates(); MetaCoreExpect(updates.size() >= 3, "Mock adapter 应输出至少三条更新"); MetaCoreExpect(adapter.GetStatus().State == MetaCore::MetaCoreRuntimeDataSourceState::Connected, "Mock adapter 状态应为 connected"); } void MetaCoreTestRuntimeDataDispatcherMarksStaleBindings() { MetaCore::MetaCoreScene scene; MetaCore::MetaCoreGameObject& cube = scene.CreateGameObject("Cube"); cube.MeshRenderer = MetaCore::MetaCoreMeshRendererComponent{}; MetaCore::MetaCoreRuntimeDataDispatcher dispatcher(scene); dispatcher.SetDataPointDefinitions({ MetaCore::MetaCoreDataPointDefinition{ "cube.visible", "mock-source", "cube.visible", MetaCore::MetaCoreRuntimeValueType::Bool } }); dispatcher.SetBindingDefinitions({ MetaCore::MetaCoreSceneBindingDefinition{ "binding.visible", "cube.visible", cube.Id, MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue } }); dispatcher.ApplyUpdates({ MetaCore::MetaCoreRuntimeDataUpdate{ "cube.visible", MetaCore::MetaCoreRuntimeDataValue{ MetaCore::MetaCoreRuntimeValueType::Bool, true, 0, 0.0, {}, glm::vec3(0.0F, 0.0F, 0.0F), 100, MetaCore::MetaCoreRuntimeDataQuality::Good }, 1 } }); dispatcher.TickStaleness(1500); MetaCoreExpect(dispatcher.GetBindingStatuses().front().Stale, "Binding 应在超时后标记 stale"); MetaCoreExpect(dispatcher.HasBindingFaults(), "存在 stale binding 时应报告 faults"); } void MetaCoreTestMockRuntimeDataSourceAdapterDegradedState() { MetaCore::MetaCoreMockRuntimeDataSourceAdapter adapter; MetaCoreExpect(adapter.Configure(MetaCore::MetaCoreDataSourceDefinition{ "mock-source", "mock", "Mock Source", {}, true, 1000 }), "Mock adapter 应能配置"); MetaCoreExpect(adapter.Connect(), "Mock adapter 应能连接"); adapter.SetEmitUpdates(false); adapter.Tick(0.25); MetaCoreExpect(adapter.GetStatus().State == MetaCore::MetaCoreRuntimeDataSourceState::Degraded, "停止发包后应进入 degraded 状态"); MetaCoreExpect(adapter.PollUpdates().empty(), "Degraded 状态下不应继续输出更新"); } void MetaCoreTestFileReplayRuntimeDataSourceAdapterEmitsReplayFrames() { const std::filesystem::path tempDirectory = std::filesystem::temp_directory_path() / "MetaCoreRuntimeReplay"; std::filesystem::remove_all(tempDirectory); std::filesystem::create_directories(tempDirectory); const std::filesystem::path replayPath = tempDirectory / "RuntimeReplay.mcstream"; { std::ofstream replayFile(replayPath, std::ios::trunc); replayFile << "0.00 cube.visible bool true\n"; replayFile << "0.25 cube.position vec3 1.0 2.0 3.0\n"; } MetaCore::MetaCoreFileReplayRuntimeDataSourceAdapter adapter; MetaCoreExpect(adapter.Configure(MetaCore::MetaCoreDataSourceDefinition{ "replay-source", "file_replay", "Replay Source", {MetaCore::MetaCoreDataSourceSetting{"file_path", replayPath.string()}}, true, 1000 }), "File replay adapter 应能配置"); MetaCoreExpect(adapter.Connect(), "File replay adapter 应能连接"); adapter.Tick(0.10); auto updates = adapter.PollUpdates(); MetaCoreExpect(updates.size() == 1, "File replay adapter 首批应输出一条更新"); MetaCoreExpect(updates.front().DataPointId == "cube.visible", "首批更新应命中第一个数据点"); adapter.Tick(0.20); updates = adapter.PollUpdates(); MetaCoreExpect(updates.size() == 1, "File replay adapter 第二批应输出一条更新"); MetaCoreExpect(updates.front().DataPointId == "cube.position", "第二批更新应命中第二个数据点"); MetaCoreExpect( adapter.GetStatus().State == MetaCore::MetaCoreRuntimeDataSourceState::Connected, "File replay adapter 回放期间应保持 connected" ); adapter.Tick(1.0); MetaCoreExpect( adapter.PollUpdates().empty(), "File replay adapter 回放结束后不应继续输出更新" ); MetaCoreExpect( adapter.GetStatus().State == MetaCore::MetaCoreRuntimeDataSourceState::Degraded, "File replay adapter 回放结束后应进入 degraded 状态" ); std::filesystem::remove_all(tempDirectory); } void MetaCoreTestRuntimeDiagnosticsSnapshotReportsFaults() { MetaCore::MetaCoreScene scene; MetaCore::MetaCoreGameObject& cube = scene.CreateGameObject("Cube"); cube.MeshRenderer = MetaCore::MetaCoreMeshRendererComponent{}; MetaCore::MetaCoreRuntimeDataDispatcher dispatcher(scene); dispatcher.SetDataPointDefinitions({ MetaCore::MetaCoreDataPointDefinition{ "cube.visible", "mock-source", "cube.visible", MetaCore::MetaCoreRuntimeValueType::Bool } }); dispatcher.SetBindingDefinitions({ MetaCore::MetaCoreSceneBindingDefinition{ "binding.visible", "cube.visible", cube.Id, MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue } }); dispatcher.TickStaleness(2000); const auto diagnostics = dispatcher.BuildDiagnosticsSnapshot({ MetaCore::MetaCoreRuntimeDataSourceStatus{ "mock-source", MetaCore::MetaCoreRuntimeDataSourceState::Degraded, 1, 0, "Degraded source" } }); MetaCoreExpect(diagnostics.HasFaults, "Diagnostics snapshot 应报告 faults"); MetaCoreExpect(diagnostics.SourceStatuses.size() == 1, "Diagnostics snapshot 应保留 source status"); MetaCoreExpect(diagnostics.BindingStatuses.size() == 1, "Diagnostics snapshot 应保留 binding status"); } void MetaCoreTestRuntimeDataBinaryDocumentRejectsCorruption() { MetaCore::MetaCoreTypeRegistry registry; MetaCoreRegisterRuntimeDataGeneratedTypes(registry); const std::filesystem::path tempDirectory = std::filesystem::temp_directory_path() / "MetaCoreRuntimeDataCorruptIo"; std::filesystem::remove_all(tempDirectory); std::filesystem::create_directories(tempDirectory); const std::filesystem::path corruptSourcesPath = tempDirectory / "CorruptSources.mcruntime"; { std::ofstream output(corruptSourcesPath, std::ios::binary | std::ios::trunc); output << "not-a-valid-runtime-config"; } const auto loadedSources = MetaCore::MetaCoreReadRuntimeDataSourcesDocument(corruptSourcesPath, registry); MetaCoreExpect(!loadedSources.has_value(), "损坏的 RuntimeDataSourcesDocument 二进制应被拒绝"); std::filesystem::remove_all(tempDirectory); } void MetaCoreTestEditorContextRuntimeDataConfigSaveLoad() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorConfigProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Library"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); projectFile << "{\n" << " \"name\": \"RuntimeConfigProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); coreServicesModule->Startup(moduleRegistry); MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); MetaCoreExpect(editorContext.EnsureRuntimeDataConfigLoaded(), "EditorContext 应能加载 Runtime 配置"); auto& sources = editorContext.AccessRuntimeDataSourcesDocument(); auto& bindings = editorContext.AccessRuntimeBindingsDocument(); sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ "replay-source", "file_replay", "Replay Source", {MetaCore::MetaCoreDataSourceSetting{"file_path", "Runtime/RuntimeReplay.mcstream"}}, true, 1000 }); sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ "cube.position", "replay-source", "cube.position", MetaCore::MetaCoreRuntimeValueType::Vec3 }); bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ "binding.cube.position", "cube.position", 3, MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }); MetaCoreExpect(editorContext.SaveRuntimeDataConfig(), "EditorContext 应能保存 Runtime 配置"); MetaCore::MetaCoreTypeRegistry registry; MetaCoreRegisterRuntimeDataGeneratedTypes(registry); const auto loadedSources = MetaCore::MetaCoreReadRuntimeDataSourcesDocument( tempProjectRoot / "Runtime" / "DataSources.mcruntime", registry ); const auto loadedBindings = MetaCore::MetaCoreReadRuntimeBindingsDocument( tempProjectRoot / "Runtime" / "Bindings.mcruntime", registry ); const auto loadedProjectRuntime = MetaCore::MetaCoreReadRuntimeProjectDocument( tempProjectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", registry ); MetaCoreExpect(loadedSources.has_value(), "保存后应能读回 Runtime data sources"); MetaCoreExpect(loadedBindings.has_value(), "保存后应能读回 Runtime bindings"); MetaCoreExpect(loadedProjectRuntime.has_value(), "保存后应能读回 Runtime project"); MetaCoreExpect(loadedSources->Sources.size() == 1, "保存后应保留 source"); MetaCoreExpect(loadedBindings->Bindings.size() == 1, "保存后应保留 binding"); MetaCoreExpect(loadedProjectRuntime->StartupScenePath == std::filesystem::path("Scenes") / "Main.mcscene", "保存后应写入默认 startup scene 路径"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestRuntimeDataConfigValidationRejectsBrokenBinding() { MetaCore::MetaCoreRuntimeDataSourcesDocument sources; sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ "replay-source", "file_replay", "Replay Source", {MetaCore::MetaCoreDataSourceSetting{"file_path", "Runtime/RuntimeReplay.mcstream"}}, true, 1000 }); MetaCore::MetaCoreRuntimeBindingsDocument bindings; bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ "binding.invalid", "missing.point", 1, MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }); const auto issues = MetaCore::MetaCoreValidateRuntimeDataDocuments(sources, bindings); MetaCoreExpect(!issues.empty(), "坏 Runtime 配置应产生校验问题"); MetaCoreExpect( std::any_of(issues.begin(), issues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error; }), "坏 Runtime 配置应产生至少一个错误级问题" ); } void MetaCoreTestRuntimeDataConfigValidationRejectsMissingReplayFilePath() { MetaCore::MetaCoreRuntimeDataSourcesDocument sources; sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ "replay-source", "file_replay", "Replay Source", {}, true, 1000 }); const auto issues = MetaCore::MetaCoreValidateRuntimeDataDocuments(sources, {}); MetaCoreExpect( std::any_of(issues.begin(), issues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && issue.Message.find("file_path") != std::string::npos; }), "缺失 replay file_path 应产生错误" ); } void MetaCoreTestRuntimeDataConfigValidationRejectsMissingTcpPort() { MetaCore::MetaCoreRuntimeDataSourcesDocument sources; sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ "tcp-source", "tcp", "Tcp Source", {MetaCore::MetaCoreDataSourceSetting{"host", "127.0.0.1"}}, true, 1000 }); const auto issues = MetaCore::MetaCoreValidateRuntimeDataDocuments(sources, {}); MetaCoreExpect( std::any_of(issues.begin(), issues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && issue.Message.find("port") != std::string::npos; }), "缺失 tcp port 应产生错误" ); } void MetaCoreTestTcpRuntimeDataSourceAdapterReadsSocketStream() { WSADATA wsaData{}; MetaCoreExpect(WSAStartup(MAKEWORD(2, 2), &wsaData) == 0, "Smoke test 应能初始化 Winsock"); SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); MetaCoreExpect(listenSocket != INVALID_SOCKET, "Smoke test 应能创建监听 socket"); sockaddr_in address{}; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); address.sin_port = 0; MetaCoreExpect(bind(listenSocket, reinterpret_cast(&address), sizeof(address)) != SOCKET_ERROR, "Smoke test bind 应成功"); MetaCoreExpect(listen(listenSocket, 1) != SOCKET_ERROR, "Smoke test listen 应成功"); int addressLength = sizeof(address); MetaCoreExpect( getsockname(listenSocket, reinterpret_cast(&address), &addressLength) != SOCKET_ERROR, "Smoke test getsockname 应成功" ); const unsigned short port = ntohs(address.sin_port); std::thread serverThread([listenSocket]() { SOCKET clientSocket = accept(listenSocket, nullptr, nullptr); if (clientSocket != INVALID_SOCKET) { const char* payload = "cube.visible bool true\n" "cube.position vec3 1.0 2.0 3.0\n"; send(clientSocket, payload, static_cast(std::strlen(payload)), 0); shutdown(clientSocket, SD_SEND); closesocket(clientSocket); } closesocket(listenSocket); }); MetaCore::MetaCoreTcpRuntimeDataSourceAdapter adapter; MetaCoreExpect(adapter.Configure(MetaCore::MetaCoreDataSourceDefinition{ "tcp-source", "tcp", "Tcp Source", { MetaCore::MetaCoreDataSourceSetting{"host", "127.0.0.1"}, MetaCore::MetaCoreDataSourceSetting{"port", std::to_string(port)} }, true, 1000 }), "Tcp adapter 应能配置"); MetaCoreExpect(adapter.Connect(), "Tcp adapter 应能连接"); for (int index = 0; index < 20; ++index) { adapter.Tick(0.05); const auto updates = adapter.PollUpdates(); if (!updates.empty()) { MetaCoreExpect(updates.size() == 2, "Tcp adapter 应读到两条更新"); MetaCoreExpect(updates[0].DataPointId == "cube.visible", "Tcp adapter 第一条更新应正确"); MetaCoreExpect(updates[1].DataPointId == "cube.position", "Tcp adapter 第二条更新应正确"); MetaCoreExpect(updates[1].Value.Type == MetaCore::MetaCoreRuntimeValueType::Vec3, "Tcp adapter 类型应正确"); serverThread.join(); WSACleanup(); return; } std::this_thread::sleep_for(std::chrono::milliseconds(10)); } serverThread.join(); WSACleanup(); MetaCoreExpect(false, "Tcp adapter 应在轮询内收到更新"); } void MetaCoreTestEditorContextRejectsInvalidRuntimeDataSave() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorInvalidConfigProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Library"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); projectFile << "{\n" << " \"name\": \"RuntimeInvalidConfigProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); coreServicesModule->Startup(moduleRegistry); MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); MetaCoreExpect(editorContext.EnsureRuntimeDataConfigLoaded(), "EditorContext 应能加载 Runtime 配置"); auto& bindings = editorContext.AccessRuntimeBindingsDocument(); bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ "binding.invalid", "missing.point", 3, MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }); MetaCoreExpect(!editorContext.SaveRuntimeDataConfig(), "坏 Runtime 配置应被拒绝保存"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestUiDocumentSerialization() { MetaCore::MetaCoreTypeRegistry registry; MetaCoreRegisterFoundationGeneratedTypes(registry); MetaCoreRegisterSceneGeneratedTypes(registry); MetaCoreRegisterEditorGeneratedTypes(registry); MetaCore::MetaCoreUiDocument document; document.Name = "MainHud"; document.ReferenceWidth = 1920; document.ReferenceHeight = 1080; document.RootNodeIds = {"root.panel"}; MetaCore::MetaCoreUiNodeDocument rootNode; rootNode.Id = "root.panel"; rootNode.Name = "Root Panel"; rootNode.Type = MetaCore::MetaCoreUiNodeType::Panel; rootNode.Visible = true; rootNode.Children = {"header.text", "status.button"}; rootNode.RectTransform.AnchorMin = glm::vec3(0.0F, 0.0F, 0.0F); rootNode.RectTransform.AnchorMax = glm::vec3(1.0F, 1.0F, 0.0F); rootNode.RectTransform.Size = glm::vec3(0.0F, 0.0F, 0.0F); rootNode.Style.BackgroundColor = glm::vec3(0.05F, 0.08F, 0.12F); MetaCore::MetaCoreUiNodeDocument textNode; textNode.Id = "header.text"; textNode.Name = "Header"; textNode.Type = MetaCore::MetaCoreUiNodeType::Text; textNode.ParentId = "root.panel"; textNode.Text = "MetaCore"; textNode.RectTransform.AnchorMin = glm::vec3(0.0F, 1.0F, 0.0F); textNode.RectTransform.AnchorMax = glm::vec3(1.0F, 1.0F, 0.0F); textNode.RectTransform.Position = glm::vec3(0.0F, -24.0F, 0.0F); textNode.Style.FontSize = 28.0F; textNode.Style.HorizontalAlignment = MetaCore::MetaCoreUiHorizontalAlignment::Center; textNode.Style.TextColor = glm::vec3(0.95F, 0.95F, 0.95F); MetaCore::MetaCoreUiNodeDocument buttonNode; buttonNode.Id = "status.button"; buttonNode.Name = "Status Button"; buttonNode.Type = MetaCore::MetaCoreUiNodeType::Button; buttonNode.ParentId = "root.panel"; buttonNode.Text = "Start"; buttonNode.Interactable = true; buttonNode.RectTransform.AnchorMin = glm::vec3(1.0F, 0.0F, 0.0F); buttonNode.RectTransform.AnchorMax = glm::vec3(1.0F, 0.0F, 0.0F); buttonNode.RectTransform.Pivot = glm::vec3(1.0F, 0.0F, 0.0F); buttonNode.RectTransform.Position = glm::vec3(-32.0F, 32.0F, 0.0F); buttonNode.RectTransform.Size = glm::vec3(180.0F, 48.0F, 0.0F); buttonNode.Style.BackgroundColor = glm::vec3(0.12F, 0.42F, 0.82F); document.Nodes = {rootNode, textNode, buttonNode}; const auto bytes = MetaCore::MetaCoreSerializeToBytes(document, registry); MetaCoreExpect(bytes.has_value(), "UiDocument 应能序列化"); MetaCore::MetaCoreUiDocument roundTrip; MetaCoreExpect( MetaCore::MetaCoreDeserializeFromBytes(*bytes, roundTrip, registry), "UiDocument 应能反序列化" ); MetaCoreExpect(roundTrip.Name == document.Name, "UiDocument 名称应保持"); MetaCoreExpect(roundTrip.ReferenceWidth == 1920, "UiDocument 参考宽度应保持"); MetaCoreExpect(roundTrip.ReferenceHeight == 1080, "UiDocument 参考高度应保持"); MetaCoreExpect(roundTrip.RootNodeIds.size() == 1, "UiDocument 根节点数应保持"); MetaCoreExpect(roundTrip.Nodes.size() == 3, "UiDocument 节点数量应保持"); MetaCoreExpect(roundTrip.Nodes[0].Children.size() == 2, "UiNode 子节点列表应保持"); MetaCoreExpect(roundTrip.Nodes[1].Type == MetaCore::MetaCoreUiNodeType::Text, "UiNode 类型应保持"); MetaCoreExpect(roundTrip.Nodes[1].Text == "MetaCore", "Ui Text 内容应保持"); MetaCoreExpect(roundTrip.Nodes[1].Style.FontSize == 28.0F, "Ui 字体大小应保持"); MetaCoreExpect(roundTrip.Nodes[2].Interactable, "Ui Button 可交互状态应保持"); MetaCoreExpect(roundTrip.Nodes[2].Style.HorizontalAlignment == MetaCore::MetaCoreUiHorizontalAlignment::Left, "默认水平对齐应保持"); MetaCoreExpectVec3Near(roundTrip.Nodes[0].Style.BackgroundColor, glm::vec3(0.05F, 0.08F, 0.12F), "Ui 背景色应保持"); } } // namespace int main() { MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); MetaCoreExpect(scene.GetGameObjects().size() == 6, "默认场景应包含六个对象"); bool hasCamera = false; bool hasLight = false; bool hasCube = false; for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) { hasCamera = hasCamera || object.Camera.has_value(); hasLight = hasLight || object.Light.has_value(); hasCube = hasCube || object.MeshRenderer.has_value(); } MetaCoreExpect(hasCamera, "默认场景应包含摄像机"); MetaCoreExpect(hasLight, "默认场景应包含光源"); MetaCoreExpect(hasCube, "默认场景应包含立方体"); MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; moduleRegistry.RegisterPanelProvider(std::make_unique()); MetaCoreExpect(!moduleRegistry.AccessPanelOpenState("Dummy"), "面板默认状态应为关闭"); MetaCore::MetaCoreLogService logService; logService.AddEntry(MetaCore::MetaCoreLogLevel::Info, "Test", "Smoke"); MetaCoreExpect(logService.GetEntries().size() == 1, "日志服务应记录一条日志"); MetaCoreTestSceneEditApi(); MetaCoreTestSelectionStateMachine(); MetaCoreTestUndoRedo(); MetaCoreTestBuiltinModuleComposition(); MetaCoreTestScenePackageProjectStartupLoad(); MetaCoreTestComponentRegistryDescriptors(); MetaCoreTestScenePersistenceRoundTrip(); MetaCoreTestImportPipelineAndCook(); MetaCoreTestProjectDescriptorAndGltfImporterSkeleton(); MetaCoreTestInstantiateImportedModelAssetIntoScene(); MetaCoreTestBootstrapStartupSceneCreation(); MetaCoreTestPrefabWorkflow(); MetaCoreTestComponentRegistryOperations(); MetaCoreTestRuntimeDataTypeSerialization(); MetaCoreTestRuntimeDataProjectDocumentSerialization(); MetaCoreTestMeshRendererResourceSerialization(); MetaCoreTestRuntimeDataBinaryDocumentIo(); MetaCoreTestRuntimeProjectDocumentIo(); MetaCoreTestRuntimeDiagnosticsSnapshotIo(); MetaCoreTestRuntimeDataDispatcherConstruction(); MetaCoreTestRuntimeDataDispatcherAppliesUpdates(); MetaCoreTestMockRuntimeDataSourceAdapterEmitsUpdates(); MetaCoreTestRuntimeDataDispatcherMarksStaleBindings(); MetaCoreTestMockRuntimeDataSourceAdapterDegradedState(); MetaCoreTestFileReplayRuntimeDataSourceAdapterEmitsReplayFrames(); MetaCoreTestRuntimeDiagnosticsSnapshotReportsFaults(); MetaCoreTestRuntimeDataBinaryDocumentRejectsCorruption(); MetaCoreTestEditorContextRuntimeDataConfigSaveLoad(); MetaCoreTestRuntimeDataConfigValidationRejectsBrokenBinding(); MetaCoreTestRuntimeDataConfigValidationRejectsMissingReplayFilePath(); MetaCoreTestRuntimeDataConfigValidationRejectsMissingTcpPort(); MetaCoreTestEditorContextRejectsInvalidRuntimeDataSave(); MetaCoreTestTcpRuntimeDataSourceAdapterReadsSocketStream(); MetaCoreTestUiDocumentSerialization(); std::cout << "MetaCoreSmokeTests passed\n"; return 0; }