5263 lines
276 KiB
C++
5263 lines
276 KiB
C++
#include "MetaCoreEditor/MetaCoreBuiltinModules.h"
|
||
#include "MetaCoreEditor/MetaCoreEditorAssetTypes.h"
|
||
#include "MetaCoreEditor/MetaCoreEditorContext.h"
|
||
#include "MetaCoreEditor/MetaCoreEditorModule.h"
|
||
#include "MetaCoreEditor/MetaCoreEditorServices.h"
|
||
#include "MetaCoreEditor/MetaCoreSceneInteractionService.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::size_t MetaCoreCountSceneLights(const MetaCore::MetaCoreScene& scene) {
|
||
std::size_t lightCount = 0;
|
||
for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) {
|
||
if (object.HasComponent<MetaCore::MetaCoreLightComponent>()) {
|
||
++lightCount;
|
||
}
|
||
}
|
||
return lightCount;
|
||
}
|
||
|
||
[[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 MetaCoreTestProjectSkeletonHelpers() {
|
||
const std::filesystem::path tempProjectRoot =
|
||
std::filesystem::temp_directory_path() / "MetaCoreProjectSkeletonHelpers";
|
||
const std::filesystem::path createdProjectRoot = tempProjectRoot / "CreatedBySkeleton";
|
||
std::filesystem::remove_all(tempProjectRoot);
|
||
|
||
MetaCoreExpect(
|
||
MetaCore::MetaCoreCreateProjectSkeleton(createdProjectRoot, "CreatedBySkeleton"),
|
||
"项目骨架 helper 应能创建项目"
|
||
);
|
||
MetaCoreExpect(std::filesystem::exists(createdProjectRoot / "MetaCore.project.json"), "项目骨架应包含项目文件");
|
||
MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets"), "项目骨架应包含 Assets");
|
||
MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Scenes"), "项目骨架应包含 Scenes");
|
||
MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Runtime"), "项目骨架应包含 Runtime");
|
||
MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Ui"), "项目骨架应包含 Ui");
|
||
MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Library"), "项目骨架应包含 Library");
|
||
MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Build"), "项目骨架应包含 Build");
|
||
MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Models"), "项目骨架应包含 Models");
|
||
MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Materials"), "项目骨架应包含 Materials");
|
||
MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Textures"), "项目骨架应包含 Textures");
|
||
MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Prefabs"), "项目骨架应包含 Prefabs");
|
||
MetaCoreExpect(std::filesystem::exists(createdProjectRoot / "Ui" / "Hud.rml"), "项目骨架应包含默认 HUD RML");
|
||
MetaCoreExpect(std::filesystem::exists(createdProjectRoot / "Ui" / "Hud.rcss"), "项目骨架应包含默认 HUD RCSS");
|
||
|
||
const auto projectDocument = MetaCore::MetaCoreReadProjectFile(createdProjectRoot / "MetaCore.project.json");
|
||
MetaCoreExpect(projectDocument.has_value(), "项目骨架项目文件应能读取");
|
||
MetaCoreExpect(projectDocument->Name == "CreatedBySkeleton", "项目骨架项目名称应正确");
|
||
MetaCoreExpect(
|
||
projectDocument->StartupScenePath == std::filesystem::path("Scenes") / "Main.mcscene.json",
|
||
"项目骨架默认启动场景路径应正确"
|
||
);
|
||
|
||
const auto resolvedFromRoot = MetaCore::MetaCoreResolveProjectRootCandidate(createdProjectRoot);
|
||
MetaCoreExpect(resolvedFromRoot.has_value(), "项目根路径应可解析");
|
||
MetaCoreExpect(
|
||
std::filesystem::equivalent(*resolvedFromRoot, createdProjectRoot),
|
||
"项目根路径解析结果应正确"
|
||
);
|
||
|
||
const auto resolvedFromProjectFile =
|
||
MetaCore::MetaCoreResolveProjectRootCandidate(createdProjectRoot / "MetaCore.project.json");
|
||
MetaCoreExpect(resolvedFromProjectFile.has_value(), "项目文件路径应可解析");
|
||
MetaCoreExpect(
|
||
std::filesystem::equivalent(*resolvedFromProjectFile, createdProjectRoot),
|
||
"项目文件路径解析结果应正确"
|
||
);
|
||
|
||
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(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Models"), "创建后应存在模型目录");
|
||
MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Materials"), "创建后应存在材质目录");
|
||
MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Textures"), "创建后应存在贴图目录");
|
||
MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Prefabs"), "创建后应存在 Prefabs 目录");
|
||
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 / "Ui");
|
||
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";
|
||
}
|
||
{
|
||
std::ofstream hudFile(tempProjectRoot / "Ui" / "Hud.rml", std::ios::trunc);
|
||
hudFile << "<rml><head><link type=\"text/rcss\" href=\"Hud.rcss\" /></head><body><div id=\"hud\">HUD</div></body></rml>";
|
||
}
|
||
{
|
||
std::ofstream styleFile(tempProjectRoot / "Ui" / "Hud.rcss", std::ios::trunc);
|
||
styleFile << "body { color: #ffffff; }";
|
||
}
|
||
|
||
_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 MetaCoreTestViewportPickingPrefersModelChildNode() {
|
||
MetaCore::MetaCoreWindow window;
|
||
MetaCore::MetaCoreRenderDevice renderDevice;
|
||
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
|
||
MetaCore::MetaCoreScene scene;
|
||
MetaCore::MetaCoreLogService logService;
|
||
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
|
||
|
||
MetaCore::MetaCoreEditorContext editorContext(
|
||
window,
|
||
renderDevice,
|
||
viewportRenderer,
|
||
scene,
|
||
logService,
|
||
moduleRegistry
|
||
);
|
||
|
||
MetaCore::MetaCoreGameObject modelRoot = scene.CreateGameObject("Imported Model Root");
|
||
modelRoot.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(0.0F, 5.0F, 0.0F);
|
||
modelRoot.AddComponent<MetaCore::MetaCoreMeshRendererComponent>();
|
||
|
||
MetaCore::MetaCoreGameObject childNode = scene.CreateGameObject("Imported Mesh Child", modelRoot.GetId());
|
||
childNode.AddComponent<MetaCore::MetaCoreMeshRendererComponent>();
|
||
|
||
MetaCore::MetaCoreSceneView sceneView;
|
||
sceneView.CameraPosition = glm::vec3(0.0F, 0.0F, 0.0F);
|
||
sceneView.CameraTarget = glm::vec3(0.0F, 1.0F, 0.0F);
|
||
sceneView.CameraUp = glm::vec3(0.0F, 0.0F, 1.0F);
|
||
sceneView.VerticalFieldOfViewDegrees = 60.0F;
|
||
|
||
const MetaCore::MetaCoreSceneViewportState viewportState{
|
||
0.0F,
|
||
0.0F,
|
||
100.0F,
|
||
100.0F,
|
||
true,
|
||
true
|
||
};
|
||
|
||
const MetaCore::MetaCoreSceneInteractionService interactionService;
|
||
const MetaCore::MetaCoreId pickedObjectId = interactionService.PickGameObjectFromViewport(
|
||
editorContext,
|
||
sceneView,
|
||
viewportState,
|
||
glm::vec2(50.0F, 50.0F)
|
||
);
|
||
|
||
MetaCoreExpect(pickedObjectId == childNode.GetId(), "视口拾取应优先选中模型内被点击的更具体子节点");
|
||
}
|
||
|
||
void MetaCoreTestViewportPickingUsesMeshTrianglesBeforeHierarchyDepth() {
|
||
const std::filesystem::path tempProjectRoot =
|
||
std::filesystem::temp_directory_path() / "MetaCoreTrianglePickProject";
|
||
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\": \"TrianglePickProject\",\n"
|
||
<< " \"version\": \"0.1.0\",\n"
|
||
<< " \"scenes\": [],\n"
|
||
<< " \"startup_scene\": \"\"\n"
|
||
<< "}\n";
|
||
}
|
||
{
|
||
std::ofstream gltfFile(tempProjectRoot / "Assets" / "House.gltf", std::ios::trunc);
|
||
gltfFile << "{ \"meshes\": [{\"name\": \"Placeholder\"}] }\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 packageService = moduleRegistry.ResolveService<MetaCore::MetaCoreIPackageService>();
|
||
const auto reflectionRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIReflectionRegistry>();
|
||
MetaCoreExpect(assetDatabase != nullptr, "三角面拾取测试应解析到 AssetDatabaseService");
|
||
MetaCoreExpect(packageService != nullptr, "三角面拾取测试应解析到 PackageService");
|
||
MetaCoreExpect(reflectionRegistry != nullptr, "三角面拾取测试应解析到 ReflectionRegistry");
|
||
|
||
const auto modelRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "House.gltf");
|
||
MetaCoreExpect(modelRecord.has_value(), "三角面拾取测试应能找到模型资源");
|
||
|
||
struct TestPickVertex {
|
||
float px, py, pz;
|
||
float nx, ny, nz;
|
||
float u, v;
|
||
};
|
||
const auto makePlanePayload = [](float halfExtent) {
|
||
const std::vector<TestPickVertex> vertices{
|
||
{-halfExtent, halfExtent, 0.0F, 0.0F, 0.0F, -1.0F, 0.0F, 0.0F},
|
||
{ halfExtent, halfExtent, 0.0F, 0.0F, 0.0F, -1.0F, 1.0F, 0.0F},
|
||
{ halfExtent, -halfExtent, 0.0F, 0.0F, 0.0F, -1.0F, 1.0F, 1.0F},
|
||
{-halfExtent, -halfExtent, 0.0F, 0.0F, 0.0F, -1.0F, 0.0F, 1.0F}
|
||
};
|
||
const std::vector<std::uint32_t> indices{0, 1, 2, 0, 2, 3};
|
||
std::vector<std::byte> payload(sizeof(std::uint32_t) * 2 + vertices.size() * sizeof(TestPickVertex) + indices.size() * sizeof(std::uint32_t));
|
||
std::byte* cursor = payload.data();
|
||
const std::uint32_t vertexCount = static_cast<std::uint32_t>(vertices.size());
|
||
const std::uint32_t indexCount = static_cast<std::uint32_t>(indices.size());
|
||
std::memcpy(cursor, &vertexCount, sizeof(std::uint32_t));
|
||
cursor += sizeof(std::uint32_t);
|
||
std::memcpy(cursor, &indexCount, sizeof(std::uint32_t));
|
||
cursor += sizeof(std::uint32_t);
|
||
std::memcpy(cursor, vertices.data(), vertices.size() * sizeof(TestPickVertex));
|
||
cursor += vertices.size() * sizeof(TestPickVertex);
|
||
std::memcpy(cursor, indices.data(), indices.size() * sizeof(std::uint32_t));
|
||
return payload;
|
||
};
|
||
|
||
MetaCore::MetaCoreModelAssetDocument document;
|
||
document.AssetType = "model";
|
||
document.ImporterId = "GltfModelImporter";
|
||
document.SourcePath = std::filesystem::path("Assets") / "House.gltf";
|
||
document.SourceHash = 5678;
|
||
document.ModelKind = MetaCore::MetaCoreModelAssetKind::Gltf;
|
||
document.SourceFormat = "gltf";
|
||
|
||
MetaCore::MetaCoreImportedGltfNodeDocument shellNode;
|
||
shellNode.Name = "Shell";
|
||
shellNode.ParentIndex = -1;
|
||
shellNode.MeshIndex = 0;
|
||
document.Nodes.push_back(shellNode);
|
||
|
||
MetaCore::MetaCoreImportedGltfNodeDocument interiorNode;
|
||
interiorNode.Name = "Interior";
|
||
interiorNode.ParentIndex = 0;
|
||
interiorNode.MeshIndex = 1;
|
||
document.Nodes.push_back(interiorNode);
|
||
|
||
MetaCore::MetaCoreImportedGltfMeshDocument shellMeshDocument;
|
||
shellMeshDocument.Name = "ShellMesh";
|
||
shellMeshDocument.PrimitiveCount = 1;
|
||
document.Meshes.push_back(shellMeshDocument);
|
||
|
||
MetaCore::MetaCoreImportedGltfMeshDocument interiorMeshDocument;
|
||
interiorMeshDocument.Name = "InteriorMesh";
|
||
interiorMeshDocument.PrimitiveCount = 1;
|
||
document.Meshes.push_back(interiorMeshDocument);
|
||
|
||
MetaCore::MetaCoreMeshAssetDocument shellMesh;
|
||
shellMesh.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate();
|
||
shellMesh.Name = "ShellMesh";
|
||
shellMesh.VertexCount = 4;
|
||
shellMesh.IndexCount = 6;
|
||
shellMesh.PayloadIndex = 1;
|
||
shellMesh.PayloadSize = makePlanePayload(2.0F).size();
|
||
shellMesh.BoundingBoxMin = glm::vec3(-2.0F, -2.0F, 0.0F);
|
||
shellMesh.BoundingBoxMax = glm::vec3(2.0F, 2.0F, 0.0F);
|
||
document.GeneratedMeshAssets.push_back(shellMesh);
|
||
|
||
MetaCore::MetaCoreMeshAssetDocument interiorMesh;
|
||
interiorMesh.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate();
|
||
interiorMesh.Name = "InteriorMesh";
|
||
interiorMesh.VertexCount = 4;
|
||
interiorMesh.IndexCount = 6;
|
||
interiorMesh.PayloadIndex = 2;
|
||
interiorMesh.PayloadSize = makePlanePayload(1.0F).size();
|
||
interiorMesh.BoundingBoxMin = glm::vec3(-1.0F, -1.0F, 0.0F);
|
||
interiorMesh.BoundingBoxMax = glm::vec3(1.0F, 1.0F, 0.0F);
|
||
document.GeneratedMeshAssets.push_back(interiorMesh);
|
||
|
||
const auto documentPayload = MetaCore::MetaCoreSerializeToBytes(document, reflectionRegistry->GetTypeRegistry());
|
||
MetaCoreExpect(documentPayload.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>(documentPayload->size())
|
||
});
|
||
package.PayloadSections.push_back(*documentPayload);
|
||
package.PayloadSections.push_back(makePlanePayload(2.0F));
|
||
package.PayloadSections.push_back(makePlanePayload(1.0F));
|
||
MetaCoreExpect(
|
||
packageService->WritePackage(tempProjectRoot / modelRecord->PackagePath, std::move(package)),
|
||
"三角面拾取测试应能写入模型包"
|
||
);
|
||
MetaCoreExpect(assetDatabase->Refresh(), "三角面拾取测试写入模型包后应能刷新资产数据库");
|
||
|
||
MetaCore::MetaCoreGameObject shellObject = scene.CreateGameObject("Shell");
|
||
shellObject.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(0.0F, 5.0F, 0.0F);
|
||
auto& shellRenderer = shellObject.AddComponent<MetaCore::MetaCoreMeshRendererComponent>();
|
||
shellRenderer.MeshSource = MetaCore::MetaCoreMeshSourceKind::Asset;
|
||
shellRenderer.SourceModelAssetGuid = modelRecord->Guid;
|
||
shellRenderer.MeshAssetGuid = shellMesh.AssetGuid;
|
||
shellRenderer.ModelNodeIndex = 0;
|
||
|
||
MetaCore::MetaCoreGameObject interiorObject = scene.CreateGameObject("Interior", shellObject.GetId());
|
||
interiorObject.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(0.0F, 1.0F, 0.0F);
|
||
auto& interiorRenderer = interiorObject.AddComponent<MetaCore::MetaCoreMeshRendererComponent>();
|
||
interiorRenderer.MeshSource = MetaCore::MetaCoreMeshSourceKind::Asset;
|
||
interiorRenderer.SourceModelAssetGuid = modelRecord->Guid;
|
||
interiorRenderer.MeshAssetGuid = interiorMesh.AssetGuid;
|
||
interiorRenderer.ModelNodeIndex = 1;
|
||
|
||
MetaCore::MetaCoreSceneView sceneView;
|
||
sceneView.CameraPosition = glm::vec3(0.0F, 0.0F, 0.0F);
|
||
sceneView.CameraTarget = glm::vec3(0.0F, 1.0F, 0.0F);
|
||
sceneView.CameraUp = glm::vec3(0.0F, 0.0F, 1.0F);
|
||
sceneView.VerticalFieldOfViewDegrees = 60.0F;
|
||
const MetaCore::MetaCoreSceneViewportState viewportState{0.0F, 0.0F, 100.0F, 100.0F, true, true};
|
||
|
||
const MetaCore::MetaCoreSceneInteractionService interactionService;
|
||
const MetaCore::MetaCoreId pickedObjectId = interactionService.PickGameObjectFromViewport(
|
||
editorContext,
|
||
sceneView,
|
||
viewportState,
|
||
glm::vec2(50.0F, 50.0F)
|
||
);
|
||
|
||
MetaCoreExpect(pickedObjectId == shellObject.GetId(), "三角面拾取应选择可见的外壳,而不是层级更深的内部部件");
|
||
|
||
coreServicesModule->Shutdown(moduleRegistry);
|
||
moduleRegistry.ShutdownServices();
|
||
_putenv_s("METACORE_PROJECT_PATH", "");
|
||
std::filesystem::remove_all(tempProjectRoot);
|
||
}
|
||
|
||
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 / "Ui");
|
||
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";
|
||
}
|
||
{
|
||
std::ofstream hudFile(tempProjectRoot / "Ui" / "Hud.rml", std::ios::trunc);
|
||
hudFile << "<rml><head><link type=\"text/rcss\" href=\"Hud.rcss\" /></head><body><div id=\"hud\">HUD</div></body></rml>";
|
||
}
|
||
{
|
||
std::ofstream styleFile(tempProjectRoot / "Ui" / "Hud.rcss", std::ios::trunc);
|
||
styleFile << "body { font-family: sans-serif; } #hud { color: white; }";
|
||
}
|
||
|
||
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
|
||
|
||
MetaCore::MetaCoreWindow window;
|
||
MetaCore::MetaCoreRenderDevice renderDevice;
|
||
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
|
||
MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene();
|
||
const std::size_t defaultSceneObjectCount = scene.GetGameObjects().size();
|
||
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() == defaultSceneObjectCount, "重新加载后对象数量应恢复");
|
||
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 assetEditingService = moduleRegistry.ResolveService<MetaCore::MetaCoreIAssetEditingService>();
|
||
const auto sceneEditingService = moduleRegistry.ResolveService<MetaCore::MetaCoreISceneEditingService>();
|
||
MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService");
|
||
MetaCoreExpect(assetEditingService != nullptr, "应解析到 AssetEditingService");
|
||
MetaCoreExpect(sceneEditingService != nullptr, "应解析到 SceneEditingService");
|
||
|
||
const auto modelRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Pump.gltf");
|
||
MetaCoreExpect(modelRecord.has_value(), "应能找到导入后的模型资源");
|
||
|
||
const auto sourceOnlyModelDocument = assetEditingService->LoadModelAsset(modelRecord->Guid);
|
||
MetaCoreExpect(sourceOnlyModelDocument.has_value(), "开发期无模型包文件时仍应能读取模型资产文档");
|
||
MetaCoreExpect(!sourceOnlyModelDocument->GeneratedMaterialAssets.empty(), "模型资产文档应包含生成材质");
|
||
|
||
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 - 1.0F) < 0.0001F, "缺少粗糙度的导入材质 Roughness 应默认为 1");
|
||
MetaCoreExpect(editorContext.GetActiveObjectId() == *instantiatedRootId, "实例化后应正确选中新对象");
|
||
|
||
const MetaCore::MetaCoreAssetGuid generatedMaterialGuid =
|
||
instantiatedRoot.GetComponent<MetaCore::MetaCoreMeshRendererComponent>().MaterialAssetGuids.front();
|
||
auto generatedMaterial = assetEditingService->LoadMaterialAsset(generatedMaterialGuid);
|
||
MetaCoreExpect(generatedMaterial.has_value(), "源模型生成材质应可作为材质资源读取");
|
||
generatedMaterial->BaseColor = glm::vec3(0.25F, 0.5F, 0.75F);
|
||
generatedMaterial->Metallic = 0.35F;
|
||
generatedMaterial->Roughness = 0.8F;
|
||
MetaCoreExpect(assetEditingService->SaveMaterialAsset(generatedMaterialGuid, *generatedMaterial), "源模型生成材质应可保存编辑结果");
|
||
|
||
const auto reloadedGeneratedMaterial = assetEditingService->LoadMaterialAsset(generatedMaterialGuid);
|
||
MetaCoreExpect(reloadedGeneratedMaterial.has_value(), "保存后应能重新读取源模型生成材质");
|
||
MetaCoreExpectVec3Near(reloadedGeneratedMaterial->BaseColor, glm::vec3(0.25F, 0.5F, 0.75F), "生成材质基础色保存后应保持");
|
||
MetaCoreExpect(std::abs(reloadedGeneratedMaterial->Metallic - 0.35F) < 0.0001F, "生成材质 Metallic 保存后应保持");
|
||
MetaCoreExpect(std::abs(reloadedGeneratedMaterial->Roughness - 0.8F) < 0.0001F, "生成材质 Roughness 保存后应保持");
|
||
|
||
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 - 1.0F) < 0.0001F, "真实 GLTF 材质缺少 roughnessFactor 时 Roughness 应默认为 1");
|
||
|
||
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");
|
||
if (!winOk) {
|
||
std::cout << "[SKIP] Delete model viewport smoke requires an available display." << std::endl;
|
||
coreServicesModule->Shutdown(moduleRegistry);
|
||
moduleRegistry.ShutdownServices();
|
||
_putenv_s("METACORE_PROJECT_PATH", "");
|
||
return;
|
||
}
|
||
|
||
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");
|
||
if (!winOk) {
|
||
std::cout << "[SKIP] Delete model child viewport smoke requires an available display." << std::endl;
|
||
coreServicesModule->Shutdown(moduleRegistry);
|
||
moduleRegistry.ShutdownServices();
|
||
_putenv_s("METACORE_PROJECT_PATH", "");
|
||
std::filesystem::remove_all(tempProjectRoot);
|
||
return;
|
||
}
|
||
|
||
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();
|
||
const std::size_t defaultSceneObjectCount = scene.GetGameObjects().size();
|
||
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() == defaultSceneObjectCount, "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";
|
||
writtenDocument.UiManifestPath = std::filesystem::path("Runtime") / "Ui.mcruntime";
|
||
|
||
MetaCoreExpect(MetaCore::MetaCoreWriteRuntimeProjectDocument(tempPath, writtenDocument, registry), "应能写入 RuntimeProject 文档");
|
||
|
||
const auto loadedDocument = MetaCore::MetaCoreReadRuntimeProjectDocument(tempPath, registry);
|
||
MetaCoreExpect(loadedDocument.has_value(), "应能读回 RuntimeProject 文档");
|
||
MetaCoreExpect(loadedDocument->StartupScenePath == writtenDocument.StartupScenePath, "StartupScenePath 应一致");
|
||
MetaCoreExpect(loadedDocument->DiagnosticsPath == writtenDocument.DiagnosticsPath, "DiagnosticsPath 应一致");
|
||
MetaCoreExpect(loadedDocument->UiManifestPath == writtenDocument.UiManifestPath, "UiManifestPath 应一致");
|
||
|
||
std::filesystem::remove(tempPath);
|
||
}
|
||
|
||
void MetaCoreTestRuntimeUiManifestIo() {
|
||
MetaCore::MetaCoreTypeRegistry registry;
|
||
MetaCore::MetaCoreRegisterRuntimeDataGeneratedTypes(registry);
|
||
|
||
const std::filesystem::path tempPath = std::filesystem::temp_directory_path() / "MetaCoreRuntimeUi.mcruntime";
|
||
MetaCore::MetaCoreRuntimeUiManifest writtenManifest;
|
||
writtenManifest.Documents.push_back(MetaCore::MetaCoreRuntimeUiDocumentEntry{
|
||
"hud",
|
||
std::filesystem::path("Hud.rml"),
|
||
0,
|
||
true,
|
||
true
|
||
});
|
||
writtenManifest.Documents.push_back(MetaCore::MetaCoreRuntimeUiDocumentEntry{
|
||
"pause",
|
||
std::filesystem::path("Menus") / "Pause.rml",
|
||
10,
|
||
false,
|
||
false
|
||
});
|
||
|
||
MetaCoreExpect(MetaCore::MetaCoreWriteRuntimeUiManifest(tempPath, writtenManifest, registry), "应能写入 Runtime UI 清单");
|
||
const auto loadedManifest = MetaCore::MetaCoreReadRuntimeUiManifest(tempPath, registry);
|
||
MetaCoreExpect(loadedManifest.has_value(), "应能读回 Runtime UI 清单");
|
||
MetaCoreExpect(loadedManifest->Documents.size() == 2, "Runtime UI 清单应保留文档数量");
|
||
MetaCoreExpect(loadedManifest->Documents.front().DocumentId == "hud", "Runtime UI 清单应保留 DocumentId");
|
||
MetaCoreExpect(loadedManifest->Documents.front().RmlPath == std::filesystem::path("Hud.rml"), "Runtime UI 清单应保留 RML 路径");
|
||
MetaCoreExpect(loadedManifest->Documents.back().Layer == 10, "Runtime UI 清单应保留层级");
|
||
MetaCoreExpect(!loadedManifest->Documents.back().Visible, "Runtime UI 清单应保留初始显示状态");
|
||
MetaCoreExpect(!loadedManifest->Documents.back().InputEnabled, "Runtime UI 清单应保留输入开关");
|
||
|
||
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 / "Ui");
|
||
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";
|
||
}
|
||
{
|
||
std::ofstream hudFile(tempProjectRoot / "Ui" / "Hud.rml", std::ios::trunc);
|
||
hudFile << "<rml><head><link type=\"text/rcss\" href=\"Hud.rcss\" /></head><body><div id=\"hud\">HUD</div></body></rml>";
|
||
}
|
||
{
|
||
std::ofstream styleFile(tempProjectRoot / "Ui" / "Hud.rcss", std::ios::trunc);
|
||
styleFile << "body { font-family: sans-serif; } #hud { color: white; }";
|
||
}
|
||
|
||
_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
|
||
);
|
||
const auto loadedUiManifest = MetaCore::MetaCoreReadRuntimeUiManifest(
|
||
tempProjectRoot / "Runtime" / "Ui.mcruntime",
|
||
registry
|
||
);
|
||
MetaCoreExpect(loadedSources.has_value(), "保存后应能读回 Runtime data sources");
|
||
MetaCoreExpect(loadedBindings.has_value(), "保存后应能读回 Runtime bindings");
|
||
MetaCoreExpect(loadedProjectRuntime.has_value(), "保存后应能读回 Runtime project");
|
||
MetaCoreExpect(loadedUiManifest.has_value(), "保存后应能读回 Runtime UI 清单");
|
||
MetaCoreExpect(loadedSources->Sources.size() == 1, "保存后应保留 source");
|
||
MetaCoreExpect(loadedBindings->Bindings.size() == 1, "保存后应保留 binding");
|
||
MetaCoreExpect(loadedProjectRuntime->StartupScenePath == std::filesystem::path("Scenes") / "Main.mcscene.json", "保存后应写入默认 startup scene 路径");
|
||
MetaCoreExpect(loadedProjectRuntime->UiManifestPath == std::filesystem::path("Runtime") / "Ui.mcruntime", "保存后应写入 UI 清单路径");
|
||
MetaCoreExpect(loadedUiManifest->Documents.size() == 1, "保存后默认 UI 清单应包含 HUD");
|
||
MetaCoreExpect(loadedUiManifest->Documents.front().RmlPath == std::filesystem::path("Hud.rml"), "默认 UI 清单应引用 HUD RML");
|
||
|
||
const auto cookService = moduleRegistry.ResolveService<MetaCore::MetaCoreICookService>();
|
||
MetaCoreExpect(cookService != nullptr, "应解析到 CookService");
|
||
MetaCoreExpect(cookService->CookRuntimeUiAssets(), "CookService 应能复制 Runtime UI 资源到 Build");
|
||
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / "Build" / "Runtime" / "Ui.mcruntime"), "Build 应包含 Runtime UI 清单");
|
||
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / "Build" / "Ui" / "Hud.rml"), "Build 应包含 HUD RML");
|
||
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / "Build" / "Ui" / "Hud.rcss"), "Build 应包含 HUD RCSS");
|
||
|
||
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() {
|
||
if (!MetaCoreTestInitializeSocketApi()) {
|
||
std::cout << "[SKIP] Tcp runtime data source smoke requires socket API." << std::endl;
|
||
return;
|
||
}
|
||
|
||
MetaCoreTestSocket listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||
if (listenSocket == MetaCoreTestInvalidSocket) {
|
||
std::cout << "[SKIP] Tcp runtime data source smoke requires a local socket." << std::endl;
|
||
MetaCoreTestCleanupSocketApi();
|
||
return;
|
||
}
|
||
|
||
sockaddr_in address{};
|
||
address.sin_family = AF_INET;
|
||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||
address.sin_port = 0;
|
||
if (bind(listenSocket, reinterpret_cast<sockaddr*>(&address), sizeof(address)) == MetaCoreTestSocketError ||
|
||
listen(listenSocket, 1) == MetaCoreTestSocketError) {
|
||
std::cout << "[SKIP] Tcp runtime data source smoke requires loopback listen support." << std::endl;
|
||
MetaCoreTestCloseSocket(listenSocket);
|
||
MetaCoreTestCleanupSocketApi();
|
||
return;
|
||
}
|
||
|
||
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 MetaCoreTestUiRendererComponentSnapshotAndJson() {
|
||
MetaCore::MetaCoreTypeRegistry registry;
|
||
MetaCoreRegisterFoundationGeneratedTypes(registry);
|
||
MetaCoreRegisterSceneGeneratedTypes(registry);
|
||
|
||
const MetaCore::MetaCoreAssetGuid uiGuid = MetaCore::MetaCoreAssetGuid::Generate();
|
||
MetaCore::MetaCoreScene scene;
|
||
MetaCore::MetaCoreGameObject uiObject = scene.CreateGameObject("Hud Canvas");
|
||
auto& uiRenderer = uiObject.AddComponent<MetaCore::MetaCoreUiRendererComponent>();
|
||
uiRenderer.UiDocumentAssetGuid = uiGuid;
|
||
uiRenderer.RenderMode = MetaCore::MetaCoreUiRenderMode::WorldSpace;
|
||
uiRenderer.Visible = false;
|
||
uiRenderer.InputEnabled = false;
|
||
uiRenderer.FaceCamera = true;
|
||
uiRenderer.Layer = 42;
|
||
uiRenderer.PixelsPerUnit = 512.0F;
|
||
uiRenderer.WorldSize = glm::vec3(3.0F, 2.0F, 0.0F);
|
||
|
||
const MetaCore::MetaCoreSceneSnapshot snapshot = scene.CaptureSnapshot();
|
||
MetaCoreExpect(snapshot.GameObjects.size() == 1, "UI Renderer 对象应进入场景快照");
|
||
MetaCoreExpect(snapshot.GameObjects.front().UiRenderer.has_value(), "场景快照应保存 UI Renderer 组件");
|
||
|
||
MetaCore::MetaCoreScene restoredScene;
|
||
restoredScene.RestoreSnapshot(snapshot);
|
||
MetaCore::MetaCoreGameObject restoredObject = restoredScene.FindGameObject(uiObject.GetId());
|
||
MetaCoreExpect(restoredObject && restoredObject.HasComponent<MetaCore::MetaCoreUiRendererComponent>(), "恢复快照后对象应保留 UI Renderer");
|
||
const auto& restoredRenderer = restoredObject.GetComponent<MetaCore::MetaCoreUiRendererComponent>();
|
||
MetaCoreExpect(restoredRenderer.UiDocumentAssetGuid == uiGuid, "恢复快照后 UI 资产 Guid 应一致");
|
||
MetaCoreExpect(restoredRenderer.RenderMode == MetaCore::MetaCoreUiRenderMode::WorldSpace, "恢复快照后渲染模式应一致");
|
||
MetaCoreExpect(restoredRenderer.FaceCamera, "恢复快照后朝向相机设置应一致");
|
||
MetaCoreExpect(restoredRenderer.Layer == 42, "恢复快照后层级应一致");
|
||
MetaCoreExpectVec3Near(restoredRenderer.WorldSize, glm::vec3(3.0F, 2.0F, 0.0F), "恢复快照后世界尺寸应一致");
|
||
|
||
MetaCore::MetaCoreSceneRenderSync renderSync;
|
||
const MetaCore::MetaCoreSceneRenderSyncSnapshot hiddenRenderSnapshot = renderSync.BuildSnapshot(scene);
|
||
MetaCoreExpect(hiddenRenderSnapshot.Renderables.empty(), "隐藏的 3D UI Renderer 不应生成预览 Renderable");
|
||
scene.FindGameObject(uiObject.GetId()).GetComponent<MetaCore::MetaCoreUiRendererComponent>().Visible = true;
|
||
const MetaCore::MetaCoreSceneRenderSyncSnapshot renderSnapshot = renderSync.BuildSnapshot(scene);
|
||
MetaCoreExpect(renderSnapshot.Renderables.size() == 1, "可见的 3D UI Renderer 应生成一个预览 Renderable");
|
||
MetaCoreExpect(renderSnapshot.Renderables.front().ObjectId == uiObject.GetId(), "UI 预览 Renderable 应映射回 UI 对象");
|
||
MetaCoreExpect(renderSnapshot.Renderables.front().BuiltinMesh == MetaCore::MetaCoreBuiltinMeshType::Cube, "UI 预览应使用内置 Cube 面板");
|
||
|
||
MetaCore::MetaCoreSceneDocument sceneDocument;
|
||
sceneDocument.Name = "UiRendererScene";
|
||
sceneDocument.GameObjects = snapshot.GameObjects;
|
||
const std::filesystem::path scenePath = std::filesystem::temp_directory_path() / "MetaCoreUiRendererScene.mcscene.json";
|
||
MetaCoreExpect(MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(scenePath, sceneDocument, registry), "应能保存带 UI Renderer 的场景 JSON");
|
||
const auto loadedDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(scenePath, registry);
|
||
std::error_code ec;
|
||
std::filesystem::remove(scenePath, ec);
|
||
MetaCoreExpect(loadedDocument.has_value(), "应能加载带 UI Renderer 的场景 JSON");
|
||
MetaCoreExpect(loadedDocument->GameObjects.size() == 1, "加载后场景对象数量应一致");
|
||
MetaCoreExpect(loadedDocument->GameObjects.front().UiRenderer.has_value(), "加载后应保留 UI Renderer 组件");
|
||
const auto& loadedRenderer = *loadedDocument->GameObjects.front().UiRenderer;
|
||
MetaCoreExpect(loadedRenderer.UiDocumentAssetGuid == uiGuid, "JSON 往返后 UI 资产 Guid 应一致");
|
||
MetaCoreExpect(loadedRenderer.RenderMode == MetaCore::MetaCoreUiRenderMode::WorldSpace, "JSON 往返后渲染模式应一致");
|
||
MetaCoreExpect(!loadedRenderer.Visible, "JSON 往返后可见状态应一致");
|
||
MetaCoreExpect(!loadedRenderer.InputEnabled, "JSON 往返后输入状态应一致");
|
||
MetaCoreExpect(loadedRenderer.FaceCamera, "JSON 往返后朝向相机设置应一致");
|
||
MetaCoreExpect(loadedRenderer.Layer == 42, "JSON 往返后层级应一致");
|
||
MetaCoreExpect(std::abs(loadedRenderer.PixelsPerUnit - 512.0F) <= 0.0001F, "JSON 往返后 PPU 应一致");
|
||
MetaCoreExpectVec3Near(loadedRenderer.WorldSize, glm::vec3(3.0F, 2.0F, 0.0F), "JSON 往返后世界尺寸应一致");
|
||
}
|
||
|
||
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");
|
||
if (!winOk) {
|
||
std::cout << "[SKIP] Decoupled snapshot Filament smoke requires an available display." << std::endl;
|
||
return;
|
||
}
|
||
|
||
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");
|
||
if (!winOk) {
|
||
std::cout << "[SKIP] Nested model hierarchy Filament smoke requires an available display." << std::endl;
|
||
return;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
void MetaCoreTestSceneEnvironmentSettings() {
|
||
const std::filesystem::path tempRoot = std::filesystem::temp_directory_path() / "MetaCoreSceneEnvironmentSmoke";
|
||
std::filesystem::remove_all(tempRoot);
|
||
std::filesystem::create_directories(tempRoot / "Assets" / "Environments");
|
||
|
||
const std::filesystem::path sourceEnvironmentDirectory =
|
||
MetaCoreTestRepositoryRoot() / "third_party" / "filament_installed" / "bin" / "assets" / "ibl" / "lightroom_14b";
|
||
const std::filesystem::path sourceIbl = sourceEnvironmentDirectory / "lightroom_14b_ibl.ktx";
|
||
const std::filesystem::path sourceSkybox = sourceEnvironmentDirectory / "lightroom_14b_skybox.ktx";
|
||
MetaCoreExpect(std::filesystem::exists(sourceIbl) && std::filesystem::exists(sourceSkybox), "应提供默认 KTX 环境资源");
|
||
const std::filesystem::path projectIbl = tempRoot / "Assets" / "Environments" / "studio_ibl.ktx";
|
||
const std::filesystem::path projectSkybox = tempRoot / "Assets" / "Environments" / "studio_skybox.ktx";
|
||
std::filesystem::copy_file(sourceIbl, projectIbl, std::filesystem::copy_options::overwrite_existing);
|
||
std::filesystem::copy_file(sourceSkybox, projectSkybox, std::filesystem::copy_options::overwrite_existing);
|
||
|
||
MetaCore::MetaCoreSceneEnvironmentSettings environment;
|
||
environment.Source = MetaCore::MetaCoreSceneEnvironmentSource::ProjectKtxPair;
|
||
environment.IblKtxPath = std::filesystem::path("Assets") / "Environments" / "studio_ibl.ktx";
|
||
environment.SkyboxKtxPath = std::filesystem::path("Assets") / "Environments" / "studio_skybox.ktx";
|
||
environment.SkyboxVisible = true;
|
||
environment.RotationDegrees = 137.5F;
|
||
environment.IndirectLightIntensity = 25000.0F;
|
||
environment.SkyboxIntensity = 7500.0F;
|
||
|
||
MetaCore::MetaCoreScene scene;
|
||
MetaCoreExpect(scene.SetEnvironmentSettings(environment), "应能设置场景环境");
|
||
const MetaCore::MetaCoreSceneSnapshot environmentSnapshot = scene.CaptureSnapshot();
|
||
(void)scene.SetEnvironmentSettings(MetaCore::MetaCoreSceneEnvironmentSettings{});
|
||
scene.RestoreSnapshot(environmentSnapshot);
|
||
MetaCoreExpect(scene.GetEnvironmentSettings().Source == MetaCore::MetaCoreSceneEnvironmentSource::ProjectKtxPair, "场景快照应恢复环境来源");
|
||
MetaCoreExpect(scene.GetEnvironmentSettings().IblKtxPath == environment.IblKtxPath, "场景快照应恢复 IBL 路径");
|
||
MetaCoreExpect(std::abs(scene.GetEnvironmentSettings().SkyboxIntensity - environment.SkyboxIntensity) <= 0.0001F, "场景快照应恢复天空盒亮度");
|
||
|
||
MetaCore::MetaCoreWindow commandWindow;
|
||
MetaCore::MetaCoreRenderDevice commandRenderDevice;
|
||
MetaCore::MetaCoreEditorViewportRenderer commandViewportRenderer;
|
||
MetaCore::MetaCoreLogService commandLogService;
|
||
MetaCore::MetaCoreEditorModuleRegistry commandModuleRegistry;
|
||
MetaCore::MetaCoreEditorContext commandContext(
|
||
commandWindow,
|
||
commandRenderDevice,
|
||
commandViewportRenderer,
|
||
scene,
|
||
commandLogService,
|
||
commandModuleRegistry
|
||
);
|
||
MetaCoreExpect(commandContext.ExecuteSnapshotCommand("修改天空盒显示", [&]() {
|
||
MetaCore::MetaCoreSceneEnvironmentSettings changed = scene.GetEnvironmentSettings();
|
||
changed.SkyboxVisible = false;
|
||
return scene.SetEnvironmentSettings(changed);
|
||
}), "环境设置修改应进入撤销栈");
|
||
MetaCoreExpect(!scene.GetEnvironmentSettings().SkyboxVisible, "执行环境设置命令后应更新天空盒显示");
|
||
MetaCoreExpect(commandContext.UndoCommand(), "环境设置应支持撤销");
|
||
MetaCoreExpect(scene.GetEnvironmentSettings().SkyboxVisible, "撤销后应恢复天空盒显示");
|
||
MetaCoreExpect(commandContext.RedoCommand(), "环境设置应支持重做");
|
||
MetaCoreExpect(!scene.GetEnvironmentSettings().SkyboxVisible, "重做后应再次关闭天空盒显示");
|
||
(void)scene.SetEnvironmentSettings(environment);
|
||
|
||
MetaCore::MetaCoreSceneRenderSync renderSync;
|
||
const MetaCore::MetaCoreSceneRenderSyncSnapshot renderSnapshot = renderSync.BuildSnapshot(scene);
|
||
MetaCoreExpect(renderSnapshot.Environment.IblKtxPath == environment.IblKtxPath, "渲染快照应包含环境路径");
|
||
MetaCoreExpect(renderSnapshot.Renderables.empty(), "环境设置不应生成可渲染场景对象");
|
||
|
||
MetaCore::MetaCoreTypeRegistry registry;
|
||
MetaCore::MetaCoreRegisterFoundationGeneratedTypes(registry);
|
||
MetaCore::MetaCoreRegisterSceneGeneratedTypes(registry);
|
||
MetaCore::MetaCoreSceneDocument document;
|
||
document.Name = "EnvironmentScene";
|
||
document.Environment = environment;
|
||
const std::filesystem::path jsonPath = tempRoot / "Environment.mcscene.json";
|
||
MetaCoreExpect(MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(jsonPath, document, registry), "环境场景 JSON 应可保存");
|
||
const auto loadedDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(jsonPath, registry);
|
||
MetaCoreExpect(loadedDocument.has_value(), "环境场景 JSON 应可读取");
|
||
MetaCoreExpect(loadedDocument->Environment.SkyboxKtxPath == environment.SkyboxKtxPath, "环境场景 JSON 应保留天空盒路径");
|
||
MetaCoreExpect(std::abs(loadedDocument->Environment.RotationDegrees - environment.RotationDegrees) <= 0.0001F, "环境场景 JSON 应保留旋转");
|
||
|
||
const std::filesystem::path packagePath = tempRoot / "Environment.mcscene";
|
||
MetaCoreExpect(MetaCore::MetaCoreWriteScenePackage(packagePath, document), "环境场景包应可保存");
|
||
const auto loadedPackage = MetaCore::MetaCoreReadScenePackage(packagePath);
|
||
MetaCoreExpect(loadedPackage.has_value(), "环境场景包应可读取");
|
||
MetaCoreExpect(loadedPackage->Environment.Source == MetaCore::MetaCoreSceneEnvironmentSource::ProjectKtxPair, "环境场景包应保留来源");
|
||
|
||
const std::filesystem::path legacyJsonPath = tempRoot / "Legacy.mcscene.json";
|
||
{
|
||
std::ofstream legacyOutput(legacyJsonPath, std::ios::trunc);
|
||
legacyOutput << "{\"SceneName\":\"Legacy\",\"GameObjects\":[]}";
|
||
}
|
||
const auto legacyDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(legacyJsonPath, registry);
|
||
MetaCoreExpect(legacyDocument.has_value(), "旧场景缺少环境字段时仍应可读取");
|
||
MetaCoreExpect(legacyDocument->Environment.Source == MetaCore::MetaCoreSceneEnvironmentSource::Builtin, "旧场景应使用内置环境默认值");
|
||
|
||
MetaCore::MetaCoreWindow window;
|
||
if (!window.Initialize(640, 480, "SceneEnvironmentSmoke")) {
|
||
std::cout << "[SKIP] Scene environment Filament smoke requires an available display." << std::endl;
|
||
std::filesystem::remove_all(tempRoot);
|
||
return;
|
||
}
|
||
MetaCore::MetaCoreFilamentSceneBridge bridge;
|
||
bridge.SetProjectRootPath(tempRoot);
|
||
MetaCoreExpect(bridge.Initialize(window), "环境测试桥接器应初始化成功");
|
||
bridge.SyncScene(renderSnapshot, nullptr, false, false);
|
||
bridge.ApplySceneView(MetaCore::MetaCoreSceneView{});
|
||
MetaCoreExpect(bridge.VerifyEnvironmentLoadedForTesting(), "自定义 KTX 对应加载为环境资源");
|
||
MetaCoreExpect(bridge.VerifyEnvironmentBackgroundVisibleForTesting(), "天空盒应在 Skybox 清屏模式下显示");
|
||
MetaCoreExpect(
|
||
std::abs(bridge.GetEnvironmentIndirectLightIntensityForTesting() - environment.IndirectLightIntensity) <= 0.0001F,
|
||
"环境光强度应写入 Filament"
|
||
);
|
||
|
||
MetaCore::MetaCoreSceneEnvironmentSettings hiddenEnvironment = environment;
|
||
hiddenEnvironment.SkyboxVisible = false;
|
||
(void)scene.SetEnvironmentSettings(hiddenEnvironment);
|
||
bridge.SyncScene(renderSync.BuildSnapshot(scene), nullptr, false, false);
|
||
MetaCoreExpect(!bridge.VerifyEnvironmentBackgroundVisibleForTesting(), "关闭天空盒后背景不应显示");
|
||
MetaCoreExpect(bridge.VerifyEnvironmentLoadedForTesting(), "关闭天空盒不应销毁环境光");
|
||
|
||
MetaCore::MetaCoreSceneEnvironmentSettings invalidEnvironment = environment;
|
||
invalidEnvironment.IblKtxPath = std::filesystem::path("..") / "unsafe_ibl.ktx";
|
||
(void)scene.SetEnvironmentSettings(invalidEnvironment);
|
||
bridge.SyncScene(renderSync.BuildSnapshot(scene), nullptr, false, false);
|
||
MetaCoreExpect(bridge.VerifyEnvironmentLoadedForTesting(), "无效自定义路径应回退到内置环境");
|
||
|
||
bridge.Shutdown();
|
||
window.Shutdown();
|
||
std::filesystem::remove_all(tempRoot);
|
||
}
|
||
|
||
} // 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() >= 3, "默认场景应包含相机和基础光照对象");
|
||
|
||
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, "默认场景应包含光源");
|
||
MetaCoreExpect(MetaCoreCountSceneLights(scene) >= 2, "默认场景应包含主光和补光");
|
||
|
||
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] MetaCoreTestProjectSkeletonHelpers..." << std::endl;
|
||
MetaCoreTestProjectSkeletonHelpers();
|
||
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] MetaCoreTestSceneEnvironmentSettings..." << std::endl;
|
||
MetaCoreTestSceneEnvironmentSettings();
|
||
std::cout << "[RUN] MetaCoreTestCameraComponentJsonRoundTripAndLegacyDefaults..." << std::endl;
|
||
MetaCoreTestCameraComponentJsonRoundTripAndLegacyDefaults();
|
||
std::cout << "[RUN] MetaCoreTestCameraSelectionAndProjectionUtilities..." << std::endl;
|
||
MetaCoreTestCameraSelectionAndProjectionUtilities();
|
||
std::cout << "[RUN] MetaCoreTestViewportPickingPrefersModelChildNode..." << std::endl;
|
||
MetaCoreTestViewportPickingPrefersModelChildNode();
|
||
std::cout << "[RUN] MetaCoreTestViewportPickingUsesMeshTrianglesBeforeHierarchyDepth..." << std::endl;
|
||
MetaCoreTestViewportPickingUsesMeshTrianglesBeforeHierarchyDepth();
|
||
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] MetaCoreTestRuntimeUiManifestIo..." << std::endl;
|
||
MetaCoreTestRuntimeUiManifestIo();
|
||
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] MetaCoreTestUiRendererComponentSnapshotAndJson..." << std::endl;
|
||
MetaCoreTestUiRendererComponentSnapshotAndJson();
|
||
|
||
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;
|
||
}
|