MetaCore/tests/MetaCoreSmokeTests.cpp
2026-06-16 10:30:12 +08:00

4618 lines
239 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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 "MetaCoreFoundation/MetaCoreAssetRegistry.h"
#include "MetaCoreFoundation/MetaCoreAssetDependencyGraph.h"
#include "MetaCoreFoundation/MetaCoreRuntimeAssetRegistry.h"
#include "MetaCorePlatform/MetaCoreWindow.h"
#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h"
#include "MetaCoreRender/MetaCoreRenderDevice.h"
#include "MetaCoreRender/MetaCoreSceneRenderSync.h"
#include "MetaCoreRender/MetaCoreFilamentSceneBridge.h"
#include <glm/gtc/matrix_transform.hpp>
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataDispatcher.h"
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataSource.h"
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h"
#include "MetaCoreScene/MetaCoreScenePackage.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include "MetaCoreScene/MetaCoreTransformUtils.h"
#include "MetaCoreFoundation/MetaCoreHash.h"
#include <nlohmann/json.hpp>
#include <cstdlib>
#include <cmath>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <memory>
#include <thread>
#include <unordered_map>
#include <utility>
#if !defined(_WIN32)
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#endif
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#endif
namespace {
#if !defined(_WIN32)
int _putenv_s(const char* name, const char* value) {
if (name == nullptr) {
return -1;
}
if (value == nullptr || value[0] == '\0') {
return unsetenv(name);
}
return setenv(name, value, 1);
}
#endif
void MetaCoreExpect(bool condition, const char* message) {
if (!condition) {
std::cerr << "MetaCoreSmokeTests failed: " << message << '\n';
std::exit(1);
}
}
[[nodiscard]] std::filesystem::path MetaCoreTestRepositoryRoot() {
return std::filesystem::path(__FILE__).parent_path().parent_path();
}
#if defined(_WIN32)
using MetaCoreTestSocket = SOCKET;
using MetaCoreTestSocketLength = int;
constexpr MetaCoreTestSocket MetaCoreTestInvalidSocket = INVALID_SOCKET;
constexpr int MetaCoreTestSocketError = SOCKET_ERROR;
#else
using MetaCoreTestSocket = int;
using MetaCoreTestSocketLength = socklen_t;
constexpr MetaCoreTestSocket MetaCoreTestInvalidSocket = -1;
constexpr int MetaCoreTestSocketError = -1;
constexpr int SD_SEND = SHUT_WR;
#endif
[[nodiscard]] bool MetaCoreTestInitializeSocketApi() {
#if defined(_WIN32)
WSADATA wsaData{};
return WSAStartup(MAKEWORD(2, 2), &wsaData) == 0;
#else
return true;
#endif
}
void MetaCoreTestCleanupSocketApi() {
#if defined(_WIN32)
WSACleanup();
#endif
}
void MetaCoreTestCloseSocket(MetaCoreTestSocket socketHandle) {
if (socketHandle == MetaCoreTestInvalidSocket) {
return;
}
#if defined(_WIN32)
closesocket(socketHandle);
#else
close(socketHandle);
#endif
}
void MetaCoreExpectVec3Near(const glm::vec3& actual, const glm::vec3& expected, const char* message) {
const auto nearlyEqual = [](float lhs, float rhs) {
return std::abs(lhs - rhs) <= 0.0001F;
};
if (!nearlyEqual(actual.x, expected.x) ||
!nearlyEqual(actual.y, expected.y) ||
!nearlyEqual(actual.z, expected.z)) {
std::cerr << "MetaCoreSmokeTests failed: " << message
<< " (actual=" << actual.x << "," << actual.y << "," << actual.z
<< " expected=" << expected.x << "," << expected.y << "," << expected.z << ")\n";
std::exit(1);
}
}
void MetaCoreExpectMat4Near(const glm::mat4& actual, const glm::mat4& expected, const char* message) {
for (int column = 0; column < 4; ++column) {
for (int row = 0; row < 4; ++row) {
if (std::abs(actual[column][row] - expected[column][row]) > 0.0001F) {
std::cerr << "MetaCoreSmokeTests failed: " << message
<< " (column=" << column
<< " row=" << row
<< " actual=" << actual[column][row]
<< " expected=" << expected[column][row] << ")\n";
std::exit(1);
}
}
}
}
class MetaCoreDummyPanelProvider final : public MetaCore::MetaCoreIEditorPanelProvider {
public:
std::string GetPanelId() const override { return "Dummy"; }
std::string GetPanelTitle() const override { return "Dummy"; }
bool IsOpenByDefault() const override { return false; }
void DrawPanel(MetaCore::MetaCoreEditorContext&) override {}
};
class MetaCoreCountingPlayModeSystem final : public MetaCore::MetaCoreIPlayModeComponentSystem {
public:
[[nodiscard]] std::string GetSystemId() const override { return "MetaCore.Tests.CountingPlayModeSystem"; }
void OnPlayStart(MetaCore::MetaCoreEditorContext&) override { ++StartCount; }
void FixedUpdate(MetaCore::MetaCoreEditorContext&, double fixedDeltaSeconds) override {
++FixedUpdateCount;
LastFixedDeltaSeconds = fixedDeltaSeconds;
}
void Update(MetaCore::MetaCoreEditorContext&, double deltaSeconds) override {
++UpdateCount;
LastUpdateDeltaSeconds = deltaSeconds;
}
void LateUpdate(MetaCore::MetaCoreEditorContext&, double deltaSeconds) override {
++LateUpdateCount;
LastLateDeltaSeconds = deltaSeconds;
}
void OnPlayStop(MetaCore::MetaCoreEditorContext&) override { ++StopCount; }
int StartCount = 0;
int FixedUpdateCount = 0;
int UpdateCount = 0;
int LateUpdateCount = 0;
int StopCount = 0;
double LastFixedDeltaSeconds = 0.0;
double LastUpdateDeltaSeconds = 0.0;
double LastLateDeltaSeconds = 0.0;
};
struct MetaCoreScriptLifecycleCounters {
int StartCount = 0;
int FixedUpdateCount = 0;
int UpdateCount = 0;
int LateUpdateCount = 0;
int StopCount = 0;
};
class MetaCoreCountingNativeScript final : public MetaCore::MetaCoreIScriptBehaviour {
public:
explicit MetaCoreCountingNativeScript(std::shared_ptr<MetaCoreScriptLifecycleCounters> counters)
: Counters_(std::move(counters)) {}
void OnPlayStart(MetaCore::MetaCoreScriptExecutionContext&) override {
++Counters_->StartCount;
}
void FixedUpdate(MetaCore::MetaCoreScriptExecutionContext&, double) override {
++Counters_->FixedUpdateCount;
}
void Update(MetaCore::MetaCoreScriptExecutionContext&, double) override {
++Counters_->UpdateCount;
}
void LateUpdate(MetaCore::MetaCoreScriptExecutionContext&, double) override {
++Counters_->LateUpdateCount;
}
void OnPlayStop(MetaCore::MetaCoreScriptExecutionContext&) override {
++Counters_->StopCount;
}
private:
std::shared_ptr<MetaCoreScriptLifecycleCounters> Counters_{};
};
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), "复制后的对象应可被查找到");
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), "删除后对象不应存在");
}
void MetaCoreTestSelectionStateMachine() {
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
const MetaCore::MetaCoreId objectAId = scene.CreateGameObject("A").GetId();
const MetaCore::MetaCoreId objectBId = scene.CreateGameObject("B").GetId();
const MetaCore::MetaCoreId objectCId = scene.CreateGameObject("C").GetId();
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
editorContext.SelectOnly(objectAId);
MetaCoreExpect(editorContext.GetActiveObjectId() == objectAId, "单选后 Active 应为 A");
MetaCoreExpect(editorContext.GetSelectedObjectIds().size() == 1, "单选后应仅包含一个对象");
editorContext.ToggleSelection(objectBId);
MetaCoreExpect(editorContext.IsObjectSelected(objectAId), "Toggle 后应保留已选对象 A");
MetaCoreExpect(editorContext.IsObjectSelected(objectBId), "Toggle 后应包含对象 B");
MetaCoreExpect(editorContext.GetActiveObjectId() == objectBId, "Toggle 新增对象后 Active 应为 B");
editorContext.SetSelectionAnchorId(objectAId);
editorContext.SelectRangeByOrderedIds(scene.BuildHierarchyPreorder(), objectCId, false);
MetaCoreExpect(editorContext.IsObjectSelected(objectAId), "范围选择应包含起点 A");
MetaCoreExpect(editorContext.IsObjectSelected(objectBId), "范围选择应包含中间对象 B");
MetaCoreExpect(editorContext.IsObjectSelected(objectCId), "范围选择应包含终点 C");
MetaCoreExpect(editorContext.GetActiveObjectId() == objectCId, "范围选择后 Active 应为终点 C");
}
void MetaCoreTestUndoRedo() {
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene();
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const std::size_t beforeCreateCount = scene.GetGameObjects().size();
const bool created = editorContext.ExecuteSnapshotCommand("创建对象", [&]() {
MetaCore::MetaCoreGameObject object = scene.CreateGameObject("UndoRedoObject");
editorContext.SelectOnly(object.GetId());
return true;
});
MetaCoreExpect(created, "创建快照命令应执行成功");
MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount + 1, "执行命令后对象数量应增加");
MetaCoreExpect(editorContext.GetCommandService().CanUndo(), "执行命令后应可撤销");
const bool undoSucceeded = editorContext.UndoCommand();
MetaCoreExpect(undoSucceeded, "Undo 应成功");
MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount, "Undo 后对象数量应恢复");
MetaCoreExpect(editorContext.GetCommandService().CanRedo(), "Undo 后应可重做");
const bool redoSucceeded = editorContext.RedoCommand();
MetaCoreExpect(redoSucceeded, "Redo 应成功");
MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount + 1, "Redo 后对象数量应再次增加");
}
void MetaCoreTestBuiltinModuleComposition() {
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
auto editorViewsModule = MetaCore::MetaCoreCreateBuiltinEditorViewsModule();
coreServicesModule->Startup(moduleRegistry);
editorViewsModule->Startup(moduleRegistry);
MetaCoreExpect(moduleRegistry.ResolveService<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::MetaCoreIAssetHotReloadService>() != nullptr, "应注册 AssetHotReloadService");
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();
_putenv_s("METACORE_PROJECT_PATH", "");
}
void MetaCoreTestLegacyBinarySceneStartupLoadCompatibility() {
const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreScenePackageSmoke";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Scenes");
MetaCore::MetaCoreSceneDocument sceneDocument;
sceneDocument.Name = "Main";
const MetaCore::MetaCoreScene sourceScene = MetaCore::MetaCoreCreateDefaultScene();
sceneDocument.GameObjects = sourceScene.CaptureSnapshot().GameObjects;
const std::filesystem::path scenePath = tempProjectRoot / "Scenes" / "Main.mcscene";
MetaCoreExpect(MetaCore::MetaCoreWriteScenePackage(scenePath, sceneDocument), "应能写入二进制场景包");
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
MetaCoreExpect(projectFile.is_open(), "应能写入项目文件");
projectFile
<< "{\n"
<< " \"name\": \"SmokeProject\",\n"
<< " \"scenes\": [\n"
<< " \"Scenes/Main.mcscene\"\n"
<< " ],\n"
<< " \"startup_scene\": \"Scenes/Main.mcscene\",\n"
<< " \"version\": \"0.1.0\"\n"
<< "}\n";
}
const auto loadedSceneDocument = MetaCore::MetaCoreLoadStartupSceneDocument(tempProjectRoot / "MetaCore.project.json");
MetaCoreExpect(loadedSceneDocument.has_value(), "应能从项目文件加载 startup scene");
MetaCoreExpect(loadedSceneDocument->GameObjects.size() == sourceScene.GetGameObjects().size(), "startup scene 对象数量应一致");
MetaCoreExpect(!loadedSceneDocument->GameObjects.empty(), "startup scene 应包含对象");
MetaCoreExpect(loadedSceneDocument->GameObjects.front().Name == "Main Camera", "startup scene 首个对象应为 Main Camera");
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().CaptureSnapshot().GameObjects;
MetaCoreExpect(
MetaCore::MetaCoreWriteScenePackage(tempProjectRoot / "Scenes" / "Main.mcscene", sceneDocument),
"应能写入场景包"
);
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
const auto assetDatabase = moduleRegistry.ResolveService<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().CaptureSnapshot().GameObjects;
MetaCoreExpect(
MetaCore::MetaCoreWriteScenePackage(tempProjectRoot / "Scenes" / "Main.mcscene", sceneDocument),
"应能写入场景包"
);
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
const auto assetDatabase = moduleRegistry.ResolveService<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 MetaCoreTestProjectAssetDatabaseOperations() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreProjectAssetDatabaseOperations";
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\": \"ProjectAssetOps\",\n"
<< " \"version\": \"0.1.0\",\n"
<< " \"scenes\": [],\n"
<< " \"startup_scene\": \"\"\n"
<< "}\n";
}
{
std::ofstream sourceFile(tempProjectRoot / "Assets" / "Source.dat", std::ios::trunc);
sourceFile << "asset";
}
_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->Refresh(), "应能刷新资产数据库");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / "Assets" / "Source.dat.mcmeta"), "Refresh 应生成源资产 mcmeta");
const std::filesystem::path movedPath = std::filesystem::path("Assets") / "Moved" / "SourceMoved.dat";
MetaCoreExpect(assetDatabase->MovePathTo(std::filesystem::path("Assets") / "Source.dat", movedPath), "MovePathTo 应能精确移动文件");
MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / "Assets" / "Source.dat"), "移动后旧源文件应消失");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / movedPath), "移动后目标文件应存在");
MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / "Assets" / "Source.dat.mcmeta"), "移动后旧 mcmeta 应消失");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / "Assets" / "Moved" / "SourceMoved.dat.mcmeta"), "移动后新 mcmeta 应存在");
MetaCoreExpect(!assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Source.dat").has_value(), "移动后旧路径资产记录应失效");
MetaCoreExpect(assetDatabase->FindAssetByRelativePath(movedPath).has_value(), "移动后新路径资产记录应存在");
const std::filesystem::path materialPath = std::filesystem::path("Assets") / "Materials" / "PanelMaterial.mcmaterial.json";
MetaCoreExpect(assetDatabase->CreateMaterialAsset(materialPath), "应能创建材质资源");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / materialPath), "材质源文件应存在");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / (materialPath.generic_string() + ".mcmeta")), "材质 mcmeta 应存在");
const auto materialRecord = assetDatabase->FindAssetByRelativePath(materialPath);
MetaCoreExpect(materialRecord.has_value(), "材质应进入资产数据库");
MetaCoreExpect(materialRecord->Guid.IsValid(), "材质 GUID 应有效");
MetaCoreExpect(materialRecord->Type == "material", "材质资产类型应正确");
const auto assetEditingService = moduleRegistry.ResolveService<MetaCore::MetaCoreIAssetEditingService>();
MetaCoreExpect(assetEditingService != nullptr, "应解析到 AssetEditingService");
const std::filesystem::path materialMetaPath = tempProjectRoot / (materialPath.generic_string() + ".mcmeta");
nlohmann::json oldMaterialMeta;
{
std::ifstream input(materialMetaPath);
input >> oldMaterialMeta;
}
const std::uint64_t oldMaterialHash = oldMaterialMeta.value("source_hash", 0ULL);
auto editableMaterial = assetEditingService->LoadMaterialAsset(materialRecord->Guid);
MetaCoreExpect(editableMaterial.has_value(), "AssetEditingService 应能加载独立材质");
editableMaterial->Name = "PanelMaterialEdited";
editableMaterial->BaseColor = glm::vec3(0.12F, 0.34F, 0.56F);
editableMaterial->Metallic = 0.42F;
editableMaterial->Roughness = 0.68F;
editableMaterial->DoubleSided = true;
editableMaterial->AlphaMode = MetaCore::MetaCoreMaterialAlphaMode::Mask;
editableMaterial->AlphaCutoff = 0.31F;
editableMaterial->EmissiveColor = glm::vec3(0.03F, 0.04F, 0.05F);
MetaCoreExpect(assetEditingService->SaveMaterialAsset(materialRecord->Guid, *editableMaterial), "AssetEditingService 应能保存独立材质");
const auto reloadedMaterial = assetEditingService->LoadMaterialAsset(materialRecord->Guid);
MetaCoreExpect(reloadedMaterial.has_value(), "保存后应能重新加载独立材质");
MetaCoreExpect(reloadedMaterial->Name == "PanelMaterialEdited", "材质名称应 round-trip");
MetaCoreExpectVec3Near(reloadedMaterial->BaseColor, glm::vec3(0.12F, 0.34F, 0.56F), "材质 BaseColor 应 round-trip");
MetaCoreExpect(std::abs(reloadedMaterial->Metallic - 0.42F) < 0.0001F, "材质 Metallic 应 round-trip");
MetaCoreExpect(std::abs(reloadedMaterial->Roughness - 0.68F) < 0.0001F, "材质 Roughness 应 round-trip");
MetaCoreExpect(reloadedMaterial->DoubleSided, "材质 DoubleSided 应 round-trip");
MetaCoreExpect(reloadedMaterial->AlphaMode == MetaCore::MetaCoreMaterialAlphaMode::Mask, "材质 AlphaMode 应 round-trip");
MetaCoreExpect(std::abs(reloadedMaterial->AlphaCutoff - 0.31F) < 0.0001F, "材质 AlphaCutoff 应 round-trip");
nlohmann::json newMaterialMeta;
{
std::ifstream input(materialMetaPath);
input >> newMaterialMeta;
}
const std::uint64_t newMaterialHash = newMaterialMeta.value("source_hash", 0ULL);
MetaCoreExpect(newMaterialHash != 0ULL, "保存材质后 mcmeta source_hash 应有效");
MetaCoreExpect(newMaterialHash != oldMaterialHash, "保存材质后 mcmeta source_hash 应更新");
MetaCoreExpect(newMaterialHash == MetaCore::MetaCoreHashFile(tempProjectRoot / materialPath).value_or(0), "mcmeta source_hash 应匹配材质文件 hash");
const std::filesystem::path scenePath = std::filesystem::path("Scenes") / "PanelScene.mcscene.json";
MetaCoreExpect(assetDatabase->CreateEmptySceneAsset(scenePath, true), "应能创建空场景资源");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / scenePath), "场景源文件应存在");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / (scenePath.generic_string() + ".mcmeta")), "场景 mcmeta 应存在");
MetaCoreExpect(assetDatabase->GetProjectDescriptor().StartupScenePath == scenePath, "创建场景可设置为启动场景");
MetaCoreExpect(
std::find(
assetDatabase->GetProjectDescriptor().ScenePaths.begin(),
assetDatabase->GetProjectDescriptor().ScenePaths.end(),
scenePath
) != assetDatabase->GetProjectDescriptor().ScenePaths.end(),
"创建场景后应注册到项目场景列表"
);
MetaCoreExpect(assetDatabase->DeletePath(scenePath), "应能删除场景资源");
MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / scenePath), "删除后场景源文件应消失");
MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / (scenePath.generic_string() + ".mcmeta")), "删除后场景 mcmeta 应消失");
MetaCoreExpect(assetDatabase->GetProjectDescriptor().StartupScenePath.empty(), "删除启动场景后 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* rotatorDescriptor = componentRegistry->FindDescriptor("Rotator");
const auto* scriptDescriptor = componentRegistry->FindDescriptor("Script");
const auto* meshRendererDescriptor = componentRegistry->FindDescriptor("MeshRenderer");
std::cout << "[CHECK] transformDescriptor: " << transformDescriptor << std::endl;
std::cout << "[CHECK] cameraDescriptor: " << cameraDescriptor << std::endl;
std::cout << "[CHECK] lightDescriptor: " << lightDescriptor << std::endl;
std::cout << "[CHECK] rotatorDescriptor: " << rotatorDescriptor << std::endl;
std::cout << "[CHECK] scriptDescriptor: " << scriptDescriptor << std::endl;
std::cout << "[CHECK] meshRendererDescriptor: " << meshRendererDescriptor << std::endl;
if (transformDescriptor) std::cout << "[CHECK] transform DrawInspector: " << static_cast<bool>(transformDescriptor->DrawInspector) << std::endl;
if (cameraDescriptor) std::cout << "[CHECK] camera DrawInspector: " << static_cast<bool>(cameraDescriptor->DrawInspector) << std::endl;
if (lightDescriptor) std::cout << "[CHECK] light DrawInspector: " << static_cast<bool>(lightDescriptor->DrawInspector) << std::endl;
if (rotatorDescriptor) std::cout << "[CHECK] rotator DrawInspector: " << static_cast<bool>(rotatorDescriptor->DrawInspector) << std::endl;
if (scriptDescriptor) std::cout << "[CHECK] script DrawInspector: " << static_cast<bool>(scriptDescriptor->DrawInspector) << std::endl;
if (meshRendererDescriptor) std::cout << "[CHECK] meshRenderer DrawInspector: " << static_cast<bool>(meshRendererDescriptor->DrawInspector) << std::endl;
MetaCoreExpect(transformDescriptor != nullptr, "Transform descriptor 应存在");
MetaCoreExpect(cameraDescriptor != nullptr, "Camera descriptor 应存在");
MetaCoreExpect(lightDescriptor != nullptr, "Light descriptor 应存在");
MetaCoreExpect(rotatorDescriptor != nullptr, "Rotator descriptor 应存在");
MetaCoreExpect(scriptDescriptor != nullptr, "Script 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>(rotatorDescriptor->DrawInspector), "Rotator descriptor 应提供 drawer");
MetaCoreExpect(static_cast<bool>(scriptDescriptor->DrawInspector), "Script descriptor 应提供 drawer");
MetaCoreExpect(static_cast<bool>(meshRendererDescriptor->DrawInspector), "MeshRenderer descriptor 应提供 drawer");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
}
void MetaCoreTestSceneEditingServiceCreateCamera() {
{
_putenv_s("METACORE_PROJECT_PATH", "");
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 sceneEditingService = moduleRegistry.ResolveService<MetaCore::MetaCoreISceneEditingService>();
MetaCoreExpect(sceneEditingService != nullptr, "应解析到 SceneEditingService");
const auto cameraId = sceneEditingService->CreateCamera(editorContext, std::nullopt);
MetaCoreExpect(cameraId.has_value(), "应能创建相机对象");
MetaCore::MetaCoreGameObject cameraObject = scene.FindGameObject(*cameraId);
MetaCoreExpect(cameraObject, "创建后的相机对象应存在");
MetaCoreExpect(cameraObject.HasComponent<MetaCore::MetaCoreCameraComponent>(), "创建后的对象应带 Camera 组件");
MetaCoreExpect(cameraObject.GetComponent<MetaCore::MetaCoreCameraComponent>().IsPrimary, "空场景创建的首个相机应成为主相机");
MetaCoreExpect(editorContext.GetActiveObjectId() == *cameraId, "创建相机后应选中新相机");
MetaCoreExpect(editorContext.GetCommandService().CanUndo(), "创建相机应进入 Undo 栈");
MetaCoreExpect(editorContext.UndoCommand(), "创建相机应可 Undo");
MetaCoreExpect(!scene.FindGameObject(*cameraId), "Undo 后相机对象应被移除");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
}
{
_putenv_s("METACORE_PROJECT_PATH", "");
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 sceneEditingService = moduleRegistry.ResolveService<MetaCore::MetaCoreISceneEditingService>();
MetaCoreExpect(sceneEditingService != nullptr, "默认场景应解析到 SceneEditingService");
const auto cameraId = sceneEditingService->CreateCamera(editorContext, std::nullopt);
MetaCoreExpect(cameraId.has_value(), "默认场景应能创建第二个相机");
const MetaCore::MetaCoreGameObject cameraObject = scene.FindGameObject(*cameraId);
MetaCoreExpect(cameraObject.HasComponent<MetaCore::MetaCoreCameraComponent>(), "第二个相机对象应带 Camera 组件");
MetaCoreExpect(!cameraObject.GetComponent<MetaCore::MetaCoreCameraComponent>().IsPrimary, "已有主相机时新相机不应抢占主相机");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
}
}
void MetaCoreTestPlayModeStateMachineAndLifecycle() {
_putenv_s("METACORE_PROJECT_PATH", "");
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 playMode = moduleRegistry.ResolveService<MetaCore::MetaCoreIPlayModeService>();
const auto componentSystemRegistry =
moduleRegistry.ResolveService<MetaCore::MetaCoreIPlayModeComponentSystemRegistry>();
MetaCoreExpect(playMode != nullptr, "应解析到 PlayModeService");
MetaCoreExpect(componentSystemRegistry != nullptr, "应解析到 PlayModeComponentSystemRegistry");
auto countingSystem = std::make_shared<MetaCoreCountingPlayModeSystem>();
componentSystemRegistry->RegisterSystem(countingSystem);
MetaCoreExpect(playMode->GetState() == MetaCore::MetaCorePlayModeState::Edit, "播放模式初始应为 Edit");
MetaCoreExpect(playMode->CanEnterPlayMode(), "Edit 状态应允许进入播放");
MetaCoreExpect(playMode->EnterPlayMode(editorContext), "应能进入播放模式");
MetaCoreExpect(playMode->GetState() == MetaCore::MetaCorePlayModeState::Playing, "进入后应为 Playing");
MetaCoreExpect(countingSystem->StartCount == 1, "OnPlayStart 应只调用一次");
playMode->Tick(editorContext, 1.0 / 60.0);
MetaCoreExpect(countingSystem->FixedUpdateCount == 1, "播放 Tick 应执行一次 FixedUpdate");
MetaCoreExpect(countingSystem->UpdateCount == 1, "播放 Tick 应执行一次 Update");
MetaCoreExpect(countingSystem->LateUpdateCount == 1, "播放 Tick 应执行一次 LateUpdate");
MetaCoreExpect(std::abs(countingSystem->LastFixedDeltaSeconds - (1.0 / 60.0)) < 0.0001, "FixedUpdate delta 应为固定步长");
MetaCoreExpect(playMode->Pause(), "应能暂停播放");
MetaCoreExpect(playMode->GetState() == MetaCore::MetaCorePlayModeState::Paused, "暂停后应为 Paused");
playMode->Tick(editorContext, 1.0 / 60.0);
MetaCoreExpect(countingSystem->UpdateCount == 1, "暂停 Tick 不应执行 Update");
MetaCoreExpect(countingSystem->LateUpdateCount == 1, "暂停 Tick 不应执行 LateUpdate");
MetaCoreExpect(playMode->Step(editorContext), "暂停时应能单步");
MetaCoreExpect(countingSystem->FixedUpdateCount == 2, "单步应执行一次 FixedUpdate");
MetaCoreExpect(countingSystem->UpdateCount == 2, "单步应执行一次 Update");
MetaCoreExpect(countingSystem->LateUpdateCount == 2, "单步应执行一次 LateUpdate");
MetaCoreExpect(playMode->Resume(), "应能继续播放");
MetaCoreExpect(playMode->GetState() == MetaCore::MetaCorePlayModeState::Playing, "继续后应回到 Playing");
MetaCoreExpect(playMode->ExitPlayMode(editorContext), "应能退出播放模式");
MetaCoreExpect(playMode->GetState() == MetaCore::MetaCorePlayModeState::Edit, "退出后应回到 Edit");
MetaCoreExpect(countingSystem->StopCount == 1, "OnPlayStop 应只调用一次");
playMode->Tick(editorContext, 0.0);
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
}
void MetaCoreTestPlayModeRotatorRestoresPrePlaySnapshotOnStop() {
_putenv_s("METACORE_PROJECT_PATH", "");
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreGameObject spinner = scene.CreateGameObject("Spinner");
spinner.AddComponent<MetaCore::MetaCoreRotatorComponent>().DegreesPerSecond = 60.0F;
const MetaCore::MetaCoreId spinnerId = spinner.GetId();
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const auto playMode = moduleRegistry.ResolveService<MetaCore::MetaCoreIPlayModeService>();
MetaCoreExpect(playMode != nullptr, "应解析到 PlayModeService");
MetaCoreExpect(editorContext.GetCommandService().GetUndoCount() == 0, "播放前 Undo 栈应为空");
MetaCoreExpect(playMode->EnterPlayMode(editorContext), "应能进入播放模式");
for (int frameIndex = 0; frameIndex < 5; ++frameIndex) {
playMode->Tick(editorContext, 0.1);
}
MetaCore::MetaCoreGameObject updatedSpinner = scene.FindGameObject(spinnerId);
MetaCoreExpectVec3Near(
updatedSpinner.GetComponent<MetaCore::MetaCoreTransformComponent>().RotationEulerDegrees,
glm::vec3(0.0F, 30.0F, 0.0F),
"Rotator 播放 0.5 秒后应旋转 30 度"
);
MetaCoreExpect(playMode->Pause(), "应能暂停播放");
playMode->Tick(editorContext, 0.5);
updatedSpinner = scene.FindGameObject(spinnerId);
MetaCoreExpectVec3Near(
updatedSpinner.GetComponent<MetaCore::MetaCoreTransformComponent>().RotationEulerDegrees,
glm::vec3(0.0F, 30.0F, 0.0F),
"暂停期间 Rotator 不应继续旋转"
);
MetaCoreExpect(playMode->Step(editorContext), "暂停后应能单步");
updatedSpinner = scene.FindGameObject(spinnerId);
MetaCoreExpectVec3Near(
updatedSpinner.GetComponent<MetaCore::MetaCoreTransformComponent>().RotationEulerDegrees,
glm::vec3(0.0F, 31.0F, 0.0F),
"单步应按固定步长推进 Rotator"
);
const bool changedDuringPlay = editorContext.ExecuteSnapshotCommand("播放期手动改动", [&]() {
MetaCore::MetaCoreGameObject object = scene.FindGameObject(spinnerId);
if (!object) {
return false;
}
object.GetComponent<MetaCore::MetaCoreTransformComponent>().Position.x = 2.0F;
scene.IncrementRevision();
return true;
});
MetaCoreExpect(changedDuringPlay, "播放期间应允许 Inspector/编辑命令改变场景");
MetaCoreExpect(editorContext.GetCommandService().GetUndoCount() == 0, "播放期间改动不应单独写入 Undo 栈");
MetaCoreExpect(playMode->ExitPlayMode(editorContext), "应能停止播放");
MetaCoreExpect(playMode->GetState() == MetaCore::MetaCorePlayModeState::Edit, "停止后应回到 Edit");
MetaCoreExpect(editorContext.GetCommandService().GetUndoCount() == 0, "停止后不应把播放期差异写入 Undo 栈");
playMode->Tick(editorContext, 0.0);
updatedSpinner = scene.FindGameObject(spinnerId);
MetaCoreExpectVec3Near(
updatedSpinner.GetComponent<MetaCore::MetaCoreTransformComponent>().RotationEulerDegrees,
glm::vec3(0.0F, 0.0F, 0.0F),
"停止后 Rotator 旋转应恢复播放前"
);
MetaCoreExpectVec3Near(
updatedSpinner.GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(0.0F, 0.0F, 0.0F),
"停止后播放期手动改动应丢弃"
);
MetaCoreExpect(updatedSpinner.HasComponent<MetaCore::MetaCoreRotatorComponent>(), "停止恢复后 Rotator 组件应保留");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
}
void MetaCoreTestNativeScriptRegistryAndDefaults() {
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
const auto scriptRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIScriptRegistry>();
MetaCoreExpect(scriptRegistry != nullptr, "应解析到 ScriptRegistry");
const auto* rotatorDefinition = scriptRegistry->FindScript("MetaCore.Script.Rotator");
const auto* moverDefinition = scriptRegistry->FindScript("MetaCore.Script.Mover");
MetaCoreExpect(rotatorDefinition != nullptr, "应注册内置 RotatorScript");
MetaCoreExpect(moverDefinition != nullptr, "应注册内置 MoverScript");
MetaCoreExpect(scriptRegistry->CreateBehaviour("MetaCore.Script.Rotator") != nullptr, "RotatorScript 应能创建运行时对象");
const nlohmann::json rotatorDefaults = nlohmann::json::parse(scriptRegistry->BuildDefaultFieldsJson("MetaCore.Script.Rotator"));
MetaCoreExpect(rotatorDefaults.contains("Enabled") && rotatorDefaults["Enabled"].get<bool>(), "RotatorScript 默认应启用");
MetaCoreExpect(rotatorDefaults.contains("DegreesPerSecond"), "RotatorScript 默认字段应包含角速度");
const nlohmann::json moverEnsured =
nlohmann::json::parse(scriptRegistry->EnsureFieldsJsonDefaults("MetaCore.Script.Mover", "{}"));
MetaCoreExpect(moverEnsured.contains("Enabled"), "MoverScript 应补齐 Enabled 默认字段");
MetaCoreExpect(moverEnsured.contains("Velocity"), "MoverScript 应补齐 Velocity 默认字段");
MetaCore::MetaCoreScriptDefinition replacement;
replacement.TypeId = "MetaCore.Script.Mover";
replacement.DisplayName = "替换移动脚本";
replacement.Fields = {
MetaCore::MetaCoreScriptFieldDefinition{
"Label",
"标签",
MetaCore::MetaCoreScriptFieldType::String,
false,
0.0F,
glm::vec3(0.0F),
"demo"
}
};
replacement.Factory = []() { return std::make_unique<MetaCoreCountingNativeScript>(std::make_shared<MetaCoreScriptLifecycleCounters>()); };
scriptRegistry->RegisterScript(std::move(replacement));
const auto* replacedDefinition = scriptRegistry->FindScript("MetaCore.Script.Mover");
MetaCoreExpect(replacedDefinition != nullptr && replacedDefinition->DisplayName == "替换移动脚本", "重复 TypeId 注册应替换旧定义");
MetaCoreExpect(scriptRegistry->CreateBehaviour("MetaCore.Script.Mover") != nullptr, "替换后的脚本应能创建运行时对象");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
}
void MetaCoreTestScriptComponentSerializationSnapshotAndJson() {
MetaCore::MetaCoreTypeRegistry registry;
MetaCore::MetaCoreRegisterFoundationGeneratedTypes(registry);
MetaCore::MetaCoreRegisterSceneGeneratedTypes(registry);
MetaCore::MetaCoreScriptComponent component;
component.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{
1001,
"MetaCore.Script.Rotator",
true,
"{\"Enabled\":true,\"DegreesPerSecond\":45.0}"
});
const auto serializedComponent = MetaCore::MetaCoreSerializeToBytes(component, registry);
MetaCoreExpect(serializedComponent.has_value(), "Script 组件应可二进制序列化");
MetaCore::MetaCoreScriptComponent roundTripComponent;
MetaCoreExpect(
MetaCore::MetaCoreDeserializeFromBytes(*serializedComponent, roundTripComponent, registry),
"Script 组件应可二进制反序列化"
);
MetaCoreExpect(roundTripComponent.Instances.size() == 1, "Script 组件 round-trip 后实例数量应保留");
MetaCoreExpect(roundTripComponent.Instances.front().ScriptTypeId == "MetaCore.Script.Rotator", "Script TypeId 应保留");
MetaCoreExpect(roundTripComponent.Instances.front().FieldsJson == component.Instances.front().FieldsJson, "Script FieldsJson 应保留");
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreGameObject object = scene.CreateGameObject("ScriptObject");
object.AddComponent<MetaCore::MetaCoreScriptComponent>(component);
const MetaCore::MetaCoreId objectId = object.GetId();
const MetaCore::MetaCoreSceneSnapshot snapshot = scene.CaptureSnapshot();
MetaCoreExpect(snapshot.GameObjects.size() == 1, "快照应包含脚本对象");
MetaCoreExpect(snapshot.GameObjects.front().Script.has_value(), "快照应保存 Script 组件");
scene.RestoreSnapshot(MetaCore::MetaCoreSceneSnapshot{});
MetaCoreExpect(!scene.FindGameObject(objectId), "恢复空快照后对象应不存在");
scene.RestoreSnapshot(snapshot);
MetaCore::MetaCoreGameObject restoredObject = scene.FindGameObject(objectId);
MetaCoreExpect(restoredObject && restoredObject.HasComponent<MetaCore::MetaCoreScriptComponent>(), "恢复快照后 Script 组件应保留");
MetaCoreExpect(
restoredObject.GetComponent<MetaCore::MetaCoreScriptComponent>().Instances.front().InstanceId == 1001,
"恢复快照后脚本 InstanceId 应保留"
);
const std::filesystem::path tempDirectory =
std::filesystem::temp_directory_path() / "MetaCoreScriptSerialization";
std::filesystem::remove_all(tempDirectory);
std::filesystem::create_directories(tempDirectory);
MetaCore::MetaCoreSceneDocument sceneDocument;
sceneDocument.Name = "ScriptScene";
sceneDocument.GameObjects = scene.CaptureSnapshot().GameObjects;
const std::filesystem::path scenePath = tempDirectory / "ScriptScene.mcscene.json";
MetaCoreExpect(
MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(scenePath, sceneDocument, registry),
"包含 Script 的场景 JSON 应可保存"
);
const auto loadedSceneDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(scenePath, registry);
MetaCoreExpect(loadedSceneDocument.has_value(), "包含 Script 的场景 JSON 应可读取");
MetaCoreExpect(!loadedSceneDocument->GameObjects.empty(), "读取后场景对象应保留");
MetaCoreExpect(loadedSceneDocument->GameObjects.front().Script.has_value(), "读取后 Script 组件应保留");
MetaCoreExpect(
loadedSceneDocument->GameObjects.front().Script->Instances.front().FieldsJson == component.Instances.front().FieldsJson,
"读取后 Script FieldsJson 应保留"
);
const std::filesystem::path oldScenePath = tempDirectory / "OldSceneNoScript.mcscene.json";
{
std::ofstream oldSceneFile(oldScenePath, std::ios::trunc);
oldSceneFile << "{\n"
<< " \"SceneName\": \"OldScene\",\n"
<< " \"GameObjects\": [\n"
<< " {\"Id\": 42, \"ParentId\": 0, \"Name\": \"OldObject\", \"Transform\": {"
<< "\"Position\": [0, 0, 0], \"RotationEulerDegrees\": [0, 0, 0], \"Scale\": [1, 1, 1]}}\n"
<< " ]\n"
<< "}\n";
}
const auto oldSceneDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(oldScenePath, registry);
MetaCoreExpect(oldSceneDocument.has_value(), "旧场景缺少 Script 字段时仍应可读取");
MetaCoreExpect(!oldSceneDocument->GameObjects.front().Script.has_value(), "旧场景缺少 Script 字段时不应生成脚本组件");
std::filesystem::remove_all(tempDirectory);
}
void MetaCoreTestNativeScriptLifecycle() {
_putenv_s("METACORE_PROJECT_PATH", "");
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreGameObject object = scene.CreateGameObject("ScriptLifecycleObject");
auto& scriptComponent = object.AddComponent<MetaCore::MetaCoreScriptComponent>();
scriptComponent.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{
2001,
"MetaCore.Tests.CountingScript",
true,
"{}"
});
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
const auto counters = std::make_shared<MetaCoreScriptLifecycleCounters>();
const auto scriptRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIScriptRegistry>();
MetaCoreExpect(scriptRegistry != nullptr, "应解析到 ScriptRegistry");
MetaCore::MetaCoreScriptDefinition countingDefinition;
countingDefinition.TypeId = "MetaCore.Tests.CountingScript";
countingDefinition.DisplayName = "计数脚本";
countingDefinition.Factory = [counters]() { return std::make_unique<MetaCoreCountingNativeScript>(counters); };
scriptRegistry->RegisterScript(std::move(countingDefinition));
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const auto playMode = moduleRegistry.ResolveService<MetaCore::MetaCoreIPlayModeService>();
MetaCoreExpect(playMode != nullptr, "应解析到 PlayModeService");
MetaCoreExpect(playMode->EnterPlayMode(editorContext), "脚本生命周期测试应能进入播放模式");
MetaCoreExpect(counters->StartCount == 1, "脚本 OnPlayStart 应只调用一次");
playMode->Tick(editorContext, 1.0 / 60.0);
MetaCoreExpect(counters->FixedUpdateCount == 1, "脚本播放 Tick 应执行一次 FixedUpdate");
MetaCoreExpect(counters->UpdateCount == 1, "脚本播放 Tick 应执行一次 Update");
MetaCoreExpect(counters->LateUpdateCount == 1, "脚本播放 Tick 应执行一次 LateUpdate");
MetaCoreExpect(playMode->Pause(), "脚本生命周期测试应能暂停");
playMode->Tick(editorContext, 1.0 / 60.0);
MetaCoreExpect(counters->FixedUpdateCount == 1, "暂停时脚本 FixedUpdate 不应继续调用");
MetaCoreExpect(counters->UpdateCount == 1, "暂停时脚本 Update 不应继续调用");
MetaCoreExpect(counters->LateUpdateCount == 1, "暂停时脚本 LateUpdate 不应继续调用");
MetaCoreExpect(playMode->Step(editorContext), "暂停时脚本应能单步");
MetaCoreExpect(counters->FixedUpdateCount == 2, "脚本单步应执行一次 FixedUpdate");
MetaCoreExpect(counters->UpdateCount == 2, "脚本单步应执行一次 Update");
MetaCoreExpect(counters->LateUpdateCount == 2, "脚本单步应执行一次 LateUpdate");
MetaCoreExpect(playMode->ExitPlayMode(editorContext), "脚本生命周期测试应能退出播放模式");
MetaCoreExpect(counters->StopCount == 1, "脚本 OnPlayStop 应调用一次并清理运行时实例");
playMode->Tick(editorContext, 0.0);
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
}
void MetaCoreTestNativeScriptBehavioursRestorePrePlaySnapshotOnStop() {
_putenv_s("METACORE_PROJECT_PATH", "");
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreGameObject object = scene.CreateGameObject("ScriptActor");
auto& scriptComponent = object.AddComponent<MetaCore::MetaCoreScriptComponent>();
scriptComponent.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{
3001,
"MetaCore.Script.Rotator",
true,
"{\"Enabled\":true,\"DegreesPerSecond\":60.0}"
});
scriptComponent.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{
3002,
"MetaCore.Script.Mover",
true,
"{\"Enabled\":true,\"Velocity\":[2.0,0.0,0.0]}"
});
const MetaCore::MetaCoreId objectId = object.GetId();
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const auto playMode = moduleRegistry.ResolveService<MetaCore::MetaCoreIPlayModeService>();
MetaCoreExpect(playMode != nullptr, "应解析到 PlayModeService");
MetaCoreExpect(editorContext.GetCommandService().GetUndoCount() == 0, "脚本播放前 Undo 栈应为空");
MetaCoreExpect(playMode->EnterPlayMode(editorContext), "脚本行为测试应能进入播放模式");
for (int frameIndex = 0; frameIndex < 5; ++frameIndex) {
playMode->Tick(editorContext, 0.1);
}
MetaCore::MetaCoreGameObject updatedObject = scene.FindGameObject(objectId);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().RotationEulerDegrees,
glm::vec3(0.0F, 30.0F, 0.0F),
"RotatorScript 播放 0.5 秒后应旋转 30 度"
);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(1.0F, 0.0F, 0.0F),
"MoverScript 播放 0.5 秒后应移动 1 个单位"
);
MetaCoreExpect(playMode->Pause(), "脚本行为测试应能暂停");
playMode->Tick(editorContext, 0.5);
updatedObject = scene.FindGameObject(objectId);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().RotationEulerDegrees,
glm::vec3(0.0F, 30.0F, 0.0F),
"暂停期间 RotatorScript 不应继续旋转"
);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(1.0F, 0.0F, 0.0F),
"暂停期间 MoverScript 不应继续移动"
);
MetaCoreExpect(playMode->Step(editorContext), "暂停后脚本行为应能单步");
updatedObject = scene.FindGameObject(objectId);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().RotationEulerDegrees,
glm::vec3(0.0F, 31.0F, 0.0F),
"RotatorScript 单步应按固定步长旋转"
);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(1.0F + (2.0F / 60.0F), 0.0F, 0.0F),
"MoverScript 单步应按固定步长移动"
);
MetaCoreExpect(playMode->ExitPlayMode(editorContext), "脚本行为测试应能退出播放模式");
MetaCoreExpect(editorContext.GetCommandService().GetUndoCount() == 0, "停止脚本播放后不应写入 Undo 步");
playMode->Tick(editorContext, 0.0);
updatedObject = scene.FindGameObject(objectId);
MetaCoreExpect(updatedObject.HasComponent<MetaCore::MetaCoreScriptComponent>(), "停止恢复后 Script 组件应保留");
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().RotationEulerDegrees,
glm::vec3(0.0F, 0.0F, 0.0F),
"停止后 RotatorScript 旋转应恢复播放前"
);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(0.0F, 0.0F, 0.0F),
"停止后 MoverScript 位移应恢复播放前"
);
MetaCoreExpect(updatedObject.GetComponent<MetaCore::MetaCoreScriptComponent>().Instances.size() == 2, "停止恢复后脚本实例列表应保留");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
}
void MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot() {
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreGameObject root = scene.CreateGameObject("RenderRoot");
root.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(1.0F, 2.0F, 3.0F);
auto& rootMesh = root.AddComponent<MetaCore::MetaCoreMeshRendererComponent>();
rootMesh.MeshSource = MetaCore::MetaCoreMeshSourceKind::Asset;
rootMesh.SourceModelPath = "Assets/Models/SyncRoot.glb";
rootMesh.BaseColor = glm::vec3(0.5F, 0.6F, 0.7F);
rootMesh.Metallic = 0.35F;
rootMesh.Roughness = 0.45F;
rootMesh.EmissiveColor = glm::vec3(0.05F, 0.06F, 0.07F);
rootMesh.AlphaMode = MetaCore::MetaCoreMeshAlphaMode::Mask;
rootMesh.AlphaCutoff = 0.42F;
rootMesh.DoubleSided = false;
rootMesh.BaseColorTexturePath = "Assets/Textures/SyncBaseColor.png";
rootMesh.MetallicRoughnessTexturePath = "Assets/Textures/SyncMetallicRoughness.png";
rootMesh.NormalTexturePath = "Assets/Textures/SyncNormal.png";
rootMesh.EmissiveTexturePath = "Assets/Textures/SyncEmissive.png";
rootMesh.AoTexturePath = "Assets/Textures/SyncAo.png";
MetaCore::MetaCoreGameObject child = scene.CreateGameObject("RenderChild", root.GetId());
child.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(4.0F, 5.0F, 6.0F);
auto& childMesh = child.AddComponent<MetaCore::MetaCoreMeshRendererComponent>();
childMesh.MeshSource = MetaCore::MetaCoreMeshSourceKind::Builtin;
childMesh.Visible = false;
childMesh.BaseColor = glm::vec3(0.1F, 0.2F, 0.3F);
MetaCore::MetaCoreGameObject cameraObject = scene.CreateGameObject("PrimaryCamera");
auto& camera = cameraObject.AddComponent<MetaCore::MetaCoreCameraComponent>();
camera.IsPrimary = true;
camera.FieldOfViewDegrees = 75.0F;
camera.NearClip = 0.25F;
camera.FarClip = 250.0F;
cameraObject.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(0.0F, 1.0F, 9.0F);
MetaCore::MetaCoreGameObject lightObject = scene.CreateGameObject("KeyLight");
auto& light = lightObject.AddComponent<MetaCore::MetaCoreLightComponent>();
light.Color = glm::vec3(0.25F, 0.5F, 1.0F);
light.Intensity = 3.0F;
const MetaCore::MetaCoreSceneRenderSync renderSync;
const MetaCore::MetaCoreSceneRenderSyncSnapshot snapshot = renderSync.BuildSnapshot(scene);
MetaCoreExpect(snapshot.Renderables.size() == 2, "RenderSync 应收集 MeshRenderer 对象");
MetaCoreExpect(snapshot.Cameras.size() == 1, "RenderSync 应收集 Camera 对象");
MetaCoreExpect(snapshot.Lights.size() == 1, "RenderSync 应收集 Light 对象");
for (const auto& renderable : snapshot.Renderables) {
if (renderable.ObjectId == root.GetId()) {
MetaCoreExpect(renderable.Visible == true, "RenderSync 根对象 Visible 应为 true");
MetaCoreExpectVec3Near(renderable.BaseColor, glm::vec3(0.5F, 0.6F, 0.7F), "RenderSync 根对象 BaseColor 应正确");
MetaCoreExpect(std::abs(renderable.Metallic - 0.35F) < 0.0001F, "RenderSync 根对象 Metallic 应正确");
MetaCoreExpect(std::abs(renderable.Roughness - 0.45F) < 0.0001F, "RenderSync 根对象 Roughness 应正确");
MetaCoreExpectVec3Near(renderable.EmissiveColor, glm::vec3(0.05F, 0.06F, 0.07F), "RenderSync 根对象 EmissiveColor 应正确");
MetaCoreExpect(renderable.AlphaMode == MetaCore::MetaCoreMeshAlphaMode::Mask, "RenderSync 根对象 AlphaMode 应正确");
MetaCoreExpect(std::abs(renderable.AlphaCutoff - 0.42F) < 0.0001F, "RenderSync 根对象 AlphaCutoff 应正确");
MetaCoreExpect(renderable.DoubleSided == false, "RenderSync 根对象 DoubleSided 应正确");
MetaCoreExpect(renderable.BaseColorTexturePath == "Assets/Textures/SyncBaseColor.png", "RenderSync 根对象 BaseColor 贴图路径应正确");
MetaCoreExpect(renderable.MetallicRoughnessTexturePath == "Assets/Textures/SyncMetallicRoughness.png", "RenderSync 根对象金属粗糙贴图路径应正确");
MetaCoreExpect(renderable.NormalTexturePath == "Assets/Textures/SyncNormal.png", "RenderSync 根对象法线贴图路径应正确");
MetaCoreExpect(renderable.EmissiveTexturePath == "Assets/Textures/SyncEmissive.png", "RenderSync 根对象自发光贴图路径应正确");
MetaCoreExpect(renderable.AoTexturePath == "Assets/Textures/SyncAo.png", "RenderSync 根对象 AO 贴图路径应正确");
} else if (renderable.ObjectId == child.GetId()) {
MetaCoreExpect(renderable.Visible == false, "RenderSync 子对象 Visible 应为 false");
MetaCoreExpectVec3Near(renderable.BaseColor, glm::vec3(0.1F, 0.2F, 0.3F), "RenderSync 子对象 BaseColor 应正确");
}
}
MetaCoreExpect(snapshot.PrimaryCamera.has_value(), "RenderSync 应识别主相机");
MetaCoreExpect(snapshot.PrimaryCamera->ObjectId == cameraObject.GetId(), "RenderSync 主相机 ID 应正确");
const glm::mat4 expectedRootWorld =
MetaCore::MetaCoreBuildTransformMatrix(root.GetComponent<MetaCore::MetaCoreTransformComponent>());
const glm::mat4 expectedChildWorld =
expectedRootWorld * MetaCore::MetaCoreBuildTransformMatrix(child.GetComponent<MetaCore::MetaCoreTransformComponent>());
MetaCoreExpectMat4Near(snapshot.WorldMatrices.at(root.GetId()), expectedRootWorld, "RenderSync 根对象 world matrix 应正确");
MetaCoreExpectMat4Near(snapshot.WorldMatrices.at(child.GetId()), expectedChildWorld, "RenderSync 子对象 world matrix 应合成父级");
MetaCore::MetaCoreSceneView primarySceneView;
MetaCoreExpect(
MetaCore::MetaCoreSceneRenderSync::TryBuildSceneViewFromPrimaryCamera(snapshot, primarySceneView),
"RenderSync 应能从主相机构建 SceneView"
);
MetaCoreExpectVec3Near(primarySceneView.CameraPosition, glm::vec3(0.0F, 1.0F, 9.0F), "主相机 SceneView 位置应来自 Transform");
MetaCoreExpect(std::abs(primarySceneView.VerticalFieldOfViewDegrees - 75.0F) <= 0.0001F, "主相机 FOV 应同步");
MetaCoreExpect(std::abs(primarySceneView.NearClip - 0.25F) <= 0.0001F, "主相机 NearClip 应同步");
MetaCoreExpect(std::abs(primarySceneView.FarClip - 250.0F) <= 0.0001F, "主相机 FarClip 应同步");
}
void MetaCoreTestCameraComponentJsonRoundTripAndLegacyDefaults() {
MetaCore::MetaCoreTypeRegistry registry;
MetaCore::MetaCoreRegisterFoundationGeneratedTypes(registry);
MetaCore::MetaCoreRegisterSceneGeneratedTypes(registry);
const std::filesystem::path tempRoot = std::filesystem::temp_directory_path() / "MetaCoreCameraJsonRoundTrip";
std::filesystem::remove_all(tempRoot);
std::filesystem::create_directories(tempRoot);
const std::filesystem::path scenePath = tempRoot / "camera.mcscene";
MetaCore::MetaCoreSceneDocument sceneDocument;
sceneDocument.Name = "CameraFields";
MetaCore::MetaCoreGameObjectData cameraObject;
cameraObject.Id = 1001;
cameraObject.Name = "Camera";
cameraObject.Camera = MetaCore::MetaCoreCameraComponent{};
cameraObject.Camera->FieldOfViewDegrees = 72.0F;
cameraObject.Camera->NearClip = 0.2F;
cameraObject.Camera->FarClip = 600.0F;
cameraObject.Camera->IsPrimary = true;
cameraObject.Camera->ProjectionMode = MetaCore::MetaCoreCameraProjectionMode::Orthographic;
cameraObject.Camera->OrthographicSize = 8.0F;
cameraObject.Camera->Depth = -2.0F;
cameraObject.Camera->CullingMask = 0x0000000FU;
cameraObject.Camera->ClearFlags = MetaCore::MetaCoreCameraClearFlags::SolidColor;
cameraObject.Camera->BackgroundColor = glm::vec3(0.2F, 0.3F, 0.4F);
cameraObject.Camera->BackgroundAlpha = 0.75F;
sceneDocument.GameObjects.push_back(cameraObject);
MetaCoreExpect(
MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(scenePath, sceneDocument, registry),
"Camera JSON round-trip 应能保存场景"
);
const auto loadedSceneDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(scenePath, registry);
MetaCoreExpect(loadedSceneDocument.has_value(), "Camera JSON round-trip 应能读取场景");
MetaCoreExpect(loadedSceneDocument->GameObjects.size() == 1, "Camera JSON round-trip 应保留对象");
const auto& loadedCamera = *loadedSceneDocument->GameObjects.front().Camera;
MetaCoreExpect(loadedCamera.ProjectionMode == MetaCore::MetaCoreCameraProjectionMode::Orthographic, "Camera ProjectionMode 应保留");
MetaCoreExpect(std::abs(loadedCamera.OrthographicSize - 8.0F) <= 0.0001F, "Camera OrthographicSize 应保留");
MetaCoreExpect(std::abs(loadedCamera.Depth - -2.0F) <= 0.0001F, "Camera Depth 应保留");
MetaCoreExpect(loadedCamera.CullingMask == 0x0000000FU, "Camera CullingMask 应保留");
MetaCoreExpect(loadedCamera.ClearFlags == MetaCore::MetaCoreCameraClearFlags::SolidColor, "Camera ClearFlags 应保留");
MetaCoreExpectVec3Near(loadedCamera.BackgroundColor, glm::vec3(0.2F, 0.3F, 0.4F), "Camera BackgroundColor 应保留");
MetaCoreExpect(std::abs(loadedCamera.BackgroundAlpha - 0.75F) <= 0.0001F, "Camera BackgroundAlpha 应保留");
const std::filesystem::path legacyScenePath = tempRoot / "legacy_camera.mcscene";
{
std::ofstream legacyScene(legacyScenePath, std::ios::trunc);
legacyScene
<< "{\n"
<< " \"SceneName\": \"LegacyCamera\",\n"
<< " \"GameObjects\": [{\n"
<< " \"Id\": 42,\n"
<< " \"ParentId\": 0,\n"
<< " \"Name\": \"Legacy Camera\",\n"
<< " \"Transform\": {\"Position\": [0, 0, 0], \"RotationEulerDegrees\": [0, 0, 0], \"Scale\": [1, 1, 1]},\n"
<< " \"Camera\": {\"FieldOfViewDegrees\": 61.0, \"NearClip\": 0.3, \"FarClip\": 333.0, \"IsPrimary\": true, \"Enabled\": true}\n"
<< " }]\n"
<< "}\n";
}
const auto legacyDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(legacyScenePath, registry);
MetaCoreExpect(legacyDocument.has_value(), "旧 Camera JSON 缺字段应能读取");
const auto& legacyCamera = *legacyDocument->GameObjects.front().Camera;
MetaCoreExpect(legacyCamera.ProjectionMode == MetaCore::MetaCoreCameraProjectionMode::Perspective, "旧 Camera ProjectionMode 应使用默认值");
MetaCoreExpect(std::abs(legacyCamera.OrthographicSize - 5.0F) <= 0.0001F, "旧 Camera OrthographicSize 应使用默认值");
MetaCoreExpect(legacyCamera.ClearFlags == MetaCore::MetaCoreCameraClearFlags::Skybox, "旧 Camera ClearFlags 应使用默认值");
std::filesystem::remove_all(tempRoot);
}
void MetaCoreTestCameraSelectionAndProjectionUtilities() {
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreGameObject disabledPrimary = scene.CreateGameObject("DisabledPrimary");
auto& disabledPrimaryCamera = disabledPrimary.AddComponent<MetaCore::MetaCoreCameraComponent>();
disabledPrimaryCamera.IsPrimary = true;
disabledPrimaryCamera.Enabled = false;
disabledPrimaryCamera.Depth = -100.0F;
MetaCore::MetaCoreGameObject depthCamera = scene.CreateGameObject("DepthCamera");
auto& depthCameraComponent = depthCamera.AddComponent<MetaCore::MetaCoreCameraComponent>();
depthCameraComponent.Depth = -5.0F;
depthCameraComponent.ProjectionMode = MetaCore::MetaCoreCameraProjectionMode::Orthographic;
depthCameraComponent.OrthographicSize = 4.0F;
depthCameraComponent.ClearFlags = MetaCore::MetaCoreCameraClearFlags::SolidColor;
depthCameraComponent.BackgroundColor = glm::vec3(0.4F, 0.5F, 0.6F);
MetaCore::MetaCoreGameObject fallbackCamera = scene.CreateGameObject("FallbackCamera");
auto& fallbackCameraComponent = fallbackCamera.AddComponent<MetaCore::MetaCoreCameraComponent>();
fallbackCameraComponent.Depth = 3.0F;
MetaCore::MetaCoreSceneRenderSync renderSync;
MetaCore::MetaCoreSceneRenderSyncSnapshot snapshot = renderSync.BuildSnapshot(scene);
MetaCoreExpect(snapshot.PrimaryCamera.has_value(), "无启用主相机时应按 Depth 自动选择相机");
MetaCoreExpect(snapshot.PrimaryCamera->ObjectId == depthCamera.GetId(), "Depth 最小的启用相机应成为 Game 相机");
fallbackCameraComponent.IsPrimary = true;
snapshot = renderSync.BuildSnapshot(scene);
MetaCoreExpect(snapshot.PrimaryCamera.has_value(), "启用主相机应可被选中");
MetaCoreExpect(snapshot.PrimaryCamera->ObjectId == fallbackCamera.GetId(), "启用主相机应优先于 Depth");
fallbackCameraComponent.Enabled = false;
depthCameraComponent.Enabled = false;
snapshot = renderSync.BuildSnapshot(scene);
MetaCoreExpect(!snapshot.PrimaryCamera.has_value(), "全部相机禁用时不应选择 Game 相机");
MetaCore::MetaCoreSceneView noCameraView;
MetaCoreExpect(!MetaCore::MetaCoreSceneRenderSync::TryBuildSceneViewFromPrimaryCamera(snapshot, noCameraView), "无 Game 相机时不应构建 SceneView");
MetaCore::MetaCoreSceneView perspectiveView;
perspectiveView.CameraPosition = glm::vec3(0.0F, 0.0F, 0.0F);
perspectiveView.CameraTarget = glm::vec3(0.0F, 1.0F, 0.0F);
perspectiveView.CameraUp = glm::vec3(0.0F, 0.0F, 1.0F);
perspectiveView.VerticalFieldOfViewDegrees = 60.0F;
const MetaCore::MetaCoreViewportRect viewport{0.0F, 0.0F, 100.0F, 100.0F};
const MetaCore::MetaCoreCameraRay centerRay =
MetaCore::MetaCoreScreenPointToRay(perspectiveView, viewport, glm::vec2(50.0F, 50.0F));
MetaCoreExpectVec3Near(centerRay.Origin, glm::vec3(0.0F), "透视中心射线起点应为相机位置");
MetaCoreExpectVec3Near(centerRay.Direction, glm::vec3(0.0F, 1.0F, 0.0F), "透视中心射线方向应等于相机 forward");
MetaCore::MetaCoreSceneView orthographicView = perspectiveView;
orthographicView.ProjectionMode = MetaCore::MetaCoreCameraProjectionMode::Orthographic;
orthographicView.OrthographicSize = 5.0F;
const MetaCore::MetaCoreCameraRay orthoRightRay =
MetaCore::MetaCoreScreenPointToRay(orthographicView, viewport, glm::vec2(100.0F, 50.0F));
MetaCoreExpectVec3Near(orthoRightRay.Origin, glm::vec3(5.0F, 0.0F, 0.0F), "正交右侧射线起点应按视平面偏移");
MetaCoreExpectVec3Near(orthoRightRay.Direction, glm::vec3(0.0F, 1.0F, 0.0F), "正交射线方向应等于相机 forward");
const auto projectedPoint = MetaCore::MetaCoreWorldToScreenPoint(
perspectiveView,
viewport,
glm::vec3(0.0F, 5.0F, 0.0F)
);
MetaCoreExpect(projectedPoint.has_value(), "WorldToScreen 应能投影相机前方点");
MetaCoreExpect(std::abs(projectedPoint->x - 50.0F) <= 0.001F, "WorldToScreen X 应位于中心");
MetaCoreExpect(std::abs(projectedPoint->y - 50.0F) <= 0.001F, "WorldToScreen Y 应位于中心");
}
void MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreSceneRoundTripProject";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets");
std::filesystem::create_directories(tempProjectRoot / "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 lightId = 0;
for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) {
if (object.GetName() == "Directional Light") {
lightId = object.GetId();
break;
}
}
MetaCoreExpect(lightId != 0, "默认场景应包含 Directional Light");
editorContext.SelectOnly(lightId);
const std::filesystem::path scenePath = std::filesystem::path("Scenes") / "Main.mcscene.json";
MetaCoreExpect(scenePersistenceService->SaveSceneAs(editorContext, scenePath), "应能保存 JSON 场景");
MetaCoreExpect(scenePersistenceService->GetCurrentScenePath() == scenePath, "当前场景路径应使用 .mcscene.json");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / scenePath), "应写出 .mcscene.json 场景文件");
MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / "Scenes" / "Main.mcscene"), "JSON 主路线不应写出新的 .mcscene 文件");
MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "保存后场景不应为 dirty");
const bool renamed = editorContext.ExecuteSnapshotCommand("重命名对象", [&]() {
return scene.RenameGameObject(lightId, "RoundTripLight");
});
MetaCoreExpect(renamed, "应能修改场景对象");
MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "修改后场景应变为 dirty");
MetaCoreExpect(scenePersistenceService->SaveCurrentScene(editorContext), "应能再次保存当前场景");
MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "再次保存后 dirty 应清除");
scene.RestoreSnapshot(MetaCore::MetaCoreSceneSnapshot{});
editorContext.ClearSelection();
MetaCoreExpect(scene.GetGameObjects().empty(), "清空快照后场景应为空");
MetaCoreExpect(scenePersistenceService->LoadScene(editorContext, scenePath), "应能重新加载 JSON 场景");
MetaCoreExpect(scene.GetGameObjects().size() == 2, "重新加载后对象数量应恢复");
MetaCoreExpect(editorContext.GetSelectedObjectId() != 0, "重新加载后应恢复选中对象");
bool foundRenamedLight = false;
for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) {
if (object.GetName() == "RoundTripLight") {
foundRenamedLight = true;
break;
}
}
MetaCoreExpect(foundRenamedLight, "重新加载后应保留保存过的对象名称");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestImportPipelineAndCook() {
const unsigned char ppmDataV1[] = {
0x50, 0x36, 0x0A, 0x31, 0x20, 0x31, 0x0A, 0x32,
0x35, 0x35, 0x0A, 0xFF, 0x00, 0x00
};
const unsigned char ppmDataV2[] = {
0x50, 0x36, 0x0A, 0x31, 0x20, 0x31, 0x0A, 0x32,
0x35, 0x35, 0x0A, 0x00, 0xFF, 0x00
};
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreImportCookProject";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets");
std::filesystem::create_directories(tempProjectRoot / "Assets" / "Textures");
std::filesystem::create_directories(tempProjectRoot / "Scenes");
std::filesystem::create_directories(tempProjectRoot / "Library");
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
projectFile << "{\n"
<< " \"name\": \"ImportProject\",\n"
<< " \"version\": \"0.1.0\",\n"
<< " \"scenes\": [],\n"
<< " \"startup_scene\": \"\"\n"
<< "}\n";
}
{
std::ofstream rawAsset(tempProjectRoot / "Assets" / "Texture.ppm", std::ios::binary | std::ios::trunc);
rawAsset.write(reinterpret_cast<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(tempProjectRoot / "Assets" / "Texture.ppm.mcmeta.mcmeta"), "ImportPipeline 不应把 mcmeta 当源资产再次导入");
// 【敏捷开发决策】:开发期不物理生成体积大的 .mcasset在此放宽物理存在校验
// MetaCoreExpect(std::filesystem::exists(packagePath), "导入后应生成 mcasset");
const auto sourceRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Texture.ppm");
MetaCoreExpect(sourceRecord.has_value(), "应能找到原始资源记录");
MetaCoreExpect(!assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Texture.ppm.mcmeta").has_value(), ".mcmeta 不应注册为源资产记录");
MetaCoreExpect(sourceRecord->Guid.IsValid(), "原始资源应有稳定 GUID");
MetaCoreExpect(sourceRecord->PackagePath == std::filesystem::path("Assets") / "Texture.ppm.mcasset", "原始资源应关联包路径");
// 【敏捷开发决策】:放宽库中 cooked 二进制文件物理存在的断言校验
// const std::filesystem::path cookedPath = tempProjectRoot / cookService->GetCookedPathForAsset(sourceRecord->Guid);
// MetaCoreExpect(std::filesystem::exists(cookedPath), "导入后应生成 cooked 结果");
const MetaCore::MetaCoreAssetGuid initialGuid = sourceRecord->Guid;
{
std::ofstream rawAsset(tempProjectRoot / "Assets" / "Texture.ppm", std::ios::binary | std::ios::trunc);
rawAsset.write(reinterpret_cast<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 / "Assets" / "Textures");
std::filesystem::create_directories(tempProjectRoot / "Scenes");
std::filesystem::create_directories(tempProjectRoot / "ConfigRuntime");
std::filesystem::create_directories(tempProjectRoot / "UiDocuments");
std::filesystem::create_directories(tempProjectRoot / "BuildOutput");
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
projectFile << "{\n"
<< " \"name\": \"ImporterProject\",\n"
<< " \"version\": \"0.2.0\",\n"
<< " \"runtime_directory\": \"ConfigRuntime\",\n"
<< " \"ui_directory\": \"UiDocuments\",\n"
<< " \"build_directory\": \"BuildOutput\",\n"
<< " \"scenes\": [],\n"
<< " \"startup_scene\": \"\"\n"
<< "}\n";
}
{
const unsigned char valveTextureData[] = {
0x50, 0x36, 0x0A, 0x31, 0x20, 0x31, 0x0A, 0x32,
0x35, 0x35, 0x0A, 0xFF, 0x80, 0x40
};
std::ofstream textureFile(tempProjectRoot / "Assets" / "Textures" / "ValveBaseColor.ppm", std::ios::binary | std::ios::trunc);
textureFile.write(reinterpret_cast<const char*>(valveTextureData), sizeof(valveTextureData));
}
{
std::ofstream gltfFile(tempProjectRoot / "Assets" / "Valve.gltf", std::ios::trunc);
gltfFile
<< "{\n"
<< " \"asset\": {\"version\": \"2.0\"},\n"
<< " \"scenes\": [{\"nodes\": [0]}],\n"
<< " \"nodes\": [{\"mesh\": 0, \"name\": \"Valve\"}],\n"
<< " \"meshes\": [{\"name\": \"ValveMesh\", \"primitives\": [{\"material\": 0}]}],\n"
<< " \"materials\": [{\"name\": \"ValveMaterial\", \"pbrMetallicRoughness\": {\"baseColorTexture\": {\"index\": 0}}}],\n"
<< " \"textures\": [{\"source\": 0}],\n"
<< " \"images\": [{\"uri\": \"Textures/ValveBaseColor.ppm\"}]\n"
<< "}\n";
}
{
std::ofstream gltfFile(tempProjectRoot / "Assets" / "EmbeddedTexture.gltf", std::ios::trunc);
gltfFile
<< "{\n"
<< " \"asset\": {\"version\": \"2.0\"},\n"
<< " \"scenes\": [{\"nodes\": [0]}],\n"
<< " \"nodes\": [{\"mesh\": 0, \"name\": \"EmbeddedTextureNode\"}],\n"
<< " \"meshes\": [{\"name\": \"EmbeddedTextureMesh\", \"primitives\": [{\"material\": 0}]}],\n"
<< " \"materials\": [{\"name\": \"EmbeddedTextureMaterial\", \"pbrMetallicRoughness\": {\"baseColorTexture\": {\"index\": 0}}}],\n"
<< " \"textures\": [{\"source\": 0}],\n"
<< " \"images\": [{\"bufferView\": 0, \"mimeType\": \"image/x-portable-pixmap\", \"name\": \"EmbeddedBaseColor\"}],\n"
<< " \"bufferViews\": [{\"buffer\": 0, \"byteOffset\": 0, \"byteLength\": 14}],\n"
<< " \"buffers\": [{\"byteLength\": 14, \"uri\": \"data:application/octet-stream;base64,UDYKMSAxCjI1NQr/gEA=\"}]\n"
<< "}\n";
}
{
std::ofstream glbFile(tempProjectRoot / "Assets" / "Tank.glb", std::ios::binary | std::ios::trunc);
glbFile << "glTF_BINARY_PLACEHOLDER";
}
{
std::ofstream objFile(tempProjectRoot / "Assets" / "Crate.obj", std::ios::trunc);
objFile << "o Crate\n";
}
{
std::ofstream fbxFile(tempProjectRoot / "Assets" / "Robot.fbx", std::ios::binary | std::ios::trunc);
fbxFile << "Kaydara FBX Binary";
}
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
const auto assetDatabase = moduleRegistry.ResolveService<MetaCore::MetaCoreIAssetDatabaseService>();
const auto packageService = moduleRegistry.ResolveService<MetaCore::MetaCoreIPackageService>();
const auto reflectionRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIReflectionRegistry>();
const auto assetEditingService = moduleRegistry.ResolveService<MetaCore::MetaCoreIAssetEditingService>();
MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService");
MetaCoreExpect(packageService != nullptr, "应解析到 PackageService");
MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry");
MetaCoreExpect(assetEditingService != nullptr, "应解析到 AssetEditingService");
const auto& project = assetDatabase->GetProjectDescriptor();
MetaCoreExpect(project.RuntimePath.filename() == "ConfigRuntime", "项目应读取 runtime_directory");
MetaCoreExpect(project.UiPath.filename() == "UiDocuments", "项目应读取 ui_directory");
MetaCoreExpect(project.BuildPath.filename() == "BuildOutput", "项目应读取 build_directory");
const auto gltfRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Valve.gltf");
MetaCoreExpect(gltfRecord.has_value(), "应能找到 glTF 资源记录");
MetaCoreExpect(gltfRecord->ImporterId == "GltfModelImporter", "glTF 应使用 GltfModelImporter");
MetaCoreExpect(gltfRecord->Type == "model", "glTF 资源类型应为 model");
// 【敏捷开发决策】:开发期不生成庞大的二进制 mcasset 包,仅做 Record 存在性及元数据 mcmeta 文件的校验
const std::filesystem::path valveMeta = tempProjectRoot / "Assets" / "Valve.gltf.mcmeta";
MetaCoreExpect(std::filesystem::exists(valveMeta), "gltf 应生成 mcmeta 文件");
const auto embeddedTextureRecord =
assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "EmbeddedTexture.gltf");
MetaCoreExpect(embeddedTextureRecord.has_value(), "应能找到内嵌贴图 glTF 资源记录");
const std::filesystem::path embeddedMeta = tempProjectRoot / "Assets" / "EmbeddedTexture.gltf.mcmeta";
MetaCoreExpect(std::filesystem::exists(embeddedMeta), "内嵌贴图 glTF 应生成 mcmeta 文件");
const auto glbRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Tank.glb");
MetaCoreExpect(glbRecord.has_value(), "应能找到 glb 资源记录");
MetaCoreExpect(glbRecord->ImporterId == "GltfModelImporter", "glb 应使用 GltfModelImporter");
const std::filesystem::path glbMeta = tempProjectRoot / "Assets" / "Tank.glb.mcmeta";
MetaCoreExpect(std::filesystem::exists(glbMeta), "glb 应生成 mcmeta 文件");
const auto objRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Crate.obj");
MetaCoreExpect(objRecord.has_value(), "应能找到 obj 资源记录");
MetaCoreExpect(objRecord->ImporterId == "ModelImporter", "obj 应使用通用 ModelImporter");
MetaCoreExpect(objRecord->Type == "model", "obj 资源类型应为 model");
const std::filesystem::path objMeta = tempProjectRoot / "Assets" / "Crate.obj.mcmeta";
MetaCoreExpect(std::filesystem::exists(objMeta), "obj 应生成 mcmeta 文件");
const auto fbxRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Robot.fbx");
MetaCoreExpect(fbxRecord.has_value(), "应能找到 fbx 资源记录");
MetaCoreExpect(fbxRecord->ImporterId == "ModelImporter", "fbx 应使用通用 ModelImporter");
MetaCoreExpect(fbxRecord->Type == "model", "fbx 资源类型应为 model");
const std::filesystem::path fbxMeta = tempProjectRoot / "Assets" / "Robot.fbx.mcmeta";
MetaCoreExpect(std::filesystem::exists(fbxMeta), "fbx 应生成 mcmeta 文件");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestInstantiateImportedModelAssetIntoScene() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreInstantiateModelProject";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets");
std::filesystem::create_directories(tempProjectRoot / "Scenes");
std::filesystem::create_directories(tempProjectRoot / "Library");
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
projectFile << "{\n"
<< " \"name\": \"InstantiateProject\",\n"
<< " \"version\": \"0.1.0\",\n"
<< " \"scenes\": [],\n"
<< " \"startup_scene\": \"\"\n"
<< "}\n";
}
{
std::ofstream gltfFile(tempProjectRoot / "Assets" / "Pump.gltf", std::ios::trunc);
gltfFile
<< "{\n"
<< " \"meshes\": [{\"name\": \"PumpMesh\"}],\n"
<< " \"materials\": [{\"name\": \"PumpMaterial\"}],\n"
<< " \"images\": [{\"uri\": \"Textures/PumpBaseColor.png\"}]\n"
<< "}\n";
}
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const auto assetDatabase = moduleRegistry.ResolveService<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(), "应能将导入模型实例化到场景");
MetaCore::MetaCoreGameObject instantiatedRoot = scene.FindGameObject(*instantiatedRootId);
MetaCoreExpect(instantiatedRoot, "实例化后应能找到根对象");
MetaCoreExpect(instantiatedRoot.GetName() == "Pump.gltf", "model root should use source file name");
const std::vector<MetaCore::MetaCoreId> instantiatedChildren = scene.GetChildrenOf(*instantiatedRootId);
MetaCoreExpect(instantiatedChildren.empty(), "平坦化后单节点模型的根节点下不应再包含子节点");
MetaCoreExpect(instantiatedRoot.HasComponent<MetaCore::MetaCoreMeshRendererComponent>(), "平坦化后根节点应直接包含 MeshRenderer 渲染组件");
MetaCoreExpect(instantiatedRoot.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().MeshSource == MetaCore::MetaCoreMeshSourceKind::Asset, "根节点 Mesh 资源来源应为 Asset");
MetaCoreExpect(instantiatedRoot.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().MeshAssetGuid.IsValid(), "根节点 Mesh 应绑定合法的 Asset Guid");
MetaCoreExpect(!instantiatedRoot.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().MaterialAssetGuids.empty(), "根节点应绑定材质 slot");
MetaCoreExpect(
instantiatedRoot.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().SourceModelPath == (std::filesystem::path("Assets") / "Pump.gltf").generic_string(),
"根节点 MeshRenderer 应保存源模型文件路径"
);
MetaCoreExpect(
instantiatedRoot.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().BaseColorTexturePath == (std::filesystem::path("Textures") / "PumpBaseColor.png").generic_string(),
"根节点 MeshRenderer 应保存基础颜色贴图路径"
);
MetaCoreExpect(instantiatedRoot.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().BaseColor.x == 1.0F, "根节点应继承材质颜色通道数值");
MetaCoreExpect(std::abs(instantiatedRoot.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().Metallic) < 0.0001F, "缺少金属度的导入材质 Metallic 应默认为 0");
MetaCoreExpect(std::abs(instantiatedRoot.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().Roughness) < 0.0001F, "缺少粗糙度的导入材质 Roughness 应默认为 0");
MetaCoreExpect(editorContext.GetActiveObjectId() == *instantiatedRootId, "实例化后应正确选中新对象");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestInstantiateRealGltfHierarchyIntoScene() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreInstantiateRealGltfHierarchyProject";
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\": \"InstantiateRealGltfHierarchyProject\",\n"
<< " \"version\": \"0.1.0\",\n"
<< " \"scenes\": [],\n"
<< " \"startup_scene\": \"\"\n"
<< "}\n";
}
{
std::ofstream gltfFile(tempProjectRoot / "Assets" / "HierarchySample.gltf", std::ios::trunc);
gltfFile
<< "{\n"
<< " \"asset\": {\"version\": \"2.0\"},\n"
<< " \"scene\": 0,\n"
<< " \"scenes\": [{\"nodes\": [0]}],\n"
<< " \"nodes\": [\n"
<< " {\"name\": \"ImportedRoot\", \"children\": [1], \"translation\": [1.0, 2.0, 3.0]},\n"
<< " {\"name\": \"ImportedChild\", \"mesh\": 0, \"translation\": [0.0, 1.0, 0.0]}\n"
<< " ],\n"
<< " \"meshes\": [{\"name\": \"HierarchyMesh\", \"primitives\": [{\"material\": 0}]}],\n"
<< " \"materials\": [{\"name\": \"MissingScalarMaterial\", \"pbrMetallicRoughness\": {\"baseColorFactor\": [0.2, 0.4, 0.6, 1.0]}}]\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");
MetaCoreExpect(assetDatabase->Refresh(), "应能刷新真实 GLTF 层级测试项目");
const auto modelRecord =
assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "HierarchySample.gltf");
MetaCoreExpect(modelRecord.has_value(), "应能找到真实 GLTF 层级模型资源");
const auto instantiatedRootId = sceneEditingService->InstantiateModelAsset(editorContext, modelRecord->Guid, std::nullopt);
MetaCoreExpect(instantiatedRootId.has_value(), "应能实例化真实 GLTF 层级模型");
const MetaCore::MetaCoreGameObject instantiatedRoot = scene.FindGameObject(*instantiatedRootId);
MetaCoreExpect(instantiatedRoot, "真实 GLTF 层级实例化后应能找到资产根对象");
MetaCoreExpect(instantiatedRoot.GetName() == "HierarchySample.gltf", "真实 GLTF 资产根对象应使用源文件名");
const std::vector<MetaCore::MetaCoreId> assetRootChildren = scene.GetChildrenOf(*instantiatedRootId);
MetaCoreExpect(assetRootChildren.size() == 1, "真实 GLTF 资产根对象下应保留导入根节点");
const MetaCore::MetaCoreGameObject importedRoot = scene.FindGameObject(assetRootChildren.front());
MetaCoreExpect(importedRoot, "应能找到真实 GLTF 导入根节点");
MetaCoreExpect(importedRoot.GetName() == "ImportedRoot", "真实 GLTF 导入根节点名称应保持");
MetaCoreExpect(importedRoot.GetParentId() == instantiatedRoot.GetId(), "真实 GLTF 导入根节点应挂在资产根对象下");
const std::vector<MetaCore::MetaCoreId> importedRootChildren = scene.GetChildrenOf(importedRoot.GetId());
MetaCoreExpect(importedRootChildren.size() == 1, "真实 GLTF 导入根节点下应保留子节点");
const MetaCore::MetaCoreGameObject importedChild = scene.FindGameObject(importedRootChildren.front());
MetaCoreExpect(importedChild, "应能找到真实 GLTF 子节点");
MetaCoreExpect(importedChild.GetName() == "ImportedChild", "真实 GLTF 子节点名称应保持");
MetaCoreExpect(importedChild.GetParentId() == importedRoot.GetId(), "真实 GLTF 子节点应挂在导入根节点下");
MetaCoreExpect(importedChild.HasComponent<MetaCore::MetaCoreMeshRendererComponent>(), "真实 GLTF 子节点应挂载 MeshRenderer");
const MetaCore::MetaCoreMeshRendererComponent& childMeshRenderer =
importedChild.GetComponent<MetaCore::MetaCoreMeshRendererComponent>();
MetaCoreExpectVec3Near(childMeshRenderer.BaseColor, glm::vec3(0.2F, 0.4F, 0.6F), "真实 GLTF 子节点应继承导入材质基础色");
MetaCoreExpect(std::abs(childMeshRenderer.Metallic) < 0.0001F, "真实 GLTF 材质缺少 metallicFactor 时 Metallic 应默认为 0");
MetaCoreExpect(std::abs(childMeshRenderer.Roughness) < 0.0001F, "真实 GLTF 材质缺少 roughnessFactor 时 Roughness 应默认为 0");
const std::vector<MetaCore::MetaCoreId> hierarchyOrder = scene.BuildHierarchyPreorder();
MetaCoreExpect(hierarchyOrder.size() == 3, "真实 GLTF 层级应进入层级树遍历结果");
MetaCoreExpect(hierarchyOrder[0] == instantiatedRoot.GetId(), "层级遍历应先返回资产根对象");
MetaCoreExpect(hierarchyOrder[1] == importedRoot.GetId(), "层级遍历应返回导入根节点");
MetaCoreExpect(hierarchyOrder[2] == importedChild.GetId(), "层级遍历应返回导入子节点");
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>();
const auto assetEditingService = moduleRegistry.ResolveService<MetaCore::MetaCoreIAssetEditingService>();
MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService");
MetaCoreExpect(sceneEditingService != nullptr, "应解析到 SceneEditingService");
MetaCoreExpect(packageService != nullptr, "应解析到 PackageService");
MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry");
MetaCoreExpect(assetEditingService != nullptr, "应解析到 AssetEditingService");
const auto modelRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Assembly.gltf");
MetaCoreExpect(modelRecord.has_value(), "应能找到导入后的多节点模型资源");
MetaCore::MetaCoreModelAssetDocument document;
document.AssetType = "model";
document.ImporterId = "GltfModelImporter";
document.SourcePath = std::filesystem::path("Assets") / "Assembly.gltf";
document.SourceHash = 1234;
document.ModelKind = MetaCore::MetaCoreModelAssetKind::Gltf;
document.SourceFormat = "gltf";
MetaCore::MetaCoreImportedGltfNodeDocument rootNode;
rootNode.Name = "AssemblyRoot";
rootNode.ParentIndex = -1;
rootNode.MeshIndex = -1;
document.Nodes.push_back(rootNode);
MetaCore::MetaCoreImportedGltfNodeDocument childNode;
childNode.Name = "Valve_A";
childNode.ParentIndex = 0;
childNode.MeshIndex = 0;
document.Nodes.push_back(childNode);
MetaCore::MetaCoreImportedGltfMeshDocument meshDocument;
meshDocument.Name = "ValveMesh";
meshDocument.PrimitiveCount = 2;
meshDocument.MaterialSlots.push_back(0);
meshDocument.MaterialSlots.push_back(1);
document.Meshes.push_back(meshDocument);
MetaCore::MetaCoreMaterialAssetDocument materialAsset;
materialAsset.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate();
materialAsset.Name = "ValveMaterial";
document.GeneratedMaterialAssets.push_back(materialAsset);
MetaCore::MetaCoreMaterialAssetDocument secondaryMaterialAsset;
secondaryMaterialAsset.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate();
secondaryMaterialAsset.Name = "ValveSecondaryMaterial";
document.GeneratedMaterialAssets.push_back(secondaryMaterialAsset);
MetaCore::MetaCoreMeshAssetDocument meshAsset;
meshAsset.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate();
meshAsset.Name = "ValveMesh";
MetaCore::MetaCoreMeshSubMeshDocument firstSubMesh;
firstSubMesh.Name = "Primitive_0";
firstSubMesh.MaterialSlotIndex = 0;
firstSubMesh.FirstIndex = 0;
firstSubMesh.IndexCount = 3;
meshAsset.SubMeshes.push_back(firstSubMesh);
MetaCore::MetaCoreMeshSubMeshDocument secondSubMesh;
secondSubMesh.Name = "Primitive_1";
secondSubMesh.MaterialSlotIndex = 1;
secondSubMesh.FirstIndex = 3;
secondSubMesh.IndexCount = 3;
meshAsset.SubMeshes.push_back(secondSubMesh);
document.GeneratedMeshAssets.push_back(meshAsset);
const auto payload = MetaCore::MetaCoreSerializeToBytes(document, reflectionRegistry->GetTypeRegistry());
MetaCoreExpect(payload.has_value(), "多节点模型资源应能序列化");
MetaCore::MetaCorePackageDocument package;
package.Header.PackageType = MetaCore::MetaCorePackageType::Asset;
package.Header.PackageGuid = modelRecord->Guid;
package.Header.SourceHash = document.SourceHash;
package.NameTable.emplace_back("MetaCoreModelAssetDocument");
package.Exports.push_back(MetaCore::MetaCoreExportEntry{
modelRecord->Guid,
modelRecord->RelativePath.filename().string(),
MetaCore::MetaCoreMakeTypeId("MetaCoreModelAssetDocument"),
0,
static_cast<std::uint64_t>(payload->size())
});
package.PayloadSections.push_back(*payload);
MetaCoreExpect(
packageService->WritePackage(tempProjectRoot / modelRecord->PackagePath, std::move(package)),
"应能写入多节点模型资源包"
);
MetaCoreExpect(assetDatabase->Refresh(), "写入模型包后应能刷新资产数据库");
{
MetaCore::MetaCoreMaterialAssetDocument generatedMaterial = materialAsset;
const MetaCore::MetaCoreAssetGuid generatedMaterialGuid = generatedMaterial.AssetGuid;
generatedMaterial.Name = "GeneratedMaterialEdited";
generatedMaterial.BaseColor = glm::vec3(0.8F, 0.2F, 0.1F);
generatedMaterial.Metallic = 0.77F;
generatedMaterial.Roughness = 0.22F;
generatedMaterial.DoubleSided = true;
MetaCoreExpect(assetEditingService->SaveMaterialAsset(generatedMaterialGuid, generatedMaterial), "应能保存模型生成材质");
const auto reloadedGeneratedMaterial = assetEditingService->LoadMaterialAsset(generatedMaterialGuid);
MetaCoreExpect(reloadedGeneratedMaterial.has_value(), "保存后应能重新加载模型生成材质");
MetaCoreExpect(reloadedGeneratedMaterial->AssetGuid == generatedMaterialGuid, "保存模型生成材质应保持 GUID 不变");
MetaCoreExpect(reloadedGeneratedMaterial->Name == "GeneratedMaterialEdited", "模型生成材质名称应写回");
MetaCoreExpectVec3Near(reloadedGeneratedMaterial->BaseColor, glm::vec3(0.8F, 0.2F, 0.1F), "模型生成材质 BaseColor 应写回");
MetaCoreExpect(std::abs(reloadedGeneratedMaterial->Metallic - 0.77F) < 0.0001F, "模型生成材质 Metallic 应写回");
MetaCoreExpect(std::abs(reloadedGeneratedMaterial->Roughness - 0.22F) < 0.0001F, "模型生成材质 Roughness 应写回");
const auto reloadedModelAfterMaterialEdit = assetEditingService->LoadModelAsset(modelRecord->Guid);
MetaCoreExpect(reloadedModelAfterMaterialEdit.has_value(), "保存生成材质后应能重新加载源模型");
const auto generatedMaterialIterator = std::find_if(
reloadedModelAfterMaterialEdit->GeneratedMaterialAssets.begin(),
reloadedModelAfterMaterialEdit->GeneratedMaterialAssets.end(),
[&](const MetaCore::MetaCoreMaterialAssetDocument& material) {
return material.AssetGuid == generatedMaterialGuid;
}
);
MetaCoreExpect(generatedMaterialIterator != reloadedModelAfterMaterialEdit->GeneratedMaterialAssets.end(), "源模型中应保留已编辑的生成材质");
MetaCoreExpect(generatedMaterialIterator->Name == "GeneratedMaterialEdited", "源模型生成材质文档应更新");
}
const auto instantiatedRootId = sceneEditingService->InstantiateModelAsset(editorContext, modelRecord->Guid, std::nullopt);
MetaCoreExpect(instantiatedRootId.has_value(), "应能将多节点模型实例化到场景");
MetaCore::MetaCoreGameObject instantiatedRoot = scene.FindGameObject(*instantiatedRootId);
MetaCoreExpect(instantiatedRoot, "多节点实例化后应能找到根对象");
MetaCoreExpect(instantiatedRoot.GetName() == "Assembly.gltf", "multi-node model root should use source file name");
MetaCoreExpect(!instantiatedRoot.HasComponent<MetaCore::MetaCoreMeshRendererComponent>(), "asset root should not force MeshRenderer");
const auto importedRootIterator = std::find_if(scene.GetGameObjects().begin(), scene.GetGameObjects().end(), [&](const MetaCore::MetaCoreGameObject& object) {
return object.GetName() == "AssemblyRoot";
});
MetaCoreExpect(importedRootIterator != scene.GetGameObjects().end(), "multi-node instantiation should create imported root node");
MetaCoreExpect(importedRootIterator->GetParentId() == instantiatedRoot.GetId(), "imported root node should be parented under asset root");
MetaCoreExpect(!importedRootIterator->HasComponent<MetaCore::MetaCoreMeshRendererComponent>(), "imported empty root should not force MeshRenderer");
const auto childIterator = std::find_if(scene.GetGameObjects().begin(), scene.GetGameObjects().end(), [&](const MetaCore::MetaCoreGameObject& object) {
return object.GetName() == "Valve_A";
});
MetaCoreExpect(childIterator != scene.GetGameObjects().end(), "multi-node instantiation should create child node object");
MetaCoreExpect(childIterator->GetParentId() == importedRootIterator->GetId(), "child node should be parented under imported root node");
MetaCoreExpect(childIterator->HasComponent<MetaCore::MetaCoreMeshRendererComponent>(), "mesh child should have MeshRenderer");
MetaCoreExpect(childIterator->GetComponent<MetaCore::MetaCoreMeshRendererComponent>().MeshAssetGuid == meshAsset.AssetGuid, "child node should bind generated mesh asset");
MetaCoreExpect(childIterator->GetComponent<MetaCore::MetaCoreMeshRendererComponent>().MaterialAssetGuids.size() == 2, "child node should bind all material slots");
MetaCoreExpect(childIterator->GetComponent<MetaCore::MetaCoreMeshRendererComponent>().MaterialAssetGuids.front() == materialAsset.AssetGuid, "child node should bind generated material asset");
MetaCoreExpect(childIterator->GetComponent<MetaCore::MetaCoreMeshRendererComponent>().MaterialAssetGuids[1] == secondaryMaterialAsset.AssetGuid, "child node should bind secondary material asset");
MetaCoreExpect(editorContext.GetActiveObjectId() == instantiatedRoot.GetId(), "multi-node instantiation should select asset root");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestDeleteModelAssetCorrectlyClearsFromViewport() {
const std::filesystem::path projectRoot = "d:/MetaCore/SandboxProject";
_putenv_s("METACORE_PROJECT_PATH", projectRoot.string().c_str());
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const auto assetDatabase = moduleRegistry.ResolveService<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/Models/box1.glb"));
MetaCoreExpect(modelRecord.has_value(), "应能找到导入后的 box1.glb 模型资源");
const auto instantiatedRootId = sceneEditingService->InstantiateModelAsset(editorContext, modelRecord->Guid, std::nullopt);
MetaCoreExpect(instantiatedRootId.has_value(), "应能将导入模型实例化到场景");
// 初始化窗口和 FilamentSceneBridge
MetaCore::MetaCoreWindow testWindow;
bool winOk = testWindow.Initialize(800, 600, "DeleteModelAssetTestWindow");
MetaCoreExpect(winOk, "测试窗口初始化应成功");
MetaCore::MetaCoreFilamentSceneBridge bridge;
bridge.SetProjectRootPath(projectRoot);
bool bridgeOk = bridge.Initialize(testWindow);
MetaCoreExpect(bridgeOk, "测试桥接器初始化应成功");
// 同步场景,将模型加载到 FilamentSceneBridge 中
bridge.SyncScene(scene, false, false);
// 验证模型已被桥接器加载和映射
glm::mat4 modelWorldMatrix;
bool hasModel = bridge.TryGetObjectWorldMatrix(*instantiatedRootId, modelWorldMatrix);
MetaCoreExpect(hasModel, "同步场景后,桥接器中应包含已实例化模型的变换映射");
// 在场景中删除该模型 GameObject
const std::vector<MetaCore::MetaCoreId> deletedIds = scene.DeleteGameObjects({*instantiatedRootId});
MetaCoreExpect(!deletedIds.empty(), "删除模型对象应返回被删除的节点ID");
// 再次同步场景,此时应触发卸载
bridge.SyncScene(scene, false, false);
// 验证删除后,桥接器中是否还存在该模型的映射
bool hasModelAfterDelete = bridge.TryGetObjectWorldMatrix(*instantiatedRootId, modelWorldMatrix);
MetaCoreExpect(!hasModelAfterDelete, "删除模型后,桥接器映射中不应再包含该模型");
bridge.Shutdown();
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
}
void MetaCoreTestDeleteModelSubChildCorrectlyClearsFromViewport() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreDeleteModelSubChildProject";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets");
std::filesystem::create_directories(tempProjectRoot / "Scenes");
std::filesystem::create_directories(tempProjectRoot / "Library");
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
projectFile << "{\n"
<< " \"name\": \"DeleteModelSubChildProject\",\n"
<< " \"version\": \"0.1.0\",\n"
<< " \"scenes\": [],\n"
<< " \"startup_scene\": \"\"\n"
<< "}\n";
}
{
std::ofstream gltfFile(tempProjectRoot / "Assets" / "Assembly.gltf", std::ios::trunc);
gltfFile << "{\n"
<< " \"asset\": { \"version\": \"2.0\" },\n"
<< " \"scenes\": [{ \"nodes\": [0] }],\n"
<< " \"nodes\": [\n"
<< " { \"name\": \"AssemblyRoot\", \"children\": [1, 2] },\n"
<< " { \"name\": \"Valve_A\" },\n"
<< " { \"name\": \"Valve_B\" }\n"
<< " ]\n"
<< "}\n";
}
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const auto assetDatabase = moduleRegistry.ResolveService<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::MetaCoreImportedGltfNodeDocument childNodeB;
childNodeB.Name = "Valve_B";
childNodeB.ParentIndex = 0;
childNodeB.MeshIndex = 0;
document.Nodes.push_back(childNodeB);
MetaCore::MetaCoreMeshAssetDocument meshAsset;
meshAsset.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate();
meshAsset.Name = "ValveMesh";
MetaCore::MetaCoreMeshSubMeshDocument firstSubMesh;
firstSubMesh.Name = "Primitive_0";
firstSubMesh.MaterialSlotIndex = 0;
firstSubMesh.FirstIndex = 0;
firstSubMesh.IndexCount = 3;
meshAsset.SubMeshes.push_back(firstSubMesh);
document.GeneratedMeshAssets.push_back(meshAsset);
const auto payload = MetaCore::MetaCoreSerializeToBytes(document, reflectionRegistry->GetTypeRegistry());
MetaCoreExpect(payload.has_value(), "多节点模型资源应能序列化");
MetaCore::MetaCorePackageDocument package;
package.Header.PackageType = MetaCore::MetaCorePackageType::Asset;
package.Header.PackageGuid = modelRecord->Guid;
package.Header.SourceHash = document.SourceHash;
package.NameTable.emplace_back("MetaCoreModelAssetDocument");
package.Exports.push_back(MetaCore::MetaCoreExportEntry{
modelRecord->Guid,
modelRecord->RelativePath.filename().string(),
MetaCore::MetaCoreMakeTypeId("MetaCoreModelAssetDocument"),
0,
static_cast<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(), "应能将多节点模型实例化到场景");
MetaCore::MetaCoreId childId = 0;
MetaCore::MetaCoreId childIdB = 0;
for (const auto& obj : scene.GetGameObjects()) {
if (obj.GetName() == "Valve_A") {
childId = obj.GetId();
} else if (obj.GetName() == "Valve_B") {
childIdB = obj.GetId();
}
}
MetaCoreExpect(childId != 0, "应能找到子节点 Valve_A");
MetaCoreExpect(childIdB != 0, "应能找到子节点 Valve_B");
// 初始化窗口和 FilamentSceneBridge
MetaCore::MetaCoreWindow testWindow;
bool winOk = testWindow.Initialize(800, 600, "DeleteModelSubChildTestWindow");
MetaCoreExpect(winOk, "测试窗口初始化应成功");
MetaCore::MetaCoreFilamentSceneBridge bridge;
bridge.SetProjectRootPath(tempProjectRoot);
bool bridgeOk = bridge.Initialize(testWindow);
MetaCoreExpect(bridgeOk, "测试桥接器初始化应成功");
// 同步场景,将模型加载到 FilamentSceneBridge 中
bridge.SyncScene(scene, false, false);
// 验证子节点对应的实体是否已经在场景中渲染
bool isChildInScene = bridge.VerifyEntityInSceneForTesting(childId);
bool isChildBInScene = bridge.VerifyEntityInSceneForTesting(childIdB);
MetaCoreExpect(isChildInScene, "同步场景后,子节点 Valve_A 实体应在场景中被渲染");
MetaCoreExpect(isChildBInScene, "同步场景后,子节点 Valve_B 实体应在场景中被渲染");
// 在场景中删除子节点 Valve_A GameObject在 Command 中执行,为了后续 Undo
const bool deleted = editorContext.ExecuteSnapshotCommand("删除子节点 Valve_A", [&]() {
scene.DeleteGameObjects({childId});
return true;
});
MetaCoreExpect(deleted, "删除子节点 Valve_A 快照命令应成功");
// 再次同步场景
bridge.SyncScene(scene, false, false);
// 验证删除子节点 Valve_A 后,桥接器中该子节点对应的实体是否已被移出场景渲染
bool isChildInSceneAfterDelete = bridge.VerifyEntityInSceneForTesting(childId);
bool isChildBInSceneAfterDelete = bridge.VerifyEntityInSceneForTesting(childIdB);
MetaCoreExpect(!isChildInSceneAfterDelete, "删除子节点 Valve_A 后,其实体应被移出场景渲染");
MetaCoreExpect(isChildBInSceneAfterDelete, "删除子节点 Valve_A 后Valve_B 实体应依然保留在场景中渲染(没有卸载资产)");
// 撤销删除子节点
const bool undoSucceeded = editorContext.UndoCommand();
MetaCoreExpect(undoSucceeded, "撤销删除应成功");
// 再次同步场景
bridge.SyncScene(scene, false, false);
// 验证撤销后,该子节点实体是否已被重新恢复渲染
bool isChildInSceneAfterUndo = bridge.VerifyEntityInSceneForTesting(childId);
MetaCoreExpect(isChildInSceneAfterUndo, "撤销删除后,子节点 Valve_A 实体应重新被加回场景渲染");
bridge.Shutdown();
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestModelReimportKeepsGeneratedAssetReferencesStable() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreModelReimportStableProject";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets");
std::filesystem::create_directories(tempProjectRoot / "Scenes");
std::filesystem::create_directories(tempProjectRoot / "Library");
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
projectFile << "{\n"
<< " \"name\": \"ModelReimportStableProject\",\n"
<< " \"version\": \"0.1.0\",\n"
<< " \"scenes\": [],\n"
<< " \"startup_scene\": \"\"\n"
<< "}\n";
}
const std::filesystem::path modelPath = tempProjectRoot / "Assets" / "Pump.gltf";
{
std::ofstream gltfFile(modelPath, std::ios::trunc);
gltfFile
<< "{\n"
<< " \"meshes\": [{\"name\": \"PumpMesh\"}],\n"
<< " \"materials\": [{\"name\": \"PumpMaterial\"}],\n"
<< " \"images\": [{\"uri\": \"Textures/PumpBaseColor.png\"}]\n"
<< "}\n";
}
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const auto assetDatabase = moduleRegistry.ResolveService<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(), "应能实例化模型资源");
MetaCore::MetaCoreGameObject instantiatedRoot = scene.FindGameObject(*instantiatedRootId);
MetaCoreExpect(instantiatedRoot, "instantiated model root should exist");
const std::vector<MetaCore::MetaCoreId> instantiatedChildren = scene.GetChildrenOf(*instantiatedRootId);
MetaCoreExpect(instantiatedChildren.empty(), "平坦化后单节点模型的根节点下不包含任何子节点");
MetaCoreExpect(instantiatedRoot && instantiatedRoot.HasComponent<MetaCore::MetaCoreMeshRendererComponent>(), "平坦化后根节点应直接包含 MeshRenderer 渲染组件");
const MetaCore::MetaCoreAssetGuid originalMeshGuid = instantiatedRoot.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().MeshAssetGuid;
const MetaCore::MetaCoreAssetGuid originalMaterialGuid = instantiatedRoot.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().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(), "重导入后仍应能找到模型资源记录");
// 【敏捷开发决策】:重导入时无需在磁盘上创建/读取体积沉重的 .mcasset 包文件,只做极简的 mcmeta 元数据校验
const std::filesystem::path refreshedMeta = tempProjectRoot / "Assets" / "Pump.gltf.mcmeta";
MetaCoreExpect(std::filesystem::exists(refreshedMeta), "重导入后应生成 mcmeta 元数据文件");
instantiatedRoot = scene.FindGameObject(*instantiatedRootId);
MetaCoreExpect(instantiatedRoot, "reimport should keep model root alive");
MetaCoreExpect(instantiatedRoot && instantiatedRoot.HasComponent<MetaCore::MetaCoreMeshRendererComponent>(), "reimport should keep renderable instance alive");
MetaCoreExpect(instantiatedRoot.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().MeshAssetGuid == originalMeshGuid, "reimport should keep mesh reference stable");
MetaCoreExpect(!instantiatedRoot.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().MaterialAssetGuids.empty(), "reimport should keep material references");
MetaCoreExpect(instantiatedRoot.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().MaterialAssetGuids.front() == originalMaterialGuid, "reimport should keep material reference stable");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestBootstrapStartupSceneCreation() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreBootstrapSceneProject";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets");
std::filesystem::create_directories(tempProjectRoot / "Scenes");
std::filesystem::create_directories(tempProjectRoot / "Library");
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
projectFile << "{\n"
<< " \"name\": \"BootstrapProject\",\n"
<< " \"version\": \"0.1.0\",\n"
<< " \"scenes\": [],\n"
<< " \"startup_scene\": \"\"\n"
<< "}\n";
}
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene();
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const auto scenePersistenceService = moduleRegistry.ResolveService<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.json";
MetaCoreExpect(scenePersistenceService->SaveSceneAs(editorContext, bootstrapScenePath), "应能为默认场景创建二进制启动场景");
MetaCoreExpect(assetDatabaseService->SetStartupScenePath(bootstrapScenePath), "应能设置 startup scene");
MetaCoreExpect(assetDatabaseService->Refresh(), "刷新资产数据库应成功");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / bootstrapScenePath), "应生成 Main.mcscene.json");
MetaCoreExpect(scenePersistenceService->LoadStartupScene(editorContext), "设置后应能加载 startup scene");
MetaCoreExpect(scenePersistenceService->GetCurrentScenePath() == bootstrapScenePath, "startup scene 路径应正确");
MetaCoreExpect(scene.GetGameObjects().size() == 2, "bootstrap scene 应保存默认场景内容");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestPrefabWorkflow() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCorePrefabWorkflowProject";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets" / "Prefabs");
std::filesystem::create_directories(tempProjectRoot / "Scenes");
std::filesystem::create_directories(tempProjectRoot / "Library");
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
projectFile << "{\n"
<< " \"name\": \"PrefabProject\",\n"
<< " \"version\": \"0.1.0\",\n"
<< " \"scenes\": [],\n"
<< " \"startup_scene\": \"\"\n"
<< "}\n";
}
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
MetaCore::MetaCoreGameObject root = scene.CreateGameObject("PrefabRoot");
root.AddComponent<MetaCore::MetaCoreMeshRendererComponent>();
auto& rootScript = root.AddComponent<MetaCore::MetaCoreScriptComponent>();
rootScript.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{
4101,
"MetaCore.Script.Mover",
true,
"{\"Enabled\":true,\"Velocity\":[1.0,2.0,3.0]}"
});
root.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(1.0F, 2.0F, 3.0F);
const MetaCore::MetaCoreId rootId = root.GetId();
MetaCore::MetaCoreGameObject child = scene.CreateGameObject("PrefabChild", rootId);
child.AddComponent<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.json";
const auto createdPrefabPath = prefabService->CreatePrefabFromSelection(editorContext, prefabPath);
MetaCoreExpect(createdPrefabPath.has_value(), "应能从选中对象创建 prefab");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / prefabPath), "应写出 mcprefab.json 文件");
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, "实例化后应能找到根对象");
MetaCoreExpect(instantiatedRoot.HasComponent<MetaCore::MetaCorePrefabInstanceMetadata>(), "实例根应带有 prefab 元数据");
MetaCoreExpect(instantiatedRoot.HasComponent<MetaCore::MetaCoreScriptComponent>(), "实例根应保留 Script 组件");
MetaCoreExpect(
instantiatedRoot.GetComponent<MetaCore::MetaCoreScriptComponent>().Instances.size() == 1,
"Prefab 实例应保留脚本实例"
);
instantiatedRoot.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(8.0F, 9.0F, 10.0F);
editorContext.SelectOnly(instantiatedRoot.GetId());
MetaCoreExpect(prefabService->ApplySelectedPrefabInstance(editorContext), "应能应用 prefab 实例");
const auto appliedPrefabDocument = MetaCore::MetaCoreSceneSerializer::LoadPrefabFromJson(tempProjectRoot / prefabPath, reflectionRegistry->GetTypeRegistry());
MetaCoreExpect(appliedPrefabDocument.has_value(), "Apply 后 prefab 应能被 JSON 解析");
MetaCoreExpect(!appliedPrefabDocument->GameObjects.empty(), "Apply 后 prefab 应保留对象数据");
MetaCoreExpect(appliedPrefabDocument->GameObjects.front().Script.has_value(), "Apply 后 prefab 资源应保留 Script 组件");
MetaCoreExpect(
appliedPrefabDocument->GameObjects.front().Script->Instances.front().ScriptTypeId == "MetaCore.Script.Mover",
"Apply 后 prefab 资源应保留脚本 TypeId"
);
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, "第二个实例应存在");
MetaCoreExpect(secondInstanceRoot.HasComponent<MetaCore::MetaCoreScriptComponent>(), "第二个实例应保留 Script 组件");
MetaCoreExpectVec3Near(secondInstanceRoot.GetComponent<MetaCore::MetaCoreTransformComponent>().Position, glm::vec3(8.0F, 9.0F, 10.0F), "Apply 后新实例应继承修改过的 transform");
secondInstanceRoot.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(-1.0F, -2.0F, -3.0F);
editorContext.SelectOnly(secondInstanceRoot.GetId());
MetaCoreExpect(prefabService->RevertSelectedPrefabInstance(editorContext), "应能还原 prefab 实例");
MetaCore::MetaCoreGameObject revertedRoot = scene.FindGameObject(editorContext.GetActiveObjectId());
MetaCoreExpect(revertedRoot, "还原后应重新选中实例根");
MetaCoreExpectVec3Near(revertedRoot.GetComponent<MetaCore::MetaCoreTransformComponent>().Position, glm::vec3(8.0F, 9.0F, 10.0F), "Revert 后实例应恢复为 prefab 内容");
MetaCore::MetaCoreGameObject draggedSource = scene.CreateGameObject("DraggedPrefabSource");
draggedSource.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(4.0F, 5.0F, 6.0F);
const std::filesystem::path draggedPrefabPath = std::filesystem::path("Assets") / "Prefabs" / "DraggedPrefabSource.mcprefab.json";
const auto draggedCreatedPath = prefabService->CreatePrefabFromObject(editorContext, draggedSource.GetId(), draggedPrefabPath);
MetaCoreExpect(draggedCreatedPath.has_value(), "应能从指定对象创建 prefab");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / draggedPrefabPath), "指定对象 prefab 文件应存在");
MetaCoreExpect(assetDatabaseService->Refresh(), "指定对象 prefab 创建后应能刷新资产数据库");
const auto draggedPrefabRecord = assetDatabaseService->FindAssetByRelativePath(draggedPrefabPath);
MetaCoreExpect(draggedPrefabRecord.has_value(), "指定对象 prefab 应进入资产数据库");
const auto draggedPrefabDocument = MetaCore::MetaCoreSceneSerializer::LoadPrefabFromJson(tempProjectRoot / draggedPrefabPath, reflectionRegistry->GetTypeRegistry());
MetaCoreExpect(draggedPrefabDocument.has_value(), "指定对象 prefab 应能被 JSON 解析");
MetaCoreExpect(!draggedPrefabDocument->GameObjects.empty(), "指定对象 prefab 应包含对象数据");
MetaCoreExpect(draggedPrefabDocument->GameObjects.front().Name == "DraggedPrefabSource", "指定对象 prefab 应使用目标对象数据");
MetaCoreExpectVec3Near(
draggedPrefabDocument->GameObjects.front().Transform.Position,
glm::vec3(4.0F, 5.0F, 6.0F),
"指定对象 prefab 应保留目标对象 transform"
);
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestCookPipelineCooksJsonScenePrefabMaterialUi() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreJsonCookPipelineProject";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets" / "Prefabs");
std::filesystem::create_directories(tempProjectRoot / "Assets" / "Materials");
std::filesystem::create_directories(tempProjectRoot / "Assets" / "UI");
std::filesystem::create_directories(tempProjectRoot / "Scenes");
std::filesystem::create_directories(tempProjectRoot / "Library");
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
projectFile << "{\n"
<< " \"name\": \"JsonCookProject\",\n"
<< " \"version\": \"0.1.0\",\n"
<< " \"scenes\": [\"Scenes/Main.mcscene.json\"],\n"
<< " \"startup_scene\": \"Scenes/Main.mcscene.json\"\n"
<< "}\n";
}
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
const auto assetDatabase = moduleRegistry.ResolveService<MetaCore::MetaCoreIAssetDatabaseService>();
const auto cookService = moduleRegistry.ResolveService<MetaCore::MetaCoreICookService>();
const auto reflectionRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIReflectionRegistry>();
MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService");
MetaCoreExpect(cookService != nullptr, "应解析到 CookService");
MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry");
const auto& registry = reflectionRegistry->GetTypeRegistry();
const std::filesystem::path scenePath = std::filesystem::path("Scenes") / "Main.mcscene.json";
MetaCore::MetaCoreSceneDocument sceneDocument;
sceneDocument.Name = "Main";
sceneDocument.GameObjects = MetaCore::MetaCoreCreateDefaultScene().CaptureSnapshot().GameObjects;
MetaCoreExpect(
MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(tempProjectRoot / scenePath, sceneDocument, registry),
"应写出 JSON scene"
);
const std::filesystem::path prefabPath = std::filesystem::path("Assets") / "Prefabs" / "Widget.mcprefab.json";
MetaCore::MetaCorePrefabDocument prefabDocument;
prefabDocument.Name = "Widget";
MetaCore::MetaCoreGameObjectData prefabObject;
prefabObject.Id = 1;
prefabObject.Name = "WidgetRoot";
prefabDocument.GameObjects.push_back(prefabObject);
MetaCoreExpect(
MetaCore::MetaCoreSceneSerializer::SavePrefabToJson(tempProjectRoot / prefabPath, prefabDocument, registry),
"应写出 JSON prefab"
);
const std::filesystem::path materialPath = std::filesystem::path("Assets") / "Materials" / "Default.mcmaterial.json";
MetaCore::MetaCoreMaterialAssetDocument materialDocument;
materialDocument.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate();
materialDocument.Name = "Default";
materialDocument.BaseColor = glm::vec3(0.25F, 0.5F, 0.75F);
MetaCoreExpect(
MetaCore::MetaCoreSceneSerializer::SaveMaterialToJson(tempProjectRoot / materialPath, materialDocument, registry),
"应写出 JSON material"
);
const std::filesystem::path uiPath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json";
MetaCore::MetaCoreUiDocument uiDocument;
uiDocument.Name = "Hud";
uiDocument.RootNodeIds.push_back("root");
MetaCore::MetaCoreUiNodeDocument uiRoot;
uiRoot.Id = "root";
uiRoot.Name = "Root";
uiRoot.Type = MetaCore::MetaCoreUiNodeType::Panel;
uiDocument.Nodes.push_back(uiRoot);
MetaCoreExpect(
MetaCore::MetaCoreSceneSerializer::SaveUiToJson(tempProjectRoot / uiPath, uiDocument, registry),
"应写出 JSON UI"
);
MetaCoreExpect(assetDatabase->Refresh(), "刷新后应扫描 JSON 源资产");
const auto sceneRecord = assetDatabase->FindAssetByRelativePath(scenePath);
const auto prefabRecord = assetDatabase->FindAssetByRelativePath(prefabPath);
const auto materialRecord = assetDatabase->FindAssetByRelativePath(materialPath);
const auto uiRecord = assetDatabase->FindAssetByRelativePath(uiPath);
MetaCoreExpect(sceneRecord.has_value() && sceneRecord->Type == "scene", "应注册 JSON scene 源资产");
MetaCoreExpect(prefabRecord.has_value() && prefabRecord->Type == "prefab", "应注册 JSON prefab 源资产");
MetaCoreExpect(materialRecord.has_value() && materialRecord->Type == "material", "应注册 JSON material 源资产");
MetaCoreExpect(uiRecord.has_value() && uiRecord->Type == "ui_document", "应注册 JSON UI 源资产");
MetaCoreExpect(prefabRecord->PackagePath == prefabPath, "JSON prefab 的 PackagePath 应保持源路径");
MetaCoreExpect(materialRecord->PackagePath == materialPath, "JSON material 的 PackagePath 应保持源路径");
MetaCoreExpect(uiRecord->PackagePath == uiPath, "JSON UI 的 PackagePath 应保持源路径");
MetaCoreExpect(cookService->CookAsset(sceneRecord->Guid), "应能 Cook JSON scene");
MetaCoreExpect(cookService->CookAsset(prefabRecord->Guid), "应能 Cook JSON prefab");
MetaCoreExpect(cookService->CookAsset(materialRecord->Guid), "应能 Cook JSON material");
MetaCoreExpect(cookService->CookAsset(uiRecord->Guid), "应能 Cook JSON UI");
const auto expectCookedOutput = [&](const MetaCore::MetaCoreAssetGuid& guid, const char* message) {
const std::filesystem::path cookedPath = cookService->GetCookedPathForAsset(guid);
MetaCoreExpect(!cookedPath.empty(), message);
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / cookedPath), message);
};
expectCookedOutput(sceneRecord->Guid, "JSON scene 应生成 cooked 输出");
expectCookedOutput(prefabRecord->Guid, "JSON prefab 应生成 cooked 输出");
expectCookedOutput(materialRecord->Guid, "JSON material 应生成 cooked 输出");
expectCookedOutput(uiRecord->Guid, "JSON UI 应生成 cooked 输出");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / (prefabPath.generic_string() + ".mcmeta")), "JSON prefab 应生成 mcmeta");
MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / (prefabPath.generic_string() + ".mcmeta.mcmeta")), "JSON prefab 的 mcmeta 不应被递归导入");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestComponentRegistryOperations() {
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
MetaCore::MetaCoreGameObject object = scene.CreateGameObject("RegistryObject");
editorContext.SelectOnly(object.GetId());
const auto componentRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIComponentTypeRegistry>();
MetaCoreExpect(componentRegistry != nullptr, "应解析到 ComponentTypeRegistry");
const auto* cameraDescriptor = componentRegistry->FindDescriptor("Camera");
const auto* lightDescriptor = componentRegistry->FindDescriptor("Light");
const auto* scriptDescriptor = componentRegistry->FindDescriptor("Script");
MetaCoreExpect(cameraDescriptor != nullptr, "应能找到 Camera 描述符");
MetaCoreExpect(lightDescriptor != nullptr, "应能找到 Light 描述符");
MetaCoreExpect(scriptDescriptor != nullptr, "应能找到 Script 描述符");
MetaCoreExpect(!cameraDescriptor->HasComponent(object), "初始时不应有 Camera");
const bool addedCamera = editorContext.ExecuteSnapshotCommand("添加 Camera", [&]() {
return cameraDescriptor->AddComponent(object);
});
MetaCoreExpect(addedCamera, "应能通过注册表添加 Camera");
MetaCoreExpect(object.HasComponent<MetaCore::MetaCoreCameraComponent>(), "添加后对象应拥有 Camera");
const bool addedLight = editorContext.ExecuteSnapshotCommand("添加 Light", [&]() {
return lightDescriptor->AddComponent(object);
});
MetaCoreExpect(addedLight, "应能通过注册表添加 Light");
MetaCoreExpect(object.HasComponent<MetaCore::MetaCoreLightComponent>(), "添加后对象应拥有 Light");
const bool addedScript = editorContext.ExecuteSnapshotCommand("添加 Script", [&]() {
return scriptDescriptor->AddComponent(object);
});
MetaCoreExpect(addedScript, "应能通过注册表添加 Script");
MetaCoreExpect(object.HasComponent<MetaCore::MetaCoreScriptComponent>(), "添加后对象应拥有 Script");
object.GetComponent<MetaCore::MetaCoreScriptComponent>().Instances.push_back(MetaCore::MetaCoreScriptInstanceData{
5101,
"MetaCore.Script.Rotator",
true,
"{\"Enabled\":true,\"DegreesPerSecond\":120.0}"
});
const bool removedCamera = editorContext.ExecuteSnapshotCommand("移除 Camera", [&]() {
return cameraDescriptor->RemoveComponent(object);
});
MetaCoreExpect(removedCamera, "应能通过注册表移除 Camera");
MetaCoreExpect(!object.HasComponent<MetaCore::MetaCoreCameraComponent>(), "移除后对象不应再拥有 Camera");
const bool readdedCamera = editorContext.ExecuteSnapshotCommand("重新添加 Camera", [&]() {
return cameraDescriptor->AddComponent(object);
});
MetaCoreExpect(readdedCamera, "应能重新添加 Camera");
object.GetComponent<MetaCore::MetaCoreCameraComponent>().FieldOfViewDegrees = 77.0F;
object.GetComponent<MetaCore::MetaCoreCameraComponent>().NearClip = 0.25F;
object.GetComponent<MetaCore::MetaCoreCameraComponent>().FarClip = 250.0F;
object.GetComponent<MetaCore::MetaCoreCameraComponent>().IsPrimary = true;
object.GetComponent<MetaCore::MetaCoreCameraComponent>().ProjectionMode = MetaCore::MetaCoreCameraProjectionMode::Orthographic;
object.GetComponent<MetaCore::MetaCoreCameraComponent>().OrthographicSize = 9.0F;
object.GetComponent<MetaCore::MetaCoreCameraComponent>().Depth = -3.0F;
object.GetComponent<MetaCore::MetaCoreCameraComponent>().CullingMask = 0x000000FFU;
object.GetComponent<MetaCore::MetaCoreCameraComponent>().ClearFlags = MetaCore::MetaCoreCameraClearFlags::SolidColor;
object.GetComponent<MetaCore::MetaCoreCameraComponent>().BackgroundColor = glm::vec3(0.1F, 0.2F, 0.3F);
object.GetComponent<MetaCore::MetaCoreCameraComponent>().BackgroundAlpha = 0.5F;
MetaCoreExpect(componentRegistry->CopyComponent("Camera", object), "应能复制 Camera 组件值");
MetaCore::MetaCoreGameObject pasteTarget = scene.CreateGameObject("PasteTarget");
MetaCore::MetaCoreGameObject secondPasteTarget = scene.CreateGameObject("SecondPasteTarget");
MetaCoreExpect(!pasteTarget.HasComponent<MetaCore::MetaCoreCameraComponent>(), "新对象初始不应拥有 Camera");
MetaCoreExpect(!secondPasteTarget.HasComponent<MetaCore::MetaCoreCameraComponent>(), "第二个新对象初始不应拥有 Camera");
MetaCoreExpect(componentRegistry->CanPasteComponent("Camera"), "复制后应可粘贴 Camera");
MetaCoreExpect(componentRegistry->PasteComponent("Camera", pasteTarget), "应能粘贴 Camera 组件值");
MetaCoreExpect(componentRegistry->PasteComponent("Camera", secondPasteTarget), "应能向第二个对象粘贴 Camera 组件值");
MetaCoreExpect(pasteTarget.HasComponent<MetaCore::MetaCoreCameraComponent>(), "粘贴后目标对象应拥有 Camera");
MetaCoreExpect(secondPasteTarget.HasComponent<MetaCore::MetaCoreCameraComponent>(), "第二个目标对象粘贴后应拥有 Camera");
MetaCoreExpect(std::abs(pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().FieldOfViewDegrees - 77.0F) <= 0.0001F, "粘贴后 FOV 应一致");
MetaCoreExpect(std::abs(pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().NearClip - 0.25F) <= 0.0001F, "粘贴后 NearClip 应一致");
MetaCoreExpect(std::abs(pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().FarClip - 250.0F) <= 0.0001F, "粘贴后 FarClip 应一致");
MetaCoreExpect(pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().IsPrimary, "粘贴后 IsPrimary 应一致");
MetaCoreExpect(
pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().ProjectionMode == MetaCore::MetaCoreCameraProjectionMode::Orthographic,
"粘贴后 ProjectionMode 应一致"
);
MetaCoreExpect(std::abs(pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().OrthographicSize - 9.0F) <= 0.0001F, "粘贴后 OrthographicSize 应一致");
MetaCoreExpect(std::abs(pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().Depth - -3.0F) <= 0.0001F, "粘贴后 Depth 应一致");
MetaCoreExpect(pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().CullingMask == 0x000000FFU, "粘贴后 CullingMask 应一致");
MetaCoreExpect(
pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().ClearFlags == MetaCore::MetaCoreCameraClearFlags::SolidColor,
"粘贴后 ClearFlags 应一致"
);
MetaCoreExpectVec3Near(
pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().BackgroundColor,
glm::vec3(0.1F, 0.2F, 0.3F),
"粘贴后 BackgroundColor 应一致"
);
MetaCoreExpect(std::abs(pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().BackgroundAlpha - 0.5F) <= 0.0001F, "粘贴后 BackgroundAlpha 应一致");
MetaCoreExpect(std::abs(secondPasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().FieldOfViewDegrees - 77.0F) <= 0.0001F, "第二个对象粘贴后 FOV 应一致");
MetaCoreExpect(componentRegistry->CopyComponent("Script", object), "应能复制 Script 组件值");
MetaCoreExpect(componentRegistry->CanPasteComponent("Script"), "复制后应可粘贴 Script");
MetaCoreExpect(componentRegistry->PasteComponent("Script", pasteTarget), "应能粘贴 Script 组件值");
MetaCoreExpect(pasteTarget.HasComponent<MetaCore::MetaCoreScriptComponent>(), "粘贴后目标对象应拥有 Script");
MetaCoreExpect(
pasteTarget.GetComponent<MetaCore::MetaCoreScriptComponent>().Instances.size() == 1,
"粘贴后 Script 实例数量应一致"
);
MetaCoreExpect(
pasteTarget.GetComponent<MetaCore::MetaCoreScriptComponent>().Instances.front().ScriptTypeId == "MetaCore.Script.Rotator",
"粘贴后 Script TypeId 应一致"
);
MetaCoreExpect(cameraDescriptor->ResetComponent != nullptr, "Camera descriptor 应提供 Reset");
pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().FieldOfViewDegrees = 10.0F;
pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().ProjectionMode = MetaCore::MetaCoreCameraProjectionMode::Orthographic;
MetaCoreExpect(cameraDescriptor->ResetComponent(pasteTarget), "应能重置 Camera");
MetaCoreExpect(std::abs(pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().FieldOfViewDegrees - 60.0F) <= 0.0001F, "重置后 Camera 应恢复默认值");
MetaCoreExpect(
pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().ProjectionMode == MetaCore::MetaCoreCameraProjectionMode::Perspective,
"重置后 Camera ProjectionMode 应恢复默认值"
);
MetaCoreExpect(scriptDescriptor->ResetComponent != nullptr, "Script descriptor 应提供 Reset");
MetaCoreExpect(scriptDescriptor->ResetComponent(pasteTarget), "应能重置 Script");
MetaCoreExpect(pasteTarget.GetComponent<MetaCore::MetaCoreScriptComponent>().Instances.empty(), "重置后 Script 实例列表应清空");
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().GetId(),
MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition,
MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue
}
});
MetaCoreExpect(dispatcher.GetBindingStatuses().size() == 1, "Dispatcher 应构建一个 binding status");
MetaCoreExpect(dispatcher.GetBindingStatuses().front().Healthy, "初始 binding status 应为 healthy");
}
void MetaCoreTestRuntimeDataDispatcherAppliesUpdates() {
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreGameObject cube = scene.CreateGameObject("Cube");
cube.AddComponent<MetaCore::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.GetId(),
MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition,
MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue
},
MetaCore::MetaCoreSceneBindingDefinition{
"binding.visible",
"cube.visible",
cube.GetId(),
MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible,
MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue
}
});
dispatcher.ApplyUpdates({
MetaCore::MetaCoreRuntimeDataUpdate{
"cube.position",
MetaCore::MetaCoreRuntimeDataValue{
MetaCore::MetaCoreRuntimeValueType::Vec3,
false,
0,
0.0,
{},
glm::vec3(4.0F, 5.0F, 6.0F),
100,
MetaCore::MetaCoreRuntimeDataQuality::Good
},
1
},
MetaCore::MetaCoreRuntimeDataUpdate{
"cube.visible",
MetaCore::MetaCoreRuntimeDataValue{
MetaCore::MetaCoreRuntimeValueType::Bool,
false,
0,
0.0,
{},
glm::vec3(0.0F, 0.0F, 0.0F),
100,
MetaCore::MetaCoreRuntimeDataQuality::Good
},
2
}
});
MetaCoreExpectVec3Near(cube.GetComponent<MetaCore::MetaCoreTransformComponent>().Position, glm::vec3(4.0F, 5.0F, 6.0F), "Dispatcher 应更新 Transform.Position");
MetaCoreExpect(!cube.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().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.AddComponent<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.GetId(),
MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible,
MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue
}
});
dispatcher.ApplyUpdates({
MetaCore::MetaCoreRuntimeDataUpdate{
"cube.visible",
MetaCore::MetaCoreRuntimeDataValue{
MetaCore::MetaCoreRuntimeValueType::Bool,
true,
0,
0.0,
{},
glm::vec3(0.0F, 0.0F, 0.0F),
100,
MetaCore::MetaCoreRuntimeDataQuality::Good
},
1
}
});
dispatcher.TickStaleness(1500);
MetaCoreExpect(dispatcher.GetBindingStatuses().front().Stale, "Binding 应在超时后标记 stale");
MetaCoreExpect(dispatcher.HasBindingFaults(), "存在 stale binding 时应报告 faults");
}
void MetaCoreTestMockRuntimeDataSourceAdapterDegradedState() {
MetaCore::MetaCoreMockRuntimeDataSourceAdapter adapter;
MetaCoreExpect(adapter.Configure(MetaCore::MetaCoreDataSourceDefinition{
"mock-source",
"mock",
"Mock Source",
{},
true,
1000
}), "Mock adapter 应能配置");
MetaCoreExpect(adapter.Connect(), "Mock adapter 应能连接");
adapter.SetEmitUpdates(false);
adapter.Tick(0.25);
MetaCoreExpect(adapter.GetStatus().State == MetaCore::MetaCoreRuntimeDataSourceState::Degraded, "停止发包后应进入 degraded 状态");
MetaCoreExpect(adapter.PollUpdates().empty(), "Degraded 状态下不应继续输出更新");
}
void MetaCoreTestFileReplayRuntimeDataSourceAdapterEmitsReplayFrames() {
const std::filesystem::path tempDirectory = std::filesystem::temp_directory_path() / "MetaCoreRuntimeReplay";
std::filesystem::remove_all(tempDirectory);
std::filesystem::create_directories(tempDirectory);
const std::filesystem::path replayPath = tempDirectory / "RuntimeReplay.mcstream";
{
std::ofstream replayFile(replayPath, std::ios::trunc);
replayFile << "0.00 cube.visible bool true\n";
replayFile << "0.25 cube.position vec3 1.0 2.0 3.0\n";
}
MetaCore::MetaCoreFileReplayRuntimeDataSourceAdapter adapter;
MetaCoreExpect(adapter.Configure(MetaCore::MetaCoreDataSourceDefinition{
"replay-source",
"file_replay",
"Replay Source",
{MetaCore::MetaCoreDataSourceSetting{"file_path", replayPath.string()}},
true,
1000
}), "File replay adapter 应能配置");
MetaCoreExpect(adapter.Connect(), "File replay adapter 应能连接");
adapter.Tick(0.10);
auto updates = adapter.PollUpdates();
MetaCoreExpect(updates.size() == 1, "File replay adapter 首批应输出一条更新");
MetaCoreExpect(updates.front().DataPointId == "cube.visible", "首批更新应命中第一个数据点");
adapter.Tick(0.20);
updates = adapter.PollUpdates();
MetaCoreExpect(updates.size() == 1, "File replay adapter 第二批应输出一条更新");
MetaCoreExpect(updates.front().DataPointId == "cube.position", "第二批更新应命中第二个数据点");
MetaCoreExpect(
adapter.GetStatus().State == MetaCore::MetaCoreRuntimeDataSourceState::Connected,
"File replay adapter 回放期间应保持 connected"
);
adapter.Tick(1.0);
MetaCoreExpect(
adapter.PollUpdates().empty(),
"File replay adapter 回放结束后不应继续输出更新"
);
MetaCoreExpect(
adapter.GetStatus().State == MetaCore::MetaCoreRuntimeDataSourceState::Degraded,
"File replay adapter 回放结束后应进入 degraded 状态"
);
std::filesystem::remove_all(tempDirectory);
}
void MetaCoreTestRuntimeDiagnosticsSnapshotReportsFaults() {
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreGameObject cube = scene.CreateGameObject("Cube");
cube.AddComponent<MetaCore::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.GetId(),
MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible,
MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue
}
});
dispatcher.TickStaleness(2000);
const auto diagnostics = dispatcher.BuildDiagnosticsSnapshot({
MetaCore::MetaCoreRuntimeDataSourceStatus{
"mock-source",
MetaCore::MetaCoreRuntimeDataSourceState::Degraded,
1,
0,
"Degraded source"
}
});
MetaCoreExpect(diagnostics.HasFaults, "Diagnostics snapshot 应报告 faults");
MetaCoreExpect(diagnostics.SourceStatuses.size() == 1, "Diagnostics snapshot 应保留 source status");
MetaCoreExpect(diagnostics.BindingStatuses.size() == 1, "Diagnostics snapshot 应保留 binding status");
}
void MetaCoreTestRuntimeDataBinaryDocumentRejectsCorruption() {
MetaCore::MetaCoreTypeRegistry registry;
MetaCoreRegisterRuntimeDataGeneratedTypes(registry);
const std::filesystem::path tempDirectory = std::filesystem::temp_directory_path() / "MetaCoreRuntimeDataCorruptIo";
std::filesystem::remove_all(tempDirectory);
std::filesystem::create_directories(tempDirectory);
const std::filesystem::path corruptSourcesPath = tempDirectory / "CorruptSources.mcruntime";
{
std::ofstream output(corruptSourcesPath, std::ios::binary | std::ios::trunc);
output << "not-a-valid-runtime-config";
}
const auto loadedSources = MetaCore::MetaCoreReadRuntimeDataSourcesDocument(corruptSourcesPath, registry);
MetaCoreExpect(!loadedSources.has_value(), "损坏的 RuntimeDataSourcesDocument 二进制应被拒绝");
std::filesystem::remove_all(tempDirectory);
}
void MetaCoreTestEditorContextRuntimeDataConfigSaveLoad() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorConfigProject";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets");
std::filesystem::create_directories(tempProjectRoot / "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.json", "保存后应写入默认 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() {
MetaCoreExpect(MetaCoreTestInitializeSocketApi(), "Smoke test 应能初始化 socket API");
MetaCoreTestSocket listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
MetaCoreExpect(listenSocket != MetaCoreTestInvalidSocket, "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)) != MetaCoreTestSocketError, "Smoke test bind 应成功");
MetaCoreExpect(listen(listenSocket, 1) != MetaCoreTestSocketError, "Smoke test listen 应成功");
MetaCoreTestSocketLength addressLength = sizeof(address);
MetaCoreExpect(
getsockname(listenSocket, reinterpret_cast<sockaddr*>(&address), &addressLength) != MetaCoreTestSocketError,
"Smoke test getsockname 应成功"
);
const unsigned short port = ntohs(address.sin_port);
std::thread serverThread([listenSocket]() {
MetaCoreTestSocket clientSocket = accept(listenSocket, nullptr, nullptr);
if (clientSocket != MetaCoreTestInvalidSocket) {
const char* payload =
"cube.visible bool true\n"
"cube.position vec3 1.0 2.0 3.0\n";
send(clientSocket, payload, std::strlen(payload), 0);
shutdown(clientSocket, SD_SEND);
MetaCoreTestCloseSocket(clientSocket);
}
MetaCoreTestCloseSocket(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();
MetaCoreTestCleanupSocketApi();
return;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
serverThread.join();
MetaCoreTestCleanupSocketApi();
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 背景色应保持");
}
void MetaCoreTestDecoupledSnapshotSync() {
const std::filesystem::path projectRoot = MetaCoreTestRepositoryRoot() / "TestProject";
const std::filesystem::path modelRelativePath = std::filesystem::path("Assets") / "Models" / "Cube.glb";
const std::string modelRelativePathString = modelRelativePath.generic_string();
// 1. 创建默认场景并收集核心对象 ID
MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene();
// 动态添加一个模型对象,测试需要它作为网格对象进行解耦同步验证
MetaCore::MetaCoreGameObject cubeObj = scene.CreateGameObject("Cube.glb");
auto& meshRenderer = cubeObj.AddComponent<MetaCore::MetaCoreMeshRendererComponent>();
meshRenderer.MeshSource = MetaCore::MetaCoreMeshSourceKind::Asset;
meshRenderer.SourceModelPath = modelRelativePathString;
meshRenderer.SourceModelAssetGuid = MetaCore::MetaCoreAssetRegistry::Get().ResolvePathToGuid(modelRelativePathString);
cubeObj.AddComponent<MetaCore::MetaCoreModelRootTag>(MetaCore::MetaCoreModelRootTag{ modelRelativePathString });
cubeObj.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(0.0F, 0.5F, 0.0F);
MetaCore::MetaCoreId cubeId = 0;
MetaCore::MetaCoreId lightId = 0;
MetaCore::MetaCoreId cameraId = 0;
for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) {
if (object.GetName() == "Cube.glb") {
cubeId = object.GetId();
}
if (object.GetName() == "Directional Light") {
lightId = object.GetId();
}
if (object.GetName() == "Main Camera") {
cameraId = object.GetId();
}
}
MetaCoreExpect(cubeId != 0, "默认场景应包含网格渲染器 Cube");
MetaCoreExpect(lightId != 0, "默认场景应包含光源 Light");
MetaCoreExpect(cameraId != 0, "默认场景应包含相机 Camera");
// 2. 生成快照数据
MetaCore::MetaCoreSceneRenderSync renderSync;
MetaCore::MetaCoreSceneRenderSyncSnapshot snapshot = renderSync.BuildSnapshot(scene);
// 3. 修改快照中的参数以测试解耦逻辑和 Enabled 属性过滤
glm::mat4 customLocalMatrix = glm::translate(glm::mat4(1.0F), glm::vec3(15.0F, -5.0F, 3.5F));
snapshot.LocalMatrices[cubeId] = customLocalMatrix;
snapshot.WorldMatrices[cubeId] = customLocalMatrix;
// 禁用所有光源和相机以测试过滤和默认光源自动起降
for (auto& light : snapshot.Lights) {
light.Enabled = false;
}
for (auto& camera : snapshot.Cameras) {
camera.Enabled = false;
}
snapshot.PrimaryCamera = std::nullopt;
// 4. 初始化窗口和 FilamentSceneBridge
MetaCore::MetaCoreWindow window;
bool winOk = window.Initialize(800, 600, "DecoupledSnapshotSyncTestWindow");
MetaCoreExpect(winOk, "测试窗口初始化应成功");
MetaCore::MetaCoreFilamentSceneBridge bridge;
bridge.SetProjectRootPath(projectRoot);
bool bridgeOk = bridge.Initialize(window);
MetaCoreExpect(bridgeOk, "测试桥接器初始化应成功");
// 5. 在 scene == nullptr 的快照渲染模式下执行同步
std::cout << "[DEBUG] SyncScene starting..." << std::endl;
bridge.SyncScene(snapshot, nullptr, false, false);
std::cout << "[DEBUG] SyncScene completed." << std::endl;
// 6. 验证映射重建和变换同步
glm::mat4 cubeWorldMatrix;
bool hasCube = bridge.TryGetObjectWorldMatrix(cubeId, cubeWorldMatrix);
std::cout << "[DEBUG] hasCube: " << hasCube << std::endl;
MetaCoreExpect(hasCube, "无 scene 模式下重建映射后应包含 Cube 变换");
bool transOk = bridge.VerifyTransformForTesting(cubeId, customLocalMatrix);
std::cout << "[DEBUG] transOk: " << transOk << std::endl;
MetaCoreExpect(transOk, "快照中的自定义局部变换应正确同步到 Filament");
bool cubeVisibleInScene = bridge.VerifyEntityInSceneForTesting(cubeId);
std::cout << "[DEBUG] cubeVisibleInScene: " << cubeVisibleInScene << std::endl;
MetaCoreExpect(cubeVisibleInScene, "默认可见的网格实体应该在 Filament 场景中");
// 7. 验证 Enabled 状态为 false 的光源被过滤排除,且默认灯光起降机制生效 (自动亮起)
bool hasLight = bridge.VerifyLightExistsForTesting(lightId);
std::cout << "[DEBUG] hasLight: " << hasLight << std::endl;
MetaCoreExpect(!hasLight, "Enabled 为 false 的自定义光源不应被实例化");
float defaultLightIntensity = bridge.GetDefaultLightIntensityForTesting();
std::cout << "[DEBUG] defaultLightIntensity: " << defaultLightIntensity << std::endl;
MetaCoreExpect(defaultLightIntensity > 99000.0F, "无有效自定义光源时,默认灯光应起飞作为兜底");
// 8. 动态开启光源属性,验证默认灯光动态变暗
for (auto& light : snapshot.Lights) {
if (light.ObjectId == lightId) {
light.Enabled = true;
}
}
// 同时我们将 cube 的 Visible 改为 false测试动态隐藏
for (auto& r : snapshot.Renderables) {
if (r.ObjectId == cubeId) {
r.Visible = false;
}
}
std::cout << "[DEBUG] SyncScene 2 starting..." << std::endl;
bridge.SyncScene(snapshot, nullptr, false, false);
std::cout << "[DEBUG] SyncScene 2 completed." << std::endl;
bool hasLightNow = bridge.VerifyLightExistsForTesting(lightId);
std::cout << "[DEBUG] hasLightNow: " << hasLightNow << std::endl;
MetaCoreExpect(hasLightNow, "启用光源后自定义光源应被实例化");
float defaultLightIntensityNow = bridge.GetDefaultLightIntensityForTesting();
std::cout << "[DEBUG] defaultLightIntensityNow: " << defaultLightIntensityNow << std::endl;
MetaCoreExpect(defaultLightIntensityNow < 1.0F, "启用自定义光源时,默认灯光强度应自动降为 0");
bool cubeVisibleInScene2 = bridge.VerifyEntityInSceneForTesting(cubeId);
std::cout << "[DEBUG] cubeVisibleInScene2: " << cubeVisibleInScene2 << std::endl;
MetaCoreExpect(!cubeVisibleInScene2, "禁用可见性后,网格实体应该从 Filament 场景中移除");
// 9. 再次禁用光源,验证自定义光源被销毁并且默认灯光复亮
for (auto& light : snapshot.Lights) {
if (light.ObjectId == lightId) {
light.Enabled = false;
}
}
std::cout << "[DEBUG] SyncScene 3 starting..." << std::endl;
bridge.SyncScene(snapshot, nullptr, false, false);
std::cout << "[DEBUG] SyncScene 3 completed." << std::endl;
bool hasLightFinal = bridge.VerifyLightExistsForTesting(lightId);
std::cout << "[DEBUG] hasLightFinal: " << hasLightFinal << std::endl;
MetaCoreExpect(!hasLightFinal, "再次禁用光源后自定义光源实体应被正确销毁移出");
float defaultLightIntensityFinal = bridge.GetDefaultLightIntensityForTesting();
std::cout << "[DEBUG] defaultLightIntensityFinal: " << defaultLightIntensityFinal << std::endl;
MetaCoreExpect(defaultLightIntensityFinal > 99000.0F, "再次无激活光源时,默认灯光应重新亮起兜底");
// 9.5 模拟删除网格对象,验证其在 FilamentSceneBridge 中被成功卸载
std::cout << "[DEBUG] Deleting mesh object..." << std::endl;
for (auto it = snapshot.Renderables.begin(); it != snapshot.Renderables.end(); ) {
if (it->ObjectId == cubeId) {
it = snapshot.Renderables.erase(it);
} else {
++it;
}
}
std::cout << "[DEBUG] SyncScene 4 starting..." << std::endl;
bridge.SyncScene(snapshot, nullptr, false, false);
std::cout << "[DEBUG] SyncScene 4 completed." << std::endl;
glm::mat4 deletedWorldMatrix;
bool hasCubeAfterDelete = bridge.TryGetObjectWorldMatrix(cubeId, deletedWorldMatrix);
std::cout << "[DEBUG] hasCubeAfterDelete: " << hasCubeAfterDelete << std::endl;
MetaCoreExpect(!hasCubeAfterDelete, "删除网格对象后,其在桥接器映射中不应再存在");
// 10. 资源清理
std::cout << "[DEBUG] bridge.Shutdown starting..." << std::endl;
bridge.Shutdown();
std::cout << "[DEBUG] bridge.Shutdown completed." << std::endl;
window.Shutdown();
}
void MetaCoreTestNestedModelHierarchyTransformSync() {
const std::filesystem::path projectRoot = MetaCoreTestRepositoryRoot() / "TestProject";
const std::filesystem::path modelRelativePath = std::filesystem::path("Assets") / "Models" / "Cube.glb";
const std::string modelRelativePathString = modelRelativePath.generic_string();
// 1. 创建测试场景,建立嵌套模型和父子变换结构
MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene();
// 模拟根模型
MetaCore::MetaCoreGameObject parentModel = scene.CreateGameObject("ParentModel");
auto& parentMesh = parentModel.AddComponent<MetaCore::MetaCoreMeshRendererComponent>();
parentMesh.MeshSource = MetaCore::MetaCoreMeshSourceKind::Asset;
parentMesh.SourceModelPath = modelRelativePathString;
parentMesh.SourceModelAssetGuid = MetaCore::MetaCoreAssetRegistry::Get().ResolvePathToGuid(modelRelativePathString);
parentModel.AddComponent<MetaCore::MetaCoreModelRootTag>(MetaCore::MetaCoreModelRootTag{ modelRelativePathString });
parentModel.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(0.0F, 1.0F, 0.0F);
// 模拟子模型 (嵌套相同模型,其 parent 为 parentModel)
MetaCore::MetaCoreGameObject childModel = scene.CreateGameObject("ChildModel", parentModel.GetId());
auto& childMesh = childModel.AddComponent<MetaCore::MetaCoreMeshRendererComponent>();
childMesh.MeshSource = MetaCore::MetaCoreMeshSourceKind::Asset;
childMesh.SourceModelPath = modelRelativePathString;
childMesh.SourceModelAssetGuid = MetaCore::MetaCoreAssetRegistry::Get().ResolvePathToGuid(modelRelativePathString);
childModel.AddComponent<MetaCore::MetaCoreModelRootTag>(MetaCore::MetaCoreModelRootTag{ modelRelativePathString });
childModel.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(2.0F, 0.0F, 0.0F); // 局部偏移 x + 2.0
MetaCore::MetaCoreSceneRenderSync renderSync;
MetaCore::MetaCoreSceneRenderSyncSnapshot snapshot = renderSync.BuildSnapshot(scene);
// 验证 Bug 2 修复:子模型的 HostRootId 应该为它自己,而不是父模型
bool foundChild = false;
for (const auto& renderable : snapshot.Renderables) {
if (renderable.ObjectId == childModel.GetId()) {
foundChild = true;
MetaCoreExpect(renderable.HostRootId == childModel.GetId(), "子模型的 HostRootId 必须等于其自身的 ID不能被错判为父模型 ID");
}
}
MetaCoreExpect(foundChild, "快照的渲染实体列表中应包含 ChildModel");
// 2. 初始化桥接器
MetaCore::MetaCoreWindow window;
bool winOk = window.Initialize(800, 600, "NestedModelHierarchyTestWindow");
MetaCoreExpect(winOk, "测试窗口初始化成功");
MetaCore::MetaCoreFilamentSceneBridge bridge;
bridge.SetProjectRootPath(projectRoot);
bool bridgeOk = bridge.Initialize(window);
MetaCoreExpect(bridgeOk, "测试桥接器初始化成功");
// 同步快照
bridge.SyncScene(snapshot, nullptr, false, false);
// 验证 Bug 1 修复:在没有 Filament 层级关系下,子模型在 Filament 侧的变换矩阵应当正确跟随父模型的世界矩阵
// 子模型局部矩阵
glm::mat4 childLocalMat = glm::translate(glm::mat4(1.0F), glm::vec3(2.0F, 0.0F, 0.0F));
// 父模型世界矩阵
glm::mat4 parentWorldMat = glm::translate(glm::mat4(1.0F), glm::vec3(0.0F, 1.0F, 0.0F));
// 预期的子模型世界矩阵
glm::mat4 expectedChildWorldMat = parentWorldMat * childLocalMat;
bool transOk = bridge.VerifyTransformForTesting(childModel.GetId(), expectedChildWorldMat);
MetaCoreExpect(transOk, "子模型在 Filament 侧的最终变换矩阵应正确应用世界矩阵");
// 3. 拖拽父级:修改父模型位置,重新同步,并校验子模型是否跟着移动
parentModel.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(10.0F, 5.0F, 0.0F);
// 重新构建快照与同步
snapshot = renderSync.BuildSnapshot(scene);
bridge.SyncScene(snapshot, nullptr, false, false);
glm::mat4 newParentWorldMat = glm::translate(glm::mat4(1.0F), glm::vec3(10.0F, 5.0F, 0.0F));
glm::mat4 newExpectedChildWorldMat = newParentWorldMat * childLocalMat;
bool transOk2 = bridge.VerifyTransformForTesting(childModel.GetId(), newExpectedChildWorldMat);
MetaCoreExpect(transOk2, "拖拽父级后,子模型在 Filament 侧的最终变换矩阵应实时跟随父模型世界矩阵更新");
bridge.Shutdown();
window.Shutdown();
}
void MetaCoreTestP3ProductivityStage() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreTestP3Project";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets");
std::filesystem::create_directories(tempProjectRoot / "Scenes");
std::filesystem::create_directories(tempProjectRoot / "Library");
// 1. 写入默认的 MetaCore.project.json
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
projectFile << "{\n"
<< " \"name\": \"P3Project\",\n"
<< " \"version\": \"0.1.0\",\n"
<< " \"scenes\": [],\n"
<< " \"startup_scene\": \"\"\n"
<< "}\n";
}
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
const auto assetDatabase = moduleRegistry.ResolveService<MetaCore::MetaCoreIAssetDatabaseService>();
MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService");
MetaCoreExpect(assetDatabase->HasProject(), "应成功打开项目");
// 2. 测试资产自动导入与默认目录 (P3.2)
// 写入一个没有 .mcmeta 的 .ktx 贴图文件
const std::filesystem::path texturePath = tempProjectRoot / "Assets" / "test_texture.ktx";
{
std::ofstream textureFile(texturePath, std::ios::trunc);
textureFile << "dummy-ktx-data";
}
assetDatabase->Refresh();
// 验证是否在 Assets 下自动生成了 test_texture.ktx.mcmeta 物理元文件
const std::filesystem::path metaPath = tempProjectRoot / "Assets" / "test_texture.ktx.mcmeta";
MetaCoreExpect(std::filesystem::exists(metaPath), "Refresh 后应生成 test_texture.ktx.mcmeta 物理元文件");
// 读取该 .mcmeta 文件内容,校验结构
std::ifstream metaFile(metaPath);
nlohmann::json metaJson;
metaFile >> metaJson;
metaFile.close();
MetaCoreExpect(metaJson.contains("guid"), "元文件应包含 guid");
MetaCoreExpect(metaJson.contains("asset_type") && metaJson["asset_type"] == "texture", "元文件 asset_type 应为 texture");
MetaCoreExpect(metaJson.contains("importer_id") && metaJson["importer_id"] == "TextureImporter", "元文件 importer_id 应为 TextureImporter");
const std::string textureGuidStr = metaJson["guid"].get<std::string>();
MetaCore::MetaCoreAssetGuid textureGuid = MetaCore::MetaCoreAssetGuid::Parse(textureGuidStr).value_or(MetaCore::MetaCoreAssetGuid{});
MetaCoreExpect(textureGuid.IsValid(), "自动分配的材质 GUID 应为有效 GUID");
// 3. 测试资产重命名与删除 (P3.1, P3.4)
// 重命名该贴图文件
MetaCoreExpect(assetDatabase->RenamePath(std::filesystem::path("Assets") / "test_texture.ktx", "renamed_texture.ktx"), "应能重命名贴图文件");
const std::filesystem::path renamedTexturePath = tempProjectRoot / "Assets" / "renamed_texture.ktx";
const std::filesystem::path renamedMetaPath = tempProjectRoot / "Assets" / "renamed_texture.ktx.mcmeta";
MetaCoreExpect(std::filesystem::exists(renamedTexturePath), "重命名后新贴图文件应存在");
MetaCoreExpect(!std::filesystem::exists(texturePath), "重命名后旧贴图文件不应存在");
MetaCoreExpect(std::filesystem::exists(renamedMetaPath), "重命名后新 mcmeta 文件应存在");
MetaCoreExpect(!std::filesystem::exists(metaPath), "重命名后旧 mcmeta 文件不应存在");
// 删除重命名后的贴图文件
MetaCoreExpect(assetDatabase->DeletePath(std::filesystem::path("Assets") / "renamed_texture.ktx"), "应能删除贴图文件");
MetaCoreExpect(!std::filesystem::exists(renamedTexturePath), "删除后贴图文件应不存在");
MetaCoreExpect(!std::filesystem::exists(renamedMetaPath), "删除后 mcmeta 文件应不存在");
// 4. 测试材质创建、加载、编辑保存及物理 Hash 更新 (P3.4)
// 保存一个初始材质
const std::filesystem::path materialPath = tempProjectRoot / "Assets" / "test_material.mcmaterial.json";
MetaCore::MetaCoreMaterialAssetDocument materialDoc;
materialDoc.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate();
materialDoc.Name = "test_material";
materialDoc.BaseColor = glm::vec3(1.0F, 0.0F, 0.0F);
materialDoc.Metallic = 0.5F;
materialDoc.Roughness = 0.8F;
MetaCore::MetaCoreTypeRegistry registry;
MetaCoreRegisterFoundationGeneratedTypes(registry);
MetaCoreRegisterSceneGeneratedTypes(registry);
MetaCoreExpect(
MetaCore::MetaCoreSceneSerializer::SaveMaterialToJson(materialPath, materialDoc, registry),
"应能保存材质文件"
);
// 自动刷新生成 .mcmeta 物理元文件
assetDatabase->Refresh();
const std::filesystem::path materialMetaPath = tempProjectRoot / "Assets" / "test_material.mcmaterial.json.mcmeta";
MetaCoreExpect(std::filesystem::exists(materialMetaPath), "Refresh 后应生成材质的 mcmeta 元文件");
// 编辑材质属性并重新保存
materialDoc.BaseColor = glm::vec3(0.0F, 1.0F, 0.0F);
materialDoc.Metallic = 0.9F;
materialDoc.Roughness = 0.1F;
MetaCoreExpect(
MetaCore::MetaCoreSceneSerializer::SaveMaterialToJson(materialPath, materialDoc, registry),
"应能重新保存修改后的材质文件"
);
// 模拟材质编辑器的物理 Hash 保存更新逻辑
std::uint64_t initialHash = 0;
{
std::ifstream input(materialMetaPath);
nlohmann::json json;
input >> json;
input.close();
if (json.contains("source_hash")) {
initialHash = json["source_hash"].get<std::uint64_t>();
}
}
// 更新 Hash
const std::uint64_t newHash = MetaCore::MetaCoreHashFile(materialPath).value_or(0);
{
std::ifstream input(materialMetaPath);
nlohmann::json json;
input >> json;
input.close();
json["source_hash"] = newHash;
std::ofstream output(materialMetaPath);
output << json.dump(4);
output.close();
}
// 读回以校验
std::uint64_t savedHash = 0;
{
std::ifstream input(materialMetaPath);
nlohmann::json json;
input >> json;
input.close();
if (json.contains("source_hash")) {
savedHash = json["source_hash"].get<std::uint64_t>();
}
}
MetaCoreExpect(savedHash == newHash, "保存并更新后,材质 mcmeta 物理哈希值应被更新且一致");
// 5. 测试拖拽绑定到 Mesh 槽位 (P3.3)
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreGameObject gameObject = scene.CreateGameObject("MeshObject");
auto& meshRenderer = gameObject.AddComponent<MetaCore::MetaCoreMeshRendererComponent>();
// 模拟槽位绑定
if (meshRenderer.MaterialAssetGuids.empty()) {
meshRenderer.MaterialAssetGuids.resize(1);
}
meshRenderer.MaterialAssetGuids[0] = materialDoc.AssetGuid;
MetaCoreExpect(meshRenderer.MaterialAssetGuids[0] == materialDoc.AssetGuid, "拖拽绑定后槽位 0 材质 Guid 应正确");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestAssetDependencyGraphAndRuntimeRegistry() {
auto& graph = MetaCore::MetaCoreAssetDependencyGraph::Get();
graph.Clear();
const MetaCore::MetaCoreAssetGuid modelGuid = MetaCore::MetaCoreAssetGuid::Generate();
const MetaCore::MetaCoreAssetGuid materialGuid = MetaCore::MetaCoreAssetGuid::Generate();
const MetaCore::MetaCoreAssetGuid textureGuid = MetaCore::MetaCoreAssetGuid::Generate();
graph.SetDependencies(modelGuid, {materialGuid});
graph.SetDependencies(materialGuid, {textureGuid});
MetaCoreExpect(graph.GetDependencies(modelGuid).contains(materialGuid), "依赖图应记录正向依赖");
MetaCoreExpect(graph.GetDependents(textureGuid).contains(materialGuid), "依赖图应记录反向依赖");
graph.SetDependencies(modelGuid, {textureGuid});
MetaCoreExpect(!graph.GetDependents(materialGuid).contains(modelGuid), "替换依赖后旧反向边应移除");
MetaCoreExpect(graph.GetDependents(textureGuid).contains(modelGuid), "替换依赖后新反向边应建立");
graph.RemoveAsset(textureGuid);
MetaCoreExpect(graph.GetDependencies(modelGuid).empty(), "删除资源后引用它的边应移除");
auto& runtimeRegistry = MetaCore::MetaCoreRuntimeAssetRegistry::Get();
runtimeRegistry.Clear();
int callbackCount = 0;
const auto subscription = runtimeRegistry.Subscribe([&](const MetaCore::MetaCoreAssetChangeEvent& event) {
if (event.AssetGuid == modelGuid) {
++callbackCount;
}
});
runtimeRegistry.QueueEvent(MetaCore::MetaCoreAssetChangeEvent{
MetaCore::MetaCoreAssetChangeType::Modified,
modelGuid,
modelGuid,
"Assets/Model.glb",
"Assets/Model.glb"
});
MetaCoreExpect(callbackCount == 0, "运行时失效事件应先进入队列");
runtimeRegistry.DispatchQueuedEvents();
MetaCoreExpect(callbackCount == 1, "主线程分发后运行时失效订阅者应收到事件");
runtimeRegistry.Unsubscribe(subscription);
graph.Clear();
}
void MetaCoreTestAssetRegistryPathIdentity() {
auto& registry = MetaCore::MetaCoreAssetRegistry::Get();
registry.Clear();
const MetaCore::MetaCoreAssetGuid modelGuid = MetaCore::MetaCoreAssetGuid::Generate();
const MetaCore::MetaCoreAssetGuid meshGuid = MetaCore::MetaCoreAssetGuid::Generate();
MetaCoreExpect(registry.RegisterAsset(modelGuid, "Assets/Models/CaseModel.glb"), "主资源应能注册");
MetaCoreExpect(registry.RegisterSubAsset(meshGuid, "Assets/Models/CaseModel.glb"), "子资源应能注册");
MetaCoreExpect(registry.ResolvePathToGuid("Assets/Models/CaseModel.glb") == modelGuid, "子资源不应覆盖主资源路径反查");
MetaCoreExpect(registry.ResolveGuidToPath(meshGuid) == std::filesystem::path("Assets/Models/CaseModel.glb"), "子资源 GUID 应解析到源文件");
#if defined(_WIN32)
MetaCoreExpect(registry.ResolvePathToGuid("assets/models/casemodel.glb") == modelGuid, "Windows 路径反查应忽略 ASCII 大小写");
#else
MetaCoreExpect(!registry.ResolvePathToGuid("assets/models/casemodel.glb").IsValid(), "Linux 路径反查应保留大小写");
#endif
MetaCoreExpect(registry.MoveAsset(modelGuid, "Assets/Models/Moved.glb"), "主资源路径应能移动");
MetaCoreExpect(!registry.ResolvePathToGuid("Assets/Models/CaseModel.glb").IsValid(), "移动后旧路径应失效");
MetaCoreExpect(registry.ResolvePathToGuid("Assets/Models/Moved.glb") == modelGuid, "移动后新路径应生效");
registry.Clear();
}
void MetaCoreTestModelMetadataKeepsSubAssetGuidsAfterMove() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreStableSubAssetMoveProject";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets" / "Moved");
std::filesystem::create_directories(tempProjectRoot / "Scenes");
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
projectFile << "{\"name\":\"StableMove\",\"version\":\"0.1.0\",\"scenes\":[],\"startup_scene\":\"\"}";
}
{
std::ofstream modelFile(tempProjectRoot / "Assets" / "Pump.gltf", std::ios::trunc);
modelFile << "{\"meshes\":[{\"name\":\"PumpMesh\"}],\"materials\":[{\"name\":\"PumpMaterial\"}],\"images\":[{\"uri\":\"Textures/PumpBaseColor.png\"}]}";
}
_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 importPipeline = moduleRegistry.ResolveService<MetaCore::MetaCoreIImportPipelineService>();
MetaCoreExpect(assetDatabase != nullptr && importPipeline != nullptr, "应解析到资源服务");
const auto sourceRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Pump.gltf");
MetaCoreExpect(sourceRecord.has_value(), "应导入 Pump 模型");
nlohmann::json beforeJson;
{
std::ifstream input(tempProjectRoot / "Assets" / "Pump.gltf.mcmeta");
input >> beforeJson;
}
MetaCoreExpect(beforeJson.contains("sub_assets") && !beforeJson["sub_assets"].empty(), "模型元数据应包含子资源");
std::unordered_map<std::string, std::string> guidByStableKey;
for (const auto& subAsset : beforeJson["sub_assets"]) {
MetaCoreExpect(subAsset.contains("stable_import_key"), "子资源元数据应写入 stable_import_key");
guidByStableKey[subAsset["stable_import_key"].get<std::string>()] = subAsset["guid"].get<std::string>();
}
MetaCoreExpect(assetDatabase->MovePath(std::filesystem::path("Assets") / "Pump.gltf", std::filesystem::path("Assets") / "Moved"), "应移动模型资源");
MetaCoreExpect(importPipeline->ReimportAsset(sourceRecord->Guid), "移动后应能强制重导模型");
MetaCoreExpect(assetDatabase->Refresh(), "重导后应能刷新数据库");
nlohmann::json afterJson;
{
std::ifstream input(tempProjectRoot / "Assets" / "Moved" / "Pump.gltf.mcmeta");
input >> afterJson;
}
MetaCoreExpect(afterJson["source_path"] == "Assets/Moved/Pump.gltf", "移动后 mcmeta source_path 应更新");
for (const auto& subAsset : afterJson["sub_assets"]) {
const std::string stableKey = subAsset["stable_import_key"].get<std::string>();
MetaCoreExpect(guidByStableKey.at(stableKey) == subAsset["guid"].get<std::string>(), "移动并重导后子资源 GUID 应保持");
}
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestHotReloadQueuesExternalMoveUntilTick() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreHotReloadMoveProject";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets");
std::filesystem::create_directories(tempProjectRoot / "Scenes");
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
projectFile << "{\"name\":\"HotReloadMove\",\"version\":\"0.1.0\",\"scenes\":[],\"startup_scene\":\"\"}";
}
{
std::ofstream sourceFile(tempProjectRoot / "Assets" / "Original.dat", std::ios::trunc);
sourceFile << "stable move payload";
}
_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 hotReload = moduleRegistry.ResolveService<MetaCore::MetaCoreIAssetHotReloadService>();
MetaCoreExpect(assetDatabase != nullptr && hotReload != nullptr, "应解析到热更新服务");
const auto originalRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Original.dat");
MetaCoreExpect(originalRecord.has_value(), "应导入原始文件");
std::filesystem::rename(tempProjectRoot / "Assets" / "Original.dat", tempProjectRoot / "Assets" / "Renamed.dat");
std::this_thread::sleep_for(std::chrono::milliseconds(1300));
MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / "Assets" / "Renamed.dat.mcmeta"), "后台监听线程不应直接移动 mcmeta");
hotReload->Tick();
const auto renamedRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Renamed.dat");
MetaCoreExpect(renamedRecord.has_value(), "主线程 Tick 后应注册外部移动后的文件");
MetaCoreExpect(renamedRecord->Guid == originalRecord->Guid, "外部移动后应保留资源 GUID");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / "Assets" / "Renamed.dat.mcmeta"), "主线程 Tick 后应移动 mcmeta");
MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / "Assets" / "Original.dat.mcmeta"), "外部移动后旧 mcmeta 应消失");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
} // namespace
int main() {
std::setvbuf(stdout, nullptr, _IONBF, 0);
std::setvbuf(stderr, nullptr, _IONBF, 0);
std::cout << "[RUN] MetaCoreCreateDefaultScene..." << std::endl;
MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene();
std::cout << "[RUN] MetaCoreExpect default objects count..." << std::endl;
MetaCoreExpect(scene.GetGameObjects().size() == 2, "默认场景应包含两个对象");
bool hasCamera = false;
bool hasLight = false;
for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) {
hasCamera = hasCamera || object.HasComponent<MetaCore::MetaCoreCameraComponent>();
hasLight = hasLight || object.HasComponent<MetaCore::MetaCoreLightComponent>();
}
MetaCoreExpect(hasCamera, "默认场景应包含摄像机");
MetaCoreExpect(hasLight, "默认场景应包含光源");
std::cout << "[RUN] MetaCoreDummyPanelProvider test..." << std::endl;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreDummyPanelProvider>());
MetaCoreExpect(!moduleRegistry.AccessPanelOpenState("Dummy"), "面板默认状态应为关闭");
std::cout << "[RUN] MetaCoreLogService test..." << std::endl;
MetaCore::MetaCoreLogService logService;
logService.AddEntry(MetaCore::MetaCoreLogLevel::Info, "Test", "Smoke");
MetaCoreExpect(logService.GetEntries().size() == 1, "日志服务应记录一条日志");
std::cout << "[RUN] MetaCoreTestAssetDependencyGraphAndRuntimeRegistry..." << std::endl;
MetaCoreTestAssetDependencyGraphAndRuntimeRegistry();
std::cout << "[RUN] MetaCoreTestAssetRegistryPathIdentity..." << std::endl;
MetaCoreTestAssetRegistryPathIdentity();
std::cout << "[RUN] MetaCoreTestModelMetadataKeepsSubAssetGuidsAfterMove..." << std::endl;
MetaCoreTestModelMetadataKeepsSubAssetGuidsAfterMove();
std::cout << "[RUN] MetaCoreTestHotReloadQueuesExternalMoveUntilTick..." << std::endl;
MetaCoreTestHotReloadQueuesExternalMoveUntilTick();
std::cout << "[RUN] MetaCoreTestSceneEditApi..." << std::endl;
MetaCoreTestSceneEditApi();
std::cout << "[RUN] MetaCoreTestSelectionStateMachine..." << std::endl;
MetaCoreTestSelectionStateMachine();
std::cout << "[RUN] MetaCoreTestUndoRedo..." << std::endl;
MetaCoreTestUndoRedo();
std::cout << "[RUN] MetaCoreTestBuiltinModuleComposition..." << std::endl;
MetaCoreTestBuiltinModuleComposition();
std::cout << "[RUN] MetaCoreTestLegacyBinarySceneStartupLoadCompatibility..." << std::endl;
MetaCoreTestLegacyBinarySceneStartupLoadCompatibility();
std::cout << "[RUN] MetaCoreTestProjectFileRoundTripAndDiscovery..." << std::endl;
MetaCoreTestProjectFileRoundTripAndDiscovery();
std::cout << "[RUN] MetaCoreTestAssetDatabaseCreateAndOpenProject..." << std::endl;
MetaCoreTestAssetDatabaseCreateAndOpenProject();
std::cout << "[RUN] MetaCoreTestAssetDatabaseRenamePathUpdatesProjectDescriptor..." << std::endl;
MetaCoreTestAssetDatabaseRenamePathUpdatesProjectDescriptor();
std::cout << "[RUN] MetaCoreTestAssetDatabaseMovePathUpdatesProjectDescriptor..." << std::endl;
MetaCoreTestAssetDatabaseMovePathUpdatesProjectDescriptor();
std::cout << "[RUN] MetaCoreTestProjectAssetDatabaseOperations..." << std::endl;
MetaCoreTestProjectAssetDatabaseOperations();
std::cout << "[RUN] MetaCoreTestComponentRegistryDescriptors..." << std::endl;
MetaCoreTestComponentRegistryDescriptors();
std::cout << "[RUN] MetaCoreTestSceneEditingServiceCreateCamera..." << std::endl;
MetaCoreTestSceneEditingServiceCreateCamera();
std::cout << "[RUN] MetaCoreTestPlayModeStateMachineAndLifecycle..." << std::endl;
MetaCoreTestPlayModeStateMachineAndLifecycle();
std::cout << "[RUN] MetaCoreTestPlayModeRotatorRestoresPrePlaySnapshotOnStop..." << std::endl;
MetaCoreTestPlayModeRotatorRestoresPrePlaySnapshotOnStop();
std::cout << "[RUN] MetaCoreTestNativeScriptRegistryAndDefaults..." << std::endl;
MetaCoreTestNativeScriptRegistryAndDefaults();
std::cout << "[RUN] MetaCoreTestScriptComponentSerializationSnapshotAndJson..." << std::endl;
MetaCoreTestScriptComponentSerializationSnapshotAndJson();
std::cout << "[RUN] MetaCoreTestNativeScriptLifecycle..." << std::endl;
MetaCoreTestNativeScriptLifecycle();
std::cout << "[RUN] MetaCoreTestNativeScriptBehavioursRestorePrePlaySnapshotOnStop..." << std::endl;
MetaCoreTestNativeScriptBehavioursRestorePrePlaySnapshotOnStop();
std::cout << "[RUN] MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot..." << std::endl;
MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot();
std::cout << "[RUN] MetaCoreTestCameraComponentJsonRoundTripAndLegacyDefaults..." << std::endl;
MetaCoreTestCameraComponentJsonRoundTripAndLegacyDefaults();
std::cout << "[RUN] MetaCoreTestCameraSelectionAndProjectionUtilities..." << std::endl;
MetaCoreTestCameraSelectionAndProjectionUtilities();
std::cout << "[RUN] MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson..." << std::endl;
MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson();
std::cout << "[RUN] MetaCoreTestImportPipelineAndCook..." << std::endl;
MetaCoreTestImportPipelineAndCook();
std::cout << "[RUN] MetaCoreTestProjectDescriptorAndGltfImporterSkeleton..." << std::endl;
MetaCoreTestProjectDescriptorAndGltfImporterSkeleton();
std::cout << "[RUN] MetaCoreTestInstantiateImportedModelAssetIntoScene..." << std::endl;
MetaCoreTestInstantiateImportedModelAssetIntoScene();
std::cout << "[RUN] MetaCoreTestInstantiateRealGltfHierarchyIntoScene..." << std::endl;
MetaCoreTestInstantiateRealGltfHierarchyIntoScene();
std::cout << "[RUN] MetaCoreTestInstantiateMultiNodeModelAssetIntoScene..." << std::endl;
MetaCoreTestInstantiateMultiNodeModelAssetIntoScene();
std::cout << "[RUN] MetaCoreTestPrefabWorkflow..." << std::endl;
MetaCoreTestPrefabWorkflow();
std::cout << "[RUN] MetaCoreTestDeleteModelAssetCorrectlyClearsFromViewport..." << std::endl;
MetaCoreTestDeleteModelAssetCorrectlyClearsFromViewport();
std::cout << "[RUN] MetaCoreTestDeleteModelSubChildCorrectlyClearsFromViewport..." << std::endl;
MetaCoreTestDeleteModelSubChildCorrectlyClearsFromViewport();
std::cout << "[RUN] MetaCoreTestModelReimportKeepsGeneratedAssetReferencesStable..." << std::endl;
MetaCoreTestModelReimportKeepsGeneratedAssetReferencesStable();
std::cout << "[RUN] MetaCoreTestBootstrapStartupSceneCreation..." << std::endl;
MetaCoreTestBootstrapStartupSceneCreation();
std::cout << "[RUN] MetaCoreTestCookPipelineCooksJsonScenePrefabMaterialUi..." << std::endl;
MetaCoreTestCookPipelineCooksJsonScenePrefabMaterialUi();
std::cout << "[RUN] MetaCoreTestComponentRegistryOperations..." << std::endl;
MetaCoreTestComponentRegistryOperations();
std::cout << "[RUN] MetaCoreTestRuntimeDataTypeSerialization..." << std::endl;
MetaCoreTestRuntimeDataTypeSerialization();
std::cout << "[RUN] MetaCoreTestRuntimeDataProjectDocumentSerialization..." << std::endl;
MetaCoreTestRuntimeDataProjectDocumentSerialization();
std::cout << "[RUN] MetaCoreTestMeshRendererResourceSerialization..." << std::endl;
MetaCoreTestMeshRendererResourceSerialization();
std::cout << "[RUN] MetaCoreTestRuntimeDataBinaryDocumentIo..." << std::endl;
MetaCoreTestRuntimeDataBinaryDocumentIo();
std::cout << "[RUN] MetaCoreTestRuntimeProjectDocumentIo..." << std::endl;
MetaCoreTestRuntimeProjectDocumentIo();
std::cout << "[RUN] MetaCoreTestRuntimeDiagnosticsSnapshotIo..." << std::endl;
MetaCoreTestRuntimeDiagnosticsSnapshotIo();
std::cout << "[RUN] MetaCoreTestRuntimeDataDispatcherConstruction..." << std::endl;
MetaCoreTestRuntimeDataDispatcherConstruction();
std::cout << "[RUN] MetaCoreTestRuntimeDataDispatcherAppliesUpdates..." << std::endl;
MetaCoreTestRuntimeDataDispatcherAppliesUpdates();
std::cout << "[RUN] MetaCoreTestMockRuntimeDataSourceAdapterEmitsUpdates..." << std::endl;
MetaCoreTestMockRuntimeDataSourceAdapterEmitsUpdates();
std::cout << "[RUN] MetaCoreTestRuntimeDataDispatcherMarksStaleBindings..." << std::endl;
MetaCoreTestRuntimeDataDispatcherMarksStaleBindings();
std::cout << "[RUN] MetaCoreTestMockRuntimeDataSourceAdapterDegradedState..." << std::endl;
MetaCoreTestMockRuntimeDataSourceAdapterDegradedState();
std::cout << "[RUN] MetaCoreTestFileReplayRuntimeDataSourceAdapterEmitsReplayFrames..." << std::endl;
MetaCoreTestFileReplayRuntimeDataSourceAdapterEmitsReplayFrames();
std::cout << "[RUN] MetaCoreTestRuntimeDiagnosticsSnapshotReportsFaults..." << std::endl;
MetaCoreTestRuntimeDiagnosticsSnapshotReportsFaults();
std::cout << "[RUN] MetaCoreTestRuntimeDataBinaryDocumentRejectsCorruption..." << std::endl;
MetaCoreTestRuntimeDataBinaryDocumentRejectsCorruption();
std::cout << "[RUN] MetaCoreTestEditorContextRuntimeDataConfigSaveLoad..." << std::endl;
MetaCoreTestEditorContextRuntimeDataConfigSaveLoad();
std::cout << "[RUN] MetaCoreTestRuntimeDataConfigValidationRejectsBrokenBinding..." << std::endl;
MetaCoreTestRuntimeDataConfigValidationRejectsBrokenBinding();
std::cout << "[RUN] MetaCoreTestRuntimeDataConfigValidationRejectsMissingReplayFilePath..." << std::endl;
MetaCoreTestRuntimeDataConfigValidationRejectsMissingReplayFilePath();
std::cout << "[RUN] MetaCoreTestRuntimeDataConfigValidationRejectsMissingTcpPort..." << std::endl;
MetaCoreTestRuntimeDataConfigValidationRejectsMissingTcpPort();
std::cout << "[RUN] MetaCoreTestEditorContextRejectsInvalidRuntimeDataSave..." << std::endl;
MetaCoreTestEditorContextRejectsInvalidRuntimeDataSave();
std::cout << "[RUN] MetaCoreTestTcpRuntimeDataSourceAdapterReadsSocketStream..." << std::endl;
MetaCoreTestTcpRuntimeDataSourceAdapterReadsSocketStream();
std::cout << "[RUN] MetaCoreTestUiDocumentSerialization..." << std::endl;
MetaCoreTestUiDocumentSerialization();
std::cout << "[RUN] MetaCoreTestDecoupledSnapshotSync..." << std::endl;
MetaCoreTestDecoupledSnapshotSync();
std::cout << "[RUN] MetaCoreTestNestedModelHierarchyTransformSync..." << std::endl;
MetaCoreTestNestedModelHierarchyTransformSync();
std::cout << "[RUN] MetaCoreTestP3ProductivityStage..." << std::endl;
MetaCoreTestP3ProductivityStage();
std::cout << "MetaCoreSmokeTests passed\n";
return 0;
}