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