#include "MetaCoreEditor/MetaCoreBuiltinModules.h" #include "MetaCoreEditor/MetaCoreEditorAssetTypes.h" #include "MetaCoreEditor/MetaCoreEditorContext.h" #include "MetaCoreEditor/MetaCoreEditorModule.h" #include "MetaCoreEditor/MetaCoreSceneInteractionService.h" #include "MetaCoreEditor/MetaCoreEditorServices.h" #include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" #include "MetaCoreFoundation/MetaCoreLogService.h" #include "MetaCoreFoundation/MetaCoreProject.h" #include "MetaCoreFoundation/MetaCoreAssetRegistry.h" #include "MetaCorePlatform/MetaCoreWindow.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" #include "MetaCoreRender/MetaCoreRuntimeUiRenderer.h" #include "MetaCoreRender/MetaCoreSceneRenderSync.h" #include "MetaCoreRender/MetaCoreFilamentSceneBridge.h" #include #include "MetaCoreRuntimeData/MetaCoreRuntimeDataDispatcher.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataSource.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" #include "MetaCoreScene/MetaCoreScenePackage.h" #include "MetaCoreScene/MetaCoreRuntimeLifecycle.h" #include "MetaCoreScene/MetaCoreScene.h" #include "MetaCoreScene/MetaCoreSceneSerializer.h" #include "MetaCoreScene/MetaCoreUiRmlCompiler.h" #include "MetaCoreScene/MetaCoreTransformUtils.h" #include "MetaCoreFoundation/MetaCoreHash.h" #include #include #include #include #include #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 { struct MetaCoreSmokeScriptComponent { float Speed = 2.5F; bool Enabled = true; std::string Target = "Player"; MetaCore::MetaCoreMeshAlphaMode AlphaMode = MetaCore::MetaCoreMeshAlphaMode::Opaque; }; void MetaCoreExpect(bool condition, const char* message); const MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor* MetaCoreRegisterSmokeScriptComponentForTesting( MetaCore::MetaCoreIComponentTypeRegistry& componentRegistry, MetaCore::MetaCoreIReflectionRegistry& reflectionRegistry, std::string typeId = "SmokeScript" ) { MetaCore::MetaCoreFieldEditorMetadata speedMetadata{}; speedMetadata.DisplayName = "Speed"; speedMetadata.Group = "Script"; speedMetadata.Min = 0.0; speedMetadata.Max = 10.0; speedMetadata.Step = 0.25; speedMetadata.DefaultValue = "2.5"; MetaCore::MetaCoreFieldEditorMetadata targetMetadata{}; targetMetadata.DisplayName = "Target"; targetMetadata.Group = "Script"; targetMetadata.DefaultValue = "Player"; auto smokeScriptBuilder = MetaCore::MetaCoreRegisterGeneratedStruct( reflectionRegistry.AccessTypeRegistry(), "MetaCoreSmokeScriptComponent" ); smokeScriptBuilder .Field<&MetaCoreSmokeScriptComponent::Speed>("Speed", speedMetadata) .Field<&MetaCoreSmokeScriptComponent::Enabled>("Enabled") .Field<&MetaCoreSmokeScriptComponent::Target>("Target", targetMetadata) .Field<&MetaCoreSmokeScriptComponent::AlphaMode>("AlphaMode"); const MetaCore::MetaCoreStructDescriptor* smokeScriptReflection = reflectionRegistry.GetTypeRegistry().FindStruct(); MetaCoreExpect(smokeScriptReflection != nullptr, "Static C++ smoke script reflection should be registered"); MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor smokeScriptDescriptor{}; smokeScriptDescriptor.TypeId = std::move(typeId); smokeScriptDescriptor.DisplayName = "Smoke Script"; smokeScriptDescriptor.ReflectedType = smokeScriptReflection; smokeScriptDescriptor.MutableComponent = [](MetaCore::MetaCoreGameObject& gameObject) -> void* { return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; }; smokeScriptDescriptor.ConstComponent = [](const MetaCore::MetaCoreGameObject& gameObject) -> const void* { return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; }; smokeScriptDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) { return gameObject.HasComponent(); }; smokeScriptDescriptor.AddComponent = [](MetaCore::MetaCoreGameObject& gameObject) { if (gameObject.HasComponent()) { return false; } gameObject.AddComponent(); return true; }; smokeScriptDescriptor.RemoveComponent = [](MetaCore::MetaCoreGameObject& gameObject) { if (!gameObject.HasComponent()) { return false; } gameObject.RemoveComponent(); return true; }; smokeScriptDescriptor.ResetComponent = [](MetaCore::MetaCoreGameObject& gameObject) { if (!gameObject.HasComponent()) { return false; } gameObject.GetComponent() = MetaCoreSmokeScriptComponent{}; return true; }; const std::string registeredTypeId = smokeScriptDescriptor.TypeId; MetaCoreExpect( componentRegistry.RegisterComponentDescriptor(std::move(smokeScriptDescriptor)), "Component registry should accept a static C++ smoke script descriptor" ); return componentRegistry.FindDescriptor(registeredTypeId); } 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); } } void MetaCoreExpectMat4Near(const glm::mat4& actual, const glm::mat4& expected, const char* message) { for (int column = 0; column < 4; ++column) { for (int row = 0; row < 4; ++row) { if (std::abs(actual[column][row] - expected[column][row]) > 0.0001F) { std::cerr << "MetaCoreSmokeTests failed: " << message << " (column=" << column << " row=" << row << " actual=" << actual[column][row] << " expected=" << expected[column][row] << ")\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), "复制后的对象应可被查找到"); const bool reparented = scene.ReparentGameObjects({duplicatedRootId}, 0, true); MetaCoreExpect(reparented, "重挂接操作应成功"); const std::vector deletedIds = scene.DeleteGameObjects({duplicatedRootId}); MetaCoreExpect(!deletedIds.empty(), "删除应返回被删除对象"); MetaCoreExpect(!scene.FindGameObject(duplicatedRootId), "删除后对象不应存在"); } 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").GetId(); const MetaCore::MetaCoreId objectBId = scene.CreateGameObject("B").GetId(); const MetaCore::MetaCoreId objectCId = scene.CreateGameObject("C").GetId(); 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.GetId()); 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, "BuildService should be registered"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ScenePersistenceService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 SelectionService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ClipboardService"); MetaCoreExpect(!moduleRegistry.GetPanelProviders().empty(), "应注册至少一个面板提供者"); const bool hasBuildSettingsPanel = std::any_of( moduleRegistry.GetPanelProviders().begin(), moduleRegistry.GetPanelProviders().end(), [](const std::unique_ptr& panelProvider) { return panelProvider != nullptr && panelProvider->GetPanelId() == "BuildSettings"; } ); MetaCoreExpect(hasBuildSettingsPanel, "Build Settings panel should be registered"); const bool hasRuntimeDataPanel = std::any_of( moduleRegistry.GetPanelProviders().begin(), moduleRegistry.GetPanelProviders().end(), [](const std::unique_ptr& panelProvider) { return panelProvider != nullptr && panelProvider->GetPanelId() == "RuntimeData"; } ); MetaCoreExpect(hasRuntimeDataPanel, "Runtime Data panel should be registered"); editorViewsModule->Shutdown(moduleRegistry); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); } void MetaCoreTestLegacyBinarySceneStartupLoadCompatibility() { 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.CaptureSnapshot().GameObjects; 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"); MetaCore::MetaCoreSceneDocument jsonSceneDocument = sceneDocument; jsonSceneDocument.Name = "JsonMain"; const std::filesystem::path jsonScenePath = tempProjectRoot / "Scenes" / "JsonMain.mcscene.json"; MetaCoreExpect( MetaCore::MetaCoreSceneSerializer::SaveSceneToJson( jsonScenePath, jsonSceneDocument, MetaCore::MetaCoreBuildScenePackageTypeRegistry() ), "Startup scene JSON should be writable" ); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); MetaCoreExpect(projectFile.is_open(), "Project file should be writable for JSON startup scene"); projectFile << "{\n" << " \"name\": \"SmokeProject\",\n" << " \"scenes\": [\n" << " \"Scenes/JsonMain.mcscene.json\"\n" << " ],\n" << " \"startup_scene\": \"Scenes/JsonMain.mcscene.json\",\n" << " \"version\": \"0.1.0\"\n" << "}\n"; } const auto loadedJsonSceneDocument = MetaCore::MetaCoreLoadStartupSceneDocument(tempProjectRoot / "MetaCore.project.json"); MetaCoreExpect(loadedJsonSceneDocument.has_value(), "Startup scene loader should read JSON scene assets"); MetaCoreExpect(loadedJsonSceneDocument->Name == "JsonMain", "Startup scene loader should preserve JSON scene name"); MetaCoreExpect( loadedJsonSceneDocument->GameObjects.size() == sourceScene.GetGameObjects().size(), "JSON startup scene object count should match" ); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestProjectFileRoundTripAndDiscovery() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreProjectFileRoundTrip"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Scenes"); MetaCore::MetaCoreProjectFileDocument document; document.Name = "RoundTripProject"; document.Version = "1.2.3"; document.RuntimeDirectory = "RuntimeData"; document.UiDirectory = "UserInterface"; document.BuildDirectory = "BuildOutput"; document.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; document.ScenePaths = { std::filesystem::path("Scenes") / "Main.mcscene", std::filesystem::path("Scenes") / "Secondary.mcscene" }; const std::filesystem::path projectFilePath = MetaCore::MetaCoreGetProjectFilePath(tempProjectRoot); MetaCoreExpect( MetaCore::MetaCoreWriteProjectFile(projectFilePath, document), "项目文件应能成功写入" ); const auto loadedDocument = MetaCore::MetaCoreReadProjectFile(projectFilePath); MetaCoreExpect(loadedDocument.has_value(), "项目文件应能被读回"); MetaCoreExpect(loadedDocument->Name == "RoundTripProject", "项目名称应保持"); MetaCoreExpect(loadedDocument->Version == "1.2.3", "项目版本应保持"); MetaCoreExpect(loadedDocument->RuntimeDirectory == std::filesystem::path("RuntimeData"), "runtime 目录应保持"); MetaCoreExpect(loadedDocument->UiDirectory == std::filesystem::path("UserInterface"), "ui 目录应保持"); MetaCoreExpect(loadedDocument->BuildDirectory == std::filesystem::path("BuildOutput"), "build 目录应保持"); MetaCoreExpect(loadedDocument->StartupScenePath == std::filesystem::path("Scenes") / "Main.mcscene", "startup scene 应保持"); MetaCoreExpect(loadedDocument->ScenePaths.size() == 2, "scene 列表应保持"); _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); const auto discoveredFromEnvironment = MetaCore::MetaCoreDiscoverProjectRoot(std::filesystem::temp_directory_path()); MetaCoreExpect(discoveredFromEnvironment.has_value(), "环境变量应能解析项目根"); MetaCoreExpect( std::filesystem::equivalent(*discoveredFromEnvironment, tempProjectRoot), "环境变量解析结果应指向目标项目" ); _putenv_s("METACORE_PROJECT_PATH", ""); const std::filesystem::path nestedDirectory = tempProjectRoot / "Nested" / "Child"; std::filesystem::create_directories(nestedDirectory); const auto discoveredFromAncestors = MetaCore::MetaCoreDiscoverProjectRoot(nestedDirectory); MetaCoreExpect(discoveredFromAncestors.has_value(), "祖先目录搜索应能找到项目根"); MetaCoreExpect( std::filesystem::equivalent(*discoveredFromAncestors, tempProjectRoot), "祖先目录搜索应返回正确项目根" ); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestAssetDatabaseCreateAndOpenProject() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreCreateOpenProject"; std::filesystem::remove_all(tempProjectRoot); _putenv_s("METACORE_PROJECT_PATH", ""); MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); coreServicesModule->Startup(moduleRegistry); const auto assetDatabase = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); const std::filesystem::path createdProjectRoot = tempProjectRoot / "CreatedProject"; MetaCoreExpect(assetDatabase->CreateProject(createdProjectRoot, "CreatedProject"), "应能创建项目"); MetaCoreExpect(assetDatabase->HasProject(), "创建后应存在当前项目"); MetaCoreExpect(std::filesystem::exists(createdProjectRoot / "MetaCore.project.json"), "创建后应存在项目文件"); MetaCoreExpect(assetDatabase->GetProjectDescriptor().Name == "CreatedProject", "创建后项目名称应正确"); MetaCoreExpect(assetDatabase->GetProjectDescriptor().RootPath == createdProjectRoot, "创建后项目根路径应正确"); { MetaCore::MetaCoreProjectFileDocument secondProjectDocument; secondProjectDocument.Name = "OpenedProject"; secondProjectDocument.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; const std::filesystem::path secondProjectRoot = tempProjectRoot / "OpenedProject"; std::filesystem::create_directories(secondProjectRoot / "Scenes"); MetaCoreExpect( MetaCore::MetaCoreWriteProjectFile(secondProjectRoot / "MetaCore.project.json", secondProjectDocument), "应能写入第二个项目文件" ); MetaCoreExpect(assetDatabase->OpenProject(secondProjectRoot), "应能打开现有项目"); MetaCoreExpect(assetDatabase->GetProjectDescriptor().Name == "OpenedProject", "打开后项目名称应正确"); MetaCoreExpect(assetDatabase->GetProjectDescriptor().RootPath == secondProjectRoot, "打开后项目根路径应正确"); } coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestAssetDatabaseCreatesFirstClassProjectAssets() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreP2CreateProjectAssets"; std::filesystem::remove_all(tempProjectRoot); _putenv_s("METACORE_PROJECT_PATH", ""); MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); coreServicesModule->Startup(moduleRegistry); const auto assetDatabase = moduleRegistry.ResolveService(); const auto reflectionRegistry = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "AssetDatabaseService should be available"); MetaCoreExpect(reflectionRegistry != nullptr, "ReflectionRegistry should be available"); MetaCoreExpect(assetDatabase->CreateProject(tempProjectRoot, "P2CreateAssets"), "Project creation should succeed"); const auto sceneRecord = assetDatabase->CreateSceneAsset(std::filesystem::path("Scenes") / "P2Scene.mcscene", true); MetaCoreExpect(sceneRecord.has_value(), "CreateSceneAsset should create a scene record"); MetaCoreExpect(sceneRecord->Type == "scene", "Created scene should be registered as scene"); MetaCoreExpect(sceneRecord->RelativePath == std::filesystem::path("Scenes") / "P2Scene.mcscene.json", "Scene extension should normalize to .mcscene.json"); MetaCoreExpect(assetDatabase->GetProjectDescriptor().StartupScenePath == sceneRecord->RelativePath, "Created startup scene should update project descriptor"); MetaCoreExpect(std::filesystem::exists(tempProjectRoot / sceneRecord->RelativePath), "Created scene file should exist"); MetaCoreExpect(std::filesystem::exists(tempProjectRoot / (sceneRecord->RelativePath.string() + ".mcmeta")), "Created scene should have mcmeta"); const auto prefabRecord = assetDatabase->CreatePrefabAsset(std::filesystem::path("Assets") / "Prefabs" / "P2Prefab.mcprefab"); MetaCoreExpect(prefabRecord.has_value(), "CreatePrefabAsset should create a prefab record"); MetaCoreExpect(prefabRecord->Type == "prefab", "Created prefab should be registered as prefab"); MetaCoreExpect(prefabRecord->RelativePath == std::filesystem::path("Assets") / "Prefabs" / "P2Prefab.mcprefab.json", "Prefab extension should normalize to .mcprefab.json"); const auto prefabDocument = MetaCore::MetaCoreSceneSerializer::LoadPrefabFromJson( tempProjectRoot / prefabRecord->RelativePath, reflectionRegistry->GetTypeRegistry() ); MetaCoreExpect(prefabDocument.has_value(), "Created prefab should be loadable"); MetaCoreExpect(prefabDocument->GameObjects.size() == 1, "Created prefab should contain a root object"); const auto materialRecord = assetDatabase->CreateMaterialAsset(std::filesystem::path("Assets") / "Materials" / "P2Material"); MetaCoreExpect(materialRecord.has_value(), "CreateMaterialAsset should create a material record"); MetaCoreExpect(materialRecord->Type == "material", "Created material should be registered as material"); const auto materialDocument = MetaCore::MetaCoreSceneSerializer::LoadMaterialFromJson( tempProjectRoot / materialRecord->RelativePath, reflectionRegistry->GetTypeRegistry() ); MetaCoreExpect(materialDocument.has_value(), "Created material should be loadable"); MetaCoreExpect(materialDocument->AssetGuid == materialRecord->Guid, "Material document guid should match AssetDatabase guid"); const auto uiRecord = assetDatabase->CreateUiDocumentAsset(std::filesystem::path("Ui") / "P2Hud.mcui"); MetaCoreExpect(uiRecord.has_value(), "CreateUiDocumentAsset should create a UI record"); MetaCoreExpect(uiRecord->Type == "ui_document", "Created UI document should be registered as ui_document"); MetaCoreExpect(uiRecord->RelativePath == std::filesystem::path("Ui") / "P2Hud.mcui.json", "UI extension should normalize to .mcui.json"); const auto uiDocument = MetaCore::MetaCoreSceneSerializer::LoadUiFromJson( tempProjectRoot / uiRecord->RelativePath, reflectionRegistry->GetTypeRegistry() ); MetaCoreExpect(uiDocument.has_value(), "Created UI document should be loadable"); MetaCoreExpect(uiDocument->RootNodeIds.size() == 1 && uiDocument->Nodes.size() == 1, "Created UI document should contain one root panel"); MetaCoreExpect(assetDatabase->CreateFolder(std::filesystem::path("Assets") / "Moved"), "CreateFolder should create a project folder"); const MetaCore::MetaCoreAssetGuid materialGuid = materialRecord->Guid; MetaCoreExpect(assetDatabase->MovePath(materialRecord->RelativePath, std::filesystem::path("Assets") / "Moved"), "MovePath should move created material"); const std::filesystem::path movedMaterialPath = std::filesystem::path("Assets") / "Moved" / materialRecord->RelativePath.filename(); const auto movedMaterialRecord = assetDatabase->FindAssetByRelativePath(movedMaterialPath); MetaCoreExpect(movedMaterialRecord.has_value(), "Moved material should be registered at new path"); MetaCoreExpect(movedMaterialRecord->Guid == materialGuid, "Moved material should keep GUID"); MetaCoreExpect(movedMaterialRecord->PackagePath == movedMaterialPath, "Moved JSON material package path should follow source path"); const MetaCore::MetaCoreAssetGuid uiGuid = uiRecord->Guid; MetaCoreExpect(assetDatabase->RenamePath(uiRecord->RelativePath, "P2HudRenamed.mcui.json"), "RenamePath should rename created UI document"); const std::filesystem::path renamedUiPath = std::filesystem::path("Ui") / "P2HudRenamed.mcui.json"; const auto renamedUiRecord = assetDatabase->FindAssetByRelativePath(renamedUiPath); MetaCoreExpect(renamedUiRecord.has_value(), "Renamed UI should be registered at new path"); MetaCoreExpect(renamedUiRecord->Guid == uiGuid, "Renamed UI should keep GUID"); MetaCoreExpect(renamedUiRecord->PackagePath == renamedUiPath, "Renamed JSON UI package path should follow source path"); MetaCoreExpect(assetDatabase->CreateFolder(std::filesystem::path("Assets") / "FolderMoveSource"), "CreateFolder should create folder move source"); const auto folderMaterialRecord = assetDatabase->CreateMaterialAsset(std::filesystem::path("Assets") / "FolderMoveSource" / "FolderMaterial"); MetaCoreExpect(folderMaterialRecord.has_value(), "CreateMaterialAsset should create material inside folder"); const MetaCore::MetaCoreAssetGuid folderMaterialGuid = folderMaterialRecord->Guid; MetaCoreExpect(assetDatabase->CreateFolder(std::filesystem::path("Assets") / "FolderMoveTarget"), "CreateFolder should create folder move target"); MetaCoreExpect( assetDatabase->MovePath(std::filesystem::path("Assets") / "FolderMoveSource", std::filesystem::path("Assets") / "FolderMoveTarget"), "MovePath should move a folder containing assets" ); const std::filesystem::path movedFolderMaterialPath = std::filesystem::path("Assets") / "FolderMoveTarget" / "FolderMoveSource" / "FolderMaterial.mcmaterial.json"; const auto movedFolderMaterialRecord = assetDatabase->FindAssetByRelativePath(movedFolderMaterialPath); MetaCoreExpect(movedFolderMaterialRecord.has_value(), "Asset inside moved folder should be registered at new path"); MetaCoreExpect(movedFolderMaterialRecord->Guid == folderMaterialGuid, "Asset inside moved folder should keep GUID"); MetaCoreExpect( movedFolderMaterialRecord->PackagePath == movedFolderMaterialPath, "Asset inside moved folder should rewrite package path metadata" ); MetaCoreExpect( assetDatabase->RenamePath(std::filesystem::path("Assets") / "FolderMoveTarget" / "FolderMoveSource", "FolderRenamed"), "RenamePath should rename a folder containing assets" ); const std::filesystem::path renamedFolderMaterialPath = std::filesystem::path("Assets") / "FolderMoveTarget" / "FolderRenamed" / "FolderMaterial.mcmaterial.json"; const auto renamedFolderMaterialRecord = assetDatabase->FindAssetByRelativePath(renamedFolderMaterialPath); MetaCoreExpect(renamedFolderMaterialRecord.has_value(), "Asset inside renamed folder should be registered at new path"); MetaCoreExpect(renamedFolderMaterialRecord->Guid == folderMaterialGuid, "Asset inside renamed folder should keep GUID"); MetaCoreExpect( renamedFolderMaterialRecord->PackagePath == renamedFolderMaterialPath, "Asset inside renamed folder should rewrite package path metadata" ); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestAssetDatabaseRenamePathUpdatesProjectDescriptor() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreRenamePathProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Scenes"); MetaCore::MetaCoreProjectFileDocument projectDocument; projectDocument.Name = "RenamePathProject"; projectDocument.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; projectDocument.ScenePaths = {std::filesystem::path("Scenes") / "Main.mcscene"}; MetaCoreExpect( MetaCore::MetaCoreWriteProjectFile(tempProjectRoot / "MetaCore.project.json", projectDocument), "应能写入项目文件" ); MetaCore::MetaCoreSceneDocument sceneDocument; sceneDocument.Name = "Main"; sceneDocument.GameObjects = MetaCore::MetaCoreCreateDefaultScene().CaptureSnapshot().GameObjects; MetaCoreExpect( MetaCore::MetaCoreWriteScenePackage(tempProjectRoot / "Scenes" / "Main.mcscene", sceneDocument), "应能写入场景包" ); _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); coreServicesModule->Startup(moduleRegistry); const auto assetDatabase = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(assetDatabase->HasProject(), "应成功打开项目"); MetaCoreExpect( assetDatabase->RenamePath(std::filesystem::path("Scenes") / "Main.mcscene", "Renamed.mcscene"), "应能重命名场景路径" ); MetaCoreExpect( assetDatabase->GetProjectDescriptor().StartupScenePath == std::filesystem::path("Scenes") / "Renamed.mcscene", "重命名场景后 startup scene 应更新" ); MetaCoreExpect( std::find( assetDatabase->GetProjectDescriptor().ScenePaths.begin(), assetDatabase->GetProjectDescriptor().ScenePaths.end(), std::filesystem::path("Scenes") / "Renamed.mcscene" ) != assetDatabase->GetProjectDescriptor().ScenePaths.end(), "重命名场景后 scene 列表应更新" ); const auto loadedProjectFile = MetaCore::MetaCoreReadProjectFile(tempProjectRoot / "MetaCore.project.json"); MetaCoreExpect(loadedProjectFile.has_value(), "应能重新读回项目文件"); MetaCoreExpect( loadedProjectFile->StartupScenePath == std::filesystem::path("Scenes") / "Renamed.mcscene", "项目文件中的 startup scene 应同步更新" ); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestAssetDatabaseMovePathUpdatesProjectDescriptor() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreMovePathProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Scenes" / "Sub"); MetaCore::MetaCoreProjectFileDocument projectDocument; projectDocument.Name = "MovePathProject"; projectDocument.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; projectDocument.ScenePaths = {std::filesystem::path("Scenes") / "Main.mcscene"}; MetaCoreExpect( MetaCore::MetaCoreWriteProjectFile(tempProjectRoot / "MetaCore.project.json", projectDocument), "应能写入项目文件" ); MetaCore::MetaCoreSceneDocument sceneDocument; sceneDocument.Name = "Main"; sceneDocument.GameObjects = MetaCore::MetaCoreCreateDefaultScene().CaptureSnapshot().GameObjects; MetaCoreExpect( MetaCore::MetaCoreWriteScenePackage(tempProjectRoot / "Scenes" / "Main.mcscene", sceneDocument), "应能写入场景包" ); _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); coreServicesModule->Startup(moduleRegistry); const auto assetDatabase = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(assetDatabase->HasProject(), "应成功打开项目"); MetaCoreExpect( assetDatabase->MovePath(std::filesystem::path("Scenes") / "Main.mcscene", std::filesystem::path("Scenes") / "Sub"), "应能移动场景路径" ); MetaCoreExpect( assetDatabase->GetProjectDescriptor().StartupScenePath == std::filesystem::path("Scenes") / "Sub" / "Main.mcscene", "移动场景后 startup scene 应更新" ); MetaCoreExpect( std::find( assetDatabase->GetProjectDescriptor().ScenePaths.begin(), assetDatabase->GetProjectDescriptor().ScenePaths.end(), std::filesystem::path("Scenes") / "Sub" / "Main.mcscene" ) != assetDatabase->GetProjectDescriptor().ScenePaths.end(), "移动场景后 scene 列表应更新" ); const auto loadedProjectFile = MetaCore::MetaCoreReadProjectFile(tempProjectRoot / "MetaCore.project.json"); MetaCoreExpect(loadedProjectFile.has_value(), "应能重新读回项目文件"); MetaCoreExpect( loadedProjectFile->StartupScenePath == std::filesystem::path("Scenes") / "Sub" / "Main.mcscene", "项目文件中的 startup scene 应同步更新" ); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); 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"); std::cout << "[CHECK] transformDescriptor: " << transformDescriptor << std::endl; std::cout << "[CHECK] cameraDescriptor: " << cameraDescriptor << std::endl; std::cout << "[CHECK] lightDescriptor: " << lightDescriptor << std::endl; std::cout << "[CHECK] meshRendererDescriptor: " << meshRendererDescriptor << std::endl; if (transformDescriptor) std::cout << "[CHECK] transform DrawInspector: " << static_cast(transformDescriptor->DrawInspector) << std::endl; if (cameraDescriptor) std::cout << "[CHECK] camera DrawInspector: " << static_cast(cameraDescriptor->DrawInspector) << std::endl; if (lightDescriptor) std::cout << "[CHECK] light DrawInspector: " << static_cast(lightDescriptor->DrawInspector) << std::endl; if (meshRendererDescriptor) std::cout << "[CHECK] meshRenderer DrawInspector: " << static_cast(meshRendererDescriptor->DrawInspector) << std::endl; 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"); MetaCoreExpect(transformDescriptor->Category == "Transform", "Transform descriptor should expose a component category"); MetaCoreExpect(cameraDescriptor->Category == "Rendering", "Camera descriptor should expose a component category"); MetaCoreExpect(transformDescriptor->ReflectedType != nullptr, "Transform descriptor should bind reflected type metadata"); MetaCoreExpect(cameraDescriptor->ReflectedType != nullptr, "Camera descriptor should bind reflected type metadata"); MetaCoreExpect(static_cast(transformDescriptor->MutableComponent), "Transform descriptor should expose mutable component access"); MetaCoreExpect(static_cast(transformDescriptor->ConstComponent), "Transform descriptor should expose const component access"); MetaCoreExpect(static_cast(cameraDescriptor->MutableComponent), "Camera descriptor should expose mutable component access"); MetaCoreExpect(static_cast(cameraDescriptor->ConstComponent), "Camera descriptor should expose const component access"); MetaCore::MetaCoreScene scene; MetaCore::MetaCoreGameObject object = scene.CreateGameObject("DescriptorAccess"); void* transformComponent = transformDescriptor->MutableComponent(object); MetaCoreExpect(transformComponent != nullptr, "Transform descriptor should resolve a component pointer"); const auto positionFieldIterator = std::find_if( transformDescriptor->ReflectedType->Fields.begin(), transformDescriptor->ReflectedType->Fields.end(), [](const MetaCore::MetaCoreFieldDescriptor& field) { return field.Name == "Position"; } ); MetaCoreExpect( positionFieldIterator != transformDescriptor->ReflectedType->Fields.end(), "Transform descriptor should expose reflected Position field" ); auto* reflectedPosition = static_cast(positionFieldIterator->MutableValue(transformComponent)); *reflectedPosition = glm::vec3(7.0F, 8.0F, 9.0F); MetaCoreExpectVec3Near( object.GetComponent().Position, glm::vec3(7.0F, 8.0F, 9.0F), "Descriptor component pointer should work with reflected field access" ); MetaCoreExpect(cameraDescriptor->MutableComponent(object) == nullptr, "Camera access should be null before the component is added"); MetaCoreExpect(cameraDescriptor->AddComponent(object), "Camera descriptor should add a camera component"); MetaCoreExpect(cameraDescriptor->MutableComponent(object) != nullptr, "Camera access should resolve after the component is added"); const auto reflectionRegistry = moduleRegistry.ResolveService(); MetaCoreExpect(reflectionRegistry != nullptr, "Reflection registry should be available for static component registration"); const MetaCore::MetaCoreStructDescriptor* modelRootTagReflection = reflectionRegistry->GetTypeRegistry().FindStruct(); MetaCoreExpect(modelRootTagReflection != nullptr, "ModelRootTag reflection should be available"); MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor scriptDescriptor{}; scriptDescriptor.TypeId = "ModelRootTag"; scriptDescriptor.ReflectedType = modelRootTagReflection; scriptDescriptor.MutableComponent = [](MetaCore::MetaCoreGameObject& gameObject) -> void* { return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; }; scriptDescriptor.ConstComponent = [](const MetaCore::MetaCoreGameObject& gameObject) -> const void* { return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; }; scriptDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) { return gameObject.HasComponent(); }; scriptDescriptor.AddComponent = [](MetaCore::MetaCoreGameObject& gameObject) { if (gameObject.HasComponent()) { return false; } gameObject.AddComponent(); return true; }; scriptDescriptor.RemoveComponent = [](MetaCore::MetaCoreGameObject& gameObject) { if (!gameObject.HasComponent()) { return false; } gameObject.RemoveComponent(); return true; }; scriptDescriptor.ResetComponent = [](MetaCore::MetaCoreGameObject& gameObject) { if (!gameObject.HasComponent()) { return false; } gameObject.GetComponent() = MetaCore::MetaCoreModelRootTag{}; return true; }; MetaCoreExpect( componentRegistry->RegisterComponentDescriptor(scriptDescriptor), "Component registry should accept a static reflected component descriptor" ); MetaCoreExpect( !componentRegistry->RegisterComponentDescriptor(scriptDescriptor), "Component registry should reject duplicate component TypeId registration" ); const auto* registeredScriptDescriptor = componentRegistry->FindDescriptor("ModelRootTag"); MetaCoreExpect(registeredScriptDescriptor != nullptr, "Registered static component should be discoverable"); MetaCoreExpect(registeredScriptDescriptor->DisplayName == "ModelRootTag", "Registered component should default display name"); MetaCoreExpect(registeredScriptDescriptor->Category == "Scripts", "Registered component should default category"); MetaCoreExpect(registeredScriptDescriptor->AddComponent(object), "Registered component descriptor should add component"); MetaCoreExpect(registeredScriptDescriptor->HasComponent(object), "Registered component descriptor should report added component"); const auto sourcePathFieldIterator = std::find_if( registeredScriptDescriptor->ReflectedType->Fields.begin(), registeredScriptDescriptor->ReflectedType->Fields.end(), [](const MetaCore::MetaCoreFieldDescriptor& field) { return field.Name == "SourceModelPath"; } ); MetaCoreExpect( sourcePathFieldIterator != registeredScriptDescriptor->ReflectedType->Fields.end(), "Registered component should expose reflected SourceModelPath field" ); auto* modelSourcePath = static_cast( sourcePathFieldIterator->MutableValue(registeredScriptDescriptor->MutableComponent(object)) ); *modelSourcePath = "Assets/Models/Test.glb"; MetaCoreExpect( object.GetComponent().SourceModelPath == "Assets/Models/Test.glb", "Registered component should support reflected field mutation" ); MetaCore::MetaCoreFieldEditorMetadata speedMetadata{}; speedMetadata.DisplayName = "Speed"; speedMetadata.Group = "Script"; speedMetadata.Min = 0.0; speedMetadata.Max = 10.0; speedMetadata.Step = 0.25; speedMetadata.DefaultValue = "2.5"; MetaCore::MetaCoreFieldEditorMetadata targetMetadata{}; targetMetadata.DisplayName = "Target"; targetMetadata.Group = "Script"; targetMetadata.DefaultValue = "Player"; auto smokeScriptBuilder = MetaCore::MetaCoreRegisterGeneratedStruct( reflectionRegistry->AccessTypeRegistry(), "MetaCoreSmokeScriptComponent" ); smokeScriptBuilder .Field<&MetaCoreSmokeScriptComponent::Speed>("Speed", speedMetadata) .Field<&MetaCoreSmokeScriptComponent::Enabled>("Enabled") .Field<&MetaCoreSmokeScriptComponent::Target>("Target", targetMetadata) .Field<&MetaCoreSmokeScriptComponent::AlphaMode>("AlphaMode"); const MetaCore::MetaCoreStructDescriptor* smokeScriptReflection = reflectionRegistry->GetTypeRegistry().FindStruct(); MetaCoreExpect(smokeScriptReflection != nullptr, "Static C++ smoke script reflection should be registered"); MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor smokeScriptDescriptor{}; smokeScriptDescriptor.TypeId = "SmokeScript"; smokeScriptDescriptor.DisplayName = "Smoke Script"; smokeScriptDescriptor.ReflectedType = smokeScriptReflection; smokeScriptDescriptor.MutableComponent = [](MetaCore::MetaCoreGameObject& gameObject) -> void* { return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; }; smokeScriptDescriptor.ConstComponent = [](const MetaCore::MetaCoreGameObject& gameObject) -> const void* { return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; }; smokeScriptDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) { return gameObject.HasComponent(); }; smokeScriptDescriptor.AddComponent = [](MetaCore::MetaCoreGameObject& gameObject) { if (gameObject.HasComponent()) { return false; } gameObject.AddComponent(); return true; }; smokeScriptDescriptor.RemoveComponent = [](MetaCore::MetaCoreGameObject& gameObject) { if (!gameObject.HasComponent()) { return false; } gameObject.RemoveComponent(); return true; }; smokeScriptDescriptor.ResetComponent = [](MetaCore::MetaCoreGameObject& gameObject) { if (!gameObject.HasComponent()) { return false; } gameObject.GetComponent() = MetaCoreSmokeScriptComponent{}; return true; }; MetaCoreExpect( componentRegistry->RegisterComponentDescriptor(std::move(smokeScriptDescriptor)), "Component registry should accept a static C++ smoke script descriptor" ); const auto* registeredSmokeScriptDescriptor = componentRegistry->FindDescriptor("SmokeScript"); MetaCoreExpect(registeredSmokeScriptDescriptor != nullptr, "Static C++ smoke script should be discoverable"); MetaCoreExpect(registeredSmokeScriptDescriptor->Category == "Scripts", "Static C++ smoke script should default to Scripts category"); MetaCoreExpect( static_cast(registeredSmokeScriptDescriptor->CopyComponentPayload), "Static C++ smoke script should receive default reflected copy support" ); MetaCoreExpect( static_cast(registeredSmokeScriptDescriptor->PasteComponentPayload), "Static C++ smoke script should receive default reflected paste support" ); MetaCoreExpect(registeredSmokeScriptDescriptor->AddComponent(object), "Static C++ smoke script should be addable"); auto& smokeScript = object.GetComponent(); smokeScript.Speed = 6.75F; smokeScript.Enabled = false; smokeScript.Target = "Valve_A"; smokeScript.AlphaMode = MetaCore::MetaCoreMeshAlphaMode::Blend; const auto speedFieldIterator = std::find_if( registeredSmokeScriptDescriptor->ReflectedType->Fields.begin(), registeredSmokeScriptDescriptor->ReflectedType->Fields.end(), [](const MetaCore::MetaCoreFieldDescriptor& field) { return field.Name == "Speed"; } ); MetaCoreExpect( speedFieldIterator != registeredSmokeScriptDescriptor->ReflectedType->Fields.end(), "Static C++ smoke script should expose Speed field" ); MetaCoreExpect(speedFieldIterator->Editor.DefaultValue == "2.5", "Static C++ smoke script Speed should preserve default metadata"); auto* reflectedSpeed = static_cast(speedFieldIterator->MutableValue(registeredSmokeScriptDescriptor->MutableComponent(object))); *reflectedSpeed = 7.25F; MetaCoreExpect(std::abs(object.GetComponent().Speed - 7.25F) <= 0.0001F, "Static C++ smoke script field mutation should work"); MetaCoreExpect(componentRegistry->CopyComponent("SmokeScript", object), "Static C++ smoke script should copy through default reflected payload"); MetaCore::MetaCoreGameObject scriptPasteTarget = scene.CreateGameObject("SmokeScriptPasteTarget"); MetaCoreExpect(!scriptPasteTarget.HasComponent(), "Paste target should not start with smoke script"); MetaCoreExpect(componentRegistry->PasteComponent("SmokeScript", scriptPasteTarget), "Static C++ smoke script should paste through default reflected payload"); MetaCoreExpect(scriptPasteTarget.HasComponent(), "Paste should add static C++ smoke script to target"); const auto& pastedSmokeScript = scriptPasteTarget.GetComponent(); MetaCoreExpect(std::abs(pastedSmokeScript.Speed - 7.25F) <= 0.0001F, "Pasted static C++ smoke script Speed should match"); MetaCoreExpect(!pastedSmokeScript.Enabled, "Pasted static C++ smoke script Enabled should match"); MetaCoreExpect(pastedSmokeScript.Target == "Valve_A", "Pasted static C++ smoke script Target should match"); MetaCoreExpect(pastedSmokeScript.AlphaMode == MetaCore::MetaCoreMeshAlphaMode::Blend, "Pasted static C++ smoke script enum should match"); MetaCoreExpect(registeredSmokeScriptDescriptor->ResetComponent(scriptPasteTarget), "Static C++ smoke script should reset"); MetaCoreExpect(std::abs(scriptPasteTarget.GetComponent().Speed - 2.5F) <= 0.0001F, "Reset static C++ smoke script Speed should return to default"); MetaCoreExpect(scriptPasteTarget.GetComponent().Target == "Player", "Reset static C++ smoke script Target should return to default"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); } void MetaCoreTestReflectionFieldEditorDescriptors() { MetaCore::MetaCoreTypeRegistry registry; MetaCoreRegisterSceneGeneratedTypes(registry); const MetaCore::MetaCoreStructDescriptor* transformDescriptor = registry.FindStruct(); MetaCoreExpect(transformDescriptor != nullptr, "Transform reflection descriptor should exist"); const auto findField = [&](std::string_view name) -> const MetaCore::MetaCoreFieldDescriptor* { const auto iterator = std::find_if( transformDescriptor->Fields.begin(), transformDescriptor->Fields.end(), [&](const MetaCore::MetaCoreFieldDescriptor& field) { return field.Name == name; } ); return iterator == transformDescriptor->Fields.end() ? nullptr : &(*iterator); }; const MetaCore::MetaCoreFieldDescriptor* positionField = findField("Position"); MetaCoreExpect(positionField != nullptr, "Position field descriptor should exist"); MetaCoreExpect(positionField->ValueKind == MetaCore::MetaCoreFieldValueKind::Vec3, "Position field should be Vec3"); MetaCoreExpect(!positionField->TypeName.empty(), "Position field should expose a type name"); MetaCoreExpect(static_cast(positionField->MutableValue), "Position field should expose mutable access"); MetaCoreExpect(static_cast(positionField->ConstValue), "Position field should expose const access"); MetaCoreExpect(positionField->Editor.DisplayName == "Position", "Position field should preserve display metadata"); MetaCoreExpect(positionField->Editor.Group == "Transform", "Position field should preserve group metadata"); MetaCoreExpect( positionField->Editor.RawSpec.find("Group") != std::string::npos, "Position field should retain MC_PROPERTY metadata spec" ); MetaCore::MetaCoreTransformComponent transform; auto* mutablePosition = static_cast(positionField->MutableValue(&transform)); *mutablePosition = glm::vec3(4.0F, 5.0F, 6.0F); MetaCoreExpectVec3Near(transform.Position, glm::vec3(4.0F, 5.0F, 6.0F), "MutableValue should edit the field"); const auto* constPosition = static_cast(positionField->ConstValue(&transform)); MetaCoreExpectVec3Near(*constPosition, glm::vec3(4.0F, 5.0F, 6.0F), "ConstValue should read the field"); const MetaCore::MetaCoreStructDescriptor* cameraDescriptor = registry.FindStruct(); MetaCoreExpect(cameraDescriptor != nullptr, "Camera reflection descriptor should exist"); const auto fovFieldIterator = std::find_if( cameraDescriptor->Fields.begin(), cameraDescriptor->Fields.end(), [](const MetaCore::MetaCoreFieldDescriptor& field) { return field.Name == "FieldOfViewDegrees"; } ); MetaCoreExpect( fovFieldIterator != cameraDescriptor->Fields.end(), "Camera FOV field descriptor should exist" ); MetaCoreExpect(fovFieldIterator->Editor.DisplayName == "Field Of View", "Camera FOV should preserve display metadata"); MetaCoreExpect(fovFieldIterator->Editor.Group == "Camera", "Camera FOV should preserve group metadata"); MetaCoreExpect(fovFieldIterator->Editor.Min.has_value() && std::abs(*fovFieldIterator->Editor.Min - 1.0) <= 0.0001, "Camera FOV should preserve min metadata"); MetaCoreExpect(fovFieldIterator->Editor.Max.has_value() && std::abs(*fovFieldIterator->Editor.Max - 179.0) <= 0.0001, "Camera FOV should preserve max metadata"); MetaCoreExpect(fovFieldIterator->Editor.Step.has_value() && std::abs(*fovFieldIterator->Editor.Step - 1.0) <= 0.0001, "Camera FOV should preserve step metadata"); MetaCoreExpect(fovFieldIterator->Editor.DefaultValue == "60.0", "Camera FOV should preserve default value metadata"); const MetaCore::MetaCoreEnumDescriptor* alphaModeEnum = registry.FindEnum(); MetaCoreExpect(alphaModeEnum != nullptr, "Alpha mode enum reflection descriptor should exist"); MetaCoreExpect(alphaModeEnum->Values.size() == 3, "Alpha mode enum should expose generated values"); MetaCoreExpect( std::any_of( alphaModeEnum->Values.begin(), alphaModeEnum->Values.end(), [](const MetaCore::MetaCoreEnumValueDescriptor& value) { return value.Name == "Blend" && value.Value == static_cast(MetaCore::MetaCoreMeshAlphaMode::Blend); } ), "Alpha mode enum should expose Blend value" ); const MetaCore::MetaCoreStructDescriptor* meshRendererDescriptor = registry.FindStruct(); MetaCoreExpect(meshRendererDescriptor != nullptr, "MeshRenderer reflection descriptor should exist"); const auto alphaModeFieldIterator = std::find_if( meshRendererDescriptor->Fields.begin(), meshRendererDescriptor->Fields.end(), [](const MetaCore::MetaCoreFieldDescriptor& field) { return field.Name == "AlphaMode"; } ); MetaCoreExpect( alphaModeFieldIterator != meshRendererDescriptor->Fields.end(), "MeshRenderer AlphaMode field descriptor should exist" ); MetaCoreExpect( alphaModeFieldIterator->ValueKind == MetaCore::MetaCoreFieldValueKind::Enum, "MeshRenderer AlphaMode field should be reflected as enum" ); MetaCoreExpect( alphaModeFieldIterator->Size == sizeof(MetaCore::MetaCoreMeshAlphaMode), "Reflected enum field should retain field size for Inspector writes" ); } void MetaCoreTestPlayModeLifecycleAndSceneIsolation() { 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); const auto componentRegistry = moduleRegistry.ResolveService(); const auto reflectionRegistry = moduleRegistry.ResolveService(); const auto playModeService = moduleRegistry.ResolveService(); const auto clipboardService = moduleRegistry.ResolveService(); MetaCoreExpect(componentRegistry != nullptr, "Component registry should be available for play mode lifecycle test"); MetaCoreExpect(reflectionRegistry != nullptr, "Reflection registry should be available for play mode lifecycle test"); MetaCoreExpect(playModeService != nullptr, "Play mode service should be available"); MetaCoreExpect(clipboardService != nullptr, "Clipboard service should be available for runtime deletion test"); MetaCore::MetaCoreGameObject object = scene.CreateGameObject("LifecycleObject"); object.GetComponent().Position = glm::vec3(1.0F, 2.0F, 3.0F); object.AddComponent().SourceModelPath = "Assets/Models/Source.glb"; const MetaCore::MetaCoreId objectId = object.GetId(); MetaCore::MetaCoreGameObject runtimeDeletedObject = scene.CreateGameObject("RuntimeDeletedObject"); runtimeDeletedObject.AddComponent(MetaCore::MetaCoreCameraComponent{}); const MetaCore::MetaCoreId runtimeDeletedObjectId = runtimeDeletedObject.GetId(); MetaCore::MetaCoreGameObject runtimeComponentRemovedObject = scene.CreateGameObject("RuntimeComponentRemovedObject"); runtimeComponentRemovedObject.AddComponent(MetaCore::MetaCoreCameraComponent{}); const MetaCore::MetaCoreId runtimeComponentRemovedObjectId = runtimeComponentRemovedObject.GetId(); int runtimeStartCount = 0; int runtimeUpdateCount = 0; int runtimeDestroyCount = 0; std::vector runtimeLifecycleErrors; MetaCore::MetaCoreRuntimeLifecycleExecutor runtimeLifecycleExecutor; MetaCore::MetaCoreRuntimeLifecycleDescriptor runtimeLifecycleDescriptor{}; runtimeLifecycleDescriptor.TypeId = "RuntimeModelRootTag"; runtimeLifecycleDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) { return gameObject.HasComponent(); }; runtimeLifecycleDescriptor.OnStart = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&) { ++runtimeStartCount; }; runtimeLifecycleDescriptor.OnUpdate = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&, float) { ++runtimeUpdateCount; throw std::runtime_error("runtime executor fault"); }; runtimeLifecycleDescriptor.OnDestroy = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&) { ++runtimeDestroyCount; }; MetaCoreExpect( runtimeLifecycleExecutor.RegisterDescriptor(std::move(runtimeLifecycleDescriptor)), "Runtime lifecycle executor should register a descriptor" ); const auto runtimeErrorSink = [&](const MetaCore::MetaCoreRuntimeLifecycleError& error) { runtimeLifecycleErrors.push_back(error); }; runtimeLifecycleExecutor.EnterScene(scene, runtimeErrorSink); runtimeLifecycleExecutor.TickScene(scene, 0.25F, runtimeErrorSink); runtimeLifecycleExecutor.ExitScene(scene, runtimeErrorSink); MetaCoreExpect(runtimeStartCount == 1, "Runtime lifecycle executor should run OnStart"); MetaCoreExpect(runtimeUpdateCount == 1, "Runtime lifecycle executor should run OnUpdate"); MetaCoreExpect(runtimeDestroyCount == 1, "Runtime lifecycle executor should run OnDestroy"); MetaCoreExpect(!runtimeLifecycleExecutor.IsRunning(), "Runtime lifecycle executor should stop after ExitScene"); MetaCoreExpect(!runtimeLifecycleErrors.empty(), "Runtime lifecycle executor should report callback exceptions"); MetaCoreExpect( runtimeLifecycleErrors.front().Message.find("runtime executor fault") != std::string::npos, "Runtime lifecycle executor should preserve exception messages" ); MetaCore::MetaCoreGameObject transientObject = scene.CreateGameObject("TransientLifecycleObject"); transientObject.AddComponent().SourceModelPath = "Assets/Models/Transient.glb"; const MetaCore::MetaCoreId transientObjectId = transientObject.GetId(); int transientDestroyCount = 0; MetaCore::MetaCoreRuntimeLifecycleExecutor transientLifecycleExecutor; MetaCore::MetaCoreRuntimeLifecycleDescriptor transientLifecycleDescriptor{}; transientLifecycleDescriptor.TypeId = "TransientRuntimeModelRootTag"; transientLifecycleDescriptor.HasComponent = [transientObjectId](const MetaCore::MetaCoreGameObject& gameObject) { return gameObject.GetId() == transientObjectId && gameObject.HasComponent(); }; transientLifecycleDescriptor.OnDestroy = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&) { ++transientDestroyCount; }; MetaCoreExpect( transientLifecycleExecutor.RegisterDescriptor(std::move(transientLifecycleDescriptor)), "Runtime lifecycle executor should register transient descriptor" ); transientLifecycleExecutor.EnterScene(scene, runtimeErrorSink); transientLifecycleExecutor.NotifyGameObjectsWillBeDestroyed(scene, {transientObjectId}, runtimeErrorSink); MetaCoreExpect(transientDestroyCount == 1, "Runtime lifecycle executor should destroy before runtime object delete"); const std::vector transientDeletedIds = scene.DeleteGameObjects({transientObjectId}); MetaCoreExpect(!transientDeletedIds.empty(), "Transient runtime object should be deleted after destroy notification"); transientLifecycleExecutor.ExitScene(scene, runtimeErrorSink); MetaCoreExpect( transientDestroyCount == 1, "Runtime lifecycle executor should not destroy a notified object again on ExitScene" ); int registryStartCount = 0; auto& runtimeLifecycleRegistry = MetaCore::MetaCoreGetRuntimeLifecycleRegistry(); runtimeLifecycleRegistry.Clear(); MetaCore::MetaCoreRuntimeLifecycleDescriptor registryLifecycleDescriptor{}; registryLifecycleDescriptor.TypeId = "RegisteredRuntimeModelRootTag"; registryLifecycleDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) { return gameObject.HasComponent(); }; registryLifecycleDescriptor.OnStart = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&) { ++registryStartCount; }; MetaCoreExpect( runtimeLifecycleRegistry.RegisterDescriptor(std::move(registryLifecycleDescriptor)), "Runtime lifecycle registry should accept a valid static descriptor" ); MetaCoreExpect( !runtimeLifecycleRegistry.RegisterDescriptor(MetaCore::MetaCoreRuntimeLifecycleDescriptor{}), "Runtime lifecycle registry should reject invalid descriptors" ); MetaCore::MetaCoreRuntimeLifecycleExecutor registeredRuntimeLifecycleExecutor; MetaCoreExpect( registeredRuntimeLifecycleExecutor.RegisterDescriptors(runtimeLifecycleRegistry.GetDescriptors()) == 1, "Runtime lifecycle executor should import descriptors from the runtime registry" ); MetaCoreExpect( registeredRuntimeLifecycleExecutor.RegisterDescriptors(runtimeLifecycleRegistry.GetDescriptors()) == 0, "Runtime lifecycle executor should reject duplicate registry descriptors" ); registeredRuntimeLifecycleExecutor.EnterScene(scene, runtimeErrorSink); registeredRuntimeLifecycleExecutor.ExitScene(scene, runtimeErrorSink); MetaCoreExpect(registryStartCount == 1, "Registered runtime lifecycle descriptor should execute OnStart"); runtimeLifecycleRegistry.Clear(); MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); editorContext.SelectOnly(objectId); int startCount = 0; int updateCount = 0; int destroyCount = 0; int drawGizmosCount = 0; int drawSelectedGizmosCount = 0; int faultyDrawGizmosCount = 0; int runtimeDeleteStartCount = 0; int runtimeDeleteDestroyCount = 0; MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor lifecycleDescriptor{}; lifecycleDescriptor.TypeId = "LifecycleModelRootTag"; lifecycleDescriptor.DisplayName = "Lifecycle Model Root Tag"; lifecycleDescriptor.Category = "Scripts"; lifecycleDescriptor.ReflectedType = reflectionRegistry->GetTypeRegistry().FindStruct(); lifecycleDescriptor.MutableComponent = [](MetaCore::MetaCoreGameObject& gameObject) -> void* { return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; }; lifecycleDescriptor.ConstComponent = [](const MetaCore::MetaCoreGameObject& gameObject) -> const void* { return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; }; lifecycleDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) { return gameObject.HasComponent(); }; lifecycleDescriptor.AddComponent = [](MetaCore::MetaCoreGameObject& gameObject) { if (gameObject.HasComponent()) { return false; } gameObject.AddComponent(); return true; }; lifecycleDescriptor.RemoveComponent = [](MetaCore::MetaCoreGameObject& gameObject) { if (!gameObject.HasComponent()) { return false; } gameObject.RemoveComponent(); return true; }; lifecycleDescriptor.Lifecycle.OnStart = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject& gameObject) { ++startCount; gameObject.GetComponent().Position.x = 10.0F; }; lifecycleDescriptor.Lifecycle.OnUpdate = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject& gameObject, float deltaSeconds) { ++updateCount; gameObject.GetComponent().Position.y += deltaSeconds; }; lifecycleDescriptor.Lifecycle.OnDestroy = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) { ++destroyCount; }; lifecycleDescriptor.Lifecycle.OnDrawGizmos = [&](MetaCore::MetaCoreEditorContext& context, MetaCore::MetaCoreGameObject&) { ++drawGizmosCount; context.DrawGizmoLine( glm::vec3(0.0F), glm::vec3(1.0F, 0.0F, 0.0F), glm::vec4(1.0F, 0.0F, 0.0F, 1.0F) ); }; lifecycleDescriptor.Lifecycle.OnDrawGizmosSelected = [&](MetaCore::MetaCoreEditorContext& context, MetaCore::MetaCoreGameObject&) { ++drawSelectedGizmosCount; context.DrawGizmoWireBox( glm::vec3(0.0F), glm::vec3(2.0F), glm::vec4(0.0F, 1.0F, 0.0F, 1.0F) ); context.DrawGizmoIcon( glm::vec3(0.0F, 1.0F, 0.0F), "Lifecycle", glm::vec4(0.0F, 0.5F, 1.0F, 1.0F) ); }; MetaCoreExpect( componentRegistry->RegisterComponentDescriptor(std::move(lifecycleDescriptor)), "Play mode lifecycle component descriptor should register" ); MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor faultyLifecycleDescriptor{}; faultyLifecycleDescriptor.TypeId = "FaultyLifecycleModelRootTag"; faultyLifecycleDescriptor.DisplayName = "Faulty Lifecycle Model Root Tag"; faultyLifecycleDescriptor.Category = "Scripts"; faultyLifecycleDescriptor.ReflectedType = reflectionRegistry->GetTypeRegistry().FindStruct(); faultyLifecycleDescriptor.MutableComponent = [](MetaCore::MetaCoreGameObject& gameObject) -> void* { return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; }; faultyLifecycleDescriptor.ConstComponent = [](const MetaCore::MetaCoreGameObject& gameObject) -> const void* { return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; }; faultyLifecycleDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) { return gameObject.HasComponent(); }; faultyLifecycleDescriptor.Lifecycle.OnUpdate = []( MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&, float ) { throw std::runtime_error("intentional lifecycle fault"); }; faultyLifecycleDescriptor.Lifecycle.OnDrawGizmos = [&]( MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject& ) { ++faultyDrawGizmosCount; throw std::runtime_error("intentional gizmo fault"); }; MetaCoreExpect( componentRegistry->RegisterComponentDescriptor(std::move(faultyLifecycleDescriptor)), "Faulty play mode lifecycle descriptor should register" ); MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor runtimeDeleteDescriptor{}; runtimeDeleteDescriptor.TypeId = "LifecycleCamera"; runtimeDeleteDescriptor.DisplayName = "Lifecycle Camera"; runtimeDeleteDescriptor.Category = "Scripts"; runtimeDeleteDescriptor.ReflectedType = reflectionRegistry->GetTypeRegistry().FindStruct(); runtimeDeleteDescriptor.MutableComponent = [](MetaCore::MetaCoreGameObject& gameObject) -> void* { return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; }; runtimeDeleteDescriptor.ConstComponent = [](const MetaCore::MetaCoreGameObject& gameObject) -> const void* { return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; }; runtimeDeleteDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) { return gameObject.HasComponent(); }; runtimeDeleteDescriptor.Lifecycle.OnStart = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) { ++runtimeDeleteStartCount; }; runtimeDeleteDescriptor.Lifecycle.OnDestroy = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) { ++runtimeDeleteDestroyCount; }; MetaCoreExpect( componentRegistry->RegisterComponentDescriptor(std::move(runtimeDeleteDescriptor)), "Runtime delete lifecycle descriptor should register" ); MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Edit, "Initial play mode state should be Edit"); MetaCoreExpect(!playModeService->IsRuntimeSceneActive(), "Runtime scene should be inactive in edit mode"); MetaCoreExpect(!editorContext.IsRuntimeSceneActive(), "Editor context should start in edit scene mode"); componentRegistry->DrawComponentGizmos(editorContext); MetaCoreExpect(drawGizmosCount == 1, "OnDrawGizmos should run for registered static C++ components"); MetaCoreExpect(drawSelectedGizmosCount == 1, "OnDrawGizmosSelected should run for selected registered static C++ components"); MetaCoreExpect( faultyDrawGizmosCount == 1, "Component gizmo failures should be isolated without stopping other gizmo callbacks" ); MetaCoreExpect( editorContext.GetComponentGizmoLines().size() == 13, "Component gizmos should queue lines and wire boxes for the Scene View" ); MetaCoreExpect( editorContext.GetComponentGizmoIcons().size() == 1 && editorContext.GetComponentGizmoIcons().front().Label == "Lifecycle", "Component gizmos should queue labeled Scene View icons" ); editorContext.ClearComponentGizmoDrawList(); MetaCoreExpect( editorContext.GetComponentGizmoLines().empty() && editorContext.GetComponentGizmoIcons().empty(), "Component gizmo draw list should clear between Scene View frames" ); MetaCoreExpect( editorContext.ExecuteSnapshotCommand("Pre-play edit command", [&]() { scene.FindGameObject(objectId).GetComponent().Scale = glm::vec3(2.0F); return true; }), "Edit mode commands should be recorded before entering play mode" ); const std::size_t undoCountBeforePlay = editorContext.GetCommandService().GetUndoCount(); MetaCoreExpect(undoCountBeforePlay > 0, "Pre-play edit command should be present in undo stack"); MetaCoreExpect(playModeService->EnterPlayMode(editorContext), "EnterPlayMode should succeed"); MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Playing, "Play mode state should be Playing"); MetaCoreExpect(playModeService->IsRuntimeSceneActive(), "Runtime scene should be active in play mode"); MetaCoreExpect(editorContext.IsRuntimeSceneActive(), "Editor context should expose runtime scene mode while playing"); MetaCoreExpect(startCount == 1, "OnStart should run once on enter play mode"); MetaCoreExpect(runtimeDeleteStartCount == 2, "Runtime delete lifecycle objects should start on enter play mode"); MetaCoreExpectVec3Near( scene.FindGameObject(objectId).GetComponent().Position, glm::vec3(10.0F, 2.0F, 3.0F), "OnStart should be able to mutate runtime scene state" ); MetaCoreExpect( editorContext.ExecuteSnapshotCommand("Runtime-only edit command", [&]() { scene.FindGameObject(objectId).GetComponent().Position.z = 99.0F; return true; }), "Runtime scene commands should still mutate the runtime scene" ); MetaCoreExpect( editorContext.GetCommandService().GetUndoCount() == undoCountBeforePlay, "Runtime scene commands should not be pushed into the edit undo stack" ); MetaCoreExpect(!editorContext.UndoCommand(), "Undo should be disabled while runtime scene is active"); MetaCoreExpectVec3Near( scene.FindGameObject(objectId).GetComponent().Position, glm::vec3(10.0F, 2.0F, 99.0F), "Runtime-only edit should affect only the runtime scene" ); playModeService->TickPlayMode(editorContext, 0.5F); MetaCoreExpect(startCount == 1, "OnStart should not repeat during tick"); MetaCoreExpect(updateCount == 1, "OnUpdate should run during play tick"); const auto hasLifecycleErrorLog = [&]() { return std::any_of( logService.GetEntries().begin(), logService.GetEntries().end(), [](const MetaCore::MetaCoreLogEntry& entry) { return entry.Level == MetaCore::MetaCoreLogLevel::Error && entry.Category == "PlayMode" && entry.Message.find("intentional lifecycle fault") != std::string::npos; } ); }; MetaCoreExpect(hasLifecycleErrorLog(), "Lifecycle exceptions should be reported to the PlayMode console log"); MetaCoreExpectVec3Near( scene.FindGameObject(objectId).GetComponent().Position, glm::vec3(10.0F, 2.5F, 99.0F), "OnUpdate should mutate runtime scene state" ); MetaCoreExpect(std::abs(playModeService->GetElapsedPlayTimeSeconds() - 0.5F) <= 0.0001F, "Play time should advance during play tick"); MetaCore::MetaCoreGameObject runtimeComponentRemovedGameObject = scene.FindGameObject(runtimeComponentRemovedObjectId); MetaCoreExpect( runtimeComponentRemovedGameObject && runtimeComponentRemovedGameObject.HasComponent(), "Runtime component removal test object should have a Camera component" ); playModeService->NotifyComponentWillBeRemoved( editorContext, runtimeComponentRemovedObjectId, "LifecycleCamera" ); runtimeComponentRemovedGameObject.RemoveComponent(); MetaCoreExpect( runtimeDeleteDestroyCount == 1, "Runtime component removal should notify lifecycle OnDestroy before removing the component" ); editorContext.SelectOnly(runtimeDeletedObjectId); MetaCoreExpect(clipboardService->DeleteSelection(editorContext), "Runtime DeleteSelection should delete the selected runtime object"); MetaCoreExpect(!scene.FindGameObject(runtimeDeletedObjectId), "Deleted runtime object should be removed from the runtime scene"); MetaCoreExpect( runtimeDeleteDestroyCount == 2, "Runtime DeleteSelection should notify lifecycle OnDestroy before deleting the object" ); editorContext.SelectOnly(objectId); MetaCoreExpect(playModeService->PausePlayMode(editorContext), "PausePlayMode should succeed"); MetaCoreExpect(playModeService->IsRuntimeSceneActive(), "Runtime scene should remain active while paused"); playModeService->TickPlayMode(editorContext, 0.25F); MetaCoreExpect(updateCount == 1, "OnUpdate should not run while paused"); MetaCoreExpect(std::abs(playModeService->GetElapsedPlayTimeSeconds() - 0.5F) <= 0.0001F, "Play time should not advance during paused tick"); MetaCoreExpect(playModeService->StepPlayMode(editorContext, 0.125F), "StepPlayMode should succeed while paused"); MetaCoreExpect(updateCount == 2, "OnUpdate should run for a paused step"); MetaCoreExpect(std::abs(playModeService->GetElapsedPlayTimeSeconds() - 0.625F) <= 0.0001F, "Play time should advance by step delta"); MetaCoreExpect(playModeService->ResumePlayMode(editorContext), "ResumePlayMode should succeed"); playModeService->TickPlayMode(editorContext, 0.25F); MetaCoreExpect(updateCount == 3, "OnUpdate should resume after pause"); MetaCoreExpect(playModeService->ExitPlayMode(editorContext), "ExitPlayMode should succeed"); MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Edit, "Play mode state should return to Edit"); MetaCoreExpect(!playModeService->IsRuntimeSceneActive(), "Runtime scene should be inactive after exiting play mode"); MetaCoreExpect(!editorContext.IsRuntimeSceneActive(), "Editor context should return to edit scene mode after exiting play mode"); MetaCoreExpect(destroyCount == 1, "OnDestroy should run on exit play mode"); MetaCoreExpect( runtimeDeleteDestroyCount == 2, "Runtime removed components and deleted objects should not receive duplicate OnDestroy on exit play mode" ); MetaCoreExpect( scene.FindGameObject(runtimeDeletedObjectId), "ExitPlayMode should restore runtime-deleted objects from the edit scene snapshot" ); MetaCoreExpect( scene.FindGameObject(runtimeComponentRemovedObjectId).HasComponent(), "ExitPlayMode should restore runtime-removed components from the edit scene snapshot" ); MetaCoreExpectVec3Near( scene.FindGameObject(objectId).GetComponent().Position, glm::vec3(1.0F, 2.0F, 3.0F), "ExitPlayMode should restore edit scene snapshot" ); MetaCoreExpectVec3Near( scene.FindGameObject(objectId).GetComponent().Scale, glm::vec3(2.0F), "ExitPlayMode should keep pre-play edit scene changes" ); MetaCoreExpect( editorContext.GetCommandService().GetUndoCount() == undoCountBeforePlay, "ExitPlayMode should preserve the pre-play edit undo stack without runtime commands" ); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); } void MetaCoreTestSceneGizmoMultiSelectDeltaAndUndo() { 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::MetaCoreGameObject parent = scene.CreateGameObject("GizmoParent"); parent.GetComponent().Position = glm::vec3(1.0F, 0.0F, 0.0F); const MetaCore::MetaCoreId parentId = parent.GetId(); MetaCore::MetaCoreGameObject child = scene.CreateGameObject("GizmoChild", parentId); child.GetComponent().Position = glm::vec3(0.0F, 2.0F, 0.0F); const MetaCore::MetaCoreId childId = child.GetId(); MetaCore::MetaCoreGameObject sibling = scene.CreateGameObject("GizmoSibling"); sibling.GetComponent().Position = glm::vec3(5.0F, 0.0F, 0.0F); const MetaCore::MetaCoreId siblingId = sibling.GetId(); MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); MetaCore::MetaCoreSceneInteractionService sceneInteractionService; editorContext.SetSelection({parentId, childId, siblingId}, parentId); const glm::mat4 originalParentWorld = MetaCore::MetaCoreBuildTransformMatrix(scene.FindGameObject(parentId).GetComponent()); const glm::mat4 movedParentWorld = glm::translate(glm::mat4(1.0F), glm::vec3(3.0F, 0.0F, 0.0F)); sceneInteractionService.HandleGizmoBeginUse(editorContext, true); MetaCoreExpect( sceneInteractionService.ApplyWorldTransformDeltaToSelection(editorContext, parentId, originalParentWorld, movedParentWorld), "Gizmo delta should apply to selected transform roots" ); sceneInteractionService.HandleGizmoEndUse(editorContext, true); sceneInteractionService.HandleGizmoEndUse(editorContext, false); MetaCoreExpectVec3Near( scene.FindGameObject(parentId).GetComponent().Position, glm::vec3(3.0F, 0.0F, 0.0F), "Gizmo delta should move active selected root" ); MetaCoreExpectVec3Near( scene.FindGameObject(childId).GetComponent().Position, glm::vec3(0.0F, 2.0F, 0.0F), "Gizmo delta should not double-apply to selected children of selected roots" ); MetaCoreExpectVec3Near( scene.FindGameObject(siblingId).GetComponent().Position, glm::vec3(7.0F, 0.0F, 0.0F), "Gizmo delta should move other selected roots by the same world delta" ); MetaCoreExpect(editorContext.GetCommandService().CanUndo(), "Completed gizmo drag should create an undo command"); MetaCoreExpect(editorContext.UndoCommand(), "Undo gizmo drag should succeed"); MetaCoreExpectVec3Near( scene.FindGameObject(parentId).GetComponent().Position, glm::vec3(1.0F, 0.0F, 0.0F), "Undo gizmo drag should restore active root" ); MetaCoreExpectVec3Near( scene.FindGameObject(siblingId).GetComponent().Position, glm::vec3(5.0F, 0.0F, 0.0F), "Undo gizmo drag should restore other selected root" ); MetaCoreExpect(editorContext.RedoCommand(), "Redo gizmo drag should succeed"); MetaCoreExpectVec3Near( scene.FindGameObject(parentId).GetComponent().Position, glm::vec3(3.0F, 0.0F, 0.0F), "Redo gizmo drag should reapply active root transform" ); MetaCoreExpectVec3Near( scene.FindGameObject(siblingId).GetComponent().Position, glm::vec3(7.0F, 0.0F, 0.0F), "Redo gizmo drag should reapply other selected root transform" ); MetaCoreExpect( editorContext.GetGizmoPivotMode() == MetaCore::MetaCoreGizmoPivotMode::Pivot, "Gizmo pivot mode should default to Pivot" ); editorContext.SetGizmoPivotMode(MetaCore::MetaCoreGizmoPivotMode::Center); MetaCoreExpect( editorContext.GetGizmoPivotMode() == MetaCore::MetaCoreGizmoPivotMode::Center, "Gizmo pivot mode should switch to Center" ); const glm::mat4 originalCenterWorld = glm::translate(glm::mat4(1.0F), glm::vec3(5.0F, 0.0F, 0.0F)); const glm::mat4 rotatedCenterWorld = originalCenterWorld * glm::rotate(glm::mat4(1.0F), glm::radians(90.0F), glm::vec3(0.0F, 0.0F, 1.0F)); sceneInteractionService.HandleGizmoBeginUse(editorContext, true); MetaCoreExpect( sceneInteractionService.ApplyWorldTransformDeltaToSelection( editorContext, parentId, originalCenterWorld, rotatedCenterWorld, MetaCore::MetaCoreGizmoPivotMode::Center ), "Center gizmo delta should apply around the shared selection center" ); sceneInteractionService.HandleGizmoEndUse(editorContext, true); sceneInteractionService.HandleGizmoEndUse(editorContext, false); MetaCoreExpectVec3Near( scene.FindGameObject(parentId).GetComponent().Position, glm::vec3(5.0F, -2.0F, 0.0F), "Center rotation should rotate active root around shared center" ); MetaCoreExpectVec3Near( scene.FindGameObject(siblingId).GetComponent().Position, glm::vec3(5.0F, 2.0F, 0.0F), "Center rotation should rotate other selected root around shared center" ); MetaCoreExpectVec3Near( scene.FindGameObject(childId).GetComponent().Position, glm::vec3(0.0F, 2.0F, 0.0F), "Center rotation should not double-apply selected child local transform" ); MetaCoreExpect(editorContext.UndoCommand(), "Undo center gizmo rotation should succeed"); MetaCoreExpectVec3Near( scene.FindGameObject(parentId).GetComponent().Position, glm::vec3(3.0F, 0.0F, 0.0F), "Undo center rotation should restore active root" ); MetaCoreExpectVec3Near( scene.FindGameObject(siblingId).GetComponent().Position, glm::vec3(7.0F, 0.0F, 0.0F), "Undo center rotation should restore other root" ); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); } void MetaCoreTestSceneViewportHoverPicking() { MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene; MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; MetaCore::MetaCoreGameObject hoveredObject = scene.CreateGameObject("HoverTarget"); hoveredObject.AddComponent(MetaCore::MetaCoreMeshRendererComponent{}); const MetaCore::MetaCoreId hoveredObjectId = hoveredObject.GetId(); MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); MetaCore::MetaCoreSceneViewportState& viewportState = editorContext.GetSceneViewportState(); viewportState.Left = 0.0F; viewportState.Top = 0.0F; viewportState.Width = 128.0F; viewportState.Height = 128.0F; viewportState.Hovered = true; viewportState.Focused = true; editorContext.GetInput().SetCursorPosition(glm::vec2(64.0F, 64.0F)); MetaCore::MetaCoreSceneView sceneView; sceneView.CameraPosition = glm::vec3(0.0F, 0.0F, 5.0F); sceneView.CameraTarget = glm::vec3(0.0F, 0.0F, 0.0F); sceneView.CameraUp = glm::vec3(0.0F, 1.0F, 0.0F); sceneView.VerticalFieldOfViewDegrees = 60.0F; MetaCore::MetaCoreSceneInteractionService sceneInteractionService; sceneInteractionService.UpdateViewportHover(editorContext, sceneView, true); MetaCoreExpect(editorContext.GetHoveredObjectId() == hoveredObjectId, "Scene viewport hover should track picked object"); MetaCoreExpect(editorContext.IsObjectHovered(hoveredObjectId), "Hovered object predicate should match picked object"); MetaCoreExpect( editorContext.GetShowViewportSelectionOverlay(), "Viewport selection overlay should be enabled by default" ); const MetaCore::MetaCoreEditorStateSnapshot snapshot = editorContext.CaptureStateSnapshot(); editorContext.ClearHoveredObject(); editorContext.SetShowViewportSelectionOverlay(false); editorContext.RestoreStateSnapshot(snapshot); MetaCoreExpect( editorContext.GetHoveredObjectId() == 0, "Editor state snapshot restore should not restore transient hover state" ); MetaCoreExpect( !editorContext.GetShowViewportSelectionOverlay(), "Editor state snapshot restore should not restore transient viewport overlay state" ); editorContext.SetShowViewportSelectionOverlay(true); MetaCoreExpect( editorContext.GetShowViewportSelectionOverlay(), "Viewport selection overlay flag should be mutable" ); editorContext.SetHoveredObjectId(hoveredObjectId); sceneInteractionService.UpdateViewportHover(editorContext, sceneView, false); MetaCoreExpect(editorContext.GetHoveredObjectId() == 0, "Scene viewport hover should clear when picking is blocked"); editorContext.SetHoveredObjectId(hoveredObjectId); viewportState.Hovered = false; sceneInteractionService.UpdateViewportHover(editorContext, sceneView, true); MetaCoreExpect(editorContext.GetHoveredObjectId() == 0, "Scene viewport hover should clear outside the viewport"); } void MetaCoreTestGizmoSnapSettings() { MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene; MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); MetaCoreExpect(!editorContext.GetGizmoSnapSettings().Enabled, "Gizmo snap should be disabled by default"); MetaCoreExpect( std::abs(editorContext.GetGizmoSnapSettings().StepForOperation(MetaCore::MetaCoreGizmoOperation::Translate) - 0.5F) < 0.0001F, "Default translate snap step should be stable" ); MetaCoreExpect( std::abs(editorContext.GetGizmoSnapSettings().StepForOperation(MetaCore::MetaCoreGizmoOperation::Rotate) - 15.0F) < 0.0001F, "Default rotate snap step should be stable" ); MetaCoreExpect( std::abs(editorContext.GetGizmoSnapSettings().StepForOperation(MetaCore::MetaCoreGizmoOperation::Scale) - 0.1F) < 0.0001F, "Default scale snap step should be stable" ); editorContext.SetGizmoSnapEnabled(true); MetaCoreExpect(editorContext.GetGizmoSnapSettings().Enabled, "Gizmo snap enabled flag should be mutable"); MetaCore::MetaCoreGizmoSnapSettings customSnap; customSnap.Enabled = true; customSnap.TranslationStep = 0.25F; customSnap.RotationStepDegrees = 30.0F; customSnap.ScaleStep = 0.2F; editorContext.SetGizmoSnapSettings(customSnap); MetaCoreExpect( std::abs(editorContext.GetGizmoSnapSettings().StepForOperation(MetaCore::MetaCoreGizmoOperation::Translate) - 0.25F) < 0.0001F, "Custom translate snap step should be used" ); MetaCoreExpect( std::abs(editorContext.GetGizmoSnapSettings().StepForOperation(MetaCore::MetaCoreGizmoOperation::Rotate) - 30.0F) < 0.0001F, "Custom rotate snap step should be used" ); MetaCoreExpect( std::abs(editorContext.GetGizmoSnapSettings().StepForOperation(MetaCore::MetaCoreGizmoOperation::Scale) - 0.2F) < 0.0001F, "Custom scale snap step should be used" ); MetaCore::MetaCoreGizmoSnapSettings invalidSnap; invalidSnap.Enabled = true; invalidSnap.TranslationStep = -1.0F; invalidSnap.RotationStepDegrees = 900.0F; invalidSnap.ScaleStep = std::numeric_limits::quiet_NaN(); editorContext.SetGizmoSnapSettings(invalidSnap); MetaCoreExpect( std::abs(editorContext.GetGizmoSnapSettings().TranslationStep - 0.001F) < 0.0001F, "Negative translate snap step should clamp to a positive minimum" ); MetaCoreExpect( std::abs(editorContext.GetGizmoSnapSettings().RotationStepDegrees - 360.0F) < 0.0001F, "Oversized rotation snap step should clamp to 360 degrees" ); MetaCoreExpect( std::abs(editorContext.GetGizmoSnapSettings().ScaleStep - 0.1F) < 0.0001F, "Non-finite scale snap step should fall back to the default" ); } void MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot() { MetaCore::MetaCoreScene scene; MetaCore::MetaCoreGameObject root = scene.CreateGameObject("RenderRoot"); root.GetComponent().Position = glm::vec3(1.0F, 2.0F, 3.0F); auto& rootMesh = root.AddComponent(); rootMesh.MeshSource = MetaCore::MetaCoreMeshSourceKind::Asset; rootMesh.SourceModelPath = "Assets/Models/SyncRoot.glb"; rootMesh.BaseColor = glm::vec3(0.5F, 0.6F, 0.7F); MetaCore::MetaCoreGameObject child = scene.CreateGameObject("RenderChild", root.GetId()); child.GetComponent().Position = glm::vec3(4.0F, 5.0F, 6.0F); auto& childMesh = child.AddComponent(); childMesh.MeshSource = MetaCore::MetaCoreMeshSourceKind::Builtin; childMesh.Visible = false; childMesh.BaseColor = glm::vec3(0.1F, 0.2F, 0.3F); MetaCore::MetaCoreGameObject cameraObject = scene.CreateGameObject("PrimaryCamera"); auto& camera = cameraObject.AddComponent(); camera.IsPrimary = true; camera.FieldOfViewDegrees = 75.0F; camera.NearClip = 0.25F; camera.FarClip = 250.0F; cameraObject.GetComponent().Position = glm::vec3(0.0F, 1.0F, 9.0F); MetaCore::MetaCoreGameObject lightObject = scene.CreateGameObject("KeyLight"); auto& light = lightObject.AddComponent(); light.Color = glm::vec3(0.25F, 0.5F, 1.0F); light.Intensity = 3.0F; const MetaCore::MetaCoreSceneRenderSync renderSync; const MetaCore::MetaCoreSceneRenderSyncSnapshot snapshot = renderSync.BuildSnapshot(scene); MetaCoreExpect(snapshot.Renderables.size() == 2, "RenderSync 应收集 MeshRenderer 对象"); MetaCoreExpect(snapshot.Cameras.size() == 1, "RenderSync 应收集 Camera 对象"); MetaCoreExpect(snapshot.Lights.size() == 1, "RenderSync 应收集 Light 对象"); for (const auto& renderable : snapshot.Renderables) { if (renderable.ObjectId == root.GetId()) { MetaCoreExpect(renderable.Visible == true, "RenderSync 根对象 Visible 应为 true"); MetaCoreExpectVec3Near(renderable.BaseColor, glm::vec3(0.5F, 0.6F, 0.7F), "RenderSync 根对象 BaseColor 应正确"); } else if (renderable.ObjectId == child.GetId()) { MetaCoreExpect(renderable.Visible == false, "RenderSync 子对象 Visible 应为 false"); MetaCoreExpectVec3Near(renderable.BaseColor, glm::vec3(0.1F, 0.2F, 0.3F), "RenderSync 子对象 BaseColor 应正确"); } } MetaCoreExpect(snapshot.PrimaryCamera.has_value(), "RenderSync 应识别主相机"); MetaCoreExpect(snapshot.PrimaryCamera->ObjectId == cameraObject.GetId(), "RenderSync 主相机 ID 应正确"); const glm::mat4 expectedRootWorld = MetaCore::MetaCoreBuildTransformMatrix(root.GetComponent()); const glm::mat4 expectedChildWorld = expectedRootWorld * MetaCore::MetaCoreBuildTransformMatrix(child.GetComponent()); MetaCoreExpectMat4Near(snapshot.WorldMatrices.at(root.GetId()), expectedRootWorld, "RenderSync 根对象 world matrix 应正确"); MetaCoreExpectMat4Near(snapshot.WorldMatrices.at(child.GetId()), expectedChildWorld, "RenderSync 子对象 world matrix 应合成父级"); MetaCore::MetaCoreSceneView primarySceneView; MetaCoreExpect( MetaCore::MetaCoreSceneRenderSync::TryBuildSceneViewFromPrimaryCamera(snapshot, primarySceneView), "RenderSync 应能从主相机构建 SceneView" ); MetaCoreExpectVec3Near(primarySceneView.CameraPosition, glm::vec3(0.0F, 1.0F, 9.0F), "主相机 SceneView 位置应来自 Transform"); MetaCoreExpect(std::abs(primarySceneView.VerticalFieldOfViewDegrees - 75.0F) <= 0.0001F, "主相机 FOV 应同步"); MetaCoreExpect(std::abs(primarySceneView.NearClip - 0.25F) <= 0.0001F, "主相机 NearClip 应同步"); MetaCoreExpect(std::abs(primarySceneView.FarClip - 250.0F) <= 0.0001F, "主相机 FarClip 应同步"); } void MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson() { 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 / "Assets" / "UI"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Library"); std::filesystem::create_directories(tempProjectRoot / "Runtime"); { 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 lightId = 0; for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) { if (object.GetName() == "Directional Light") { lightId = object.GetId(); break; } } MetaCoreExpect(lightId != 0, "默认场景应包含 Directional Light"); editorContext.SelectOnly(lightId); const std::filesystem::path scenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; MetaCoreExpect(scenePersistenceService->SaveSceneAs(editorContext, scenePath), "应能保存 JSON 场景"); MetaCoreExpect(scenePersistenceService->GetCurrentScenePath() == scenePath, "当前场景路径应使用 .mcscene.json"); MetaCoreExpect(std::filesystem::exists(tempProjectRoot / scenePath), "应写出 .mcscene.json 场景文件"); MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / "Scenes" / "Main.mcscene"), "JSON 主路线不应写出新的 .mcscene 文件"); MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "保存后场景不应为 dirty"); const bool renamed = editorContext.ExecuteSnapshotCommand("重命名对象", [&]() { return scene.RenameGameObject(lightId, "RoundTripLight"); }); 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, scenePath), "应能重新加载 JSON 场景"); MetaCoreExpect(scene.GetGameObjects().size() == 2, "重新加载后对象数量应恢复"); MetaCoreExpect(editorContext.GetSelectedObjectId() != 0, "重新加载后应恢复选中对象"); bool foundRenamedLight = false; for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) { if (object.GetName() == "RoundTripLight") { foundRenamedLight = true; break; } } MetaCoreExpect(foundRenamedLight, "重新加载后应保留保存过的对象名称"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestStaticCppComponentScenePersistenceAndUndo() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreStaticCppComponentSceneProject"; 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\": \"StaticCppComponentProject\",\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); const auto componentRegistry = moduleRegistry.ResolveService(); const auto reflectionRegistry = moduleRegistry.ResolveService(); const auto scenePersistenceService = moduleRegistry.ResolveService(); MetaCoreExpect(componentRegistry != nullptr, "Component registry should be available for static C++ persistence"); MetaCoreExpect(reflectionRegistry != nullptr, "Reflection registry should be available for static C++ persistence"); MetaCoreExpect(scenePersistenceService != nullptr, "Scene persistence should be available for static C++ persistence"); const auto* smokeScriptDescriptor = MetaCoreRegisterSmokeScriptComponentForTesting( *componentRegistry, *reflectionRegistry, "PersistentSmokeScript" ); MetaCoreExpect(smokeScriptDescriptor != nullptr, "Persistent smoke script should be registered"); MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); MetaCore::MetaCoreGameObject object = scene.CreateGameObject("ScriptedValve"); const MetaCore::MetaCoreId objectId = object.GetId(); MetaCoreExpect(smokeScriptDescriptor->AddComponent(object), "Static C++ component should be addable before save"); object.GetComponent().Speed = 4.5F; object.GetComponent().Enabled = false; object.GetComponent().Target = "Valve_A"; object.GetComponent().AlphaMode = MetaCore::MetaCoreMeshAlphaMode::Blend; editorContext.SelectOnly(objectId); const std::filesystem::path scenePath = std::filesystem::path("Scenes") / "StaticCppComponent.mcscene.json"; MetaCoreExpect(scenePersistenceService->SaveSceneAs(editorContext, scenePath), "Static C++ component scene should save"); MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Static C++ component scene should be clean after save"); const auto loadedDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson( tempProjectRoot / scenePath, reflectionRegistry->GetTypeRegistry() ); MetaCoreExpect(loadedDocument.has_value(), "Saved static C++ component scene should parse"); const auto documentObjectIterator = std::find_if( loadedDocument->GameObjects.begin(), loadedDocument->GameObjects.end(), [objectId](const MetaCore::MetaCoreGameObjectData& gameObjectData) { return gameObjectData.Id == objectId; } ); MetaCoreExpect(documentObjectIterator != loadedDocument->GameObjects.end(), "Saved scene document should include scripted object"); const bool hasSerializedSmokeScript = std::any_of( documentObjectIterator->SerializedComponents.begin(), documentObjectIterator->SerializedComponents.end(), [](const MetaCore::MetaCoreSerializedComponentData& componentData) { return componentData.TypeId == "PersistentSmokeScript" && !componentData.Payload.empty(); } ); MetaCoreExpect(hasSerializedSmokeScript, "Saved scene document should include serialized static C++ component payload"); scene.RestoreSnapshot(MetaCore::MetaCoreSceneSnapshot{}); editorContext.ClearSelection(); MetaCoreExpect(scene.GetGameObjects().empty(), "Scene should be empty before static C++ component reload"); MetaCoreExpect(scenePersistenceService->LoadScene(editorContext, scenePath), "Static C++ component scene should reload"); MetaCore::MetaCoreGameObject reloadedObject = scene.FindGameObject(objectId); MetaCoreExpect(reloadedObject, "Reloaded scripted object should exist"); MetaCoreExpect(reloadedObject.HasComponent(), "Reloaded object should restore static C++ component"); MetaCoreExpect(std::abs(reloadedObject.GetComponent().Speed - 4.5F) <= 0.0001F, "Reloaded static C++ Speed should match"); MetaCoreExpect(!reloadedObject.GetComponent().Enabled, "Reloaded static C++ Enabled should match"); MetaCoreExpect(reloadedObject.GetComponent().Target == "Valve_A", "Reloaded static C++ Target should match"); MetaCoreExpect(reloadedObject.GetComponent().AlphaMode == MetaCore::MetaCoreMeshAlphaMode::Blend, "Reloaded static C++ enum should match"); const bool modified = editorContext.ExecuteSnapshotCommand("Modify static C++ component", [&]() { MetaCore::MetaCoreGameObject editedObject = scene.FindGameObject(objectId); if (!editedObject || !editedObject.HasComponent()) { return false; } editedObject.GetComponent().Speed = 9.0F; editedObject.GetComponent().Target = "Valve_B"; return true; }); MetaCoreExpect(modified, "Static C++ component field mutation should create an undoable command"); MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "Static C++ component field mutation should mark scene dirty"); MetaCoreExpect(std::abs(scene.FindGameObject(objectId).GetComponent().Speed - 9.0F) <= 0.0001F, "Static C++ mutation should apply"); MetaCoreExpect(editorContext.UndoCommand(), "Undo static C++ component mutation should succeed"); MetaCoreExpect(scene.FindGameObject(objectId).HasComponent(), "Undo should keep static C++ component present"); MetaCoreExpect(std::abs(scene.FindGameObject(objectId).GetComponent().Speed - 4.5F) <= 0.0001F, "Undo should restore static C++ Speed"); MetaCoreExpect(scene.FindGameObject(objectId).GetComponent().Target == "Valve_A", "Undo should restore static C++ Target"); MetaCoreExpect(editorContext.RedoCommand(), "Redo static C++ component mutation should succeed"); MetaCoreExpect(std::abs(scene.FindGameObject(objectId).GetComponent().Speed - 9.0F) <= 0.0001F, "Redo should restore edited static C++ Speed"); MetaCoreExpect(scene.FindGameObject(objectId).GetComponent().Target == "Valve_B", "Redo should restore edited static C++ Target"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestImportPipelineAndCook() { const unsigned char ppmDataV1[] = { 0x50, 0x36, 0x0A, 0x31, 0x20, 0x31, 0x0A, 0x32, 0x35, 0x35, 0x0A, 0xFF, 0x00, 0x00 }; const unsigned char ppmDataV2[] = { 0x50, 0x36, 0x0A, 0x31, 0x20, 0x31, 0x0A, 0x32, 0x35, 0x35, 0x0A, 0x00, 0xFF, 0x00 }; 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 / "Assets" / "Textures"); 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.ppm", std::ios::binary | std::ios::trunc); rawAsset.write(reinterpret_cast(ppmDataV1), sizeof(ppmDataV1)); } _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.ppm.mcmeta"; const std::filesystem::path packagePath = tempProjectRoot / "Assets" / "Texture.ppm.mcasset"; MetaCoreExpect(std::filesystem::exists(metaPath), "导入后应生成 mcmeta"); MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / "Assets" / "Texture.ppm.mcmeta.mcmeta"), "ImportPipeline 不应把 mcmeta 当源资产再次导入"); // 【敏捷开发决策】:开发期不物理生成体积大的 .mcasset,在此放宽物理存在校验 // MetaCoreExpect(std::filesystem::exists(packagePath), "导入后应生成 mcasset"); const auto sourceRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Texture.ppm"); MetaCoreExpect(sourceRecord.has_value(), "应能找到原始资源记录"); MetaCoreExpect(!assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Texture.ppm.mcmeta").has_value(), ".mcmeta 不应注册为源资产记录"); MetaCoreExpect(sourceRecord->Guid.IsValid(), "原始资源应有稳定 GUID"); MetaCoreExpect(sourceRecord->PackagePath == std::filesystem::path("Assets") / "Texture.ppm.mcasset", "原始资源应关联包路径"); // 【敏捷开发决策】:放宽库中 cooked 二进制文件物理存在的断言校验 // 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.ppm", std::ios::binary | std::ios::trunc); rawAsset.write(reinterpret_cast(ppmDataV2), sizeof(ppmDataV2)); } MetaCoreExpect(assetDatabase->Refresh(), "修改原始资源后应能重新刷新资产数据库"); const auto refreshedSourceRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Texture.ppm"); 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 / "Assets" / "Textures"); 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"; } { const unsigned char valveTextureData[] = { 0x50, 0x36, 0x0A, 0x31, 0x20, 0x31, 0x0A, 0x32, 0x35, 0x35, 0x0A, 0xFF, 0x80, 0x40 }; std::ofstream textureFile(tempProjectRoot / "Assets" / "Textures" / "ValveBaseColor.ppm", std::ios::binary | std::ios::trunc); textureFile.write(reinterpret_cast(valveTextureData), sizeof(valveTextureData)); } { std::ofstream gltfFile(tempProjectRoot / "Assets" / "Valve.gltf", std::ios::trunc); gltfFile << "{\n" << " \"asset\": {\"version\": \"2.0\"},\n" << " \"scenes\": [{\"nodes\": [0]}],\n" << " \"nodes\": [{\"mesh\": 0, \"name\": \"Valve\"}],\n" << " \"meshes\": [{\"name\": \"ValveMesh\", \"primitives\": [{\"material\": 0}]}],\n" << " \"materials\": [{\"name\": \"ValveMaterial\", \"pbrMetallicRoughness\": {\"baseColorTexture\": {\"index\": 0}}}],\n" << " \"textures\": [{\"source\": 0}],\n" << " \"images\": [{\"uri\": \"Textures/ValveBaseColor.ppm\"}]\n" << "}\n"; } { std::ofstream gltfFile(tempProjectRoot / "Assets" / "EmbeddedTexture.gltf", std::ios::trunc); gltfFile << "{\n" << " \"asset\": {\"version\": \"2.0\"},\n" << " \"scenes\": [{\"nodes\": [0]}],\n" << " \"nodes\": [{\"mesh\": 0, \"name\": \"EmbeddedTextureNode\"}],\n" << " \"meshes\": [{\"name\": \"EmbeddedTextureMesh\", \"primitives\": [{\"material\": 0}]}],\n" << " \"materials\": [{\"name\": \"EmbeddedTextureMaterial\", \"pbrMetallicRoughness\": {\"baseColorTexture\": {\"index\": 0}}}],\n" << " \"textures\": [{\"source\": 0}],\n" << " \"images\": [{\"bufferView\": 0, \"mimeType\": \"image/x-portable-pixmap\", \"name\": \"EmbeddedBaseColor\"}],\n" << " \"bufferViews\": [{\"buffer\": 0, \"byteOffset\": 0, \"byteLength\": 14}],\n" << " \"buffers\": [{\"byteLength\": 14, \"uri\": \"data:application/octet-stream;base64,UDYKMSAxCjI1NQr/gEA=\"}]\n" << "}\n"; } { std::ofstream glbFile(tempProjectRoot / "Assets" / "Tank.glb", std::ios::binary | std::ios::trunc); glbFile << "glTF_BINARY_PLACEHOLDER"; } { std::ofstream objFile(tempProjectRoot / "Assets" / "Crate.obj", std::ios::trunc); objFile << "o Crate\n"; } { std::ofstream fbxFile(tempProjectRoot / "Assets" / "Robot.fbx", std::ios::binary | std::ios::trunc); fbxFile << "Kaydara FBX Binary"; } _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(); const auto assetEditingService = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(packageService != nullptr, "应解析到 PackageService"); MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry"); MetaCoreExpect(assetEditingService != nullptr, "应解析到 AssetEditingService"); 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"); // 【敏捷开发决策】:开发期不生成庞大的二进制 mcasset 包,仅做 Record 存在性及元数据 mcmeta 文件的校验 const std::filesystem::path valveMeta = tempProjectRoot / "Assets" / "Valve.gltf.mcmeta"; MetaCoreExpect(std::filesystem::exists(valveMeta), "gltf 应生成 mcmeta 文件"); const auto embeddedTextureRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "EmbeddedTexture.gltf"); MetaCoreExpect(embeddedTextureRecord.has_value(), "应能找到内嵌贴图 glTF 资源记录"); const std::filesystem::path embeddedMeta = tempProjectRoot / "Assets" / "EmbeddedTexture.gltf.mcmeta"; MetaCoreExpect(std::filesystem::exists(embeddedMeta), "内嵌贴图 glTF 应生成 mcmeta 文件"); const auto glbRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Tank.glb"); MetaCoreExpect(glbRecord.has_value(), "应能找到 glb 资源记录"); MetaCoreExpect(glbRecord->ImporterId == "GltfModelImporter", "glb 应使用 GltfModelImporter"); const std::filesystem::path glbMeta = tempProjectRoot / "Assets" / "Tank.glb.mcmeta"; MetaCoreExpect(std::filesystem::exists(glbMeta), "glb 应生成 mcmeta 文件"); const auto objRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Crate.obj"); MetaCoreExpect(objRecord.has_value(), "应能找到 obj 资源记录"); MetaCoreExpect(objRecord->ImporterId == "ModelImporter", "obj 应使用通用 ModelImporter"); MetaCoreExpect(objRecord->Type == "model", "obj 资源类型应为 model"); const std::filesystem::path objMeta = tempProjectRoot / "Assets" / "Crate.obj.mcmeta"; MetaCoreExpect(std::filesystem::exists(objMeta), "obj 应生成 mcmeta 文件"); const auto fbxRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Robot.fbx"); MetaCoreExpect(fbxRecord.has_value(), "应能找到 fbx 资源记录"); MetaCoreExpect(fbxRecord->ImporterId == "ModelImporter", "fbx 应使用通用 ModelImporter"); MetaCoreExpect(fbxRecord->Type == "model", "fbx 资源类型应为 model"); const std::filesystem::path fbxMeta = tempProjectRoot / "Assets" / "Robot.fbx.mcmeta"; MetaCoreExpect(std::filesystem::exists(fbxMeta), "fbx 应生成 mcmeta 文件"); 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(), "应能将导入模型实例化到场景"); MetaCore::MetaCoreGameObject instantiatedRoot = scene.FindGameObject(*instantiatedRootId); MetaCoreExpect(instantiatedRoot, "实例化后应能找到根对象"); MetaCoreExpect(instantiatedRoot.GetName() == "Pump.gltf", "model root should use source file name"); const std::vector instantiatedChildren = scene.GetChildrenOf(*instantiatedRootId); MetaCoreExpect(instantiatedChildren.empty(), "平坦化后单节点模型的根节点下不应再包含子节点"); MetaCoreExpect(instantiatedRoot.HasComponent(), "平坦化后根节点应直接包含 MeshRenderer 渲染组件"); MetaCoreExpect(instantiatedRoot.GetComponent().MeshSource == MetaCore::MetaCoreMeshSourceKind::Asset, "根节点 Mesh 资源来源应为 Asset"); MetaCoreExpect(instantiatedRoot.GetComponent().MeshAssetGuid.IsValid(), "根节点 Mesh 应绑定合法的 Asset Guid"); MetaCoreExpect(!instantiatedRoot.GetComponent().MaterialAssetGuids.empty(), "根节点应绑定材质 slot"); MetaCoreExpect( instantiatedRoot.GetComponent().SourceModelPath == (std::filesystem::path("Assets") / "Pump.gltf").generic_string(), "根节点 MeshRenderer 应保存源模型文件路径" ); MetaCoreExpect( instantiatedRoot.GetComponent().BaseColorTexturePath == (std::filesystem::path("Textures") / "PumpBaseColor.png").generic_string(), "根节点 MeshRenderer 应保存基础颜色贴图路径" ); MetaCoreExpect(instantiatedRoot.GetComponent().BaseColor.x == 1.0F, "根节点应继承材质颜色通道数值"); MetaCoreExpect(editorContext.GetActiveObjectId() == *instantiatedRootId, "实例化后应正确选中新对象"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestInstantiateMultiNodeModelAssetIntoScene() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreInstantiateMultiNodeModelProject"; 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\": \"InstantiateMultiNodeProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } { std::ofstream gltfFile(tempProjectRoot / "Assets" / "Assembly.gltf", std::ios::trunc); gltfFile << "{ \"meshes\": [{\"name\": \"AssemblyMesh\"}] }\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(); const auto packageService = moduleRegistry.ResolveService(); const auto reflectionRegistry = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(sceneEditingService != nullptr, "应解析到 SceneEditingService"); MetaCoreExpect(packageService != nullptr, "应解析到 PackageService"); MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry"); const auto modelRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Assembly.gltf"); MetaCoreExpect(modelRecord.has_value(), "应能找到导入后的多节点模型资源"); MetaCore::MetaCoreModelAssetDocument document; document.AssetType = "model"; document.ImporterId = "GltfModelImporter"; document.SourcePath = std::filesystem::path("Assets") / "Assembly.gltf"; document.SourceHash = 1234; document.ModelKind = MetaCore::MetaCoreModelAssetKind::Gltf; document.SourceFormat = "gltf"; MetaCore::MetaCoreImportedGltfNodeDocument rootNode; rootNode.Name = "AssemblyRoot"; rootNode.ParentIndex = -1; rootNode.MeshIndex = -1; document.Nodes.push_back(rootNode); MetaCore::MetaCoreImportedGltfNodeDocument childNode; childNode.Name = "Valve_A"; childNode.ParentIndex = 0; childNode.MeshIndex = 0; document.Nodes.push_back(childNode); MetaCore::MetaCoreImportedGltfMeshDocument meshDocument; meshDocument.Name = "ValveMesh"; meshDocument.PrimitiveCount = 2; meshDocument.MaterialSlots.push_back(0); meshDocument.MaterialSlots.push_back(1); document.Meshes.push_back(meshDocument); MetaCore::MetaCoreMaterialAssetDocument materialAsset; materialAsset.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate(); materialAsset.Name = "ValveMaterial"; document.GeneratedMaterialAssets.push_back(materialAsset); MetaCore::MetaCoreMaterialAssetDocument secondaryMaterialAsset; secondaryMaterialAsset.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate(); secondaryMaterialAsset.Name = "ValveSecondaryMaterial"; document.GeneratedMaterialAssets.push_back(secondaryMaterialAsset); MetaCore::MetaCoreMeshAssetDocument meshAsset; meshAsset.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate(); meshAsset.Name = "ValveMesh"; MetaCore::MetaCoreMeshSubMeshDocument firstSubMesh; firstSubMesh.Name = "Primitive_0"; firstSubMesh.MaterialSlotIndex = 0; firstSubMesh.FirstIndex = 0; firstSubMesh.IndexCount = 3; meshAsset.SubMeshes.push_back(firstSubMesh); MetaCore::MetaCoreMeshSubMeshDocument secondSubMesh; secondSubMesh.Name = "Primitive_1"; secondSubMesh.MaterialSlotIndex = 1; secondSubMesh.FirstIndex = 3; secondSubMesh.IndexCount = 3; meshAsset.SubMeshes.push_back(secondSubMesh); document.GeneratedMeshAssets.push_back(meshAsset); const auto payload = MetaCore::MetaCoreSerializeToBytes(document, reflectionRegistry->GetTypeRegistry()); MetaCoreExpect(payload.has_value(), "多节点模型资源应能序列化"); MetaCore::MetaCorePackageDocument package; package.Header.PackageType = MetaCore::MetaCorePackageType::Asset; package.Header.PackageGuid = modelRecord->Guid; package.Header.SourceHash = document.SourceHash; package.NameTable.emplace_back("MetaCoreModelAssetDocument"); package.Exports.push_back(MetaCore::MetaCoreExportEntry{ modelRecord->Guid, modelRecord->RelativePath.filename().string(), MetaCore::MetaCoreMakeTypeId("MetaCoreModelAssetDocument"), 0, static_cast(payload->size()) }); package.PayloadSections.push_back(*payload); MetaCoreExpect( packageService->WritePackage(tempProjectRoot / modelRecord->PackagePath, std::move(package)), "应能写入多节点模型资源包" ); const auto instantiatedRootId = sceneEditingService->InstantiateModelAsset(editorContext, modelRecord->Guid, std::nullopt); MetaCoreExpect(instantiatedRootId.has_value(), "应能将多节点模型实例化到场景"); MetaCore::MetaCoreGameObject instantiatedRoot = scene.FindGameObject(*instantiatedRootId); MetaCoreExpect(instantiatedRoot, "多节点实例化后应能找到根对象"); MetaCoreExpect(instantiatedRoot.GetName() == "Assembly.gltf", "multi-node model root should use source file name"); MetaCoreExpect(!instantiatedRoot.HasComponent(), "asset root should not force MeshRenderer"); const auto importedRootIterator = std::find_if(scene.GetGameObjects().begin(), scene.GetGameObjects().end(), [&](const MetaCore::MetaCoreGameObject& object) { return object.GetName() == "AssemblyRoot"; }); MetaCoreExpect(importedRootIterator != scene.GetGameObjects().end(), "multi-node instantiation should create imported root node"); MetaCoreExpect(importedRootIterator->GetParentId() == instantiatedRoot.GetId(), "imported root node should be parented under asset root"); MetaCoreExpect(!importedRootIterator->HasComponent(), "imported empty root should not force MeshRenderer"); const auto childIterator = std::find_if(scene.GetGameObjects().begin(), scene.GetGameObjects().end(), [&](const MetaCore::MetaCoreGameObject& object) { return object.GetName() == "Valve_A"; }); MetaCoreExpect(childIterator != scene.GetGameObjects().end(), "multi-node instantiation should create child node object"); MetaCoreExpect(childIterator->GetParentId() == importedRootIterator->GetId(), "child node should be parented under imported root node"); MetaCoreExpect(childIterator->HasComponent(), "mesh child should have MeshRenderer"); MetaCoreExpect(childIterator->GetComponent().MeshAssetGuid == meshAsset.AssetGuid, "child node should bind generated mesh asset"); MetaCoreExpect(childIterator->GetComponent().MaterialAssetGuids.size() == 2, "child node should bind all material slots"); MetaCoreExpect(childIterator->GetComponent().MaterialAssetGuids.front() == materialAsset.AssetGuid, "child node should bind generated material asset"); MetaCoreExpect(childIterator->GetComponent().MaterialAssetGuids[1] == secondaryMaterialAsset.AssetGuid, "child node should bind secondary material asset"); MetaCoreExpect(editorContext.GetActiveObjectId() == instantiatedRoot.GetId(), "multi-node instantiation should select asset root"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestDeleteModelAssetCorrectlyClearsFromViewport() { const std::filesystem::path projectRoot = "d:/MetaCore/SandboxProject"; _putenv_s("METACORE_PROJECT_PATH", projectRoot.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/Models/box1.glb")); MetaCoreExpect(modelRecord.has_value(), "应能找到导入后的 box1.glb 模型资源"); const auto instantiatedRootId = sceneEditingService->InstantiateModelAsset(editorContext, modelRecord->Guid, std::nullopt); MetaCoreExpect(instantiatedRootId.has_value(), "应能将导入模型实例化到场景"); // 初始化窗口和 FilamentSceneBridge MetaCore::MetaCoreWindow testWindow; bool winOk = testWindow.Initialize(800, 600, "DeleteModelAssetTestWindow"); MetaCoreExpect(winOk, "测试窗口初始化应成功"); MetaCore::MetaCoreFilamentSceneBridge bridge; bridge.SetProjectRootPath(projectRoot); bool bridgeOk = bridge.Initialize(testWindow); MetaCoreExpect(bridgeOk, "测试桥接器初始化应成功"); // 同步场景,将模型加载到 FilamentSceneBridge 中 bridge.SyncScene(scene, false, false); // 验证模型已被桥接器加载和映射 glm::mat4 modelWorldMatrix; bool hasModel = bridge.TryGetObjectWorldMatrix(*instantiatedRootId, modelWorldMatrix); MetaCoreExpect(hasModel, "同步场景后,桥接器中应包含已实例化模型的变换映射"); // 在场景中删除该模型 GameObject const std::vector deletedIds = scene.DeleteGameObjects({*instantiatedRootId}); MetaCoreExpect(!deletedIds.empty(), "删除模型对象应返回被删除的节点ID"); // 再次同步场景,此时应触发卸载 bridge.SyncScene(scene, false, false); // 验证删除后,桥接器中是否还存在该模型的映射 bool hasModelAfterDelete = bridge.TryGetObjectWorldMatrix(*instantiatedRootId, modelWorldMatrix); MetaCoreExpect(!hasModelAfterDelete, "删除模型后,桥接器映射中不应再包含该模型"); bridge.Shutdown(); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); } void MetaCoreTestDeleteModelSubChildCorrectlyClearsFromViewport() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreDeleteModelSubChildProject"; 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\": \"DeleteModelSubChildProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } { std::ofstream gltfFile(tempProjectRoot / "Assets" / "Assembly.gltf", std::ios::trunc); gltfFile << "{\n" << " \"asset\": { \"version\": \"2.0\" },\n" << " \"scenes\": [{ \"nodes\": [0] }],\n" << " \"nodes\": [\n" << " { \"name\": \"AssemblyRoot\", \"children\": [1, 2] },\n" << " { \"name\": \"Valve_A\" },\n" << " { \"name\": \"Valve_B\" }\n" << " ]\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(); const auto packageService = moduleRegistry.ResolveService(); const auto reflectionRegistry = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(sceneEditingService != nullptr, "应解析到 SceneEditingService"); MetaCoreExpect(packageService != nullptr, "应解析到 PackageService"); MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry"); const auto modelRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Assembly.gltf"); MetaCoreExpect(modelRecord.has_value(), "应能找到导入后的多节点模型资源"); MetaCore::MetaCoreModelAssetDocument document; document.AssetType = "model"; document.ImporterId = "GltfModelImporter"; document.SourcePath = std::filesystem::path("Assets") / "Assembly.gltf"; document.SourceHash = 1234; document.ModelKind = MetaCore::MetaCoreModelAssetKind::Gltf; document.SourceFormat = "gltf"; MetaCore::MetaCoreImportedGltfNodeDocument rootNode; rootNode.Name = "AssemblyRoot"; rootNode.ParentIndex = -1; rootNode.MeshIndex = -1; document.Nodes.push_back(rootNode); MetaCore::MetaCoreImportedGltfNodeDocument childNode; childNode.Name = "Valve_A"; childNode.ParentIndex = 0; childNode.MeshIndex = 0; document.Nodes.push_back(childNode); MetaCore::MetaCoreImportedGltfNodeDocument childNodeB; childNodeB.Name = "Valve_B"; childNodeB.ParentIndex = 0; childNodeB.MeshIndex = 0; document.Nodes.push_back(childNodeB); MetaCore::MetaCoreMeshAssetDocument meshAsset; meshAsset.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate(); meshAsset.Name = "ValveMesh"; MetaCore::MetaCoreMeshSubMeshDocument firstSubMesh; firstSubMesh.Name = "Primitive_0"; firstSubMesh.MaterialSlotIndex = 0; firstSubMesh.FirstIndex = 0; firstSubMesh.IndexCount = 3; meshAsset.SubMeshes.push_back(firstSubMesh); document.GeneratedMeshAssets.push_back(meshAsset); const auto payload = MetaCore::MetaCoreSerializeToBytes(document, reflectionRegistry->GetTypeRegistry()); MetaCoreExpect(payload.has_value(), "多节点模型资源应能序列化"); MetaCore::MetaCorePackageDocument package; package.Header.PackageType = MetaCore::MetaCorePackageType::Asset; package.Header.PackageGuid = modelRecord->Guid; package.Header.SourceHash = document.SourceHash; package.NameTable.emplace_back("MetaCoreModelAssetDocument"); package.Exports.push_back(MetaCore::MetaCoreExportEntry{ modelRecord->Guid, modelRecord->RelativePath.filename().string(), MetaCore::MetaCoreMakeTypeId("MetaCoreModelAssetDocument"), 0, static_cast(payload->size()) }); package.PayloadSections.push_back(*payload); MetaCoreExpect( packageService->WritePackage(tempProjectRoot / modelRecord->PackagePath, std::move(package)), "应能写入多节点模型资源包" ); const auto instantiatedRootId = sceneEditingService->InstantiateModelAsset(editorContext, modelRecord->Guid, std::nullopt); MetaCoreExpect(instantiatedRootId.has_value(), "应能将多节点模型实例化到场景"); MetaCore::MetaCoreId childId = 0; MetaCore::MetaCoreId childIdB = 0; for (const auto& obj : scene.GetGameObjects()) { if (obj.GetName() == "Valve_A") { childId = obj.GetId(); } else if (obj.GetName() == "Valve_B") { childIdB = obj.GetId(); } } MetaCoreExpect(childId != 0, "应能找到子节点 Valve_A"); MetaCoreExpect(childIdB != 0, "应能找到子节点 Valve_B"); // 初始化窗口和 FilamentSceneBridge MetaCore::MetaCoreWindow testWindow; bool winOk = testWindow.Initialize(800, 600, "DeleteModelSubChildTestWindow"); MetaCoreExpect(winOk, "测试窗口初始化应成功"); MetaCore::MetaCoreFilamentSceneBridge bridge; bridge.SetProjectRootPath(tempProjectRoot); bool bridgeOk = bridge.Initialize(testWindow); MetaCoreExpect(bridgeOk, "测试桥接器初始化应成功"); // 同步场景,将模型加载到 FilamentSceneBridge 中 bridge.SyncScene(scene, false, false); // 验证子节点对应的实体是否已经在场景中渲染 bool isChildInScene = bridge.VerifyEntityInSceneForTesting(childId); bool isChildBInScene = bridge.VerifyEntityInSceneForTesting(childIdB); MetaCoreExpect(isChildInScene, "同步场景后,子节点 Valve_A 实体应在场景中被渲染"); MetaCoreExpect(isChildBInScene, "同步场景后,子节点 Valve_B 实体应在场景中被渲染"); // 在场景中删除子节点 Valve_A GameObject(在 Command 中执行,为了后续 Undo) const bool deleted = editorContext.ExecuteSnapshotCommand("删除子节点 Valve_A", [&]() { const std::vector deletedIds = scene.DeleteGameObjects({childId}); return !deletedIds.empty(); }); MetaCoreExpect(deleted, "删除子节点 Valve_A 快照命令应成功"); // 再次同步场景 bridge.SyncScene(scene, false, false); // 验证删除子节点 Valve_A 后,桥接器中该子节点对应的实体是否已被移出场景渲染 bool isChildInSceneAfterDelete = bridge.VerifyEntityInSceneForTesting(childId); bool isChildBInSceneAfterDelete = bridge.VerifyEntityInSceneForTesting(childIdB); MetaCoreExpect(!isChildInSceneAfterDelete, "删除子节点 Valve_A 后,其实体应被移出场景渲染"); MetaCoreExpect(isChildBInSceneAfterDelete, "删除子节点 Valve_A 后,Valve_B 实体应依然保留在场景中渲染(没有卸载资产)"); // 撤销删除子节点 const bool undoSucceeded = editorContext.UndoCommand(); MetaCoreExpect(undoSucceeded, "撤销删除应成功"); // 再次同步场景 bridge.SyncScene(scene, false, false); // 验证撤销后,该子节点实体是否已被重新恢复渲染 bool isChildInSceneAfterUndo = bridge.VerifyEntityInSceneForTesting(childId); MetaCoreExpect(isChildInSceneAfterUndo, "撤销删除后,子节点 Valve_A 实体应重新被加回场景渲染"); bridge.Shutdown(); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestModelReimportKeepsGeneratedAssetReferencesStable() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreModelReimportStableProject"; 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\": \"ModelReimportStableProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } const std::filesystem::path modelPath = tempProjectRoot / "Assets" / "Pump.gltf"; { std::ofstream gltfFile(modelPath, 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(); const auto packageService = moduleRegistry.ResolveService(); const auto reflectionRegistry = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(sceneEditingService != nullptr, "应解析到 SceneEditingService"); MetaCoreExpect(packageService != nullptr, "应解析到 PackageService"); MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry"); 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(), "应能实例化模型资源"); MetaCore::MetaCoreGameObject instantiatedRoot = scene.FindGameObject(*instantiatedRootId); MetaCoreExpect(instantiatedRoot, "instantiated model root should exist"); const std::vector instantiatedChildren = scene.GetChildrenOf(*instantiatedRootId); MetaCoreExpect(instantiatedChildren.empty(), "平坦化后单节点模型的根节点下不包含任何子节点"); MetaCoreExpect(instantiatedRoot && instantiatedRoot.HasComponent(), "平坦化后根节点应直接包含 MeshRenderer 渲染组件"); const MetaCore::MetaCoreAssetGuid originalMeshGuid = instantiatedRoot.GetComponent().MeshAssetGuid; const MetaCore::MetaCoreAssetGuid originalMaterialGuid = instantiatedRoot.GetComponent().MaterialAssetGuids.front(); { std::ofstream gltfFile(modelPath, std::ios::trunc); gltfFile << "{\n" << " \"meshes\": [{\"name\": \"PumpMesh\"}],\n" << " \"materials\": [{\"name\": \"PumpMaterial\"}],\n" << " \"images\": [{\"uri\": \"Textures/PumpBaseColor.png\"}],\n" << " \"extras\": {\"revision\": 2}\n" << "}\n"; } MetaCoreExpect(assetDatabase->ReimportAsset(modelRecord->Guid), "修改模型源文件后应能通过 AssetDatabase 重新导入"); const auto refreshedRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Pump.gltf"); MetaCoreExpect(refreshedRecord.has_value(), "重导入后仍应能找到模型资源记录"); // 【敏捷开发决策】:重导入时无需在磁盘上创建/读取体积沉重的 .mcasset 包文件,只做极简的 mcmeta 元数据校验 const std::filesystem::path refreshedMeta = tempProjectRoot / "Assets" / "Pump.gltf.mcmeta"; MetaCoreExpect(std::filesystem::exists(refreshedMeta), "重导入后应生成 mcmeta 元数据文件"); instantiatedRoot = scene.FindGameObject(*instantiatedRootId); MetaCoreExpect(instantiatedRoot, "reimport should keep model root alive"); MetaCoreExpect(instantiatedRoot && instantiatedRoot.HasComponent(), "reimport should keep renderable instance alive"); MetaCoreExpect(instantiatedRoot.GetComponent().MeshAssetGuid == originalMeshGuid, "reimport should keep mesh reference stable"); MetaCoreExpect(!instantiatedRoot.GetComponent().MaterialAssetGuids.empty(), "reimport should keep material references"); MetaCoreExpect(instantiatedRoot.GetComponent().MaterialAssetGuids.front() == originalMaterialGuid, "reimport should keep material reference stable"); 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.json"; MetaCoreExpect(scenePersistenceService->SaveSceneAs(editorContext, bootstrapScenePath), "应能为默认场景创建二进制启动场景"); MetaCoreExpect(assetDatabaseService->SetStartupScenePath(bootstrapScenePath), "应能设置 startup scene"); MetaCoreExpect(assetDatabaseService->Refresh(), "刷新资产数据库应成功"); MetaCoreExpect(std::filesystem::exists(tempProjectRoot / bootstrapScenePath), "应生成 Main.mcscene.json"); MetaCoreExpect(scenePersistenceService->LoadStartupScene(editorContext), "设置后应能加载 startup scene"); MetaCoreExpect(scenePersistenceService->GetCurrentScenePath() == bootstrapScenePath, "startup scene 路径应正确"); MetaCoreExpect(scene.GetGameObjects().size() == 2, "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 sceneParent = scene.CreateGameObject("SceneParent"); const MetaCore::MetaCoreId sceneParentId = sceneParent.GetId(); MetaCore::MetaCoreGameObject root = scene.CreateGameObject("PrefabRoot", sceneParentId); root.AddComponent(); root.AddComponent(); root.GetComponent().Position = glm::vec3(1.0F, 2.0F, 3.0F); root.GetComponent().Speed = 4.25F; root.GetComponent().Target = "PrefabTarget"; const MetaCore::MetaCoreId rootId = root.GetId(); MetaCore::MetaCoreGameObject child = scene.CreateGameObject("PrefabChild", rootId); child.AddComponent(); editorContext.SelectOnly(rootId); const auto prefabService = moduleRegistry.ResolveService(); const auto assetDatabaseService = moduleRegistry.ResolveService(); const auto packageService = moduleRegistry.ResolveService(); const auto reflectionRegistry = moduleRegistry.ResolveService(); const auto componentRegistry = moduleRegistry.ResolveService(); MetaCoreExpect(prefabService != nullptr, "应解析到 PrefabService"); MetaCoreExpect(prefabService->SupportsPrefabWorkflows(), "PrefabService 应支持工作流"); MetaCoreExpect(assetDatabaseService != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(packageService != nullptr, "应解析到 PackageService"); MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry"); MetaCoreExpect(componentRegistry != nullptr, "Prefab workflow should resolve ComponentRegistry"); MetaCoreRegisterSmokeScriptComponentForTesting(*componentRegistry, *reflectionRegistry, "PrefabSmokeScript"); const std::filesystem::path prefabRequestPath = std::filesystem::path("Assets") / "Prefabs" / "PrefabRoot.mcprefab"; const std::filesystem::path prefabPath = std::filesystem::path("Assets") / "Prefabs" / "PrefabRoot.mcprefab.json"; const auto createdPrefabPath = prefabService->CreatePrefabFromSelection(editorContext, prefabRequestPath); MetaCoreExpect(createdPrefabPath.has_value(), "应能从选中对象创建 prefab"); MetaCoreExpect(std::filesystem::exists(tempProjectRoot / prefabPath), "应写出 mcprefab.json 文件"); MetaCoreExpect(assetDatabaseService->Refresh(), "创建 prefab 后应能刷新资产数据库"); MetaCoreExpect(*createdPrefabPath == prefabPath, "CreatePrefabFromSelection should normalize .mcprefab to .mcprefab.json"); MetaCoreExpect(root.GetParentId() == sceneParentId, "Create Prefab should preserve the selected object's scene parent"); MetaCoreExpect(root.HasComponent(), "Create Prefab should link the original root as a prefab instance"); MetaCoreExpect(child.HasComponent(), "Create Prefab should link the original child as a prefab instance"); const auto prefabRecord = assetDatabaseService->FindAssetByRelativePath(prefabPath); MetaCoreExpect(prefabRecord.has_value(), "应能找到 prefab 资产记录"); MetaCoreExpect(prefabRecord->Type == "prefab", "prefab 资产类型应正确"); MetaCoreExpect(root.GetComponent().PrefabAssetGuid == prefabRecord->Guid, "Original root should store the prefab asset guid"); MetaCoreExpect(root.GetComponent().PrefabObjectId == rootId, "Original root should map to its source prefab object id"); MetaCoreExpect(root.GetComponent().PrefabInstanceRootId == rootId, "Original root should store itself as the prefab instance root"); const auto instantiatedRootId = prefabService->InstantiatePrefab(editorContext, prefabRecord->Guid, std::nullopt); MetaCoreExpect(instantiatedRootId.has_value(), "应能实例化 prefab"); MetaCore::MetaCoreGameObject instantiatedRoot = scene.FindGameObject(*instantiatedRootId); MetaCoreExpect(instantiatedRoot, "实例化后应能找到根对象"); MetaCoreExpect(instantiatedRoot.HasComponent(), "实例根应带有 prefab 元数据"); MetaCoreExpect(instantiatedRoot.HasComponent(), "Prefab instance should restore static C++ components"); MetaCoreExpect(std::abs(instantiatedRoot.GetComponent().Speed - 4.25F) <= 0.0001F, "Prefab instance should restore static C++ component field values"); MetaCoreExpect(instantiatedRoot.GetComponent().Target == "PrefabTarget", "Prefab instance should restore static C++ component string fields"); instantiatedRoot.GetComponent().Position = glm::vec3(8.0F, 9.0F, 10.0F); instantiatedRoot.GetComponent().Speed = 6.75F; instantiatedRoot.GetComponent().Target = "AppliedPrefabTarget"; editorContext.SelectOnly(instantiatedRoot.GetId()); MetaCoreExpect(prefabService->ApplySelectedPrefabInstance(editorContext), "应能应用 prefab 实例"); const auto appliedPrefabDocument = MetaCore::MetaCoreSceneSerializer::LoadPrefabFromJson(tempProjectRoot / prefabPath, reflectionRegistry->GetTypeRegistry()); MetaCoreExpect(appliedPrefabDocument.has_value(), "Apply 后 prefab 应能被 JSON 解析"); MetaCoreExpect(!appliedPrefabDocument->GameObjects.empty(), "Apply 后 prefab 应保留对象数据"); MetaCoreExpect( !appliedPrefabDocument->GameObjects.front().SerializedComponents.empty(), "Apply should write static C++ components into the prefab document" ); MetaCoreExpect( appliedPrefabDocument->GameObjects.front().SerializedComponents.front().TypeId == "PrefabSmokeScript", "Apply should preserve the static C++ component TypeId in the prefab document" ); 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, "第二个实例应存在"); MetaCoreExpectVec3Near(secondInstanceRoot.GetComponent().Position, glm::vec3(8.0F, 9.0F, 10.0F), "Apply 后新实例应继承修改过的 transform"); MetaCoreExpect(secondInstanceRoot.HasComponent(), "Apply should make new prefab instances restore static C++ components"); MetaCoreExpect(std::abs(secondInstanceRoot.GetComponent().Speed - 6.75F) <= 0.0001F, "Apply should make new prefab instances inherit static C++ component fields"); MetaCoreExpect(secondInstanceRoot.GetComponent().Target == "AppliedPrefabTarget", "Apply should make new prefab instances inherit static C++ string fields"); secondInstanceRoot.GetComponent().Position = glm::vec3(-1.0F, -2.0F, -3.0F); secondInstanceRoot.GetComponent().Speed = 1.0F; editorContext.SelectOnly(secondInstanceRoot.GetId()); MetaCoreExpect(prefabService->RevertSelectedPrefabInstance(editorContext), "应能还原 prefab 实例"); MetaCore::MetaCoreGameObject revertedRoot = scene.FindGameObject(editorContext.GetActiveObjectId()); MetaCoreExpect(revertedRoot, "还原后应重新选中实例根"); MetaCoreExpectVec3Near(revertedRoot.GetComponent().Position, glm::vec3(8.0F, 9.0F, 10.0F), "Revert 后实例应恢复为 prefab 内容"); MetaCoreExpect(revertedRoot.HasComponent(), "Revert should restore static C++ components"); MetaCoreExpect(std::abs(revertedRoot.GetComponent().Speed - 6.75F) <= 0.0001F, "Revert should restore static C++ component fields"); const MetaCore::MetaCoreId revertedRootId = revertedRoot.GetId(); const std::vector revertedSubtreeIds = scene.GetSubtreeObjectIds(revertedRootId); MetaCore::MetaCoreSceneDocument sceneRoundTripDocument; sceneRoundTripDocument.Name = "PrefabInstanceRoundTrip"; sceneRoundTripDocument.GameObjects = scene.CaptureSnapshot().GameObjects; const std::filesystem::path sceneRoundTripPath = tempProjectRoot / "Scenes" / "PrefabInstanceRoundTrip.mcscene.json"; MetaCoreExpect( MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(sceneRoundTripPath, sceneRoundTripDocument, reflectionRegistry->GetTypeRegistry()), "Scene save should preserve prefab instance metadata" ); const auto loadedSceneRoundTrip = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(sceneRoundTripPath, reflectionRegistry->GetTypeRegistry()); MetaCoreExpect(loadedSceneRoundTrip.has_value(), "Scene reload should parse prefab instance metadata"); for (MetaCore::MetaCoreId objectId : revertedSubtreeIds) { const auto objectIterator = std::find_if( loadedSceneRoundTrip->GameObjects.begin(), loadedSceneRoundTrip->GameObjects.end(), [objectId](const MetaCore::MetaCoreGameObjectData& objectData) { return objectData.Id == objectId; } ); MetaCoreExpect(objectIterator != loadedSceneRoundTrip->GameObjects.end(), "Reloaded scene should include prefab instance object"); MetaCoreExpect(objectIterator->PrefabInstance.has_value(), "Reloaded prefab instance object should keep metadata"); MetaCoreExpect( objectIterator->PrefabInstance->PrefabAssetGuid == prefabRecord->Guid, "Reloaded prefab instance should keep source prefab guid" ); MetaCoreExpect( objectIterator->PrefabInstance->PrefabInstanceRootId == revertedRootId, "Reloaded prefab instance should keep instance root id" ); } const auto expectPrefabMetadata = [&](bool expected, const char* message) { for (MetaCore::MetaCoreId objectId : scene.GetSubtreeObjectIds(revertedRootId)) { MetaCore::MetaCoreGameObject object = scene.FindGameObject(objectId); MetaCoreExpect(object, "Prefab metadata assertion object should exist"); MetaCoreExpect( object.HasComponent() == expected, message ); } }; editorContext.SelectOnly(revertedRootId); MetaCoreExpect(prefabService->BreakSelectedPrefabInstance(editorContext), "Break should remove prefab instance metadata"); expectPrefabMetadata(false, "Break should remove prefab metadata from the whole instance subtree"); MetaCoreExpect(editorContext.UndoCommand(), "Undo Break should succeed"); expectPrefabMetadata(true, "Undo Break should restore prefab metadata"); MetaCoreExpect(editorContext.RedoCommand(), "Redo Break should succeed"); expectPrefabMetadata(false, "Redo Break should remove prefab metadata again"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestCookPipelineCooksJsonScenePrefabMaterialUi() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreJsonCookPipelineProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets" / "Prefabs"); std::filesystem::create_directories(tempProjectRoot / "Assets" / "Materials"); std::filesystem::create_directories(tempProjectRoot / "Assets" / "UI"); 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\": \"JsonCookProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"scenes\": [\"Scenes/Main.mcscene.json\"],\n" << " \"startup_scene\": \"Scenes/Main.mcscene.json\"\n" << "}\n"; } _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(); const auto reflectionRegistry = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(cookService != nullptr, "应解析到 CookService"); MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry"); const auto& registry = reflectionRegistry->GetTypeRegistry(); const std::filesystem::path scenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; MetaCore::MetaCoreSceneDocument sceneDocument; sceneDocument.Name = "Main"; sceneDocument.GameObjects = MetaCore::MetaCoreCreateDefaultScene().CaptureSnapshot().GameObjects; MetaCoreExpect( MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(tempProjectRoot / scenePath, sceneDocument, registry), "应写出 JSON scene" ); const std::filesystem::path prefabPath = std::filesystem::path("Assets") / "Prefabs" / "Widget.mcprefab.json"; MetaCore::MetaCorePrefabDocument prefabDocument; prefabDocument.Name = "Widget"; MetaCore::MetaCoreGameObjectData prefabObject; prefabObject.Id = 1; prefabObject.Name = "WidgetRoot"; prefabDocument.GameObjects.push_back(prefabObject); MetaCoreExpect( MetaCore::MetaCoreSceneSerializer::SavePrefabToJson(tempProjectRoot / prefabPath, prefabDocument, registry), "应写出 JSON prefab" ); const std::filesystem::path materialPath = std::filesystem::path("Assets") / "Materials" / "Default.mcmaterial.json"; MetaCore::MetaCoreMaterialAssetDocument materialDocument; materialDocument.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate(); materialDocument.Name = "Default"; materialDocument.BaseColor = glm::vec3(0.25F, 0.5F, 0.75F); MetaCoreExpect( MetaCore::MetaCoreSceneSerializer::SaveMaterialToJson(tempProjectRoot / materialPath, materialDocument, registry), "应写出 JSON material" ); const std::filesystem::path uiPath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json"; MetaCore::MetaCoreUiDocument uiDocument; uiDocument.Name = "Hud"; uiDocument.RootNodeIds.push_back("root"); MetaCore::MetaCoreUiNodeDocument uiRoot; uiRoot.Id = "root"; uiRoot.Name = "Root"; uiRoot.Type = MetaCore::MetaCoreUiNodeType::Panel; uiDocument.Nodes.push_back(uiRoot); MetaCoreExpect( MetaCore::MetaCoreSceneSerializer::SaveUiToJson(tempProjectRoot / uiPath, uiDocument, registry), "应写出 JSON UI" ); MetaCoreExpect(assetDatabase->Refresh(), "刷新后应扫描 JSON 源资产"); const auto sceneRecord = assetDatabase->FindAssetByRelativePath(scenePath); const auto prefabRecord = assetDatabase->FindAssetByRelativePath(prefabPath); const auto materialRecord = assetDatabase->FindAssetByRelativePath(materialPath); const auto uiRecord = assetDatabase->FindAssetByRelativePath(uiPath); MetaCoreExpect(sceneRecord.has_value() && sceneRecord->Type == "scene", "应注册 JSON scene 源资产"); MetaCoreExpect(prefabRecord.has_value() && prefabRecord->Type == "prefab", "应注册 JSON prefab 源资产"); MetaCoreExpect(materialRecord.has_value() && materialRecord->Type == "material", "应注册 JSON material 源资产"); MetaCoreExpect(uiRecord.has_value() && uiRecord->Type == "ui_document", "应注册 JSON UI 源资产"); MetaCoreExpect(prefabRecord->PackagePath == prefabPath, "JSON prefab 的 PackagePath 应保持源路径"); MetaCoreExpect(materialRecord->PackagePath == materialPath, "JSON material 的 PackagePath 应保持源路径"); MetaCoreExpect(uiRecord->PackagePath == uiPath, "JSON UI 的 PackagePath 应保持源路径"); MetaCoreExpect(cookService->CookAsset(sceneRecord->Guid), "应能 Cook JSON scene"); MetaCoreExpect(cookService->CookAsset(prefabRecord->Guid), "应能 Cook JSON prefab"); MetaCoreExpect(cookService->CookAsset(materialRecord->Guid), "应能 Cook JSON material"); MetaCoreExpect(cookService->CookAsset(uiRecord->Guid), "应能 Cook JSON UI"); const auto expectCookedOutput = [&](const MetaCore::MetaCoreAssetGuid& guid, const char* message) { const std::filesystem::path cookedPath = cookService->GetCookedPathForAsset(guid); MetaCoreExpect(!cookedPath.empty(), message); MetaCoreExpect(std::filesystem::exists(tempProjectRoot / cookedPath), message); }; expectCookedOutput(sceneRecord->Guid, "JSON scene 应生成 cooked 输出"); expectCookedOutput(prefabRecord->Guid, "JSON prefab 应生成 cooked 输出"); expectCookedOutput(materialRecord->Guid, "JSON material 应生成 cooked 输出"); expectCookedOutput(uiRecord->Guid, "JSON UI 应生成 cooked 输出"); MetaCoreExpect(std::filesystem::exists(tempProjectRoot / (prefabPath.generic_string() + ".mcmeta")), "JSON prefab 应生成 mcmeta"); MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / (prefabPath.generic_string() + ".mcmeta.mcmeta")), "JSON prefab 的 mcmeta 不应被递归导入"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestBuildServiceCreatesPlayerPackageLayout() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreBuildPackageProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets" / "UI"); std::filesystem::create_directories(tempProjectRoot / "Assets" / "Materials"); std::filesystem::create_directories(tempProjectRoot / "Config" / "RuntimeData"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Runtime"); std::filesystem::create_directories(tempProjectRoot / "ConfigRuntime"); std::filesystem::create_directories(tempProjectRoot / "Tools"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); projectFile << "{\n" << " \"name\": \"BuildPackageProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"runtime_directory\": \"ConfigRuntime\",\n" << " \"scenes\": [\"Scenes/Main.mcscene.json\"],\n" << " \"startup_scene\": \"Scenes/Main.mcscene.json\"\n" << "}\n"; } { std::ofstream fakePlayer(tempProjectRoot / "Tools" / "MetaCorePlayer.exe", std::ios::binary | std::ios::trunc); fakePlayer << "fake player"; } _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 buildService = moduleRegistry.ResolveService(); const auto cookService = moduleRegistry.ResolveService(); const auto reflectionRegistry = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "Build package test should resolve AssetDatabaseService"); MetaCoreExpect(buildService != nullptr, "Build package test should resolve BuildService"); MetaCoreExpect(cookService != nullptr, "Build package test should resolve CookService"); MetaCoreExpect(reflectionRegistry != nullptr, "Build package test should resolve ReflectionRegistry"); const auto& registry = reflectionRegistry->GetTypeRegistry(); const std::filesystem::path scenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; MetaCore::MetaCoreSceneDocument sceneDocument; sceneDocument.Name = "Main"; sceneDocument.GameObjects = MetaCore::MetaCoreCreateDefaultScene().CaptureSnapshot().GameObjects; MetaCore::MetaCoreGameObjectData materialDependencyObject; materialDependencyObject.Id = 9001; materialDependencyObject.Name = "MaterialDependencyObject"; materialDependencyObject.MeshRenderer = MetaCore::MetaCoreMeshRendererComponent{}; sceneDocument.GameObjects.push_back(materialDependencyObject); MetaCoreExpect( MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(tempProjectRoot / scenePath, sceneDocument, registry), "Build package test should write startup scene" ); const std::filesystem::path materialPath = std::filesystem::path("Assets") / "Materials" / "Dependency.mcmaterial.json"; MetaCore::MetaCoreMaterialAssetDocument materialDocument; materialDocument.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate(); materialDocument.Name = "Dependency"; materialDocument.BaseColor = glm::vec3(0.8F, 0.3F, 0.2F); MetaCoreExpect( MetaCore::MetaCoreSceneSerializer::SaveMaterialToJson(tempProjectRoot / materialPath, materialDocument, registry), "Build package test should write dependency material" ); const std::filesystem::path uiPath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json"; MetaCore::MetaCoreUiDocument uiDocument; uiDocument.Name = "Hud"; uiDocument.RootNodeIds.push_back("root"); MetaCore::MetaCoreUiNodeDocument uiRoot; uiRoot.Id = "root"; uiRoot.Name = "Root"; uiRoot.Type = MetaCore::MetaCoreUiNodeType::Panel; uiDocument.Nodes.push_back(uiRoot); MetaCoreExpect( MetaCore::MetaCoreSceneSerializer::SaveUiToJson(tempProjectRoot / uiPath, uiDocument, registry), "Build package test should write startup UI" ); MetaCore::MetaCoreRuntimeProjectDocument runtimeProject; runtimeProject.StartupScenePath = scenePath; runtimeProject.StartupUiPath = uiPath; runtimeProject.BuildProfileName = "BuildSmokeProfile"; runtimeProject.TargetPlatform = "Windows"; runtimeProject.OutputDirectory = std::filesystem::path("BuildSettingsOutput"); runtimeProject.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Windows"; runtimeProject.UseCookedAssets = false; runtimeProject.DataSourcesPath = std::filesystem::path("Config") / "RuntimeData" / "DataSources.mcruntime"; runtimeProject.BindingsPath = std::filesystem::path("Config") / "RuntimeData" / "Bindings.mcruntime"; runtimeProject.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; const std::filesystem::path runtimeProjectPath = std::filesystem::path("ConfigRuntime") / "ProjectRuntime.mcruntimecfg"; MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeProjectDocument(tempProjectRoot / runtimeProjectPath, runtimeProject, registry), "Build package test should write runtime project" ); const std::filesystem::path runtimeReplayPath = std::filesystem::path("Config") / "RuntimeData" / "BuildReplay.mcstream"; { std::ofstream replayFile(tempProjectRoot / runtimeReplayPath, std::ios::trunc); replayFile << "0.00 build.status string Build package replay ready\n"; } MetaCore::MetaCoreRuntimeDataSourcesDocument runtimeSources; runtimeSources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ "build-source", "file_replay", "Build Source", {MetaCore::MetaCoreDataSourceSetting{"file_path", runtimeReplayPath.generic_string()}}, true, 1000 }); runtimeSources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ "build.status", "build-source", "build.status", MetaCore::MetaCoreRuntimeValueType::String }); MetaCore::MetaCoreRuntimeBindingsDocument runtimeBindings; runtimeBindings.UiBindings.push_back(MetaCore::MetaCoreUiBindingDefinition{ "binding.build.status", "build.status", "runtime.status", MetaCore::MetaCoreRuntimeUiBindingTarget::Text, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }); MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeDataSourcesDocument(tempProjectRoot / runtimeProject.DataSourcesPath, runtimeSources, registry), "Build package test should write runtime data sources outside Runtime" ); MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeBindingsDocument(tempProjectRoot / runtimeProject.BindingsPath, runtimeBindings, registry), "Build package test should write runtime bindings outside Runtime" ); MetaCoreExpect(assetDatabase->Refresh(), "Build package test should refresh asset database"); const auto materialRecord = assetDatabase->FindAssetByRelativePath(materialPath); MetaCoreExpect(materialRecord.has_value(), "Build package test should register dependency material"); bool assignedMaterialDependency = false; for (MetaCore::MetaCoreGameObjectData& gameObject : sceneDocument.GameObjects) { if (gameObject.MeshRenderer.has_value()) { gameObject.MeshRenderer->MaterialAssetGuids = {materialRecord->Guid}; assignedMaterialDependency = true; break; } } MetaCoreExpect(assignedMaterialDependency, "Build package test startup scene should have a MeshRenderer"); MetaCoreExpect( MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(tempProjectRoot / scenePath, sceneDocument, registry), "Build package test should rewrite startup scene with material dependency" ); MetaCoreExpect(assetDatabase->Refresh(), "Build package test should refresh material dependency scene"); MetaCore::MetaCoreBuildPlayerPackageRequest request; request.PlayerExecutablePath = tempProjectRoot / "Tools" / "MetaCorePlayer.exe"; const MetaCore::MetaCoreBuildPlayerPackageResult result = buildService->BuildPlayerPackage(request); MetaCoreExpect(result.Success, ("BuildService should create player package: " + result.Error).c_str()); const std::filesystem::path outputRoot = tempProjectRoot / "BuildSettingsOutput" / "BuildPackageProject"; MetaCoreExpect(result.OutputRoot == outputRoot, "BuildService should use runtime project output root"); MetaCoreExpect(std::filesystem::exists(outputRoot / "MetaCorePlayer.exe"), "Build output should include MetaCorePlayer.exe"); MetaCoreExpect(std::filesystem::exists(outputRoot / "MetaCore.project.json"), "Build output should include project descriptor"); const auto packagedProjectFile = MetaCore::MetaCoreReadProjectFile(outputRoot / "MetaCore.project.json"); MetaCoreExpect(packagedProjectFile.has_value(), "Build output project descriptor should be readable"); MetaCoreExpect( packagedProjectFile->RuntimeDirectory == std::filesystem::path("ConfigRuntime"), "Build output project descriptor should preserve runtime_directory" ); MetaCoreExpect(std::filesystem::exists(outputRoot / scenePath), "Build output should include startup scene"); MetaCoreExpect(std::filesystem::exists(outputRoot / uiPath), "Build output should include startup UI"); MetaCoreExpect(std::filesystem::exists(outputRoot / runtimeProjectPath), "Build output should include runtime project config"); MetaCoreExpect(std::filesystem::exists(outputRoot / runtimeProject.DataSourcesPath), "Build output should include custom runtime data sources path"); MetaCoreExpect(std::filesystem::exists(outputRoot / runtimeProject.BindingsPath), "Build output should include custom runtime bindings path"); MetaCoreExpect(std::filesystem::exists(outputRoot / runtimeReplayPath), "Build output should include RuntimeData file replay dependency"); const auto packagedLooseRuntimeProject = MetaCore::MetaCoreReadRuntimeProjectDocument( outputRoot / runtimeProjectPath, registry ); MetaCoreExpect(packagedLooseRuntimeProject.has_value(), "Build output runtime project should be readable"); MetaCoreExpect(packagedLooseRuntimeProject->BuildProfileName == "BuildSmokeProfile", "Build output should preserve saved build profile"); MetaCoreExpect(packagedLooseRuntimeProject->OutputDirectory == runtimeProject.OutputDirectory, "Build output should preserve saved output directory"); MetaCoreExpect(std::filesystem::exists(outputRoot / "Library" / "Cooked" / "Windows" / "CookManifest.bin"), "Build output should include CookManifest"); const std::filesystem::path cookedMaterialPath = cookService->GetCookedPathForAsset(materialRecord->Guid); MetaCoreExpect(!cookedMaterialPath.empty(), "BuildService should cook material dependency"); MetaCoreExpect(std::filesystem::exists(outputRoot / cookedMaterialPath), "Build output should include cooked material dependency"); MetaCoreExpect(!result.CookedAssets.empty(), "Build result should record cooked assets"); const auto materialReportIterator = std::find_if( result.DependencyReport.begin(), result.DependencyReport.end(), [&](const MetaCore::MetaCoreBuildDependencyReportEntry& entry) { return entry.AssetGuid == materialRecord->Guid; } ); MetaCoreExpect(materialReportIterator != result.DependencyReport.end(), "Build result should report material dependency"); MetaCoreExpect(materialReportIterator->Status == "Cooked", "Build material dependency report should be cooked"); MetaCoreExpect( materialReportIterator->Reason == "MeshRenderer.MaterialAssetGuids", "Build material dependency report should include reference reason" ); MetaCoreExpect( materialReportIterator->CookedPath == cookedMaterialPath, "Build material dependency report should include cooked path" ); MetaCore::MetaCoreBuildPlayerPackageRequest cookedOnlyRequest; cookedOnlyRequest.PlayerExecutablePath = tempProjectRoot / "Tools" / "MetaCorePlayer.exe"; cookedOnlyRequest.OutputDirectory = std::filesystem::path("BuildCooked"); cookedOnlyRequest.CopyLooseProjectContent = false; cookedOnlyRequest.UseCookedAssetsInPackage = true; const MetaCore::MetaCoreBuildPlayerPackageResult cookedOnlyResult = buildService->BuildPlayerPackage(cookedOnlyRequest); MetaCoreExpect( cookedOnlyResult.Success, ("BuildService should create cooked-only player package: " + cookedOnlyResult.Error).c_str() ); const std::filesystem::path cookedOnlyOutputRoot = tempProjectRoot / "BuildCooked" / "BuildPackageProject"; MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / "MetaCorePlayer.exe"), "Cooked-only build output should include MetaCorePlayer.exe"); MetaCoreExpect(!std::filesystem::exists(cookedOnlyOutputRoot / scenePath), "Cooked-only build output should not copy loose startup scene"); MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / runtimeProjectPath), "Cooked-only build output should include runtime project config"); MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / runtimeProject.DataSourcesPath), "Cooked-only build output should include custom runtime data sources path"); MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / runtimeProject.BindingsPath), "Cooked-only build output should include custom runtime bindings path"); MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / runtimeReplayPath), "Cooked-only build output should include RuntimeData file replay dependency"); const auto packagedRuntimeProject = MetaCore::MetaCoreReadRuntimeProjectDocument( cookedOnlyOutputRoot / runtimeProjectPath, registry ); MetaCoreExpect(packagedRuntimeProject.has_value(), "Cooked-only runtime project should be readable"); MetaCoreExpect(packagedRuntimeProject->UseCookedAssets, "Cooked-only runtime project should enable cooked assets"); MetaCoreExpect( std::filesystem::exists(cookedOnlyOutputRoot / "Library" / "Cooked" / "Windows" / "CookManifest.bin"), "Cooked-only build output should include CookManifest" ); MetaCoreExpect(!cookedOnlyResult.CookedAssets.empty(), "Cooked-only build result should record cooked assets"); runtimeProject.DataSourcesPath = std::filesystem::path("..") / "OutsideDataSources.mcruntime"; MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeProjectDocument(tempProjectRoot / runtimeProjectPath, runtimeProject, registry), "Build package test should rewrite unsafe runtime project" ); const MetaCore::MetaCoreBuildPlayerPackageResult unsafeRuntimeProjectResult = buildService->BuildPlayerPackage(request); MetaCoreExpect(!unsafeRuntimeProjectResult.Success, "BuildService should reject unsafe Runtime project data source paths"); MetaCoreExpect( unsafeRuntimeProjectResult.Error.find("Unsafe Runtime project DataSourcesPath") != std::string::npos, "BuildService should report unsafe Runtime project path errors" ); runtimeProject.DataSourcesPath = std::filesystem::path("Config") / "RuntimeData" / "DataSources.mcruntime"; runtimeProject.StartupUiPath = std::filesystem::path("..") / "OutsideHud.mcui.json"; MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeProjectDocument(tempProjectRoot / runtimeProjectPath, runtimeProject, registry), "Build package test should rewrite unsafe runtime startup UI" ); const MetaCore::MetaCoreBuildPlayerPackageResult unsafeStartupUiResult = buildService->BuildPlayerPackage(request); MetaCoreExpect(!unsafeStartupUiResult.Success, "BuildService should reject unsafe Runtime project startup UI paths"); MetaCoreExpect( unsafeStartupUiResult.Error.find("Unsafe Runtime project StartupUiPath") != std::string::npos, "BuildService should report unsafe Runtime project startup UI errors" ); runtimeProject.StartupUiPath = uiPath; runtimeProject.CookedAssetsDirectory = std::filesystem::path("..") / "CookedOutside"; MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeProjectDocument(tempProjectRoot / runtimeProjectPath, runtimeProject, registry), "Build package test should rewrite unsafe runtime cooked assets directory" ); const MetaCore::MetaCoreBuildPlayerPackageResult unsafeCookedAssetsResult = buildService->BuildPlayerPackage(request); MetaCoreExpect(!unsafeCookedAssetsResult.Success, "BuildService should reject unsafe Runtime project cooked assets directories"); MetaCoreExpect( unsafeCookedAssetsResult.Error.find("Unsafe Runtime project CookedAssetsDirectory") != std::string::npos, "BuildService should report unsafe Runtime project cooked assets directory errors" ); runtimeProject.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Windows"; MetaCore::MetaCoreRuntimeDataSourcesDocument unsafeReplaySources = runtimeSources; unsafeReplaySources.Sources.front().ConnectionSettings = { MetaCore::MetaCoreDataSourceSetting{"file_path", (std::filesystem::path("..") / "OutsideReplay.mcstream").generic_string()} }; MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeDataSourcesDocument(tempProjectRoot / runtimeProject.DataSourcesPath, unsafeReplaySources, registry), "Build package test should rewrite unsafe RuntimeData replay path" ); MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeProjectDocument(tempProjectRoot / runtimeProjectPath, runtimeProject, registry), "Build package test should restore safe runtime project before unsafe replay path test" ); const MetaCore::MetaCoreBuildPlayerPackageResult unsafeReplayPathResult = buildService->BuildPlayerPackage(request); MetaCoreExpect(!unsafeReplayPathResult.Success, "BuildService should reject unsafe RuntimeData file replay paths"); MetaCoreExpect( unsafeReplayPathResult.Error.find("Unsafe RuntimeData file_replay file_path") != std::string::npos, "BuildService should report unsafe RuntimeData file replay path errors" ); 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.GetId()); 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.HasComponent(), "添加后对象应拥有 Camera"); const bool addedLight = editorContext.ExecuteSnapshotCommand("添加 Light", [&]() { return lightDescriptor->AddComponent(object); }); MetaCoreExpect(addedLight, "应能通过注册表添加 Light"); MetaCoreExpect(object.HasComponent(), "添加后对象应拥有 Light"); const bool removedCamera = editorContext.ExecuteSnapshotCommand("移除 Camera", [&]() { return cameraDescriptor->RemoveComponent(object); }); MetaCoreExpect(removedCamera, "应能通过注册表移除 Camera"); MetaCoreExpect(!object.HasComponent(), "移除后对象不应再拥有 Camera"); const bool readdedCamera = editorContext.ExecuteSnapshotCommand("重新添加 Camera", [&]() { return cameraDescriptor->AddComponent(object); }); MetaCoreExpect(readdedCamera, "应能重新添加 Camera"); object.GetComponent().FieldOfViewDegrees = 77.0F; object.GetComponent().NearClip = 0.25F; object.GetComponent().FarClip = 250.0F; object.GetComponent().IsPrimary = true; MetaCoreExpect(componentRegistry->CopyComponent("Camera", object), "应能复制 Camera 组件值"); MetaCore::MetaCoreGameObject pasteTarget = scene.CreateGameObject("PasteTarget"); MetaCore::MetaCoreGameObject secondPasteTarget = scene.CreateGameObject("SecondPasteTarget"); MetaCoreExpect(!pasteTarget.HasComponent(), "新对象初始不应拥有 Camera"); MetaCoreExpect(!secondPasteTarget.HasComponent(), "第二个新对象初始不应拥有 Camera"); MetaCoreExpect(componentRegistry->CanPasteComponent("Camera"), "复制后应可粘贴 Camera"); MetaCoreExpect(componentRegistry->PasteComponent("Camera", pasteTarget), "应能粘贴 Camera 组件值"); MetaCoreExpect(componentRegistry->PasteComponent("Camera", secondPasteTarget), "应能向第二个对象粘贴 Camera 组件值"); MetaCoreExpect(pasteTarget.HasComponent(), "粘贴后目标对象应拥有 Camera"); MetaCoreExpect(secondPasteTarget.HasComponent(), "第二个目标对象粘贴后应拥有 Camera"); MetaCoreExpect(std::abs(pasteTarget.GetComponent().FieldOfViewDegrees - 77.0F) <= 0.0001F, "粘贴后 FOV 应一致"); MetaCoreExpect(std::abs(pasteTarget.GetComponent().NearClip - 0.25F) <= 0.0001F, "粘贴后 NearClip 应一致"); MetaCoreExpect(std::abs(pasteTarget.GetComponent().FarClip - 250.0F) <= 0.0001F, "粘贴后 FarClip 应一致"); MetaCoreExpect(pasteTarget.GetComponent().IsPrimary, "粘贴后 IsPrimary 应一致"); MetaCoreExpect(std::abs(secondPasteTarget.GetComponent().FieldOfViewDegrees - 77.0F) <= 0.0001F, "第二个对象粘贴后 FOV 应一致"); MetaCoreExpect(cameraDescriptor->ResetComponent != nullptr, "Camera descriptor 应提供 Reset"); pasteTarget.GetComponent().FieldOfViewDegrees = 10.0F; MetaCoreExpect(cameraDescriptor->ResetComponent(pasteTarget), "应能重置 Camera"); MetaCoreExpect(std::abs(pasteTarget.GetComponent().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 }); bindingsDocument.UiBindings.push_back(MetaCore::MetaCoreUiBindingDefinition{ "binding.ui.cube.position", "cube.position", "runtime.status", MetaCore::MetaCoreRuntimeUiBindingTarget::Text, 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"); MetaCoreExpect(loadedBindings->UiBindings.size() == 1, "RuntimeBindingsDocument should preserve UI bindings"); MetaCoreExpect(loadedBindings->UiBindings.front().TargetNodeId == "runtime.status", "UI binding target node id should round trip"); 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.StartupUiPath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json"; writtenDocument.BuildProfileName = "Shipping"; writtenDocument.TargetPlatform = "Windows"; writtenDocument.OutputDirectory = std::filesystem::path("Build") / "Windows"; writtenDocument.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Windows"; writtenDocument.UseCookedAssets = true; 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 应一致"); MetaCoreExpect(loadedDocument->StartupUiPath == writtenDocument.StartupUiPath, "StartupUiPath should round trip"); MetaCoreExpect(loadedDocument->BuildProfileName == writtenDocument.BuildProfileName, "BuildProfileName should round trip"); MetaCoreExpect(loadedDocument->TargetPlatform == writtenDocument.TargetPlatform, "TargetPlatform should round trip"); MetaCoreExpect(loadedDocument->OutputDirectory == writtenDocument.OutputDirectory, "OutputDirectory should round trip"); MetaCoreExpect(loadedDocument->CookedAssetsDirectory == writtenDocument.CookedAssetsDirectory, "CookedAssetsDirectory should round trip"); MetaCoreExpect(loadedDocument->UseCookedAssets == writtenDocument.UseCookedAssets, "UseCookedAssets should round trip"); const std::filesystem::path projectRoot = std::filesystem::temp_directory_path() / "MetaCoreRuntimeProjectDefaults"; const std::filesystem::path runtimeRoot = projectRoot / "ConfigRuntime"; MetaCoreExpect( MetaCore::MetaCoreBuildRuntimeDirectoryRelativePath(projectRoot, runtimeRoot) == std::filesystem::path("ConfigRuntime"), "Runtime directory helper should return project-relative runtime_directory" ); MetaCoreExpect( MetaCore::MetaCoreBuildRuntimeDirectoryRelativePath(projectRoot, projectRoot / ".." / "OutsideRuntime") == std::filesystem::path("Runtime"), "Runtime directory helper should fall back when runtime_directory escapes the project" ); const auto defaultRuntimeProject = MetaCore::MetaCoreBuildDefaultRuntimeProjectDocument(std::filesystem::path("ConfigRuntime")); MetaCoreExpect( defaultRuntimeProject.StartupScenePath == std::filesystem::path("Scenes") / "Main.mcscene.json", "RuntimeProject defaults should use the JSON scene asset extension" ); MetaCoreExpect( defaultRuntimeProject.DataSourcesPath == std::filesystem::path("ConfigRuntime") / "DataSources.mcruntime", "RuntimeProject defaults should place DataSources under runtime_directory" ); MetaCoreExpect( defaultRuntimeProject.BindingsPath == std::filesystem::path("ConfigRuntime") / "Bindings.mcruntime", "RuntimeProject defaults should place Bindings under runtime_directory" ); MetaCoreExpect( defaultRuntimeProject.DiagnosticsPath == std::filesystem::path("ConfigRuntime") / "Diagnostics.mcruntimestate", "RuntimeProject defaults should place Diagnostics under runtime_directory" ); MetaCore::MetaCoreRuntimeProjectDocument partialDocument; partialDocument.StartupScenePath = std::filesystem::path("Scenes") / "Custom.mcscene.json"; MetaCore::MetaCoreApplyRuntimeProjectDefaults(partialDocument, std::filesystem::path("ConfigRuntime")); MetaCoreExpect( partialDocument.StartupScenePath == std::filesystem::path("Scenes") / "Custom.mcscene.json", "RuntimeProject defaults should preserve explicit startup scene paths" ); MetaCoreExpect( partialDocument.DataSourcesPath == std::filesystem::path("ConfigRuntime") / "DataSources.mcruntime", "RuntimeProject default application should fill missing DataSourcesPath from runtime_directory" ); MetaCoreExpect( !MetaCore::MetaCoreIsUnsafeRuntimeProjectPath(std::filesystem::path("Runtime") / "DataSources.mcruntime"), "Runtime project validation should accept safe project-relative paths" ); MetaCoreExpect( MetaCore::MetaCoreIsUnsafeRuntimeProjectPath(std::filesystem::path("..") / "Outside.mcruntime"), "Runtime project validation should reject parent traversal paths" ); MetaCore::MetaCoreRuntimeProjectDocument unsafeDocument = writtenDocument; unsafeDocument.DataSourcesPath = std::filesystem::path("..") / "OutsideDataSources.mcruntime"; const auto unsafeRuntimeProjectIssues = MetaCore::MetaCoreValidateRuntimeProjectPaths(unsafeDocument); MetaCoreExpect(!unsafeRuntimeProjectIssues.empty(), "Runtime project validation should report unsafe paths"); MetaCoreExpect( std::any_of( unsafeRuntimeProjectIssues.begin(), unsafeRuntimeProjectIssues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && issue.Scope == "RuntimeProject" && issue.Message.find("DataSourcesPath") != std::string::npos; } ), "Runtime project validation should identify the unsafe DataSourcesPath" ); 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().GetId(), 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.AddComponent(); 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.GetId(), MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }, MetaCore::MetaCoreSceneBindingDefinition{ "binding.visible", "cube.visible", cube.GetId(), 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.GetComponent().Position, glm::vec3(4.0F, 5.0F, 6.0F), "Dispatcher 应更新 Transform.Position"); MetaCoreExpect(!cube.GetComponent().Visible, "Dispatcher 应更新 MeshRenderer.Visible"); } void MetaCoreTestRuntimeDataDispatcherRejectsInvalidUpdateDiagnostics() { MetaCore::MetaCoreScene scene; MetaCore::MetaCoreGameObject cube = scene.CreateGameObject("Cube"); cube.AddComponent(); cube.GetComponent().Visible = true; 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.GetId(), MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue } }); dispatcher.ApplyUpdates({ MetaCore::MetaCoreRuntimeDataUpdate{ "cube.visible", MetaCore::MetaCoreRuntimeDataValue{ MetaCore::MetaCoreRuntimeValueType::Double, false, 0, 1.0, {}, glm::vec3(0.0F, 0.0F, 0.0F), 100, MetaCore::MetaCoreRuntimeDataQuality::Good }, 1 } }); MetaCoreExpect(cube.GetComponent().Visible, "Invalid RuntimeData type should not change target component"); MetaCoreExpect(!dispatcher.GetBindingStatuses().front().Healthy, "Invalid RuntimeData type should mark binding unhealthy"); MetaCoreExpect( dispatcher.GetBindingStatuses().front().LastError == "Update type does not match data point definition", "Invalid RuntimeData type should record a precise binding diagnostic" ); dispatcher.ApplyUpdates({ MetaCore::MetaCoreRuntimeDataUpdate{ "cube.visible", MetaCore::MetaCoreRuntimeDataValue{ MetaCore::MetaCoreRuntimeValueType::Bool, false, 0, 0.0, {}, glm::vec3(0.0F, 0.0F, 0.0F), 200, MetaCore::MetaCoreRuntimeDataQuality::Bad }, 2 } }); MetaCoreExpect(cube.GetComponent().Visible, "Bad quality RuntimeData should not change target component"); MetaCoreExpect( dispatcher.GetBindingStatuses().front().LastError == "Update quality is bad", "Bad quality RuntimeData should record a precise binding diagnostic" ); dispatcher.ApplyUpdates({ MetaCore::MetaCoreRuntimeDataUpdate{ "cube.visible", MetaCore::MetaCoreRuntimeDataValue{ MetaCore::MetaCoreRuntimeValueType::Bool, false, 0, 0.0, {}, glm::vec3(0.0F, 0.0F, 0.0F), 300, MetaCore::MetaCoreRuntimeDataQuality::Good }, 3 } }); MetaCoreExpect(!cube.GetComponent().Visible, "Good RuntimeData should still apply after rejected updates"); MetaCoreExpect(dispatcher.GetBindingStatuses().front().Healthy, "Good RuntimeData should restore binding health"); MetaCoreExpect(dispatcher.GetBindingStatuses().front().LastError.empty(), "Good RuntimeData should clear prior binding diagnostics"); } 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.AddComponent(); 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.GetId(), 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.AddComponent(); 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.GetId(), 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 / "Assets" / "UI"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Library"); std::filesystem::create_directories(tempProjectRoot / "Runtime"); { 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"; } MetaCore::MetaCoreTypeRegistry registry; MetaCoreRegisterRuntimeDataGeneratedTypes(registry); MetaCore::MetaCoreRuntimeProjectDocument existingRuntimeProject; existingRuntimeProject.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; existingRuntimeProject.StartupUiPath = std::filesystem::path("Assets") / "UI" / "CustomHud.mcui.json"; existingRuntimeProject.BuildProfileName = "PreservedProfile"; existingRuntimeProject.TargetPlatform = "Windows"; existingRuntimeProject.OutputDirectory = std::filesystem::path("Build") / "Preserved"; existingRuntimeProject.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Preserved"; existingRuntimeProject.UseCookedAssets = true; existingRuntimeProject.DataSourcesPath = std::filesystem::path("Runtime") / "CustomSources.mcruntime"; existingRuntimeProject.BindingsPath = std::filesystem::path("Runtime") / "CustomBindings.mcruntime"; existingRuntimeProject.DiagnosticsPath = std::filesystem::path("Runtime") / "CustomDiagnostics.mcruntimestate"; MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeProjectDocument( tempProjectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", existingRuntimeProject, registry ), "Existing runtime project settings should be writable before editor save" ); MetaCore::MetaCoreRuntimeDataSourcesDocument existingSourcesDocument; existingSourcesDocument.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ "existing-source", "file_replay", "Existing Source", {MetaCore::MetaCoreDataSourceSetting{"file_path", "Runtime/ExistingReplay.mcstream"}}, true, 1000 }); existingSourcesDocument.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ "existing.status", "existing-source", "existing.status", MetaCore::MetaCoreRuntimeValueType::String }); MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeDataSourcesDocument( tempProjectRoot / existingRuntimeProject.DataSourcesPath, existingSourcesDocument, registry ), "Existing runtime data sources should be writable at the custom path" ); MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeBindingsDocument( tempProjectRoot / existingRuntimeProject.BindingsPath, MetaCore::MetaCoreRuntimeBindingsDocument{}, registry ), "Existing runtime bindings should be writable at the custom path" ); _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); MetaCore::MetaCoreGameObject runtimeCube = scene.CreateGameObject("RuntimeCube"); 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", runtimeCube.GetId(), MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }); MetaCoreExpect(editorContext.SaveRuntimeDataConfig(), "EditorContext 应能保存 Runtime 配置"); const auto loadedProjectRuntime = MetaCore::MetaCoreReadRuntimeProjectDocument( tempProjectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", registry ); MetaCoreExpect(loadedProjectRuntime.has_value(), "Runtime project should still be readable after RuntimeData save"); const auto loadedSources = MetaCore::MetaCoreReadRuntimeDataSourcesDocument( tempProjectRoot / loadedProjectRuntime->DataSourcesPath, registry ); const auto loadedBindings = MetaCore::MetaCoreReadRuntimeBindingsDocument( tempProjectRoot / loadedProjectRuntime->BindingsPath, registry ); MetaCoreExpect(loadedSources.has_value(), "保存后应能读回 Runtime data sources"); MetaCoreExpect(loadedBindings.has_value(), "保存后应能读回 Runtime bindings"); MetaCoreExpect(loadedProjectRuntime.has_value(), "保存后应能读回 Runtime project"); MetaCoreExpect(loadedSources->Sources.size() == 2, "保存后应保留 source"); MetaCoreExpect(loadedBindings->Bindings.size() == 1, "保存后应保留 binding"); MetaCoreExpect(loadedProjectRuntime->StartupScenePath == std::filesystem::path("Scenes") / "Main.mcscene.json", "保存后应写入默认 startup scene 路径"); MetaCoreExpect( loadedProjectRuntime->StartupUiPath == existingRuntimeProject.StartupUiPath, "RuntimeData save should preserve existing startup UI" ); MetaCoreExpect(loadedProjectRuntime->BuildProfileName == "PreservedProfile", "RuntimeData save should preserve build profile"); MetaCoreExpect(loadedProjectRuntime->TargetPlatform == "Windows", "Runtime project should preserve target platform"); MetaCoreExpect( loadedProjectRuntime->OutputDirectory == existingRuntimeProject.OutputDirectory, "RuntimeData save should preserve output directory" ); MetaCoreExpect( loadedProjectRuntime->CookedAssetsDirectory == existingRuntimeProject.CookedAssetsDirectory, "RuntimeData save should preserve cooked asset directory" ); MetaCoreExpect(loadedProjectRuntime->UseCookedAssets, "RuntimeData save should preserve cooked-assets mode"); MetaCoreExpect( loadedProjectRuntime->DataSourcesPath == existingRuntimeProject.DataSourcesPath, "RuntimeData save should preserve custom data sources path" ); MetaCoreExpect( loadedProjectRuntime->BindingsPath == existingRuntimeProject.BindingsPath, "RuntimeData save should preserve custom bindings path" ); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestEditorContextLoadsRuntimeDiagnosticsFromProjectPath() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorDiagnosticsProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Runtime"); std::filesystem::create_directories(tempProjectRoot / "ConfigRuntime"); std::filesystem::create_directories(tempProjectRoot / "Config" / "RuntimeData"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); projectFile << "{\n" << " \"name\": \"RuntimeDiagnosticsProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"runtime_directory\": \"ConfigRuntime\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } MetaCore::MetaCoreTypeRegistry registry; MetaCoreRegisterRuntimeDataGeneratedTypes(registry); MetaCore::MetaCoreRuntimeProjectDocument runtimeProject; runtimeProject.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; runtimeProject.StartupUiPath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json"; runtimeProject.BuildProfileName = "Development"; runtimeProject.TargetPlatform = "Windows"; runtimeProject.OutputDirectory = std::filesystem::path("Build") / "Windows"; runtimeProject.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Windows"; runtimeProject.DataSourcesPath = std::filesystem::path("Runtime") / "DataSources.mcruntime"; runtimeProject.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; runtimeProject.DiagnosticsPath = std::filesystem::path("Config") / "RuntimeData" / "CustomDiagnostics.mcruntimestate"; MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeProjectDocument( tempProjectRoot / "ConfigRuntime" / "ProjectRuntime.mcruntimecfg", runtimeProject, registry ), "Runtime project with custom runtime and diagnostics paths should be writable" ); MetaCore::MetaCoreRuntimeDiagnosticsSnapshot defaultSnapshot; defaultSnapshot.SourceStatuses.push_back(MetaCore::MetaCoreRuntimeDataSourceStatus{ "default-diagnostics-source", MetaCore::MetaCoreRuntimeDataSourceState::Connected, 1, 2, {} }); MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeDiagnosticsSnapshot( tempProjectRoot / "Runtime" / "Diagnostics.mcruntimestate", defaultSnapshot, registry ), "Default runtime diagnostics should be writable" ); MetaCore::MetaCoreRuntimeDiagnosticsSnapshot customSnapshot; customSnapshot.SourceStatuses.push_back(MetaCore::MetaCoreRuntimeDataSourceStatus{ "custom-diagnostics-source", MetaCore::MetaCoreRuntimeDataSourceState::Degraded, 10, 20, "custom fault" }); customSnapshot.HasFaults = true; MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeDiagnosticsSnapshot( tempProjectRoot / runtimeProject.DiagnosticsPath, customSnapshot, registry ), "Custom runtime diagnostics should be writable" ); _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); const auto assetDatabase = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "AssetDatabaseService should be available for diagnostics runtime path test"); MetaCoreExpect( assetDatabase->GetProjectDescriptor().RuntimePath.filename() == "ConfigRuntime", "Diagnostics runtime path test should load custom project runtime directory" ); MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); const auto loadedSnapshot = editorContext.LoadRuntimeDiagnosticsSnapshot(); MetaCoreExpect(loadedSnapshot.has_value(), "EditorContext should load Runtime diagnostics"); MetaCoreExpect(loadedSnapshot->HasFaults, "EditorContext should preserve custom diagnostics fault state"); MetaCoreExpect(loadedSnapshot->SourceStatuses.size() == 1, "EditorContext diagnostics should preserve source status count"); MetaCoreExpect( loadedSnapshot->SourceStatuses.front().SourceId == "custom-diagnostics-source", "EditorContext should load diagnostics from ProjectRuntime DiagnosticsPath" ); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestEditorContextRuntimeDataDefaultsFollowRuntimeDirectory() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorDefaultRuntimeDirectoryProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "ConfigRuntime"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); projectFile << "{\n" << " \"name\": \"RuntimeDefaultDirectoryProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"runtime_directory\": \"ConfigRuntime\",\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 should load default RuntimeData config"); MetaCoreExpect(editorContext.SaveRuntimeDataConfig(), "EditorContext should save default RuntimeData config"); MetaCore::MetaCoreTypeRegistry registry; MetaCoreRegisterRuntimeDataGeneratedTypes(registry); const auto runtimeProject = MetaCore::MetaCoreReadRuntimeProjectDocument( tempProjectRoot / "ConfigRuntime" / "ProjectRuntime.mcruntimecfg", registry ); MetaCoreExpect(runtimeProject.has_value(), "Default RuntimeProject should be saved under custom runtime_directory"); MetaCoreExpect( runtimeProject->DataSourcesPath == std::filesystem::path("ConfigRuntime") / "DataSources.mcruntime", "Default RuntimeData sources path should follow runtime_directory" ); MetaCoreExpect( runtimeProject->BindingsPath == std::filesystem::path("ConfigRuntime") / "Bindings.mcruntime", "Default RuntimeData bindings path should follow runtime_directory" ); MetaCoreExpect( runtimeProject->DiagnosticsPath == std::filesystem::path("ConfigRuntime") / "Diagnostics.mcruntimestate", "Default Runtime diagnostics path should follow runtime_directory" ); MetaCoreExpect( std::filesystem::exists(tempProjectRoot / runtimeProject->DataSourcesPath), "Default RuntimeData sources document should be written under runtime_directory" ); MetaCoreExpect( std::filesystem::exists(tempProjectRoot / runtimeProject->BindingsPath), "Default RuntimeData bindings document should be written under runtime_directory" ); 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 MetaCoreTestRuntimeDataConfigValidationRejectsBindingTypeMismatch() { MetaCore::MetaCoreRuntimeDataSourcesDocument sources; 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.visible", "replay-source", "cube.visible", MetaCore::MetaCoreRuntimeValueType::Bool }); MetaCore::MetaCoreRuntimeBindingsDocument bindings; bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ "binding.invalid.type", "cube.visible", 1, MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }); const auto issues = MetaCore::MetaCoreValidateRuntimeDataDocuments(sources, bindings); MetaCoreExpect( std::any_of(issues.begin(), issues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && issue.Scope == "binding.invalid.type" && issue.Message.find("expects Vec3") != std::string::npos && issue.Message.find("DataPoint is Bool") != std::string::npos; }), "RuntimeData validation should reject DataPoint type mismatches for binding targets" ); } void MetaCoreTestRuntimeDataConfigValidationRejectsMissingUiTargetNode() { MetaCore::MetaCoreRuntimeDataSourcesDocument sources; 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{ "runtime.status", "replay-source", "runtime.status", MetaCore::MetaCoreRuntimeValueType::String }); MetaCore::MetaCoreRuntimeBindingsDocument bindings; bindings.UiBindings.push_back(MetaCore::MetaCoreUiBindingDefinition{ "binding.ui.status", "runtime.status", "", MetaCore::MetaCoreRuntimeUiBindingTarget::Text, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }); const auto issues = MetaCore::MetaCoreValidateRuntimeDataDocuments(sources, bindings); MetaCoreExpect( std::any_of(issues.begin(), issues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && issue.Scope == "binding.ui.status" && issue.Message.find("TargetNodeId") != std::string::npos; }), "RuntimeData validation should reject UI bindings without target node ids" ); } 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 MetaCoreTestEditorContextRejectsUnsafeRuntimeProjectPaths() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorUnsafePathProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Library"); std::filesystem::create_directories(tempProjectRoot / "Runtime"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); projectFile << "{\n" << " \"name\": \"RuntimeUnsafePathProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } MetaCore::MetaCoreTypeRegistry registry; MetaCoreRegisterRuntimeDataGeneratedTypes(registry); MetaCore::MetaCoreRuntimeProjectDocument runtimeProject; runtimeProject.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; runtimeProject.StartupUiPath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json"; runtimeProject.BuildProfileName = "Development"; runtimeProject.TargetPlatform = "Windows"; runtimeProject.OutputDirectory = std::filesystem::path("Build") / "Windows"; runtimeProject.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Windows"; runtimeProject.DataSourcesPath = std::filesystem::path("..") / "OutsideDataSources.mcruntime"; runtimeProject.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; runtimeProject.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeProjectDocument( tempProjectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", runtimeProject, registry ), "Unsafe runtime project should be writable for validation smoke test" ); _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 should reject unsafe Runtime project data source paths" ); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestEditorContextRejectsUnsafeRuntimeReplayPath() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorUnsafeReplayPathProject"; 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\": \"RuntimeUnsafeReplayPathProject\",\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 should load runtime config for replay path validation"); auto& sources = editorContext.AccessRuntimeDataSourcesDocument(); sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ "unsafe-replay-source", "file_replay", "Unsafe Replay Source", {MetaCore::MetaCoreDataSourceSetting{"file_path", (std::filesystem::path("..") / "OutsideReplay.mcstream").generic_string()}}, true, 1000 }); const auto validationIssues = editorContext.ValidateRuntimeDataConfig(); MetaCoreExpect( std::any_of(validationIssues.begin(), validationIssues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && issue.Scope == "unsafe-replay-source" && issue.Message.find("file_replay file_path") != std::string::npos; }), "EditorContext Validate should reject unsafe RuntimeData replay file paths" ); MetaCoreExpect( !editorContext.SaveRuntimeDataConfig(), "EditorContext should reject saving unsafe RuntimeData replay file paths" ); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestEditorContextRejectsMissingRuntimeUiBindingNode() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorMissingUiBindingProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets" / "UI"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Library"); std::filesystem::create_directories(tempProjectRoot / "Runtime"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); projectFile << "{\n" << " \"name\": \"RuntimeMissingUiBindingProject\",\n" << " \"version\": \"0.1.0\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } MetaCore::MetaCoreTypeRegistry uiRegistry; MetaCoreRegisterFoundationGeneratedTypes(uiRegistry); MetaCoreRegisterSceneGeneratedTypes(uiRegistry); MetaCoreRegisterEditorGeneratedTypes(uiRegistry); MetaCore::MetaCoreUiDocument uiDocument; uiDocument.Name = "Hud"; uiDocument.RootNodeIds = {"root"}; MetaCore::MetaCoreUiNodeDocument rootNode; rootNode.Id = "root"; rootNode.Name = "Root"; rootNode.Type = MetaCore::MetaCoreUiNodeType::Panel; rootNode.Children = {"runtime.status"}; MetaCore::MetaCoreUiNodeDocument statusNode; statusNode.Id = "runtime.status"; statusNode.Name = "Runtime Status"; statusNode.Type = MetaCore::MetaCoreUiNodeType::Text; statusNode.ParentId = "root"; statusNode.Text = "Waiting"; uiDocument.Nodes = {rootNode, statusNode}; MetaCoreExpect( MetaCore::MetaCoreSceneSerializer::SaveUiToJson( tempProjectRoot / "Assets" / "UI" / "Hud.mcui.json", uiDocument, uiRegistry ), "Startup UI should be writable for RuntimeData validation" ); _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 should load runtime config for UI validation"); 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{ "runtime.status", "replay-source", "runtime.status", MetaCore::MetaCoreRuntimeValueType::String }); bindings.UiBindings.push_back(MetaCore::MetaCoreUiBindingDefinition{ "binding.ui.missing", "runtime.status", "missing.status", MetaCore::MetaCoreRuntimeUiBindingTarget::Text, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }); const auto validationIssues = editorContext.ValidateRuntimeDataConfig(); MetaCoreExpect( std::any_of(validationIssues.begin(), validationIssues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && issue.Scope == "binding.ui.missing" && issue.Message.find("Target UI node does not exist") != std::string::npos; }), "EditorContext Validate should report missing Startup UI binding targets" ); MetaCoreExpect( !editorContext.SaveRuntimeDataConfig(), "EditorContext should reject RuntimeData UI bindings that target missing Startup UI nodes" ); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } void MetaCoreTestEditorContextValidatesRuntimeSceneBindingTargets() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorSceneBindingProject"; 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\": \"RuntimeSceneBindingProject\",\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::MetaCoreGameObject transformOnlyObject = scene.CreateGameObject("TransformOnly"); 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 should load runtime config for scene binding validation"); auto& sources = editorContext.AccessRuntimeDataSourcesDocument(); auto& bindings = editorContext.AccessRuntimeBindingsDocument(); sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ "mock-source", "mock", "Mock Source", {}, true, 1000 }); sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ "pump.position", "mock-source", "pump.position", MetaCore::MetaCoreRuntimeValueType::Vec3 }); sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ "pump.visible", "mock-source", "pump.visible", MetaCore::MetaCoreRuntimeValueType::Bool }); bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ "binding.scene.missing-object", "pump.position", 999999, MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }); bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ "binding.scene.missing-mesh", "pump.visible", transformOnlyObject.GetId(), MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }); const auto validationIssues = editorContext.ValidateRuntimeDataConfig(); MetaCoreExpect( std::any_of(validationIssues.begin(), validationIssues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && issue.Scope == "binding.scene.missing-object" && issue.Message.find("Target scene object does not exist") != std::string::npos; }), "EditorContext Validate should report missing RuntimeData scene target objects" ); MetaCoreExpect( std::any_of(validationIssues.begin(), validationIssues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && issue.Scope == "binding.scene.missing-mesh" && issue.Message.find("MeshRenderer") != std::string::npos; }), "EditorContext Validate should report RuntimeData scene target component mismatches" ); MetaCoreExpect( !editorContext.SaveRuntimeDataConfig(), "EditorContext should reject RuntimeData scene bindings that cannot resolve their target components" ); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } 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 背景色应保持"); } void MetaCoreTestUiDocumentRmlCompilation() { MetaCore::MetaCoreUiDocument document; document.Name = "RuntimeHud"; document.ReferenceWidth = 1920; document.ReferenceHeight = 1080; document.RootNodeIds = {"root"}; MetaCore::MetaCoreUiNodeDocument root; root.Id = "root"; root.Name = "Root"; root.Type = MetaCore::MetaCoreUiNodeType::Panel; root.Children = {"title", "status.image", "start.button"}; root.RectTransform.AnchorMin = glm::vec3(0.0F, 0.0F, 0.0F); root.RectTransform.AnchorMax = glm::vec3(1.0F, 1.0F, 0.0F); root.RectTransform.Size = glm::vec3(0.0F, 0.0F, 0.0F); root.Style.BackgroundColor = glm::vec3(0.05F, 0.08F, 0.12F); MetaCore::MetaCoreUiNodeDocument title; title.Id = "title"; title.Name = "Title"; title.Type = MetaCore::MetaCoreUiNodeType::Text; title.ParentId = "root"; title.Text = "MetaCore "; title.RectTransform.Position = glm::vec3(32.0F, 24.0F, 0.0F); title.RectTransform.Size = glm::vec3(520.0F, 56.0F, 0.0F); title.Style.FontSize = 28.0F; title.Style.HorizontalAlignment = MetaCore::MetaCoreUiHorizontalAlignment::Center; MetaCore::MetaCoreUiNodeDocument image; image.Id = "status.image"; image.Name = "Status Image"; image.Type = MetaCore::MetaCoreUiNodeType::Image; image.ParentId = "root"; image.RectTransform.Position = glm::vec3(32.0F, 96.0F, 0.0F); image.RectTransform.Size = glm::vec3(96.0F, 96.0F, 0.0F); image.Style.TintColor = glm::vec3(0.2F, 0.8F, 0.4F); image.Style.PreserveAspect = true; MetaCore::MetaCoreUiNodeDocument button; button.Id = "start.button"; button.Name = "Start Button"; button.Type = MetaCore::MetaCoreUiNodeType::Button; button.ParentId = "root"; button.Text = "Start & Run"; button.Interactable = true; button.RectTransform.Position = glm::vec3(32.0F, 216.0F, 0.0F); button.RectTransform.Size = glm::vec3(180.0F, 48.0F, 0.0F); button.Style.BackgroundColor = glm::vec3(0.12F, 0.42F, 0.82F); document.Nodes = {root, title, image, button}; const auto compiled = MetaCore::MetaCoreCompileUiDocumentToRml(document, "RuntimeHud.rcss"); MetaCoreExpect(!compiled.Rml.empty(), "Compiled RML should not be empty"); MetaCoreExpect(!compiled.Rcss.empty(), "Compiled RCSS should not be empty"); MetaCoreExpect(compiled.Rml.find("href=\"RuntimeHud.rcss\"") != std::string::npos, "Compiled RML should reference stylesheet"); MetaCoreExpect(compiled.Rml.find("MetaCore <Runtime>") != std::string::npos, "Compiled RML should escape text"); MetaCoreExpect(compiled.Rml.find("Start & Run") != std::string::npos, "Compiled RML should escape button text"); MetaCoreExpect(compiled.Rml.find("mcui-panel") != std::string::npos, "Compiled RML should include panel class"); MetaCoreExpect(compiled.Rml.find("mcui-text") != std::string::npos, "Compiled RML should include text class"); MetaCoreExpect(compiled.Rml.find("mcui-image") != std::string::npos, "Compiled RML should include image class"); MetaCoreExpect(compiled.Rml.find("mcui-button") != std::string::npos, "Compiled RML should include button class"); MetaCoreExpect(compiled.Rml.find("data-metacore-id=\"start.button\"") != std::string::npos, "Compiled RML should preserve source ids"); MetaCoreExpect(compiled.Rcss.find(".mcui-node-0") != std::string::npos, "Compiled RCSS should include stable node class"); MetaCoreExpect(compiled.Rcss.find("width: 1920px") != std::string::npos, "Compiled RCSS should include reference width"); MetaCoreExpect(compiled.Rcss.find("text-align: center") != std::string::npos, "Compiled RCSS should include text alignment"); MetaCoreExpect(compiled.Rcss.find("rgb(13, 20, 31)") != std::string::npos, "Compiled RCSS should convert colors"); MetaCoreExpect(compiled.StylesheetHref == "RuntimeHud.rcss", "Compiled UI should preserve stylesheet href"); } void MetaCoreTestRuntimeUiRendererLifecycle() { MetaCore::MetaCoreRuntimeUiRenderer renderer; MetaCore::MetaCoreUiDocument document; document.Name = "RuntimeRendererSmoke"; document.ReferenceWidth = 1280; document.ReferenceHeight = 720; document.RootNodeIds = {"root"}; MetaCore::MetaCoreUiNodeDocument root; root.Id = "root"; root.Name = "Root"; root.Type = MetaCore::MetaCoreUiNodeType::Panel; root.Children = {"runtime.status"}; root.RectTransform.AnchorMin = glm::vec3(0.0F, 0.0F, 0.0F); root.RectTransform.AnchorMax = glm::vec3(1.0F, 1.0F, 0.0F); root.RectTransform.Size = glm::vec3(0.0F, 0.0F, 0.0F); root.Style.BackgroundColor = glm::vec3(0.1F, 0.2F, 0.3F); MetaCore::MetaCoreUiNodeDocument statusText; statusText.Id = "runtime.status"; statusText.Name = "Runtime Status"; statusText.Type = MetaCore::MetaCoreUiNodeType::Text; statusText.ParentId = "root"; statusText.Text = "RuntimeData starting"; statusText.RectTransform.Position = glm::vec3(24.0F, 24.0F, 0.0F); statusText.RectTransform.Size = glm::vec3(360.0F, 42.0F, 0.0F); statusText.Style.FontSize = 18.0F; document.Nodes = {root, statusText}; const auto compiledDocument = MetaCore::MetaCoreCompileUiDocumentToRml(document, "RuntimeRendererSmoke.rcss"); MetaCoreExpect(!renderer.LoadCompiledDocument(compiledDocument), "Runtime UI renderer should reject documents before initialization"); MetaCoreExpect(!renderer.GetStats().LastError.empty(), "Runtime UI renderer should report initialization errors"); MetaCoreExpect(renderer.Initialize(), "Runtime UI renderer should initialize"); renderer.Resize(1280, 720); MetaCoreExpect(renderer.GetStats().Initialized, "Runtime UI renderer should report initialized state"); MetaCoreExpect(renderer.GetStats().RmlInitialized, "Runtime UI renderer should initialize RmlUi core"); MetaCoreExpect(renderer.GetStats().RmlContextCreated, "Runtime UI renderer should create an RmlUi context"); MetaCoreExpect(renderer.GetStats().Width == 1280, "Runtime UI renderer should track width"); MetaCoreExpect(renderer.GetStats().Height == 720, "Runtime UI renderer should track height"); MetaCoreExpect(renderer.LoadCompiledDocument(compiledDocument), "Runtime UI renderer should accept compiled UI documents"); MetaCoreExpect(renderer.GetStats().Loaded, "Runtime UI renderer should report loaded UI state"); MetaCoreExpect(renderer.GetStats().RmlDocumentLoaded, "Runtime UI renderer should load an RmlUi document"); MetaCoreExpect(renderer.GetStats().RmlBytes == compiledDocument.Rml.size(), "Runtime UI renderer should track RML bytes"); MetaCoreExpect(renderer.GetStats().RcssBytes == compiledDocument.Rcss.size(), "Runtime UI renderer should track RCSS bytes"); MetaCoreExpect(renderer.GetStats().LastError.empty(), "Runtime UI renderer should clear load errors after success"); MetaCoreExpect(renderer.HasNode("runtime.status"), "Runtime UI renderer should report existing MetaCore UI nodes"); MetaCoreExpect(!renderer.HasNode("missing.status"), "Runtime UI renderer should report missing MetaCore UI nodes"); MetaCoreExpect( !renderer.SetNodeText("missing.status", "missing"), "Runtime UI renderer should reject missing dynamic text nodes" ); MetaCoreExpect(!renderer.GetStats().LastError.empty(), "Runtime UI renderer should report missing dynamic text nodes"); MetaCoreExpect( renderer.SetNodeText("runtime.status", "RuntimeData & stable"), "Runtime UI renderer should update dynamic text nodes by MetaCore UI id" ); MetaCoreExpect(renderer.GetStats().LastError.empty(), "Runtime UI renderer should clear dynamic text errors after success"); renderer.BeginFrame(1.0F / 60.0F); renderer.Render(); MetaCoreExpect(renderer.GetStats().FrameCount == 1, "Runtime UI renderer should count frames"); MetaCoreExpect(renderer.GetStats().LastDeltaSeconds > 0.0F, "Runtime UI renderer should track delta time"); MetaCoreExpect(renderer.GetStats().CompiledGeometryCount > 0, "Runtime UI renderer should receive RmlUi geometry"); MetaCoreExpect(renderer.GetStats().RenderGeometryCalls > 0, "Runtime UI renderer should execute RmlUi render callbacks"); MetaCoreExpect(renderer.GetStats().DrawCommandCount > 0, "Runtime UI renderer should emit draw commands"); MetaCoreExpect(renderer.GetStats().DrawVertexCount > 0, "Runtime UI renderer should emit draw vertices"); MetaCoreExpect(renderer.GetStats().DrawIndexCount > 0, "Runtime UI renderer should emit draw indices"); const MetaCore::MetaCoreRuntimeUiFrame& frame = renderer.GetLastFrame(); MetaCoreExpect(frame.Commands.size() == renderer.GetStats().DrawCommandCount, "Runtime UI frame command count should match stats"); MetaCoreExpect(frame.Vertices.size() == renderer.GetStats().DrawVertexCount, "Runtime UI frame vertex count should match stats"); MetaCoreExpect(frame.Indices.size() == renderer.GetStats().DrawIndexCount, "Runtime UI frame index count should match stats"); MetaCoreExpect(!frame.Commands.empty(), "Runtime UI frame should contain at least one command"); MetaCoreExpect(frame.Commands.front().VertexCount > 0, "Runtime UI command should reference vertices"); MetaCoreExpect(frame.Commands.front().IndexCount > 0, "Runtime UI command should reference indices"); MetaCoreExpect(frame.Commands.front().VertexOffset < frame.Vertices.size(), "Runtime UI command vertex offset should be valid"); MetaCoreExpect(frame.Commands.front().IndexOffset < frame.Indices.size(), "Runtime UI command index offset should be valid"); MetaCoreExpect(frame.Vertices.front().A > 0, "Runtime UI vertex alpha should be preserved"); const MetaCore::MetaCoreRuntimeUiRasterFrame& rasterFrame = renderer.GetLastRasterFrame(); MetaCoreExpect(rasterFrame.Width == 1280, "Runtime UI raster frame should track width"); MetaCoreExpect(rasterFrame.Height == 720, "Runtime UI raster frame should track height"); MetaCoreExpect(rasterFrame.Rgba.size() == static_cast(1280 * 720 * 4), "Runtime UI raster frame should allocate RGBA pixels"); MetaCoreExpect(renderer.GetStats().RasterPixelCount == static_cast(1280 * 720), "Runtime UI raster pixel count should match dimensions"); MetaCoreExpect(renderer.GetStats().RasterTouchedPixelCount > 0, "Runtime UI raster frame should contain touched pixels"); MetaCoreExpect( std::any_of( rasterFrame.Rgba.begin(), rasterFrame.Rgba.end(), [](std::uint8_t value) { return value != 0; } ), "Runtime UI raster frame should contain non-zero pixel data" ); renderer.Shutdown(); MetaCoreExpect(!renderer.GetStats().Initialized, "Runtime UI renderer should clear initialized state on shutdown"); MetaCoreExpect(!renderer.GetStats().Loaded, "Runtime UI renderer should clear loaded state on shutdown"); MetaCoreExpect(renderer.GetLastFrame().Commands.empty(), "Runtime UI renderer should clear draw frame on shutdown"); MetaCoreExpect(renderer.GetLastRasterFrame().Rgba.empty(), "Runtime UI renderer should clear raster frame on shutdown"); } void MetaCoreTestDecoupledSnapshotSync() { // 1. 创建默认场景并收集核心对象 ID MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); // 动态添加一个 box1.glb,测试需要它作为网格对象进行解耦同步验证 MetaCore::MetaCoreGameObject cubeObj = scene.CreateGameObject("box1.glb"); auto& meshRenderer = cubeObj.AddComponent(); meshRenderer.MeshSource = MetaCore::MetaCoreMeshSourceKind::Asset; meshRenderer.SourceModelPath = "Assets/Models/box1.glb"; meshRenderer.SourceModelAssetGuid = MetaCore::MetaCoreAssetRegistry::Get().ResolvePathToGuid("Assets/Models/box1.glb"); cubeObj.AddComponent(MetaCore::MetaCoreModelRootTag{ "Assets/Models/box1.glb" }); cubeObj.GetComponent().Position = glm::vec3(0.0F, 0.5F, 0.0F); MetaCore::MetaCoreId cubeId = 0; MetaCore::MetaCoreId lightId = 0; MetaCore::MetaCoreId cameraId = 0; for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) { if (object.GetName() == "box1.glb") { cubeId = object.GetId(); } if (object.GetName() == "Directional Light") { lightId = object.GetId(); } if (object.GetName() == "Main Camera") { cameraId = object.GetId(); } } MetaCoreExpect(cubeId != 0, "默认场景应包含网格渲染器 Cube"); MetaCoreExpect(lightId != 0, "默认场景应包含光源 Light"); MetaCoreExpect(cameraId != 0, "默认场景应包含相机 Camera"); // 2. 生成快照数据 MetaCore::MetaCoreSceneRenderSync renderSync; MetaCore::MetaCoreSceneRenderSyncSnapshot snapshot = renderSync.BuildSnapshot(scene); // 3. 修改快照中的参数以测试解耦逻辑和 Enabled 属性过滤 glm::mat4 customLocalMatrix = glm::translate(glm::mat4(1.0F), glm::vec3(15.0F, -5.0F, 3.5F)); snapshot.LocalMatrices[cubeId] = customLocalMatrix; snapshot.WorldMatrices[cubeId] = customLocalMatrix; // 禁用所有光源和相机以测试过滤和默认光源自动起降 for (auto& light : snapshot.Lights) { light.Enabled = false; } for (auto& camera : snapshot.Cameras) { camera.Enabled = false; } snapshot.PrimaryCamera = std::nullopt; // 4. 初始化窗口和 FilamentSceneBridge MetaCore::MetaCoreWindow window; bool winOk = window.Initialize(800, 600, "DecoupledSnapshotSyncTestWindow"); MetaCoreExpect(winOk, "测试窗口初始化应成功"); MetaCore::MetaCoreFilamentSceneBridge bridge; bridge.SetProjectRootPath("d:/MetaCore/SandboxProject"); bool bridgeOk = bridge.Initialize(window); MetaCoreExpect(bridgeOk, "测试桥接器初始化应成功"); // 5. 在 scene == nullptr 的快照渲染模式下执行同步 std::cout << "[DEBUG] SyncScene starting..." << std::endl; bridge.SyncScene(snapshot, nullptr, false, false); std::cout << "[DEBUG] SyncScene completed." << std::endl; // 6. 验证映射重建和变换同步 glm::mat4 cubeWorldMatrix; bool hasCube = bridge.TryGetObjectWorldMatrix(cubeId, cubeWorldMatrix); std::cout << "[DEBUG] hasCube: " << hasCube << std::endl; MetaCoreExpect(hasCube, "无 scene 模式下重建映射后应包含 Cube 变换"); bool transOk = bridge.VerifyTransformForTesting(cubeId, customLocalMatrix); std::cout << "[DEBUG] transOk: " << transOk << std::endl; MetaCoreExpect(transOk, "快照中的自定义局部变换应正确同步到 Filament"); bool cubeVisibleInScene = bridge.VerifyEntityInSceneForTesting(cubeId); std::cout << "[DEBUG] cubeVisibleInScene: " << cubeVisibleInScene << std::endl; MetaCoreExpect(cubeVisibleInScene, "默认可见的网格实体应该在 Filament 场景中"); // 7. 验证 Enabled 状态为 false 的光源被过滤排除,且默认灯光起降机制生效 (自动亮起) bool hasLight = bridge.VerifyLightExistsForTesting(lightId); std::cout << "[DEBUG] hasLight: " << hasLight << std::endl; MetaCoreExpect(!hasLight, "Enabled 为 false 的自定义光源不应被实例化"); float defaultLightIntensity = bridge.GetDefaultLightIntensityForTesting(); std::cout << "[DEBUG] defaultLightIntensity: " << defaultLightIntensity << std::endl; MetaCoreExpect(defaultLightIntensity > 99000.0F, "无有效自定义光源时,默认灯光应起飞作为兜底"); // 8. 动态开启光源属性,验证默认灯光动态变暗 for (auto& light : snapshot.Lights) { if (light.ObjectId == lightId) { light.Enabled = true; } } // 同时我们将 cube 的 Visible 改为 false,测试动态隐藏 for (auto& r : snapshot.Renderables) { if (r.ObjectId == cubeId) { r.Visible = false; } } std::cout << "[DEBUG] SyncScene 2 starting..." << std::endl; bridge.SyncScene(snapshot, nullptr, false, false); std::cout << "[DEBUG] SyncScene 2 completed." << std::endl; bool hasLightNow = bridge.VerifyLightExistsForTesting(lightId); std::cout << "[DEBUG] hasLightNow: " << hasLightNow << std::endl; MetaCoreExpect(hasLightNow, "启用光源后自定义光源应被实例化"); float defaultLightIntensityNow = bridge.GetDefaultLightIntensityForTesting(); std::cout << "[DEBUG] defaultLightIntensityNow: " << defaultLightIntensityNow << std::endl; MetaCoreExpect(defaultLightIntensityNow < 1.0F, "启用自定义光源时,默认灯光强度应自动降为 0"); bool cubeVisibleInScene2 = bridge.VerifyEntityInSceneForTesting(cubeId); std::cout << "[DEBUG] cubeVisibleInScene2: " << cubeVisibleInScene2 << std::endl; MetaCoreExpect(!cubeVisibleInScene2, "禁用可见性后,网格实体应该从 Filament 场景中移除"); // 9. 再次禁用光源,验证自定义光源被销毁并且默认灯光复亮 for (auto& light : snapshot.Lights) { if (light.ObjectId == lightId) { light.Enabled = false; } } std::cout << "[DEBUG] SyncScene 3 starting..." << std::endl; bridge.SyncScene(snapshot, nullptr, false, false); std::cout << "[DEBUG] SyncScene 3 completed." << std::endl; bool hasLightFinal = bridge.VerifyLightExistsForTesting(lightId); std::cout << "[DEBUG] hasLightFinal: " << hasLightFinal << std::endl; MetaCoreExpect(!hasLightFinal, "再次禁用光源后自定义光源实体应被正确销毁移出"); float defaultLightIntensityFinal = bridge.GetDefaultLightIntensityForTesting(); std::cout << "[DEBUG] defaultLightIntensityFinal: " << defaultLightIntensityFinal << std::endl; MetaCoreExpect(defaultLightIntensityFinal > 99000.0F, "再次无激活光源时,默认灯光应重新亮起兜底"); // 9.5 模拟删除网格对象,验证其在 FilamentSceneBridge 中被成功卸载 std::cout << "[DEBUG] Deleting mesh object..." << std::endl; for (auto it = snapshot.Renderables.begin(); it != snapshot.Renderables.end(); ) { if (it->ObjectId == cubeId) { it = snapshot.Renderables.erase(it); } else { ++it; } } std::cout << "[DEBUG] SyncScene 4 starting..." << std::endl; bridge.SyncScene(snapshot, nullptr, false, false); std::cout << "[DEBUG] SyncScene 4 completed." << std::endl; glm::mat4 deletedWorldMatrix; bool hasCubeAfterDelete = bridge.TryGetObjectWorldMatrix(cubeId, deletedWorldMatrix); std::cout << "[DEBUG] hasCubeAfterDelete: " << hasCubeAfterDelete << std::endl; MetaCoreExpect(!hasCubeAfterDelete, "删除网格对象后,其在桥接器映射中不应再存在"); // 10. 资源清理 std::cout << "[DEBUG] bridge.Shutdown starting..." << std::endl; bridge.Shutdown(); std::cout << "[DEBUG] bridge.Shutdown completed." << std::endl; window.Shutdown(); } void MetaCoreTestNestedModelHierarchyTransformSync() { // 1. 创建测试场景,建立嵌套模型和父子变换结构 MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); // 模拟根模型 (box1.glb) MetaCore::MetaCoreGameObject parentModel = scene.CreateGameObject("ParentModel"); auto& parentMesh = parentModel.AddComponent(); parentMesh.MeshSource = MetaCore::MetaCoreMeshSourceKind::Asset; parentMesh.SourceModelPath = "Assets/Models/box1.glb"; parentMesh.SourceModelAssetGuid = MetaCore::MetaCoreAssetRegistry::Get().ResolvePathToGuid("Assets/Models/box1.glb"); parentModel.AddComponent(MetaCore::MetaCoreModelRootTag{ "Assets/Models/box1.glb" }); parentModel.GetComponent().Position = glm::vec3(0.0F, 1.0F, 0.0F); // 模拟子模型 (嵌套相同的 box1.glb,其 parent 为 parentModel) MetaCore::MetaCoreGameObject childModel = scene.CreateGameObject("ChildModel", parentModel.GetId()); auto& childMesh = childModel.AddComponent(); childMesh.MeshSource = MetaCore::MetaCoreMeshSourceKind::Asset; childMesh.SourceModelPath = "Assets/Models/box1.glb"; childMesh.SourceModelAssetGuid = MetaCore::MetaCoreAssetRegistry::Get().ResolvePathToGuid("Assets/Models/box1.glb"); childModel.AddComponent(MetaCore::MetaCoreModelRootTag{ "Assets/Models/box1.glb" }); childModel.GetComponent().Position = glm::vec3(2.0F, 0.0F, 0.0F); // 局部偏移 x + 2.0 MetaCore::MetaCoreSceneRenderSync renderSync; MetaCore::MetaCoreSceneRenderSyncSnapshot snapshot = renderSync.BuildSnapshot(scene); // 验证 Bug 2 修复:子模型的 HostRootId 应该为它自己,而不是父模型 bool foundChild = false; for (const auto& renderable : snapshot.Renderables) { if (renderable.ObjectId == childModel.GetId()) { foundChild = true; MetaCoreExpect(renderable.HostRootId == childModel.GetId(), "子模型的 HostRootId 必须等于其自身的 ID,不能被错判为父模型 ID"); } } MetaCoreExpect(foundChild, "快照的渲染实体列表中应包含 ChildModel"); // 2. 初始化桥接器 MetaCore::MetaCoreWindow window; bool winOk = window.Initialize(800, 600, "NestedModelHierarchyTestWindow"); MetaCoreExpect(winOk, "测试窗口初始化成功"); MetaCore::MetaCoreFilamentSceneBridge bridge; bridge.SetProjectRootPath("d:/MetaCore/SandboxProject"); bool bridgeOk = bridge.Initialize(window); MetaCoreExpect(bridgeOk, "测试桥接器初始化成功"); // 同步快照 bridge.SyncScene(snapshot, nullptr, false, false); // 验证 Bug 1 修复:在没有 Filament 层级关系下,子模型在 Filament 侧的变换矩阵应当正确跟随父模型的世界矩阵 // 子模型局部矩阵 glm::mat4 childLocalMat = glm::translate(glm::mat4(1.0F), glm::vec3(2.0F, 0.0F, 0.0F)); // 父模型世界矩阵 glm::mat4 parentWorldMat = glm::translate(glm::mat4(1.0F), glm::vec3(0.0F, 1.0F, 0.0F)); // 预期的子模型世界矩阵 glm::mat4 expectedChildWorldMat = parentWorldMat * childLocalMat; bool transOk = bridge.VerifyTransformForTesting(childModel.GetId(), expectedChildWorldMat); MetaCoreExpect(transOk, "子模型在 Filament 侧的最终变换矩阵应正确应用世界矩阵"); // 3. 拖拽父级:修改父模型位置,重新同步,并校验子模型是否跟着移动 parentModel.GetComponent().Position = glm::vec3(10.0F, 5.0F, 0.0F); // 重新构建快照与同步 snapshot = renderSync.BuildSnapshot(scene); bridge.SyncScene(snapshot, nullptr, false, false); glm::mat4 newParentWorldMat = glm::translate(glm::mat4(1.0F), glm::vec3(10.0F, 5.0F, 0.0F)); glm::mat4 newExpectedChildWorldMat = newParentWorldMat * childLocalMat; bool transOk2 = bridge.VerifyTransformForTesting(childModel.GetId(), newExpectedChildWorldMat); MetaCoreExpect(transOk2, "拖拽父级后,子模型在 Filament 侧的最终变换矩阵应实时跟随父模型世界矩阵更新"); bridge.Shutdown(); window.Shutdown(); } void MetaCoreTestP3ProductivityStage() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreTestP3Project"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Library"); // 1. 写入默认的 MetaCore.project.json { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); projectFile << "{\n" << " \"name\": \"P3Project\",\n" << " \"version\": \"0.1.0\",\n" << " \"scenes\": [],\n" << " \"startup_scene\": \"\"\n" << "}\n"; } _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 assetEditingService = moduleRegistry.ResolveService(); MetaCoreExpect(assetEditingService != nullptr, "AssetEditingService should be available"); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(assetDatabase->HasProject(), "应成功打开项目"); // 2. 测试资产自动导入与默认目录 (P3.2) // 写入一个没有 .mcmeta 的 .ktx 贴图文件 const std::filesystem::path texturePath = tempProjectRoot / "Assets" / "test_texture.ktx"; { std::ofstream textureFile(texturePath, std::ios::trunc); textureFile << "dummy-ktx-data"; } assetDatabase->Refresh(); // 验证是否在 Assets 下自动生成了 test_texture.ktx.mcmeta 物理元文件 const std::filesystem::path metaPath = tempProjectRoot / "Assets" / "test_texture.ktx.mcmeta"; MetaCoreExpect(std::filesystem::exists(metaPath), "Refresh 后应生成 test_texture.ktx.mcmeta 物理元文件"); // 读取该 .mcmeta 文件内容,校验结构 std::ifstream metaFile(metaPath); nlohmann::json metaJson; metaFile >> metaJson; metaFile.close(); MetaCoreExpect(metaJson.contains("guid"), "元文件应包含 guid"); MetaCoreExpect(metaJson.contains("asset_type") && metaJson["asset_type"] == "texture", "元文件 asset_type 应为 texture"); MetaCoreExpect(metaJson.contains("importer_id") && metaJson["importer_id"] == "TextureImporter", "元文件 importer_id 应为 TextureImporter"); const std::string textureGuidStr = metaJson["guid"].get(); MetaCore::MetaCoreAssetGuid textureGuid = MetaCore::MetaCoreAssetGuid::Parse(textureGuidStr).value_or(MetaCore::MetaCoreAssetGuid{}); MetaCoreExpect(textureGuid.IsValid(), "自动分配的材质 GUID 应为有效 GUID"); // 3. 测试资产重命名与删除 (P3.1, P3.4) // 重命名该贴图文件 MetaCoreExpect(assetDatabase->RenamePath(std::filesystem::path("Assets") / "test_texture.ktx", "renamed_texture.ktx"), "应能重命名贴图文件"); const std::filesystem::path renamedTexturePath = tempProjectRoot / "Assets" / "renamed_texture.ktx"; const std::filesystem::path renamedMetaPath = tempProjectRoot / "Assets" / "renamed_texture.ktx.mcmeta"; MetaCoreExpect(std::filesystem::exists(renamedTexturePath), "重命名后新贴图文件应存在"); MetaCoreExpect(!std::filesystem::exists(texturePath), "重命名后旧贴图文件不应存在"); MetaCoreExpect(std::filesystem::exists(renamedMetaPath), "重命名后新 mcmeta 文件应存在"); MetaCoreExpect(!std::filesystem::exists(metaPath), "重命名后旧 mcmeta 文件不应存在"); // 删除重命名后的贴图文件 MetaCoreExpect(assetDatabase->DeletePath(std::filesystem::path("Assets") / "renamed_texture.ktx"), "应能删除贴图文件"); MetaCoreExpect(!std::filesystem::exists(renamedTexturePath), "删除后贴图文件应不存在"); MetaCoreExpect(!std::filesystem::exists(renamedMetaPath), "删除后 mcmeta 文件应不存在"); // 4. 测试材质创建、加载、编辑保存及物理 Hash 更新 (P3.4) // 保存一个初始材质 const std::filesystem::path materialPath = tempProjectRoot / "Assets" / "test_material.mcmaterial.json"; MetaCore::MetaCoreMaterialAssetDocument materialDoc; materialDoc.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate(); materialDoc.Name = "test_material"; materialDoc.BaseColor = glm::vec3(1.0F, 0.0F, 0.0F); materialDoc.Metallic = 0.5F; materialDoc.Roughness = 0.8F; MetaCore::MetaCoreTypeRegistry registry; MetaCoreRegisterFoundationGeneratedTypes(registry); MetaCoreRegisterSceneGeneratedTypes(registry); MetaCoreExpect( MetaCore::MetaCoreSceneSerializer::SaveMaterialToJson(materialPath, materialDoc, registry), "应能保存材质文件" ); // 自动刷新生成 .mcmeta 物理元文件 assetDatabase->Refresh(); const std::filesystem::path materialMetaPath = tempProjectRoot / "Assets" / "test_material.mcmaterial.json.mcmeta"; const auto materialRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "test_material.mcmaterial.json"); MetaCoreExpect(materialRecord.has_value(), "Material asset should be registered after refresh"); materialDoc.AssetGuid = materialRecord->Guid; MetaCoreExpect(std::filesystem::exists(materialMetaPath), "Refresh 后应生成材质的 mcmeta 元文件"); // 编辑材质属性并重新保存 materialDoc.BaseColor = glm::vec3(0.0F, 1.0F, 0.0F); materialDoc.Metallic = 0.9F; materialDoc.Roughness = 0.1F; MetaCoreExpect( MetaCore::MetaCoreSceneSerializer::SaveMaterialToJson(materialPath, materialDoc, registry), "应能重新保存修改后的材质文件" ); // 模拟材质编辑器的物理 Hash 保存更新逻辑 std::uint64_t initialHash = 0; { std::ifstream input(materialMetaPath); nlohmann::json json; input >> json; input.close(); if (json.contains("source_hash")) { initialHash = json["source_hash"].get(); } } // 更新 Hash const std::uint64_t newHash = MetaCore::MetaCoreHashFile(materialPath).value_or(0); { std::ifstream input(materialMetaPath); nlohmann::json json; input >> json; input.close(); json["source_hash"] = newHash; std::ofstream output(materialMetaPath); output << json.dump(4); output.close(); } // 读回以校验 std::uint64_t savedHash = 0; { std::ifstream input(materialMetaPath); nlohmann::json json; input >> json; input.close(); if (json.contains("source_hash")) { savedHash = json["source_hash"].get(); } } MetaCoreExpect(savedHash == newHash, "保存并更新后,材质 mcmeta 物理哈希值应被更新且一致"); // 5. 测试拖拽绑定到 Mesh 槽位 (P3.3) MetaCore::MetaCoreScene scene; MetaCore::MetaCoreGameObject gameObject = scene.CreateGameObject("MeshObject"); auto& meshRenderer = gameObject.AddComponent(); // 模拟槽位绑定 if (meshRenderer.MaterialAssetGuids.empty()) { meshRenderer.MaterialAssetGuids.resize(1); } meshRenderer.MaterialAssetGuids[0] = materialDoc.AssetGuid; materialDoc.BaseColor = glm::vec3(0.2F, 0.4F, 0.6F); materialDoc.Metallic = 0.7F; materialDoc.Roughness = 0.25F; materialDoc.DoubleSided = false; materialDoc.AlphaMode = MetaCore::MetaCoreMaterialAlphaMode::Blend; materialDoc.AlphaCutoff = 0.35F; materialDoc.EmissiveColor = glm::vec3(0.05F, 0.1F, 0.15F); MetaCoreExpect( assetEditingService->SaveMaterialAsset(materialDoc.AssetGuid, materialDoc), "AssetEditingService should save standalone material assets" ); const auto serviceLoadedMaterial = assetEditingService->LoadMaterialAsset(materialDoc.AssetGuid); MetaCoreExpect(serviceLoadedMaterial.has_value(), "AssetEditingService should reload saved standalone material assets"); MetaCoreExpect(serviceLoadedMaterial->AssetGuid == materialDoc.AssetGuid, "Saved material document guid should match AssetDatabase guid"); MetaCoreExpectVec3Near(serviceLoadedMaterial->BaseColor, glm::vec3(0.2F, 0.4F, 0.6F), "Saved material base color should round trip"); MetaCoreExpect(std::abs(serviceLoadedMaterial->Metallic - 0.7F) <= 0.0001F, "Saved material metallic should round trip"); MetaCoreExpect(std::abs(serviceLoadedMaterial->Roughness - 0.25F) <= 0.0001F, "Saved material roughness should round trip"); std::uint64_t serviceSavedHash = 0; { std::ifstream input(materialMetaPath); nlohmann::json json; input >> json; input.close(); if (json.contains("source_hash")) { serviceSavedHash = json["source_hash"].get(); } } MetaCoreExpect( serviceSavedHash == MetaCore::MetaCoreHashFile(materialPath).value_or(0), "AssetEditingService material save should update source hash" ); MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorContext editorContext( window, renderDevice, viewportRenderer, scene, logService, moduleRegistry ); MetaCoreExpect( assetEditingService->ApplyMaterialAssetPreviewToScene(editorContext, materialDoc.AssetGuid), "Standalone material asset preview should apply to scene mesh renderers" ); MetaCore::MetaCoreGameObject syncedObject = scene.FindGameObject(gameObject.GetId()); MetaCoreExpect(syncedObject, "Synced mesh object should still exist"); const auto& syncedMeshRenderer = syncedObject.GetComponent(); MetaCoreExpect(syncedMeshRenderer.MaterialAssetGuids[0] == materialDoc.AssetGuid, "Standalone material slot guid should stay bound"); MetaCoreExpectVec3Near(syncedMeshRenderer.BaseColor, glm::vec3(0.2F, 0.4F, 0.6F), "Standalone material base color should sync to MeshRenderer"); MetaCoreExpect(std::abs(syncedMeshRenderer.Metallic - 0.7F) <= 0.0001F, "Standalone material metallic should sync to MeshRenderer"); MetaCoreExpect(std::abs(syncedMeshRenderer.Roughness - 0.25F) <= 0.0001F, "Standalone material roughness should sync to MeshRenderer"); MetaCoreExpect(!syncedMeshRenderer.DoubleSided, "Standalone material double-sided flag should sync to MeshRenderer"); MetaCoreExpect(syncedMeshRenderer.AlphaMode == MetaCore::MetaCoreMeshAlphaMode::Blend, "Standalone material alpha mode should sync to MeshRenderer"); MetaCoreExpect(std::abs(syncedMeshRenderer.AlphaCutoff - 0.35F) <= 0.0001F, "Standalone material alpha cutoff should sync to MeshRenderer"); MetaCoreExpectVec3Near(syncedMeshRenderer.EmissiveColor, glm::vec3(0.05F, 0.1F, 0.15F), "Standalone material emissive color should sync to MeshRenderer"); MetaCoreExpect(meshRenderer.MaterialAssetGuids[0] == materialDoc.AssetGuid, "拖拽绑定后槽位 0 材质 Guid 应正确"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); std::filesystem::remove_all(tempProjectRoot); } } // namespace int main() { std::setvbuf(stdout, nullptr, _IONBF, 0); std::setvbuf(stderr, nullptr, _IONBF, 0); std::cout << "[RUN] MetaCoreCreateDefaultScene..." << std::endl; MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); std::cout << "[RUN] MetaCoreExpect default objects count..." << std::endl; MetaCoreExpect(scene.GetGameObjects().size() == 2, "默认场景应包含两个对象"); bool hasCamera = false; bool hasLight = false; for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) { hasCamera = hasCamera || object.HasComponent(); hasLight = hasLight || object.HasComponent(); } MetaCoreExpect(hasCamera, "默认场景应包含摄像机"); MetaCoreExpect(hasLight, "默认场景应包含光源"); std::cout << "[RUN] MetaCoreDummyPanelProvider test..." << std::endl; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; moduleRegistry.RegisterPanelProvider(std::make_unique()); MetaCoreExpect(!moduleRegistry.AccessPanelOpenState("Dummy"), "面板默认状态应为关闭"); std::cout << "[RUN] MetaCoreLogService test..." << std::endl; MetaCore::MetaCoreLogService logService; logService.AddEntry(MetaCore::MetaCoreLogLevel::Info, "Test", "Smoke"); MetaCoreExpect(logService.GetEntries().size() == 1, "日志服务应记录一条日志"); std::cout << "[RUN] MetaCoreTestSceneEditApi..." << std::endl; MetaCoreTestSceneEditApi(); std::cout << "[RUN] MetaCoreTestSelectionStateMachine..." << std::endl; MetaCoreTestSelectionStateMachine(); std::cout << "[RUN] MetaCoreTestUndoRedo..." << std::endl; MetaCoreTestUndoRedo(); std::cout << "[RUN] MetaCoreTestBuiltinModuleComposition..." << std::endl; MetaCoreTestBuiltinModuleComposition(); std::cout << "[RUN] MetaCoreTestLegacyBinarySceneStartupLoadCompatibility..." << std::endl; MetaCoreTestLegacyBinarySceneStartupLoadCompatibility(); std::cout << "[RUN] MetaCoreTestProjectFileRoundTripAndDiscovery..." << std::endl; MetaCoreTestProjectFileRoundTripAndDiscovery(); std::cout << "[RUN] MetaCoreTestAssetDatabaseCreateAndOpenProject..." << std::endl; MetaCoreTestAssetDatabaseCreateAndOpenProject(); std::cout << "[RUN] MetaCoreTestAssetDatabaseCreatesFirstClassProjectAssets..." << std::endl; MetaCoreTestAssetDatabaseCreatesFirstClassProjectAssets(); std::cout << "[RUN] MetaCoreTestAssetDatabaseRenamePathUpdatesProjectDescriptor..." << std::endl; MetaCoreTestAssetDatabaseRenamePathUpdatesProjectDescriptor(); std::cout << "[RUN] MetaCoreTestAssetDatabaseMovePathUpdatesProjectDescriptor..." << std::endl; MetaCoreTestAssetDatabaseMovePathUpdatesProjectDescriptor(); std::cout << "[RUN] MetaCoreTestComponentRegistryDescriptors..." << std::endl; MetaCoreTestComponentRegistryDescriptors(); std::cout << "[RUN] MetaCoreTestReflectionFieldEditorDescriptors..." << std::endl; MetaCoreTestReflectionFieldEditorDescriptors(); std::cout << "[RUN] MetaCoreTestPlayModeLifecycleAndSceneIsolation..." << std::endl; MetaCoreTestPlayModeLifecycleAndSceneIsolation(); std::cout << "[RUN] MetaCoreTestSceneGizmoMultiSelectDeltaAndUndo..." << std::endl; MetaCoreTestSceneGizmoMultiSelectDeltaAndUndo(); std::cout << "[RUN] MetaCoreTestSceneViewportHoverPicking..." << std::endl; MetaCoreTestSceneViewportHoverPicking(); std::cout << "[RUN] MetaCoreTestGizmoSnapSettings..." << std::endl; MetaCoreTestGizmoSnapSettings(); std::cout << "[RUN] MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot..." << std::endl; MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot(); std::cout << "[RUN] MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson..." << std::endl; MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson(); std::cout << "[RUN] MetaCoreTestStaticCppComponentScenePersistenceAndUndo..." << std::endl; MetaCoreTestStaticCppComponentScenePersistenceAndUndo(); std::cout << "[RUN] MetaCoreTestImportPipelineAndCook..." << std::endl; MetaCoreTestImportPipelineAndCook(); std::cout << "[RUN] MetaCoreTestProjectDescriptorAndGltfImporterSkeleton..." << std::endl; MetaCoreTestProjectDescriptorAndGltfImporterSkeleton(); std::cout << "[RUN] MetaCoreTestInstantiateImportedModelAssetIntoScene..." << std::endl; MetaCoreTestInstantiateImportedModelAssetIntoScene(); std::cout << "[RUN] MetaCoreTestInstantiateMultiNodeModelAssetIntoScene..." << std::endl; MetaCoreTestInstantiateMultiNodeModelAssetIntoScene(); std::cout << "[RUN] MetaCoreTestDeleteModelAssetCorrectlyClearsFromViewport..." << std::endl; MetaCoreTestDeleteModelAssetCorrectlyClearsFromViewport(); std::cout << "[RUN] MetaCoreTestDeleteModelSubChildCorrectlyClearsFromViewport..." << std::endl; MetaCoreTestDeleteModelSubChildCorrectlyClearsFromViewport(); std::cout << "[RUN] MetaCoreTestModelReimportKeepsGeneratedAssetReferencesStable..." << std::endl; MetaCoreTestModelReimportKeepsGeneratedAssetReferencesStable(); std::cout << "[RUN] MetaCoreTestBootstrapStartupSceneCreation..." << std::endl; MetaCoreTestBootstrapStartupSceneCreation(); std::cout << "[RUN] MetaCoreTestPrefabWorkflow..." << std::endl; MetaCoreTestPrefabWorkflow(); std::cout << "[RUN] MetaCoreTestCookPipelineCooksJsonScenePrefabMaterialUi..." << std::endl; MetaCoreTestCookPipelineCooksJsonScenePrefabMaterialUi(); std::cout << "[RUN] MetaCoreTestBuildServiceCreatesPlayerPackageLayout..." << std::endl; MetaCoreTestBuildServiceCreatesPlayerPackageLayout(); std::cout << "[RUN] MetaCoreTestComponentRegistryOperations..." << std::endl; MetaCoreTestComponentRegistryOperations(); std::cout << "[RUN] MetaCoreTestRuntimeDataTypeSerialization..." << std::endl; MetaCoreTestRuntimeDataTypeSerialization(); std::cout << "[RUN] MetaCoreTestRuntimeDataProjectDocumentSerialization..." << std::endl; MetaCoreTestRuntimeDataProjectDocumentSerialization(); std::cout << "[RUN] MetaCoreTestMeshRendererResourceSerialization..." << std::endl; MetaCoreTestMeshRendererResourceSerialization(); std::cout << "[RUN] MetaCoreTestRuntimeDataBinaryDocumentIo..." << std::endl; MetaCoreTestRuntimeDataBinaryDocumentIo(); std::cout << "[RUN] MetaCoreTestRuntimeProjectDocumentIo..." << std::endl; MetaCoreTestRuntimeProjectDocumentIo(); std::cout << "[RUN] MetaCoreTestRuntimeDiagnosticsSnapshotIo..." << std::endl; MetaCoreTestRuntimeDiagnosticsSnapshotIo(); std::cout << "[RUN] MetaCoreTestRuntimeDataDispatcherConstruction..." << std::endl; MetaCoreTestRuntimeDataDispatcherConstruction(); std::cout << "[RUN] MetaCoreTestRuntimeDataDispatcherAppliesUpdates..." << std::endl; MetaCoreTestRuntimeDataDispatcherAppliesUpdates(); std::cout << "[RUN] MetaCoreTestRuntimeDataDispatcherRejectsInvalidUpdateDiagnostics..." << std::endl; MetaCoreTestRuntimeDataDispatcherRejectsInvalidUpdateDiagnostics(); std::cout << "[RUN] MetaCoreTestMockRuntimeDataSourceAdapterEmitsUpdates..." << std::endl; MetaCoreTestMockRuntimeDataSourceAdapterEmitsUpdates(); std::cout << "[RUN] MetaCoreTestRuntimeDataDispatcherMarksStaleBindings..." << std::endl; MetaCoreTestRuntimeDataDispatcherMarksStaleBindings(); std::cout << "[RUN] MetaCoreTestMockRuntimeDataSourceAdapterDegradedState..." << std::endl; MetaCoreTestMockRuntimeDataSourceAdapterDegradedState(); std::cout << "[RUN] MetaCoreTestFileReplayRuntimeDataSourceAdapterEmitsReplayFrames..." << std::endl; MetaCoreTestFileReplayRuntimeDataSourceAdapterEmitsReplayFrames(); std::cout << "[RUN] MetaCoreTestRuntimeDiagnosticsSnapshotReportsFaults..." << std::endl; MetaCoreTestRuntimeDiagnosticsSnapshotReportsFaults(); std::cout << "[RUN] MetaCoreTestRuntimeDataBinaryDocumentRejectsCorruption..." << std::endl; MetaCoreTestRuntimeDataBinaryDocumentRejectsCorruption(); std::cout << "[RUN] MetaCoreTestEditorContextRuntimeDataConfigSaveLoad..." << std::endl; MetaCoreTestEditorContextRuntimeDataConfigSaveLoad(); std::cout << "[RUN] MetaCoreTestEditorContextLoadsRuntimeDiagnosticsFromProjectPath..." << std::endl; MetaCoreTestEditorContextLoadsRuntimeDiagnosticsFromProjectPath(); std::cout << "[RUN] MetaCoreTestEditorContextRuntimeDataDefaultsFollowRuntimeDirectory..." << std::endl; MetaCoreTestEditorContextRuntimeDataDefaultsFollowRuntimeDirectory(); std::cout << "[RUN] MetaCoreTestRuntimeDataConfigValidationRejectsBrokenBinding..." << std::endl; MetaCoreTestRuntimeDataConfigValidationRejectsBrokenBinding(); std::cout << "[RUN] MetaCoreTestRuntimeDataConfigValidationRejectsBindingTypeMismatch..." << std::endl; MetaCoreTestRuntimeDataConfigValidationRejectsBindingTypeMismatch(); std::cout << "[RUN] MetaCoreTestRuntimeDataConfigValidationRejectsMissingUiTargetNode..." << std::endl; MetaCoreTestRuntimeDataConfigValidationRejectsMissingUiTargetNode(); std::cout << "[RUN] MetaCoreTestRuntimeDataConfigValidationRejectsMissingReplayFilePath..." << std::endl; MetaCoreTestRuntimeDataConfigValidationRejectsMissingReplayFilePath(); std::cout << "[RUN] MetaCoreTestRuntimeDataConfigValidationRejectsMissingTcpPort..." << std::endl; MetaCoreTestRuntimeDataConfigValidationRejectsMissingTcpPort(); std::cout << "[RUN] MetaCoreTestEditorContextRejectsUnsafeRuntimeProjectPaths..." << std::endl; MetaCoreTestEditorContextRejectsUnsafeRuntimeProjectPaths(); std::cout << "[RUN] MetaCoreTestEditorContextRejectsUnsafeRuntimeReplayPath..." << std::endl; MetaCoreTestEditorContextRejectsUnsafeRuntimeReplayPath(); std::cout << "[RUN] MetaCoreTestEditorContextRejectsMissingRuntimeUiBindingNode..." << std::endl; MetaCoreTestEditorContextRejectsMissingRuntimeUiBindingNode(); std::cout << "[RUN] MetaCoreTestEditorContextValidatesRuntimeSceneBindingTargets..." << std::endl; MetaCoreTestEditorContextValidatesRuntimeSceneBindingTargets(); std::cout << "[RUN] MetaCoreTestEditorContextRejectsInvalidRuntimeDataSave..." << std::endl; MetaCoreTestEditorContextRejectsInvalidRuntimeDataSave(); std::cout << "[RUN] MetaCoreTestTcpRuntimeDataSourceAdapterReadsSocketStream..." << std::endl; MetaCoreTestTcpRuntimeDataSourceAdapterReadsSocketStream(); std::cout << "[RUN] MetaCoreTestUiDocumentSerialization..." << std::endl; MetaCoreTestUiDocumentSerialization(); std::cout << "[RUN] MetaCoreTestUiDocumentRmlCompilation..." << std::endl; MetaCoreTestUiDocumentRmlCompilation(); std::cout << "[RUN] MetaCoreTestRuntimeUiRendererLifecycle..." << std::endl; MetaCoreTestRuntimeUiRendererLifecycle(); std::cout << "[RUN] MetaCoreTestDecoupledSnapshotSync..." << std::endl; MetaCoreTestDecoupledSnapshotSync(); std::cout << "[RUN] MetaCoreTestNestedModelHierarchyTransformSync..." << std::endl; MetaCoreTestNestedModelHierarchyTransformSync(); std::cout << "[RUN] MetaCoreTestP3ProductivityStage..." << std::endl; MetaCoreTestP3ProductivityStage(); std::cout << "MetaCoreSmokeTests passed\n"; return 0; }