diff --git a/Apps/MetaCorePlayer/main.cpp b/Apps/MetaCorePlayer/main.cpp index 395e3be..1cfe245 100644 --- a/Apps/MetaCorePlayer/main.cpp +++ b/Apps/MetaCorePlayer/main.cpp @@ -1,11 +1,17 @@ #include "MetaCorePlatform/MetaCoreWindow.h" +#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" #include "MetaCoreRender/MetaCoreRenderTypes.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataDispatcher.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataSource.h" +#include "MetaCoreScene/MetaCoreScenePackage.h" #include "MetaCoreScene/MetaCoreScene.h" #include #include +#include namespace { @@ -18,6 +24,142 @@ MetaCore::MetaCoreSceneView MetaCoreBuildPlayerSceneView() { return sceneView; } +[[nodiscard]] std::filesystem::path MetaCoreResolveRuntimePath(const std::filesystem::path& relativePath) { + const std::vector candidates{ + std::filesystem::current_path() / relativePath, + std::filesystem::current_path() / ".." / ".." / ".." / relativePath, + std::filesystem::current_path() / ".." / ".." / ".." / "TestProject" / "Runtime" / relativePath.filename() + }; + + for (const std::filesystem::path& candidate : candidates) { + std::error_code errorCode; + if (std::filesystem::exists(candidate, errorCode)) { + return std::filesystem::weakly_canonical(candidate, errorCode); + } + } + return candidates.front(); +} + +[[nodiscard]] std::filesystem::path MetaCoreResolveProjectPath(const std::filesystem::path& relativePath) { + const std::vector candidates{ + std::filesystem::current_path() / relativePath, + std::filesystem::current_path() / ".." / ".." / ".." / relativePath + }; + + for (const std::filesystem::path& candidate : candidates) { + std::error_code errorCode; + if (std::filesystem::exists(candidate, errorCode)) { + return std::filesystem::weakly_canonical(candidate, errorCode); + } + } + return candidates.front(); +} + +[[nodiscard]] MetaCore::MetaCoreRuntimeProjectDocument MetaCoreBuildDefaultRuntimeProjectDocument() { + MetaCore::MetaCoreRuntimeProjectDocument document; + document.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; + document.DataSourcesPath = std::filesystem::path("Runtime") / "DataSources.mcruntime"; + document.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; + document.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; + return document; +} + +[[nodiscard]] MetaCore::MetaCoreRuntimeDataSourcesDocument MetaCoreBuildDefaultRuntimeDataSourcesDocument() { + MetaCore::MetaCoreRuntimeDataSourcesDocument document; + document.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + "replay-source", + "file_replay", + "Replay Source", + {MetaCore::MetaCoreDataSourceSetting{"file_path", "TestProject/Runtime/RuntimeReplay.mcstream"}}, + true, + 1000 + }); + document.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "cube.position", + "replay-source", + "cube.position", + MetaCore::MetaCoreRuntimeValueType::Vec3 + }); + document.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "cube.visible", + "replay-source", + "cube.visible", + MetaCore::MetaCoreRuntimeValueType::Bool + }); + document.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "cube.base_color", + "replay-source", + "cube.base_color", + MetaCore::MetaCoreRuntimeValueType::Vec3 + }); + document.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "valve.visible", + "replay-source", + "valve.visible", + MetaCore::MetaCoreRuntimeValueType::Bool + }); + document.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "tank.base_color", + "replay-source", + "tank.base_color", + MetaCore::MetaCoreRuntimeValueType::Vec3 + }); + document.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "alarm.intensity", + "replay-source", + "alarm.intensity", + MetaCore::MetaCoreRuntimeValueType::Double + }); + return document; +} + +[[nodiscard]] MetaCore::MetaCoreRuntimeBindingsDocument MetaCoreBuildDefaultRuntimeBindingsDocument() { + MetaCore::MetaCoreRuntimeBindingsDocument document; + document.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.cube.position", + "cube.position", + 3, + MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + document.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.cube.visible", + "cube.visible", + 3, + MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + document.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.cube.base_color", + "cube.base_color", + 3, + MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererBaseColor, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + document.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.valve.visible", + "valve.visible", + 4, + MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + document.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.tank.base_color", + "tank.base_color", + 5, + MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererBaseColor, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + document.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.alarm.intensity", + "alarm.intensity", + 6, + MetaCore::MetaCoreRuntimeBindingTarget::LightIntensity, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + return document; +} + } // namespace int main(int argc, char* argv[]) { @@ -43,8 +185,86 @@ int main(int argc, char* argv[]) { return 1; } - MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + const auto projectPath = MetaCoreResolveProjectPath("TestProject/MetaCore.project.json"); + const std::filesystem::path projectRoot = projectPath.parent_path(); + MetaCore::MetaCoreTypeRegistry typeRegistry; + MetaCore::MetaCoreRegisterRuntimeDataGeneratedTypes(typeRegistry); + const auto loadedRuntimeProjectDocument = MetaCore::MetaCoreReadRuntimeProjectDocument( + projectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", + typeRegistry + ); + const auto runtimeProjectDocument = loadedRuntimeProjectDocument.value_or(MetaCoreBuildDefaultRuntimeProjectDocument()); + std::optional startupSceneDocument; + if (!runtimeProjectDocument.StartupScenePath.empty()) { + startupSceneDocument = MetaCore::MetaCoreReadScenePackage(projectRoot / runtimeProjectDocument.StartupScenePath); + } + if (!startupSceneDocument.has_value()) { + startupSceneDocument = MetaCore::MetaCoreLoadStartupSceneDocument(projectPath); + } + MetaCore::MetaCoreScene scene; + if (startupSceneDocument.has_value()) { + MetaCore::MetaCoreSceneSnapshot snapshot; + snapshot.GameObjects = startupSceneDocument->GameObjects; + scene.RestoreSnapshot(snapshot); + std::cout << "MetaCorePlayer: loaded startup scene from project " + << projectPath.string() << '\n'; + } else { + scene = MetaCore::MetaCoreCreateDefaultScene(); + std::cout << "MetaCorePlayer: startup scene unavailable, using built-in default scene\n"; + } + MetaCore::MetaCoreRuntimeDataDispatcher runtimeDataDispatcher(scene); + + const auto sourcesPath = MetaCoreResolveRuntimePath(projectRoot / runtimeProjectDocument.DataSourcesPath); + const auto bindingsPath = MetaCoreResolveRuntimePath(projectRoot / runtimeProjectDocument.BindingsPath); + const auto diagnosticsPath = MetaCoreResolveRuntimePath(projectRoot / runtimeProjectDocument.DiagnosticsPath); + const auto loadedSourcesDocument = MetaCore::MetaCoreReadRuntimeDataSourcesDocument(sourcesPath, typeRegistry); + const auto loadedBindingsDocument = MetaCore::MetaCoreReadRuntimeBindingsDocument(bindingsPath, typeRegistry); + const auto sourcesDocument = loadedSourcesDocument.value_or(MetaCoreBuildDefaultRuntimeDataSourcesDocument()); + const auto bindingsDocument = loadedBindingsDocument.value_or(MetaCoreBuildDefaultRuntimeBindingsDocument()); + + if (loadedSourcesDocument.has_value() && loadedBindingsDocument.has_value()) { + std::cout << "MetaCorePlayer: loaded runtime config from binary .mcruntime documents\n"; + } else { + const bool sourcesExists = std::filesystem::exists(sourcesPath); + const bool bindingsExists = std::filesystem::exists(bindingsPath); + if ((sourcesExists && !loadedSourcesDocument.has_value()) || + (bindingsExists && !loadedBindingsDocument.has_value())) { + std::cerr << "MetaCorePlayer: runtime config exists but is unreadable or corrupted, using built-in fallback config\n"; + } else { + std::cout << "MetaCorePlayer: runtime config missing, using built-in fallback config\n"; + } + } + + runtimeDataDispatcher.SetDataPointDefinitions(sourcesDocument.DataPoints); + runtimeDataDispatcher.SetBindingDefinitions(bindingsDocument.Bindings); + + std::unique_ptr runtimeAdapter; + if (!sourcesDocument.Sources.empty()) { + runtimeAdapter = MetaCore::MetaCoreCreateRuntimeDataSourceAdapter(sourcesDocument.Sources.front().AdapterType); + if (runtimeAdapter == nullptr) { + std::cerr << "MetaCorePlayer: unsupported adapter type " + << sourcesDocument.Sources.front().AdapterType << '\n'; + return 1; + } + if (!runtimeAdapter->Configure(sourcesDocument.Sources.front())) { + std::cerr << "MetaCorePlayer: failed to configure runtime adapter error=" + << runtimeAdapter->GetStatus().LastError << '\n'; + return 1; + } + if (!runtimeAdapter->Connect()) { + std::cerr << "MetaCorePlayer: failed to connect runtime adapter error=" + << runtimeAdapter->GetStatus().LastError << '\n'; + return 1; + } + } + + MetaCore::MetaCoreRuntimeDataSourceState lastReportedSourceState = + runtimeAdapter != nullptr + ? runtimeAdapter->GetStatus().State + : MetaCore::MetaCoreRuntimeDataSourceState::Disconnected; + + std::uint64_t diagnosticsWriteFrame = 0; while (!window.ShouldClose()) { window.BeginFrame(); const auto [windowWidth, windowHeight] = window.GetWindowSize(); @@ -54,6 +274,35 @@ int main(int argc, char* argv[]) { static_cast(windowWidth), static_cast(windowHeight) }); + if (runtimeAdapter != nullptr) { + runtimeAdapter->Tick(1.0 / 60.0); + runtimeDataDispatcher.ApplyUpdates(runtimeAdapter->PollUpdates()); + runtimeDataDispatcher.TickStaleness(runtimeAdapter->GetStatus().LastUpdateAt); + } + const auto diagnostics = runtimeDataDispatcher.BuildDiagnosticsSnapshot( + runtimeAdapter != nullptr + ? std::vector{runtimeAdapter->GetStatus()} + : std::vector{} + ); + if ((diagnosticsWriteFrame % 15ULL) == 0ULL) { + (void)MetaCore::MetaCoreWriteRuntimeDiagnosticsSnapshot(diagnosticsPath, diagnostics, typeRegistry); + } + ++diagnosticsWriteFrame; + if (runtimeAdapter != nullptr && runtimeAdapter->GetStatus().State != lastReportedSourceState) { + std::cout << "MetaCorePlayer: data source state changed to " + << static_cast(runtimeAdapter->GetStatus().State) + << " error=" << runtimeAdapter->GetStatus().LastError << '\n'; + lastReportedSourceState = runtimeAdapter->GetStatus().State; + } + if (diagnostics.HasFaults) { + for (const MetaCore::MetaCoreRuntimeBindingStatus& bindingStatus : diagnostics.BindingStatuses) { + if (!bindingStatus.Healthy || bindingStatus.Stale) { + std::cout << "MetaCorePlayer: binding issue id=" << bindingStatus.BindingId + << " stale=" << (bindingStatus.Stale ? "true" : "false") + << " error=" << bindingStatus.LastError << '\n'; + } + } + } viewportRenderer.RenderSceneToViewport(scene, MetaCoreBuildPlayerSceneView()); renderDevice.RenderFrame(); renderDevice.PresentFrame(); diff --git a/CMakeLists.txt b/CMakeLists.txt index 054011e..123f791 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,19 +30,90 @@ metacore_prepare_panda3d() find_package(glm CONFIG REQUIRED) find_package(imgui CONFIG REQUIRED) +set(METACORE_COMMON_WARNINGS) +if(MSVC) + set(METACORE_COMMON_WARNINGS /W4 /permissive- /EHsc) +else() + set(METACORE_COMMON_WARNINGS -Wall -Wextra -Wpedantic) +endif() + +add_executable(MetaCoreHeaderTool + Tools/MetaCoreHeaderTool/main.cpp +) + +target_compile_options(MetaCoreHeaderTool PRIVATE ${METACORE_COMMON_WARNINGS}) + +add_executable(MetaCoreRuntimeConfigTool + Tools/MetaCoreRuntimeConfigTool/main.cpp +) + +target_compile_options(MetaCoreRuntimeConfigTool PRIVATE ${METACORE_COMMON_WARNINGS}) + +add_executable(MetaCoreTcpSenderTool + Tools/MetaCoreTcpSenderTool/main.cpp +) + +target_compile_options(MetaCoreTcpSenderTool PRIVATE ${METACORE_COMMON_WARNINGS}) +target_link_libraries(MetaCoreTcpSenderTool PRIVATE ws2_32) + +function(metacore_generate_reflection module_name function_name output_var) + set(generated_headers ${ARGN}) + set(absolute_headers) + foreach(header IN LISTS generated_headers) + list(APPEND absolute_headers "${CMAKE_SOURCE_DIR}/${header}") + endforeach() + + set(generated_cpp "${CMAKE_BINARY_DIR}/Generated/${module_name}/${module_name}GeneratedReflection.cpp") + add_custom_command( + OUTPUT "${generated_cpp}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/Generated/${module_name}" + COMMAND $ + --output "${generated_cpp}" + --function "${function_name}" + --module "${module_name}" + ${absolute_headers} + DEPENDS + MetaCoreHeaderTool + ${absolute_headers} + VERBATIM + ) + + set(${output_var} "${generated_cpp}" PARENT_SCOPE) +endfunction() + set(METACORE_FOUNDATION_HEADERS + Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreArchive.h + Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetGuid.h + Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreGeneratedReflection.h + Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreHash.h Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreId.h Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreLogService.h + Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCorePackage.h + Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreReflection.h ) set(METACORE_FOUNDATION_SOURCES + Source/MetaCoreFoundation/Private/MetaCoreArchive.cpp + Source/MetaCoreFoundation/Private/MetaCoreAssetGuid.cpp + Source/MetaCoreFoundation/Private/MetaCoreHash.cpp Source/MetaCoreFoundation/Private/MetaCoreId.cpp Source/MetaCoreFoundation/Private/MetaCoreLogService.cpp + Source/MetaCoreFoundation/Private/MetaCorePackage.cpp + Source/MetaCoreFoundation/Private/MetaCoreReflection.cpp +) + +metacore_generate_reflection( + Foundation + MetaCoreRegisterFoundationGeneratedTypes + METACORE_FOUNDATION_GENERATED_SOURCE + Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetGuid.h + Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCorePackage.h ) add_library(MetaCoreFoundation STATIC ${METACORE_FOUNDATION_HEADERS} ${METACORE_FOUNDATION_SOURCES} + ${METACORE_FOUNDATION_GENERATED_SOURCE} ) target_include_directories(MetaCoreFoundation @@ -50,12 +121,10 @@ target_include_directories(MetaCoreFoundation Source/MetaCoreFoundation/Public ) -set(METACORE_COMMON_WARNINGS) -if(MSVC) - set(METACORE_COMMON_WARNINGS /W4 /permissive- /EHsc) -else() - set(METACORE_COMMON_WARNINGS -Wall -Wextra -Wpedantic) -endif() +target_link_libraries(MetaCoreFoundation + PUBLIC + glm::glm +) target_compile_options(MetaCoreFoundation PRIVATE ${METACORE_COMMON_WARNINGS}) @@ -92,16 +161,30 @@ target_compile_definitions(MetaCorePlatform PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX set(METACORE_SCENE_HEADERS Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h + Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h + Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScenePackage.h Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h ) set(METACORE_SCENE_SOURCES + Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp Source/MetaCoreScene/Private/MetaCoreScene.cpp ) +metacore_generate_reflection( + Scene + MetaCoreRegisterSceneGeneratedTypes + METACORE_SCENE_GENERATED_SOURCE + Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h + Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h + Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h + Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h +) + add_library(MetaCoreScene STATIC ${METACORE_SCENE_HEADERS} ${METACORE_SCENE_SOURCES} + ${METACORE_SCENE_GENERATED_SOURCE} ) target_include_directories(MetaCoreScene @@ -151,11 +234,64 @@ target_link_libraries(MetaCoreRender target_compile_options(MetaCoreRender PRIVATE ${METACORE_COMMON_WARNINGS}) +set(METACORE_RUNTIME_DATA_HEADERS + Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataDispatcher.h + Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h + Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataSource.h + Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h +) + +set(METACORE_RUNTIME_DATA_SOURCES + Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataDispatcher.cpp + Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataProject.cpp + Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataSource.cpp +) + +metacore_generate_reflection( + RuntimeData + MetaCoreRegisterRuntimeDataGeneratedTypes + METACORE_RUNTIME_DATA_GENERATED_SOURCE + Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h + Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h +) + +add_library(MetaCoreRuntimeData STATIC + ${METACORE_RUNTIME_DATA_HEADERS} + ${METACORE_RUNTIME_DATA_SOURCES} + ${METACORE_RUNTIME_DATA_GENERATED_SOURCE} +) + +target_include_directories(MetaCoreRuntimeData + PUBLIC + Source/MetaCoreRuntimeData/Public +) + +target_link_libraries(MetaCoreRuntimeData + PUBLIC + MetaCoreFoundation + MetaCoreScene + glm::glm + PRIVATE + ws2_32 +) + +target_compile_options(MetaCoreRuntimeData PRIVATE ${METACORE_COMMON_WARNINGS}) + +target_link_libraries(MetaCoreRuntimeConfigTool + PRIVATE + MetaCoreFoundation + MetaCoreRuntimeData + MetaCoreScene +) + set(METACORE_EDITOR_HEADERS + Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreBuiltinModules.h Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h + Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorCommandService.h Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorModule.h + Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h ) @@ -166,20 +302,31 @@ set(METACORE_EDITOR_PRIVATE_HEADERS ) set(METACORE_EDITOR_SOURCES + Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.cpp Source/MetaCoreEditor/Private/MetaCoreEditorCommandService.cpp Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp + Source/MetaCoreEditor/Private/MetaCoreEditorServices.cpp Source/MetaCoreEditor/Private/MetaCoreEditorModule.cpp Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp third_party/ImGuizmo/ImGuizmo.cpp ) +metacore_generate_reflection( + Editor + MetaCoreRegisterEditorGeneratedTypes + METACORE_EDITOR_GENERATED_SOURCE + Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h + Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h +) + add_library(MetaCoreEditor STATIC ${METACORE_EDITOR_HEADERS} ${METACORE_EDITOR_PRIVATE_HEADERS} ${METACORE_EDITOR_SOURCES} + ${METACORE_EDITOR_GENERATED_SOURCE} ) target_include_directories(MetaCoreEditor @@ -187,6 +334,7 @@ target_include_directories(MetaCoreEditor Source/MetaCoreEditor/Public PRIVATE Source/MetaCoreEditor/Private + third_party third_party/ImGuizmo ) @@ -195,6 +343,7 @@ target_link_libraries(MetaCoreEditor MetaCoreFoundation MetaCorePlatform MetaCoreRender + MetaCoreRuntimeData MetaCoreScene glm::glm imgui::imgui @@ -223,6 +372,7 @@ target_link_libraries(MetaCorePlayer MetaCoreFoundation MetaCorePlatform MetaCoreRender + MetaCoreRuntimeData MetaCoreScene MetaCorePanda3D::SDK ) @@ -239,6 +389,7 @@ if(METACORE_BUILD_TESTS) target_link_libraries(MetaCoreSmokeTests PRIVATE MetaCoreEditor + MetaCoreRuntimeData ) metacore_stage_panda3d_runtime(MetaCoreSmokeTests) diff --git a/SandboxProject/Assets/Materials/default_pbr.material.json.mcasset b/SandboxProject/Assets/Materials/default_pbr.material.json.mcasset new file mode 100644 index 0000000..1e5ad89 Binary files /dev/null and b/SandboxProject/Assets/Materials/default_pbr.material.json.mcasset differ diff --git a/SandboxProject/Assets/Materials/default_pbr.material.json.mcmeta b/SandboxProject/Assets/Materials/default_pbr.material.json.mcmeta new file mode 100644 index 0000000..bbffce8 Binary files /dev/null and b/SandboxProject/Assets/Materials/default_pbr.material.json.mcmeta differ diff --git a/SandboxProject/Assets/Materials/default_pbr.material.json.meta b/SandboxProject/Assets/Materials/default_pbr.material.json.meta deleted file mode 100644 index 806cdec..0000000 --- a/SandboxProject/Assets/Materials/default_pbr.material.json.meta +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "00005031af6c28bc73f3772350037bcf00000005", - "relative_path": "Assets/Materials/default_pbr.material.json", - "type": "material" -} \ No newline at end of file diff --git a/SandboxProject/Assets/Prefabs/Cube.mcprefab b/SandboxProject/Assets/Prefabs/Cube.mcprefab new file mode 100644 index 0000000..01c2ec4 Binary files /dev/null and b/SandboxProject/Assets/Prefabs/Cube.mcprefab differ diff --git a/SandboxProject/Assets/UI/main_menu.rcss.mcasset b/SandboxProject/Assets/UI/main_menu.rcss.mcasset new file mode 100644 index 0000000..905eddc Binary files /dev/null and b/SandboxProject/Assets/UI/main_menu.rcss.mcasset differ diff --git a/SandboxProject/Assets/UI/main_menu.rcss.mcmeta b/SandboxProject/Assets/UI/main_menu.rcss.mcmeta new file mode 100644 index 0000000..7bedd69 Binary files /dev/null and b/SandboxProject/Assets/UI/main_menu.rcss.mcmeta differ diff --git a/SandboxProject/Assets/UI/main_menu.rcss.meta b/SandboxProject/Assets/UI/main_menu.rcss.meta deleted file mode 100644 index 367f6b6..0000000 --- a/SandboxProject/Assets/UI/main_menu.rcss.meta +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "00005031af6c1e30843b82e4691e8cba00000004", - "relative_path": "Assets/UI/main_menu.rcss", - "type": "ui_stylesheet" -} \ No newline at end of file diff --git a/SandboxProject/Assets/UI/main_menu.rml.mcasset b/SandboxProject/Assets/UI/main_menu.rml.mcasset new file mode 100644 index 0000000..6b24688 Binary files /dev/null and b/SandboxProject/Assets/UI/main_menu.rml.mcasset differ diff --git a/SandboxProject/Assets/UI/main_menu.rml.mcmeta b/SandboxProject/Assets/UI/main_menu.rml.mcmeta new file mode 100644 index 0000000..5e478cb Binary files /dev/null and b/SandboxProject/Assets/UI/main_menu.rml.mcmeta differ diff --git a/SandboxProject/Assets/UI/main_menu.rml.meta b/SandboxProject/Assets/UI/main_menu.rml.meta deleted file mode 100644 index 16c3705..0000000 --- a/SandboxProject/Assets/UI/main_menu.rml.meta +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "00005031af6bf9783f1fb78dcaa5ed8900000003", - "relative_path": "Assets/UI/main_menu.rml", - "type": "ui_document" -} \ No newline at end of file diff --git a/SandboxProject/Library/AssetDB.json b/SandboxProject/Library/AssetDB.json deleted file mode 100644 index 5e304af..0000000 --- a/SandboxProject/Library/AssetDB.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "records": [ - { - "id": "00005031af6bf9783f1fb78dcaa5ed8900000003", - "relative_path": "Assets/UI/main_menu.rml", - "type": "ui_document" - }, - { - "id": "00005031af6c1e30843b82e4691e8cba00000004", - "relative_path": "Assets/UI/main_menu.rcss", - "type": "ui_stylesheet" - }, - { - "id": "00005031af6c28bc73f3772350037bcf00000005", - "relative_path": "Assets/Materials/default_pbr.material.json", - "type": "material" - } - ] -} \ No newline at end of file diff --git a/SandboxProject/Library/Cooked/Windows/0e9b9f20-3baf-49b8-8d47-cdf60cbf3cc2_6a59877fea05820d.mccooked b/SandboxProject/Library/Cooked/Windows/0e9b9f20-3baf-49b8-8d47-cdf60cbf3cc2_6a59877fea05820d.mccooked new file mode 100644 index 0000000..07ddba3 Binary files /dev/null and b/SandboxProject/Library/Cooked/Windows/0e9b9f20-3baf-49b8-8d47-cdf60cbf3cc2_6a59877fea05820d.mccooked differ diff --git a/SandboxProject/Library/Cooked/Windows/17d06f0f-184b-4459-88ba-0adc5983bed7_5b4b16b79810d5df.mccooked b/SandboxProject/Library/Cooked/Windows/17d06f0f-184b-4459-88ba-0adc5983bed7_5b4b16b79810d5df.mccooked new file mode 100644 index 0000000..905eddc Binary files /dev/null and b/SandboxProject/Library/Cooked/Windows/17d06f0f-184b-4459-88ba-0adc5983bed7_5b4b16b79810d5df.mccooked differ diff --git a/SandboxProject/Library/Cooked/Windows/42242648-1f02-40b3-bc58-0275083344ed_a72c3fb55ebc2c3d.mccooked b/SandboxProject/Library/Cooked/Windows/42242648-1f02-40b3-bc58-0275083344ed_a72c3fb55ebc2c3d.mccooked new file mode 100644 index 0000000..6b24688 Binary files /dev/null and b/SandboxProject/Library/Cooked/Windows/42242648-1f02-40b3-bc58-0275083344ed_a72c3fb55ebc2c3d.mccooked differ diff --git a/SandboxProject/Library/Cooked/Windows/4cba927c-93a9-430f-aa53-84eb00a2baec_7cd233c4a5fef357.mccooked b/SandboxProject/Library/Cooked/Windows/4cba927c-93a9-430f-aa53-84eb00a2baec_7cd233c4a5fef357.mccooked new file mode 100644 index 0000000..01c2ec4 Binary files /dev/null and b/SandboxProject/Library/Cooked/Windows/4cba927c-93a9-430f-aa53-84eb00a2baec_7cd233c4a5fef357.mccooked differ diff --git a/SandboxProject/Library/Cooked/Windows/5196ffd5-8ef9-432e-a292-7e4354e84f9f_a2880f11bf5aa3ad.mccooked b/SandboxProject/Library/Cooked/Windows/5196ffd5-8ef9-432e-a292-7e4354e84f9f_a2880f11bf5aa3ad.mccooked new file mode 100644 index 0000000..1e5ad89 Binary files /dev/null and b/SandboxProject/Library/Cooked/Windows/5196ffd5-8ef9-432e-a292-7e4354e84f9f_a2880f11bf5aa3ad.mccooked differ diff --git a/SandboxProject/Library/Cooked/Windows/CookManifest.bin b/SandboxProject/Library/Cooked/Windows/CookManifest.bin new file mode 100644 index 0000000..c58a1e6 Binary files /dev/null and b/SandboxProject/Library/Cooked/Windows/CookManifest.bin differ diff --git a/SandboxProject/MetaCore.project.json b/SandboxProject/MetaCore.project.json index c11066f..d06887e 100644 --- a/SandboxProject/MetaCore.project.json +++ b/SandboxProject/MetaCore.project.json @@ -1,8 +1,8 @@ { "name": "MetaCoreSample", + "version": "0.1.0", + "startup_scene": "Scenes/Main.mcscene", "scenes": [ - "Scenes/Main.mcscene.json" - ], - "startup_scene": "Scenes/Main.mcscene.json", - "version": "0.1.0" -} \ No newline at end of file + "Scenes/Main.mcscene" + ] +} diff --git a/SandboxProject/Scenes/Main.mcscene b/SandboxProject/Scenes/Main.mcscene new file mode 100644 index 0000000..07ddba3 Binary files /dev/null and b/SandboxProject/Scenes/Main.mcscene differ diff --git a/SandboxProject/Scenes/Main.mcscene.json b/SandboxProject/Scenes/Main.mcscene.json deleted file mode 100644 index 1028dce..0000000 --- a/SandboxProject/Scenes/Main.mcscene.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "name": "Main", - "objects": [ - { - "camera": { - "far_clip": 1000, - "fov_degrees": 60, - "near_clip": 0.100000001490116, - "primary": true - }, - "editor_metadata": { - "editor_only": false, - "locked": false, - "selected": false - }, - "id": "00005031af666d280adc4216764a07c100000001", - "name": "Main Camera", - "parent_id": "", - "transform": { - "position": [ - 0, - -10, - 3 - ], - "rotation": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - } - }, - { - "editor_metadata": { - "editor_only": false, - "locked": false, - "selected": false - }, - "id": "00005031af669a14494b5bcf7b0288f500000002", - "light": { - "color": [ - 1, - 1, - 1 - ], - "intensity": 1, - "range": 10, - "spot_angle_degrees": 45, - "type": "directional" - }, - "name": "Directional Light", - "parent_id": "", - "transform": { - "position": [ - 0, - 0, - 0 - ], - "rotation": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - } - }, - { - "editor_metadata": { - "editor_only": false, - "locked": false, - "selected": false - }, - "id": "00005031af7e8fac310b0d408494fcfb00000008", - "mesh_renderer": { - "material_asset_id": "builtin://editor-default", - "mesh_asset_id": "builtin://box", - "visible": true - }, - "name": "Demo Cube", - "parent_id": "", - "transform": { - "position": [ - 0, - 0, - 0 - ], - "rotation": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - } - }, - { - "editor_metadata": { - "editor_only": false, - "locked": false, - "selected": false - }, - "id": "0000399eef6307dc90859f938da98af700000003", - "name": "UI Root", - "parent_id": "", - "transform": { - "position": [ - 0, - 0, - 0 - ], - "rotation": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - "ui_document": { - "document_path": "Assets/UI/main_menu.rml", - "stylesheet_path": "Assets/UI/main_menu.rcss", - "visible": true - } - } - ] -} \ No newline at end of file diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp new file mode 100644 index 0000000..d3842b9 --- /dev/null +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -0,0 +1,2882 @@ +#include "MetaCoreEditor/MetaCoreBuiltinModules.h" + +#include "MetaCoreEditor/MetaCoreEditorAssetTypes.h" +#include "MetaCoreEditor/MetaCoreEditorContext.h" +#include "MetaCoreEditor/MetaCoreEditorServices.h" +#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" +#include "MetaCoreFoundation/MetaCoreHash.h" +#include "MetaCorePlatform/MetaCoreInput.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +namespace { + +std::unordered_map& MetaCoreGetComponentInspectorEditSnapshots() { + static std::unordered_map snapshots; + return snapshots; +} + +void MetaCoreTrackComponentInspectorEdit(MetaCoreEditorContext& editorContext, const char* commandLabel, bool allowMerge) { + const ImGuiID itemId = ImGui::GetItemID(); + if (itemId == 0) { + return; + } + + auto& snapshots = MetaCoreGetComponentInspectorEditSnapshots(); + if (ImGui::IsItemActivated()) { + snapshots[itemId] = editorContext.CaptureStateSnapshot(); + } + + if (ImGui::IsItemDeactivatedAfterEdit()) { + const auto iterator = snapshots.find(itemId); + if (iterator != snapshots.end()) { + const MetaCoreEditorStateSnapshot afterSnapshot = editorContext.CaptureStateSnapshot(); + (void)editorContext.CommitStateTransition(commandLabel, iterator->second, afterSnapshot, allowMerge); + snapshots.erase(iterator); + } + } +} + +[[nodiscard]] bool MetaCoreNearlyEqual(float lhs, float rhs) { + return std::abs(lhs - rhs) <= 0.0001F; +} + +[[nodiscard]] bool MetaCoreNearlyEqualVec3(const glm::vec3& lhs, const glm::vec3& rhs) { + return MetaCoreNearlyEqual(lhs.x, rhs.x) && + MetaCoreNearlyEqual(lhs.y, rhs.y) && + MetaCoreNearlyEqual(lhs.z, rhs.z); +} + +template +void MetaCoreForEachSelectedGameObject(MetaCoreEditorContext& editorContext, TAccessor&& accessor); + +template +std::optional MetaCoreGetSharedSelectedValue( + MetaCoreEditorContext& editorContext, + TAccessor&& accessor, + TComparer&& comparer +); + +template +void MetaCoreApplyValueToSelectedObjects(MetaCoreEditorContext& editorContext, TAccessor&& accessor, const TValue& value); + +template +[[nodiscard]] std::optional MetaCoreReadTypedPackagePayload( + const MetaCorePackageDocument& package, + const MetaCoreTypeRegistry& registry, + std::string_view expectedTypeName +); + +[[nodiscard]] MetaCorePackageDocument MetaCoreBuildTypedPackage( + MetaCorePackageType packageType, + const MetaCoreAssetGuid& assetGuid, + const std::string& objectName, + std::string_view typeName, + std::uint64_t sourceHash, + std::vector payload +); + +[[nodiscard]] std::optional MetaCoreLoadPrefabDocumentForInstance( + MetaCoreEditorContext& editorContext, + const MetaCoreGameObject& gameObject +); + +[[nodiscard]] const MetaCoreGameObject* MetaCoreFindPrefabSourceObject( + const MetaCorePrefabDocument& prefabDocument, + MetaCoreId prefabObjectId +); + +[[nodiscard]] bool MetaCoreApplySelectedPrefabField( + MetaCoreEditorContext& editorContext, + std::string_view componentTypeId, + std::string_view fieldId +); + +[[nodiscard]] bool MetaCoreRevertSelectedPrefabField( + MetaCoreEditorContext& editorContext, + std::string_view componentTypeId, + std::string_view fieldId +); + +void MetaCoreDrawCameraComponentInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& gameObject) { + if (!gameObject.Camera.has_value()) { + return; + } + + const MetaCoreGameObject* prefabObject = nullptr; + if (const auto prefabDocument = MetaCoreLoadPrefabDocumentForInstance(editorContext, gameObject); prefabDocument.has_value() && + gameObject.PrefabInstance.has_value()) { + prefabObject = MetaCoreFindPrefabSourceObject(*prefabDocument, gameObject.PrefabInstance->PrefabObjectId); + } + + const bool multiEdit = editorContext.GetSelectedObjectIds().size() > 1; + if (multiEdit) { + ImGui::TextDisabled("Multi-edit %zu objects", editorContext.GetSelectedObjectIds().size()); + } + + float fov = MetaCoreGetSharedSelectedValue( + editorContext, + [](MetaCoreGameObject& selectedObject) -> std::optional { + return selectedObject.Camera.has_value() ? std::optional(selectedObject.Camera->FieldOfViewDegrees) : std::nullopt; + }, + [](float lhs, float rhs) { return MetaCoreNearlyEqual(lhs, rhs); } + ).value_or(gameObject.Camera->FieldOfViewDegrees); + if (multiEdit && !MetaCoreGetSharedSelectedValue( + editorContext, + [](MetaCoreGameObject& selectedObject) -> std::optional { + return selectedObject.Camera.has_value() ? std::optional(selectedObject.Camera->FieldOfViewDegrees) : std::nullopt; + }, + [](float lhs, float rhs) { return MetaCoreNearlyEqual(lhs, rhs); } + ).has_value()) { + ImGui::TextDisabled("FOV: Mixed"); + } + if (prefabObject != nullptr && prefabObject->Camera.has_value() && + !MetaCoreNearlyEqual(gameObject.Camera->FieldOfViewDegrees, prefabObject->Camera->FieldOfViewDegrees)) { + ImGui::TextColored(ImVec4(0.93F, 0.72F, 0.18F, 1.0F), "FOV Overrides"); + ImGui::SameLine(); + if (ImGui::SmallButton("Apply##CameraFOV")) { + (void)MetaCoreApplySelectedPrefabField(editorContext, "Camera", "fov"); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Revert##CameraFOV")) { + (void)MetaCoreRevertSelectedPrefabField(editorContext, "Camera", "fov"); + fov = prefabObject->Camera->FieldOfViewDegrees; + } + } + ImGui::DragFloat("FOV", &fov, 0.2F, 1.0F, 179.0F); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", true); + if (ImGui::IsItemEdited()) { + MetaCoreApplyValueToSelectedObjects( + editorContext, + [](MetaCoreGameObject& selectedObject) -> float* { + return selectedObject.Camera.has_value() ? &selectedObject.Camera->FieldOfViewDegrees : nullptr; + }, + fov + ); + } + + const auto sharedNearClip = MetaCoreGetSharedSelectedValue( + editorContext, + [](MetaCoreGameObject& selectedObject) -> std::optional { + return selectedObject.Camera.has_value() ? std::optional(selectedObject.Camera->NearClip) : std::nullopt; + }, + [](float lhs, float rhs) { return MetaCoreNearlyEqual(lhs, rhs); } + ); + float nearClip = sharedNearClip.value_or(gameObject.Camera->NearClip); + if (multiEdit && !sharedNearClip.has_value()) { + ImGui::TextDisabled("Near Clip: Mixed"); + } + if (prefabObject != nullptr && prefabObject->Camera.has_value() && + !MetaCoreNearlyEqual(gameObject.Camera->NearClip, prefabObject->Camera->NearClip)) { + ImGui::TextColored(ImVec4(0.93F, 0.72F, 0.18F, 1.0F), "Near Clip Overrides"); + ImGui::SameLine(); + if (ImGui::SmallButton("Apply##CameraNear")) { + (void)MetaCoreApplySelectedPrefabField(editorContext, "Camera", "near_clip"); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Revert##CameraNear")) { + (void)MetaCoreRevertSelectedPrefabField(editorContext, "Camera", "near_clip"); + nearClip = prefabObject->Camera->NearClip; + } + } + ImGui::DragFloat("Near Clip", &nearClip, 0.01F, 0.001F, gameObject.Camera->FarClip); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", true); + if (ImGui::IsItemEdited()) { + MetaCoreApplyValueToSelectedObjects( + editorContext, + [](MetaCoreGameObject& selectedObject) -> float* { + return selectedObject.Camera.has_value() ? &selectedObject.Camera->NearClip : nullptr; + }, + nearClip + ); + } + + const auto sharedFarClip = MetaCoreGetSharedSelectedValue( + editorContext, + [](MetaCoreGameObject& selectedObject) -> std::optional { + return selectedObject.Camera.has_value() ? std::optional(selectedObject.Camera->FarClip) : std::nullopt; + }, + [](float lhs, float rhs) { return MetaCoreNearlyEqual(lhs, rhs); } + ); + float farClip = sharedFarClip.value_or(gameObject.Camera->FarClip); + if (multiEdit && !sharedFarClip.has_value()) { + ImGui::TextDisabled("Far Clip: Mixed"); + } + if (prefabObject != nullptr && prefabObject->Camera.has_value() && + !MetaCoreNearlyEqual(gameObject.Camera->FarClip, prefabObject->Camera->FarClip)) { + ImGui::TextColored(ImVec4(0.93F, 0.72F, 0.18F, 1.0F), "Far Clip Overrides"); + ImGui::SameLine(); + if (ImGui::SmallButton("Apply##CameraFar")) { + (void)MetaCoreApplySelectedPrefabField(editorContext, "Camera", "far_clip"); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Revert##CameraFar")) { + (void)MetaCoreRevertSelectedPrefabField(editorContext, "Camera", "far_clip"); + farClip = prefabObject->Camera->FarClip; + } + } + ImGui::DragFloat("Far Clip", &farClip, 0.1F, nearClip, 10000.0F); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", true); + if (ImGui::IsItemEdited()) { + MetaCoreApplyValueToSelectedObjects( + editorContext, + [](MetaCoreGameObject& selectedObject) -> float* { + return selectedObject.Camera.has_value() ? &selectedObject.Camera->FarClip : nullptr; + }, + farClip + ); + } + + const auto sharedPrimary = MetaCoreGetSharedSelectedValue( + editorContext, + [](MetaCoreGameObject& selectedObject) -> std::optional { + return selectedObject.Camera.has_value() ? std::optional(selectedObject.Camera->IsPrimary) : std::nullopt; + }, + [](bool lhs, bool rhs) { return lhs == rhs; } + ); + bool isPrimary = sharedPrimary.value_or(gameObject.Camera->IsPrimary); + if (multiEdit && !sharedPrimary.has_value()) { + ImGui::TextDisabled("Primary: Mixed"); + } + if (prefabObject != nullptr && prefabObject->Camera.has_value() && + gameObject.Camera->IsPrimary != prefabObject->Camera->IsPrimary) { + ImGui::TextColored(ImVec4(0.93F, 0.72F, 0.18F, 1.0F), "Primary Overrides"); + ImGui::SameLine(); + if (ImGui::SmallButton("Apply##CameraPrimary")) { + (void)MetaCoreApplySelectedPrefabField(editorContext, "Camera", "primary"); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Revert##CameraPrimary")) { + (void)MetaCoreRevertSelectedPrefabField(editorContext, "Camera", "primary"); + isPrimary = prefabObject->Camera->IsPrimary; + } + } + ImGui::Checkbox("Primary", &isPrimary); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", false); + if (ImGui::IsItemEdited()) { + MetaCoreApplyValueToSelectedObjects( + editorContext, + [](MetaCoreGameObject& selectedObject) -> bool* { + return selectedObject.Camera.has_value() ? &selectedObject.Camera->IsPrimary : nullptr; + }, + isPrimary + ); + } +} + +void MetaCoreDrawLightComponentInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& gameObject) { + if (!gameObject.Light.has_value()) { + return; + } + + const MetaCoreGameObject* prefabObject = nullptr; + if (const auto prefabDocument = MetaCoreLoadPrefabDocumentForInstance(editorContext, gameObject); prefabDocument.has_value() && + gameObject.PrefabInstance.has_value()) { + prefabObject = MetaCoreFindPrefabSourceObject(*prefabDocument, gameObject.PrefabInstance->PrefabObjectId); + } + + const bool multiEdit = editorContext.GetSelectedObjectIds().size() > 1; + if (multiEdit) { + ImGui::TextDisabled("Multi-edit %zu objects", editorContext.GetSelectedObjectIds().size()); + } + + const auto sharedColor = MetaCoreGetSharedSelectedValue( + editorContext, + [](MetaCoreGameObject& selectedObject) -> std::optional { + return selectedObject.Light.has_value() ? std::optional(selectedObject.Light->Color) : std::nullopt; + }, + [](const glm::vec3& lhs, const glm::vec3& rhs) { return MetaCoreNearlyEqualVec3(lhs, rhs); } + ); + glm::vec3 color = sharedColor.value_or(gameObject.Light->Color); + if (multiEdit && !sharedColor.has_value()) { + ImGui::TextDisabled("Color: Mixed"); + } + if (prefabObject != nullptr && prefabObject->Light.has_value() && + !MetaCoreNearlyEqualVec3(gameObject.Light->Color, prefabObject->Light->Color)) { + ImGui::TextColored(ImVec4(0.93F, 0.72F, 0.18F, 1.0F), "Color Overrides"); + ImGui::SameLine(); + if (ImGui::SmallButton("Apply##LightColor")) { + (void)MetaCoreApplySelectedPrefabField(editorContext, "Light", "color"); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Revert##LightColor")) { + (void)MetaCoreRevertSelectedPrefabField(editorContext, "Light", "color"); + color = prefabObject->Light->Color; + } + } + ImGui::ColorEdit3("Color", glm::value_ptr(color)); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", true); + if (ImGui::IsItemEdited()) { + MetaCoreApplyValueToSelectedObjects( + editorContext, + [](MetaCoreGameObject& selectedObject) -> glm::vec3* { + return selectedObject.Light.has_value() ? &selectedObject.Light->Color : nullptr; + }, + color + ); + } + + const auto sharedIntensity = MetaCoreGetSharedSelectedValue( + editorContext, + [](MetaCoreGameObject& selectedObject) -> std::optional { + return selectedObject.Light.has_value() ? std::optional(selectedObject.Light->Intensity) : std::nullopt; + }, + [](float lhs, float rhs) { return MetaCoreNearlyEqual(lhs, rhs); } + ); + float intensity = sharedIntensity.value_or(gameObject.Light->Intensity); + if (multiEdit && !sharedIntensity.has_value()) { + ImGui::TextDisabled("Intensity: Mixed"); + } + if (prefabObject != nullptr && prefabObject->Light.has_value() && + !MetaCoreNearlyEqual(gameObject.Light->Intensity, prefabObject->Light->Intensity)) { + ImGui::TextColored(ImVec4(0.93F, 0.72F, 0.18F, 1.0F), "Intensity Overrides"); + ImGui::SameLine(); + if (ImGui::SmallButton("Apply##LightIntensity")) { + (void)MetaCoreApplySelectedPrefabField(editorContext, "Light", "intensity"); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Revert##LightIntensity")) { + (void)MetaCoreRevertSelectedPrefabField(editorContext, "Light", "intensity"); + intensity = prefabObject->Light->Intensity; + } + } + ImGui::DragFloat("Intensity", &intensity, 0.05F, 0.0F, 8.0F); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", true); + if (ImGui::IsItemEdited()) { + MetaCoreApplyValueToSelectedObjects( + editorContext, + [](MetaCoreGameObject& selectedObject) -> float* { + return selectedObject.Light.has_value() ? &selectedObject.Light->Intensity : nullptr; + }, + intensity + ); + } +} + +void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& gameObject) { + if (!gameObject.MeshRenderer.has_value()) { + return; + } + + const MetaCoreGameObject* prefabObject = nullptr; + if (const auto prefabDocument = MetaCoreLoadPrefabDocumentForInstance(editorContext, gameObject); prefabDocument.has_value() && + gameObject.PrefabInstance.has_value()) { + prefabObject = MetaCoreFindPrefabSourceObject(*prefabDocument, gameObject.PrefabInstance->PrefabObjectId); + } + + const bool multiEdit = editorContext.GetSelectedObjectIds().size() > 1; + if (multiEdit) { + ImGui::TextDisabled("Multi-edit %zu objects", editorContext.GetSelectedObjectIds().size()); + } + + const auto sharedColor = MetaCoreGetSharedSelectedValue( + editorContext, + [](MetaCoreGameObject& selectedObject) -> std::optional { + return selectedObject.MeshRenderer.has_value() ? std::optional(selectedObject.MeshRenderer->BaseColor) : std::nullopt; + }, + [](const glm::vec3& lhs, const glm::vec3& rhs) { return MetaCoreNearlyEqualVec3(lhs, rhs); } + ); + glm::vec3 baseColor = sharedColor.value_or(gameObject.MeshRenderer->BaseColor); + if (multiEdit && !sharedColor.has_value()) { + ImGui::TextDisabled("BaseColor: Mixed"); + } + if (prefabObject != nullptr && prefabObject->MeshRenderer.has_value() && + !MetaCoreNearlyEqualVec3(gameObject.MeshRenderer->BaseColor, prefabObject->MeshRenderer->BaseColor)) { + ImGui::TextColored(ImVec4(0.93F, 0.72F, 0.18F, 1.0F), "BaseColor Overrides"); + ImGui::SameLine(); + if (ImGui::SmallButton("Apply##MeshBaseColor")) { + (void)MetaCoreApplySelectedPrefabField(editorContext, "MeshRenderer", "base_color"); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Revert##MeshBaseColor")) { + (void)MetaCoreRevertSelectedPrefabField(editorContext, "MeshRenderer", "base_color"); + baseColor = prefabObject->MeshRenderer->BaseColor; + } + } + ImGui::ColorEdit3("BaseColor", glm::value_ptr(baseColor)); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", true); + if (ImGui::IsItemEdited()) { + MetaCoreApplyValueToSelectedObjects( + editorContext, + [](MetaCoreGameObject& selectedObject) -> glm::vec3* { + return selectedObject.MeshRenderer.has_value() ? &selectedObject.MeshRenderer->BaseColor : nullptr; + }, + baseColor + ); + } + + const auto sharedVisible = MetaCoreGetSharedSelectedValue( + editorContext, + [](MetaCoreGameObject& selectedObject) -> std::optional { + return selectedObject.MeshRenderer.has_value() ? std::optional(selectedObject.MeshRenderer->Visible) : std::nullopt; + }, + [](bool lhs, bool rhs) { return lhs == rhs; } + ); + bool visible = sharedVisible.value_or(gameObject.MeshRenderer->Visible); + if (multiEdit && !sharedVisible.has_value()) { + ImGui::TextDisabled("Visible: Mixed"); + } + if (prefabObject != nullptr && prefabObject->MeshRenderer.has_value() && + gameObject.MeshRenderer->Visible != prefabObject->MeshRenderer->Visible) { + ImGui::TextColored(ImVec4(0.93F, 0.72F, 0.18F, 1.0F), "Visible Overrides"); + ImGui::SameLine(); + if (ImGui::SmallButton("Apply##MeshVisible")) { + (void)MetaCoreApplySelectedPrefabField(editorContext, "MeshRenderer", "visible"); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Revert##MeshVisible")) { + (void)MetaCoreRevertSelectedPrefabField(editorContext, "MeshRenderer", "visible"); + visible = prefabObject->MeshRenderer->Visible; + } + } + ImGui::Checkbox("Visible", &visible); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", false); + if (ImGui::IsItemEdited()) { + MetaCoreApplyValueToSelectedObjects( + editorContext, + [](MetaCoreGameObject& selectedObject) -> bool* { + return selectedObject.MeshRenderer.has_value() ? &selectedObject.MeshRenderer->Visible : nullptr; + }, + visible + ); + } +} + +template +void MetaCoreForEachSelectedGameObject(MetaCoreEditorContext& editorContext, TAccessor&& accessor) { + for (const MetaCoreId selectedId : editorContext.GetSelectedObjectIds()) { + if (MetaCoreGameObject* selectedObject = editorContext.GetScene().FindGameObject(selectedId); selectedObject != nullptr) { + accessor(*selectedObject); + } + } +} + +template +std::optional MetaCoreGetSharedSelectedValue( + MetaCoreEditorContext& editorContext, + TAccessor&& accessor, + TComparer&& comparer +) { + bool initialized = false; + bool mixed = false; + TValue sharedValue{}; + MetaCoreForEachSelectedGameObject(editorContext, [&](MetaCoreGameObject& selectedObject) { + if (mixed) { + return; + } + + const std::optional value = accessor(selectedObject); + if (!value.has_value()) { + return; + } + + if (!initialized) { + sharedValue = *value; + initialized = true; + return; + } + + if (!comparer(sharedValue, *value)) { + mixed = true; + } + }); + + if (!initialized || mixed) { + return std::nullopt; + } + return sharedValue; +} + +template +void MetaCoreApplyValueToSelectedObjects(MetaCoreEditorContext& editorContext, TAccessor&& accessor, const TValue& value) { + MetaCoreForEachSelectedGameObject(editorContext, [&](MetaCoreGameObject& selectedObject) { + if (auto* targetValue = accessor(selectedObject); targetValue != nullptr) { + *targetValue = value; + } + }); +} + +template +[[nodiscard]] std::optional MetaCoreReadTypedPackagePayload( + const MetaCorePackageDocument& package, + const MetaCoreTypeRegistry& registry, + std::string_view expectedTypeName +) { + if (package.Exports.empty()) { + return std::nullopt; + } + + const MetaCoreStructDescriptor* descriptor = registry.FindStruct(); + const MetaCoreExportEntry& exportEntry = package.Exports.front(); + if (descriptor == nullptr || descriptor->Name != expectedTypeName || exportEntry.PayloadIndex >= package.PayloadSections.size()) { + return std::nullopt; + } + + T value{}; + if (!MetaCoreDeserializeFromBytes(package.PayloadSections[exportEntry.PayloadIndex], value, registry)) { + return std::nullopt; + } + return value; +} + +[[nodiscard]] std::optional MetaCoreLoadPrefabDocumentForInstance( + MetaCoreEditorContext& editorContext, + const MetaCoreGameObject& gameObject +) { + if (!gameObject.PrefabInstance.has_value()) { + return std::nullopt; + } + + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + const auto packageService = editorContext.GetModuleRegistry().ResolveService(); + const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || packageService == nullptr || reflectionRegistry == nullptr || !assetDatabaseService->HasProject()) { + return std::nullopt; + } + + const auto prefabAsset = assetDatabaseService->FindAssetByGuid(gameObject.PrefabInstance->PrefabAssetGuid); + if (!prefabAsset.has_value()) { + return std::nullopt; + } + + const std::filesystem::path relativePrefabPath = + !prefabAsset->PackagePath.empty() ? prefabAsset->PackagePath : prefabAsset->RelativePath; + const auto package = packageService->ReadPackage(assetDatabaseService->GetProjectDescriptor().RootPath / relativePrefabPath); + if (!package.has_value() || package->Header.PackageType != MetaCorePackageType::Prefab) { + return std::nullopt; + } + + return MetaCoreReadTypedPackagePayload( + *package, + reflectionRegistry->GetTypeRegistry(), + "MetaCorePrefabDocument" + ); +} + +[[nodiscard]] const MetaCoreGameObject* MetaCoreFindPrefabSourceObject( + const MetaCorePrefabDocument& prefabDocument, + MetaCoreId prefabObjectId +) { + const auto iterator = std::find_if(prefabDocument.GameObjects.begin(), prefabDocument.GameObjects.end(), [&](const MetaCoreGameObject& prefabObject) { + return prefabObject.Id == prefabObjectId; + }); + return iterator == prefabDocument.GameObjects.end() ? nullptr : &(*iterator); +} + +[[nodiscard]] bool MetaCoreWritePrefabDocumentForInstance( + MetaCoreEditorContext& editorContext, + const MetaCoreGameObject& instanceObject, + const MetaCorePrefabDocument& prefabDocument +) { + if (!instanceObject.PrefabInstance.has_value()) { + return false; + } + + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + const auto packageService = editorContext.GetModuleRegistry().ResolveService(); + const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || packageService == nullptr || reflectionRegistry == nullptr || !assetDatabaseService->HasProject()) { + return false; + } + + const auto payload = MetaCoreSerializeToBytes(prefabDocument, reflectionRegistry->GetTypeRegistry()); + if (!payload.has_value()) { + return false; + } + + const auto prefabAsset = assetDatabaseService->FindAssetByGuid(instanceObject.PrefabInstance->PrefabAssetGuid); + if (!prefabAsset.has_value()) { + return false; + } + + const std::filesystem::path relativePrefabPath = + !prefabAsset->PackagePath.empty() ? prefabAsset->PackagePath : prefabAsset->RelativePath; + MetaCorePackageDocument package = MetaCoreBuildTypedPackage( + MetaCorePackageType::Prefab, + instanceObject.PrefabInstance->PrefabAssetGuid, + relativePrefabPath.filename().string(), + "MetaCorePrefabDocument", + MetaCoreHashBytes(std::span(payload->data(), payload->size())), + *payload + ); + + if (!packageService->WritePackage(assetDatabaseService->GetProjectDescriptor().RootPath / relativePrefabPath, std::move(package))) { + return false; + } + + (void)assetDatabaseService->Refresh(); + return true; +} + +[[nodiscard]] bool MetaCoreApplySelectedPrefabField( + MetaCoreEditorContext& editorContext, + std::string_view componentTypeId, + std::string_view fieldId +) { + MetaCoreGameObject* selectedObject = editorContext.GetSelectedGameObject(); + if (selectedObject == nullptr || !selectedObject->PrefabInstance.has_value()) { + return false; + } + + const auto prefabDocument = MetaCoreLoadPrefabDocumentForInstance(editorContext, *selectedObject); + if (!prefabDocument.has_value()) { + return false; + } + + auto updatedPrefabDocument = *prefabDocument; + auto prefabObjectIterator = std::find_if( + updatedPrefabDocument.GameObjects.begin(), + updatedPrefabDocument.GameObjects.end(), + [&](const MetaCoreGameObject& prefabObject) { + return prefabObject.Id == selectedObject->PrefabInstance->PrefabObjectId; + } + ); + if (prefabObjectIterator == updatedPrefabDocument.GameObjects.end()) { + return false; + } + + bool changed = false; + if (componentTypeId == "Camera" && selectedObject->Camera.has_value()) { + if (!prefabObjectIterator->Camera.has_value()) { + prefabObjectIterator->Camera = MetaCoreCameraComponent{}; + } + if (fieldId == "fov") { + prefabObjectIterator->Camera->FieldOfViewDegrees = selectedObject->Camera->FieldOfViewDegrees; + changed = true; + } else if (fieldId == "near_clip") { + prefabObjectIterator->Camera->NearClip = selectedObject->Camera->NearClip; + changed = true; + } else if (fieldId == "far_clip") { + prefabObjectIterator->Camera->FarClip = selectedObject->Camera->FarClip; + changed = true; + } else if (fieldId == "primary") { + prefabObjectIterator->Camera->IsPrimary = selectedObject->Camera->IsPrimary; + changed = true; + } + } else if (componentTypeId == "Light" && selectedObject->Light.has_value()) { + if (!prefabObjectIterator->Light.has_value()) { + prefabObjectIterator->Light = MetaCoreLightComponent{}; + } + if (fieldId == "color") { + prefabObjectIterator->Light->Color = selectedObject->Light->Color; + changed = true; + } else if (fieldId == "intensity") { + prefabObjectIterator->Light->Intensity = selectedObject->Light->Intensity; + changed = true; + } + } else if (componentTypeId == "MeshRenderer" && selectedObject->MeshRenderer.has_value()) { + if (!prefabObjectIterator->MeshRenderer.has_value()) { + prefabObjectIterator->MeshRenderer = MetaCoreMeshRendererComponent{}; + } + if (fieldId == "base_color") { + prefabObjectIterator->MeshRenderer->BaseColor = selectedObject->MeshRenderer->BaseColor; + changed = true; + } else if (fieldId == "visible") { + prefabObjectIterator->MeshRenderer->Visible = selectedObject->MeshRenderer->Visible; + changed = true; + } + } + + if (!changed || !MetaCoreWritePrefabDocumentForInstance(editorContext, *selectedObject, updatedPrefabDocument)) { + return false; + } + + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Prefab", "已应用 Prefab 字段"); + return true; +} + +[[nodiscard]] bool MetaCoreRevertSelectedPrefabField( + MetaCoreEditorContext& editorContext, + std::string_view componentTypeId, + std::string_view fieldId +) { + MetaCoreGameObject* selectedObject = editorContext.GetSelectedGameObject(); + if (selectedObject == nullptr || !selectedObject->PrefabInstance.has_value()) { + return false; + } + + const auto prefabDocument = MetaCoreLoadPrefabDocumentForInstance(editorContext, *selectedObject); + if (!prefabDocument.has_value()) { + return false; + } + + const MetaCoreGameObject* prefabObject = MetaCoreFindPrefabSourceObject(*prefabDocument, selectedObject->PrefabInstance->PrefabObjectId); + if (prefabObject == nullptr) { + return false; + } + + const bool changed = editorContext.ExecuteSnapshotCommand("回退 Prefab 字段", [&]() { + if (componentTypeId == "Camera" && prefabObject->Camera.has_value()) { + if (!selectedObject->Camera.has_value()) { + selectedObject->Camera = MetaCoreCameraComponent{}; + } + if (fieldId == "fov") { + selectedObject->Camera->FieldOfViewDegrees = prefabObject->Camera->FieldOfViewDegrees; + return true; + } + if (fieldId == "near_clip") { + selectedObject->Camera->NearClip = prefabObject->Camera->NearClip; + return true; + } + if (fieldId == "far_clip") { + selectedObject->Camera->FarClip = prefabObject->Camera->FarClip; + return true; + } + if (fieldId == "primary") { + selectedObject->Camera->IsPrimary = prefabObject->Camera->IsPrimary; + return true; + } + } + + if (componentTypeId == "Light" && prefabObject->Light.has_value()) { + if (!selectedObject->Light.has_value()) { + selectedObject->Light = MetaCoreLightComponent{}; + } + if (fieldId == "color") { + selectedObject->Light->Color = prefabObject->Light->Color; + return true; + } + if (fieldId == "intensity") { + selectedObject->Light->Intensity = prefabObject->Light->Intensity; + return true; + } + } + + if (componentTypeId == "MeshRenderer" && prefabObject->MeshRenderer.has_value()) { + if (!selectedObject->MeshRenderer.has_value()) { + selectedObject->MeshRenderer = MetaCoreMeshRendererComponent{}; + } + if (fieldId == "base_color") { + selectedObject->MeshRenderer->BaseColor = prefabObject->MeshRenderer->BaseColor; + return true; + } + if (fieldId == "visible") { + selectedObject->MeshRenderer->Visible = prefabObject->MeshRenderer->Visible; + return true; + } + } + + return false; + }); + + if (changed) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Prefab", "已回退 Prefab 字段"); + } + return changed; +} + +template +std::optional> MetaCoreSerializeComponentValue( + const TComponent& component, + const MetaCoreTypeRegistry& registry +) { + return MetaCoreSerializeToBytes(component, registry); +} + +template +bool MetaCoreDeserializeComponentValue( + std::span payload, + TComponent& component, + const MetaCoreTypeRegistry& registry +) { + return MetaCoreDeserializeFromBytes(payload, component, registry); +} + +[[nodiscard]] std::string MetaCorePathToPortableString(const std::filesystem::path& path) { + return path.lexically_normal().generic_string(); +} + +[[nodiscard]] std::string MetaCoreSceneDisplayNameFromPath(const std::filesystem::path& relativeScenePath) { + const std::string fileName = relativeScenePath.filename().string(); + constexpr std::string_view suffix = ".mcscene"; + if (fileName.size() > suffix.size() && fileName.ends_with(suffix)) { + return fileName.substr(0, fileName.size() - suffix.size()); + } + return relativeScenePath.stem().string(); +} + +[[nodiscard]] bool MetaCoreIsUnsafeRelativePath(const std::filesystem::path& path) { + if (path.is_absolute()) { + return true; + } + + for (const auto& part : path.lexically_normal()) { + if (part == "..") { + return true; + } + } + return false; +} + +[[nodiscard]] std::optional MetaCoreReadProjectPathFromEnvironment() { + char* projectPathRaw = nullptr; + std::size_t projectPathLength = 0; + if (_dupenv_s(&projectPathRaw, &projectPathLength, "METACORE_PROJECT_PATH") != 0 || projectPathRaw == nullptr) { + return std::nullopt; + } + + std::filesystem::path projectPath(projectPathRaw); + free(projectPathRaw); + + if (projectPath.filename() == "MetaCore.project.json") { + projectPath = projectPath.parent_path(); + } + + if (std::filesystem::exists(projectPath / "MetaCore.project.json")) { + return projectPath; + } + + return std::nullopt; +} + +[[nodiscard]] std::optional MetaCoreDiscoverProjectRoot() { + if (const auto environmentProjectRoot = MetaCoreReadProjectPathFromEnvironment(); environmentProjectRoot.has_value()) { + return environmentProjectRoot; + } + + std::filesystem::path current = std::filesystem::current_path(); + while (!current.empty()) { + const std::array candidates = { + current, + current / "SandboxProject", + current / "TestProject" + }; + + for (const auto& candidate : candidates) { + if (std::filesystem::exists(candidate / "MetaCore.project.json")) { + return candidate; + } + } + + const std::filesystem::path parent = current.parent_path(); + if (parent == current) { + break; + } + current = parent; + } + + return std::nullopt; +} + +[[nodiscard]] std::optional MetaCoreReadTextFile(const std::filesystem::path& path) { + std::ifstream input(path); + if (!input.is_open()) { + return std::nullopt; + } + + std::ostringstream stream; + stream << input.rdbuf(); + return stream.str(); +} + +[[nodiscard]] std::optional MetaCoreFindJsonStringValue( + const std::string& document, + const std::string& key +) { + const std::regex pattern("\\\"" + key + "\\\"\\s*:\\s*\\\"([^\\\"]*)\\\""); + std::smatch match; + if (!std::regex_search(document, match, pattern)) { + return std::nullopt; + } + return match[1].str(); +} + +[[nodiscard]] std::vector MetaCoreFindJsonStringArray( + const std::string& document, + const std::string& key +) { + const std::regex arrayPattern("\\\"" + key + "\\\"\\s*:\\s*\\[([^\\]]*)\\]"); + std::smatch arrayMatch; + if (!std::regex_search(document, arrayMatch, arrayPattern)) { + return {}; + } + + std::vector results; + const std::regex stringPattern("\\\"([^\\\"]*)\\\""); + const std::string arrayBody = arrayMatch[1].str(); + for (std::sregex_iterator iterator(arrayBody.begin(), arrayBody.end(), stringPattern), end; iterator != end; ++iterator) { + results.push_back((*iterator)[1].str()); + } + return results; +} + +[[nodiscard]] std::string MetaCoreEscapeJsonString(const std::string& value) { + std::string escaped; + escaped.reserve(value.size()); + for (char character : value) { + switch (character) { + case '\\': + escaped += "\\\\"; + break; + case '"': + escaped += "\\\""; + break; + default: + escaped += character; + break; + } + } + return escaped; +} + +[[nodiscard]] bool MetaCoreIsPackagePath(const std::filesystem::path& path) { + const std::string extension = path.extension().string(); + return extension == ".mcscene" || extension == ".mcprefab" || extension == ".mcasset" || extension == ".mcmeta"; +} + +[[nodiscard]] bool MetaCoreIsScenePath(const std::filesystem::path& path) { + return path.extension() == ".mcscene"; +} + +[[nodiscard]] bool MetaCoreIsMetaPath(const std::filesystem::path& path) { + return path.extension() == ".mcmeta"; +} + +[[nodiscard]] bool MetaCoreIsLegacyMetaPath(const std::filesystem::path& path) { + return path.extension() == ".meta"; +} + +[[nodiscard]] std::filesystem::path MetaCoreBuildMetaPath(const std::filesystem::path& sourcePath) { + return std::filesystem::path(sourcePath.string() + ".mcmeta"); +} + +[[nodiscard]] std::filesystem::path MetaCoreBuildPackagePathFromSource(const std::filesystem::path& sourcePath) { + return std::filesystem::path(sourcePath.string() + ".mcasset"); +} + +[[nodiscard]] std::string MetaCoreDetectImporterId(const std::filesystem::path& path) { + const std::string extension = path.extension().string(); + if (extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".tga") { + return "TextureImporter"; + } + if (extension == ".fbx" || extension == ".obj" || extension == ".gltf") { + return "ModelImporter"; + } + if (extension == ".wav" || extension == ".ogg" || extension == ".mp3") { + return "AudioImporter"; + } + if (extension == ".rml") { + return "UiDocumentImporter"; + } + if (extension == ".rcss") { + return "UiStylesheetImporter"; + } + return "BinaryImporter"; +} + +[[nodiscard]] std::string MetaCoreDetectAssetType(const std::filesystem::path& path) { + const std::string extension = path.extension().string(); + if (extension == ".mcscene") { + return "scene"; + } + if (extension == ".mcprefab") { + return "prefab"; + } + if (extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".tga") { + return "texture"; + } + if (extension == ".fbx" || extension == ".obj" || extension == ".gltf") { + return "model"; + } + if (extension == ".wav" || extension == ".ogg" || extension == ".mp3") { + return "audio"; + } + if (extension == ".rml") { + return "ui_document"; + } + if (extension == ".rcss") { + return "ui_stylesheet"; + } + return "asset"; +} + +[[nodiscard]] MetaCorePackageDocument MetaCoreBuildTypedPackage( + MetaCorePackageType packageType, + const MetaCoreAssetGuid& assetGuid, + const std::string& objectName, + std::string_view typeName, + std::uint64_t sourceHash, + std::vector payload +) { + MetaCorePackageDocument document; + document.Header.PackageType = packageType; + document.Header.PackageGuid = assetGuid; + document.Header.SourceHash = sourceHash; + document.NameTable = {objectName, std::string(typeName)}; + document.Exports.push_back(MetaCoreExportEntry{ + assetGuid, + objectName, + MetaCoreMakeTypeId(typeName), + 0, + static_cast(payload.size()) + }); + document.PayloadSections.push_back(std::move(payload)); + return document; +} + +template +[[nodiscard]] std::optional MetaCoreReadTypedPayload( + const MetaCorePackageDocument& document, + const MetaCoreTypeRegistry& registry, + std::string_view expectedTypeName +) { + if (document.Exports.empty() || document.PayloadSections.empty()) { + return std::nullopt; + } + + const MetaCoreExportEntry& exportEntry = document.Exports.front(); + if (exportEntry.TypeId != MetaCoreMakeTypeId(expectedTypeName) || + exportEntry.PayloadIndex >= document.PayloadSections.size()) { + return std::nullopt; + } + + T payload{}; + if (!MetaCoreDeserializeFromBytes(document.PayloadSections[exportEntry.PayloadIndex], payload, registry)) { + return std::nullopt; + } + return payload; +} + +[[nodiscard]] std::vector MetaCoreCaptureSubtree( + const MetaCoreScene& scene, + MetaCoreId rootId +) { + std::vector subtreeObjects; + for (MetaCoreId objectId : scene.GetSubtreeObjectIds(rootId)) { + if (const MetaCoreGameObject* gameObject = scene.FindGameObject(objectId); gameObject != nullptr) { + subtreeObjects.push_back(*gameObject); + } + } + return subtreeObjects; +} + +void MetaCoreNormalizePrefabSubtree(std::vector& gameObjects, MetaCoreId rootId) { + for (MetaCoreGameObject& gameObject : gameObjects) { + if (gameObject.Id == rootId) { + gameObject.ParentId = 0; + } + gameObject.PrefabInstance.reset(); + } +} + +[[nodiscard]] std::optional MetaCoreFindPrefabInstanceRootId( + const MetaCoreScene& scene, + MetaCoreId objectId +) { + const MetaCoreGameObject* currentObject = scene.FindGameObject(objectId); + while (currentObject != nullptr) { + if (currentObject->PrefabInstance.has_value()) { + const MetaCoreId instanceRootId = currentObject->PrefabInstance->PrefabInstanceRootId; + if (instanceRootId != 0) { + return instanceRootId; + } + return currentObject->Id; + } + + if (currentObject->ParentId == 0) { + break; + } + currentObject = scene.FindGameObject(currentObject->ParentId); + } + + return std::nullopt; +} + +class MetaCoreBuiltinReflectionRegistryService final : public MetaCoreIReflectionRegistry { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.ReflectionRegistry"; } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + Registry_.Clear(); + MetaCoreRegisterFoundationGeneratedTypes(Registry_); + MetaCoreRegisterSceneGeneratedTypes(Registry_); + MetaCoreRegisterEditorGeneratedTypes(Registry_); + MetaCoreRegisterRuntimeDataGeneratedTypes(Registry_); + } + + [[nodiscard]] const MetaCoreTypeRegistry& GetTypeRegistry() const override { return Registry_; } + [[nodiscard]] MetaCoreTypeRegistry& AccessTypeRegistry() override { return Registry_; } + +private: + MetaCoreTypeRegistry Registry_{}; +}; + +class MetaCoreBuiltinPackageService final : public MetaCoreIPackageService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.PackageService"; } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + ReflectionRegistry_ = moduleRegistry.ResolveService(); + } + + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + ReflectionRegistry_.reset(); + } + + [[nodiscard]] bool WritePackage( + const std::filesystem::path& absolutePath, + MetaCorePackageDocument document + ) override { + if (ReflectionRegistry_ == nullptr) { + return false; + } + return MetaCoreWritePackageFile(absolutePath, std::move(document), ReflectionRegistry_->GetTypeRegistry()); + } + + [[nodiscard]] std::optional ReadPackage( + const std::filesystem::path& absolutePath + ) const override { + if (ReflectionRegistry_ == nullptr) { + return std::nullopt; + } + return MetaCoreReadPackageFile(absolutePath, ReflectionRegistry_->GetTypeRegistry()); + } + +private: + std::shared_ptr ReflectionRegistry_{}; +}; + +class MetaCoreBuiltinAssetDatabaseService final : public MetaCoreIAssetDatabaseService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.AssetDatabase"; } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + ModuleRegistry_ = &moduleRegistry; + PackageService_ = moduleRegistry.ResolveService(); + ReflectionRegistry_ = moduleRegistry.ResolveService(); + LoadProjectDescriptor(); + } + + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + AssetRecords_.clear(); + Project_ = MetaCoreProjectDescriptor{}; + PackageService_.reset(); + ReflectionRegistry_.reset(); + ModuleRegistry_ = nullptr; + } + + [[nodiscard]] bool HasProject() const override { return !Project_.RootPath.empty(); } + [[nodiscard]] const MetaCoreProjectDescriptor& GetProjectDescriptor() const override { return Project_; } + [[nodiscard]] const std::vector& GetAssetRegistry() const override { return AssetRecords_; } + + [[nodiscard]] std::optional FindAssetByGuid(const MetaCoreAssetGuid& assetGuid) const override { + const auto iterator = std::find_if(AssetRecords_.begin(), AssetRecords_.end(), [&](const MetaCoreAssetRecord& record) { + return record.Guid == assetGuid; + }); + return iterator == AssetRecords_.end() ? std::nullopt : std::optional(*iterator); + } + + [[nodiscard]] std::vector GetDirectoriesUnder(const std::filesystem::path& relativeDirectory) const override { + std::vector directories; + const auto absoluteDirectory = ResolveDirectory(relativeDirectory); + if (!absoluteDirectory.has_value() || !std::filesystem::exists(*absoluteDirectory)) { + return directories; + } + + for (const auto& entry : std::filesystem::directory_iterator(*absoluteDirectory)) { + if (entry.is_directory()) { + directories.push_back(entry.path().lexically_relative(Project_.RootPath)); + } + } + + std::sort(directories.begin(), directories.end()); + return directories; + } + + [[nodiscard]] std::vector GetAssetsUnder(const std::filesystem::path& relativeDirectory) const override { + std::vector assets; + const auto normalizedDirectory = relativeDirectory.lexically_normal(); + for (const MetaCoreAssetRecord& record : AssetRecords_) { + if (record.RelativePath.parent_path() == normalizedDirectory) { + assets.push_back(record); + } + } + + std::sort(assets.begin(), assets.end(), [](const auto& lhs, const auto& rhs) { + return MetaCorePathToPortableString(lhs.RelativePath) < MetaCorePathToPortableString(rhs.RelativePath); + }); + return assets; + } + + [[nodiscard]] std::optional FindAssetByRelativePath(const std::filesystem::path& relativePath) const override { + const auto normalizedPath = relativePath.lexically_normal(); + const auto iterator = std::find_if(AssetRecords_.begin(), AssetRecords_.end(), [&](const MetaCoreAssetRecord& record) { + return record.RelativePath == normalizedPath; + }); + return iterator == AssetRecords_.end() ? std::nullopt : std::optional(*iterator); + } + + bool Refresh() override; + bool CreateFolder(const std::filesystem::path& relativeDirectory) override; + bool RegisterScenePath(const std::filesystem::path& relativeScenePath, bool makeStartupScene) override; + bool SetStartupScenePath(const std::filesystem::path& relativeScenePath) override; + +private: + [[nodiscard]] std::optional ResolveDirectory(const std::filesystem::path& relativeDirectory) const; + void LoadProjectDescriptor(); + void SaveProjectDescriptor() const; + void RefreshScenePathsFromDisk(); + void RefreshAssetRecordsFromDisk(); + + MetaCoreEditorModuleRegistry* ModuleRegistry_ = nullptr; + MetaCoreProjectDescriptor Project_{}; + std::vector AssetRecords_{}; + std::shared_ptr PackageService_{}; + std::shared_ptr ReflectionRegistry_{}; +}; + +std::optional MetaCoreBuiltinAssetDatabaseService::ResolveDirectory( + const std::filesystem::path& relativeDirectory +) const { + if (!HasProject()) { + return std::nullopt; + } + + if (relativeDirectory.empty()) { + return Project_.RootPath; + } + + if (MetaCoreIsUnsafeRelativePath(relativeDirectory)) { + return std::nullopt; + } + + return Project_.RootPath / relativeDirectory.lexically_normal(); +} + +void MetaCoreBuiltinAssetDatabaseService::LoadProjectDescriptor() { + Project_ = MetaCoreProjectDescriptor{}; + + const auto projectRoot = MetaCoreDiscoverProjectRoot(); + if (!projectRoot.has_value()) { + return; + } + + Project_.RootPath = *projectRoot; + Project_.AssetsPath = Project_.RootPath / "Assets"; + Project_.ScenesPath = Project_.RootPath / "Scenes"; + Project_.LibraryPath = Project_.RootPath / "Library"; + Project_.Name = "MetaCoreProject"; + Project_.Version = "0.1.0"; + + const auto projectDocument = MetaCoreReadTextFile(Project_.RootPath / "MetaCore.project.json"); + if (projectDocument.has_value()) { + if (const auto name = MetaCoreFindJsonStringValue(*projectDocument, "name"); name.has_value()) { + Project_.Name = *name; + } + if (const auto version = MetaCoreFindJsonStringValue(*projectDocument, "version"); version.has_value()) { + Project_.Version = *version; + } + if (const auto startupScene = MetaCoreFindJsonStringValue(*projectDocument, "startup_scene"); startupScene.has_value()) { + Project_.StartupScenePath = std::filesystem::path(*startupScene).lexically_normal(); + } + for (const std::string& scenePath : MetaCoreFindJsonStringArray(*projectDocument, "scenes")) { + Project_.ScenePaths.push_back(std::filesystem::path(scenePath).lexically_normal()); + } + } + + RefreshScenePathsFromDisk(); +} + +void MetaCoreBuiltinAssetDatabaseService::SaveProjectDescriptor() const { + if (!HasProject()) { + return; + } + + std::ofstream output(Project_.RootPath / "MetaCore.project.json", std::ios::trunc); + if (!output.is_open()) { + return; + } + + output << "{\n"; + output << " \"name\": \"" << MetaCoreEscapeJsonString(Project_.Name) << "\",\n"; + output << " \"version\": \"" << MetaCoreEscapeJsonString(Project_.Version) << "\",\n"; + output << " \"startup_scene\": \"" << MetaCoreEscapeJsonString(MetaCorePathToPortableString(Project_.StartupScenePath)) << "\",\n"; + output << " \"scenes\": [\n"; + for (std::size_t index = 0; index < Project_.ScenePaths.size(); ++index) { + output << " \"" << MetaCoreEscapeJsonString(MetaCorePathToPortableString(Project_.ScenePaths[index])) << "\""; + output << (index + 1 < Project_.ScenePaths.size() ? ",\n" : "\n"); + } + output << " ]\n"; + output << "}\n"; +} + +void MetaCoreBuiltinAssetDatabaseService::RefreshScenePathsFromDisk() { + Project_.ScenePaths.clear(); + if (!std::filesystem::exists(Project_.ScenesPath)) { + return; + } + + for (const auto& entry : std::filesystem::recursive_directory_iterator(Project_.ScenesPath)) { + if (entry.is_regular_file() && MetaCoreIsScenePath(entry.path())) { + Project_.ScenePaths.push_back(entry.path().lexically_relative(Project_.RootPath)); + } + } + + std::sort(Project_.ScenePaths.begin(), Project_.ScenePaths.end()); + if (Project_.StartupScenePath.empty() && !Project_.ScenePaths.empty()) { + Project_.StartupScenePath = Project_.ScenePaths.front(); + } +} + +void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { + AssetRecords_.clear(); + if (!HasProject() || PackageService_ == nullptr || ReflectionRegistry_ == nullptr) { + return; + } + + const auto& registry = ReflectionRegistry_->GetTypeRegistry(); + + if (std::filesystem::exists(Project_.ScenesPath)) { + for (const auto& entry : std::filesystem::recursive_directory_iterator(Project_.ScenesPath)) { + if (!entry.is_regular_file() || !MetaCoreIsScenePath(entry.path())) { + continue; + } + + const auto package = PackageService_->ReadPackage(entry.path()); + if (!package.has_value()) { + continue; + } + + AssetRecords_.push_back(MetaCoreAssetRecord{ + package->Header.PackageGuid, + entry.path().lexically_relative(Project_.RootPath), + "scene", + MetaCoreAssetStorageKind::SourcePackage, + {}, + entry.path().lexically_relative(Project_.RootPath), + {}, + {}, + package->Header.SourceHash + }); + } + } + + if (std::filesystem::exists(Project_.AssetsPath)) { + for (const auto& entry : std::filesystem::recursive_directory_iterator(Project_.AssetsPath)) { + if (!entry.is_regular_file()) { + continue; + } + + if (MetaCoreIsMetaPath(entry.path()) || MetaCoreIsLegacyMetaPath(entry.path())) { + continue; + } + + const std::filesystem::path relativePath = entry.path().lexically_relative(Project_.RootPath); + if (entry.path().extension() == ".mcasset" || entry.path().extension() == ".mcprefab") { + const auto package = PackageService_->ReadPackage(entry.path()); + if (!package.has_value()) { + continue; + } + + AssetRecords_.push_back(MetaCoreAssetRecord{ + package->Header.PackageGuid, + relativePath, + MetaCoreDetectAssetType(entry.path()), + MetaCoreAssetStorageKind::SourcePackage, + {}, + relativePath, + {}, + {}, + package->Header.SourceHash + }); + continue; + } + + if (MetaCoreIsPackagePath(entry.path())) { + continue; + } + + MetaCoreAssetMetadataDocument metadata; + bool hasMetadata = false; + const std::filesystem::path metaPath = MetaCoreBuildMetaPath(entry.path()); + if (std::filesystem::exists(metaPath)) { + const auto package = PackageService_->ReadPackage(metaPath); + if (package.has_value() && package->Header.PackageType == MetaCorePackageType::Meta) { + const auto metadataDocument = MetaCoreReadTypedPayload( + *package, + registry, + "MetaCoreAssetMetadataDocument" + ); + if (metadataDocument.has_value()) { + metadata = *metadataDocument; + hasMetadata = true; + } + } + } + + AssetRecords_.push_back(MetaCoreAssetRecord{ + hasMetadata ? metadata.AssetGuid : MetaCoreAssetGuid{}, + relativePath, + hasMetadata ? metadata.AssetType : MetaCoreDetectAssetType(entry.path()), + MetaCoreAssetStorageKind::SourceFile, + relativePath, + hasMetadata ? metadata.PackagePath : MetaCoreBuildPackagePathFromSource(relativePath), + hasMetadata ? metaPath.lexically_relative(Project_.RootPath) : std::filesystem::path{}, + hasMetadata ? metadata.ImporterId : MetaCoreDetectImporterId(entry.path()), + hasMetadata ? metadata.SourceHash : 0 + }); + } + } + + std::sort(AssetRecords_.begin(), AssetRecords_.end(), [](const auto& lhs, const auto& rhs) { + return MetaCorePathToPortableString(lhs.RelativePath) < MetaCorePathToPortableString(rhs.RelativePath); + }); +} + +bool MetaCoreBuiltinAssetDatabaseService::Refresh() { + if (!HasProject()) { + return false; + } + + if (ModuleRegistry_ != nullptr) { + if (const auto importPipeline = ModuleRegistry_->ResolveService(); + importPipeline != nullptr) { + (void)importPipeline->RefreshImports(); + } + } + + RefreshScenePathsFromDisk(); + RefreshAssetRecordsFromDisk(); + + if (ModuleRegistry_ != nullptr) { + if (const auto cookService = ModuleRegistry_->ResolveService(); cookService != nullptr) { + std::unordered_set cookedGuids; + for (const MetaCoreAssetRecord& record : AssetRecords_) { + if (!record.Guid.IsValid()) { + continue; + } + + const std::string guidString = record.Guid.ToString(); + if (!cookedGuids.insert(guidString).second) { + continue; + } + (void)cookService->CookAsset(record.Guid); + } + } + } + + if (ModuleRegistry_ != nullptr) { + ModuleRegistry_->AccessEventBus().Publish(MetaCoreEditorEvent{ + MetaCoreEditorEventType::AssetDatabaseChanged, + "Asset registry refreshed", + Project_.RootPath, + 0, + true + }); + } + return true; +} + +bool MetaCoreBuiltinAssetDatabaseService::CreateFolder(const std::filesystem::path& relativeDirectory) { + if (!HasProject() || MetaCoreIsUnsafeRelativePath(relativeDirectory)) { + return false; + } + return std::filesystem::create_directories(Project_.RootPath / relativeDirectory.lexically_normal()); +} + +bool MetaCoreBuiltinAssetDatabaseService::RegisterScenePath( + const std::filesystem::path& relativeScenePath, + bool makeStartupScene +) { + if (!HasProject() || MetaCoreIsUnsafeRelativePath(relativeScenePath)) { + return false; + } + + const std::filesystem::path normalizedPath = relativeScenePath.lexically_normal(); + if (std::find(Project_.ScenePaths.begin(), Project_.ScenePaths.end(), normalizedPath) == Project_.ScenePaths.end()) { + Project_.ScenePaths.push_back(normalizedPath); + std::sort(Project_.ScenePaths.begin(), Project_.ScenePaths.end()); + } + if (makeStartupScene) { + Project_.StartupScenePath = normalizedPath; + } + + SaveProjectDescriptor(); + return true; +} + +bool MetaCoreBuiltinAssetDatabaseService::SetStartupScenePath(const std::filesystem::path& relativeScenePath) { + if (!HasProject() || MetaCoreIsUnsafeRelativePath(relativeScenePath)) { + return false; + } + + Project_.StartupScenePath = relativeScenePath.lexically_normal(); + SaveProjectDescriptor(); + return true; +} + +[[nodiscard]] std::string MetaCoreFormatHex64(std::uint64_t value) { + std::ostringstream stream; + stream << std::hex << std::setw(16) << std::setfill('0') << value; + return stream.str(); +} + +[[nodiscard]] std::filesystem::path MetaCoreBuildCookManifestPath(const MetaCoreProjectDescriptor& project) { + return project.LibraryPath / "Cooked" / "Windows" / "CookManifest.bin"; +} + +class MetaCoreBuiltinCookService final : public MetaCoreICookService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.CookService"; } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + AssetDatabaseService_ = moduleRegistry.ResolveService(); + ReflectionRegistry_ = moduleRegistry.ResolveService(); + } + + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + AssetDatabaseService_.reset(); + ReflectionRegistry_.reset(); + Manifest_ = MetaCoreCookManifestDocument{}; + ManifestLoaded_ = false; + } + + [[nodiscard]] bool CookAsset(const MetaCoreAssetGuid& assetGuid) override; + [[nodiscard]] bool CookScene(const std::filesystem::path& relativeScenePath) override; + [[nodiscard]] std::filesystem::path GetCookedPathForAsset(const MetaCoreAssetGuid& assetGuid) const override; + +private: + void LoadManifest(); + void SaveManifest() const; + + std::shared_ptr AssetDatabaseService_{}; + std::shared_ptr ReflectionRegistry_{}; + mutable MetaCoreCookManifestDocument Manifest_{}; + mutable bool ManifestLoaded_ = false; +}; + +class MetaCoreBuiltinImportPipelineService final : public MetaCoreIImportPipelineService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.ImportPipeline"; } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + AssetDatabaseService_ = moduleRegistry.ResolveService(); + PackageService_ = moduleRegistry.ResolveService(); + ReflectionRegistry_ = moduleRegistry.ResolveService(); + CookService_ = moduleRegistry.ResolveService(); + } + + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + AssetDatabaseService_.reset(); + PackageService_.reset(); + ReflectionRegistry_.reset(); + CookService_.reset(); + } + + [[nodiscard]] bool RefreshImports() override; + [[nodiscard]] bool ReimportAsset(const MetaCoreAssetGuid& assetGuid) override; + +private: + [[nodiscard]] bool ImportSourceFile(const std::filesystem::path& absoluteSourcePath); + + std::shared_ptr AssetDatabaseService_{}; + std::shared_ptr PackageService_{}; + std::shared_ptr ReflectionRegistry_{}; + std::shared_ptr CookService_{}; +}; + +bool MetaCoreBuiltinCookService::CookAsset(const MetaCoreAssetGuid& assetGuid) { + if (AssetDatabaseService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject()) { + return false; + } + + const auto record = AssetDatabaseService_->FindAssetByGuid(assetGuid); + if (!record.has_value()) { + return false; + } + + const std::filesystem::path sourcePackagePath = + !record->PackagePath.empty() ? record->PackagePath : + (record->StorageKind == MetaCoreAssetStorageKind::SourcePackage ? record->RelativePath : std::filesystem::path{}); + if (sourcePackagePath.empty()) { + return false; + } + + const std::filesystem::path absoluteSourcePackagePath = AssetDatabaseService_->GetProjectDescriptor().RootPath / sourcePackagePath; + const auto sourceHash = MetaCoreHashFile(absoluteSourcePackagePath); + if (!sourceHash.has_value()) { + return false; + } + + LoadManifest(); + const std::uint64_t cookedKey = MetaCoreHashCombine( + MetaCoreHashString(assetGuid.ToString()), + MetaCoreHashCombine(*sourceHash, MetaCoreHashString(record->ImporterId)) + ); + + auto manifestIterator = std::find_if(Manifest_.Entries.begin(), Manifest_.Entries.end(), [&](const MetaCoreCookManifestEntry& entry) { + return entry.AssetGuid == assetGuid; + }); + + if (manifestIterator != Manifest_.Entries.end() && + manifestIterator->CookedKey == cookedKey && + std::filesystem::exists(AssetDatabaseService_->GetProjectDescriptor().RootPath / manifestIterator->CookedPath)) { + return true; + } + + const std::filesystem::path relativeCookedPath = + std::filesystem::path("Library") / "Cooked" / "Windows" / + (assetGuid.ToString() + "_" + MetaCoreFormatHex64(cookedKey) + ".mccooked"); + const std::filesystem::path absoluteCookedPath = AssetDatabaseService_->GetProjectDescriptor().RootPath / relativeCookedPath; + std::filesystem::create_directories(absoluteCookedPath.parent_path()); + std::filesystem::copy_file(absoluteSourcePackagePath, absoluteCookedPath, std::filesystem::copy_options::overwrite_existing); + + if (manifestIterator == Manifest_.Entries.end()) { + Manifest_.Entries.push_back(MetaCoreCookManifestEntry{ + assetGuid, + sourcePackagePath, + relativeCookedPath, + *sourceHash, + cookedKey + }); + } else { + manifestIterator->SourcePackagePath = sourcePackagePath; + manifestIterator->CookedPath = relativeCookedPath; + manifestIterator->SourceHash = *sourceHash; + manifestIterator->CookedKey = cookedKey; + } + + SaveManifest(); + return true; +} + +bool MetaCoreBuiltinCookService::CookScene(const std::filesystem::path& relativeScenePath) { + if (AssetDatabaseService_ == nullptr) { + return false; + } + + const auto sceneRecord = AssetDatabaseService_->FindAssetByRelativePath(relativeScenePath); + return sceneRecord.has_value() ? CookAsset(sceneRecord->Guid) : false; +} + +std::filesystem::path MetaCoreBuiltinCookService::GetCookedPathForAsset(const MetaCoreAssetGuid& assetGuid) const { + const_cast(this)->LoadManifest(); + const auto iterator = std::find_if(Manifest_.Entries.begin(), Manifest_.Entries.end(), [&](const MetaCoreCookManifestEntry& entry) { + return entry.AssetGuid == assetGuid; + }); + return iterator == Manifest_.Entries.end() ? std::filesystem::path{} : iterator->CookedPath; +} + +void MetaCoreBuiltinCookService::LoadManifest() { + if (ManifestLoaded_ || AssetDatabaseService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject()) { + return; + } + + const std::filesystem::path manifestPath = MetaCoreBuildCookManifestPath(AssetDatabaseService_->GetProjectDescriptor()); + std::ifstream input(manifestPath, std::ios::binary); + if (input.is_open()) { + input.seekg(0, std::ios::end); + const auto size = static_cast(input.tellg()); + input.seekg(0, std::ios::beg); + std::vector buffer(size); + if (size == 0 || input.read(reinterpret_cast(buffer.data()), static_cast(size))) { + (void)MetaCoreDeserializeFromBytes( + std::span(buffer.data(), buffer.size()), + Manifest_, + ReflectionRegistry_->GetTypeRegistry() + ); + } + } + + ManifestLoaded_ = true; +} + +void MetaCoreBuiltinCookService::SaveManifest() const { + if (AssetDatabaseService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject()) { + return; + } + + const auto bytes = MetaCoreSerializeToBytes(Manifest_, ReflectionRegistry_->GetTypeRegistry()); + if (!bytes.has_value()) { + return; + } + + const std::filesystem::path manifestPath = MetaCoreBuildCookManifestPath(AssetDatabaseService_->GetProjectDescriptor()); + std::filesystem::create_directories(manifestPath.parent_path()); + std::ofstream output(manifestPath, std::ios::binary | std::ios::trunc); + if (!output.is_open()) { + return; + } + output.write(reinterpret_cast(bytes->data()), static_cast(bytes->size())); +} + +bool MetaCoreBuiltinImportPipelineService::RefreshImports() { + if (AssetDatabaseService_ == nullptr || PackageService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject()) { + return false; + } + + bool importedAnything = false; + for (const auto& entry : std::filesystem::recursive_directory_iterator(AssetDatabaseService_->GetProjectDescriptor().AssetsPath)) { + if (!entry.is_regular_file() || MetaCoreIsPackagePath(entry.path()) || MetaCoreIsLegacyMetaPath(entry.path())) { + continue; + } + importedAnything = ImportSourceFile(entry.path()) || importedAnything; + } + return importedAnything; +} + +bool MetaCoreBuiltinImportPipelineService::ReimportAsset(const MetaCoreAssetGuid& assetGuid) { + if (AssetDatabaseService_ == nullptr || !AssetDatabaseService_->HasProject()) { + return false; + } + + const auto assetRecord = AssetDatabaseService_->FindAssetByGuid(assetGuid); + if (!assetRecord.has_value() || assetRecord->SourcePath.empty()) { + return false; + } + return ImportSourceFile(AssetDatabaseService_->GetProjectDescriptor().RootPath / assetRecord->SourcePath); +} + +bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesystem::path& absoluteSourcePath) { + const auto sourceHash = MetaCoreHashFile(absoluteSourcePath); + if (!sourceHash.has_value()) { + return false; + } + + const auto& registry = ReflectionRegistry_->GetTypeRegistry(); + const std::filesystem::path relativeSourcePath = + absoluteSourcePath.lexically_relative(AssetDatabaseService_->GetProjectDescriptor().RootPath); + const std::filesystem::path absolutePackagePath = MetaCoreBuildPackagePathFromSource(absoluteSourcePath); + const std::filesystem::path relativePackagePath = + absolutePackagePath.lexically_relative(AssetDatabaseService_->GetProjectDescriptor().RootPath); + const std::filesystem::path absoluteMetaPath = MetaCoreBuildMetaPath(absoluteSourcePath); + + MetaCoreAssetMetadataDocument metadata; + if (std::filesystem::exists(absoluteMetaPath)) { + const auto package = PackageService_->ReadPackage(absoluteMetaPath); + if (package.has_value() && package->Header.PackageType == MetaCorePackageType::Meta) { + if (const auto loadedMetadata = MetaCoreReadTypedPayload( + *package, + registry, + "MetaCoreAssetMetadataDocument"); + loadedMetadata.has_value()) { + metadata = *loadedMetadata; + } + } + } + + if (!metadata.AssetGuid.IsValid()) { + metadata.AssetGuid = MetaCoreAssetGuid::Generate(); + } + + metadata.AssetType = MetaCoreDetectAssetType(absoluteSourcePath); + metadata.ImporterId = MetaCoreDetectImporterId(absoluteSourcePath); + metadata.SourcePath = relativeSourcePath; + metadata.PackagePath = relativePackagePath; + metadata.SourceHash = *sourceHash; + + bool needsPackageWrite = !std::filesystem::exists(absolutePackagePath); + if (!needsPackageWrite) { + const auto package = PackageService_->ReadPackage(absolutePackagePath); + needsPackageWrite = !package.has_value() || package->Header.SourceHash != *sourceHash; + } + + if (needsPackageWrite) { + MetaCoreImportedAssetDocument importedAsset; + importedAsset.AssetType = metadata.AssetType; + importedAsset.ImporterId = metadata.ImporterId; + importedAsset.SourcePath = metadata.SourcePath; + importedAsset.SourceHash = metadata.SourceHash; + + const auto importedAssetBytes = MetaCoreSerializeToBytes(importedAsset, registry); + const auto metadataBytes = MetaCoreSerializeToBytes(metadata, registry); + if (!importedAssetBytes.has_value() || !metadataBytes.has_value()) { + return false; + } + + MetaCorePackageDocument assetPackage = MetaCoreBuildTypedPackage( + MetaCorePackageType::Asset, + metadata.AssetGuid, + absoluteSourcePath.filename().string(), + "MetaCoreImportedAssetDocument", + metadata.SourceHash, + *importedAssetBytes + ); + + MetaCorePackageDocument metaPackage = MetaCoreBuildTypedPackage( + MetaCorePackageType::Meta, + metadata.AssetGuid, + absoluteSourcePath.filename().string(), + "MetaCoreAssetMetadataDocument", + metadata.SourceHash, + *metadataBytes + ); + + if (!PackageService_->WritePackage(absolutePackagePath, std::move(assetPackage)) || + !PackageService_->WritePackage(absoluteMetaPath, std::move(metaPackage))) { + return false; + } + } + + if (CookService_ != nullptr) { + (void)CookService_->CookAsset(metadata.AssetGuid); + } + return true; +} + +[[nodiscard]] std::optional> MetaCoreSerializeSceneSnapshotBytes( + const MetaCoreTypeRegistry& registry, + const MetaCoreSceneSnapshot& snapshot +) { + return MetaCoreSerializeToBytes(snapshot, registry); +} + +class MetaCoreBuiltinScenePersistenceService final : public MetaCoreIScenePersistenceService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.ScenePersistence"; } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + ModuleRegistry_ = &moduleRegistry; + AssetDatabaseService_ = moduleRegistry.ResolveService(); + PackageService_ = moduleRegistry.ResolveService(); + ReflectionRegistry_ = moduleRegistry.ResolveService(); + CookService_ = moduleRegistry.ResolveService(); + } + + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + AssetDatabaseService_.reset(); + PackageService_.reset(); + ReflectionRegistry_.reset(); + CookService_.reset(); + ModuleRegistry_ = nullptr; + CurrentSceneGuid_ = MetaCoreAssetGuid{}; + CurrentScenePath_.clear(); + CurrentSceneName_.clear(); + SavedSnapshotBytes_.clear(); + Dirty_ = false; + } + + [[nodiscard]] bool HasOpenScene() const override { return !CurrentScenePath_.empty(); } + [[nodiscard]] bool IsSceneDirty() const override { return Dirty_; } + [[nodiscard]] std::filesystem::path GetCurrentScenePath() const override { return CurrentScenePath_; } + [[nodiscard]] std::string GetCurrentSceneDisplayName() const override { return CurrentSceneName_; } + + [[nodiscard]] std::vector GetKnownScenePaths() const override { + return AssetDatabaseService_ == nullptr ? std::vector{} + : AssetDatabaseService_->GetProjectDescriptor().ScenePaths; + } + + bool LoadStartupScene(MetaCoreEditorContext& editorContext) override { + if (AssetDatabaseService_ == nullptr || !AssetDatabaseService_->HasProject()) { + return false; + } + + const auto& startupScenePath = AssetDatabaseService_->GetProjectDescriptor().StartupScenePath; + return startupScenePath.empty() ? false : LoadScene(editorContext, startupScenePath); + } + + bool LoadScene(MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeScenePath) override { + if (AssetDatabaseService_ == nullptr || + PackageService_ == nullptr || + ReflectionRegistry_ == nullptr || + !AssetDatabaseService_->HasProject() || + MetaCoreIsUnsafeRelativePath(relativeScenePath)) { + return false; + } + + const std::filesystem::path normalizedScenePath = relativeScenePath.lexically_normal(); + const std::filesystem::path absoluteScenePath = AssetDatabaseService_->GetProjectDescriptor().RootPath / normalizedScenePath; + const auto package = PackageService_->ReadPackage(absoluteScenePath); + if (!package.has_value() || package->Header.PackageType != MetaCorePackageType::Scene) { + return false; + } + + const auto sceneDocument = MetaCoreReadTypedPayload( + *package, + ReflectionRegistry_->GetTypeRegistry(), + "MetaCoreSceneDocument" + ); + if (!sceneDocument.has_value()) { + return false; + } + + MetaCoreEditorStateSnapshot editorStateSnapshot; + editorStateSnapshot.SceneSnapshot.GameObjects = sceneDocument->GameObjects; + editorStateSnapshot.SelectionSnapshot = sceneDocument->Selection; + editorContext.RestoreStateSnapshot(editorStateSnapshot); + editorContext.GetCommandService().Clear(); + + CurrentSceneGuid_ = package->Header.PackageGuid; + CurrentScenePath_ = normalizedScenePath; + CurrentSceneName_ = sceneDocument->Name.empty() + ? MetaCoreSceneDisplayNameFromPath(normalizedScenePath) + : sceneDocument->Name; + SavedSnapshotBytes_ = MetaCoreSerializeSceneSnapshotBytes( + ReflectionRegistry_->GetTypeRegistry(), + editorContext.GetScene().CaptureSnapshot() + ).value_or(std::vector{}); + SetDirtyState(false); + + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已加载场景: " + MetaCorePathToPortableString(CurrentScenePath_)); + if (ModuleRegistry_ != nullptr) { + ModuleRegistry_->AccessEventBus().Publish(MetaCoreEditorEvent{ + MetaCoreEditorEventType::SceneLoaded, + "场景已加载", + CurrentScenePath_, + 0, + true + }); + } + return true; + } + + bool SaveCurrentScene(MetaCoreEditorContext& editorContext) override { + return CurrentScenePath_.empty() + ? SaveSceneAs(editorContext, std::filesystem::path("Scenes") / "Untitled.mcscene") + : SaveSceneAs(editorContext, CurrentScenePath_); + } + + bool SaveSceneAs(MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeScenePath) override { + if (AssetDatabaseService_ == nullptr || + PackageService_ == nullptr || + ReflectionRegistry_ == nullptr || + !AssetDatabaseService_->HasProject() || + MetaCoreIsUnsafeRelativePath(relativeScenePath)) { + return false; + } + + const std::filesystem::path normalizedScenePath = relativeScenePath.lexically_normal(); + const std::filesystem::path absoluteScenePath = AssetDatabaseService_->GetProjectDescriptor().RootPath / normalizedScenePath; + + MetaCoreAssetGuid sceneGuid = CurrentSceneGuid_; + if (!sceneGuid.IsValid() && std::filesystem::exists(absoluteScenePath)) { + if (const auto existingPackage = PackageService_->ReadPackage(absoluteScenePath); existingPackage.has_value()) { + sceneGuid = existingPackage->Header.PackageGuid; + } + } + if (!sceneGuid.IsValid()) { + sceneGuid = MetaCoreAssetGuid::Generate(); + } + + MetaCoreSceneDocument sceneDocument; + sceneDocument.Name = CurrentSceneName_.empty() ? MetaCoreSceneDisplayNameFromPath(normalizedScenePath) : CurrentSceneName_; + sceneDocument.GameObjects = editorContext.GetScene().GetGameObjects(); + sceneDocument.Selection.SelectedObjectIds = editorContext.GetSelectedObjectIds(); + sceneDocument.Selection.ActiveObjectId = editorContext.GetActiveObjectId(); + sceneDocument.Selection.SelectionAnchorId = editorContext.GetSelectionAnchorId(); + + const auto payload = MetaCoreSerializeToBytes(sceneDocument, ReflectionRegistry_->GetTypeRegistry()); + if (!payload.has_value()) { + return false; + } + + MetaCorePackageDocument document = MetaCoreBuildTypedPackage( + MetaCorePackageType::Scene, + sceneGuid, + normalizedScenePath.filename().string(), + "MetaCoreSceneDocument", + MetaCoreHashBytes(std::span(payload->data(), payload->size())), + *payload + ); + + if (!PackageService_->WritePackage(absoluteScenePath, std::move(document))) { + return false; + } + + CurrentSceneGuid_ = sceneGuid; + CurrentScenePath_ = normalizedScenePath; + CurrentSceneName_ = sceneDocument.Name; + SavedSnapshotBytes_ = MetaCoreSerializeSceneSnapshotBytes( + ReflectionRegistry_->GetTypeRegistry(), + editorContext.GetScene().CaptureSnapshot() + ).value_or(std::vector{}); + SetDirtyState(false); + + (void)AssetDatabaseService_->RegisterScenePath(CurrentScenePath_, false); + (void)AssetDatabaseService_->Refresh(); + if (CookService_ != nullptr) { + (void)CookService_->CookScene(CurrentScenePath_); + } + + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已保存场景: " + MetaCorePathToPortableString(CurrentScenePath_)); + if (ModuleRegistry_ != nullptr) { + ModuleRegistry_->AccessEventBus().Publish(MetaCoreEditorEvent{ + MetaCoreEditorEventType::SceneSaved, + "场景已保存", + CurrentScenePath_, + 0, + true + }); + } + return true; + } + + void UpdateDirtyState(const MetaCoreSceneSnapshot& snapshot) override { + if (ReflectionRegistry_ == nullptr) { + SetDirtyState(true); + return; + } + + const auto snapshotBytes = MetaCoreSerializeSceneSnapshotBytes(ReflectionRegistry_->GetTypeRegistry(), snapshot) + .value_or(std::vector{}); + SetDirtyState(snapshotBytes != SavedSnapshotBytes_); + } + +private: + void SetDirtyState(bool dirty) { + if (Dirty_ == dirty) { + return; + } + + Dirty_ = dirty; + if (ModuleRegistry_ != nullptr) { + ModuleRegistry_->AccessEventBus().Publish(MetaCoreEditorEvent{ + MetaCoreEditorEventType::SceneDirtyStateChanged, + Dirty_ ? "场景已变脏" : "场景已同步", + CurrentScenePath_, + 0, + Dirty_ + }); + } + } + + MetaCoreEditorModuleRegistry* ModuleRegistry_ = nullptr; + std::shared_ptr AssetDatabaseService_{}; + std::shared_ptr PackageService_{}; + std::shared_ptr ReflectionRegistry_{}; + std::shared_ptr CookService_{}; + MetaCoreAssetGuid CurrentSceneGuid_{}; + std::filesystem::path CurrentScenePath_{}; + std::string CurrentSceneName_{}; + std::vector SavedSnapshotBytes_{}; + bool Dirty_ = false; +}; + +class MetaCoreBuiltinSelectionService final : public MetaCoreISelectionService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.Selection"; } + void ClearSelection(MetaCoreEditorContext& editorContext) override { editorContext.ClearSelection(); } + void SelectOnly(MetaCoreEditorContext& editorContext, MetaCoreId objectId) override { editorContext.SelectOnly(objectId); } + void ToggleSelection(MetaCoreEditorContext& editorContext, MetaCoreId objectId) override { editorContext.ToggleSelection(objectId); } + + void ApplyHierarchySelection(MetaCoreEditorContext& editorContext, MetaCoreId objectId, bool allowToggle) override { + if (objectId == 0 || editorContext.GetScene().FindGameObject(objectId) == nullptr) { + return; + } + + const ImGuiIO& io = ImGui::GetIO(); + if (io.KeyShift) { + editorContext.SelectRangeByOrderedIds(editorContext.GetScene().BuildHierarchyPreorder(), objectId, io.KeyCtrl); + return; + } + if (allowToggle && io.KeyCtrl) { + editorContext.ToggleSelection(objectId); + return; + } + editorContext.SelectOnly(objectId); + } + + void ApplyViewportSelection(MetaCoreEditorContext& editorContext, MetaCoreId pickedObjectId) override { + if (editorContext.GetInput().IsKeyDown(MetaCoreInputKey::Control)) { + if (pickedObjectId != 0) { + editorContext.ToggleSelection(pickedObjectId); + } + return; + } + editorContext.SelectOnly(pickedObjectId); + } +}; + +class MetaCoreBuiltinClipboardService final : public MetaCoreIClipboardService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.Clipboard"; } + [[nodiscard]] bool DuplicateSelection(MetaCoreEditorContext& editorContext) override; + [[nodiscard]] bool DeleteSelection(MetaCoreEditorContext& editorContext) override; +}; + +class MetaCoreBuiltinSceneEditingService final : public MetaCoreISceneEditingService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.SceneEditing"; } + [[nodiscard]] std::optional CreateGameObject( + MetaCoreEditorContext& editorContext, + const std::string& objectName, + bool withMeshRenderer, + std::optional forcedParentId + ) override; + [[nodiscard]] bool RenameGameObject( + MetaCoreEditorContext& editorContext, + MetaCoreId objectId, + const std::string& name + ) override; + [[nodiscard]] bool ReparentSelection(MetaCoreEditorContext& editorContext, MetaCoreId newParentId) override; +}; + +class MetaCoreBuiltinPrefabService final : public MetaCoreIPrefabService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.Prefab"; } + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + AssetDatabaseService_ = moduleRegistry.ResolveService(); + PackageService_ = moduleRegistry.ResolveService(); + ReflectionRegistry_ = moduleRegistry.ResolveService(); + } + + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + AssetDatabaseService_.reset(); + PackageService_.reset(); + ReflectionRegistry_.reset(); + } + + [[nodiscard]] bool SupportsPrefabWorkflows() const override { return true; } + [[nodiscard]] std::optional CreatePrefabFromSelection( + MetaCoreEditorContext& editorContext, + const std::filesystem::path& relativePrefabPath + ) override; + [[nodiscard]] std::optional InstantiatePrefab( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& prefabAssetGuid, + std::optional forcedParentId + ) override; + [[nodiscard]] bool ApplySelectedPrefabInstance(MetaCoreEditorContext& editorContext) override; + [[nodiscard]] bool RevertSelectedPrefabInstance(MetaCoreEditorContext& editorContext) override; + +private: + [[nodiscard]] std::optional LoadPrefabPackage(const MetaCoreAssetGuid& prefabAssetGuid) const; + [[nodiscard]] std::optional LoadPrefabDocument(const MetaCoreAssetGuid& prefabAssetGuid) const; + [[nodiscard]] std::optional FindPrefabAsset(const MetaCoreAssetGuid& prefabAssetGuid) const; + [[nodiscard]] std::optional InstantiatePrefabDocument( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& prefabAssetGuid, + const MetaCorePrefabDocument& prefabDocument, + std::optional forcedParentId + ); + + std::shared_ptr AssetDatabaseService_{}; + std::shared_ptr PackageService_{}; + std::shared_ptr ReflectionRegistry_{}; +}; + +class MetaCoreBuiltinComponentTypeRegistry final : public MetaCoreIComponentTypeRegistry { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.ComponentTypeRegistry"; } + [[nodiscard]] std::vector GetRegisteredComponentTypeIds() const override { + std::vector typeIds; + typeIds.reserve(Descriptors_.size()); + for (const auto& descriptor : Descriptors_) { + typeIds.push_back(descriptor.TypeId); + } + return typeIds; + } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + ReflectionRegistry_ = moduleRegistry.ResolveService(); + Descriptors_.clear(); + Descriptors_.push_back(MetaCoreComponentDescriptor{ + "Transform", + "Transform", + [](const MetaCoreGameObject&) { return true; }, + [](MetaCoreGameObject&) { return false; }, + [](MetaCoreGameObject&) { return false; }, + [](MetaCoreGameObject& gameObject) { + gameObject.Transform = MetaCoreTransformComponent{}; + return true; + }, + [](const MetaCoreGameObject& gameObject, const MetaCoreTypeRegistry& registry) { + return MetaCoreSerializeComponentValue(gameObject.Transform, registry); + }, + [](MetaCoreGameObject& gameObject, std::span payload, const MetaCoreTypeRegistry& registry) { + return MetaCoreDeserializeComponentValue(payload, gameObject.Transform, registry); + }, + nullptr + }); + Descriptors_.push_back(MetaCoreComponentDescriptor{ + "Camera", + "Camera", + [](const MetaCoreGameObject& gameObject) { return gameObject.Camera.has_value(); }, + [](MetaCoreGameObject& gameObject) { + if (gameObject.Camera.has_value()) { + return false; + } + gameObject.Camera = MetaCoreCameraComponent{}; + return true; + }, + [](MetaCoreGameObject& gameObject) { + if (!gameObject.Camera.has_value()) { + return false; + } + gameObject.Camera.reset(); + return true; + }, + [](MetaCoreGameObject& gameObject) { + if (!gameObject.Camera.has_value()) { + return false; + } + gameObject.Camera = MetaCoreCameraComponent{}; + return true; + }, + [](const MetaCoreGameObject& gameObject, const MetaCoreTypeRegistry& registry) -> std::optional> { + if (!gameObject.Camera.has_value()) { + return std::nullopt; + } + return MetaCoreSerializeComponentValue(*gameObject.Camera, registry); + }, + [](MetaCoreGameObject& gameObject, std::span payload, const MetaCoreTypeRegistry& registry) { + MetaCoreCameraComponent component{}; + if (!MetaCoreDeserializeComponentValue(payload, component, registry)) { + return false; + } + gameObject.Camera = component; + return true; + }, + MetaCoreDrawCameraComponentInspector + }); + Descriptors_.push_back(MetaCoreComponentDescriptor{ + "Light", + "Light", + [](const MetaCoreGameObject& gameObject) { return gameObject.Light.has_value(); }, + [](MetaCoreGameObject& gameObject) { + if (gameObject.Light.has_value()) { + return false; + } + gameObject.Light = MetaCoreLightComponent{}; + return true; + }, + [](MetaCoreGameObject& gameObject) { + if (!gameObject.Light.has_value()) { + return false; + } + gameObject.Light.reset(); + return true; + }, + [](MetaCoreGameObject& gameObject) { + if (!gameObject.Light.has_value()) { + return false; + } + gameObject.Light = MetaCoreLightComponent{}; + return true; + }, + [](const MetaCoreGameObject& gameObject, const MetaCoreTypeRegistry& registry) -> std::optional> { + if (!gameObject.Light.has_value()) { + return std::nullopt; + } + return MetaCoreSerializeComponentValue(*gameObject.Light, registry); + }, + [](MetaCoreGameObject& gameObject, std::span payload, const MetaCoreTypeRegistry& registry) { + MetaCoreLightComponent component{}; + if (!MetaCoreDeserializeComponentValue(payload, component, registry)) { + return false; + } + gameObject.Light = component; + return true; + }, + MetaCoreDrawLightComponentInspector + }); + Descriptors_.push_back(MetaCoreComponentDescriptor{ + "MeshRenderer", + "Mesh Renderer", + [](const MetaCoreGameObject& gameObject) { return gameObject.MeshRenderer.has_value(); }, + [](MetaCoreGameObject& gameObject) { + if (gameObject.MeshRenderer.has_value()) { + return false; + } + gameObject.MeshRenderer = MetaCoreMeshRendererComponent{}; + return true; + }, + [](MetaCoreGameObject& gameObject) { + if (!gameObject.MeshRenderer.has_value()) { + return false; + } + gameObject.MeshRenderer.reset(); + return true; + }, + [](MetaCoreGameObject& gameObject) { + if (!gameObject.MeshRenderer.has_value()) { + return false; + } + gameObject.MeshRenderer = MetaCoreMeshRendererComponent{}; + return true; + }, + [](const MetaCoreGameObject& gameObject, const MetaCoreTypeRegistry& registry) -> std::optional> { + if (!gameObject.MeshRenderer.has_value()) { + return std::nullopt; + } + return MetaCoreSerializeComponentValue(*gameObject.MeshRenderer, registry); + }, + [](MetaCoreGameObject& gameObject, std::span payload, const MetaCoreTypeRegistry& registry) { + MetaCoreMeshRendererComponent component{}; + if (!MetaCoreDeserializeComponentValue(payload, component, registry)) { + return false; + } + gameObject.MeshRenderer = component; + return true; + }, + MetaCoreDrawMeshRendererComponentInspector + }); + } + + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + ReflectionRegistry_.reset(); + CopiedComponentTypeId_.clear(); + CopiedComponentPayload_.clear(); + } + + [[nodiscard]] const std::vector& GetComponentDescriptors() const override { + return Descriptors_; + } + + [[nodiscard]] const MetaCoreComponentDescriptor* FindDescriptor(std::string_view typeId) const override { + const auto iterator = std::find_if(Descriptors_.begin(), Descriptors_.end(), [&](const MetaCoreComponentDescriptor& descriptor) { + return descriptor.TypeId == typeId; + }); + return iterator == Descriptors_.end() ? nullptr : &(*iterator); + } + + [[nodiscard]] bool CopyComponent(std::string_view typeId, const MetaCoreGameObject& gameObject) override { + if (ReflectionRegistry_ == nullptr) { + return false; + } + const MetaCoreComponentDescriptor* descriptor = FindDescriptor(typeId); + if (descriptor == nullptr || !descriptor->CopyComponentPayload) { + return false; + } + + const auto payload = descriptor->CopyComponentPayload(gameObject, ReflectionRegistry_->GetTypeRegistry()); + if (!payload.has_value()) { + return false; + } + + CopiedComponentTypeId_ = descriptor->TypeId; + CopiedComponentPayload_ = *payload; + return true; + } + + [[nodiscard]] bool CanPasteComponent(std::string_view typeId) const override { + return ReflectionRegistry_ != nullptr && + !CopiedComponentPayload_.empty() && + CopiedComponentTypeId_ == typeId; + } + + [[nodiscard]] bool PasteComponent(std::string_view typeId, MetaCoreGameObject& gameObject) const override { + if (!CanPasteComponent(typeId)) { + return false; + } + + const MetaCoreComponentDescriptor* descriptor = FindDescriptor(typeId); + if (descriptor == nullptr || !descriptor->PasteComponentPayload) { + return false; + } + + return descriptor->PasteComponentPayload( + gameObject, + std::span(CopiedComponentPayload_.data(), CopiedComponentPayload_.size()), + ReflectionRegistry_->GetTypeRegistry() + ); + } + +private: + std::vector Descriptors_{}; + std::shared_ptr ReflectionRegistry_{}; + std::string CopiedComponentTypeId_{}; + std::vector CopiedComponentPayload_{}; +}; + +class MetaCoreBuiltinPlayModeService final : public MetaCoreIPlayModeService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.PlayMode"; } + [[nodiscard]] MetaCorePlayModeState GetState() const override { return MetaCorePlayModeState::Edit; } + [[nodiscard]] bool CanEnterPlayMode() const override { return false; } +}; + +std::optional MetaCoreBuiltinPrefabService::FindPrefabAsset(const MetaCoreAssetGuid& prefabAssetGuid) const { + if (AssetDatabaseService_ == nullptr) { + return std::nullopt; + } + const auto assetRecord = AssetDatabaseService_->FindAssetByGuid(prefabAssetGuid); + if (!assetRecord.has_value() || assetRecord->Type != "prefab") { + return std::nullopt; + } + return assetRecord; +} + +std::optional MetaCoreBuiltinPrefabService::LoadPrefabPackage(const MetaCoreAssetGuid& prefabAssetGuid) const { + if (AssetDatabaseService_ == nullptr || PackageService_ == nullptr || !AssetDatabaseService_->HasProject()) { + return std::nullopt; + } + + const auto assetRecord = FindPrefabAsset(prefabAssetGuid); + if (!assetRecord.has_value()) { + return std::nullopt; + } + + const std::filesystem::path relativePackagePath = + !assetRecord->PackagePath.empty() ? assetRecord->PackagePath : assetRecord->RelativePath; + return PackageService_->ReadPackage(AssetDatabaseService_->GetProjectDescriptor().RootPath / relativePackagePath); +} + +std::optional MetaCoreBuiltinPrefabService::LoadPrefabDocument(const MetaCoreAssetGuid& prefabAssetGuid) const { + if (ReflectionRegistry_ == nullptr) { + return std::nullopt; + } + + const auto package = LoadPrefabPackage(prefabAssetGuid); + if (!package.has_value() || package->Header.PackageType != MetaCorePackageType::Prefab) { + return std::nullopt; + } + + return MetaCoreReadTypedPayload( + *package, + ReflectionRegistry_->GetTypeRegistry(), + "MetaCorePrefabDocument" + ); +} + +std::optional MetaCoreBuiltinPrefabService::CreatePrefabFromSelection( + MetaCoreEditorContext& editorContext, + const std::filesystem::path& relativePrefabPath +) { + if (AssetDatabaseService_ == nullptr || + PackageService_ == nullptr || + ReflectionRegistry_ == nullptr || + !AssetDatabaseService_->HasProject() || + MetaCoreIsUnsafeRelativePath(relativePrefabPath)) { + return std::nullopt; + } + + const MetaCoreId activeObjectId = editorContext.GetActiveObjectId(); + if (activeObjectId == 0 || editorContext.GetScene().FindGameObject(activeObjectId) == nullptr) { + return std::nullopt; + } + + std::vector prefabObjects = MetaCoreCaptureSubtree(editorContext.GetScene(), activeObjectId); + if (prefabObjects.empty()) { + return std::nullopt; + } + + MetaCoreNormalizePrefabSubtree(prefabObjects, activeObjectId); + + const MetaCoreGameObject* rootObject = editorContext.GetScene().FindGameObject(activeObjectId); + const std::filesystem::path normalizedPrefabPath = relativePrefabPath.lexically_normal(); + const std::filesystem::path absolutePrefabPath = AssetDatabaseService_->GetProjectDescriptor().RootPath / normalizedPrefabPath; + + MetaCoreAssetGuid prefabGuid; + if (std::filesystem::exists(absolutePrefabPath)) { + if (const auto existingPackage = PackageService_->ReadPackage(absolutePrefabPath); existingPackage.has_value()) { + prefabGuid = existingPackage->Header.PackageGuid; + } + } + if (!prefabGuid.IsValid()) { + prefabGuid = MetaCoreAssetGuid::Generate(); + } + + MetaCorePrefabDocument prefabDocument; + prefabDocument.Name = rootObject != nullptr ? rootObject->Name : normalizedPrefabPath.stem().string(); + prefabDocument.GameObjects = std::move(prefabObjects); + + const auto payload = MetaCoreSerializeToBytes(prefabDocument, ReflectionRegistry_->GetTypeRegistry()); + if (!payload.has_value()) { + return std::nullopt; + } + + MetaCorePackageDocument package = MetaCoreBuildTypedPackage( + MetaCorePackageType::Prefab, + prefabGuid, + normalizedPrefabPath.filename().string(), + "MetaCorePrefabDocument", + MetaCoreHashBytes(std::span(payload->data(), payload->size())), + *payload + ); + + if (!PackageService_->WritePackage(absolutePrefabPath, std::move(package))) { + return std::nullopt; + } + + (void)AssetDatabaseService_->Refresh(); + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Prefab", "已创建 Prefab: " + normalizedPrefabPath.generic_string()); + return normalizedPrefabPath; +} + +std::optional MetaCoreBuiltinPrefabService::InstantiatePrefabDocument( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& prefabAssetGuid, + const MetaCorePrefabDocument& prefabDocument, + std::optional forcedParentId +) { + if (prefabDocument.GameObjects.empty()) { + return std::nullopt; + } + + MetaCoreId instantiatedRootId = 0; + const bool created = editorContext.ExecuteSnapshotCommand("实例化 Prefab", [&]() { + std::unordered_map idMap; + std::vector clonedObjects; + clonedObjects.reserve(prefabDocument.GameObjects.size()); + + const MetaCoreId externalParentId = forcedParentId.value_or(0); + for (const MetaCoreGameObject& sourceObject : prefabDocument.GameObjects) { + MetaCoreGameObject clonedObject = sourceObject; + clonedObject.Id = MetaCoreIdGenerator::Generate(); + idMap[sourceObject.Id] = clonedObject.Id; + clonedObjects.push_back(std::move(clonedObject)); + } + + for (std::size_t index = 0; index < prefabDocument.GameObjects.size(); ++index) { + const MetaCoreGameObject& sourceObject = prefabDocument.GameObjects[index]; + MetaCoreGameObject& clonedObject = clonedObjects[index]; + if (sourceObject.ParentId == 0) { + clonedObject.ParentId = externalParentId; + instantiatedRootId = clonedObject.Id; + } else { + clonedObject.ParentId = idMap[sourceObject.ParentId]; + } + + clonedObject.PrefabInstance = MetaCorePrefabInstanceMetadata{ + prefabAssetGuid, + sourceObject.Id, + 0 + }; + } + + for (MetaCoreGameObject& clonedObject : clonedObjects) { + if (clonedObject.PrefabInstance.has_value()) { + clonedObject.PrefabInstance->PrefabInstanceRootId = instantiatedRootId; + } + editorContext.GetScene().GetGameObjects().push_back(std::move(clonedObject)); + } + + if (instantiatedRootId == 0) { + return false; + } + editorContext.SelectOnly(instantiatedRootId); + editorContext.SetSelectionAnchorId(instantiatedRootId); + return true; + }); + + if (!created || instantiatedRootId == 0) { + return std::nullopt; + } + + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Prefab", "已实例化 Prefab"); + return instantiatedRootId; +} + +std::optional MetaCoreBuiltinPrefabService::InstantiatePrefab( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& prefabAssetGuid, + std::optional forcedParentId +) { + const auto prefabDocument = LoadPrefabDocument(prefabAssetGuid); + if (!prefabDocument.has_value()) { + return std::nullopt; + } + return InstantiatePrefabDocument(editorContext, prefabAssetGuid, *prefabDocument, forcedParentId); +} + +bool MetaCoreBuiltinPrefabService::ApplySelectedPrefabInstance(MetaCoreEditorContext& editorContext) { + if (AssetDatabaseService_ == nullptr || PackageService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject()) { + return false; + } + + const MetaCoreId activeObjectId = editorContext.GetActiveObjectId(); + if (activeObjectId == 0) { + return false; + } + + const auto instanceRootId = MetaCoreFindPrefabInstanceRootId(editorContext.GetScene(), activeObjectId); + if (!instanceRootId.has_value()) { + return false; + } + + const MetaCoreGameObject* rootObject = editorContext.GetScene().FindGameObject(*instanceRootId); + if (rootObject == nullptr || !rootObject->PrefabInstance.has_value()) { + return false; + } + + const MetaCoreAssetGuid prefabAssetGuid = rootObject->PrefabInstance->PrefabAssetGuid; + const auto prefabAsset = FindPrefabAsset(prefabAssetGuid); + if (!prefabAsset.has_value()) { + return false; + } + + std::vector prefabObjects = MetaCoreCaptureSubtree(editorContext.GetScene(), *instanceRootId); + if (prefabObjects.empty()) { + return false; + } + + std::unordered_map prefabIdMap; + prefabIdMap.reserve(prefabObjects.size()); + for (const MetaCoreGameObject& gameObject : prefabObjects) { + if (!gameObject.PrefabInstance.has_value()) { + return false; + } + prefabIdMap.emplace(gameObject.Id, gameObject.PrefabInstance->PrefabObjectId); + } + + for (MetaCoreGameObject& gameObject : prefabObjects) { + if (!gameObject.PrefabInstance.has_value()) { + return false; + } + + const MetaCoreId originalSceneId = gameObject.Id; + const MetaCoreId originalParentId = gameObject.ParentId; + gameObject.Id = gameObject.PrefabInstance->PrefabObjectId; + + if (originalSceneId == *instanceRootId) { + gameObject.ParentId = 0; + } else if (originalParentId != 0) { + const auto parentIterator = prefabIdMap.find(originalParentId); + if (parentIterator == prefabIdMap.end()) { + return false; + } + gameObject.ParentId = parentIterator->second; + } + gameObject.PrefabInstance.reset(); + } + + MetaCorePrefabDocument prefabDocument; + prefabDocument.Name = rootObject->Name; + prefabDocument.GameObjects = std::move(prefabObjects); + + const auto payload = MetaCoreSerializeToBytes(prefabDocument, ReflectionRegistry_->GetTypeRegistry()); + if (!payload.has_value()) { + return false; + } + + const std::filesystem::path relativePrefabPath = + !prefabAsset->PackagePath.empty() ? prefabAsset->PackagePath : prefabAsset->RelativePath; + MetaCorePackageDocument package = MetaCoreBuildTypedPackage( + MetaCorePackageType::Prefab, + prefabAssetGuid, + relativePrefabPath.filename().string(), + "MetaCorePrefabDocument", + MetaCoreHashBytes(std::span(payload->data(), payload->size())), + *payload + ); + + if (!PackageService_->WritePackage(AssetDatabaseService_->GetProjectDescriptor().RootPath / relativePrefabPath, std::move(package))) { + return false; + } + + (void)AssetDatabaseService_->Refresh(); + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Prefab", "已应用 Prefab 实例到资源"); + return true; +} + +bool MetaCoreBuiltinPrefabService::RevertSelectedPrefabInstance(MetaCoreEditorContext& editorContext) { + const MetaCoreId activeObjectId = editorContext.GetActiveObjectId(); + if (activeObjectId == 0) { + return false; + } + + const auto instanceRootId = MetaCoreFindPrefabInstanceRootId(editorContext.GetScene(), activeObjectId); + if (!instanceRootId.has_value()) { + return false; + } + + const MetaCoreGameObject* rootObject = editorContext.GetScene().FindGameObject(*instanceRootId); + if (rootObject == nullptr || !rootObject->PrefabInstance.has_value()) { + return false; + } + + const MetaCoreAssetGuid prefabAssetGuid = rootObject->PrefabInstance->PrefabAssetGuid; + const auto prefabDocument = LoadPrefabDocument(prefabAssetGuid); + if (!prefabDocument.has_value()) { + return false; + } + + const bool reverted = editorContext.ExecuteSnapshotCommand("还原 Prefab 实例", [&]() { + std::vector deletedIds = editorContext.GetScene().DeleteGameObjects({*instanceRootId}); + if (deletedIds.empty()) { + return false; + } + return InstantiatePrefabDocument( + editorContext, + prefabAssetGuid, + *prefabDocument, + rootObject->ParentId + ).has_value(); + }); + + if (reverted) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Prefab", "已还原 Prefab 实例"); + } + return reverted; +} + +bool MetaCoreBuiltinClipboardService::DuplicateSelection(MetaCoreEditorContext& editorContext) { + const std::vector selectedIds = editorContext.GetSelectedObjectIds(); + if (selectedIds.empty()) { + return false; + } + + std::vector duplicatedRootIds; + const bool duplicated = editorContext.ExecuteSnapshotCommand("复制对象", [&]() { + duplicatedRootIds = editorContext.GetScene().DuplicateGameObjects(selectedIds); + if (duplicatedRootIds.empty()) { + return false; + } + editorContext.SetSelection(duplicatedRootIds, duplicatedRootIds.back()); + return true; + }); + + if (duplicated) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已复制对象"); + } + return duplicated; +} + +bool MetaCoreBuiltinClipboardService::DeleteSelection(MetaCoreEditorContext& editorContext) { + const std::vector selectedIds = editorContext.GetSelectedObjectIds(); + if (selectedIds.empty()) { + return false; + } + + std::vector deletedIds; + const bool deleted = editorContext.ExecuteSnapshotCommand("删除对象", [&]() { + deletedIds = editorContext.GetScene().DeleteGameObjects(selectedIds); + if (deletedIds.empty()) { + return false; + } + editorContext.ClearSelection(); + return true; + }); + + if (deleted) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已删除对象"); + } + return deleted; +} + +std::optional MetaCoreBuiltinSceneEditingService::CreateGameObject( + MetaCoreEditorContext& editorContext, + const std::string& objectName, + bool withMeshRenderer, + std::optional forcedParentId +) { + MetaCoreId createdObjectId = 0; + const bool created = editorContext.ExecuteSnapshotCommand("创建对象", [&]() { + MetaCoreId parentId = forcedParentId.value_or(editorContext.GetActiveObjectId()); + if (parentId != 0 && editorContext.GetScene().FindGameObject(parentId) == nullptr) { + parentId = 0; + } + + MetaCoreGameObject& object = editorContext.GetScene().CreateGameObject(objectName, parentId); + if (withMeshRenderer) { + object.MeshRenderer = MetaCoreMeshRendererComponent{}; + object.Transform.Position = glm::vec3(0.0F, 0.5F, 0.0F); + } + + createdObjectId = object.Id; + editorContext.SelectOnly(createdObjectId); + editorContext.SetSelectionAnchorId(createdObjectId); + return true; + }); + + if (!created || createdObjectId == 0) { + return std::nullopt; + } + + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Info, + "Scene", + withMeshRenderer ? "已创建立方体对象" : "已创建空对象" + ); + return createdObjectId; +} + +bool MetaCoreBuiltinSceneEditingService::RenameGameObject( + MetaCoreEditorContext& editorContext, + MetaCoreId objectId, + const std::string& name +) { + if (objectId == 0 || name.empty()) { + return false; + } + + const bool renamed = editorContext.ExecuteSnapshotCommand("重命名对象", [&]() { + return editorContext.GetScene().RenameGameObject(objectId, name); + }); + + if (renamed) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Hierarchy", "已重命名对象: " + name); + } + return renamed; +} + +bool MetaCoreBuiltinSceneEditingService::ReparentSelection(MetaCoreEditorContext& editorContext, MetaCoreId newParentId) { + const std::vector selectedIds = editorContext.GetSelectedObjectIds(); + if (selectedIds.empty()) { + return false; + } + + const bool keepWorldTransform = editorContext.GetReparentTransformRule() == MetaCoreReparentTransformRule::KeepWorld; + const bool reparented = editorContext.ExecuteSnapshotCommand("重挂接对象", [&]() { + return editorContext.GetScene().ReparentGameObjects(selectedIds, newParentId, keepWorldTransform); + }); + + if (reparented) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Info, + "Hierarchy", + keepWorldTransform ? "已重挂接对象(保持世界变换)" : "已重挂接对象(保持局部变换)" + ); + } + return reparented; +} + +class MetaCoreBuiltinCoreServicesModule final : public MetaCoreIModule { +public: + [[nodiscard]] std::string GetModuleName() const override { return "MetaCoreBuiltinCoreServicesModule"; } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); + + if (const auto assetDatabase = moduleRegistry.ResolveService(); + assetDatabase != nullptr && assetDatabase->Refresh() && assetDatabase->HasProject()) { + moduleRegistry.AccessEventBus().Publish(MetaCoreEditorEvent{ + MetaCoreEditorEventType::ProjectLoaded, + "MetaCore 项目已加载", + assetDatabase->GetProjectDescriptor().RootPath, + 0, + true + }); + } + } + + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + } +}; + +} // namespace + +std::unique_ptr MetaCoreCreateBuiltinCoreServicesModule() { + return std::make_unique(); +} + +} // namespace MetaCore diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index b1c51ec..2f687ff 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -1,15 +1,22 @@ #include "MetaCoreBuiltinEditorModule.h" #include "MetaCoreEditor/MetaCoreEditorContext.h" +#include "MetaCoreEditor/MetaCoreEditorAssetTypes.h" +#include "MetaCoreEditor/MetaCoreEditorServices.h" #include "MetaCoreEditorCameraController.h" +#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" +#include "MetaCoreFoundation/MetaCoreHash.h" +#include "MetaCoreFoundation/MetaCorePackage.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h" #include "MetaCorePlatform/MetaCoreWindow.h" #include -#include - #include +#include #include +#include #include #include #include @@ -20,19 +27,57 @@ namespace MetaCore { namespace { +[[nodiscard]] MetaCoreTypeRegistry MetaCoreBuildRuntimePanelTypeRegistry() { + MetaCoreTypeRegistry registry; + MetaCoreRegisterFoundationGeneratedTypes(registry); + MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + return registry; +} + +[[nodiscard]] const char* MetaCoreAssetStorageLabel(MetaCoreAssetStorageKind storageKind) { + switch (storageKind) { + case MetaCoreAssetStorageKind::SourcePackage: + return "Pkg"; + case MetaCoreAssetStorageKind::SourceFile: + return "Src"; + case MetaCoreAssetStorageKind::Metadata: + return "Meta"; + case MetaCoreAssetStorageKind::Cooked: + return "Cooked"; + } + return "Asset"; +} + +[[nodiscard]] MetaCorePackageDocument MetaCoreBuildTypedPackage( + MetaCorePackageType packageType, + const MetaCoreAssetGuid& assetGuid, + const std::string& objectName, + std::string_view typeName, + std::uint64_t sourceHash, + std::vector payload +) { + MetaCorePackageDocument document; + document.Header.PackageType = packageType; + document.Header.PackageGuid = assetGuid; + document.Header.SourceHash = sourceHash; + document.NameTable.emplace_back(typeName); + document.Exports.push_back(MetaCoreExportEntry{ + assetGuid, + objectName, + MetaCoreMakeTypeId(typeName), + 0, + static_cast(payload.size()) + }); + document.PayloadSections.push_back(std::move(payload)); + return document; +} + [[nodiscard]] bool MetaCoreRenameObject(MetaCoreEditorContext& editorContext, MetaCoreId objectId, const std::string& name) { - if (objectId == 0 || name.empty()) { + const auto sceneEditingService = editorContext.GetModuleRegistry().ResolveService(); + if (sceneEditingService == nullptr) { return false; } - - const bool renamed = editorContext.ExecuteSnapshotCommand("重命名对象", [&]() { - return editorContext.GetScene().RenameGameObject(objectId, name); - }); - - if (renamed) { - editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Hierarchy", "已重命名对象: " + name); - } - return renamed; + return sceneEditingService->RenameGameObject(editorContext, objectId, name); } [[nodiscard]] std::optional MetaCoreCreateObject( @@ -41,102 +86,73 @@ namespace { bool withMeshRenderer, std::optional forcedParentId ) { - MetaCoreId createdObjectId = 0; - const bool created = editorContext.ExecuteSnapshotCommand("创建对象", [&]() { - MetaCoreId parentId = forcedParentId.value_or(editorContext.GetActiveObjectId()); - if (parentId != 0 && editorContext.GetScene().FindGameObject(parentId) == nullptr) { - parentId = 0; - } - - MetaCoreGameObject& object = editorContext.GetScene().CreateGameObject(objectName, parentId); - if (withMeshRenderer) { - object.MeshRenderer = MetaCoreMeshRendererComponent{}; - object.Transform.Position = glm::vec3(0.0F, 0.5F, 0.0F); - } - - createdObjectId = object.Id; - editorContext.SelectOnly(createdObjectId); - editorContext.SetSelectionAnchorId(createdObjectId); - return true; - }); - - if (!created || createdObjectId == 0) { + const auto sceneEditingService = editorContext.GetModuleRegistry().ResolveService(); + if (sceneEditingService == nullptr) { return std::nullopt; } - - editorContext.AddConsoleMessage( - MetaCoreLogLevel::Info, - "Scene", - withMeshRenderer ? "已创建立方体对象" : "已创建空对象" - ); - return createdObjectId; + return sceneEditingService->CreateGameObject(editorContext, objectName, withMeshRenderer, forcedParentId); } [[nodiscard]] bool MetaCoreDuplicateSelection(MetaCoreEditorContext& editorContext) { - const std::vector selectedIds = editorContext.GetSelectedObjectIds(); - if (selectedIds.empty()) { + const auto clipboardService = editorContext.GetModuleRegistry().ResolveService(); + if (clipboardService == nullptr) { return false; } - - std::vector duplicatedRootIds; - const bool duplicated = editorContext.ExecuteSnapshotCommand("复制对象", [&]() { - duplicatedRootIds = editorContext.GetScene().DuplicateGameObjects(selectedIds); - if (duplicatedRootIds.empty()) { - return false; - } - - editorContext.SetSelection(duplicatedRootIds, duplicatedRootIds.back()); - return true; - }); - - if (duplicated) { - editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已复制对象"); - } - return duplicated; + return clipboardService->DuplicateSelection(editorContext); } [[nodiscard]] bool MetaCoreDeleteSelection(MetaCoreEditorContext& editorContext) { - const std::vector selectedIds = editorContext.GetSelectedObjectIds(); - if (selectedIds.empty()) { + const auto clipboardService = editorContext.GetModuleRegistry().ResolveService(); + if (clipboardService == nullptr) { return false; } - - std::vector deletedIds; - const bool deleted = editorContext.ExecuteSnapshotCommand("删除对象", [&]() { - deletedIds = editorContext.GetScene().DeleteGameObjects(selectedIds); - if (deletedIds.empty()) { - return false; - } - - editorContext.ClearSelection(); - return true; - }); - - if (deleted) { - editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已删除对象"); - } - return deleted; + return clipboardService->DeleteSelection(editorContext); } [[nodiscard]] bool MetaCoreReparentSelection(MetaCoreEditorContext& editorContext, MetaCoreId newParentId) { - const std::vector selectedIds = editorContext.GetSelectedObjectIds(); - if (selectedIds.empty()) { + const auto sceneEditingService = editorContext.GetModuleRegistry().ResolveService(); + if (sceneEditingService == nullptr) { return false; } + return sceneEditingService->ReparentSelection(editorContext, newParentId); +} - const bool keepWorldTransform = editorContext.GetReparentTransformRule() == MetaCoreReparentTransformRule::KeepWorld; - const bool reparented = editorContext.ExecuteSnapshotCommand("重挂接对象", [&]() { - return editorContext.GetScene().ReparentGameObjects(selectedIds, newParentId, keepWorldTransform); - }); - - if (reparented) { - editorContext.AddConsoleMessage( - MetaCoreLogLevel::Info, - "Hierarchy", - keepWorldTransform ? "已重挂接对象(保持世界变换)" : "已重挂接对象(保持局部变换)" - ); +[[nodiscard]] std::optional MetaCoreCreatePrefabFromSelection(MetaCoreEditorContext& editorContext) { + const auto prefabService = editorContext.GetModuleRegistry().ResolveService(); + if (prefabService == nullptr || !prefabService->SupportsPrefabWorkflows()) { + return std::nullopt; } - return reparented; + + const MetaCoreGameObject* activeObject = editorContext.GetSelectedGameObject(); + if (activeObject == nullptr) { + return std::nullopt; + } + + const std::filesystem::path prefabPath = + std::filesystem::path("Assets") / "Prefabs" / (activeObject->Name + ".mcprefab"); + return prefabService->CreatePrefabFromSelection(editorContext, prefabPath); +} + +[[nodiscard]] std::optional MetaCoreInstantiatePrefab( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& prefabAssetGuid, + std::optional forcedParentId +) { + const auto prefabService = editorContext.GetModuleRegistry().ResolveService(); + if (prefabService == nullptr || !prefabService->SupportsPrefabWorkflows()) { + return std::nullopt; + } + return prefabService->InstantiatePrefab(editorContext, prefabAssetGuid, forcedParentId); +} + +[[nodiscard]] bool MetaCoreApplySelectedPrefabInstance(MetaCoreEditorContext& editorContext) { + const auto prefabService = editorContext.GetModuleRegistry().ResolveService(); + return prefabService != nullptr && prefabService->ApplySelectedPrefabInstance(editorContext); +} + +[[nodiscard]] bool MetaCoreRevertSelectedPrefabInstance(MetaCoreEditorContext& editorContext) { + const auto prefabService = editorContext.GetModuleRegistry().ResolveService(); + return prefabService != nullptr && prefabService->RevertSelectedPrefabInstance(editorContext); } void MetaCoreHandleUndo(MetaCoreEditorContext& editorContext) { @@ -151,6 +167,20 @@ void MetaCoreHandleRedo(MetaCoreEditorContext& editorContext) { } } +void MetaCoreHandleSaveScene(MetaCoreEditorContext& editorContext) { + const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService(); + if (scenePersistenceService != nullptr) { + (void)scenePersistenceService->SaveCurrentScene(editorContext); + } +} + +void MetaCoreHandleReloadAssets(MetaCoreEditorContext& editorContext) { + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService != nullptr && assetDatabaseService->Refresh()) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Assets", "已刷新 Asset 数据库"); + } +} + void MetaCoreHandleGlobalEditorShortcuts(MetaCoreEditorContext& editorContext) { const ImGuiIO& io = ImGui::GetIO(); if (io.WantTextInput) { @@ -180,6 +210,11 @@ void MetaCoreHandleGlobalEditorShortcuts(MetaCoreEditorContext& editorContext) { return; } + if (ctrlDown && ImGui::IsKeyPressed(ImGuiKey_S, false)) { + MetaCoreHandleSaveScene(editorContext); + return; + } + if (ImGui::IsKeyPressed(ImGuiKey_Delete, false)) { (void)MetaCoreDeleteSelection(editorContext); return; @@ -191,22 +226,11 @@ void MetaCoreHandleGlobalEditorShortcuts(MetaCoreEditorContext& editorContext) { } void MetaCoreApplyHierarchySelection(MetaCoreEditorContext& editorContext, MetaCoreId objectId, bool allowToggle) { - if (objectId == 0 || editorContext.GetScene().FindGameObject(objectId) == nullptr) { + const auto selectionService = editorContext.GetModuleRegistry().ResolveService(); + if (selectionService == nullptr) { return; } - - const ImGuiIO& io = ImGui::GetIO(); - if (io.KeyShift) { - editorContext.SelectRangeByOrderedIds(editorContext.GetScene().BuildHierarchyPreorder(), objectId, io.KeyCtrl); - return; - } - - if (allowToggle && io.KeyCtrl) { - editorContext.ToggleSelection(objectId); - return; - } - - editorContext.SelectOnly(objectId); + selectionService->ApplyHierarchySelection(editorContext, objectId, allowToggle); } std::unordered_map& MetaCoreGetInspectorEditSnapshots() { @@ -214,6 +238,182 @@ std::unordered_map& MetaCoreGetInspectorEd return snapshots; } +template +void MetaCoreForEachSelectedObject(MetaCoreEditorContext& editorContext, TAccessor&& accessor) { + for (const MetaCoreId selectedId : editorContext.GetSelectedObjectIds()) { + if (MetaCoreGameObject* selectedObject = editorContext.GetScene().FindGameObject(selectedId); selectedObject != nullptr) { + accessor(*selectedObject); + } + } +} + +[[nodiscard]] bool MetaCoreNearlyEqual(float lhs, float rhs) { + return std::abs(lhs - rhs) <= 0.0001F; +} + +[[nodiscard]] bool MetaCoreNearlyEqualVec3(const glm::vec3& lhs, const glm::vec3& rhs) { + return MetaCoreNearlyEqual(lhs.x, rhs.x) && + MetaCoreNearlyEqual(lhs.y, rhs.y) && + MetaCoreNearlyEqual(lhs.z, rhs.z); +} + +template +std::optional MetaCoreGetSharedSelectedValue( + MetaCoreEditorContext& editorContext, + TAccessor&& accessor, + TComparer&& comparer +) { + bool initialized = false; + bool mixed = false; + TValue sharedValue{}; + MetaCoreForEachSelectedObject(editorContext, [&](MetaCoreGameObject& selectedObject) { + if (mixed) { + return; + } + + const std::optional value = accessor(selectedObject); + if (!value.has_value()) { + return; + } + + if (!initialized) { + sharedValue = *value; + initialized = true; + return; + } + + if (!comparer(sharedValue, *value)) { + mixed = true; + } + }); + + if (!initialized || mixed) { + return std::nullopt; + } + return sharedValue; +} + +template +void MetaCoreApplyValueToSelectedObjects(MetaCoreEditorContext& editorContext, TAccessor&& accessor, const TValue& value) { + MetaCoreForEachSelectedObject(editorContext, [&](MetaCoreGameObject& selectedObject) { + if (auto* targetValue = accessor(selectedObject); targetValue != nullptr) { + *targetValue = value; + } + }); +} + +template +[[nodiscard]] std::optional MetaCoreReadTypedPackagePayload( + const MetaCorePackageDocument& package, + const MetaCoreTypeRegistry& registry, + std::string_view expectedTypeName +) { + if (package.Exports.empty()) { + return std::nullopt; + } + + const MetaCoreStructDescriptor* descriptor = registry.FindStruct(); + const MetaCoreExportEntry& exportEntry = package.Exports.front(); + if (descriptor == nullptr || descriptor->Name != expectedTypeName || exportEntry.PayloadIndex >= package.PayloadSections.size()) { + return std::nullopt; + } + + T value{}; + if (!MetaCoreDeserializeFromBytes(package.PayloadSections[exportEntry.PayloadIndex], value, registry)) { + return std::nullopt; + } + return value; +} + +[[nodiscard]] std::optional MetaCoreLoadPrefabDocumentForInstance( + MetaCoreEditorContext& editorContext, + const MetaCoreGameObject& gameObject +) { + if (!gameObject.PrefabInstance.has_value()) { + return std::nullopt; + } + + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + const auto packageService = editorContext.GetModuleRegistry().ResolveService(); + const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || packageService == nullptr || reflectionRegistry == nullptr || !assetDatabaseService->HasProject()) { + return std::nullopt; + } + + const auto prefabAsset = assetDatabaseService->FindAssetByGuid(gameObject.PrefabInstance->PrefabAssetGuid); + if (!prefabAsset.has_value()) { + return std::nullopt; + } + + const std::filesystem::path relativePrefabPath = + !prefabAsset->PackagePath.empty() ? prefabAsset->PackagePath : prefabAsset->RelativePath; + const auto package = packageService->ReadPackage(assetDatabaseService->GetProjectDescriptor().RootPath / relativePrefabPath); + if (!package.has_value() || package->Header.PackageType != MetaCorePackageType::Prefab) { + return std::nullopt; + } + + return MetaCoreReadTypedPackagePayload( + *package, + reflectionRegistry->GetTypeRegistry(), + "MetaCorePrefabDocument" + ); +} + +[[nodiscard]] const MetaCoreGameObject* MetaCoreFindPrefabSourceObject( + const MetaCorePrefabDocument& prefabDocument, + MetaCoreId prefabObjectId +) { + const auto iterator = std::find_if(prefabDocument.GameObjects.begin(), prefabDocument.GameObjects.end(), [&](const MetaCoreGameObject& prefabObject) { + return prefabObject.Id == prefabObjectId; + }); + return iterator == prefabDocument.GameObjects.end() ? nullptr : &(*iterator); +} + +[[nodiscard]] bool MetaCoreHasTransformOverride(const MetaCoreGameObject& instanceObject, const MetaCoreGameObject& prefabObject) { + return !MetaCoreNearlyEqualVec3(instanceObject.Transform.Position, prefabObject.Transform.Position) || + !MetaCoreNearlyEqualVec3(instanceObject.Transform.RotationEulerDegrees, prefabObject.Transform.RotationEulerDegrees) || + !MetaCoreNearlyEqualVec3(instanceObject.Transform.Scale, prefabObject.Transform.Scale); +} + +[[nodiscard]] bool MetaCoreHasCameraOverride(const MetaCoreGameObject& instanceObject, const MetaCoreGameObject& prefabObject) { + if (instanceObject.Camera.has_value() != prefabObject.Camera.has_value()) { + return true; + } + if (!instanceObject.Camera.has_value()) { + return false; + } + + return !MetaCoreNearlyEqual(instanceObject.Camera->FieldOfViewDegrees, prefabObject.Camera->FieldOfViewDegrees) || + !MetaCoreNearlyEqual(instanceObject.Camera->NearClip, prefabObject.Camera->NearClip) || + !MetaCoreNearlyEqual(instanceObject.Camera->FarClip, prefabObject.Camera->FarClip) || + instanceObject.Camera->IsPrimary != prefabObject.Camera->IsPrimary; +} + +[[nodiscard]] bool MetaCoreHasLightOverride(const MetaCoreGameObject& instanceObject, const MetaCoreGameObject& prefabObject) { + if (instanceObject.Light.has_value() != prefabObject.Light.has_value()) { + return true; + } + if (!instanceObject.Light.has_value()) { + return false; + } + + return !MetaCoreNearlyEqualVec3(instanceObject.Light->Color, prefabObject.Light->Color) || + !MetaCoreNearlyEqual(instanceObject.Light->Intensity, prefabObject.Light->Intensity); +} + +[[nodiscard]] bool MetaCoreHasMeshRendererOverride(const MetaCoreGameObject& instanceObject, const MetaCoreGameObject& prefabObject) { + if (instanceObject.MeshRenderer.has_value() != prefabObject.MeshRenderer.has_value()) { + return true; + } + if (!instanceObject.MeshRenderer.has_value()) { + return false; + } + + return instanceObject.MeshRenderer->BuiltinMesh != prefabObject.MeshRenderer->BuiltinMesh || + !MetaCoreNearlyEqualVec3(instanceObject.MeshRenderer->BaseColor, prefabObject.MeshRenderer->BaseColor) || + instanceObject.MeshRenderer->Visible != prefabObject.MeshRenderer->Visible; +} + void MetaCoreTrackInspectorEdit(MetaCoreEditorContext& editorContext, const char* commandLabel, bool allowMerge) { const ImGuiID itemId = ImGui::GetItemID(); if (itemId == 0) { @@ -235,14 +435,426 @@ void MetaCoreTrackInspectorEdit(MetaCoreEditorContext& editorContext, const char } } +[[nodiscard]] bool MetaCoreMutateSelectedObjectWithComponentDescriptor( + MetaCoreEditorContext& editorContext, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, + bool addComponent +) { + if (editorContext.GetSelectedObjectIds().empty()) { + return false; + } + + const bool changed = editorContext.ExecuteSnapshotCommand( + addComponent ? "添加组件" : "移除组件", + [&]() { + bool anyChanged = false; + for (const MetaCoreId selectedId : editorContext.GetSelectedObjectIds()) { + MetaCoreGameObject* selectedObject = editorContext.GetScene().FindGameObject(selectedId); + if (selectedObject == nullptr) { + continue; + } + anyChanged |= addComponent ? descriptor.AddComponent(*selectedObject) : descriptor.RemoveComponent(*selectedObject); + } + return anyChanged; + } + ); + + if (changed) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Info, + "Inspector", + std::string(addComponent ? "已添加组件: " : "已移除组件: ") + descriptor.DisplayName + ); + } + return changed; +} + +[[nodiscard]] bool MetaCoreResetSelectedObjectComponent( + MetaCoreEditorContext& editorContext, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor +) { + if (editorContext.GetSelectedObjectIds().empty() || !descriptor.ResetComponent) { + return false; + } + + const bool changed = editorContext.ExecuteSnapshotCommand("重置组件", [&]() { + bool anyChanged = false; + for (const MetaCoreId selectedId : editorContext.GetSelectedObjectIds()) { + MetaCoreGameObject* selectedObject = editorContext.GetScene().FindGameObject(selectedId); + if (selectedObject == nullptr) { + continue; + } + anyChanged |= descriptor.ResetComponent(*selectedObject); + } + return anyChanged; + }); + + if (changed) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Inspector", "已重置组件: " + descriptor.DisplayName); + } + return changed; +} + +[[nodiscard]] bool MetaCoreCopySelectedObjectComponent( + MetaCoreEditorContext& editorContext, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor +) { + const MetaCoreGameObject* selectedObject = editorContext.GetSelectedGameObject(); + if (selectedObject == nullptr) { + return false; + } + + const auto componentRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (componentRegistry == nullptr || !componentRegistry->CopyComponent(descriptor.TypeId, *selectedObject)) { + return false; + } + + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Inspector", "已复制组件值: " + descriptor.DisplayName); + return true; +} + +[[nodiscard]] bool MetaCorePasteSelectedObjectComponent( + MetaCoreEditorContext& editorContext, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor +) { + if (editorContext.GetSelectedObjectIds().empty()) { + return false; + } + + const auto componentRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (componentRegistry == nullptr || !componentRegistry->CanPasteComponent(descriptor.TypeId)) { + return false; + } + + const bool changed = editorContext.ExecuteSnapshotCommand("粘贴组件值", [&]() { + bool anyChanged = false; + for (const MetaCoreId selectedId : editorContext.GetSelectedObjectIds()) { + MetaCoreGameObject* selectedObject = editorContext.GetScene().FindGameObject(selectedId); + if (selectedObject == nullptr) { + continue; + } + anyChanged |= componentRegistry->PasteComponent(descriptor.TypeId, *selectedObject); + } + return anyChanged; + }); + + if (changed) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Inspector", "已粘贴组件值: " + descriptor.DisplayName); + } + return changed; +} + +[[nodiscard]] bool MetaCoreRevertSelectedObjectComponentToPrefab( + MetaCoreEditorContext& editorContext, + const std::string_view componentTypeId +) { + MetaCoreGameObject* selectedObject = editorContext.GetSelectedGameObject(); + if (selectedObject == nullptr || !selectedObject->PrefabInstance.has_value()) { + return false; + } + + const auto prefabDocument = MetaCoreLoadPrefabDocumentForInstance(editorContext, *selectedObject); + if (!prefabDocument.has_value()) { + return false; + } + + const MetaCoreGameObject* prefabObject = MetaCoreFindPrefabSourceObject(*prefabDocument, selectedObject->PrefabInstance->PrefabObjectId); + if (prefabObject == nullptr) { + return false; + } + + const bool changed = editorContext.ExecuteSnapshotCommand("回退组件到 Prefab", [&]() { + if (componentTypeId == "Transform") { + selectedObject->Transform = prefabObject->Transform; + return true; + } + if (componentTypeId == "Camera") { + selectedObject->Camera = prefabObject->Camera; + return true; + } + if (componentTypeId == "Light") { + selectedObject->Light = prefabObject->Light; + return true; + } + if (componentTypeId == "MeshRenderer") { + selectedObject->MeshRenderer = prefabObject->MeshRenderer; + return true; + } + return false; + }); + + if (changed) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Prefab", "已回退组件到 Prefab"); + } + return changed; +} + +[[nodiscard]] bool MetaCoreRevertSelectedTransformFieldToPrefab( + MetaCoreEditorContext& editorContext, + const std::string_view fieldId +) { + MetaCoreGameObject* selectedObject = editorContext.GetSelectedGameObject(); + if (selectedObject == nullptr || !selectedObject->PrefabInstance.has_value()) { + return false; + } + + const auto prefabDocument = MetaCoreLoadPrefabDocumentForInstance(editorContext, *selectedObject); + if (!prefabDocument.has_value()) { + return false; + } + + const MetaCoreGameObject* prefabObject = MetaCoreFindPrefabSourceObject(*prefabDocument, selectedObject->PrefabInstance->PrefabObjectId); + if (prefabObject == nullptr) { + return false; + } + + const bool changed = editorContext.ExecuteSnapshotCommand("回退 Transform 字段到 Prefab", [&]() { + if (fieldId == "position") { + selectedObject->Transform.Position = prefabObject->Transform.Position; + return true; + } + if (fieldId == "rotation") { + selectedObject->Transform.RotationEulerDegrees = prefabObject->Transform.RotationEulerDegrees; + return true; + } + if (fieldId == "scale") { + selectedObject->Transform.Scale = prefabObject->Transform.Scale; + return true; + } + return false; + }); + + if (changed) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Prefab", "已回退 Transform 字段到 Prefab"); + } + return changed; +} + +[[nodiscard]] const char* MetaCoreRuntimeBindingTargetLabel(MetaCoreRuntimeBindingTarget target) { + switch (target) { + case MetaCoreRuntimeBindingTarget::TransformPosition: + return "Transform.Position"; + case MetaCoreRuntimeBindingTarget::MeshRendererVisible: + return "MeshRenderer.Visible"; + case MetaCoreRuntimeBindingTarget::MeshRendererBaseColor: + return "MeshRenderer.BaseColor"; + case MetaCoreRuntimeBindingTarget::LightIntensity: + return "Light.Intensity"; + case MetaCoreRuntimeBindingTarget::LightColor: + return "Light.Color"; + } + return "Unknown"; +} + +[[nodiscard]] std::array MetaCoreRuntimeBindingTargets() { + return { + MetaCoreRuntimeBindingTarget::TransformPosition, + MetaCoreRuntimeBindingTarget::MeshRendererVisible, + MetaCoreRuntimeBindingTarget::MeshRendererBaseColor, + MetaCoreRuntimeBindingTarget::LightIntensity, + MetaCoreRuntimeBindingTarget::LightColor + }; +} + +[[nodiscard]] const char* MetaCoreRuntimeValueTypeLabel(MetaCoreRuntimeValueType valueType) { + switch (valueType) { + case MetaCoreRuntimeValueType::Bool: + return "Bool"; + case MetaCoreRuntimeValueType::Int64: + return "Int64"; + case MetaCoreRuntimeValueType::Double: + return "Double"; + case MetaCoreRuntimeValueType::String: + return "String"; + case MetaCoreRuntimeValueType::Vec3: + return "Vec3"; + } + return "Unknown"; +} + +[[nodiscard]] std::array MetaCoreRuntimeValueTypes() { + return { + MetaCoreRuntimeValueType::Bool, + MetaCoreRuntimeValueType::Int64, + MetaCoreRuntimeValueType::Double, + MetaCoreRuntimeValueType::String, + MetaCoreRuntimeValueType::Vec3 + }; +} + +[[nodiscard]] std::array MetaCoreRuntimeAdapterTypes() { + return {"file_replay", "tcp"}; +} + +[[nodiscard]] bool MetaCoreWritePrefabDocumentForInstance( + MetaCoreEditorContext& editorContext, + const MetaCoreGameObject& instanceObject, + const MetaCorePrefabDocument& prefabDocument +) { + if (!instanceObject.PrefabInstance.has_value()) { + return false; + } + + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + const auto packageService = editorContext.GetModuleRegistry().ResolveService(); + const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || packageService == nullptr || reflectionRegistry == nullptr || !assetDatabaseService->HasProject()) { + return false; + } + + const auto prefabAsset = assetDatabaseService->FindAssetByGuid(instanceObject.PrefabInstance->PrefabAssetGuid); + if (!prefabAsset.has_value()) { + return false; + } + + const auto payload = MetaCoreSerializeToBytes(prefabDocument, reflectionRegistry->GetTypeRegistry()); + if (!payload.has_value()) { + return false; + } + + const std::filesystem::path relativePrefabPath = + !prefabAsset->PackagePath.empty() ? prefabAsset->PackagePath : prefabAsset->RelativePath; + MetaCorePackageDocument package = MetaCoreBuildTypedPackage( + MetaCorePackageType::Prefab, + instanceObject.PrefabInstance->PrefabAssetGuid, + relativePrefabPath.filename().string(), + "MetaCorePrefabDocument", + MetaCoreHashBytes(std::span(payload->data(), payload->size())), + *payload + ); + + if (!packageService->WritePackage(assetDatabaseService->GetProjectDescriptor().RootPath / relativePrefabPath, std::move(package))) { + return false; + } + + (void)assetDatabaseService->Refresh(); + return true; +} + +[[nodiscard]] bool MetaCoreApplySelectedPrefabField( + MetaCoreEditorContext& editorContext, + std::string_view componentTypeId, + std::string_view fieldId +) { + MetaCoreGameObject* selectedObject = editorContext.GetSelectedGameObject(); + if (selectedObject == nullptr || !selectedObject->PrefabInstance.has_value()) { + return false; + } + + const auto prefabDocument = MetaCoreLoadPrefabDocumentForInstance(editorContext, *selectedObject); + if (!prefabDocument.has_value()) { + return false; + } + + auto updatedPrefabDocument = *prefabDocument; + auto prefabObjectIterator = std::find_if( + updatedPrefabDocument.GameObjects.begin(), + updatedPrefabDocument.GameObjects.end(), + [&](const MetaCoreGameObject& prefabObject) { + return prefabObject.Id == selectedObject->PrefabInstance->PrefabObjectId; + } + ); + if (prefabObjectIterator == updatedPrefabDocument.GameObjects.end()) { + return false; + } + + MetaCoreGameObject prefabObject = *prefabObjectIterator; + const bool changed = [&]() { + if (componentTypeId == "Transform") { + if (fieldId == "position") { + prefabObject.Transform.Position = selectedObject->Transform.Position; + return true; + } + if (fieldId == "rotation") { + prefabObject.Transform.RotationEulerDegrees = selectedObject->Transform.RotationEulerDegrees; + return true; + } + if (fieldId == "scale") { + prefabObject.Transform.Scale = selectedObject->Transform.Scale; + return true; + } + } else if (componentTypeId == "Camera" && selectedObject->Camera.has_value()) { + if (!prefabObject.Camera.has_value()) { + prefabObject.Camera = MetaCoreCameraComponent{}; + } + if (fieldId == "fov") { + prefabObject.Camera->FieldOfViewDegrees = selectedObject->Camera->FieldOfViewDegrees; + return true; + } + if (fieldId == "near_clip") { + prefabObject.Camera->NearClip = selectedObject->Camera->NearClip; + return true; + } + if (fieldId == "far_clip") { + prefabObject.Camera->FarClip = selectedObject->Camera->FarClip; + return true; + } + if (fieldId == "primary") { + prefabObject.Camera->IsPrimary = selectedObject->Camera->IsPrimary; + return true; + } + } else if (componentTypeId == "Light" && selectedObject->Light.has_value()) { + if (!prefabObject.Light.has_value()) { + prefabObject.Light = MetaCoreLightComponent{}; + } + if (fieldId == "color") { + prefabObject.Light->Color = selectedObject->Light->Color; + return true; + } + if (fieldId == "intensity") { + prefabObject.Light->Intensity = selectedObject->Light->Intensity; + return true; + } + } else if (componentTypeId == "MeshRenderer" && selectedObject->MeshRenderer.has_value()) { + if (!prefabObject.MeshRenderer.has_value()) { + prefabObject.MeshRenderer = MetaCoreMeshRendererComponent{}; + } + if (fieldId == "base_color") { + prefabObject.MeshRenderer->BaseColor = selectedObject->MeshRenderer->BaseColor; + return true; + } + if (fieldId == "visible") { + prefabObject.MeshRenderer->Visible = selectedObject->MeshRenderer->Visible; + return true; + } + } + return false; + }(); + + if (!changed) { + return false; + } + + prefabObjectIterator->Transform = prefabObject.Transform; + prefabObjectIterator->Camera = prefabObject.Camera; + prefabObjectIterator->Light = prefabObject.Light; + prefabObjectIterator->MeshRenderer = prefabObject.MeshRenderer; + + if (!MetaCoreWritePrefabDocumentForInstance(editorContext, *selectedObject, updatedPrefabDocument)) { + return false; + } + + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Prefab", "已应用 Prefab 字段"); + return true; +} + class MetaCoreDefaultMenuProvider final : public MetaCoreIMenuProvider { public: std::string GetProviderId() const override { return "MetaCoreDefaultMenuProvider"; } void DrawMenuBar(MetaCoreEditorContext& editorContext) override { MetaCoreHandleGlobalEditorShortcuts(editorContext); + const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService(); + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); if (ImGui::BeginMenu("文件")) { + if (ImGui::MenuItem("保存场景", "Ctrl+S", false, scenePersistenceService != nullptr)) { + MetaCoreHandleSaveScene(editorContext); + } + if (ImGui::MenuItem("刷新 Asset 数据库", nullptr, false, assetDatabaseService != nullptr)) { + MetaCoreHandleReloadAssets(editorContext); + } + ImGui::Separator(); if (ImGui::MenuItem("重置布局")) { editorContext.SetDockLayoutBuilt(false); } @@ -274,6 +886,9 @@ public: if (ImGui::BeginMenu("资源")) { ImGui::MenuItem("导入资源", nullptr, false, false); + if (ImGui::MenuItem("从选中对象创建 Prefab", nullptr, false, editorContext.GetActiveObjectId() != 0)) { + (void)MetaCoreCreatePrefabFromSelection(editorContext); + } ImGui::EndMenu(); } @@ -285,6 +900,13 @@ public: (void)MetaCoreCreateObject(editorContext, "Cube", true, std::nullopt); } ImGui::Separator(); + if (ImGui::MenuItem("应用当前 Prefab", nullptr, false, editorContext.GetActiveObjectId() != 0)) { + (void)MetaCoreApplySelectedPrefabInstance(editorContext); + } + if (ImGui::MenuItem("还原当前 Prefab", nullptr, false, editorContext.GetActiveObjectId() != 0)) { + (void)MetaCoreRevertSelectedPrefabInstance(editorContext); + } + ImGui::Separator(); if (ImGui::MenuItem("复制", "Ctrl+D", false, !editorContext.GetSelectedObjectIds().empty())) { (void)MetaCoreDuplicateSelection(editorContext); } @@ -321,6 +943,7 @@ public: bool IsOpenByDefault() const override { return true; } void DrawPanel(MetaCoreEditorContext& editorContext) override { + const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService(); if (const MetaCoreId requestedRenameId = editorContext.ConsumeRenameRequestObjectId(); requestedRenameId != 0) { BeginRename(editorContext, requestedRenameId); } @@ -328,7 +951,15 @@ public: ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 8)); ImGui::TextColored(ImVec4(0.4F, 0.6F, 1.0F, 1.0F), " (S) "); ImGui::SameLine(); - ImGui::TextUnformatted("SampleScene"); + const std::string sceneLabel = + scenePersistenceService != nullptr && scenePersistenceService->HasOpenScene() + ? scenePersistenceService->GetCurrentSceneDisplayName() + : "Untitled"; + ImGui::TextUnformatted(sceneLabel.c_str()); + if (scenePersistenceService != nullptr && scenePersistenceService->IsSceneDirty()) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.90F, 0.74F, 0.22F, 1.0F), "*"); + } ImGui::PopStyleVar(); ImGui::Separator(); @@ -551,34 +1182,99 @@ public: std::string GetPanelTitle() const override { return "项目"; } bool IsOpenByDefault() const override { return true; } - void DrawPanel(MetaCoreEditorContext&) override { + void DrawPanel(MetaCoreEditorContext& editorContext) override { + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || !assetDatabaseService->HasProject()) { + ImGui::TextUnformatted("当前没有可用项目。"); + return; + } + + const MetaCoreProjectDescriptor& projectDescriptor = assetDatabaseService->GetProjectDescriptor(); + if (SelectedDirectory_.empty()) { + SelectedDirectory_ = std::filesystem::path("Assets"); + } + + ImGui::TextDisabled("%s", projectDescriptor.Name.c_str()); + ImGui::SameLine(); + if (ImGui::Button("刷新##ProjectAssets")) { + MetaCoreHandleReloadAssets(editorContext); + } + ImGui::Columns(2, "ProjectColumns", true); if (ImGui::IsWindowAppearing()) { ImGui::SetColumnWidth(0, 150.0F); } - if (ImGui::TreeNodeEx("Assets", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Selectable("Scenes"); - ImGui::Selectable("Materials"); - ImGui::TreePop(); - } - - if (ImGui::TreeNodeEx("Packages", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::TextDisabled("Built-in"); - ImGui::TreePop(); - } + DrawDirectoryTree(*assetDatabaseService, std::filesystem::path("Assets"), "Assets"); + DrawDirectoryTree(*assetDatabaseService, std::filesystem::path("Scenes"), "Scenes"); ImGui::NextColumn(); - ImGui::TextDisabled("Assets > Scenes"); + ImGui::TextDisabled("%s", SelectedDirectory_.generic_string().c_str()); ImGui::Separator(); - // Placeholder for icons - ImGui::Button("Scene01", ImVec2(64, 64)); - ImGui::SameLine(); - ImGui::Button("MaterialX", ImVec2(64, 64)); + + for (const auto& directory : assetDatabaseService->GetDirectoriesUnder(SelectedDirectory_)) { + const std::string directoryLabel = "[Dir] " + directory.filename().string(); + if (ImGui::Selectable(directoryLabel.c_str(), SelectedDirectory_ == directory, ImGuiSelectableFlags_AllowDoubleClick)) { + SelectedDirectory_ = directory; + } + } + + for (const MetaCoreAssetRecord& record : assetDatabaseService->GetAssetsUnder(SelectedDirectory_)) { + std::string assetLabel = + record.RelativePath.filename().string() + " [" + record.Type + " | " + MetaCoreAssetStorageLabel(record.StorageKind) + "]"; + const bool isCurrentScene = + scenePersistenceService != nullptr && + scenePersistenceService->GetCurrentScenePath() == record.RelativePath; + + if (isCurrentScene) { + assetLabel += scenePersistenceService->IsSceneDirty() ? " *" : " (Open)"; + } + + if (ImGui::Selectable(assetLabel.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && + record.StorageKind == MetaCoreAssetStorageKind::SourcePackage) { + if (record.Type == "scene" && scenePersistenceService != nullptr) { + (void)scenePersistenceService->LoadScene(editorContext, record.RelativePath); + } else if (record.Type == "prefab") { + (void)MetaCoreInstantiatePrefab(editorContext, record.Guid, std::nullopt); + } + } + } + } ImGui::Columns(1); } + +private: + void DrawDirectoryTree( + MetaCoreIAssetDatabaseService& assetDatabaseService, + const std::filesystem::path& relativeDirectory, + const char* fallbackLabel + ) { + const std::string label = relativeDirectory.empty() ? fallbackLabel : relativeDirectory.filename().string(); + const ImGuiTreeNodeFlags flags = + ImGuiTreeNodeFlags_OpenOnArrow | + ImGuiTreeNodeFlags_SpanFullWidth | + (SelectedDirectory_ == relativeDirectory ? ImGuiTreeNodeFlags_Selected : 0); + + const bool opened = ImGui::TreeNodeEx(relativeDirectory.generic_string().c_str(), flags, "%s", label.c_str()); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + SelectedDirectory_ = relativeDirectory; + } + + if (opened) { + for (const auto& childDirectory : assetDatabaseService.GetDirectoriesUnder(relativeDirectory)) { + if (childDirectory.parent_path() == relativeDirectory) { + DrawDirectoryTree(assetDatabaseService, childDirectory, childDirectory.filename().string().c_str()); + } + } + ImGui::TreePop(); + } + } + + std::filesystem::path SelectedDirectory_{}; }; class MetaCoreConsolePanelProvider final : public MetaCoreIEditorPanelProvider { @@ -603,20 +1299,187 @@ public: } }; +class MetaCoreRuntimeDataPanelProvider final : public MetaCoreIEditorPanelProvider { +public: + std::string GetPanelId() const override { return "RuntimeData"; } + std::string GetPanelTitle() const override { return "运行时数据"; } + bool IsOpenByDefault() const override { return true; } + + void DrawPanel(MetaCoreEditorContext& editorContext) override { + if (!editorContext.EnsureRuntimeDataConfigLoaded()) { + ImGui::TextUnformatted("Runtime 配置尚未就绪。"); + return; + } + + const auto& sourcesDocument = editorContext.GetRuntimeDataSourcesDocument(); + const auto& bindingsDocument = editorContext.GetRuntimeBindingsDocument(); + const auto validationIssues = MetaCoreValidateRuntimeDataDocuments(sourcesDocument, bindingsDocument); + const auto errorCount = static_cast(std::count_if( + validationIssues.begin(), + validationIssues.end(), + [](const MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCoreRuntimeConfigIssueSeverity::Error; + } + )); + const int warningCount = static_cast(validationIssues.size()) - errorCount; + + ImGui::Text("Sources: %d", static_cast(sourcesDocument.Sources.size())); + ImGui::Text("DataPoints: %d", static_cast(sourcesDocument.DataPoints.size())); + ImGui::Text("Bindings: %d", static_cast(bindingsDocument.Bindings.size())); + ImGui::TextColored(errorCount > 0 ? ImVec4(0.92F, 0.35F, 0.32F, 1.0F) : ImVec4(0.70F, 0.80F, 0.70F, 1.0F), + "Errors: %d", errorCount); + ImGui::SameLine(); + ImGui::TextColored(warningCount > 0 ? ImVec4(0.95F, 0.78F, 0.25F, 1.0F) : ImVec4(0.70F, 0.80F, 0.70F, 1.0F), + "Warnings: %d", warningCount); + + MetaCoreRuntimeProjectDocument runtimeProjectDocument; + runtimeProjectDocument.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; + runtimeProjectDocument.DataSourcesPath = std::filesystem::path("Runtime") / "DataSources.mcruntime"; + runtimeProjectDocument.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; + runtimeProjectDocument.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; + std::optional runtimeDiagnostics; + if (const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + assetDatabaseService != nullptr && assetDatabaseService->HasProject()) { + const MetaCoreTypeRegistry runtimeRegistry = MetaCoreBuildRuntimePanelTypeRegistry(); + const std::filesystem::path runtimeDirectory = assetDatabaseService->GetProjectDescriptor().RootPath / "Runtime"; + if (const auto loadedProjectDocument = MetaCoreReadRuntimeProjectDocument(runtimeDirectory / "ProjectRuntime.mcruntimecfg", runtimeRegistry); + loadedProjectDocument.has_value()) { + runtimeProjectDocument = *loadedProjectDocument; + } + runtimeDiagnostics = MetaCoreReadRuntimeDiagnosticsSnapshot( + assetDatabaseService->GetProjectDescriptor().RootPath / runtimeProjectDocument.DiagnosticsPath, + runtimeRegistry + ); + } + + ImGui::Separator(); + if (ImGui::CollapsingHeader("Project Runtime", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("Startup Scene: %s", runtimeProjectDocument.StartupScenePath.string().c_str()); + ImGui::Text("Sources: %s", runtimeProjectDocument.DataSourcesPath.string().c_str()); + ImGui::Text("Bindings: %s", runtimeProjectDocument.BindingsPath.string().c_str()); + ImGui::Text("Diagnostics: %s", runtimeProjectDocument.DiagnosticsPath.string().c_str()); + } + + if (ImGui::CollapsingHeader("Sources", ImGuiTreeNodeFlags_DefaultOpen)) { + for (const auto& source : sourcesDocument.Sources) { + ImGui::BulletText("%s (%s)", source.Id.c_str(), source.AdapterType.c_str()); + } + if (sourcesDocument.Sources.empty()) { + ImGui::TextDisabled("当前没有 Source。"); + } + } + + if (ImGui::CollapsingHeader("Last Runtime Diagnostics", ImGuiTreeNodeFlags_DefaultOpen)) { + if (!runtimeDiagnostics.has_value()) { + ImGui::TextDisabled("尚未找到 Player 写出的诊断快照。"); + } else { + ImGui::Text("Source Statuses: %d", static_cast(runtimeDiagnostics->SourceStatuses.size())); + ImGui::Text("Binding Statuses: %d", static_cast(runtimeDiagnostics->BindingStatuses.size())); + ImGui::Text("Has Faults: %s", runtimeDiagnostics->HasFaults ? "true" : "false"); + for (const auto& sourceStatus : runtimeDiagnostics->SourceStatuses) { + ImGui::BulletText( + "Source %s state=%d error=%s", + sourceStatus.SourceId.c_str(), + static_cast(sourceStatus.State), + sourceStatus.LastError.c_str() + ); + } + for (const auto& bindingStatus : runtimeDiagnostics->BindingStatuses) { + if (bindingStatus.Healthy && !bindingStatus.Stale) { + continue; + } + ImGui::TextColored( + ImVec4(0.95F, 0.78F, 0.25F, 1.0F), + "Binding %s healthy=%s stale=%s error=%s", + bindingStatus.BindingId.c_str(), + bindingStatus.Healthy ? "true" : "false", + bindingStatus.Stale ? "true" : "false", + bindingStatus.LastError.c_str() + ); + } + } + } + + if (ImGui::CollapsingHeader("Validation", ImGuiTreeNodeFlags_DefaultOpen)) { + for (const auto& issue : validationIssues) { + ImGui::TextColored( + issue.Severity == MetaCoreRuntimeConfigIssueSeverity::Error + ? ImVec4(0.92F, 0.35F, 0.32F, 1.0F) + : ImVec4(0.95F, 0.78F, 0.25F, 1.0F), + "[%s] %s: %s", + issue.Severity == MetaCoreRuntimeConfigIssueSeverity::Error ? "Error" : "Warning", + issue.Scope.c_str(), + issue.Message.c_str() + ); + } + if (validationIssues.empty()) { + ImGui::TextColored(ImVec4(0.70F, 0.80F, 0.70F, 1.0F), "No validation issues."); + } + } + } +}; + class MetaCoreTransformInspectorDrawer final : public MetaCoreIInspectorDrawer { public: std::string GetDrawerId() const override { return "Transform"; } bool Supports(const MetaCoreGameObject&) const override { return true; } void DrawInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& gameObject) override { + const MetaCoreGameObject* prefabObject = nullptr; + bool hasTransformOverride = false; + if (const auto prefabDocument = MetaCoreLoadPrefabDocumentForInstance(editorContext, gameObject); prefabDocument.has_value() && + gameObject.PrefabInstance.has_value()) { + prefabObject = MetaCoreFindPrefabSourceObject(*prefabDocument, gameObject.PrefabInstance->PrefabObjectId); + if (prefabObject != nullptr) { + hasTransformOverride = MetaCoreHasTransformOverride(gameObject, *prefabObject); + } + } + ImGui::SetNextItemOpen(true, ImGuiCond_Once); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 3)); if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::PopStyleVar(); + if (hasTransformOverride) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.93F, 0.72F, 0.18F, 1.0F), "Overrides"); + ImGui::SameLine(); + if (ImGui::SmallButton("Revert##TransformComponent")) { + (void)MetaCoreRevertSelectedObjectComponentToPrefab(editorContext, "Transform"); + } + } ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 2)); - DrawVec3Control("位置", gameObject.Transform.Position, 0.0F, 0.05F, editorContext); - DrawVec3Control("旋转", gameObject.Transform.RotationEulerDegrees, 0.0F, 0.5F, editorContext); - DrawVec3Control("缩放", gameObject.Transform.Scale, 1.0F, 0.05F, editorContext, true); + const std::size_t selectedCount = editorContext.GetSelectedObjectIds().size(); + if (selectedCount > 1) { + ImGui::TextDisabled("Multi-edit %zu objects", selectedCount); + } + DrawVec3Control( + "位置", + gameObject.Transform.Position, + prefabObject != nullptr ? std::optional(prefabObject->Transform.Position) : std::nullopt, + "position", + 0.0F, + 0.05F, + editorContext + ); + DrawVec3Control( + "旋转", + gameObject.Transform.RotationEulerDegrees, + prefabObject != nullptr ? std::optional(prefabObject->Transform.RotationEulerDegrees) : std::nullopt, + "rotation", + 0.0F, + 0.5F, + editorContext + ); + DrawVec3Control( + "缩放", + gameObject.Transform.Scale, + prefabObject != nullptr ? std::optional(prefabObject->Transform.Scale) : std::nullopt, + "scale", + 1.0F, + 0.05F, + editorContext, + true + ); ImGui::PopStyleVar(); } else { ImGui::PopStyleVar(); @@ -626,9 +1489,41 @@ public: private: static bool GScaleLocked; - void DrawVec3Control(const std::string& label, glm::vec3& values, float resetValue, float speed, MetaCoreEditorContext& editorContext, bool isScale = false) { + void DrawVec3Control( + const std::string& label, + glm::vec3& values, + const std::optional& prefabValue, + std::string_view prefabFieldId, + float resetValue, + float speed, + MetaCoreEditorContext& editorContext, + bool isScale = false + ) { ImGui::PushID(label.c_str()); + glm::vec3 displayValues = values; + const bool multiEdit = editorContext.GetSelectedObjectIds().size() > 1; + auto getTargetValue = [&](MetaCoreGameObject& selectedObject) -> glm::vec3* { + if (label == "位置") { + return &selectedObject.Transform.Position; + } + if (label == "旋转") { + return &selectedObject.Transform.RotationEulerDegrees; + } + return &selectedObject.Transform.Scale; + }; + + const auto sharedValue = MetaCoreGetSharedSelectedValue( + editorContext, + [&](MetaCoreGameObject& selectedObject) -> std::optional { + return *getTargetValue(selectedObject); + }, + [](const glm::vec3& lhs, const glm::vec3& rhs) { return MetaCoreNearlyEqualVec3(lhs, rhs); } + ); + if (sharedValue.has_value()) { + displayValues = *sharedValue; + } + ImGui::Columns(2); ImGui::SetColumnWidth(0, 80.0F); @@ -643,8 +1538,25 @@ private: } ImGui::Text("%s", label.c_str()); + if (prefabValue.has_value() && !MetaCoreNearlyEqualVec3(values, *prefabValue)) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.93F, 0.72F, 0.18F, 1.0F), "Overrides"); + ImGui::SameLine(); + if (ImGui::SmallButton(("Apply##" + label).c_str())) { + (void)MetaCoreApplySelectedPrefabField(editorContext, "Transform", prefabFieldId); + } + ImGui::SameLine(); + if (ImGui::SmallButton(("Revert##" + label).c_str())) { + (void)MetaCoreRevertSelectedTransformFieldToPrefab(editorContext, prefabFieldId); + values = *prefabValue; + } + } ImGui::NextColumn(); + if (multiEdit && !sharedValue.has_value()) { + ImGui::TextDisabled("Mixed"); + } + const float labelWidth = 14.0F; // Smaller labels const float itemWidth = (ImGui::GetContentRegionAvail().x - labelWidth * 3.0F - 12.0F) / 3.0F; @@ -658,19 +1570,44 @@ private: if (ImGui::DragFloat(("##" + std::string(compLabel)).c_str(), &val, speed)) { if (isScale && GScaleLocked && oldVal != 0.0f) { float ratio = val / oldVal; - if (compLabel[0] == 'X') { values.y *= ratio; values.z *= ratio; } - else if (compLabel[0] == 'Y') { values.x *= ratio; values.z *= ratio; } - else if (compLabel[0] == 'Z') { values.x *= ratio; values.y *= ratio; } + if (compLabel[0] == 'X') { displayValues.y *= ratio; displayValues.z *= ratio; } + else if (compLabel[0] == 'Y') { displayValues.x *= ratio; displayValues.z *= ratio; } + else if (compLabel[0] == 'Z') { displayValues.x *= ratio; displayValues.y *= ratio; } } MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", true); } }; - drawComponent("X", values.x, ImVec4(0.9F, 0.3F, 0.3F, 1.0F)); + drawComponent("X", displayValues.x, ImVec4(0.9F, 0.3F, 0.3F, 1.0F)); ImGui::SameLine(); - drawComponent("Y", values.y, ImVec4(0.3F, 0.9F, 0.3F, 1.0F)); + drawComponent("Y", displayValues.y, ImVec4(0.3F, 0.9F, 0.3F, 1.0F)); ImGui::SameLine(); - drawComponent("Z", values.z, ImVec4(0.3F, 0.4F, 1.0F, 1.0F)); + drawComponent("Z", displayValues.z, ImVec4(0.3F, 0.4F, 1.0F, 1.0F)); + + if (!MetaCoreNearlyEqualVec3(displayValues, values)) { + MetaCoreApplyValueToSelectedObjects( + editorContext, + [&](MetaCoreGameObject& selectedObject) -> glm::vec3* { + return getTargetValue(selectedObject); + }, + displayValues + ); + values = displayValues; + } + + if (ImGui::BeginPopupContextItem("TransformContext")) { + if (ImGui::MenuItem("Reset")) { + MetaCoreApplyValueToSelectedObjects( + editorContext, + [&](MetaCoreGameObject& selectedObject) -> glm::vec3* { + return getTargetValue(selectedObject); + }, + glm::vec3(resetValue) + ); + values = glm::vec3(resetValue); + } + ImGui::EndPopup(); + } ImGui::PopStyleVar(); ImGui::Columns(1); @@ -743,9 +1680,35 @@ public: ImGui::Columns(1); ImGui::PopStyleVar(2); + if (selectedCount > 1) { + ImGui::TextDisabled("%zu objects selected", selectedCount); + } ImGui::Separator(); + if (selectedObject->PrefabInstance.has_value()) { + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.18F, 0.27F, 0.44F, 0.35F)); + ImGui::BeginChild("PrefabStatus", ImVec2(0.0F, 34.0F), true); + ImGui::Text("Prefab Instance"); + ImGui::SameLine(); + if (ImGui::SmallButton("Apply")) { + (void)MetaCoreApplySelectedPrefabInstance(editorContext); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Revert")) { + (void)MetaCoreRevertSelectedPrefabInstance(editorContext); + } + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::Spacing(); + } + // Components + const auto prefabDocument = MetaCoreLoadPrefabDocumentForInstance(editorContext, *selectedObject); + const MetaCoreGameObject* prefabSourceObject = nullptr; + if (prefabDocument.has_value() && selectedObject->PrefabInstance.has_value()) { + prefabSourceObject = MetaCoreFindPrefabSourceObject(*prefabDocument, selectedObject->PrefabInstance->PrefabObjectId); + } + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 4)); for (const auto& drawer : editorContext.GetModuleRegistry().GetInspectorDrawers()) { if (drawer->Supports(*selectedObject)) { @@ -753,45 +1716,444 @@ public: } } - if (selectedObject->Camera.has_value()) { - ImGui::SetNextItemOpen(true, ImGuiCond_Once); - if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text("FOV: %.1f", selectedObject->Camera->FieldOfViewDegrees); - } - } + if (const auto componentRegistry = editorContext.GetModuleRegistry().ResolveService(); + componentRegistry != nullptr) { + for (const auto& descriptor : componentRegistry->GetComponentDescriptors()) { + if (descriptor.TypeId == "Transform") { + continue; + } - if (selectedObject->Light.has_value()) { - ImGui::SetNextItemOpen(true, ImGuiCond_Once); - if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::DragFloat("Intensity", &selectedObject->Light->Intensity, 0.05F, 0.0F, 8.0F); - MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", true); - } - } + bool hasComponentOnAllSelected = true; + for (const MetaCoreId selectedId : editorContext.GetSelectedObjectIds()) { + const MetaCoreGameObject* selectedGameObject = editorContext.GetScene().FindGameObject(selectedId); + if (selectedGameObject == nullptr || !descriptor.HasComponent(*selectedGameObject)) { + hasComponentOnAllSelected = false; + break; + } + } - if (selectedObject->MeshRenderer.has_value()) { - ImGui::SetNextItemOpen(true, ImGuiCond_Once); - if (ImGui::CollapsingHeader("Mesh Renderer", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::ColorEdit3("BaseColor", glm::value_ptr(selectedObject->MeshRenderer->BaseColor)); - MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", true); - ImGui::Checkbox("Visible", &selectedObject->MeshRenderer->Visible); - MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", false); + if (!hasComponentOnAllSelected) { + continue; + } + + ImGui::PushID(descriptor.TypeId.c_str()); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (ImGui::CollapsingHeader(descriptor.DisplayName.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { + bool hasPrefabOverride = false; + if (prefabSourceObject != nullptr) { + if (descriptor.TypeId == "Camera") { + hasPrefabOverride = MetaCoreHasCameraOverride(*selectedObject, *prefabSourceObject); + } else if (descriptor.TypeId == "Light") { + hasPrefabOverride = MetaCoreHasLightOverride(*selectedObject, *prefabSourceObject); + } else if (descriptor.TypeId == "MeshRenderer") { + hasPrefabOverride = MetaCoreHasMeshRendererOverride(*selectedObject, *prefabSourceObject); + } + } + + if (hasPrefabOverride) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.93F, 0.72F, 0.18F, 1.0F), "Overrides"); + ImGui::SameLine(); + if (ImGui::SmallButton("Revert")) { + (void)MetaCoreRevertSelectedObjectComponentToPrefab(editorContext, descriptor.TypeId); + } + } + ImGui::SameLine(); + if (ImGui::SmallButton("Reset")) { + (void)MetaCoreResetSelectedObjectComponent(editorContext, descriptor); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Copy")) { + (void)MetaCoreCopySelectedObjectComponent(editorContext, descriptor); + } + ImGui::SameLine(); + const bool canPasteComponent = componentRegistry->CanPasteComponent(descriptor.TypeId); + if (!canPasteComponent) { + ImGui::BeginDisabled(); + } + if (ImGui::SmallButton("Paste")) { + (void)MetaCorePasteSelectedObjectComponent(editorContext, descriptor); + } + if (!canPasteComponent) { + ImGui::EndDisabled(); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Remove")) { + (void)MetaCoreMutateSelectedObjectWithComponentDescriptor(editorContext, descriptor, false); + ImGui::PopID(); + continue; + } + + if (descriptor.DrawInspector) { + descriptor.DrawInspector(editorContext, *selectedObject); + } + } + ImGui::PopID(); } } ImGui::PopStyleVar(); + DrawRuntimeDataBindings(editorContext, *selectedObject); + ImGui::Spacing(); ImGui::Separator(); - if (ImGui::Button("添加组件", ImVec2(-1, 26))) { /* Add Component */ } + if (ImGui::Button("添加组件", ImVec2(-1, 26))) { + ImGui::OpenPopup("AddComponentPopup"); + } + + if (ImGui::BeginPopup("AddComponentPopup")) { + if (const auto componentRegistry = editorContext.GetModuleRegistry().ResolveService(); + componentRegistry != nullptr) { + for (const auto& descriptor : componentRegistry->GetComponentDescriptors()) { + if (descriptor.TypeId == "Transform" || descriptor.HasComponent(*selectedObject)) { + continue; + } + + if (ImGui::MenuItem(descriptor.DisplayName.c_str())) { + (void)MetaCoreMutateSelectedObjectWithComponentDescriptor(editorContext, descriptor, true); + } + } + } + ImGui::EndPopup(); + } } private: + void DrawRuntimeDataBindings(MetaCoreEditorContext& editorContext, const MetaCoreGameObject& selectedObject) { + ImGui::Spacing(); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (!ImGui::CollapsingHeader("Runtime Data Bindings", ImGuiTreeNodeFlags_DefaultOpen)) { + return; + } + + if (!editorContext.EnsureRuntimeDataConfigLoaded()) { + ImGui::TextDisabled("Runtime 配置尚未就绪。"); + return; + } + + auto& sourcesDocument = editorContext.AccessRuntimeDataSourcesDocument(); + auto& bindingsDocument = editorContext.AccessRuntimeBindingsDocument(); + const auto validationIssues = MetaCoreValidateRuntimeDataDocuments(sourcesDocument, bindingsDocument); + const bool hasValidationErrors = std::any_of( + validationIssues.begin(), + validationIssues.end(), + [](const MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCoreRuntimeConfigIssueSeverity::Error; + } + ); + + if (ImGui::Button("保存 Runtime 配置")) { + (void)editorContext.SaveRuntimeDataConfig(); + } + ImGui::SameLine(); + if (ImGui::Button("新增 Replay Source")) { + const auto sourceExists = std::find_if( + sourcesDocument.Sources.begin(), + sourcesDocument.Sources.end(), + [](const MetaCoreDataSourceDefinition& source) { + return source.Id == "replay-source"; + } + ); + if (sourceExists == sourcesDocument.Sources.end()) { + sourcesDocument.Sources.push_back(MetaCoreDataSourceDefinition{ + "replay-source", + "file_replay", + "Replay Source", + {MetaCoreDataSourceSetting{"file_path", "TestProject/Runtime/RuntimeReplay.mcstream"}}, + true, + 1000 + }); + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "RuntimeData", "已新增默认 Replay Source"); + } + } + ImGui::SameLine(); + if (ImGui::Button("新增 TCP Source")) { + const auto sourceExists = std::find_if( + sourcesDocument.Sources.begin(), + sourcesDocument.Sources.end(), + [](const MetaCoreDataSourceDefinition& source) { + return source.Id == "tcp-source"; + } + ); + if (sourceExists == sourcesDocument.Sources.end()) { + sourcesDocument.Sources.push_back(MetaCoreDataSourceDefinition{ + "tcp-source", + "tcp", + "Tcp Source", + { + MetaCoreDataSourceSetting{"host", "127.0.0.1"}, + MetaCoreDataSourceSetting{"port", "7001"} + }, + true, + 1000 + }); + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "RuntimeData", "已新增默认 TCP Source"); + } + } + ImGui::SameLine(); + if (ImGui::Button("新增 DataPoint")) { + const std::string objectName = selectedObject.Name.empty() ? std::to_string(selectedObject.Id) : selectedObject.Name; + std::string sanitizedName = objectName; + std::replace_if(sanitizedName.begin(), sanitizedName.end(), [](char ch) { + return ch == ' ' || ch == '/'; + }, '_'); + const std::string pointId = sanitizedName + ".position"; + const auto pointExists = std::find_if( + sourcesDocument.DataPoints.begin(), + sourcesDocument.DataPoints.end(), + [&](const MetaCoreDataPointDefinition& point) { + return point.Id == pointId; + } + ); + if (pointExists == sourcesDocument.DataPoints.end()) { + const std::string sourceId = !sourcesDocument.Sources.empty() ? sourcesDocument.Sources.front().Id : "replay-source"; + sourcesDocument.DataPoints.push_back(MetaCoreDataPointDefinition{ + pointId, + sourceId, + pointId, + MetaCoreRuntimeValueType::Vec3 + }); + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "RuntimeData", "已新增默认 DataPoint: " + pointId); + } + } + ImGui::SameLine(); + if (ImGui::Button("新增绑定")) { + MetaCoreSceneBindingDefinition newBinding; + newBinding.BindingId = "binding.object." + std::to_string(selectedObject.Id) + "." + std::to_string(bindingsDocument.Bindings.size() + 1); + newBinding.TargetObjectId = selectedObject.Id; + newBinding.Target = MetaCoreRuntimeBindingTarget::TransformPosition; + newBinding.MissingDataPolicy = MetaCoreRuntimeMissingDataPolicy::KeepLastValue; + if (!sourcesDocument.DataPoints.empty()) { + newBinding.DataPointId = sourcesDocument.DataPoints.front().Id; + } + bindingsDocument.Bindings.push_back(std::move(newBinding)); + } + + ImGui::TextDisabled("Sources: %d", static_cast(sourcesDocument.Sources.size())); + if (sourcesDocument.DataPoints.empty()) { + ImGui::TextDisabled("当前没有可用 DataPoint,请先生成或导入 Runtime 配置。"); + } else { + ImGui::TextDisabled("DataPoints: %d", static_cast(sourcesDocument.DataPoints.size())); + } + + for (std::size_t sourceIndex = 0; sourceIndex < sourcesDocument.Sources.size(); ++sourceIndex) { + auto& source = sourcesDocument.Sources[sourceIndex]; + ImGui::PushID(static_cast(1000 + sourceIndex)); + ImGui::Separator(); + ImGui::Text("Source %s", source.Id.c_str()); + std::array displayNameBuffer{}; + std::snprintf(displayNameBuffer.data(), displayNameBuffer.size(), "%s", source.DisplayName.c_str()); + if (ImGui::InputText("Display Name", displayNameBuffer.data(), displayNameBuffer.size())) { + source.DisplayName = displayNameBuffer.data(); + } + + const auto adapterTypes = MetaCoreRuntimeAdapterTypes(); + if (ImGui::BeginCombo("Adapter Type", source.AdapterType.c_str())) { + for (const char* adapterType : adapterTypes) { + const bool isSelected = source.AdapterType == adapterType; + if (ImGui::Selectable(adapterType, isSelected)) { + source.AdapterType = adapterType; + } + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + auto ensureSetting = [&](std::string_view key, const std::string& defaultValue) -> MetaCoreDataSourceSetting& { + auto iterator = std::find_if( + source.ConnectionSettings.begin(), + source.ConnectionSettings.end(), + [&](const MetaCoreDataSourceSetting& setting) { + return setting.Key == key; + } + ); + if (iterator == source.ConnectionSettings.end()) { + source.ConnectionSettings.push_back(MetaCoreDataSourceSetting{std::string(key), defaultValue}); + return source.ConnectionSettings.back(); + } + return *iterator; + }; + + if (source.AdapterType == "file_replay") { + auto& filePathSetting = ensureSetting("file_path", ""); + std::array filePathBuffer{}; + std::snprintf(filePathBuffer.data(), filePathBuffer.size(), "%s", filePathSetting.Value.c_str()); + if (ImGui::InputText("Replay File", filePathBuffer.data(), filePathBuffer.size())) { + filePathSetting.Value = filePathBuffer.data(); + } + } else if (source.AdapterType == "tcp") { + auto& hostSetting = ensureSetting("host", "127.0.0.1"); + auto& portSetting = ensureSetting("port", "7001"); + + std::array hostBuffer{}; + std::snprintf(hostBuffer.data(), hostBuffer.size(), "%s", hostSetting.Value.c_str()); + if (ImGui::InputText("Host", hostBuffer.data(), hostBuffer.size())) { + hostSetting.Value = hostBuffer.data(); + } + + std::array portBuffer{}; + std::snprintf(portBuffer.data(), portBuffer.size(), "%s", portSetting.Value.c_str()); + if (ImGui::InputText("Port", portBuffer.data(), portBuffer.size())) { + portSetting.Value = portBuffer.data(); + } + } + ImGui::PopID(); + } + + for (std::size_t pointIndex = 0; pointIndex < sourcesDocument.DataPoints.size(); ++pointIndex) { + auto& dataPoint = sourcesDocument.DataPoints[pointIndex]; + std::string objectName = selectedObject.Name.empty() ? std::to_string(selectedObject.Id) : selectedObject.Name; + std::replace_if(objectName.begin(), objectName.end(), [](char ch) { + return ch == ' ' || ch == '/'; + }, '_'); + const bool pointLikelyBelongsToObject = + dataPoint.Id.find(objectName) != std::string::npos || dataPoint.Id.find(std::to_string(selectedObject.Id)) != std::string::npos; + if (!pointLikelyBelongsToObject) { + continue; + } + + ImGui::PushID(static_cast(2000 + pointIndex)); + ImGui::Separator(); + ImGui::Text("DataPoint %s", dataPoint.Id.c_str()); + + std::array pointIdBuffer{}; + std::snprintf(pointIdBuffer.data(), pointIdBuffer.size(), "%s", dataPoint.Id.c_str()); + if (ImGui::InputText("Point Id", pointIdBuffer.data(), pointIdBuffer.size())) { + dataPoint.Id = pointIdBuffer.data(); + } + + std::array addressBuffer{}; + std::snprintf(addressBuffer.data(), addressBuffer.size(), "%s", dataPoint.ExternalAddress.c_str()); + if (ImGui::InputText("External Address", addressBuffer.data(), addressBuffer.size())) { + dataPoint.ExternalAddress = addressBuffer.data(); + } + + const char* currentSourceLabel = dataPoint.SourceId.empty() ? "" : dataPoint.SourceId.c_str(); + if (ImGui::BeginCombo("Source", currentSourceLabel)) { + for (const auto& source : sourcesDocument.Sources) { + const bool isSelected = source.Id == dataPoint.SourceId; + if (ImGui::Selectable(source.Id.c_str(), isSelected)) { + dataPoint.SourceId = source.Id; + } + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + const auto valueTypes = MetaCoreRuntimeValueTypes(); + if (ImGui::BeginCombo("Value Type", MetaCoreRuntimeValueTypeLabel(dataPoint.ValueType))) { + for (const auto valueType : valueTypes) { + const bool isSelected = valueType == dataPoint.ValueType; + if (ImGui::Selectable(MetaCoreRuntimeValueTypeLabel(valueType), isSelected)) { + dataPoint.ValueType = valueType; + } + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + ImGui::PopID(); + } + + if (!validationIssues.empty()) { + ImGui::Spacing(); + ImGui::TextColored( + hasValidationErrors ? ImVec4(0.92F, 0.35F, 0.32F, 1.0F) : ImVec4(0.95F, 0.78F, 0.25F, 1.0F), + "Validation Issues: %d", + static_cast(validationIssues.size()) + ); + for (const auto& issue : validationIssues) { + ImGui::BulletText( + "[%s] %s: %s", + issue.Severity == MetaCoreRuntimeConfigIssueSeverity::Error ? "Error" : "Warning", + issue.Scope.c_str(), + issue.Message.c_str() + ); + } + } + + bool anyBindingFound = false; + for (std::size_t bindingIndex = 0; bindingIndex < bindingsDocument.Bindings.size();) { + auto& binding = bindingsDocument.Bindings[bindingIndex]; + if (binding.TargetObjectId != selectedObject.Id) { + ++bindingIndex; + continue; + } + + anyBindingFound = true; + ImGui::PushID(static_cast(bindingIndex)); + ImGui::Separator(); + ImGui::Text("Binding %s", binding.BindingId.c_str()); + + int selectedPointIndex = 0; + for (std::size_t pointIndex = 0; pointIndex < sourcesDocument.DataPoints.size(); ++pointIndex) { + if (sourcesDocument.DataPoints[pointIndex].Id == binding.DataPointId) { + selectedPointIndex = static_cast(pointIndex); + break; + } + } + std::string currentPointLabel = + sourcesDocument.DataPoints.empty() ? std::string("") : sourcesDocument.DataPoints[selectedPointIndex].Id; + if (ImGui::BeginCombo("Data Point", currentPointLabel.c_str())) { + for (std::size_t pointIndex = 0; pointIndex < sourcesDocument.DataPoints.size(); ++pointIndex) { + const bool isSelected = static_cast(pointIndex) == selectedPointIndex; + if (ImGui::Selectable(sourcesDocument.DataPoints[pointIndex].Id.c_str(), isSelected)) { + binding.DataPointId = sourcesDocument.DataPoints[pointIndex].Id; + } + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + auto bindingTargets = MetaCoreRuntimeBindingTargets(); + int selectedTargetIndex = 0; + for (std::size_t targetIndex = 0; targetIndex < bindingTargets.size(); ++targetIndex) { + if (bindingTargets[targetIndex] == binding.Target) { + selectedTargetIndex = static_cast(targetIndex); + break; + } + } + if (ImGui::BeginCombo("Target", MetaCoreRuntimeBindingTargetLabel(binding.Target))) { + for (std::size_t targetIndex = 0; targetIndex < bindingTargets.size(); ++targetIndex) { + const bool isSelected = static_cast(targetIndex) == selectedTargetIndex; + if (ImGui::Selectable(MetaCoreRuntimeBindingTargetLabel(bindingTargets[targetIndex]), isSelected)) { + binding.Target = bindingTargets[targetIndex]; + } + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + if (ImGui::Button("删除绑定")) { + bindingsDocument.Bindings.erase(bindingsDocument.Bindings.begin() + static_cast(bindingIndex)); + ImGui::PopID(); + continue; + } + ImGui::PopID(); + ++bindingIndex; + } + + if (!anyBindingFound) { + ImGui::TextDisabled("当前对象还没有 Runtime 绑定。"); + } + } + MetaCoreId BoundObjectId_ = 0; std::array NameBuffer_{}; }; -class MetaCoreBuiltinEditorModule final : public MetaCoreIModule { +class MetaCoreBuiltinEditorViewsModule final : public MetaCoreIModule { public: - std::string GetModuleName() const override { return "MetaCoreBuiltinEditorModule"; } + std::string GetModuleName() const override { return "MetaCoreBuiltinEditorViewsModule"; } void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { moduleRegistry.RegisterMenuProvider(std::make_unique()); @@ -799,6 +2161,7 @@ public: moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); + moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterInspectorDrawer(std::make_unique()); } @@ -809,8 +2172,12 @@ public: } // namespace +std::unique_ptr MetaCoreCreateBuiltinEditorViewsModule() { + return std::make_unique(); +} + std::unique_ptr MetaCoreCreateBuiltinEditorModule() { - return std::make_unique(); + return MetaCoreCreateBuiltinEditorViewsModule(); } } // namespace MetaCore diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.h b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.h index 8375406..6768186 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.h +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.h @@ -1,12 +1,3 @@ #pragma once -#include "MetaCoreEditor/MetaCoreEditorModule.h" - -#include - -namespace MetaCore { - -// 创建内置 Unity-like 编辑器模块。 -std::unique_ptr MetaCoreCreateBuiltinEditorModule(); - -} // namespace MetaCore +#include "MetaCoreEditor/MetaCoreBuiltinModules.h" diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index 237efb3..7c307c2 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -1,6 +1,7 @@ #include "MetaCoreEditor/MetaCoreEditorApp.h" -#include "MetaCoreBuiltinEditorModule.h" +#include "MetaCoreEditor/MetaCoreBuiltinModules.h" +#include "MetaCoreEditor/MetaCoreEditorServices.h" #include "MetaCoreEditorCameraController.h" #include @@ -188,10 +189,11 @@ bool MetaCoreEditorApp::Initialize() { return false; } - MetaCoreTraceStartup("init: create default scene"); + MetaCoreTraceStartup("init: create bootstrap scene"); Scene_ = MetaCoreCreateDefaultScene(); MetaCoreTraceStartup("init: create modules"); - Modules_.push_back(MetaCoreCreateBuiltinEditorModule()); + Modules_.push_back(MetaCoreCreateBuiltinCoreServicesModule()); + Modules_.push_back(MetaCoreCreateBuiltinEditorViewsModule()); MetaCoreTraceStartup("init: startup modules"); for (const auto& module : Modules_) { module->Startup(ModuleRegistry_); @@ -207,18 +209,48 @@ bool MetaCoreEditorApp::Initialize() { ModuleRegistry_ ); - MetaCoreTraceStartup("init: select default cube"); - for (const MetaCoreGameObject& object : Scene_.GetGameObjects()) { - if (object.Name == "Cube") { - EditorContext_->SetSelectedObjectId(object.Id); - EditorContext_->GetCameraController().FocusGameObject(object); - break; + MetaCoreTraceStartup("init: load startup scene"); + bool loadedStartupScene = false; + if (const auto scenePersistenceService = ModuleRegistry_.ResolveService(); + scenePersistenceService != nullptr) { + loadedStartupScene = scenePersistenceService->LoadStartupScene(*EditorContext_); + + if (!loadedStartupScene) { + if (const auto assetDatabaseService = ModuleRegistry_.ResolveService(); + assetDatabaseService != nullptr && + assetDatabaseService->HasProject() && + assetDatabaseService->GetProjectDescriptor().ScenePaths.empty()) { + const std::filesystem::path bootstrapScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; + if (scenePersistenceService->SaveSceneAs(*EditorContext_, bootstrapScenePath)) { + (void)assetDatabaseService->SetStartupScenePath(bootstrapScenePath); + loadedStartupScene = true; + EditorContext_->AddConsoleMessage( + MetaCoreLogLevel::Info, + "Project", + "已创建默认启动场景: " + bootstrapScenePath.generic_string() + ); + } + } + } + } + + MetaCoreTraceStartup("init: select default focus object"); + if (EditorContext_->GetSelectedObjectId() == 0) { + for (const MetaCoreGameObject& object : Scene_.GetGameObjects()) { + if (object.Name == "Cube" || object.Name == "Demo Cube") { + EditorContext_->SetSelectedObjectId(object.Id); + EditorContext_->GetCameraController().FocusGameObject(object); + break; + } } } MetaCoreTraceStartup("init: add console messages"); EditorContext_->AddConsoleMessage(MetaCoreLogLevel::Info, "System", "MetaCore 编辑器已初始化"); EditorContext_->AddConsoleMessage(MetaCoreLogLevel::Info, "Render", "Panda3D 场景视口已准备完成"); + if (loadedStartupScene) { + EditorContext_->AddConsoleMessage(MetaCoreLogLevel::Info, "Project", "已加载启动场景"); + } Initialized_ = true; MetaCoreTraceStartup("init: done"); @@ -256,6 +288,7 @@ void MetaCoreEditorApp::Shutdown() { for (const auto& module : Modules_) { module->Shutdown(ModuleRegistry_); } + ModuleRegistry_.ShutdownServices(); Modules_.clear(); EditorContext_.reset(); ViewportRenderer_.Shutdown(); diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp index b7f14c8..83c2adb 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp @@ -1,12 +1,17 @@ #include "MetaCoreEditor/MetaCoreEditorContext.h" +#include "MetaCoreEditor/MetaCoreEditorModule.h" +#include "MetaCoreEditor/MetaCoreEditorServices.h" #include "MetaCoreEditorCameraController.h" +#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" #include "MetaCorePlatform/MetaCoreWindow.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" #include #include +#include #include namespace MetaCore { @@ -133,6 +138,33 @@ private: lhs.SelectionSnapshot.SelectionAnchorId == rhs.SelectionSnapshot.SelectionAnchorId; } +[[nodiscard]] std::optional> MetaCoreTrySerializeStateSnapshot( + const MetaCoreEditorModuleRegistry& moduleRegistry, + const MetaCoreEditorStateSnapshot& snapshot +) { + const auto reflectionRegistry = moduleRegistry.ResolveService(); + if (reflectionRegistry == nullptr) { + return std::nullopt; + } + return MetaCoreSerializeToBytes(snapshot, reflectionRegistry->GetTypeRegistry()); +} + +[[nodiscard]] MetaCoreTypeRegistry MetaCoreBuildRuntimeDataTypeRegistry() { + MetaCoreTypeRegistry registry; + MetaCoreRegisterFoundationGeneratedTypes(registry); + MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + return registry; +} + +[[nodiscard]] MetaCoreRuntimeProjectDocument MetaCoreBuildDefaultRuntimeProjectDocument() { + MetaCoreRuntimeProjectDocument document; + document.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; + document.DataSourcesPath = std::filesystem::path("Runtime") / "DataSources.mcruntime"; + document.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; + document.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; + return document; +} + } // namespace MetaCoreEditorContext::MetaCoreEditorContext( @@ -181,6 +213,7 @@ void MetaCoreEditorContext::ClearSelection() { SelectedObjectIds_.clear(); ActiveObjectId_ = 0; SelectionAnchorId_ = 0; + NotifySelectionChanged(); } void MetaCoreEditorContext::SelectOnly(MetaCoreId objectId) { @@ -205,6 +238,7 @@ void MetaCoreEditorContext::ToggleSelection(MetaCoreId objectId) { SelectionAnchorId_ = ActiveObjectId_ != 0 ? ActiveObjectId_ : SelectionAnchorId_; NormalizeSelection(); + NotifySelectionChanged(); } void MetaCoreEditorContext::AddToSelection(MetaCoreId objectId, bool makeActive) { @@ -220,6 +254,7 @@ void MetaCoreEditorContext::AddToSelection(MetaCoreId objectId, bool makeActive) SelectionAnchorId_ = objectId; } NormalizeSelection(); + NotifySelectionChanged(); } void MetaCoreEditorContext::SetSelection(const std::vector& objectIds, MetaCoreId activeObjectId) { @@ -243,6 +278,7 @@ void MetaCoreEditorContext::SetSelection(const std::vector& objectId SelectionAnchorId_ = ActiveObjectId_; NormalizeSelection(); + NotifySelectionChanged(); } MetaCoreId MetaCoreEditorContext::GetSelectionAnchorId() const { return SelectionAnchorId_; } @@ -312,6 +348,7 @@ bool MetaCoreEditorContext::ExecuteCommand(std::unique_ptr(); + if (assetDatabaseService == nullptr || !assetDatabaseService->HasProject()) { + AddConsoleMessage(MetaCoreLogLevel::Warning, "RuntimeData", "项目尚未就绪,无法加载 Runtime 配置"); + return false; + } + + const std::filesystem::path runtimeDirectory = ResolveRuntimeDirectory(); + std::error_code errorCode; + std::filesystem::create_directories(runtimeDirectory, errorCode); + + const MetaCoreTypeRegistry runtimeRegistry = MetaCoreBuildRuntimeDataTypeRegistry(); + const auto loadedProjectRuntime = MetaCoreReadRuntimeProjectDocument( + runtimeDirectory / "ProjectRuntime.mcruntimecfg", + runtimeRegistry + ); + const auto loadedSources = MetaCoreReadRuntimeDataSourcesDocument( + runtimeDirectory / "DataSources.mcruntime", + runtimeRegistry + ); + const auto loadedBindings = MetaCoreReadRuntimeBindingsDocument( + runtimeDirectory / "Bindings.mcruntime", + runtimeRegistry + ); + + RuntimeDataSourcesDocument_ = loadedSources.value_or(MetaCoreRuntimeDataSourcesDocument{}); + RuntimeBindingsDocument_ = loadedBindings.value_or(MetaCoreRuntimeBindingsDocument{}); + RuntimeDataConfigLoaded_ = true; + + if (!loadedSources.has_value() || !loadedBindings.has_value()) { + AddConsoleMessage(MetaCoreLogLevel::Info, "RuntimeData", "未找到完整 Runtime 配置,已使用空配置初始化"); + } + if (!loadedProjectRuntime.has_value()) { + AddConsoleMessage(MetaCoreLogLevel::Info, "RuntimeData", "未找到 ProjectRuntime 配置,将按默认项目运行路径保存"); + } + return true; +} + +bool MetaCoreEditorContext::SaveRuntimeDataConfig() { + if (!EnsureRuntimeDataConfigLoaded()) { + return false; + } + + const auto assetDatabaseService = ModuleRegistry_.ResolveService(); + if (assetDatabaseService == nullptr || !assetDatabaseService->HasProject()) { + AddConsoleMessage(MetaCoreLogLevel::Error, "RuntimeData", "项目尚未就绪,无法保存 Runtime 配置"); + return false; + } + + const std::filesystem::path runtimeDirectory = ResolveRuntimeDirectory(); + std::error_code errorCode; + std::filesystem::create_directories(runtimeDirectory, errorCode); + + const auto validationIssues = MetaCoreValidateRuntimeDataDocuments(RuntimeDataSourcesDocument_, RuntimeBindingsDocument_); + const bool hasValidationErrors = std::any_of(validationIssues.begin(), validationIssues.end(), [](const MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCoreRuntimeConfigIssueSeverity::Error; + }); + for (const auto& issue : validationIssues) { + AddConsoleMessage( + issue.Severity == MetaCoreRuntimeConfigIssueSeverity::Error ? MetaCoreLogLevel::Error : MetaCoreLogLevel::Warning, + "RuntimeData", + issue.Scope + ": " + issue.Message + ); + } + if (hasValidationErrors) { + AddConsoleMessage(MetaCoreLogLevel::Error, "RuntimeData", "Runtime 配置校验失败,已拒绝保存"); + return false; + } + + const MetaCoreTypeRegistry runtimeRegistry = MetaCoreBuildRuntimeDataTypeRegistry(); + const MetaCoreRuntimeProjectDocument runtimeProjectDocument = MetaCoreBuildDefaultRuntimeProjectDocument(); + const bool wroteProjectRuntime = MetaCoreWriteRuntimeProjectDocument( + runtimeDirectory / "ProjectRuntime.mcruntimecfg", + runtimeProjectDocument, + runtimeRegistry + ); + const bool wroteSources = MetaCoreWriteRuntimeDataSourcesDocument( + runtimeDirectory / "DataSources.mcruntime", + RuntimeDataSourcesDocument_, + runtimeRegistry + ); + const bool wroteBindings = MetaCoreWriteRuntimeBindingsDocument( + runtimeDirectory / "Bindings.mcruntime", + RuntimeBindingsDocument_, + runtimeRegistry + ); + + if (!wroteProjectRuntime || !wroteSources || !wroteBindings) { + AddConsoleMessage(MetaCoreLogLevel::Error, "RuntimeData", "保存 Runtime 配置失败"); + return false; + } + + AddConsoleMessage(MetaCoreLogLevel::Info, "RuntimeData", "已保存 Runtime 配置"); + return true; +} + +MetaCoreRuntimeDataSourcesDocument& MetaCoreEditorContext::AccessRuntimeDataSourcesDocument() { + return RuntimeDataSourcesDocument_; +} + +MetaCoreRuntimeBindingsDocument& MetaCoreEditorContext::AccessRuntimeBindingsDocument() { + return RuntimeBindingsDocument_; +} + +const MetaCoreRuntimeDataSourcesDocument& MetaCoreEditorContext::GetRuntimeDataSourcesDocument() const { + return RuntimeDataSourcesDocument_; +} + +const MetaCoreRuntimeBindingsDocument& MetaCoreEditorContext::GetRuntimeBindingsDocument() const { + return RuntimeBindingsDocument_; +} + void MetaCoreEditorContext::SetDockLayoutBuilt(bool built) { DockLayoutBuilt_ = built; } bool MetaCoreEditorContext::HasDockLayoutBuilt() const { return DockLayoutBuilt_; } @@ -420,4 +583,29 @@ void MetaCoreEditorContext::NormalizeSelection() { } } +void MetaCoreEditorContext::NotifySelectionChanged() { + ModuleRegistry_.AccessEventBus().Publish(MetaCoreEditorEvent{ + MetaCoreEditorEventType::SelectionChanged, + "Selection changed", + {}, + ActiveObjectId_, + !SelectedObjectIds_.empty() + }); +} + +void MetaCoreEditorContext::RefreshSceneDirtyState() { + if (const auto scenePersistenceService = ModuleRegistry_.ResolveService(); + scenePersistenceService != nullptr) { + scenePersistenceService->UpdateDirtyState(Scene_.CaptureSnapshot()); + } +} + +std::filesystem::path MetaCoreEditorContext::ResolveRuntimeDirectory() const { + if (const auto assetDatabaseService = ModuleRegistry_.ResolveService(); + assetDatabaseService != nullptr && assetDatabaseService->HasProject()) { + return assetDatabaseService->GetProjectDescriptor().RootPath / "Runtime"; + } + return std::filesystem::current_path() / "Runtime"; +} + } // namespace MetaCore diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorModule.cpp index 76bc135..3edd844 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorModule.cpp @@ -29,6 +29,18 @@ const std::vector>& MetaCoreEditorModu return InspectorDrawers_; } +const std::vector>& MetaCoreEditorModuleRegistry::GetServices() const { + return Services_; +} + +std::shared_ptr MetaCoreEditorModuleRegistry::ResolveServiceById(const std::string& serviceId) const { + const auto iterator = ServicesById_.find(serviceId); + if (iterator == ServicesById_.end()) { + return nullptr; + } + return iterator->second; +} + bool& MetaCoreEditorModuleRegistry::AccessPanelOpenState(const std::string& panelId) { return PanelOpenStates_[panelId]; } @@ -38,4 +50,22 @@ bool MetaCoreEditorModuleRegistry::IsPanelOpen(const std::string& panelId) const return iterator != PanelOpenStates_.end() ? iterator->second : false; } +MetaCoreEditorEventBus& MetaCoreEditorModuleRegistry::AccessEventBus() { + return EventBus_; +} + +const MetaCoreEditorEventBus& MetaCoreEditorModuleRegistry::GetEventBus() const { + return EventBus_; +} + +void MetaCoreEditorModuleRegistry::ShutdownServices() { + for (auto iterator = Services_.rbegin(); iterator != Services_.rend(); ++iterator) { + if (*iterator != nullptr) { + (*iterator)->Shutdown(*this); + } + } + Services_.clear(); + ServicesById_.clear(); +} + } // namespace MetaCore diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorServices.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorServices.cpp new file mode 100644 index 0000000..0dabd69 --- /dev/null +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorServices.cpp @@ -0,0 +1,52 @@ +#include "MetaCoreEditor/MetaCoreEditorServices.h" + +#include +#include + +namespace MetaCore { + +MetaCoreEditorEventBus::MetaCoreEditorEventSubscriptionId MetaCoreEditorEventBus::Subscribe( + MetaCoreEditorEventType eventType, + MetaCoreEditorEventHandler handler +) { + if (!handler) { + return 0; + } + + const MetaCoreEditorEventSubscriptionId subscriptionId = NextSubscriptionId_++; + Subscriptions_.push_back(std::pair{ + eventType, + MetaCoreEditorEventSubscription{ + subscriptionId, + std::move(handler) + } + }); + return subscriptionId; +} + +void MetaCoreEditorEventBus::Unsubscribe( + MetaCoreEditorEventType eventType, + MetaCoreEditorEventSubscriptionId subscriptionId +) { + std::erase_if(Subscriptions_, [&](const auto& entry) { + return entry.first == eventType && entry.second.Id == subscriptionId; + }); +} + +void MetaCoreEditorEventBus::Publish(const MetaCoreEditorEvent& event) const { + for (const auto& entry : Subscriptions_) { + if (entry.first == event.Type && entry.second.Handler) { + entry.second.Handler(event); + } + } +} + +void MetaCoreIEditorService::Startup(MetaCoreEditorModuleRegistry& moduleRegistry) { + (void)moduleRegistry; +} + +void MetaCoreIEditorService::Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) { + (void)moduleRegistry; +} + +} // namespace MetaCore diff --git a/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp b/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp index 7089d86..cd9e102 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp @@ -1,5 +1,7 @@ #include "MetaCoreEditor/MetaCoreSceneInteractionService.h" #include "MetaCoreEditor/MetaCoreEditorContext.h" +#include "MetaCoreEditor/MetaCoreEditorModule.h" +#include "MetaCoreEditor/MetaCoreEditorServices.h" #include "MetaCoreEditorCameraController.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include "MetaCoreScene/MetaCoreScene.h" @@ -36,15 +38,25 @@ glm::mat4 MetaCoreBuildTransformMatrix(const MetaCoreTransformComponent& transfo return translationMatrix * rotationMatrix * scaleMatrix; } -// Helper to convert Panda3D (Z-up) matrix to MetaCore (Y-up) space -glm::mat4 MetaCoreConvertPandaSpaceMatrixToMetaCoreMatrix(const glm::mat4& pandaMatrix) { - const glm::mat4 pandaToMetaCore( - 1.0F, 0.0F, 0.0F, 0.0F, - 0.0F, 0.0F, 1.0F, 0.0F, - 0.0F, 1.0F, 0.0F, 0.0F, - 0.0F, 0.0F, 0.0F, 1.0F - ); - return pandaToMetaCore * pandaMatrix * pandaToMetaCore; +glm::mat4 MetaCoreBuildWorldTransformMatrix(const MetaCoreScene& scene, MetaCoreId objectId) { + const MetaCoreGameObject* object = scene.FindGameObject(objectId); + if (object == nullptr) { + return glm::mat4(1.0F); + } + + glm::mat4 worldMatrix = MetaCoreBuildTransformMatrix(object->Transform); + MetaCoreId parentId = object->ParentId; + while (parentId != 0) { + const MetaCoreGameObject* parentObject = scene.FindGameObject(parentId); + if (parentObject == nullptr) { + break; + } + + worldMatrix = MetaCoreBuildTransformMatrix(parentObject->Transform) * worldMatrix; + parentId = parentObject->ParentId; + } + + return worldMatrix; } // Helper to apply MetaCore matrix back to Transform component @@ -213,13 +225,20 @@ MetaCoreId MetaCoreSceneInteractionService::PickGameObjectFromViewport( } void MetaCoreSceneInteractionService::ApplyViewportSelection(MetaCoreEditorContext& editorContext, MetaCoreId pickedObjectId) const { + if (const auto selectionService = editorContext.GetModuleRegistry().ResolveService(); + selectionService != nullptr) { + selectionService->ApplyViewportSelection(editorContext, pickedObjectId); + return; + } + if (editorContext.GetInput().IsKeyDown(MetaCoreInputKey::Control)) { if (pickedObjectId != 0) { editorContext.ToggleSelection(pickedObjectId); } - } else { - editorContext.SelectOnly(pickedObjectId); + return; } + + editorContext.SelectOnly(pickedObjectId); } void MetaCoreSceneInteractionService::HandleGizmoBeginUse(MetaCoreEditorContext& editorContext, bool gizmoUsing) { @@ -253,8 +272,6 @@ void MetaCoreSceneInteractionService::HandleGizmoManipulation(MetaCoreEditorCont const MetaCoreSceneView sceneView = editorContext.GetCameraController().BuildSceneView(); const MetaCoreSceneViewportState& viewportState = editorContext.GetSceneViewportState(); - MetaCoreEditorViewportRenderer& viewportRenderer = editorContext.GetViewportRenderer(); - const float gizmoAspect = viewportState.Width / viewportState.Height; const glm::mat4 gizmoViewMatrix = glm::lookAt(sceneView.CameraPosition, sceneView.CameraTarget, sceneView.CameraUp); const glm::mat4 gizmoProjectionMatrix = glm::perspective( @@ -262,83 +279,53 @@ void MetaCoreSceneInteractionService::HandleGizmoManipulation(MetaCoreEditorCont gizmoAspect, 0.05F, 500.0F ); - // 1. Fetch World Matrix from ViewportRenderer (in Panda3D Z-up space). - glm::mat4 worldMatrixPanda(1.0F); - if (viewportRenderer.TryGetObjectWorldMatrix(selectedObject->Id, worldMatrixPanda)) { - // 2. Convert to MetaCore Y-up World Space. - glm::mat4 worldMatrixMetaCore = MetaCoreConvertPandaSpaceMatrixToMetaCoreMatrix(worldMatrixPanda); - const glm::mat4 originalWorldMatrixMetaCore = worldMatrixMetaCore; + glm::mat4 worldMatrixMetaCore = MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), selectedObject->Id); + const glm::mat4 originalWorldMatrixMetaCore = worldMatrixMetaCore; - // 3. Setup ImGuizmo state. - ImGuizmo::SetOrthographic(false); - ImGuizmo::Enable(true); - ImGuizmo::AllowAxisFlip(false); - const MetaCoreGizmoOperation currentOp = editorContext.GetGizmoOperation(); - const float gizmoSize = (currentOp == MetaCoreGizmoOperation::Rotate) ? 0.20F : 0.30F; - ImGuizmo::SetGizmoSizeClipSpace(gizmoSize); - ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList()); - ImGuizmo::SetRect( - ImGui::GetWindowPos().x, - ImGui::GetWindowPos().y, - viewportState.Width, - viewportState.Height - ); + ImGuizmo::SetOrthographic(false); + ImGuizmo::Enable(true); + ImGuizmo::AllowAxisFlip(false); + const MetaCoreGizmoOperation currentOp = editorContext.GetGizmoOperation(); + const float gizmoSize = (currentOp == MetaCoreGizmoOperation::Rotate) ? 0.20F : 0.30F; + ImGuizmo::SetGizmoSizeClipSpace(gizmoSize); + ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList()); + ImGuizmo::SetRect( + ImGui::GetWindowPos().x, + ImGui::GetWindowPos().y, + viewportState.Width, + viewportState.Height + ); - // 4. Determine Gizmo Mode (force Local for Scale). - const MetaCoreGizmoMode mcMode = editorContext.GetGizmoMode(); - - ImGuizmo::MODE effectiveMode = (mcMode == MetaCoreGizmoMode::Global) ? ImGuizmo::WORLD : ImGuizmo::LOCAL; - - if (currentOp == MetaCoreGizmoOperation::Scale) { - effectiveMode = ImGuizmo::LOCAL; - } - - // 5. Manipulate. - ImGuizmo::PushID(reinterpret_cast(selectedObject)); - - // WORLD mode handle alignment fix. - glm::mat4 gizmoMatrix = worldMatrixMetaCore; - if (effectiveMode == ImGuizmo::WORLD) { - gizmoMatrix = glm::translate(glm::mat4(1.0F), glm::vec3(worldMatrixMetaCore[3])); - } - - ImGuizmo::Manipulate( - glm::value_ptr(gizmoViewMatrix), - glm::value_ptr(gizmoProjectionMatrix), - MetaCoreToImGuizmoOperation(currentOp), - effectiveMode, - glm::value_ptr(gizmoMatrix) - ); - - bool gizmoUsing = ImGuizmo::IsUsing(); - ImGuizmo::PopID(); - - if (gizmoUsing && (gizmoMatrix != worldMatrixMetaCore || gizmoMatrix != originalWorldMatrixMetaCore)) { - if (effectiveMode == ImGuizmo::WORLD) { - if (currentOp == MetaCoreGizmoOperation::Translate) { - worldMatrixMetaCore[3] = gizmoMatrix[3]; - } else if (currentOp == MetaCoreGizmoOperation::Rotate) { - worldMatrixMetaCore = gizmoMatrix; - } - } else { - worldMatrixMetaCore = gizmoMatrix; - } - - // 6. Decompose back to Local Space. - glm::mat4 newLocalMatrix = worldMatrixMetaCore; - if (selectedObject->ParentId != 0) { - glm::mat4 parentWorldMatrixPanda(1.0F); - if (viewportRenderer.TryGetObjectWorldMatrix(selectedObject->ParentId, parentWorldMatrixPanda)) { - const glm::mat4 parentWorldMatrixMetaCore = MetaCoreConvertPandaSpaceMatrixToMetaCoreMatrix(parentWorldMatrixPanda); - newLocalMatrix = glm::inverse(parentWorldMatrixMetaCore) * worldMatrixMetaCore; - } - } - MetaCoreApplyMatrixToTransform(newLocalMatrix, selectedObject->Transform); - } - - HandleGizmoBeginUse(editorContext, gizmoUsing); - HandleGizmoEndUse(editorContext, gizmoUsing); + ImGuizmo::MODE effectiveMode = (editorContext.GetGizmoMode() == MetaCoreGizmoMode::Global) ? ImGuizmo::WORLD : ImGuizmo::LOCAL; + if (currentOp == MetaCoreGizmoOperation::Scale) { + effectiveMode = ImGuizmo::LOCAL; } + + ImGuizmo::PushID(reinterpret_cast(selectedObject)); + glm::mat4 gizmoMatrix = worldMatrixMetaCore; + ImGuizmo::Manipulate( + glm::value_ptr(gizmoViewMatrix), + glm::value_ptr(gizmoProjectionMatrix), + MetaCoreToImGuizmoOperation(currentOp), + effectiveMode, + glm::value_ptr(gizmoMatrix) + ); + + const bool gizmoUsing = ImGuizmo::IsUsing(); + ImGuizmo::PopID(); + + if (gizmoUsing && gizmoMatrix != originalWorldMatrixMetaCore) { + glm::mat4 newLocalMatrix = gizmoMatrix; + if (selectedObject->ParentId != 0) { + const glm::mat4 parentWorldMatrixMetaCore = MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), selectedObject->ParentId); + newLocalMatrix = glm::inverse(parentWorldMatrixMetaCore) * gizmoMatrix; + } + + MetaCoreApplyMatrixToTransform(newLocalMatrix, selectedObject->Transform); + } + + HandleGizmoBeginUse(editorContext, gizmoUsing); + HandleGizmoEndUse(editorContext, gizmoUsing); } void MetaCoreSceneInteractionService::DrawViewportToolbar(MetaCoreEditorContext& editorContext) { diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreBuiltinModules.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreBuiltinModules.h new file mode 100644 index 0000000..13dd6f8 --- /dev/null +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreBuiltinModules.h @@ -0,0 +1,13 @@ +#pragma once + +#include "MetaCoreEditor/MetaCoreEditorModule.h" + +#include + +namespace MetaCore { + +std::unique_ptr MetaCoreCreateBuiltinCoreServicesModule(); +std::unique_ptr MetaCoreCreateBuiltinEditorViewsModule(); +std::unique_ptr MetaCoreCreateBuiltinEditorModule(); + +} // namespace MetaCore diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h new file mode 100644 index 0000000..7713f62 --- /dev/null +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h @@ -0,0 +1,93 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreAssetGuid.h" +#include "MetaCoreFoundation/MetaCoreReflection.h" +#include "MetaCoreScene/MetaCoreSceneDocument.h" +#include "MetaCoreScene/MetaCoreGameObject.h" + +#include +#include +#include + +namespace MetaCore { + +MC_STRUCT() +struct MetaCoreAssetMetadataDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid AssetGuid{}; + + MC_PROPERTY() + std::string AssetType{}; + + MC_PROPERTY() + std::string ImporterId{}; + + MC_PROPERTY() + std::filesystem::path SourcePath{}; + + MC_PROPERTY() + std::filesystem::path PackagePath{}; + + MC_PROPERTY() + std::uint64_t SourceHash = 0; +}; + +MC_STRUCT() +struct MetaCoreImportedAssetDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string AssetType{}; + + MC_PROPERTY() + std::string ImporterId{}; + + MC_PROPERTY() + std::filesystem::path SourcePath{}; + + MC_PROPERTY() + std::uint64_t SourceHash = 0; +}; + +MC_STRUCT() +struct MetaCorePrefabDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string Name{}; + + MC_PROPERTY() + std::vector GameObjects{}; +}; + +MC_STRUCT() +struct MetaCoreCookManifestEntry { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid AssetGuid{}; + + MC_PROPERTY() + std::filesystem::path SourcePackagePath{}; + + MC_PROPERTY() + std::filesystem::path CookedPath{}; + + MC_PROPERTY() + std::uint64_t SourceHash = 0; + + MC_PROPERTY() + std::uint64_t CookedKey = 0; +}; + +MC_STRUCT() +struct MetaCoreCookManifestDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::vector Entries{}; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h index ce39e8b..9d313b7 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h @@ -2,7 +2,10 @@ #include "MetaCoreFoundation/MetaCoreId.h" #include "MetaCoreFoundation/MetaCoreLogService.h" +#include "MetaCoreFoundation/MetaCoreReflection.h" #include "MetaCoreEditor/MetaCoreEditorCommandService.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" +#include "MetaCoreScene/MetaCoreSceneDocument.h" #include "MetaCoreScene/MetaCoreScene.h" #include @@ -37,17 +40,17 @@ enum class MetaCoreReparentTransformRule { KeepLocal }; -struct MetaCoreEditorSelectionSnapshot { - std::vector SelectedObjectIds; - MetaCoreId ActiveObjectId = 0; - MetaCoreId SelectionAnchorId = 0; -}; - +MC_STRUCT() struct MetaCoreEditorStateSnapshot { + MC_GENERATED_BODY() + + MC_PROPERTY() MetaCoreSceneSnapshot SceneSnapshot{}; + MC_PROPERTY() MetaCoreEditorSelectionSnapshot SelectionSnapshot{}; }; + struct MetaCoreSceneViewportState { float Left = 0.0F; float Top = 0.0F; @@ -122,11 +125,20 @@ public: void RequestRenameActiveObject(); [[nodiscard]] MetaCoreId ConsumeRenameRequestObjectId(); void AddConsoleMessage(MetaCoreLogLevel level, const std::string& category, const std::string& message); + [[nodiscard]] bool EnsureRuntimeDataConfigLoaded(); + [[nodiscard]] bool SaveRuntimeDataConfig(); + [[nodiscard]] MetaCoreRuntimeDataSourcesDocument& AccessRuntimeDataSourcesDocument(); + [[nodiscard]] MetaCoreRuntimeBindingsDocument& AccessRuntimeBindingsDocument(); + [[nodiscard]] const MetaCoreRuntimeDataSourcesDocument& GetRuntimeDataSourcesDocument() const; + [[nodiscard]] const MetaCoreRuntimeBindingsDocument& GetRuntimeBindingsDocument() const; void SetDockLayoutBuilt(bool built); [[nodiscard]] bool HasDockLayoutBuilt() const; private: void NormalizeSelection(); + void NotifySelectionChanged(); + void RefreshSceneDirtyState(); + [[nodiscard]] std::filesystem::path ResolveRuntimeDirectory() const; MetaCoreWindow& Window_; MetaCoreRenderDevice& RenderDevice_; @@ -145,6 +157,9 @@ private: MetaCoreEditorCommandService CommandService_{}; MetaCoreId PendingRenameObjectId_ = 0; bool DockLayoutBuilt_ = false; + bool RuntimeDataConfigLoaded_ = false; + MetaCoreRuntimeDataSourcesDocument RuntimeDataSourcesDocument_{}; + MetaCoreRuntimeBindingsDocument RuntimeBindingsDocument_{}; }; } // namespace MetaCore diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorModule.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorModule.h index 337aaf2..f09de88 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorModule.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorModule.h @@ -1,7 +1,10 @@ #pragma once +#include "MetaCoreEditor/MetaCoreEditorServices.h" + #include #include +#include #include #include @@ -42,19 +45,51 @@ public: void RegisterMenuProvider(std::unique_ptr provider); void RegisterPanelProvider(std::unique_ptr provider); void RegisterInspectorDrawer(std::unique_ptr drawer); + template + void RegisterService(std::shared_ptr service) { + static_assert(std::is_base_of_v); + if (service == nullptr) { + return; + } + + const std::string serviceId = service->GetServiceId(); + if (!serviceId.empty()) { + ServicesById_[serviceId] = service; + } + Services_.push_back(service); + service->Startup(*this); + } [[nodiscard]] const std::vector>& GetMenuProviders() const; [[nodiscard]] const std::vector>& GetPanelProviders() const; [[nodiscard]] const std::vector>& GetInspectorDrawers() const; + [[nodiscard]] const std::vector>& GetServices() const; + template + [[nodiscard]] std::shared_ptr ResolveService() const { + static_assert(std::is_base_of_v); + for (const auto& service : Services_) { + if (const auto typedService = std::dynamic_pointer_cast(service); typedService != nullptr) { + return typedService; + } + } + return nullptr; + } + [[nodiscard]] std::shared_ptr ResolveServiceById(const std::string& serviceId) const; bool& AccessPanelOpenState(const std::string& panelId); [[nodiscard]] bool IsPanelOpen(const std::string& panelId) const; + [[nodiscard]] MetaCoreEditorEventBus& AccessEventBus(); + [[nodiscard]] const MetaCoreEditorEventBus& GetEventBus() const; + void ShutdownServices(); private: std::vector> MenuProviders_{}; std::vector> PanelProviders_{}; std::vector> InspectorDrawers_{}; + std::vector> Services_{}; std::unordered_map PanelOpenStates_{}; + std::unordered_map> ServicesById_{}; + MetaCoreEditorEventBus EventBus_{}; }; class MetaCoreIModule { diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h new file mode 100644 index 0000000..5d5ed81 --- /dev/null +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h @@ -0,0 +1,257 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreAssetGuid.h" +#include "MetaCoreFoundation/MetaCoreId.h" +#include "MetaCoreFoundation/MetaCorePackage.h" +#include "MetaCoreFoundation/MetaCoreReflection.h" +#include "MetaCoreScene/MetaCoreScene.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +class MetaCoreEditorContext; +class MetaCoreEditorModuleRegistry; +struct MetaCoreGameObject; + +enum class MetaCoreEditorEventType { + ProjectLoaded = 0, + AssetDatabaseChanged, + SceneLoaded, + SceneSaved, + SceneDirtyStateChanged, + SelectionChanged +}; + +struct MetaCoreEditorEvent { + MetaCoreEditorEventType Type = MetaCoreEditorEventType::ProjectLoaded; + std::string Message{}; + std::filesystem::path Path{}; + MetaCoreId ObjectId = 0; + bool BoolValue = false; +}; + +class MetaCoreEditorEventBus { +public: + using MetaCoreEditorEventHandler = std::function; + using MetaCoreEditorEventSubscriptionId = std::size_t; + + MetaCoreEditorEventSubscriptionId Subscribe( + MetaCoreEditorEventType eventType, + MetaCoreEditorEventHandler handler + ); + + void Unsubscribe( + MetaCoreEditorEventType eventType, + MetaCoreEditorEventSubscriptionId subscriptionId + ); + + void Publish(const MetaCoreEditorEvent& event) const; + +private: + struct MetaCoreEditorEventSubscription { + MetaCoreEditorEventSubscriptionId Id = 0; + MetaCoreEditorEventHandler Handler{}; + }; + + std::vector> Subscriptions_{}; + MetaCoreEditorEventSubscriptionId NextSubscriptionId_ = 1; +}; + +class MetaCoreIEditorService { +public: + virtual ~MetaCoreIEditorService() = default; + + [[nodiscard]] virtual std::string GetServiceId() const = 0; + virtual void Startup(MetaCoreEditorModuleRegistry& moduleRegistry); + virtual void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry); +}; + +struct MetaCoreProjectDescriptor { + std::string Name{}; + std::string Version{}; + std::filesystem::path RootPath{}; + std::filesystem::path AssetsPath{}; + std::filesystem::path ScenesPath{}; + std::filesystem::path LibraryPath{}; + std::filesystem::path StartupScenePath{}; + std::vector ScenePaths{}; +}; + +enum class MetaCoreAssetStorageKind { + SourcePackage = 0, + SourceFile, + Metadata, + Cooked +}; + +struct MetaCoreAssetRecord { + MetaCoreAssetGuid Guid{}; + std::filesystem::path RelativePath{}; + std::string Type{}; + MetaCoreAssetStorageKind StorageKind = MetaCoreAssetStorageKind::SourcePackage; + std::filesystem::path SourcePath{}; + std::filesystem::path PackagePath{}; + std::filesystem::path MetaPath{}; + std::string ImporterId{}; + std::uint64_t SourceHash = 0; +}; + +class MetaCoreIReflectionRegistry : public MetaCoreIEditorService { +public: + [[nodiscard]] virtual const MetaCoreTypeRegistry& GetTypeRegistry() const = 0; + [[nodiscard]] virtual MetaCoreTypeRegistry& AccessTypeRegistry() = 0; +}; + +class MetaCoreIPackageService : public MetaCoreIEditorService { +public: + [[nodiscard]] virtual bool WritePackage( + const std::filesystem::path& absolutePath, + MetaCorePackageDocument document + ) = 0; + + [[nodiscard]] virtual std::optional ReadPackage( + const std::filesystem::path& absolutePath + ) const = 0; +}; + +class MetaCoreIAssetRegistryService : public MetaCoreIEditorService { +public: + [[nodiscard]] virtual const std::vector& GetAssetRegistry() const = 0; + [[nodiscard]] virtual std::optional FindAssetByGuid(const MetaCoreAssetGuid& assetGuid) const = 0; +}; + +class MetaCoreIAssetDatabaseService : public MetaCoreIAssetRegistryService { +public: + [[nodiscard]] virtual bool HasProject() const = 0; + [[nodiscard]] virtual const MetaCoreProjectDescriptor& GetProjectDescriptor() const = 0; + [[nodiscard]] virtual std::vector GetDirectoriesUnder(const std::filesystem::path& relativeDirectory) const = 0; + [[nodiscard]] virtual std::vector GetAssetsUnder(const std::filesystem::path& relativeDirectory) const = 0; + [[nodiscard]] virtual std::optional FindAssetByRelativePath(const std::filesystem::path& relativePath) const = 0; + virtual bool Refresh() = 0; + virtual bool CreateFolder(const std::filesystem::path& relativeDirectory) = 0; + virtual bool RegisterScenePath(const std::filesystem::path& relativeScenePath, bool makeStartupScene) = 0; + virtual bool SetStartupScenePath(const std::filesystem::path& relativeScenePath) = 0; +}; + +class MetaCoreIImportPipelineService : public MetaCoreIEditorService { +public: + [[nodiscard]] virtual bool RefreshImports() = 0; + [[nodiscard]] virtual bool ReimportAsset(const MetaCoreAssetGuid& assetGuid) = 0; +}; + +class MetaCoreICookService : public MetaCoreIEditorService { +public: + [[nodiscard]] virtual bool CookAsset(const MetaCoreAssetGuid& assetGuid) = 0; + [[nodiscard]] virtual bool CookScene(const std::filesystem::path& relativeScenePath) = 0; + [[nodiscard]] virtual std::filesystem::path GetCookedPathForAsset(const MetaCoreAssetGuid& assetGuid) const = 0; +}; + +class MetaCoreIScenePersistenceService : public MetaCoreIEditorService { +public: + [[nodiscard]] virtual bool HasOpenScene() const = 0; + [[nodiscard]] virtual bool IsSceneDirty() const = 0; + [[nodiscard]] virtual std::filesystem::path GetCurrentScenePath() const = 0; + [[nodiscard]] virtual std::string GetCurrentSceneDisplayName() const = 0; + [[nodiscard]] virtual std::vector GetKnownScenePaths() const = 0; + virtual bool LoadStartupScene(MetaCoreEditorContext& editorContext) = 0; + virtual bool LoadScene(MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeScenePath) = 0; + virtual bool SaveCurrentScene(MetaCoreEditorContext& editorContext) = 0; + virtual bool SaveSceneAs(MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeScenePath) = 0; + virtual void UpdateDirtyState(const MetaCoreSceneSnapshot& snapshot) = 0; +}; + +class MetaCoreISelectionService : public MetaCoreIEditorService { +public: + virtual void ClearSelection(MetaCoreEditorContext& editorContext) = 0; + virtual void SelectOnly(MetaCoreEditorContext& editorContext, MetaCoreId objectId) = 0; + virtual void ToggleSelection(MetaCoreEditorContext& editorContext, MetaCoreId objectId) = 0; + virtual void ApplyHierarchySelection(MetaCoreEditorContext& editorContext, MetaCoreId objectId, bool allowToggle) = 0; + virtual void ApplyViewportSelection(MetaCoreEditorContext& editorContext, MetaCoreId pickedObjectId) = 0; +}; + +class MetaCoreIClipboardService : public MetaCoreIEditorService { +public: + [[nodiscard]] virtual bool DuplicateSelection(MetaCoreEditorContext& editorContext) = 0; + [[nodiscard]] virtual bool DeleteSelection(MetaCoreEditorContext& editorContext) = 0; +}; + +class MetaCoreISceneEditingService : public MetaCoreIEditorService { +public: + [[nodiscard]] virtual std::optional CreateGameObject( + MetaCoreEditorContext& editorContext, + const std::string& objectName, + bool withMeshRenderer, + std::optional forcedParentId + ) = 0; + + [[nodiscard]] virtual bool RenameGameObject( + MetaCoreEditorContext& editorContext, + MetaCoreId objectId, + const std::string& name + ) = 0; + + [[nodiscard]] virtual bool ReparentSelection( + MetaCoreEditorContext& editorContext, + MetaCoreId newParentId + ) = 0; +}; + +class MetaCoreIPrefabService : public MetaCoreIEditorService { +public: + [[nodiscard]] virtual bool SupportsPrefabWorkflows() const = 0; + [[nodiscard]] virtual std::optional CreatePrefabFromSelection( + MetaCoreEditorContext& editorContext, + const std::filesystem::path& relativePrefabPath + ) = 0; + [[nodiscard]] virtual std::optional InstantiatePrefab( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& prefabAssetGuid, + std::optional forcedParentId + ) = 0; + [[nodiscard]] virtual bool ApplySelectedPrefabInstance(MetaCoreEditorContext& editorContext) = 0; + [[nodiscard]] virtual bool RevertSelectedPrefabInstance(MetaCoreEditorContext& editorContext) = 0; +}; + +class MetaCoreIComponentTypeRegistry : public MetaCoreIEditorService { +public: + struct MetaCoreComponentDescriptor { + std::string TypeId{}; + std::string DisplayName{}; + std::function HasComponent{}; + std::function AddComponent{}; + std::function RemoveComponent{}; + std::function ResetComponent{}; + std::function>(const MetaCoreGameObject&, const MetaCoreTypeRegistry&)> CopyComponentPayload{}; + std::function, const MetaCoreTypeRegistry&)> PasteComponentPayload{}; + std::function DrawInspector{}; + }; + + [[nodiscard]] virtual std::vector GetRegisteredComponentTypeIds() const = 0; + [[nodiscard]] virtual const std::vector& GetComponentDescriptors() const = 0; + [[nodiscard]] virtual const MetaCoreComponentDescriptor* FindDescriptor(std::string_view typeId) const = 0; + [[nodiscard]] virtual bool CopyComponent(std::string_view typeId, const MetaCoreGameObject& gameObject) = 0; + [[nodiscard]] virtual bool CanPasteComponent(std::string_view typeId) const = 0; + [[nodiscard]] virtual bool PasteComponent(std::string_view typeId, MetaCoreGameObject& gameObject) const = 0; +}; + +enum class MetaCorePlayModeState { + Edit = 0, + Playing, + Paused +}; + +class MetaCoreIPlayModeService : public MetaCoreIEditorService { +public: + [[nodiscard]] virtual MetaCorePlayModeState GetState() const = 0; + [[nodiscard]] virtual bool CanEnterPlayMode() const = 0; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreFoundation/Private/MetaCoreArchive.cpp b/Source/MetaCoreFoundation/Private/MetaCoreArchive.cpp new file mode 100644 index 0000000..625870f --- /dev/null +++ b/Source/MetaCoreFoundation/Private/MetaCoreArchive.cpp @@ -0,0 +1,87 @@ +#include "MetaCoreFoundation/MetaCoreArchive.h" + +#include + +namespace MetaCore { + +void MetaCoreArchiveWriter::WriteBytes(const void* data, std::size_t size) { + if (data == nullptr || size == 0) { + return; + } + + const auto* bytes = static_cast(data); + Buffer_.insert(Buffer_.end(), bytes, bytes + size); +} + +void MetaCoreArchiveWriter::WriteSpan(std::span data) { + Buffer_.insert(Buffer_.end(), data.begin(), data.end()); +} + +const std::vector& MetaCoreArchiveWriter::GetBuffer() const { + return Buffer_; +} + +std::size_t MetaCoreArchiveWriter::GetSize() const { + return Buffer_.size(); +} + +MetaCoreArchiveReader::MetaCoreArchiveReader(std::span buffer) + : Buffer_(buffer) { +} + +bool MetaCoreArchiveReader::ReadBytes(void* destination, std::size_t size) { + if (size == 0) { + return true; + } + if (destination == nullptr || Offset_ + size > Buffer_.size()) { + return false; + } + + std::memcpy(destination, Buffer_.data() + Offset_, size); + Offset_ += size; + return true; +} + +std::optional> MetaCoreArchiveReader::ReadSpan(std::size_t size) { + if (Offset_ + size > Buffer_.size()) { + return std::nullopt; + } + + const std::span result = Buffer_.subspan(Offset_, size); + Offset_ += size; + return result; +} + +std::optional> MetaCoreArchiveReader::PeekSpan(std::size_t size) const { + if (Offset_ + size > Buffer_.size()) { + return std::nullopt; + } + return Buffer_.subspan(Offset_, size); +} + +bool MetaCoreArchiveReader::Skip(std::size_t size) { + if (Offset_ + size > Buffer_.size()) { + return false; + } + Offset_ += size; + return true; +} + +bool MetaCoreArchiveReader::IsEof() const { + return Offset_ >= Buffer_.size(); +} + +std::size_t MetaCoreArchiveReader::GetOffset() const { + return Offset_; +} + +std::size_t MetaCoreArchiveReader::GetRemainingSize() const { + return Buffer_.size() - Offset_; +} + +std::span MetaCoreArchiveReader::GetBuffer() const { + return Buffer_; +} + +} // namespace MetaCore + diff --git a/Source/MetaCoreFoundation/Private/MetaCoreAssetGuid.cpp b/Source/MetaCoreFoundation/Private/MetaCoreAssetGuid.cpp new file mode 100644 index 0000000..bd30ca0 --- /dev/null +++ b/Source/MetaCoreFoundation/Private/MetaCoreAssetGuid.cpp @@ -0,0 +1,106 @@ +#include "MetaCoreFoundation/MetaCoreAssetGuid.h" + +#include +#include +#include +#include + +namespace MetaCore { + +namespace { + +[[nodiscard]] std::optional MetaCoreParseHexNibble(char value) { + if (value >= '0' && value <= '9') { + return static_cast(value - '0'); + } + if (value >= 'a' && value <= 'f') { + return static_cast(10 + value - 'a'); + } + if (value >= 'A' && value <= 'F') { + return static_cast(10 + value - 'A'); + } + return std::nullopt; +} + +} // namespace + +bool MetaCoreAssetGuid::IsValid() const { + for (std::uint8_t byteValue : Bytes) { + if (byteValue != 0) { + return true; + } + } + return false; +} + +std::string MetaCoreAssetGuid::ToString() const { + std::ostringstream stream; + stream << std::hex << std::setfill('0'); + + for (std::size_t index = 0; index < Bytes.size(); ++index) { + if (index == 4 || index == 6 || index == 8 || index == 10) { + stream << '-'; + } + stream << std::setw(2) << static_cast(Bytes[index]); + } + return stream.str(); +} + +MetaCoreAssetGuid MetaCoreAssetGuid::Generate() { + static thread_local std::mt19937_64 generator(std::random_device{}()); + MetaCoreAssetGuid guid; + + for (std::size_t index = 0; index < guid.Bytes.size(); index += sizeof(std::uint64_t)) { + const std::uint64_t randomValue = generator(); + for (std::size_t byteIndex = 0; byteIndex < sizeof(std::uint64_t) && (index + byteIndex) < guid.Bytes.size(); ++byteIndex) { + guid.Bytes[index + byteIndex] = static_cast((randomValue >> (byteIndex * 8U)) & 0xFFU); + } + } + + guid.Bytes[6] = static_cast((guid.Bytes[6] & 0x0FU) | 0x40U); + guid.Bytes[8] = static_cast((guid.Bytes[8] & 0x3FU) | 0x80U); + return guid; +} + +std::optional MetaCoreAssetGuid::Parse(std::string_view value) { + std::array hexDigits{}; + std::size_t digitCount = 0; + + for (char character : value) { + if (character == '-') { + continue; + } + if (digitCount >= hexDigits.size()) { + return std::nullopt; + } + hexDigits[digitCount++] = character; + } + + if (digitCount != hexDigits.size()) { + return std::nullopt; + } + + MetaCoreAssetGuid guid; + for (std::size_t index = 0; index < guid.Bytes.size(); ++index) { + const auto highNibble = MetaCoreParseHexNibble(hexDigits[index * 2]); + const auto lowNibble = MetaCoreParseHexNibble(hexDigits[index * 2 + 1]); + if (!highNibble.has_value() || !lowNibble.has_value()) { + return std::nullopt; + } + guid.Bytes[index] = static_cast((highNibble.value() << 4U) | lowNibble.value()); + } + + return guid; +} + +std::size_t MetaCoreAssetGuidHasher::operator()(const MetaCoreAssetGuid& guid) const noexcept { + std::size_t hashValue = 1469598103934665603ULL; + for (std::uint8_t byteValue : guid.Bytes) { + hashValue ^= static_cast(byteValue); + hashValue *= 1099511628211ULL; + } + return hashValue; +} + +} // namespace MetaCore + diff --git a/Source/MetaCoreFoundation/Private/MetaCoreHash.cpp b/Source/MetaCoreFoundation/Private/MetaCoreHash.cpp new file mode 100644 index 0000000..966256f --- /dev/null +++ b/Source/MetaCoreFoundation/Private/MetaCoreHash.cpp @@ -0,0 +1,59 @@ +#include "MetaCoreFoundation/MetaCoreHash.h" + +#include +#include + +namespace MetaCore { + +namespace { + +constexpr std::uint64_t GMetaCoreFnvOffsetBasis = 1469598103934665603ULL; +constexpr std::uint64_t GMetaCoreFnvPrime = 1099511628211ULL; + +} // namespace + +std::uint64_t MetaCoreHashBytes(std::span data) { + std::uint64_t hashValue = GMetaCoreFnvOffsetBasis; + for (std::byte byteValue : data) { + hashValue ^= static_cast(std::to_integer(byteValue)); + hashValue *= GMetaCoreFnvPrime; + } + return hashValue; +} + +std::uint64_t MetaCoreHashString(std::string_view value) { + const auto* bytes = reinterpret_cast(value.data()); + return MetaCoreHashBytes(std::span(bytes, value.size())); +} + +std::optional MetaCoreHashFile(const std::filesystem::path& path) { + std::ifstream input(path, std::ios::binary); + if (!input.is_open()) { + return std::nullopt; + } + + std::array buffer{}; + std::uint64_t hashValue = GMetaCoreFnvOffsetBasis; + while (input.good()) { + input.read(buffer.data(), static_cast(buffer.size())); + const std::streamsize readCount = input.gcount(); + if (readCount <= 0) { + break; + } + + const auto* bytes = reinterpret_cast(buffer.data()); + for (std::streamsize index = 0; index < readCount; ++index) { + hashValue ^= static_cast(std::to_integer(bytes[index])); + hashValue *= GMetaCoreFnvPrime; + } + } + + return hashValue; +} + +std::uint64_t MetaCoreHashCombine(std::uint64_t seed, std::uint64_t value) { + seed ^= value + 0x9E3779B97F4A7C15ULL + (seed << 6U) + (seed >> 2U); + return seed; +} + +} // namespace MetaCore diff --git a/Source/MetaCoreFoundation/Private/MetaCorePackage.cpp b/Source/MetaCoreFoundation/Private/MetaCorePackage.cpp new file mode 100644 index 0000000..e6f4288 --- /dev/null +++ b/Source/MetaCoreFoundation/Private/MetaCorePackage.cpp @@ -0,0 +1,150 @@ +#include "MetaCoreFoundation/MetaCorePackage.h" + +#include + +namespace MetaCore { + +namespace { + +[[nodiscard]] std::optional> MetaCoreReadAllBytes(const std::filesystem::path& path) { + std::ifstream input(path, std::ios::binary); + if (!input.is_open()) { + return std::nullopt; + } + + input.seekg(0, std::ios::end); + const auto size = static_cast(input.tellg()); + input.seekg(0, std::ios::beg); + + std::vector bytes(size); + if (size > 0 && !input.read(reinterpret_cast(bytes.data()), static_cast(size))) { + return std::nullopt; + } + return bytes; +} + +template +[[nodiscard]] std::optional> MetaCoreSerializeSection( + const T& value, + const MetaCoreTypeRegistry& registry +) { + return MetaCoreSerializeToBytes(value, registry); +} + +template +[[nodiscard]] bool MetaCoreDeserializeSection( + const MetaCorePackageSection& section, + std::span buffer, + T& value, + const MetaCoreTypeRegistry& registry +) { + if (section.Offset + section.Size > buffer.size()) { + return false; + } + + return MetaCoreDeserializeFromBytes( + buffer.subspan(static_cast(section.Offset), static_cast(section.Size)), + value, + registry + ); +} + +} // namespace + +bool MetaCoreWritePackageFile( + const std::filesystem::path& path, + MetaCorePackageDocument document, + const MetaCoreTypeRegistry& registry +) { + const auto serializedNames = MetaCoreSerializeSection(document.NameTable, registry); + const auto serializedImports = MetaCoreSerializeSection(document.Imports, registry); + const auto serializedExports = MetaCoreSerializeSection(document.Exports, registry); + const auto serializedDependencies = MetaCoreSerializeSection(document.Dependencies, registry); + const auto serializedCustomVersions = MetaCoreSerializeSection(document.CustomVersions, registry); + const auto serializedPayloads = MetaCoreSerializeSection(document.PayloadSections, registry); + if (!serializedNames.has_value() || + !serializedImports.has_value() || + !serializedExports.has_value() || + !serializedDependencies.has_value() || + !serializedCustomVersions.has_value() || + !serializedPayloads.has_value()) { + return false; + } + + const auto provisionalHeaderBytes = MetaCoreSerializeToBytes(document.Header, registry); + if (!provisionalHeaderBytes.has_value()) { + return false; + } + + std::uint64_t cursor = static_cast(provisionalHeaderBytes->size()); + document.Header.NameTable = MetaCorePackageSection{cursor, static_cast(serializedNames->size())}; + cursor += document.Header.NameTable.Size; + document.Header.ImportTable = MetaCorePackageSection{cursor, static_cast(serializedImports->size())}; + cursor += document.Header.ImportTable.Size; + document.Header.ExportTable = MetaCorePackageSection{cursor, static_cast(serializedExports->size())}; + cursor += document.Header.ExportTable.Size; + document.Header.DependencyTable = MetaCorePackageSection{cursor, static_cast(serializedDependencies->size())}; + cursor += document.Header.DependencyTable.Size; + document.Header.CustomVersionTable = MetaCorePackageSection{cursor, static_cast(serializedCustomVersions->size())}; + cursor += document.Header.CustomVersionTable.Size; + document.Header.PayloadSections = MetaCorePackageSection{cursor, static_cast(serializedPayloads->size())}; + cursor += document.Header.PayloadSections.Size; + document.Header.FileSize = cursor; + + const auto finalHeaderBytes = MetaCoreSerializeToBytes(document.Header, registry); + if (!finalHeaderBytes.has_value()) { + return false; + } + + std::filesystem::create_directories(path.parent_path()); + std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (!output.is_open()) { + return false; + } + + output.write(reinterpret_cast(finalHeaderBytes->data()), static_cast(finalHeaderBytes->size())); + output.write(reinterpret_cast(serializedNames->data()), static_cast(serializedNames->size())); + output.write(reinterpret_cast(serializedImports->data()), static_cast(serializedImports->size())); + output.write(reinterpret_cast(serializedExports->data()), static_cast(serializedExports->size())); + output.write(reinterpret_cast(serializedDependencies->data()), static_cast(serializedDependencies->size())); + output.write(reinterpret_cast(serializedCustomVersions->data()), static_cast(serializedCustomVersions->size())); + output.write(reinterpret_cast(serializedPayloads->data()), static_cast(serializedPayloads->size())); + return output.good(); +} + +std::optional MetaCoreReadPackageFile( + const std::filesystem::path& path, + const MetaCoreTypeRegistry& registry +) { + const auto bytes = MetaCoreReadAllBytes(path); + if (!bytes.has_value()) { + return std::nullopt; + } + + MetaCorePackageDocument document; + MetaCoreArchiveReader headerReader(std::span(bytes->data(), bytes->size())); + if (!MetaCoreDeserializeValue(headerReader, document.Header, registry)) { + return std::nullopt; + } + + if (document.Header.Magic != GMetaCorePackageMagic || + document.Header.FormatVersion != GMetaCorePackageFormatVersion || + document.Header.Endianness != MetaCorePackageEndianness::Little || + document.Header.FileSize != bytes->size()) { + return std::nullopt; + } + + const std::span byteSpan(bytes->data(), bytes->size()); + if (!MetaCoreDeserializeSection(document.Header.NameTable, byteSpan, document.NameTable, registry) || + !MetaCoreDeserializeSection(document.Header.ImportTable, byteSpan, document.Imports, registry) || + !MetaCoreDeserializeSection(document.Header.ExportTable, byteSpan, document.Exports, registry) || + !MetaCoreDeserializeSection(document.Header.DependencyTable, byteSpan, document.Dependencies, registry) || + !MetaCoreDeserializeSection(document.Header.CustomVersionTable, byteSpan, document.CustomVersions, registry) || + !MetaCoreDeserializeSection(document.Header.PayloadSections, byteSpan, document.PayloadSections, registry)) { + return std::nullopt; + } + + return document; +} + +} // namespace MetaCore diff --git a/Source/MetaCoreFoundation/Private/MetaCoreReflection.cpp b/Source/MetaCoreFoundation/Private/MetaCoreReflection.cpp new file mode 100644 index 0000000..7eab158 --- /dev/null +++ b/Source/MetaCoreFoundation/Private/MetaCoreReflection.cpp @@ -0,0 +1,27 @@ +#include "MetaCoreFoundation/MetaCoreReflection.h" + +#include "MetaCoreFoundation/MetaCoreHash.h" + +namespace MetaCore { + +const MetaCoreStructDescriptor* MetaCoreTypeRegistry::FindStructByName(std::string_view name) const { + const auto nameIterator = StructNames_.find(std::string(name)); + if (nameIterator == StructNames_.end()) { + return nullptr; + } + + const auto descriptorIterator = StructsByRuntimeType_.find(nameIterator->second); + return descriptorIterator == StructsByRuntimeType_.end() ? nullptr : &descriptorIterator->second; +} + +void MetaCoreTypeRegistry::Clear() { + StructsByRuntimeType_.clear(); + StructNames_.clear(); + EnumsByRuntimeType_.clear(); +} + +MetaCoreTypeId MetaCoreMakeTypeId(std::string_view value) { + return MetaCoreHashString(value); +} + +} // namespace MetaCore diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreArchive.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreArchive.h new file mode 100644 index 0000000..e90a258 --- /dev/null +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreArchive.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include + +namespace MetaCore { + +class MetaCoreArchiveWriter { +public: + void WriteBytes(const void* data, std::size_t size); + void WriteSpan(std::span data); + [[nodiscard]] const std::vector& GetBuffer() const; + [[nodiscard]] std::size_t GetSize() const; + +private: + std::vector Buffer_{}; +}; + +class MetaCoreArchiveReader { +public: + explicit MetaCoreArchiveReader(std::span buffer); + + [[nodiscard]] bool ReadBytes(void* destination, std::size_t size); + [[nodiscard]] std::optional> ReadSpan(std::size_t size); + [[nodiscard]] std::optional> PeekSpan(std::size_t size) const; + [[nodiscard]] bool Skip(std::size_t size); + [[nodiscard]] bool IsEof() const; + [[nodiscard]] std::size_t GetOffset() const; + [[nodiscard]] std::size_t GetRemainingSize() const; + [[nodiscard]] std::span GetBuffer() const; + +private: + std::span Buffer_{}; + std::size_t Offset_ = 0; +}; + +} // namespace MetaCore + diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetGuid.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetGuid.h new file mode 100644 index 0000000..4ce16e2 --- /dev/null +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetGuid.h @@ -0,0 +1,35 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreReflection.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +MC_STRUCT() +struct MetaCoreAssetGuid { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::array Bytes{}; + + [[nodiscard]] bool IsValid() const; + [[nodiscard]] std::string ToString() const; + + [[nodiscard]] static MetaCoreAssetGuid Generate(); + [[nodiscard]] static std::optional Parse(std::string_view value); + + auto operator<=>(const MetaCoreAssetGuid&) const = default; +}; + +struct MetaCoreAssetGuidHasher { + [[nodiscard]] std::size_t operator()(const MetaCoreAssetGuid& guid) const noexcept; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreGeneratedReflection.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreGeneratedReflection.h new file mode 100644 index 0000000..00b9e0e --- /dev/null +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreGeneratedReflection.h @@ -0,0 +1,12 @@ +#pragma once + +namespace MetaCore { + +class MetaCoreTypeRegistry; + +void MetaCoreRegisterFoundationGeneratedTypes(MetaCoreTypeRegistry& registry); +void MetaCoreRegisterSceneGeneratedTypes(MetaCoreTypeRegistry& registry); +void MetaCoreRegisterEditorGeneratedTypes(MetaCoreTypeRegistry& registry); +void MetaCoreRegisterRuntimeDataGeneratedTypes(MetaCoreTypeRegistry& registry); + +} // namespace MetaCore diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreHash.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreHash.h new file mode 100644 index 0000000..810de7b --- /dev/null +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreHash.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +[[nodiscard]] std::uint64_t MetaCoreHashBytes(std::span data); +[[nodiscard]] std::uint64_t MetaCoreHashString(std::string_view value); +[[nodiscard]] std::optional MetaCoreHashFile(const std::filesystem::path& path); +[[nodiscard]] std::uint64_t MetaCoreHashCombine(std::uint64_t seed, std::uint64_t value); + +} // namespace MetaCore + diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCorePackage.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCorePackage.h new file mode 100644 index 0000000..8e156ab --- /dev/null +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCorePackage.h @@ -0,0 +1,167 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreAssetGuid.h" + +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +constexpr std::uint32_t GMetaCorePackageMagic = 0x4D434F52U; +constexpr std::uint32_t GMetaCorePackageFormatVersion = 1U; +constexpr std::uint64_t GMetaCoreGeneratorVersion = 1U; + +MC_ENUM() +enum class MetaCorePackageType : std::uint32_t { + Scene = 1, + Prefab = 2, + Asset = 3, + Meta = 4 +}; + +MC_ENUM() +enum class MetaCorePackageEndianness : std::uint32_t { + Little = 1 +}; + +MC_STRUCT() +struct MetaCorePackageSection { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::uint64_t Offset = 0; + + MC_PROPERTY() + std::uint64_t Size = 0; +}; + +MC_STRUCT() +struct MetaCorePackageHeader { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::uint32_t Magic = GMetaCorePackageMagic; + + MC_PROPERTY() + std::uint32_t FormatVersion = GMetaCorePackageFormatVersion; + + MC_PROPERTY() + MetaCorePackageType PackageType = MetaCorePackageType::Asset; + + MC_PROPERTY() + MetaCorePackageEndianness Endianness = MetaCorePackageEndianness::Little; + + MC_PROPERTY() + MetaCoreAssetGuid PackageGuid{}; + + MC_PROPERTY() + std::uint64_t SourceHash = 0; + + MC_PROPERTY() + std::uint64_t GeneratorVersion = GMetaCoreGeneratorVersion; + + MC_PROPERTY() + MetaCorePackageSection NameTable{}; + + MC_PROPERTY() + MetaCorePackageSection ImportTable{}; + + MC_PROPERTY() + MetaCorePackageSection ExportTable{}; + + MC_PROPERTY() + MetaCorePackageSection DependencyTable{}; + + MC_PROPERTY() + MetaCorePackageSection CustomVersionTable{}; + + MC_PROPERTY() + MetaCorePackageSection PayloadSections{}; + + MC_PROPERTY() + std::uint64_t FileSize = 0; +}; + +MC_STRUCT() +struct MetaCoreImportEntry { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid AssetGuid{}; + + MC_PROPERTY() + std::string ObjectPath{}; + + MC_PROPERTY() + std::string Type{}; +}; + +MC_STRUCT() +struct MetaCoreExportEntry { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid AssetGuid{}; + + MC_PROPERTY() + std::string ObjectName{}; + + MC_PROPERTY() + MetaCoreTypeId TypeId = 0; + + MC_PROPERTY() + std::uint64_t PayloadIndex = 0; + + MC_PROPERTY() + std::uint64_t PayloadSize = 0; +}; + +MC_STRUCT() +struct MetaCoreDependencyEntry { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid AssetGuid{}; + + MC_PROPERTY() + std::string Reason{}; +}; + +MC_STRUCT() +struct MetaCoreCustomVersion { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string Key{}; + + MC_PROPERTY() + std::uint32_t Version = 0; +}; + +struct MetaCorePackageDocument { + MetaCorePackageHeader Header{}; + std::vector NameTable{}; + std::vector Imports{}; + std::vector Exports{}; + std::vector Dependencies{}; + std::vector CustomVersions{}; + std::vector> PayloadSections{}; +}; + +[[nodiscard]] bool MetaCoreWritePackageFile( + const std::filesystem::path& path, + MetaCorePackageDocument document, + const MetaCoreTypeRegistry& registry +); + +[[nodiscard]] std::optional MetaCoreReadPackageFile( + const std::filesystem::path& path, + const MetaCoreTypeRegistry& registry +); + +} // namespace MetaCore + diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreReflection.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreReflection.h new file mode 100644 index 0000000..4501d2c --- /dev/null +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreReflection.h @@ -0,0 +1,469 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreArchive.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +using MetaCoreTypeId = std::uint64_t; + +struct MetaCoreFieldDescriptor { + std::string Name{}; + std::function Serialize{}; + std::function Deserialize{}; +}; + +struct MetaCoreStructDescriptor { + MetaCoreTypeId TypeId = 0; + std::string Name{}; + std::uint32_t Version = 1; + std::type_index RuntimeType = typeid(void); + std::vector Fields{}; +}; + +struct MetaCoreEnumDescriptor { + MetaCoreTypeId TypeId = 0; + std::string Name{}; + std::type_index RuntimeType = typeid(void); +}; + +class MetaCoreTypeRegistry { +public: + template + MetaCoreStructDescriptor& RegisterStruct(std::string_view name, std::uint32_t version = 1); + + template + MetaCoreEnumDescriptor& RegisterEnum(std::string_view name); + + template + [[nodiscard]] const MetaCoreStructDescriptor* FindStruct() const; + + template + [[nodiscard]] const MetaCoreEnumDescriptor* FindEnum() const; + + [[nodiscard]] const MetaCoreStructDescriptor* FindStructByName(std::string_view name) const; + void Clear(); + +private: + std::unordered_map StructsByRuntimeType_{}; + std::unordered_map StructNames_{}; + std::unordered_map EnumsByRuntimeType_{}; +}; + +template +class MetaCoreStructRegistrationBuilder { +public: + explicit MetaCoreStructRegistrationBuilder(MetaCoreStructDescriptor& descriptor) + : Descriptor_(descriptor) { + } + + template + MetaCoreStructRegistrationBuilder& Field(std::string_view name); + +private: + MetaCoreStructDescriptor& Descriptor_; +}; + +[[nodiscard]] MetaCoreTypeId MetaCoreMakeTypeId(std::string_view value); + +template +[[nodiscard]] bool MetaCoreSerializeValue( + MetaCoreArchiveWriter& writer, + const T& value, + const MetaCoreTypeRegistry& registry +); + +template +[[nodiscard]] bool MetaCoreDeserializeValue( + MetaCoreArchiveReader& reader, + T& value, + const MetaCoreTypeRegistry& registry +); + +template +[[nodiscard]] std::optional> MetaCoreSerializeToBytes( + const T& value, + const MetaCoreTypeRegistry& registry +); + +template +[[nodiscard]] bool MetaCoreDeserializeFromBytes( + std::span bytes, + T& value, + const MetaCoreTypeRegistry& registry +); + +template +[[nodiscard]] MetaCoreStructRegistrationBuilder MetaCoreRegisterGeneratedStruct( + MetaCoreTypeRegistry& registry, + std::string_view name, + std::uint32_t version = 1 +); + +template +void MetaCoreRegisterGeneratedEnum( + MetaCoreTypeRegistry& registry, + std::string_view name +); + +#define MC_STRUCT(...) +#define MC_CLASS(...) +#define MC_ENUM(...) +#define MC_PROPERTY(...) +#define MC_GENERATED_BODY() + +namespace Detail { + +template +struct MetaCoreIsVector : std::false_type {}; + +template +struct MetaCoreIsVector> : std::true_type { + using ValueType = TValue; +}; + +template +struct MetaCoreIsOptional : std::false_type {}; + +template +struct MetaCoreIsOptional> : std::true_type { + using ValueType = TValue; +}; + +template +struct MetaCoreIsStdArray : std::false_type {}; + +template +struct MetaCoreIsStdArray> : std::true_type { + using ValueType = TValue; + static constexpr std::size_t Size = TSize; +}; + +template +inline constexpr bool GMetaCoreAlwaysFalse = false; + +} // namespace Detail + +template +MetaCoreStructDescriptor& MetaCoreTypeRegistry::RegisterStruct(std::string_view name, std::uint32_t version) { + auto& descriptor = StructsByRuntimeType_[std::type_index(typeid(T))]; + descriptor.TypeId = MetaCoreMakeTypeId(name); + descriptor.Name = std::string(name); + descriptor.Version = version; + descriptor.RuntimeType = std::type_index(typeid(T)); + descriptor.Fields.clear(); + StructNames_.insert_or_assign(descriptor.Name, descriptor.RuntimeType); + return descriptor; +} + +template +MetaCoreEnumDescriptor& MetaCoreTypeRegistry::RegisterEnum(std::string_view name) { + auto& descriptor = EnumsByRuntimeType_[std::type_index(typeid(T))]; + descriptor.TypeId = MetaCoreMakeTypeId(name); + descriptor.Name = std::string(name); + descriptor.RuntimeType = std::type_index(typeid(T)); + return descriptor; +} + +template +const MetaCoreStructDescriptor* MetaCoreTypeRegistry::FindStruct() const { + const auto iterator = StructsByRuntimeType_.find(std::type_index(typeid(T))); + return iterator == StructsByRuntimeType_.end() ? nullptr : &iterator->second; +} + +template +const MetaCoreEnumDescriptor* MetaCoreTypeRegistry::FindEnum() const { + const auto iterator = EnumsByRuntimeType_.find(std::type_index(typeid(T))); + return iterator == EnumsByRuntimeType_.end() ? nullptr : &iterator->second; +} + +template +template +MetaCoreStructRegistrationBuilder& MetaCoreStructRegistrationBuilder::Field(std::string_view name) { + Descriptor_.Fields.push_back(MetaCoreFieldDescriptor{ + std::string(name), + [](const void* instance, MetaCoreArchiveWriter& writer, const MetaCoreTypeRegistry& registry) { + const auto& typedInstance = *static_cast(instance); + return MetaCoreSerializeValue(writer, typedInstance.*MemberPointer, registry); + }, + [](void* instance, MetaCoreArchiveReader& reader, const MetaCoreTypeRegistry& registry) { + auto& typedInstance = *static_cast(instance); + return MetaCoreDeserializeValue(reader, typedInstance.*MemberPointer, registry); + } + }); + return *this; +} + +template +MetaCoreStructRegistrationBuilder MetaCoreRegisterGeneratedStruct( + MetaCoreTypeRegistry& registry, + std::string_view name, + std::uint32_t version +) { + return MetaCoreStructRegistrationBuilder(registry.RegisterStruct(name, version)); +} + +template +void MetaCoreRegisterGeneratedEnum(MetaCoreTypeRegistry& registry, std::string_view name) { + (void)registry.RegisterEnum(name); +} + +template +bool MetaCoreSerializeValue( + MetaCoreArchiveWriter& writer, + const T& value, + const MetaCoreTypeRegistry& registry +) { + using TValue = std::remove_cvref_t; + + if constexpr (std::is_same_v) { + const std::uint8_t byteValue = value ? 1U : 0U; + writer.WriteBytes(&byteValue, sizeof(byteValue)); + return true; + } else if constexpr (std::is_integral_v || std::is_floating_point_v) { + writer.WriteBytes(&value, sizeof(TValue)); + return true; + } else if constexpr (std::is_enum_v) { + const auto enumValue = static_cast>(value); + return MetaCoreSerializeValue(writer, enumValue, registry); + } else if constexpr (std::is_same_v) { + const std::uint64_t size = static_cast(value.size()); + writer.WriteBytes(&size, sizeof(size)); + if (!value.empty()) { + writer.WriteBytes(value.data(), value.size()); + } + return true; + } else if constexpr (std::is_same_v) { + return MetaCoreSerializeValue(writer, value.generic_string(), registry); + } else if constexpr (std::is_same_v) { + writer.WriteBytes(&value.x, sizeof(value.x)); + writer.WriteBytes(&value.y, sizeof(value.y)); + writer.WriteBytes(&value.z, sizeof(value.z)); + return true; + } else if constexpr (Detail::MetaCoreIsStdArray::value) { + for (const auto& item : value) { + if (!MetaCoreSerializeValue(writer, item, registry)) { + return false; + } + } + return true; + } else if constexpr (Detail::MetaCoreIsVector::value) { + const std::uint64_t size = static_cast(value.size()); + writer.WriteBytes(&size, sizeof(size)); + for (const auto& item : value) { + if (!MetaCoreSerializeValue(writer, item, registry)) { + return false; + } + } + return true; + } else if constexpr (Detail::MetaCoreIsOptional::value) { + const bool hasValue = value.has_value(); + if (!MetaCoreSerializeValue(writer, hasValue, registry)) { + return false; + } + if (hasValue) { + return MetaCoreSerializeValue(writer, value.value(), registry); + } + return true; + } else { + const MetaCoreStructDescriptor* descriptor = registry.FindStruct(); + if (descriptor == nullptr) { + return false; + } + + writer.WriteBytes(&descriptor->Version, sizeof(descriptor->Version)); + const std::uint32_t fieldCount = static_cast(descriptor->Fields.size()); + writer.WriteBytes(&fieldCount, sizeof(fieldCount)); + + for (const MetaCoreFieldDescriptor& field : descriptor->Fields) { + if (!MetaCoreSerializeValue(writer, field.Name, registry)) { + return false; + } + + MetaCoreArchiveWriter fieldWriter; + if (!field.Serialize(&value, fieldWriter, registry)) { + return false; + } + + const std::uint64_t fieldSize = static_cast(fieldWriter.GetSize()); + writer.WriteBytes(&fieldSize, sizeof(fieldSize)); + writer.WriteSpan(fieldWriter.GetBuffer()); + } + return true; + } +} + +template +bool MetaCoreDeserializeValue( + MetaCoreArchiveReader& reader, + T& value, + const MetaCoreTypeRegistry& registry +) { + using TValue = std::remove_cvref_t; + + if constexpr (std::is_same_v) { + std::uint8_t byteValue = 0; + if (!reader.ReadBytes(&byteValue, sizeof(byteValue))) { + return false; + } + value = byteValue != 0; + return true; + } else if constexpr (std::is_integral_v || std::is_floating_point_v) { + return reader.ReadBytes(&value, sizeof(TValue)); + } else if constexpr (std::is_enum_v) { + std::underlying_type_t enumValue{}; + if (!MetaCoreDeserializeValue(reader, enumValue, registry)) { + return false; + } + value = static_cast(enumValue); + return true; + } else if constexpr (std::is_same_v) { + std::uint64_t size = 0; + if (!reader.ReadBytes(&size, sizeof(size))) { + return false; + } + const auto bytes = reader.ReadSpan(static_cast(size)); + if (!bytes.has_value()) { + return false; + } + value.assign(reinterpret_cast(bytes->data()), bytes->size()); + return true; + } else if constexpr (std::is_same_v) { + std::string serializedPath; + if (!MetaCoreDeserializeValue(reader, serializedPath, registry)) { + return false; + } + value = std::filesystem::path(serializedPath); + return true; + } else if constexpr (std::is_same_v) { + return reader.ReadBytes(&value.x, sizeof(value.x)) && + reader.ReadBytes(&value.y, sizeof(value.y)) && + reader.ReadBytes(&value.z, sizeof(value.z)); + } else if constexpr (Detail::MetaCoreIsStdArray::value) { + for (auto& item : value) { + if (!MetaCoreDeserializeValue(reader, item, registry)) { + return false; + } + } + return true; + } else if constexpr (Detail::MetaCoreIsVector::value) { + std::uint64_t size = 0; + if (!reader.ReadBytes(&size, sizeof(size))) { + return false; + } + value.clear(); + value.reserve(static_cast(size)); + for (std::uint64_t index = 0; index < size; ++index) { + typename Detail::MetaCoreIsVector::ValueType item{}; + if (!MetaCoreDeserializeValue(reader, item, registry)) { + return false; + } + value.push_back(std::move(item)); + } + return true; + } else if constexpr (Detail::MetaCoreIsOptional::value) { + bool hasValue = false; + if (!MetaCoreDeserializeValue(reader, hasValue, registry)) { + return false; + } + if (!hasValue) { + value.reset(); + return true; + } + + typename Detail::MetaCoreIsOptional::ValueType innerValue{}; + if (!MetaCoreDeserializeValue(reader, innerValue, registry)) { + return false; + } + value = std::move(innerValue); + return true; + } else { + const MetaCoreStructDescriptor* descriptor = registry.FindStruct(); + if (descriptor == nullptr) { + return false; + } + + std::uint32_t serializedVersion = 0; + std::uint32_t fieldCount = 0; + if (!reader.ReadBytes(&serializedVersion, sizeof(serializedVersion)) || + !reader.ReadBytes(&fieldCount, sizeof(fieldCount))) { + return false; + } + + (void)serializedVersion; + + for (std::uint32_t index = 0; index < fieldCount; ++index) { + std::string fieldName; + std::uint64_t fieldSize = 0; + if (!MetaCoreDeserializeValue(reader, fieldName, registry) || + !reader.ReadBytes(&fieldSize, sizeof(fieldSize))) { + return false; + } + + const auto fieldBytes = reader.ReadSpan(static_cast(fieldSize)); + if (!fieldBytes.has_value()) { + return false; + } + + const auto fieldIterator = std::find_if( + descriptor->Fields.begin(), + descriptor->Fields.end(), + [&](const MetaCoreFieldDescriptor& field) { + return field.Name == fieldName; + } + ); + + if (fieldIterator == descriptor->Fields.end()) { + continue; + } + + MetaCoreArchiveReader fieldReader(*fieldBytes); + if (!fieldIterator->Deserialize(&value, fieldReader, registry)) { + return false; + } + } + + return true; + } +} + +template +std::optional> MetaCoreSerializeToBytes( + const T& value, + const MetaCoreTypeRegistry& registry +) { + MetaCoreArchiveWriter writer; + if (!MetaCoreSerializeValue(writer, value, registry)) { + return std::nullopt; + } + return writer.GetBuffer(); +} + +template +bool MetaCoreDeserializeFromBytes( + std::span bytes, + T& value, + const MetaCoreTypeRegistry& registry +) { + MetaCoreArchiveReader reader(bytes); + return MetaCoreDeserializeValue(reader, value, registry) && reader.IsEof(); +} + +} // namespace MetaCore diff --git a/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataDispatcher.cpp b/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataDispatcher.cpp new file mode 100644 index 0000000..d2fc462 --- /dev/null +++ b/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataDispatcher.cpp @@ -0,0 +1,183 @@ +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataDispatcher.h" + +#include "MetaCoreScene/MetaCoreComponents.h" + +#include + +namespace MetaCore { + +MetaCoreRuntimeDataDispatcher::MetaCoreRuntimeDataDispatcher(MetaCoreScene& scene) + : Scene_(scene) { +} + +void MetaCoreRuntimeDataDispatcher::SetDataPointDefinitions(std::vector dataPointDefinitions) { + DataPointDefinitions_ = std::move(dataPointDefinitions); +} + +void MetaCoreRuntimeDataDispatcher::SetBindingDefinitions(std::vector bindingDefinitions) { + BindingDefinitions_ = std::move(bindingDefinitions); + RebuildBindingStatuses(); +} + +void MetaCoreRuntimeDataDispatcher::ApplyUpdates(const std::vector& updates) { + for (const MetaCoreRuntimeDataUpdate& update : updates) { + const MetaCoreDataPointDefinition* dataPointDefinition = FindDataPointDefinition(update.DataPointId); + if (dataPointDefinition == nullptr) { + continue; + } + + for (const MetaCoreSceneBindingDefinition& bindingDefinition : BindingDefinitions_) { + if (bindingDefinition.DataPointId != update.DataPointId) { + continue; + } + + MetaCoreGameObject* gameObject = Scene_.FindGameObject(bindingDefinition.TargetObjectId); + if (gameObject == nullptr) { + MarkBindingFault(bindingDefinition.BindingId, "Target object missing"); + continue; + } + + bool applied = false; + switch (bindingDefinition.Target) { + case MetaCoreRuntimeBindingTarget::TransformPosition: + if (update.Value.Type == MetaCoreRuntimeValueType::Vec3) { + gameObject->Transform.Position = update.Value.Vec3Value; + applied = true; + } + break; + case MetaCoreRuntimeBindingTarget::MeshRendererVisible: + if (update.Value.Type == MetaCoreRuntimeValueType::Bool && gameObject->MeshRenderer.has_value()) { + gameObject->MeshRenderer->Visible = update.Value.BoolValue; + applied = true; + } + break; + case MetaCoreRuntimeBindingTarget::MeshRendererBaseColor: + if (update.Value.Type == MetaCoreRuntimeValueType::Vec3 && gameObject->MeshRenderer.has_value()) { + gameObject->MeshRenderer->BaseColor = update.Value.Vec3Value; + applied = true; + } + break; + case MetaCoreRuntimeBindingTarget::LightIntensity: + if (update.Value.Type == MetaCoreRuntimeValueType::Double && gameObject->Light.has_value()) { + gameObject->Light->Intensity = static_cast(update.Value.DoubleValue); + applied = true; + } + break; + case MetaCoreRuntimeBindingTarget::LightColor: + if (update.Value.Type == MetaCoreRuntimeValueType::Vec3 && gameObject->Light.has_value()) { + gameObject->Light->Color = update.Value.Vec3Value; + applied = true; + } + break; + } + + if (!applied) { + MarkBindingFault(bindingDefinition.BindingId, "Update type incompatible with binding target"); + continue; + } + + MarkBindingHealthy(bindingDefinition.BindingId, update.Value.SourceTimestamp); + } + } +} + +void MetaCoreRuntimeDataDispatcher::TickStaleness(std::uint64_t currentTimestamp) { + for (MetaCoreRuntimeBindingStatus& bindingStatus : BindingStatuses_) { + if (bindingStatus.LastAppliedAt == 0) { + bindingStatus.Stale = true; + continue; + } + + bindingStatus.Stale = + currentTimestamp > bindingStatus.LastAppliedAt && + (currentTimestamp - bindingStatus.LastAppliedAt) > bindingStatus.StaleAfterMs; + } +} + +const std::vector& MetaCoreRuntimeDataDispatcher::GetBindingStatuses() const { + return BindingStatuses_; +} + +bool MetaCoreRuntimeDataDispatcher::HasBindingFaults() const { + return std::any_of( + BindingStatuses_.begin(), + BindingStatuses_.end(), + [](const MetaCoreRuntimeBindingStatus& bindingStatus) { + return !bindingStatus.Healthy || bindingStatus.Stale; + } + ); +} + +MetaCoreRuntimeDiagnosticsSnapshot MetaCoreRuntimeDataDispatcher::BuildDiagnosticsSnapshot( + const std::vector& sourceStatuses +) const { + MetaCoreRuntimeDiagnosticsSnapshot snapshot; + snapshot.SourceStatuses = sourceStatuses; + snapshot.BindingStatuses = BindingStatuses_; + snapshot.HasFaults = HasBindingFaults(); + snapshot.HasFaults = snapshot.HasFaults || std::any_of( + snapshot.SourceStatuses.begin(), + snapshot.SourceStatuses.end(), + [](const MetaCoreRuntimeDataSourceStatus& sourceStatus) { + return sourceStatus.State == MetaCoreRuntimeDataSourceState::Degraded || + sourceStatus.State == MetaCoreRuntimeDataSourceState::Faulted; + } + ); + return snapshot; +} + +void MetaCoreRuntimeDataDispatcher::RebuildBindingStatuses() { + BindingStatuses_.clear(); + BindingStatusIndexById_.clear(); + BindingStatuses_.reserve(BindingDefinitions_.size()); + + for (const MetaCoreSceneBindingDefinition& bindingDefinition : BindingDefinitions_) { + BindingStatusIndexById_[bindingDefinition.BindingId] = BindingStatuses_.size(); + BindingStatuses_.push_back(MetaCoreRuntimeBindingStatus{ + bindingDefinition.BindingId, + true, + false, + 0, + 1000, + {} + }); + } +} + +void MetaCoreRuntimeDataDispatcher::MarkBindingFault(const std::string& bindingId, const std::string& error) { + const auto iterator = BindingStatusIndexById_.find(bindingId); + if (iterator == BindingStatusIndexById_.end()) { + return; + } + + MetaCoreRuntimeBindingStatus& status = BindingStatuses_[iterator->second]; + status.Healthy = false; + status.Stale = false; + status.LastError = error; +} + +void MetaCoreRuntimeDataDispatcher::MarkBindingHealthy(const std::string& bindingId, std::uint64_t appliedAt) { + const auto iterator = BindingStatusIndexById_.find(bindingId); + if (iterator == BindingStatusIndexById_.end()) { + return; + } + + MetaCoreRuntimeBindingStatus& status = BindingStatuses_[iterator->second]; + status.Healthy = true; + status.Stale = false; + status.LastAppliedAt = appliedAt; + status.LastError.clear(); +} + +const MetaCoreDataPointDefinition* MetaCoreRuntimeDataDispatcher::FindDataPointDefinition(const std::string& dataPointId) const { + const auto iterator = std::find_if( + DataPointDefinitions_.begin(), + DataPointDefinitions_.end(), + [&](const MetaCoreDataPointDefinition& definition) { + return definition.Id == dataPointId; + } + ); + return iterator == DataPointDefinitions_.end() ? nullptr : &(*iterator); +} + +} // namespace MetaCore diff --git a/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataProject.cpp b/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataProject.cpp new file mode 100644 index 0000000..80f8c06 --- /dev/null +++ b/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataProject.cpp @@ -0,0 +1,207 @@ +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" + +#include +#include +#include + +namespace MetaCore { + +namespace { + +template +[[nodiscard]] bool MetaCoreWriteBinaryDocument( + const std::filesystem::path& path, + const T& document, + const MetaCoreTypeRegistry& registry +) { + const auto bytes = MetaCoreSerializeToBytes(document, registry); + if (!bytes.has_value()) { + return false; + } + + std::filesystem::create_directories(path.parent_path()); + std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (!output.is_open()) { + return false; + } + + if (!bytes->empty()) { + output.write(reinterpret_cast(bytes->data()), static_cast(bytes->size())); + } + return output.good(); +} + +template +[[nodiscard]] std::optional MetaCoreReadBinaryDocument( + const std::filesystem::path& path, + const MetaCoreTypeRegistry& registry +) { + std::ifstream input(path, std::ios::binary); + if (!input.is_open()) { + return std::nullopt; + } + + input.seekg(0, std::ios::end); + const auto size = static_cast(input.tellg()); + input.seekg(0, std::ios::beg); + + std::vector bytes(size); + if (size > 0 && !input.read(reinterpret_cast(bytes.data()), static_cast(size))) { + return std::nullopt; + } + + T document{}; + if (!MetaCoreDeserializeFromBytes(std::span(bytes.data(), bytes.size()), document, registry)) { + return std::nullopt; + } + return document; +} + +} // namespace + +bool MetaCoreWriteRuntimeDataSourcesDocument( + const std::filesystem::path& path, + const MetaCoreRuntimeDataSourcesDocument& document, + const MetaCoreTypeRegistry& registry +) { + return MetaCoreWriteBinaryDocument(path, document, registry); +} + +bool MetaCoreWriteRuntimeBindingsDocument( + const std::filesystem::path& path, + const MetaCoreRuntimeBindingsDocument& document, + const MetaCoreTypeRegistry& registry +) { + return MetaCoreWriteBinaryDocument(path, document, registry); +} + +std::optional MetaCoreReadRuntimeDataSourcesDocument( + const std::filesystem::path& path, + const MetaCoreTypeRegistry& registry +) { + return MetaCoreReadBinaryDocument(path, registry); +} + +std::optional MetaCoreReadRuntimeBindingsDocument( + const std::filesystem::path& path, + const MetaCoreTypeRegistry& registry +) { + return MetaCoreReadBinaryDocument(path, registry); +} + +bool MetaCoreWriteRuntimeProjectDocument( + const std::filesystem::path& path, + const MetaCoreRuntimeProjectDocument& document, + const MetaCoreTypeRegistry& registry +) { + return MetaCoreWriteBinaryDocument(path, document, registry); +} + +std::optional MetaCoreReadRuntimeProjectDocument( + const std::filesystem::path& path, + const MetaCoreTypeRegistry& registry +) { + return MetaCoreReadBinaryDocument(path, registry); +} + +bool MetaCoreWriteRuntimeDiagnosticsSnapshot( + const std::filesystem::path& path, + const MetaCoreRuntimeDiagnosticsSnapshot& snapshot, + const MetaCoreTypeRegistry& registry +) { + return MetaCoreWriteBinaryDocument(path, snapshot, registry); +} + +std::optional MetaCoreReadRuntimeDiagnosticsSnapshot( + const std::filesystem::path& path, + const MetaCoreTypeRegistry& registry +) { + return MetaCoreReadBinaryDocument(path, registry); +} + +std::vector MetaCoreValidateRuntimeDataDocuments( + const MetaCoreRuntimeDataSourcesDocument& sourcesDocument, + const MetaCoreRuntimeBindingsDocument& bindingsDocument +) { + std::vector issues; + + std::unordered_set sourceIds; + for (const auto& source : sourcesDocument.Sources) { + if (source.Id.empty()) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, "Source", "存在空的 SourceId"}); + continue; + } + if (!sourceIds.insert(source.Id).second) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, source.Id, "SourceId 重复"}); + } + if (source.AdapterType.empty()) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, source.Id, "AdapterType 不能为空"}); + } + if (source.AdapterType == "file_replay") { + const auto filePathSetting = std::find_if( + source.ConnectionSettings.begin(), + source.ConnectionSettings.end(), + [](const MetaCoreDataSourceSetting& setting) { + return setting.Key == "file_path"; + } + ); + if (filePathSetting == source.ConnectionSettings.end() || filePathSetting->Value.empty()) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, source.Id, "file_replay 缺少 file_path"}); + } + } + if (source.AdapterType == "tcp") { + const auto portSetting = std::find_if( + source.ConnectionSettings.begin(), + source.ConnectionSettings.end(), + [](const MetaCoreDataSourceSetting& setting) { + return setting.Key == "port"; + } + ); + if (portSetting == source.ConnectionSettings.end() || portSetting->Value.empty()) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, source.Id, "tcp 缺少 port"}); + } + } + } + + std::unordered_set dataPointIds; + for (const auto& dataPoint : sourcesDocument.DataPoints) { + if (dataPoint.Id.empty()) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, "DataPoint", "存在空的 DataPointId"}); + continue; + } + if (!dataPointIds.insert(dataPoint.Id).second) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, dataPoint.Id, "DataPointId 重复"}); + } + if (dataPoint.SourceId.empty() || !sourceIds.contains(dataPoint.SourceId)) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, dataPoint.Id, "DataPoint 引用了不存在的 SourceId"}); + } + if (dataPoint.ExternalAddress.empty()) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Warning, dataPoint.Id, "ExternalAddress 为空"}); + } + } + + std::unordered_set bindingIds; + for (const auto& binding : bindingsDocument.Bindings) { + if (binding.BindingId.empty()) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, "Binding", "存在空的 BindingId"}); + continue; + } + if (!bindingIds.insert(binding.BindingId).second) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, binding.BindingId, "BindingId 重复"}); + } + if (binding.DataPointId.empty() || !dataPointIds.contains(binding.DataPointId)) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, binding.BindingId, "Binding 引用了不存在的 DataPointId"}); + } + if (binding.TargetObjectId == 0) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, binding.BindingId, "Binding 缺少 TargetObjectId"}); + } + } + + if (sourcesDocument.Sources.empty()) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Warning, "Runtime", "当前没有任何 Source"}); + } + + return issues; +} + +} // namespace MetaCore diff --git a/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataSource.cpp b/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataSource.cpp new file mode 100644 index 0000000..45edaea --- /dev/null +++ b/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataSource.cpp @@ -0,0 +1,618 @@ +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataSource.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include + +namespace MetaCore { + +namespace { + +constexpr SOCKET MetaCoreInvalidSocket = INVALID_SOCKET; + +[[nodiscard]] std::string MetaCoreTrim(std::string value) { + const auto isSpace = [](unsigned char ch) { + return std::isspace(ch) != 0; + }; + + value.erase(value.begin(), std::find_if(value.begin(), value.end(), [&](unsigned char ch) { + return !isSpace(ch); + })); + value.erase(std::find_if(value.rbegin(), value.rend(), [&](unsigned char ch) { + return !isSpace(ch); + }).base(), value.end()); + return value; +} + +[[nodiscard]] const std::string* MetaCoreFindSetting( + const MetaCoreDataSourceDefinition& sourceDefinition, + const std::string& key +) { + const auto iterator = std::find_if( + sourceDefinition.ConnectionSettings.begin(), + sourceDefinition.ConnectionSettings.end(), + [&](const MetaCoreDataSourceSetting& setting) { + return setting.Key == key; + } + ); + if (iterator == sourceDefinition.ConnectionSettings.end()) { + return nullptr; + } + return &iterator->Value; +} + +[[nodiscard]] bool MetaCoreParseValuePayload( + std::istringstream& parser, + const std::string& valueType, + MetaCoreRuntimeDataValue& value, + std::string& errorMessage +) { + value.Quality = MetaCoreRuntimeDataQuality::Good; + + if (valueType == "bool") { + std::string token; + if (!(parser >> token)) { + errorMessage = "bool value missing"; + return false; + } + value.Type = MetaCoreRuntimeValueType::Bool; + value.BoolValue = (token == "true" || token == "1"); + return true; + } + if (valueType == "int64") { + std::int64_t parsedValue = 0; + if (!(parser >> parsedValue)) { + errorMessage = "int64 value missing"; + return false; + } + value.Type = MetaCoreRuntimeValueType::Int64; + value.Int64Value = parsedValue; + return true; + } + if (valueType == "double") { + double parsedValue = 0.0; + if (!(parser >> parsedValue)) { + errorMessage = "double value missing"; + return false; + } + value.Type = MetaCoreRuntimeValueType::Double; + value.DoubleValue = parsedValue; + return true; + } + if (valueType == "string") { + std::string parsedValue; + std::getline(parser >> std::ws, parsedValue); + value.Type = MetaCoreRuntimeValueType::String; + value.StringValue = MetaCoreTrim(parsedValue); + return true; + } + if (valueType == "vec3") { + float x = 0.0F; + float y = 0.0F; + float z = 0.0F; + if (!(parser >> x >> y >> z)) { + errorMessage = "vec3 value missing"; + return false; + } + value.Type = MetaCoreRuntimeValueType::Vec3; + value.Vec3Value = glm::vec3(x, y, z); + return true; + } + + errorMessage = "value type not supported"; + return false; +} + +[[nodiscard]] std::string MetaCoreResolveReplayFilePath(const std::string& replayFilePath) { + const std::filesystem::path inputPath(replayFilePath); + const std::vector candidates{ + std::filesystem::current_path() / inputPath, + std::filesystem::current_path() / ".." / ".." / ".." / inputPath, + std::filesystem::current_path() / ".." / ".." / ".." / "TestProject" / "Runtime" / inputPath.filename() + }; + + for (const auto& candidate : candidates) { + std::error_code errorCode; + if (std::filesystem::exists(candidate, errorCode)) { + return std::filesystem::weakly_canonical(candidate, errorCode).string(); + } + } + return (std::filesystem::current_path() / inputPath).string(); +} + +} // namespace + +std::string MetaCoreMockRuntimeDataSourceAdapter::GetAdapterType() const { + return "mock"; +} + +bool MetaCoreMockRuntimeDataSourceAdapter::Configure(const MetaCoreDataSourceDefinition& sourceDefinition) { + SourceDefinition_ = sourceDefinition; + Status_.SourceId = sourceDefinition.Id; + Status_.State = MetaCoreRuntimeDataSourceState::Disconnected; + Status_.LastConnectedAt = 0; + Status_.LastUpdateAt = 0; + Status_.LastError.clear(); + ElapsedSeconds_ = 0.0; + Sequence_ = 0; + return true; +} + +bool MetaCoreMockRuntimeDataSourceAdapter::Connect() { + Status_.State = MetaCoreRuntimeDataSourceState::Connected; + Status_.LastConnectedAt = 1; + Status_.LastError.clear(); + return true; +} + +void MetaCoreMockRuntimeDataSourceAdapter::Disconnect() { + Status_.State = MetaCoreRuntimeDataSourceState::Disconnected; + Status_.LastError.clear(); +} + +void MetaCoreMockRuntimeDataSourceAdapter::Tick(double deltaSeconds) { + if (Status_.State != MetaCoreRuntimeDataSourceState::Connected) { + return; + } + ElapsedSeconds_ += std::max(0.0, deltaSeconds); + if (!EmitUpdates_) { + Status_.State = MetaCoreRuntimeDataSourceState::Degraded; + Status_.LastError = "Mock adapter update emission disabled"; + } else { + Status_.State = MetaCoreRuntimeDataSourceState::Connected; + Status_.LastError.clear(); + } +} + +std::vector MetaCoreMockRuntimeDataSourceAdapter::PollUpdates() { + if (Status_.State == MetaCoreRuntimeDataSourceState::Disconnected || !EmitUpdates_) { + return {}; + } + + const std::uint64_t timestamp = static_cast(ElapsedSeconds_ * 1000.0); + Status_.LastUpdateAt = timestamp; + + std::vector updates; + updates.push_back(MetaCoreRuntimeDataUpdate{ + "cube.position", + MetaCoreRuntimeDataValue{ + MetaCoreRuntimeValueType::Vec3, + false, + 0, + 0.0, + {}, + glm::vec3(static_cast(std::sin(ElapsedSeconds_)) * 2.0F, 0.5F, 0.0F), + timestamp, + MetaCoreRuntimeDataQuality::Good + }, + ++Sequence_ + }); + updates.push_back(MetaCoreRuntimeDataUpdate{ + "cube.visible", + MetaCoreRuntimeDataValue{ + MetaCoreRuntimeValueType::Bool, + std::fmod(ElapsedSeconds_, 2.0) < 1.0, + 0, + 0.0, + {}, + glm::vec3(0.0F, 0.0F, 0.0F), + timestamp, + MetaCoreRuntimeDataQuality::Good + }, + ++Sequence_ + }); + updates.push_back(MetaCoreRuntimeDataUpdate{ + "cube.base_color", + MetaCoreRuntimeDataValue{ + MetaCoreRuntimeValueType::Vec3, + false, + 0, + 0.0, + {}, + glm::vec3( + 0.5F + static_cast(std::sin(ElapsedSeconds_)) * 0.5F, + 0.6F, + 0.9F - static_cast(std::sin(ElapsedSeconds_)) * 0.3F + ), + timestamp, + MetaCoreRuntimeDataQuality::Good + }, + ++Sequence_ + }); + return updates; +} + +const MetaCoreRuntimeDataSourceStatus& MetaCoreMockRuntimeDataSourceAdapter::GetStatus() const { + return Status_; +} + +void MetaCoreMockRuntimeDataSourceAdapter::SetEmitUpdates(bool emitUpdates) { + EmitUpdates_ = emitUpdates; +} + +std::string MetaCoreFileReplayRuntimeDataSourceAdapter::GetAdapterType() const { + return "file_replay"; +} + +bool MetaCoreFileReplayRuntimeDataSourceAdapter::Configure(const MetaCoreDataSourceDefinition& sourceDefinition) { + SourceDefinition_ = sourceDefinition; + Status_.SourceId = sourceDefinition.Id; + Status_.State = MetaCoreRuntimeDataSourceState::Disconnected; + Status_.LastConnectedAt = 0; + Status_.LastUpdateAt = 0; + Status_.LastError.clear(); + Frames_.clear(); + ReplayFilePath_.clear(); + NextFrameIndex_ = 0; + ElapsedSeconds_ = 0.0; + Sequence_ = 0; + + const std::string* replayFilePath = MetaCoreFindSetting(sourceDefinition, "file_path"); + if (replayFilePath == nullptr || replayFilePath->empty()) { + Status_.State = MetaCoreRuntimeDataSourceState::Faulted; + Status_.LastError = "Missing file_path connection setting"; + return false; + } + + ReplayFilePath_ = MetaCoreResolveReplayFilePath(*replayFilePath); + return true; +} + +bool MetaCoreFileReplayRuntimeDataSourceAdapter::Connect() { + Status_.State = MetaCoreRuntimeDataSourceState::Connecting; + if (!LoadReplayFile()) { + Status_.State = MetaCoreRuntimeDataSourceState::Faulted; + if (Status_.LastError.empty()) { + Status_.LastError = "Failed to load replay file"; + } + return false; + } + + Status_.State = MetaCoreRuntimeDataSourceState::Connected; + Status_.LastConnectedAt = 1; + Status_.LastError.clear(); + return true; +} + +void MetaCoreFileReplayRuntimeDataSourceAdapter::Disconnect() { + Status_.State = MetaCoreRuntimeDataSourceState::Disconnected; + Status_.LastError.clear(); + NextFrameIndex_ = 0; + ElapsedSeconds_ = 0.0; +} + +void MetaCoreFileReplayRuntimeDataSourceAdapter::Tick(double deltaSeconds) { + if (Status_.State != MetaCoreRuntimeDataSourceState::Connected && + Status_.State != MetaCoreRuntimeDataSourceState::Degraded) { + return; + } + + ElapsedSeconds_ += std::max(0.0, deltaSeconds); + if (Frames_.empty()) { + Status_.State = MetaCoreRuntimeDataSourceState::Degraded; + Status_.LastError = "Replay file has no frames"; + return; + } + + if (NextFrameIndex_ >= Frames_.size()) { + Status_.State = MetaCoreRuntimeDataSourceState::Degraded; + Status_.LastError = "Replay stream exhausted"; + } else { + Status_.State = MetaCoreRuntimeDataSourceState::Connected; + Status_.LastError.clear(); + } +} + +std::vector MetaCoreFileReplayRuntimeDataSourceAdapter::PollUpdates() { + if (Status_.State == MetaCoreRuntimeDataSourceState::Disconnected || + Status_.State == MetaCoreRuntimeDataSourceState::Faulted || + Frames_.empty()) { + return {}; + } + + std::vector updates; + while (NextFrameIndex_ < Frames_.size() && + Frames_[NextFrameIndex_].EmitAfterSeconds <= ElapsedSeconds_) { + MetaCoreRuntimeDataUpdate update = Frames_[NextFrameIndex_].Update; + update.Sequence = ++Sequence_; + update.Value.SourceTimestamp = static_cast(Frames_[NextFrameIndex_].EmitAfterSeconds * 1000.0); + Status_.LastUpdateAt = update.Value.SourceTimestamp; + updates.push_back(update); + ++NextFrameIndex_; + } + + if (NextFrameIndex_ >= Frames_.size() && updates.empty()) { + Status_.State = MetaCoreRuntimeDataSourceState::Degraded; + Status_.LastError = "Replay stream exhausted"; + } + + return updates; +} + +const MetaCoreRuntimeDataSourceStatus& MetaCoreFileReplayRuntimeDataSourceAdapter::GetStatus() const { + return Status_; +} + +bool MetaCoreFileReplayRuntimeDataSourceAdapter::LoadReplayFile() { + Frames_.clear(); + std::ifstream input(ReplayFilePath_); + if (!input) { + Status_.LastError = "Unable to open replay file: " + ReplayFilePath_; + return false; + } + + std::string line; + std::size_t lineNumber = 0; + while (std::getline(input, line)) { + ++lineNumber; + line = MetaCoreTrim(line); + if (line.empty() || line.front() == '#') { + continue; + } + + std::istringstream parser(line); + double emitAfterSeconds = 0.0; + std::string dataPointId; + std::string valueType; + if (!(parser >> emitAfterSeconds >> dataPointId >> valueType)) { + Status_.LastError = "Replay parse error at line " + std::to_string(lineNumber); + return false; + } + + MetaCoreRuntimeDataUpdate update; + update.DataPointId = dataPointId; + update.Sequence = 0; + std::string parseError; + if (!MetaCoreParseValuePayload(parser, valueType, update.Value, parseError)) { + Status_.LastError = "Replay " + parseError + " at line " + std::to_string(lineNumber); + return false; + } + + Frames_.push_back(ReplayFrame{ + emitAfterSeconds, + update + }); + } + + std::sort(Frames_.begin(), Frames_.end(), [](const ReplayFrame& lhs, const ReplayFrame& rhs) { + return lhs.EmitAfterSeconds < rhs.EmitAfterSeconds; + }); + return true; +} + +std::string MetaCoreTcpRuntimeDataSourceAdapter::GetAdapterType() const { + return "tcp"; +} + +bool MetaCoreTcpRuntimeDataSourceAdapter::Configure(const MetaCoreDataSourceDefinition& sourceDefinition) { + SourceDefinition_ = sourceDefinition; + Status_.SourceId = sourceDefinition.Id; + Status_.State = MetaCoreRuntimeDataSourceState::Disconnected; + Status_.LastConnectedAt = 0; + Status_.LastUpdateAt = 0; + Status_.LastError.clear(); + Host_ = "127.0.0.1"; + Port_ = 0; + Sequence_ = 0; + ElapsedSeconds_ = 0.0; + ReceiveBuffer_.clear(); + PendingUpdates_.clear(); + + if (const std::string* host = MetaCoreFindSetting(sourceDefinition, "host"); host != nullptr && !host->empty()) { + Host_ = *host; + } + const std::string* portValue = MetaCoreFindSetting(sourceDefinition, "port"); + if (portValue == nullptr || portValue->empty()) { + Status_.State = MetaCoreRuntimeDataSourceState::Faulted; + Status_.LastError = "Missing port connection setting"; + return false; + } + + try { + Port_ = static_cast(std::stoul(*portValue)); + } catch (...) { + Status_.State = MetaCoreRuntimeDataSourceState::Faulted; + Status_.LastError = "Invalid port connection setting"; + return false; + } + + return true; +} + +bool MetaCoreTcpRuntimeDataSourceAdapter::EnsureWinsockInitialized() { + static bool initialized = false; + if (initialized) { + return true; + } + + WSADATA wsaData{}; + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { + Status_.LastError = "WSAStartup failed"; + return false; + } + initialized = true; + return true; +} + +bool MetaCoreTcpRuntimeDataSourceAdapter::Connect() { + Status_.State = MetaCoreRuntimeDataSourceState::Connecting; + if (!EnsureWinsockInitialized()) { + Status_.State = MetaCoreRuntimeDataSourceState::Faulted; + return false; + } + + addrinfo hints{}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + addrinfo* result = nullptr; + const std::string portString = std::to_string(Port_); + if (getaddrinfo(Host_.c_str(), portString.c_str(), &hints, &result) != 0 || result == nullptr) { + Status_.State = MetaCoreRuntimeDataSourceState::Faulted; + Status_.LastError = "getaddrinfo failed"; + return false; + } + + SOCKET socketHandle = socket(result->ai_family, result->ai_socktype, result->ai_protocol); + if (socketHandle == MetaCoreInvalidSocket) { + freeaddrinfo(result); + Status_.State = MetaCoreRuntimeDataSourceState::Faulted; + Status_.LastError = "socket creation failed"; + return false; + } + + if (connect(socketHandle, result->ai_addr, static_cast(result->ai_addrlen)) == SOCKET_ERROR) { + closesocket(socketHandle); + freeaddrinfo(result); + Status_.State = MetaCoreRuntimeDataSourceState::Faulted; + Status_.LastError = "tcp connect failed"; + return false; + } + freeaddrinfo(result); + + u_long nonBlocking = 1; + ioctlsocket(socketHandle, FIONBIO, &nonBlocking); + SocketHandle_ = reinterpret_cast(socketHandle); + Status_.State = MetaCoreRuntimeDataSourceState::Connected; + Status_.LastConnectedAt = 1; + Status_.LastError.clear(); + return true; +} + +void MetaCoreTcpRuntimeDataSourceAdapter::Disconnect() { + if (SocketHandle_ != nullptr) { + closesocket(reinterpret_cast(SocketHandle_)); + SocketHandle_ = nullptr; + } + Status_.State = MetaCoreRuntimeDataSourceState::Disconnected; + Status_.LastError.clear(); + ReceiveBuffer_.clear(); + PendingUpdates_.clear(); +} + +void MetaCoreTcpRuntimeDataSourceAdapter::Tick(double deltaSeconds) { + ElapsedSeconds_ += std::max(0.0, deltaSeconds); + if (SocketHandle_ == nullptr || Status_.State == MetaCoreRuntimeDataSourceState::Faulted) { + return; + } + + SOCKET socketHandle = reinterpret_cast(SocketHandle_); + std::array buffer{}; + for (;;) { + const int received = recv(socketHandle, buffer.data(), static_cast(buffer.size()), 0); + if (received > 0) { + ReceiveBuffer_.append(buffer.data(), static_cast(received)); + Status_.State = MetaCoreRuntimeDataSourceState::Connected; + Status_.LastError.clear(); + continue; + } + if (received == 0) { + Status_.State = MetaCoreRuntimeDataSourceState::Degraded; + Status_.LastError = "tcp stream closed by peer"; + break; + } + + const int socketError = WSAGetLastError(); + if (socketError == WSAEWOULDBLOCK) { + break; + } + Status_.State = MetaCoreRuntimeDataSourceState::Faulted; + Status_.LastError = "tcp recv failed"; + break; + } + + for (;;) { + const std::size_t newlineIndex = ReceiveBuffer_.find('\n'); + if (newlineIndex == std::string::npos) { + break; + } + + std::string line = MetaCoreTrim(ReceiveBuffer_.substr(0, newlineIndex)); + ReceiveBuffer_.erase(0, newlineIndex + 1); + if (line.empty() || line.front() == '#') { + continue; + } + + MetaCoreRuntimeDataUpdate update; + if (!ParseSocketLine(line, update)) { + Status_.State = MetaCoreRuntimeDataSourceState::Degraded; + if (Status_.LastError.empty()) { + Status_.LastError = "tcp line parse failed"; + } + continue; + } + PendingUpdates_.push_back(std::move(update)); + } +} + +std::vector MetaCoreTcpRuntimeDataSourceAdapter::PollUpdates() { + if (PendingUpdates_.empty()) { + return {}; + } + + const std::uint64_t timestamp = static_cast(ElapsedSeconds_ * 1000.0); + for (auto& update : PendingUpdates_) { + update.Sequence = ++Sequence_; + update.Value.SourceTimestamp = timestamp; + } + Status_.LastUpdateAt = timestamp; + + std::vector updates = std::move(PendingUpdates_); + PendingUpdates_.clear(); + return updates; +} + +const MetaCoreRuntimeDataSourceStatus& MetaCoreTcpRuntimeDataSourceAdapter::GetStatus() const { + return Status_; +} + +bool MetaCoreTcpRuntimeDataSourceAdapter::ParseSocketLine(const std::string& line, MetaCoreRuntimeDataUpdate& update) { + std::istringstream parser(line); + std::string dataPointId; + std::string valueType; + if (!(parser >> dataPointId >> valueType)) { + Status_.LastError = "tcp line missing header fields"; + return false; + } + + update = MetaCoreRuntimeDataUpdate{}; + update.DataPointId = dataPointId; + std::string parseError; + if (!MetaCoreParseValuePayload(parser, valueType, update.Value, parseError)) { + Status_.LastError = "tcp " + parseError; + return false; + } + return true; +} + +std::unique_ptr MetaCoreCreateRuntimeDataSourceAdapter( + const std::string& adapterType +) { + if (adapterType == "mock") { + return std::make_unique(); + } + if (adapterType == "file_replay") { + return std::make_unique(); + } + if (adapterType == "tcp") { + return std::make_unique(); + } + return nullptr; +} + +} // namespace MetaCore diff --git a/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataDispatcher.h b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataDispatcher.h new file mode 100644 index 0000000..93e5dd3 --- /dev/null +++ b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataDispatcher.h @@ -0,0 +1,42 @@ +#pragma once + +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h" +#include "MetaCoreScene/MetaCoreScene.h" + +#include +#include +#include + +namespace MetaCore { + +class MetaCoreRuntimeDataDispatcher { +public: + explicit MetaCoreRuntimeDataDispatcher(MetaCoreScene& scene); + + void SetDataPointDefinitions(std::vector dataPointDefinitions); + void SetBindingDefinitions(std::vector bindingDefinitions); + + void ApplyUpdates(const std::vector& updates); + void TickStaleness(std::uint64_t currentTimestamp); + + [[nodiscard]] const std::vector& GetBindingStatuses() const; + [[nodiscard]] bool HasBindingFaults() const; + [[nodiscard]] MetaCoreRuntimeDiagnosticsSnapshot BuildDiagnosticsSnapshot( + const std::vector& sourceStatuses + ) const; + +private: + void RebuildBindingStatuses(); + void MarkBindingFault(const std::string& bindingId, const std::string& error); + void MarkBindingHealthy(const std::string& bindingId, std::uint64_t appliedAt); + const MetaCoreDataPointDefinition* FindDataPointDefinition(const std::string& dataPointId) const; + + MetaCoreScene& Scene_; + std::vector DataPointDefinitions_{}; + std::vector BindingDefinitions_{}; + std::vector BindingStatuses_{}; + std::unordered_map BindingStatusIndexById_{}; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h new file mode 100644 index 0000000..4c10b86 --- /dev/null +++ b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h @@ -0,0 +1,170 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreId.h" +#include "MetaCoreFoundation/MetaCoreReflection.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h" + +#include +#include +#include +#include + +namespace MetaCore { + +MC_STRUCT() +struct MetaCoreDataSourceSetting { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string Key{}; + + MC_PROPERTY() + std::string Value{}; +}; + +MC_STRUCT() +struct MetaCoreDataSourceDefinition { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string Id{}; + + MC_PROPERTY() + std::string AdapterType{}; + + MC_PROPERTY() + std::string DisplayName{}; + + MC_PROPERTY() + std::vector ConnectionSettings{}; + + MC_PROPERTY() + bool AutoConnect = true; + + MC_PROPERTY() + std::uint64_t ReconnectDelayMs = 1000; +}; + +MC_STRUCT() +struct MetaCoreDataPointDefinition { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string Id{}; + + MC_PROPERTY() + std::string SourceId{}; + + MC_PROPERTY() + std::string ExternalAddress{}; + + MC_PROPERTY() + MetaCoreRuntimeValueType ValueType = MetaCoreRuntimeValueType::Bool; +}; + +MC_STRUCT() +struct MetaCoreSceneBindingDefinition { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string BindingId{}; + + MC_PROPERTY() + std::string DataPointId{}; + + MC_PROPERTY() + MetaCoreId TargetObjectId = 0; + + MC_PROPERTY() + MetaCoreRuntimeBindingTarget Target = MetaCoreRuntimeBindingTarget::TransformPosition; + + MC_PROPERTY() + MetaCoreRuntimeMissingDataPolicy MissingDataPolicy = MetaCoreRuntimeMissingDataPolicy::KeepLastValue; +}; + +MC_STRUCT() +struct MetaCoreRuntimeDataSourcesDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::vector Sources{}; + + MC_PROPERTY() + std::vector DataPoints{}; +}; + +MC_STRUCT() +struct MetaCoreRuntimeBindingsDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::vector Bindings{}; +}; + +MC_STRUCT() +struct MetaCoreRuntimeProjectDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::filesystem::path StartupScenePath{}; + + MC_PROPERTY() + std::filesystem::path DataSourcesPath{}; + + MC_PROPERTY() + std::filesystem::path BindingsPath{}; + + MC_PROPERTY() + std::filesystem::path DiagnosticsPath{}; +}; + +[[nodiscard]] bool MetaCoreWriteRuntimeDataSourcesDocument( + const std::filesystem::path& path, + const MetaCoreRuntimeDataSourcesDocument& document, + const MetaCoreTypeRegistry& registry +); + +[[nodiscard]] bool MetaCoreWriteRuntimeBindingsDocument( + const std::filesystem::path& path, + const MetaCoreRuntimeBindingsDocument& document, + const MetaCoreTypeRegistry& registry +); + +[[nodiscard]] std::optional MetaCoreReadRuntimeDataSourcesDocument( + const std::filesystem::path& path, + const MetaCoreTypeRegistry& registry +); + +[[nodiscard]] std::optional MetaCoreReadRuntimeBindingsDocument( + const std::filesystem::path& path, + const MetaCoreTypeRegistry& registry +); + +[[nodiscard]] bool MetaCoreWriteRuntimeProjectDocument( + const std::filesystem::path& path, + const MetaCoreRuntimeProjectDocument& document, + const MetaCoreTypeRegistry& registry +); + +[[nodiscard]] std::optional MetaCoreReadRuntimeProjectDocument( + const std::filesystem::path& path, + const MetaCoreTypeRegistry& registry +); + +[[nodiscard]] bool MetaCoreWriteRuntimeDiagnosticsSnapshot( + const std::filesystem::path& path, + const MetaCoreRuntimeDiagnosticsSnapshot& snapshot, + const MetaCoreTypeRegistry& registry +); + +[[nodiscard]] std::optional MetaCoreReadRuntimeDiagnosticsSnapshot( + const std::filesystem::path& path, + const MetaCoreTypeRegistry& registry +); + +[[nodiscard]] std::vector MetaCoreValidateRuntimeDataDocuments( + const MetaCoreRuntimeDataSourcesDocument& sourcesDocument, + const MetaCoreRuntimeBindingsDocument& bindingsDocument +); + +} // namespace MetaCore diff --git a/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataSource.h b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataSource.h new file mode 100644 index 0000000..c1221e3 --- /dev/null +++ b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataSource.h @@ -0,0 +1,101 @@ +#pragma once + +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h" + +#include +#include +#include + +namespace MetaCore { + +class MetaCoreIRuntimeDataSourceAdapter { +public: + virtual ~MetaCoreIRuntimeDataSourceAdapter() = default; + + [[nodiscard]] virtual std::string GetAdapterType() const = 0; + virtual bool Configure(const MetaCoreDataSourceDefinition& sourceDefinition) = 0; + virtual bool Connect() = 0; + virtual void Disconnect() = 0; + virtual void Tick(double deltaSeconds) = 0; + [[nodiscard]] virtual std::vector PollUpdates() = 0; + [[nodiscard]] virtual const MetaCoreRuntimeDataSourceStatus& GetStatus() const = 0; +}; + +class MetaCoreMockRuntimeDataSourceAdapter final : public MetaCoreIRuntimeDataSourceAdapter { +public: + [[nodiscard]] std::string GetAdapterType() const override; + bool Configure(const MetaCoreDataSourceDefinition& sourceDefinition) override; + bool Connect() override; + void Disconnect() override; + void Tick(double deltaSeconds) override; + [[nodiscard]] std::vector PollUpdates() override; + [[nodiscard]] const MetaCoreRuntimeDataSourceStatus& GetStatus() const override; + + void SetEmitUpdates(bool emitUpdates); + +private: + MetaCoreDataSourceDefinition SourceDefinition_{}; + MetaCoreRuntimeDataSourceStatus Status_{}; + double ElapsedSeconds_ = 0.0; + std::uint64_t Sequence_ = 0; + bool EmitUpdates_ = true; +}; + +class MetaCoreFileReplayRuntimeDataSourceAdapter final : public MetaCoreIRuntimeDataSourceAdapter { +public: + [[nodiscard]] std::string GetAdapterType() const override; + bool Configure(const MetaCoreDataSourceDefinition& sourceDefinition) override; + bool Connect() override; + void Disconnect() override; + void Tick(double deltaSeconds) override; + [[nodiscard]] std::vector PollUpdates() override; + [[nodiscard]] const MetaCoreRuntimeDataSourceStatus& GetStatus() const override; + +private: + struct ReplayFrame { + double EmitAfterSeconds = 0.0; + MetaCoreRuntimeDataUpdate Update{}; + }; + + [[nodiscard]] bool LoadReplayFile(); + + MetaCoreDataSourceDefinition SourceDefinition_{}; + MetaCoreRuntimeDataSourceStatus Status_{}; + std::string ReplayFilePath_{}; + std::vector Frames_{}; + std::size_t NextFrameIndex_ = 0; + double ElapsedSeconds_ = 0.0; + std::uint64_t Sequence_ = 0; +}; + +class MetaCoreTcpRuntimeDataSourceAdapter final : public MetaCoreIRuntimeDataSourceAdapter { +public: + [[nodiscard]] std::string GetAdapterType() const override; + bool Configure(const MetaCoreDataSourceDefinition& sourceDefinition) override; + bool Connect() override; + void Disconnect() override; + void Tick(double deltaSeconds) override; + [[nodiscard]] std::vector PollUpdates() override; + [[nodiscard]] const MetaCoreRuntimeDataSourceStatus& GetStatus() const override; + +private: + [[nodiscard]] bool EnsureWinsockInitialized(); + [[nodiscard]] bool ParseSocketLine(const std::string& line, MetaCoreRuntimeDataUpdate& update); + + MetaCoreDataSourceDefinition SourceDefinition_{}; + MetaCoreRuntimeDataSourceStatus Status_{}; + std::string Host_{"127.0.0.1"}; + std::uint16_t Port_ = 0; + void* SocketHandle_ = nullptr; + std::string ReceiveBuffer_{}; + std::vector PendingUpdates_{}; + std::uint64_t Sequence_ = 0; + double ElapsedSeconds_ = 0.0; +}; + +[[nodiscard]] std::unique_ptr MetaCoreCreateRuntimeDataSourceAdapter( + const std::string& adapterType +); + +} // namespace MetaCore diff --git a/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h new file mode 100644 index 0000000..8c72ba6 --- /dev/null +++ b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h @@ -0,0 +1,174 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreId.h" +#include "MetaCoreFoundation/MetaCoreReflection.h" + +#include +#include + +#include + +namespace MetaCore { + +MC_ENUM() +enum class MetaCoreRuntimeValueType : std::uint32_t { + Bool = 0, + Int64, + Double, + String, + Vec3 +}; + +MC_ENUM() +enum class MetaCoreRuntimeDataQuality : std::uint32_t { + Good = 0, + Uncertain, + Bad +}; + +MC_ENUM() +enum class MetaCoreRuntimeDataSourceState : std::uint32_t { + Disconnected = 0, + Connecting, + Connected, + Degraded, + Faulted +}; + +MC_ENUM() +enum class MetaCoreRuntimeBindingTarget : std::uint32_t { + TransformPosition = 0, + MeshRendererVisible, + MeshRendererBaseColor, + LightIntensity, + LightColor +}; + +MC_ENUM() +enum class MetaCoreRuntimeMissingDataPolicy : std::uint32_t { + KeepLastValue = 0, + ResetToDefault, + MarkFaultOnly +}; + +MC_STRUCT() +struct MetaCoreRuntimeDataValue { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreRuntimeValueType Type = MetaCoreRuntimeValueType::Bool; + + MC_PROPERTY() + bool BoolValue = false; + + MC_PROPERTY() + std::int64_t Int64Value = 0; + + MC_PROPERTY() + double DoubleValue = 0.0; + + MC_PROPERTY() + std::string StringValue{}; + + MC_PROPERTY() + glm::vec3 Vec3Value{0.0F, 0.0F, 0.0F}; + + MC_PROPERTY() + std::uint64_t SourceTimestamp = 0; + + MC_PROPERTY() + MetaCoreRuntimeDataQuality Quality = MetaCoreRuntimeDataQuality::Good; +}; + +MC_STRUCT() +struct MetaCoreRuntimeDataUpdate { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string DataPointId{}; + + MC_PROPERTY() + MetaCoreRuntimeDataValue Value{}; + + MC_PROPERTY() + std::uint64_t Sequence = 0; +}; + +MC_STRUCT() +struct MetaCoreRuntimeDataSourceStatus { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string SourceId{}; + + MC_PROPERTY() + MetaCoreRuntimeDataSourceState State = MetaCoreRuntimeDataSourceState::Disconnected; + + MC_PROPERTY() + std::uint64_t LastConnectedAt = 0; + + MC_PROPERTY() + std::uint64_t LastUpdateAt = 0; + + MC_PROPERTY() + std::string LastError{}; +}; + +MC_STRUCT() +struct MetaCoreRuntimeBindingStatus { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string BindingId{}; + + MC_PROPERTY() + bool Healthy = true; + + MC_PROPERTY() + bool Stale = false; + + MC_PROPERTY() + std::uint64_t LastAppliedAt = 0; + + MC_PROPERTY() + std::uint64_t StaleAfterMs = 1000; + + MC_PROPERTY() + std::string LastError{}; +}; + +MC_STRUCT() +struct MetaCoreRuntimeDiagnosticsSnapshot { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::vector SourceStatuses{}; + + MC_PROPERTY() + std::vector BindingStatuses{}; + + MC_PROPERTY() + bool HasFaults = false; +}; + +MC_ENUM() +enum class MetaCoreRuntimeConfigIssueSeverity : std::uint32_t { + Warning = 0, + Error +}; + +MC_STRUCT() +struct MetaCoreRuntimeConfigIssue { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreRuntimeConfigIssueSeverity Severity = MetaCoreRuntimeConfigIssueSeverity::Warning; + + MC_PROPERTY() + std::string Scope{}; + + MC_PROPERTY() + std::string Message{}; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreScene/Private/MetaCoreScene.cpp b/Source/MetaCoreScene/Private/MetaCoreScene.cpp index 9a12696..9f39abb 100644 --- a/Source/MetaCoreScene/Private/MetaCoreScene.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreScene.cpp @@ -407,6 +407,23 @@ MetaCoreScene MetaCoreCreateDefaultScene() { cube.MeshRenderer = MetaCoreMeshRendererComponent{}; cube.Transform.Position = glm::vec3(0.0F, 0.5F, 0.0F); + MetaCoreGameObject& valve = scene.CreateGameObject("Valve"); + valve.MeshRenderer = MetaCoreMeshRendererComponent{}; + valve.Transform.Position = glm::vec3(-2.2F, 0.5F, 0.0F); + valve.MeshRenderer->BaseColor = glm::vec3(0.85F, 0.45F, 0.20F); + + MetaCoreGameObject& tank = scene.CreateGameObject("Tank"); + tank.MeshRenderer = MetaCoreMeshRendererComponent{}; + tank.Transform.Position = glm::vec3(2.2F, 0.5F, 0.0F); + tank.Transform.Scale = glm::vec3(1.4F, 1.4F, 1.4F); + tank.MeshRenderer->BaseColor = glm::vec3(0.35F, 0.65F, 0.90F); + + MetaCoreGameObject& alarmBeacon = scene.CreateGameObject("Alarm Beacon"); + alarmBeacon.Light = MetaCoreLightComponent{}; + alarmBeacon.Light->Intensity = 0.0F; + alarmBeacon.Light->Color = glm::vec3(1.0F, 0.15F, 0.1F); + alarmBeacon.Transform.Position = glm::vec3(0.0F, 2.5F, 0.0F); + return scene; } diff --git a/Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp b/Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp new file mode 100644 index 0000000..bed1571 --- /dev/null +++ b/Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp @@ -0,0 +1,167 @@ +#include "MetaCoreScene/MetaCoreScenePackage.h" + +#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" +#include "MetaCoreFoundation/MetaCoreHash.h" +#include "MetaCoreFoundation/MetaCorePackage.h" + +#include +#include + +namespace MetaCore { + +namespace { + +[[nodiscard]] MetaCorePackageDocument MetaCoreBuildTypedScenePackage( + const MetaCoreAssetGuid& assetGuid, + const std::string& objectName, + std::vector payload +) { + MetaCorePackageDocument document; + document.Header.PackageType = MetaCorePackageType::Scene; + document.Header.PackageGuid = assetGuid; + document.Header.SourceHash = MetaCoreHashBytes(std::span(payload.data(), payload.size())); + document.NameTable = {objectName, "MetaCoreSceneDocument"}; + document.Exports.push_back(MetaCoreExportEntry{ + assetGuid, + objectName, + MetaCoreMakeTypeId("MetaCoreSceneDocument"), + 0, + static_cast(payload.size()) + }); + document.PayloadSections.push_back(std::move(payload)); + return document; +} + +template +[[nodiscard]] std::optional MetaCoreReadTypedPayload( + const MetaCorePackageDocument& document, + const MetaCoreTypeRegistry& registry, + std::string_view expectedTypeName +) { + if (document.Exports.empty() || document.PayloadSections.empty()) { + return std::nullopt; + } + + const MetaCoreExportEntry& exportEntry = document.Exports.front(); + if (exportEntry.TypeId != MetaCoreMakeTypeId(expectedTypeName) || + exportEntry.PayloadIndex >= document.PayloadSections.size()) { + return std::nullopt; + } + + T payload{}; + if (!MetaCoreDeserializeFromBytes(document.PayloadSections[exportEntry.PayloadIndex], payload, registry)) { + return std::nullopt; + } + return payload; +} + +[[nodiscard]] std::optional MetaCoreFindJsonStringValue(std::string_view json, std::string_view key) { + const std::string needle = "\"" + std::string(key) + "\""; + std::size_t keyPosition = json.find(needle); + if (keyPosition == std::string_view::npos) { + return std::nullopt; + } + + std::size_t colonPosition = json.find(':', keyPosition + needle.size()); + if (colonPosition == std::string_view::npos) { + return std::nullopt; + } + + std::size_t firstQuote = json.find('"', colonPosition + 1); + if (firstQuote == std::string_view::npos) { + return std::nullopt; + } + + std::size_t secondQuote = firstQuote + 1; + while (secondQuote < json.size()) { + secondQuote = json.find('"', secondQuote); + if (secondQuote == std::string_view::npos) { + return std::nullopt; + } + if (secondQuote == firstQuote + 1 || json[secondQuote - 1] != '\\') { + break; + } + ++secondQuote; + } + + return std::string(json.substr(firstQuote + 1, secondQuote - firstQuote - 1)); +} + +} // namespace + +MetaCoreTypeRegistry MetaCoreBuildScenePackageTypeRegistry() { + MetaCoreTypeRegistry registry; + MetaCoreRegisterFoundationGeneratedTypes(registry); + MetaCoreRegisterSceneGeneratedTypes(registry); + return registry; +} + +bool MetaCoreWriteScenePackage( + const std::filesystem::path& path, + const MetaCoreSceneDocument& document +) { + const MetaCoreTypeRegistry registry = MetaCoreBuildScenePackageTypeRegistry(); + const auto payload = MetaCoreSerializeToBytes(document, registry); + if (!payload.has_value()) { + return false; + } + + const std::string objectName = path.stem().string().empty() ? "Scene" : path.stem().string(); + MetaCorePackageDocument package = MetaCoreBuildTypedScenePackage( + MetaCoreAssetGuid::Generate(), + objectName, + *payload + ); + return MetaCoreWritePackageFile(path, std::move(package), registry); +} + +std::optional MetaCoreReadScenePackage(const std::filesystem::path& path) { + const MetaCoreTypeRegistry registry = MetaCoreBuildScenePackageTypeRegistry(); + const auto package = MetaCoreReadPackageFile(path, registry); + if (!package.has_value() || package->Header.PackageType != MetaCorePackageType::Scene) { + return std::nullopt; + } + return MetaCoreReadTypedPayload(*package, registry, "MetaCoreSceneDocument"); +} + +std::optional MetaCoreResolveStartupScenePackagePath(const std::filesystem::path& projectFilePath) { + std::ifstream input(projectFilePath, std::ios::binary); + if (!input.is_open()) { + return std::nullopt; + } + + std::ostringstream buffer; + buffer << input.rdbuf(); + const auto startupSceneValue = MetaCoreFindJsonStringValue(buffer.str(), "startup_scene"); + if (!startupSceneValue.has_value() || startupSceneValue->empty()) { + return std::nullopt; + } + + const std::filesystem::path relativeStartupScenePath = std::filesystem::path(*startupSceneValue).lexically_normal(); + const std::filesystem::path projectRoot = projectFilePath.parent_path(); + + std::vector candidates; + if (relativeStartupScenePath.extension() == ".json") { + candidates.push_back((projectRoot / relativeStartupScenePath).replace_extension("")); + } + candidates.push_back(projectRoot / relativeStartupScenePath); + + for (const std::filesystem::path& candidate : candidates) { + std::error_code errorCode; + if (std::filesystem::exists(candidate, errorCode)) { + return std::filesystem::weakly_canonical(candidate, errorCode); + } + } + + return (projectRoot / relativeStartupScenePath).lexically_normal(); +} + +std::optional MetaCoreLoadStartupSceneDocument(const std::filesystem::path& projectFilePath) { + const auto startupScenePath = MetaCoreResolveStartupScenePackagePath(projectFilePath); + if (!startupScenePath.has_value()) { + return std::nullopt; + } + return MetaCoreReadScenePackage(*startupScenePath); +} + +} // namespace MetaCore diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h index 6c594cf..d060d7f 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h @@ -1,5 +1,7 @@ #pragma once +#include "MetaCoreFoundation/MetaCoreReflection.h" + #include #include @@ -7,36 +9,61 @@ namespace MetaCore { +MC_ENUM() // 定义第一阶段内置的网格类型。 enum class MetaCoreBuiltinMeshType { Cube }; +MC_STRUCT() // 表示 Unity 风格对象的变换组件。 struct MetaCoreTransformComponent { + MC_GENERATED_BODY() + + MC_PROPERTY() glm::vec3 Position{0.0F, 0.0F, 0.0F}; + MC_PROPERTY() glm::vec3 RotationEulerDegrees{0.0F, 0.0F, 0.0F}; + MC_PROPERTY() glm::vec3 Scale{1.0F, 1.0F, 1.0F}; }; +MC_STRUCT() // 表示场景中用于观察的摄像机组件。 struct MetaCoreCameraComponent { + MC_GENERATED_BODY() + + MC_PROPERTY() float FieldOfViewDegrees = 60.0F; + MC_PROPERTY() float NearClip = 0.1F; + MC_PROPERTY() float FarClip = 100.0F; + MC_PROPERTY() bool IsPrimary = false; }; +MC_STRUCT() // 表示一个最小可用的网格渲染组件。 struct MetaCoreMeshRendererComponent { + MC_GENERATED_BODY() + + MC_PROPERTY() MetaCoreBuiltinMeshType BuiltinMesh = MetaCoreBuiltinMeshType::Cube; + MC_PROPERTY() glm::vec3 BaseColor{0.75F, 0.78F, 0.84F}; + MC_PROPERTY() bool Visible = true; }; +MC_STRUCT() // 表示第一阶段场景中的方向光组件。 struct MetaCoreLightComponent { + MC_GENERATED_BODY() + + MC_PROPERTY() glm::vec3 Color{1.0F, 1.0F, 1.0F}; + MC_PROPERTY() float Intensity = 1.5F; }; diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h index d406ed2..59460ef 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h @@ -1,6 +1,8 @@ #pragma once #include "MetaCoreFoundation/MetaCoreId.h" +#include "MetaCoreFoundation/MetaCoreAssetGuid.h" +#include "MetaCoreFoundation/MetaCoreReflection.h" #include "MetaCoreScene/MetaCoreComponents.h" #include @@ -8,15 +10,41 @@ namespace MetaCore { +MC_STRUCT() +struct MetaCorePrefabInstanceMetadata { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid PrefabAssetGuid{}; + + MC_PROPERTY() + MetaCoreId PrefabObjectId = 0; + + MC_PROPERTY() + MetaCoreId PrefabInstanceRootId = 0; +}; + +MC_STRUCT() // 表示一个 Unity 风格的场景对象。 struct MetaCoreGameObject { + MC_GENERATED_BODY() + + MC_PROPERTY() MetaCoreId Id = 0; + MC_PROPERTY() MetaCoreId ParentId = 0; + MC_PROPERTY() std::string Name; + MC_PROPERTY() MetaCoreTransformComponent Transform{}; + MC_PROPERTY() std::optional Camera; + MC_PROPERTY() std::optional MeshRenderer; + MC_PROPERTY() std::optional Light; + MC_PROPERTY() + std::optional PrefabInstance; }; } // namespace MetaCore diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h index 54dbd83..29379e0 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h @@ -1,12 +1,18 @@ #pragma once +#include "MetaCoreFoundation/MetaCoreReflection.h" +#include "MetaCoreScene/MetaCoreSceneDocument.h" #include "MetaCoreScene/MetaCoreGameObject.h" #include namespace MetaCore { +MC_STRUCT() struct MetaCoreSceneSnapshot { + MC_GENERATED_BODY() + + MC_PROPERTY() std::vector GameObjects; }; diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h new file mode 100644 index 0000000..8a2f636 --- /dev/null +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h @@ -0,0 +1,40 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreId.h" +#include "MetaCoreFoundation/MetaCoreReflection.h" +#include "MetaCoreScene/MetaCoreGameObject.h" + +#include +#include + +namespace MetaCore { + +MC_STRUCT() +struct MetaCoreEditorSelectionSnapshot { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::vector SelectedObjectIds{}; + + MC_PROPERTY() + MetaCoreId ActiveObjectId = 0; + + MC_PROPERTY() + MetaCoreId SelectionAnchorId = 0; +}; + +MC_STRUCT() +struct MetaCoreSceneDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string Name{}; + + MC_PROPERTY() + std::vector GameObjects{}; + + MC_PROPERTY() + MetaCoreEditorSelectionSnapshot Selection{}; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScenePackage.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScenePackage.h new file mode 100644 index 0000000..fc3ed8a --- /dev/null +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScenePackage.h @@ -0,0 +1,29 @@ +#pragma once + +#include "MetaCoreScene/MetaCoreSceneDocument.h" + +#include +#include + +namespace MetaCore { + +[[nodiscard]] MetaCoreTypeRegistry MetaCoreBuildScenePackageTypeRegistry(); + +[[nodiscard]] bool MetaCoreWriteScenePackage( + const std::filesystem::path& path, + const MetaCoreSceneDocument& document +); + +[[nodiscard]] std::optional MetaCoreReadScenePackage( + const std::filesystem::path& path +); + +[[nodiscard]] std::optional MetaCoreResolveStartupScenePackagePath( + const std::filesystem::path& projectFilePath +); + +[[nodiscard]] std::optional MetaCoreLoadStartupSceneDocument( + const std::filesystem::path& projectFilePath +); + +} // namespace MetaCore diff --git a/TestProject/Assets/Materials/default_pbr.material.json.meta b/TestProject/Assets/Materials/default_pbr.material.json.meta index 8bcf0f2..abe2c29 100644 --- a/TestProject/Assets/Materials/default_pbr.material.json.meta +++ b/TestProject/Assets/Materials/default_pbr.material.json.meta @@ -1,5 +1,5 @@ { - "id": "00005052332aa8d4625dd7775e94e6d800000005", + "id": "0000573e4df2db1c0fff4788060abca900000005", "relative_path": "Assets/Materials/default_pbr.material.json", "type": "material" } \ No newline at end of file diff --git a/TestProject/Assets/UI/main_menu.rcss.meta b/TestProject/Assets/UI/main_menu.rcss.meta index 7fa65bc..3d1e6f0 100644 --- a/TestProject/Assets/UI/main_menu.rcss.meta +++ b/TestProject/Assets/UI/main_menu.rcss.meta @@ -1,5 +1,5 @@ { - "id": "00005052332a9f74171e39271225dd0800000004", + "id": "0000573e4df29210cf02d084bb56a04a00000004", "relative_path": "Assets/UI/main_menu.rcss", "type": "ui_stylesheet" } \ No newline at end of file diff --git a/TestProject/Assets/UI/main_menu.rml.meta b/TestProject/Assets/UI/main_menu.rml.meta index 9001f47..0c462f5 100644 --- a/TestProject/Assets/UI/main_menu.rml.meta +++ b/TestProject/Assets/UI/main_menu.rml.meta @@ -1,5 +1,5 @@ { - "id": "00005052332a822841724324849eccb900000003", + "id": "0000573e4df220c85f0baaf5dd5242f900000003", "relative_path": "Assets/UI/main_menu.rml", "type": "ui_document" } \ No newline at end of file diff --git a/TestProject/Library/AssetDB.json b/TestProject/Library/AssetDB.json index 872f886..c5d5378 100644 --- a/TestProject/Library/AssetDB.json +++ b/TestProject/Library/AssetDB.json @@ -1,17 +1,17 @@ { "records": [ { - "id": "00005052332a822841724324849eccb900000003", + "id": "0000573e4df220c85f0baaf5dd5242f900000003", "relative_path": "Assets/UI/main_menu.rml", "type": "ui_document" }, { - "id": "00005052332a9f74171e39271225dd0800000004", + "id": "0000573e4df29210cf02d084bb56a04a00000004", "relative_path": "Assets/UI/main_menu.rcss", "type": "ui_stylesheet" }, { - "id": "00005052332aa8d4625dd7775e94e6d800000005", + "id": "0000573e4df2db1c0fff4788060abca900000005", "relative_path": "Assets/Materials/default_pbr.material.json", "type": "material" } diff --git a/TestProject/MetaCore.project.json b/TestProject/MetaCore.project.json index ad7e563..1eef1c7 100644 --- a/TestProject/MetaCore.project.json +++ b/TestProject/MetaCore.project.json @@ -1,8 +1,8 @@ { "name": "MetaCoreTest", "scenes": [ - "Scenes/Main.mcscene.json" + "Scenes/Main.mcscene" ], - "startup_scene": "Scenes/Main.mcscene.json", + "startup_scene": "Scenes/Main.mcscene", "version": "0.1.0" -} \ No newline at end of file +} diff --git a/TestProject/Runtime/Bindings.mcruntime b/TestProject/Runtime/Bindings.mcruntime new file mode 100644 index 0000000..92a18ba Binary files /dev/null and b/TestProject/Runtime/Bindings.mcruntime differ diff --git a/TestProject/Runtime/DataSources.mcruntime b/TestProject/Runtime/DataSources.mcruntime new file mode 100644 index 0000000..25dc485 Binary files /dev/null and b/TestProject/Runtime/DataSources.mcruntime differ diff --git a/TestProject/Runtime/ProjectRuntime.mcruntimecfg b/TestProject/Runtime/ProjectRuntime.mcruntimecfg new file mode 100644 index 0000000..8d590d8 Binary files /dev/null and b/TestProject/Runtime/ProjectRuntime.mcruntimecfg differ diff --git a/TestProject/Runtime/RuntimeReplay.mcstream b/TestProject/Runtime/RuntimeReplay.mcstream new file mode 100644 index 0000000..8b52118 --- /dev/null +++ b/TestProject/Runtime/RuntimeReplay.mcstream @@ -0,0 +1,17 @@ +# emit_after_seconds data_point_id value_type payload +0.00 cube.position vec3 0.0 0.5 0.0 +0.00 cube.visible bool true +0.00 cube.base_color vec3 0.4 0.6 0.9 +0.00 valve.visible bool true +0.00 tank.base_color vec3 0.35 0.65 0.90 +0.00 alarm.intensity double 0.0 +0.50 cube.position vec3 1.5 0.5 0.0 +0.50 cube.base_color vec3 0.8 0.6 0.5 +0.50 tank.base_color vec3 0.20 0.80 0.25 +0.50 alarm.intensity double 2.0 +1.00 cube.visible bool false +1.00 valve.visible bool false +1.50 cube.position vec3 -1.5 0.5 0.0 +1.50 cube.visible bool true +1.50 valve.visible bool true +1.50 alarm.intensity double 0.0 diff --git a/TestProject/Scenes/Main.mcscene b/TestProject/Scenes/Main.mcscene new file mode 100644 index 0000000..bf92b41 Binary files /dev/null and b/TestProject/Scenes/Main.mcscene differ diff --git a/TestProject/Scenes/Main.mcscene.json b/TestProject/Scenes/Main.mcscene.json index c888922..c2688eb 100644 --- a/TestProject/Scenes/Main.mcscene.json +++ b/TestProject/Scenes/Main.mcscene.json @@ -13,7 +13,7 @@ "locked": false, "selected": false }, - "id": "0000505233216b341f8e0361195336fe00000001", + "id": "0000573e4dab34ec66e90e17c04d1c5d00000001", "name": "Main Camera", "parent_id": "", "transform": { @@ -40,7 +40,7 @@ "locked": false, "selected": false }, - "id": "00005052332197bcd649b574240bc6be00000002", + "id": "0000573e4dc9f2d82e684c2197ceab9a00000002", "light": { "color": [ 1, diff --git a/TestProject/Scenes/Test.mcscene.json b/TestProject/Scenes/Test.mcscene.json index f29431f..c4b201d 100644 --- a/TestProject/Scenes/Test.mcscene.json +++ b/TestProject/Scenes/Test.mcscene.json @@ -13,7 +13,7 @@ "locked": false, "selected": false }, - "id": "00005052333cdf54bd42defb639e412c00000006", + "id": "0000573e4e1b3d64d8ce209463b7208500000006", "name": "Main Camera", "parent_id": "", "transform": { @@ -40,7 +40,7 @@ "locked": false, "selected": false }, - "id": "00005052333ceef49f76de9c4d0fd70200000007", + "id": "0000573e4e1b84e073179b24ce5ac48c00000007", "light": { "color": [ 1, @@ -78,7 +78,7 @@ "locked": false, "selected": false }, - "id": "00005052333cf6c426b72f1a10f785ee00000008", + "id": "0000573e4e1bbde8c5e3ba6a189478bd00000008", "mesh_renderer": { "material_asset_id": "", "mesh_asset_id": "mesh.cube", diff --git a/docs/designs/metacore-engine-foundation-priority.md b/docs/designs/metacore-engine-foundation-priority.md new file mode 100644 index 0000000..be0bf29 --- /dev/null +++ b/docs/designs/metacore-engine-foundation-priority.md @@ -0,0 +1,363 @@ +# MetaCore 引擎基础能力优先级 + +生成时间:2026-03-28 +状态:草案 +读者:产品、架构、引擎、编辑器、交付 + +## 目的 + +这份文档用于纠正当前第一阶段的开发顺序。 + +核心结论很简单: + +**MetaCore 第一阶段应该先成为一个可用于构建工业三维应用的可用引擎和编辑器,然后再叠加数字孪生特有的运行时数据能力。** + +运行时数据很重要,但它不是引擎主干。 + +## 执行摘要 + +当前项目在 RuntimeData 方向已经有了不少有效进展: + +- 独立的运行时数据模块 +- 二进制运行时配置 +- 多种适配器 +- 编辑器侧 binding 配置 +- Player 侧数据分发 + +这些工作都应该保留。 + +但它不应该继续定义第一阶段的关键路径。 + +如果 MetaCore 的目标是“像 Unity / Unreal 一样,用于开发工业数字孪生项目的引擎”,那第一阶段的正确顺序应该是: + +1. 项目与内容工作流 +2. 资源与模型导入 +3. 场景编辑与层级 +4. 材质与光照 +5. 场景持久化 +6. UI 工作流 +7. 打包与运行时 +8. RuntimeData 集成 + +## 为什么顺序这么重要 + +数字孪生团队不会先从把一个点位绑定到对象开始。 + +他们会先做这些事: + +- 创建项目 +- 导入模型 +- 组织场景层级 +- 调整变换 +- 指派材质 +- 设置灯光与相机 +- 搭界面 +- 保存场景 +- 打包一个可运行客户端 + +只有这些步骤成立之后,运行时数据才真正有价值。 + +如果引擎不能顺畅支撑这些基础步骤,那 RuntimeData 只能驱动一个薄 demo,而不是可交付流程。 + +## 专业判断 + +从产品和工程两个视角看,第一阶段的正确定义应该是: + +**MetaCore 第一阶段是一个具备项目、资源、场景、材质、光照、UI、持久化和打包工作流的工业级引擎/编辑器底座,并在其上提供第一版 RuntimeData 子系统用于数字孪生集成。** + +它不是: + +- 一个数据驱动 demo 壳 +- 一个完整数字孪生平台 +- 一个完整 VR 训练平台 + +## 第一阶段的正确开发顺序 + +### Phase 1A:引擎生产力工作流 + +这是新的主干。 + +必须包含: + +- 项目创建与打开 +- 启动场景行为 +- 资源导入与资源数据库 +- 模型导入与层级保留 +- 场景层级编辑 +- Transform 编辑与 Gizmo +- 材质创建、复用与指派 +- 基础光照 +- 场景保存/打开 +- UI 基础编辑 +- 打包与独立运行时 + +### Phase 1B:数字孪生使能 + +这部分建立在引擎工作流之上。 + +必须包含: + +- 运行时数据源抽象 +- 数据点模型 +- 场景 binding 模型 +- 运行时诊断 +- 一个或多个 live adapter + +## 优先级模型 + +### P0:现在必须立刻抬高优先级 + +这些才是真正的引擎阻塞项。 + +- 项目系统 +- 模型导入 +- 场景层级编辑 +- Transform 工具与 Gizmo +- 材质系统 +- 光照基础 +- 场景保存/打开 +- 打包与独立运行时 + +### P1:紧随 P0 + +- Project 面板与资源管理 +- UI 基础编辑 +- prefab 强化 +- 相机与视口工作流 +- 资源刷新与重新导入稳定性 +- 运行时诊断可见性 + +### P2:重要,但应晚于引擎底座 + +- RuntimeData 集成 +- 数字孪生样板模板 +- 更多 live adapter +- 行业化运行时组件 + +### P3:后续扩展 + +- VR/XR 工作流 +- 浏览器嵌入产品化 +- AI 辅助工具 +- 仿真编排 + +## 首批推荐能力集合 + +### 1. 项目系统 + +必须支持: + +- 创建/打开项目 +- 稳定的项目目录布局 +- 启动场景 +- 项目描述文件持久化 + +为什么重要: + +- 这是所有后续工作流的入口 + +### 2. 模型导入 + +必须支持: + +- 一等支持 `glTF/.glb` +- 第二阶段支持 `FBX` +- 可选支持 `OBJ` + +建议: + +- 把 `glTF/.glb` 做成第一优先的生产级导入器 + +原因: + +- 现代、清晰、适合可控工具链 +- 比 FBX-first 路线更可预测 +- 足够覆盖早期很多工业可视化场景 + +### 3. 场景编辑与层级 + +必须支持: + +- 创建/删除/复制 +- 重命名 +- 父子级 +- 拖拽重挂接 +- 多选 +- 稳定的选择行为 + +### 4. Transform 工具 + +必须支持: + +- 移动 +- 旋转 +- 缩放 +- 本地/世界坐标模式 +- 基础吸附 + +没有这套东西,编辑器就谈不上生产可用。 + +### 5. 材质系统 + +必须支持: + +- 材质资源 +- 基础色 +- 纹理槽 +- 基础 PBR 参数 +- 材质复用 + +### 6. 光照 + +必须支持: + +- Directional Light +- Point Light +- Spot Light +- 稳定的场景照明 +- 条件允许时补基础阴影 + +### 7. 场景持久化 + +必须支持: + +- 新建场景 +- 保存 +- 另存 +- 重新打开 +- 启动场景加载 + +对这个仓库来说,二进制 package 持久化是正确方向。 + +### 8. UI 工作流 + +必须支持: + +- 静态面板 +- 文本 +- 图片 +- 按钮 +- 基础布局 +- UI 资源引用 + +这是数字孪生项目的硬需求。 + +### 9. 打包 + +必须支持: + +- 独立 Player +- 启动场景接管 +- 项目内容打包带出 +- clean machine 运行验证 + +## 当前思维模型里的遗漏项 + +下面这些点很容易被低估,但它们都属于核心引擎工作。 + +### 资源管理 + +导入远远不够。 + +还需要: + +- Project 面板 +- 元数据可见 +- 重命名/移动行为 +- 重新导入行为 +- package 引用稳定性 + +### 视口工作流 + +一个 3D 编辑器必须具备: + +- 稳定的 orbit/fly 导航 +- 聚焦选中对象 +- 明确的相机行为 +- 编辑器视角和运行时视角分离 + +### Prefab/模板工作流 + +工业场景会重复大量对象: + +- 阀门 +- 罐体 +- 指示器 +- 告警灯 +- 设备 + +没有 prefab/template 复用,内容生产效率会很差。 + +### 导入归一化 + +一个真正可用的导入器最终必须处理: + +- 坐标系归一化 +- 缩放归一化 +- 网格/材质映射 +- 层级保留 +- 重新导入一致性 + +### 诊断与错误可见性 + +交付团队需要清晰的失败面: + +- 导入失败 +- 资源缺失 +- 场景无法读取 +- runtime config 不匹配 +- binding 失败 + +### 扩展点 + +即使 VR 或脚本系统还没完成,引擎也应该先预留: + +- 应用模块挂点 +- 运行时组件扩展点 +- 导入器扩展点 +- adapter 扩展点 + +## RuntimeData 现有工作哪些仍然有效 + +当前已经做的 RuntimeData 工作不是错误。 + +应该保留,但重新归类: + +- 它不是第一阶段主干 +- 它是 Phase 1B 子系统 +- 它在一些方向上已经超前于部分基础层 + +这其实是有价值的: + +- 它证明了引擎已经能承载一个行业化子系统 +- 它提前打通了未来的数据路径 +- 一旦内容工作流更稳,它就能直接复用 + +## 修正后的里程碑形态 + +旧的里程碑顺序让 RuntimeData 过早主导节奏。 + +修正后的顺序应该是: + +```text +M1 引擎壳与项目循环 + -> M2 资源与模型导入循环 + -> M3 场景编辑、材质与光照工作流 + -> M4 UI、持久化与打包循环 + -> M5 面向数字孪生的 RuntimeData 集成 + -> M6 硬化与未来预留 +``` + +## 最终建议 + +MetaCore 应该先被做成: + +**一个可用的工业三维引擎/编辑器** + +然后再做成: + +**一个具备数字孪生能力的引擎** + +顺序不能反。 + +如果不强制执行这个顺序,项目会继续滑向“行业功能优先”,而不是“引擎基础能力优先”。 diff --git a/docs/designs/metacore-phase1-implementation-plan.md b/docs/designs/metacore-phase1-implementation-plan.md new file mode 100644 index 0000000..adf4f45 --- /dev/null +++ b/docs/designs/metacore-phase1-implementation-plan.md @@ -0,0 +1,307 @@ +# MetaCore 第一阶段实施计划 + +生成时间:2026-03-27 +状态:草案 +范围:第一阶段执行 + +## 目的 + +这份文档把第一阶段范围翻译成实施路线图。 + +它要回答: + +- 先做什么 +- 谁依赖谁 +- 每个里程碑在证明什么 +- 当前真正应该推进哪一段 + +## 里程碑总览 + +第一阶段应拆成 6 个里程碑,并把引擎生产力工作流排在 RuntimeData 前面。 + +```text +M1 引擎壳与项目循环 + -> M2 资源与模型导入循环 + -> M3 场景编辑、材质与光照工作流 + -> M4 UI、持久化与打包循环 + -> M5 面向数字孪生的 RuntimeData 集成 + -> M6 引擎硬化与未来预留 +``` + +## M1 引擎壳与项目循环 + +### 目标 + +让 MetaCore 成为一个具备稳定项目行为的引擎壳。 + +### 交付物 + +- 稳定的 Windows 应用主循环 +- 稳定的平台层 +- 项目打开/创建行为 +- 启动场景行为 +- 独立 Player 执行能力 + +### 关键工作流 + +- 应用生命周期 +- 模块与服务启动 +- 项目描述文件处理 +- 视口渲染稳定性 +- 编辑器基线工作流与项目引导 + +### 完成标准 + +- 项目能稳定打开 +- 启动场景能被解析 +- 运行时能加载并渲染启动场景 + +## M2 资源与模型导入循环 + +### 目标 + +让 MetaCore 从“只有场景原型”变成“能围绕资源工作的工具”。 + +### 交付物 + +- 资源元数据生成 +- 资源数据库稳定 +- 导入流程稳定 +- 一等支持的模型导入工作流 +- package 流程稳定 +- 重新导入与资源浏览基础 + +### 关键工作流 + +- 资源元数据与 package 格式 +- 导入流水线 +- 模型格式支持 +- Project 面板工作流 +- prefab 基础 + +### 完成标准 + +- 导入的资源稳定出现 +- 导入的模型能在场景中使用 +- 场景和资源在重载后仍然有效 + +## M3 场景编辑、材质与光照工作流 + +### 目标 + +让 MetaCore 真正可用于内容搭建。 + +### 交付物 + +- 稳定的层级编辑 +- Transform Gizmo +- 材质工作流 +- 光照工作流 +- 可用的视口编辑体验 + +### 关键工作流 + +- 层级行为 +- 多选和操作 +- Transform 工具 +- 材质资源与材质指派 +- 灯光编辑 + +### 完成标准 + +- 可以基于导入资源搭一个场景 +- 材质和光照可以稳定编辑 +- 编辑器内容生产不再依赖硬编码 demo 对象 + +## M4 UI、持久化与打包循环 + +### 目标 + +让 MetaCore 作为项目工具具备交付能力。 + +### 交付物 + +- UI 基础编辑 +- 场景保存/打开成熟化 +- Windows 打包验证 +- 项目级 pilot 打包流程 + +### 关键工作流 + +- UI 资源与 UI 运行时 +- 二进制场景持久化 +- 启动场景打包 +- clean machine 验证 + +### 完成标准 + +- 开发者能构建并打包一个小型工业三维可视化客户端 +- 打包后的运行时在编辑器外表现稳定 + +## M5 面向数字孪生的 RuntimeData 集成 + +### 目标 + +在引擎基础工作流之上叠加数字孪生所需的 RuntimeData 能力。 + +### 交付物 + +- 独立的 RuntimeData 模块 +- source / point / binding 模型 +- runtime config 持久化 +- live adapter 支持 +- binding 配置工作流 +- runtime diagnostics + +### 关键工作流 + +- RuntimeData 架构 +- adapter 与 binding 模型 +- 运行时分发与故障处理 +- 编辑器侧配置 +- 数字孪生样板工程 + +### 完成标准 + +- 打包后的运行时能把外部数据应用到场景目标 +- 运行时故障可见且不会导致崩溃 +- 引擎能支撑一个小型数字孪生项目 + +## M6 引擎硬化与未来预留 + +### 目标 + +把 MetaCore 强化成一个可长期演进的引擎底座。 + +### 交付物 + +- 更清晰的 Runtime / Editor 分离 +- 更强的诊断能力 +- 更干净的子系统边界 +- XR 与 B/S 的未来预留在代码结构中落地 + +### 关键工作流 + +- 模块边界清理 +- 诊断与健康可见性 +- 未来扩展接口 +- 样板与测试扩充 + +### 完成标准 + +- 第二阶段工作不需要推翻第一阶段架构 + +## 任务树 + +### A. 引擎基础 + +- A1 应用生命周期 +- A2 平台服务 +- A3 日志 +- A4 模块启动 + +### B. 场景与对象系统 + +- B1 场景结构 +- B2 GameObject 层级 +- B3 组件状态 +- B4 持久化 +- B5 prefab 基础 + +### C. 渲染与运行时 + +- C1 渲染桥 +- C2 场景视口 +- C3 运行时视口 +- C4 相机控制 +- C5 光照基础 + +### D. 项目与资源循环 + +- D1 项目描述文件 +- D2 资源元数据 +- D3 资源数据库 +- D4 导入流程 +- D5 package 流程 +- D6 cook 流程 + +### E. 编辑器 + +- E1 Hierarchy 面板 +- E2 Scene 视图 +- E3 Inspector +- E4 Project 面板 +- E5 Console +- E6 Undo / Redo + +### F. UI 与交付 + +- F1 UI 资源与 UI 运行时 +- F2 场景持久化 +- F3 打包 +- F4 clean-machine 验证 + +### G. RuntimeData + +- G1 RuntimeData 模块 +- G2 RuntimeData 类型系统 +- G3 source 与 binding 文档 +- G4 adapter 接口 +- G5 dispatcher +- G6 live adapter +- G7 binding 编辑 +- G8 诊断与 stale 处理 + +### H. 未来预留 + +- H1 XR 扩展边界 +- H2 B/S 扩展边界 +- H3 子系统清理 + +## 修正后的实施重点 + +旧建议过早把 RuntimeData 放在主线中央。 + +修正后的顺序应该是: + +1. 先做 M1 和 M2 +2. 再做 M3 +3. 再做 M4 +4. 最后再让 M5 成为主导 + +RuntimeData 仍然重要,但它现在应该被视为 Phase 1B 子系统,而不是第一阶段最先主导实现节奏的模块。 + +## 当前关键路径 + +当前关键路径应该是: + +```text +项目循环 + -> 资源与模型导入 + -> 层级与 Transform 工作流 + -> 材质与光照工作流 + -> 场景保存/打开 + -> UI 基础 + -> 打包 + -> RuntimeData 集成 +``` + +如果这条路径被过早的行业子系统实现打断,MetaCore 会继续表现得像一个专用 demo,而不是一个可用于开发项目的引擎工具。 + +## 当前执行焦点的完成标准 + +当前执行焦点完成时,应满足: + +- 项目可以稳定打开并引导 +- 导入的模型资源可以进入场景 +- 层级编辑和 Transform 操作稳定 +- 材质和光照可以重复性编辑 +- 场景可以保存、重开并打包 + +只有在这些点成立之后,RuntimeData 才应该重新成为第一阶段主导任务。 + +## 立即下一步 + +从 M1 和 M2 开始推进。 + +在引擎内容工作流足够强之前,不要继续优先扩 adapter、工业协议或更多 RuntimeData 专属工具。 diff --git a/docs/designs/metacore-phase1-scope.md b/docs/designs/metacore-phase1-scope.md new file mode 100644 index 0000000..329618a --- /dev/null +++ b/docs/designs/metacore-phase1-scope.md @@ -0,0 +1,261 @@ +# MetaCore 第一阶段范围定义 + +生成时间:2026-03-27 +状态:草案 +范围:第一阶段 + +## 目的 + +这份文档定义 MetaCore 第一阶段到底要交付什么、要给未来预留什么,以及明确不准备在这一阶段完成什么。 + +目标是防止范围漂移,同时保留真实的产品野心: + +**MetaCore 第一阶段要成为一个足够完整的引擎版本,能够支撑工业数字孪生应用开发,并为后续扩展到 VR 虚拟维修训练留下清晰路径。** + +## 第一阶段产品定义 + +MetaCore 第一阶段是: + +- 一个以 Windows 为先的 C/S 引擎与编辑器栈 +- 能用于构建工业三维可视化和数字孪生类应用 +- 具备实时数据接入接口 +- 在架构上为未来 XR 和 B/S 扩展保留空间 + +MetaCore 第一阶段不是: + +- 一个完整行业平台 +- 一个完整 VR 训练平台 +- 一个浏览器优先的引擎 +- 一个要在广度上正面对标 Unity 的通用引擎 + +## 第一阶段完成陈述 + +当 MetaCore 能够被用于构建并交付一个 Windows C/S 工业三维可视化应用,并且具备以下能力时,第一阶段才算完成: + +- 项目与资源管理 +- 场景制作 +- 运行时打包 +- 实时数据源集成 +- 场景对象状态绑定 +- 稳定的运行时执行 + +并且同一套架构仍然保留了通往以下方向的清晰路径: + +- VR 应用开发 +- B/S 嵌入式产品化 + +## 第一阶段功能分层 + +### 1. 引擎核心 + +必须包含: + +- 应用生命周期 +- 平台抽象 +- 日志 +- 模块与服务基础设施 +- 反射与序列化 +- package 格式与资产标识 + +### 2. 场景与对象系统 + +必须包含: + +- 场景模型 +- GameObject 层级 +- 组件模型 +- Transform 编辑 +- 场景保存与加载 +- prefab 基础工作流 + +### 3. 渲染与运行时 + +必须包含: + +- 场景渲染 +- 相机系统 +- 基础光照 +- 视口渲染 +- 运行时 Player + +### 4. 项目与资源系统 + +必须包含: + +- 项目结构 +- 资源导入 +- 元数据生成 +- 资源数据库 +- package 与 cook 流程 +- 启动场景与 runtime config + +### 5. 编辑器系统 + +必须包含: + +- Hierarchy 面板 +- Scene 视图 +- Inspector +- Project 面板 +- Console +- Undo / Redo +- 对象与组件编辑 + +### 6. 支撑应用开发的 RuntimeData 层 + +必须包含: + +- 实时数据 adapter 接口 +- 数据源定义 +- 数据点定义 +- 场景 binding 定义 +- 运行时更新分发 +- 诊断与降级行为 + +## 第一阶段必须具备的功能 + +这些功能构成第一阶段闭环。 + +### 引擎与编辑器 + +- 打开项目 +- 加载启动场景 +- 编辑场景 +- 保存并重新加载场景 +- 查看并修改对象属性 +- 导入并管理项目资源 +- 打包并运行独立 Windows 客户端 + +### RuntimeData + +- 定义数据源 +- 定义逻辑数据点 +- 把数据点绑定到场景目标 +- 用外部更新驱动运行时场景状态 +- 可见地处理 source fault 和 stale data + +### 交付 + +- 在编辑器外运行一个已打包的样板项目 +- 把样板项目交付为 Windows C/S 应用 + +## 第一阶段支持的 binding 目标 + +第一阶段只应支持受控白名单。 + +### 初始必须支持 + +- `Transform.Position` +- `MeshRenderer.Visible` +- `MeshRenderer.BaseColor` +- `Light.Intensity` +- `Light.Color` + +### 后置目标 + +- 任意组件字段绑定 +- UI 文本绑定 +- 动画参数绑定 +- 骨骼状态绑定 + +## 第一阶段必须预留的区域 + +这些不需要在第一阶段完全产品化,但架构必须给它们留口子。 + +### XR 与 VR + +- 输入系统不能从设计上只服务桌面 +- 运行时逻辑不能默认只有键鼠交互 +- 对象模型和运行时系统要能复用于训练场景 + +### B/S 与嵌入 + +- 场景和资源格式不能锁死桌面形态 +- RuntimeData 模型要能脱离编辑器被服务化 +- Player 运行时不能依赖编辑器专属系统 + +### 行业解决方案分层 + +- 第一阶段的应用逻辑不能把某一种客户业务流程硬编码进引擎核心 + +## 第一阶段明确不做的内容 + +这些内容不进入第一阶段完成定义。 + +- 完整 B/S 运行时产品 +- 嵌入式 SDK 产品化 +- 完整 VR 虚拟维修训练工作流 +- XR Interaction Toolkit 等价能力 +- 训练评分与回放系统 +- AI 辅助工作流 +- 仿真编排平台 +- 大规模工业协议矩阵 +- 多人协作编辑 +- Unity 级别的编辑器广度 +- 通用可视化脚本平台 + +## MVP / V1 / V1.5 分层 + +### MVP + +目的: + +- 证明引擎闭环是真的 + +包括: + +- 引擎主循环与运行时壳 +- 项目与场景基础 +- 编辑器基础 +- Windows 独立运行 +- RuntimeData 子系统骨架 +- mock adapter +- 基础 scene binding +- 一个样板项目 + +### V1 + +目的: + +- 证明 MetaCore 能支撑第一类真实交付场景 + +包括: + +- 稳定的资源与 package 流程 +- 稳定的 prefab 基础 +- 一个简单 live adapter +- runtime config 持久化 +- binding config 持久化 +- 最小 binding 编辑 UI +- runtime diagnostics +- 更强的回归覆盖 + +### V1.5 + +目的: + +- 让 MetaCore 更像引擎,而不是 pilot 壳 + +包括: + +- 更成熟的编辑器工作流 +- 更强的诊断能力 +- 更清晰的 Runtime / Editor 分离 +- 更稳的资源工作流 +- 更明确的 XR 与 B/S 架构预留 + +## 验收标准 + +MetaCore 第一阶段应该按以下标准判断。 + +### 产品验收 + +- 开发者可以使用 MetaCore 构建一个小型工业三维可视化应用 +- 运行时应用可以响应外部数据更新 +- 打包后的应用可以独立在 Windows 上运行 + +### 工程验收 + +- Runtime 与 Editor 边界清晰 +- 数据接入以独立子系统实现,而不是散落在各处逻辑里 +- 核心工作流具备回归测试 diff --git a/docs/designs/metacore-product-plan.md b/docs/designs/metacore-product-plan.md new file mode 100644 index 0000000..ecc2a40 --- /dev/null +++ b/docs/designs/metacore-product-plan.md @@ -0,0 +1,263 @@ +# MetaCore 产品计划 + +生成时间:2026-03-27 +状态:草案 +读者:创始人、产品、设计、工程、交付 + +## 一句话定位 + +MetaCore 是一套面向国产化部署项目的自主可控三维引擎底座,第一阶段聚焦工业数字孪生项目交付。 + +## 为什么要做这件事 + +上一代方案已经证明底层路线能够通过自主可控评测,但基于 Python 的架构在持续功能演进、第三方软件适配和长期稳定交付上成本太高。 + +C++ 重构不是表面上的技术升级,而是产品动作。 + +它要让引擎: + +- 更容易适配国产平台 +- 更容易在交付中审计和裁剪 +- 更容易接入复杂企业环境 +- 更适合长生命周期行业项目 + +## 产品假设 + +第一商业切口不是“做一个通用引擎”,而是: + +**在国产化环境下更可靠地运行,并更快交付工业数字孪生项目。** + +这才是采购故事。 + +客户不是因为它像 Unity 才买,而是因为: + +- 它能跑在目标国产环境 +- 它能降低交付成本和排期风险 +- 它足够可控,适合长期掌握 + +## 核心客户 + +### 直接客户 + +- 在国产化约束下做工业数字孪生项目交付的团队 +- 面向国企、重工、军工相关客户的系统集成商 +- 需要掌握三维底座而不愿长期依赖国外商业引擎的企业技术团队 + +### 最终运行环境 + +- 国产 CPU、国产 OS、国产 GPU、中间件及网络受限环境 +- 带有自主可控、可信、可追溯、供应链审查要求的项目环境 + +## 第一商业切口 + +### Stage 1 + +通过 C/S 架构完成工业数字孪生项目交付。 + +### Stage 2 + +在同一引擎底座上产品化 B/S 嵌入能力和 VR 虚拟维修训练能力。 + +### Stage 3 + +扩展到仿真编排、AI 辅助工作流和更高层行业解决方案。 + +## 核心价值主张 + +MetaCore 必须同时赢在两件事上: + +1. 国产化平台适配能力 +2. 快速项目交付能力 + +如果只有其中一项成立,产品就是弱的: + +- 只适配、不快交付:技术上有意思,商业上偏弱 +- 只快交付、不适配国产化:容易被替代,护城河不足 + +## 产品分层 + +MetaCore 应被定义为一个分层产品,而不是一个单体引擎。 + +### 1. MetaCore Core + +引擎底座。 + +职责: + +- 平台抽象 +- 运行时主循环 +- 渲染桥 +- 场景模型 +- 资源流水线 +- package 与序列化 +- 面向交付的模块化能力 + +### 2. MetaCore Studio + +内部团队与交付团队使用的编辑器。 + +职责: + +- 场景搭建 +- 资源导入与整理 +- 项目配置 +- package 和部署准备 +- 面向交付的编辑工作流 + +非目标: + +- 在广度上复制 Unity 编辑器 + +### 3. MetaCore Industry Twin Kit + +面向数字孪生项目交付的解决方案层。 + +职责: + +- 行业项目模板 +- 标准场景骨架 +- 常用运行时组件 +- 数据绑定 adapter +- 部署预设 + +### 4. MetaCore XR Training Kit + +第二阶段的解决方案层。 + +职责: + +- VR 运行时支持 +- 维修训练流程编辑 +- 交互脚本支持 +- 训练评分与回放挂点 + +## 产品原则 + +### 原则 1:可控优先于便利 + +凡是会让审计、裁剪、替换、国产适配变难的依赖,默认都应被视为高风险依赖。 + +### 原则 2:交付优先于平台演示 + +每一个重大功能都应该帮助真实客户项目更快、更稳或更低适配成本地交付。 + +### 原则 3:模块替换点本身就是产品能力 + +渲染桥、导入流水线、数据连接器、打包方式、XR 层,都应被视为可替换模块,而不是写死的实现。 + +### 原则 4:编辑器围绕交付工作流服务 + +编辑器不是拿来做“像不像 Unity”的面子工程,而是项目团队的生产工具。 + +### 原则 5:可审计性属于架构的一部分 + +资源流、package 流、场景持久化和部署配置,都应该是显式且可检查的。 + +## Stage 1 必须交付什么 + +只有当下面这条最小 C/S 交付链真实成立时,Stage 1 才算完成: + +1. 在目标国产化环境中运行 +2. 导入并组织项目资源 +3. 搭建和编辑一个可用的工业场景 +4. 配置基础交互与展示行为 +5. 打包并运行一个可交付应用 +6. 接入基础状态或数据驱动可视化 +7. 把一个可部署项目包移交给交付团队,而不需要额外手工拼接 + +如果这条链没闭环,就不该过早加 AI 或高级仿真能力。 + +## Stage 1 范围 + +### 范围内 + +- 以 Windows 为先的 C++ 引擎重构,同时把国产适配作为架构要求 +- 第一阶段只承诺 C/S 交付 +- 场景、对象、组件、资源、package 和编辑器基础 +- 工业数字孪生场景搭建工作流 +- 面向交付的项目结构 +- 部署打包与项目移交准备 +- 基础数据驱动可视化挂点 +- 为后续平台与模块适配保留稳定扩展点 +- 为未来 B/S 嵌入保留架构边界 + +### 明确不在范围内 + +- 与 Unity 全工作流完全对齐 +- 在游戏引擎功能广度上竞争 +- 第一阶段完整 B/S 产品化 +- 第一阶段浏览器优先运行时 +- 高级可视化脚本系统 +- 复杂多人协作 +- AI-first 工作流 +- 完整仿真平台 +- 第一阶段完整 VR 训练产品化 + +## Stage 1 成功标准 + +### 产品结果 + +- 一个交付团队能在 MetaCore 上完成一个小型工业数字孪生项目 +- 该项目能在目标国产化环境中稳定运行到可接受程度 +- 核心项目工作流不再被 Python 时代的架构限制卡住 + +### 商业结果 + +- MetaCore 可以被讲成“国产化交付底座”,而不只是内部原型 +- 至少有一个项目机会可以用新的 C++ 架构作为主技术故事去推进 +- 产品叙事优先支持项目交付收入,再延伸到买断或融资故事 + +### 工程结果 + +- 引擎核心足够模块化,便于平台适配和长期演进 +- 资源与场景流水线是确定且可审计的 +- 运行时与编辑器的依赖边界清晰可控 + +## 为什么是纯 C++ + +这次重构应该从产品角度来解释: + +- 降低国产环境适配复杂度 +- 增强长生命周期企业部署的稳定性 +- 让性能和打包结果更可预测 +- 更容易接入现有原生软件体系 +- 对依赖和执行边界有更强控制 + +不应该把话术讲成“Python 不好”,正确的说法应该是: + +**Python 版本足以支撑上一代评测,但不足以支撑下一代交付。** + +## 交付架构策略 + +MetaCore 最终应该同时支持 C/S 和 B/S。 + +但第一阶段不应同时完整交付两者。 + +### 第一阶段承诺 + +- C/S 是唯一承诺的生产交付架构 +- 第一条付费项目闭环必须通过桌面式国产化部署完成 + +### 第一阶段对 B/S 的预留 + +- 场景与资源格式不能写死桌面假设 +- 运行时边界不能把业务逻辑锁死在单一桌面壳里 +- package 与服务边界要保留未来嵌入第三方系统的路径 + +### 第二阶段承诺 + +- 产品化 B/S 输出和嵌入模式,用于第三方业务系统集成 + +规则很简单: + +**为两者设计,只在第一阶段承诺 C/S。** + +## 技术方向约束 + +### 架构约束 + +- 保持严格模块边界 +- 优先显式服务契约,避免隐式耦合 +- 反射与序列化体系必须掌握在自己手里 +- package 格式必须稳定且可版本化 +- 平台适配必须作为一等架构问题 diff --git a/docs/designs/metacore-runtime-data-access-design.md b/docs/designs/metacore-runtime-data-access-design.md new file mode 100644 index 0000000..a96482e --- /dev/null +++ b/docs/designs/metacore-runtime-data-access-design.md @@ -0,0 +1,348 @@ +# MetaCore RuntimeData 接入设计 + +生成时间:2026-03-27 +状态:草案 +范围:第一阶段 A + +## 目的 + +这份文档定义 MetaCore RuntimeData 子系统的第一版正式设计。 + +目标不是一次性解决所有未来接入问题,而是在 Windows C/S 交付闭环里,为实时数据接入提供一条清晰、显式且不会污染场景逻辑或编辑器代码的路径。 + +## 问题陈述 + +第一阶段要求从一开始就支持实时数据接入。 + +这意味着 MetaCore 必须支持: + +- 连接外部数据源 +- 把输入值归一化 +- 把值绑定到场景对象和组件 +- 安全处理断连和非法 payload +- 在不改源码的情况下打包和交付项目 + +当前代码库已经具备: + +- 场景与对象持久化 +- package 与资源格式 +- 编辑器服务注册 +- 运行时场景基础 + +当前代码库尚未完整具备: + +- 独立的运行时数据源抽象 +- 项目级 binding 模型 +- 归一化的运行时更新格式 +- 明确的故障与重连策略 + +## 设计原则 + +### 1. Runtime 优先 + +RuntimeData 首先是运行时子系统。Studio 只是它的配置编辑端。 + +### 2. 协议相关逻辑必须隔离 + +场景对象不应该直接解析协议 payload。 + +### 3. 显式优先于聪明 + +binding 必须是可检查、可追踪、数据驱动的,不应该依赖对象名推断或隐式约定。 + +### 4. 安全降级 + +坏数据、目标缺失或断连必须以可见且可预测的方式失败。 + +### 5. 第一版表面要小 + +第一阶段只支持最小集合的数据类型和 adapter,再根据真实交付压力扩展。 + +## 推荐模块位置 + +新增一个模块: + +- `Source/MetaCoreRuntimeData` + +该模块应依赖: + +- `MetaCoreFoundation` +- `MetaCoreScene` + +该模块不应依赖: + +- `MetaCoreEditor` + +下游消费者可以是: + +- `MetaCorePlayer` +- 未来用于预览和配置的 `MetaCoreEditor` + +## 高层架构 + +```text +外部数据源 + | + v +数据源 Adapter + | + v +归一化 Runtime Update + | + v +Binding Dispatcher + | + +--> 场景对象 Binding Applier + | + +--> Runtime Diagnostics / Fault State +``` + +## 核心概念 + +### 1. Data Source Definition + +这部分由项目级配置定义。 + +它描述: + +- source id +- adapter 类型 +- 连接参数 +- 启动模式 +- 重连策略 + +建议结构: + +```text +MetaCoreDataSourceDefinition + Id + AdapterType + DisplayName + ConnectionSettings + AutoConnect + ReconnectPolicy +``` + +### 2. Data Point Definition + +这是项目面对的数据契约。 + +它定义: + +- source id +- 外部地址或键 +- 逻辑 point id +- 期望值类型 +- 更新语义 + +建议结构: + +```text +MetaCoreDataPointDefinition + Id + SourceId + ExternalAddress + ValueType + UpdateMode + FallbackBehavior +``` + +### 3. Scene Binding Definition + +它把逻辑数据点绑定到场景目标。 + +它定义: + +- 目标对象 id +- 目标组件路径 +- 转换规则 +- 可选转换器 +- 缺失数据行为 + +建议结构: + +```text +MetaCoreSceneBindingDefinition + BindingId + DataPointId + TargetObjectId + TargetPath + BindingMode + ConverterId + MissingDataPolicy +``` + +### 4. Runtime Update + +这是 adapter 输出的归一化 payload。 + +它不应该暴露协议专属表示。 + +建议结构: + +```text +MetaCoreRuntimeDataValue + Type + BoolValue + IntValue + FloatValue + Vec3Value + StringValue + Timestamp + Quality +``` + +```text +MetaCoreRuntimeDataUpdate + DataPointId + Value + Sequence + SourceTimestamp +``` + +## 第一阶段值类型 + +第一阶段故意保持小集合: + +- `bool` +- `int64` +- `double` +- `string` +- `vec3` + +除非有真实客户场景逼迫,不要在第一阶段引入任意 JSON 形态的通用绑定。 + +## Binding 目标 + +第一阶段只应支持一组很窄的 binding 目标。 + +建议初始目标: + +- `Transform.Position` +- `MeshRenderer.Visible` +- `MeshRenderer.BaseColor` +- `Light.Intensity` +- `Light.Color` + +如有必要可追加: + +- 对象 active / visible 状态 +- 未来 UI 组件的文本字段 + +不要一开始就做“任意反射字段随便绑”的通用系统。看起来优雅,实际通常不可控。 + +## Adapter 接口 + +建议运行时接口: + +```text +MetaCoreIRuntimeDataSourceAdapter + GetAdapterType() + Configure() + Connect() + Disconnect() + Tick() + PollUpdates() + GetStatus() +``` + +要求: + +- adapter 拥有传输与协议细节 +- adapter 只输出归一化 update +- adapter 暴露明确状态 + +### Adapter 状态 + +建议状态: + +- `Disconnected` +- `Connecting` +- `Connected` +- `Degraded` +- `Faulted` + +## 第一阶段推荐 adapter + +建议先做两类: + +- `mock` +- `file_replay` 或一个简单 live adapter + +如果要尽快验证交付闭环,最现实的顺序是: + +1. `mock` +2. `file_replay` +3. `tcp` + +不要一开始就冲复杂工业协议。 + +## Dispatcher 职责 + +Dispatcher 负责: + +- 接收 runtime updates +- 找到对应 binding +- 找到目标场景对象 +- 把值应用到目标字段 +- 更新 binding 健康状态 +- 记录 fault 和 stale + +它不负责: + +- 协议解析 +- 编辑器 UI +- 行业业务逻辑 + +## 配置与持久化 + +第一阶段走二进制配置路线。 + +建议产物: + +- `ProjectRuntime.mcruntimecfg` +- `DataSources.mcruntime` +- `Bindings.mcruntime` +- `Diagnostics.mcruntimestate` + +这样可以: + +- 保持运行时配置清晰 +- 便于打包 +- 与现有二进制 package 方向保持一致 + +## 失败模式 + +第一阶段必须显式处理: + +- source 不存在 +- 连接失败 +- payload 类型不匹配 +- binding 指向不存在对象 +- 长时间无更新导致 stale +- 配置文件损坏 + +这些都不能静默失败。 + +## 测试要求 + +至少要覆盖: + +- 配置读写 +- 坏配置拒绝 +- dispatcher 应用更新 +- 缺失目标不崩 +- stale 检测 +- adapter 状态变化 +- 二进制配置可回读 + +## 结论 + +RuntimeData 是 MetaCore 的重要行业子系统,但它应该是: + +- 明确隔离的 +- 运行时优先的 +- 二进制配置驱动的 +- 小范围起步的 + +第一阶段只要把这条线做成清晰、稳定、可诊断的子系统就够了,不应该过早抽象成一个万能绑定平台。 diff --git a/docs/designs/metacore-runtime-data-usage.md b/docs/designs/metacore-runtime-data-usage.md new file mode 100644 index 0000000..ebd15fa --- /dev/null +++ b/docs/designs/metacore-runtime-data-usage.md @@ -0,0 +1,248 @@ +# MetaCore RuntimeData 使用说明 + +生成时间:2026-03-27 +状态:草案 +范围:第一阶段 A / Sprint 4+ + +## 目的 + +这份文档说明当前 MetaCore RuntimeData 管线应该如何实际使用。 + +它主要服务于: + +- 内部开发 +- 演示准备 +- 第一轮交付验证 + +当前支持的 adapter 类型: + +- `mock` +- `file_replay` +- `tcp` + +当前运行时配置产物: + +- `Runtime/ProjectRuntime.mcruntimecfg` +- `Runtime/DataSources.mcruntime` +- `Runtime/Bindings.mcruntime` +- `Runtime/Diagnostics.mcruntimestate` + +## 当前交付模型 + +当前流程如下: + +1. 编写或生成运行时配置 +2. 保存二进制 `.mcruntime` 文档 +3. 启动 `MetaCorePlayer` +4. 通过某个 adapter 喂入运行时更新 +5. 验证场景对象是否按照 binding 发生变化 + +## RuntimeData 文件 + +默认样例位置: + +- [ProjectRuntime.mcruntimecfg](D:/MetaCore/TestProject/Runtime/ProjectRuntime.mcruntimecfg) +- [DataSources.mcruntime](D:/MetaCore/TestProject/Runtime/DataSources.mcruntime) +- [Bindings.mcruntime](D:/MetaCore/TestProject/Runtime/Bindings.mcruntime) +- [Diagnostics.mcruntimestate](D:/MetaCore/TestProject/Runtime/Diagnostics.mcruntimestate) +- [RuntimeReplay.mcstream](D:/MetaCore/TestProject/Runtime/RuntimeReplay.mcstream) +- [Main.mcscene](D:/MetaCore/TestProject/Scenes/Main.mcscene) + +## 当前支持的 binding 目标 + +第一阶段当前支持: + +- `Transform.Position` +- `MeshRenderer.Visible` +- `MeshRenderer.BaseColor` +- `Light.Intensity` +- `Light.Color` + +## 方案一:File Replay 演示 + +这是最简单的演示路径。 + +### 生成 replay 配置 + +```powershell +D:\MetaCore\build\vs2022-debug\Debug\MetaCoreRuntimeConfigTool.exe D:\MetaCore\TestProject\Runtime +``` + +这会生成: + +- `ProjectRuntime.mcruntimecfg` +- replay 版本的 `DataSources.mcruntime` +- `Bindings.mcruntime` +- `Main.mcscene` +- `RuntimeReplay.mcstream` + +### 启动 Player + +```powershell +D:\MetaCore\build\vs2022-debug\Debug\MetaCorePlayer.exe +``` + +预期结果: + +- `MetaCorePlayer` 会从项目 / runtime 配置里加载启动场景 +- `MetaCorePlayer` 会加载二进制 `.mcruntime` +- 运行时数据来自 `file_replay` +- 样板对象会随时间改变位置、显隐、颜色和告警灯强度 + +## 方案二:TCP 演示 + +这是当前最小 live adapter 路线。 + +### 生成 TCP 配置 + +```powershell +D:\MetaCore\build\vs2022-debug\Debug\MetaCoreRuntimeConfigTool.exe D:\MetaCore\TestProject\Runtime --tcp +``` + +这会生成: + +- `ProjectRuntime.mcruntimecfg` +- TCP 版本的 `DataSources.mcruntime` +- `Bindings.mcruntime` +- `Main.mcscene` + +当前默认 TCP source 使用: + +- `host = 127.0.0.1` +- `port = 7001` + +### 启动 Player + +```powershell +D:\MetaCore\build\vs2022-debug\Debug\MetaCorePlayer.exe +``` + +### 启动 TCP 发送器 + +```powershell +D:\MetaCore\build\vs2022-debug\Debug\MetaCoreTcpSenderTool.exe +``` + +预期结果: + +- `MetaCorePlayer` 连接 TCP source +- 发送器逐行输出 runtime updates +- 样板对象会改变位置、显隐、颜色和告警灯强度 + +## TCP 文本协议 + +当前行格式: + +```text + +``` + +示例: + +```text +cube.visible bool true +cube.position vec3 1.0 2.0 3.0 +cube.base_color vec3 0.7 0.6 0.5 +``` + +支持的值类型: + +- `bool` +- `int64` +- `double` +- `string` +- `vec3` + +## Studio 中的使用方式 + +当前 Studio 的入口有: + +- Inspector 面板 + - `Runtime Data Bindings` +- `运行时数据` 面板 + +### Inspector 当前支持 + +- 新增 `Replay Source` +- 新增 `TCP Source` +- 编辑 source 显示名 +- 编辑 source adapter 类型 +- 编辑 replay 的 `file_path` +- 编辑 TCP 的 `host` +- 编辑 TCP 的 `port` +- 新增 `DataPoint` +- 编辑 data point 的 `Point Id` +- 编辑 data point 的 `External Address` +- 编辑 data point 的 `Source` +- 编辑 data point 的 `Value Type` +- 给当前对象新增 binding +- 编辑 binding target 和绑定的数据点 +- 保存 runtime 配置 + +### RuntimeData 面板当前支持 + +- source / data point / binding 数量统计 +- 项目 runtime 路径摘要 +- 最近一次 Player 写出的 diagnostics 快照 +- source 摘要 +- 校验结果摘要 +- 当前所有校验问题列表 + +## 校验规则 + +当 runtime 配置中存在 `Error` 级问题时,保存会被拒绝。 + +当前阻断规则包括: + +- 空或重复的 `SourceId` +- 空 `AdapterType` +- `file_replay` source 缺少 `file_path` +- `tcp` source 缺少 `port` +- 空或重复的 `DataPointId` +- `DataPoint` 引用了不存在的 `SourceId` +- 空或重复的 `BindingId` +- `Binding` 引用了不存在的 `DataPointId` +- `Binding` 缺少 `TargetObjectId` + +当前 Warning 包括: + +- 空 `ExternalAddress` +- 没有定义任何 source + +## 推荐演示流程 + +如果只是为了最快演示: + +1. 用 `MetaCoreRuntimeConfigTool` 生成配置 +2. 打开 `MetaCoreEditorApp` +3. 如有需要,在 Inspector 中调整 binding +4. 保存 runtime 配置 +5. 启动 `MetaCorePlayer` +6. 用 replay 或 TCP sender 喂数据 + +## 常用构建命令 + +```powershell +cmake --build --preset build-debug --target MetaCoreEditorApp MetaCorePlayer MetaCoreRuntimeConfigTool MetaCoreTcpSenderTool MetaCoreSmokeTests +``` + +## Smoke Test 命令 + +```powershell +D:\MetaCore\build\vs2022-debug\Debug\MetaCoreSmokeTests.exe +``` + +## 当前限制 + +这些限制是当前阶段有意为之: + +- TCP adapter 仍然是最小文本协议,不是工业协议 +- runtime diagnostics 当前是二进制快照导出,不是完整时序监控系统 +- binding 目标是白名单,不支持任意反射路径 +- Studio 的 source 编辑还是表单式,不是完整表格或节点图 + +## 下一步建议 + +- 给 Player 增加运行时 diagnostics overlay 或面板 +- 增加多对象、多 source 的样板项目 +- 在当前 TCP 边界之上接一个更接近生产的 adapter diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index 0a15d12..7301fe5 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -1,13 +1,36 @@ +#include "MetaCoreEditor/MetaCoreBuiltinModules.h" +#include "MetaCoreEditor/MetaCoreEditorAssetTypes.h" #include "MetaCoreEditor/MetaCoreEditorContext.h" #include "MetaCoreEditor/MetaCoreEditorModule.h" +#include "MetaCoreEditor/MetaCoreEditorServices.h" +#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" #include "MetaCoreFoundation/MetaCoreLogService.h" #include "MetaCorePlatform/MetaCoreWindow.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataDispatcher.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataSource.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" +#include "MetaCoreScene/MetaCoreScenePackage.h" #include "MetaCoreScene/MetaCoreScene.h" #include +#include +#include +#include +#include +#include #include +#include + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include namespace { @@ -18,6 +41,21 @@ void MetaCoreExpect(bool condition, const char* message) { } } +void MetaCoreExpectVec3Near(const glm::vec3& actual, const glm::vec3& expected, const char* message) { + const auto nearlyEqual = [](float lhs, float rhs) { + return std::abs(lhs - rhs) <= 0.0001F; + }; + + if (!nearlyEqual(actual.x, expected.x) || + !nearlyEqual(actual.y, expected.y) || + !nearlyEqual(actual.z, expected.z)) { + std::cerr << "MetaCoreSmokeTests failed: " << message + << " (actual=" << actual.x << "," << actual.y << "," << actual.z + << " expected=" << expected.x << "," << expected.y << "," << expected.z << ")\n"; + std::exit(1); + } +} + class MetaCoreDummyPanelProvider final : public MetaCore::MetaCoreIEditorPanelProvider { public: std::string GetPanelId() const override { return "Dummy"; } @@ -123,11 +161,1251 @@ void MetaCoreTestUndoRedo() { MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount + 1, "Redo 后对象数量应再次增加"); } +void MetaCoreTestBuiltinModuleComposition() { + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + auto editorViewsModule = MetaCore::MetaCoreCreateBuiltinEditorViewsModule(); + + coreServicesModule->Startup(moduleRegistry); + editorViewsModule->Startup(moduleRegistry); + + MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ReflectionRegistry"); + MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 PackageService"); + MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 AssetDatabaseService"); + MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ImportPipelineService"); + MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 CookService"); + MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ScenePersistenceService"); + MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 SelectionService"); + MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ClipboardService"); + MetaCoreExpect(!moduleRegistry.GetPanelProviders().empty(), "应注册至少一个面板提供者"); + + editorViewsModule->Shutdown(moduleRegistry); + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); +} + +void MetaCoreTestScenePackageProjectStartupLoad() { + const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreScenePackageSmoke"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + + MetaCore::MetaCoreSceneDocument sceneDocument; + sceneDocument.Name = "Main"; + const MetaCore::MetaCoreScene sourceScene = MetaCore::MetaCoreCreateDefaultScene(); + sceneDocument.GameObjects = sourceScene.GetGameObjects(); + + const std::filesystem::path scenePath = tempProjectRoot / "Scenes" / "Main.mcscene"; + MetaCoreExpect(MetaCore::MetaCoreWriteScenePackage(scenePath, sceneDocument), "应能写入二进制场景包"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + MetaCoreExpect(projectFile.is_open(), "应能写入项目文件"); + projectFile + << "{\n" + << " \"name\": \"SmokeProject\",\n" + << " \"scenes\": [\n" + << " \"Scenes/Main.mcscene\"\n" + << " ],\n" + << " \"startup_scene\": \"Scenes/Main.mcscene\",\n" + << " \"version\": \"0.1.0\"\n" + << "}\n"; + } + + const auto loadedSceneDocument = MetaCore::MetaCoreLoadStartupSceneDocument(tempProjectRoot / "MetaCore.project.json"); + MetaCoreExpect(loadedSceneDocument.has_value(), "应能从项目文件加载 startup scene"); + MetaCoreExpect(loadedSceneDocument->GameObjects.size() == sourceScene.GetGameObjects().size(), "startup scene 对象数量应一致"); + MetaCoreExpect(!loadedSceneDocument->GameObjects.empty(), "startup scene 应包含对象"); + MetaCoreExpect(loadedSceneDocument->GameObjects.front().Name == "Main Camera", "startup scene 首个对象应为 Main Camera"); + + std::filesystem::remove_all(tempProjectRoot); +} + +void MetaCoreTestComponentRegistryDescriptors() { + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + const auto componentRegistry = moduleRegistry.ResolveService(); + MetaCoreExpect(componentRegistry != nullptr, "应解析到 ComponentTypeRegistry"); + + const auto* transformDescriptor = componentRegistry->FindDescriptor("Transform"); + const auto* cameraDescriptor = componentRegistry->FindDescriptor("Camera"); + const auto* lightDescriptor = componentRegistry->FindDescriptor("Light"); + const auto* meshRendererDescriptor = componentRegistry->FindDescriptor("MeshRenderer"); + MetaCoreExpect(transformDescriptor != nullptr, "Transform descriptor 应存在"); + MetaCoreExpect(cameraDescriptor != nullptr, "Camera descriptor 应存在"); + MetaCoreExpect(lightDescriptor != nullptr, "Light descriptor 应存在"); + MetaCoreExpect(meshRendererDescriptor != nullptr, "MeshRenderer descriptor 应存在"); + MetaCoreExpect(!transformDescriptor->DrawInspector, "Transform 应继续由独立 InspectorDrawer 处理"); + MetaCoreExpect(static_cast(cameraDescriptor->DrawInspector), "Camera descriptor 应提供 drawer"); + MetaCoreExpect(static_cast(lightDescriptor->DrawInspector), "Light descriptor 应提供 drawer"); + MetaCoreExpect(static_cast(meshRendererDescriptor->DrawInspector), "MeshRenderer descriptor 应提供 drawer"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); +} + +void MetaCoreTestScenePersistenceRoundTrip() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreSceneRoundTripProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Library"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"RoundTripProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + const auto scenePersistenceService = moduleRegistry.ResolveService(); + MetaCoreExpect(scenePersistenceService != nullptr, "应解析到 ScenePersistenceService"); + + MetaCore::MetaCoreId cubeId = 0; + for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) { + if (object.Name == "Cube") { + cubeId = object.Id; + break; + } + } + MetaCoreExpect(cubeId != 0, "默认场景应包含 Cube"); + + editorContext.SelectOnly(cubeId); + MetaCoreExpect(scenePersistenceService->SaveSceneAs(editorContext, std::filesystem::path("Scenes") / "Main.mcscene"), "应能保存场景"); + MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "保存后场景不应为 dirty"); + + const bool renamed = editorContext.ExecuteSnapshotCommand("重命名对象", [&]() { + return scene.RenameGameObject(cubeId, "RoundTripCube"); + }); + MetaCoreExpect(renamed, "应能修改场景对象"); + MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "修改后场景应变为 dirty"); + + MetaCoreExpect(scenePersistenceService->SaveCurrentScene(editorContext), "应能再次保存当前场景"); + MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "再次保存后 dirty 应清除"); + + scene.RestoreSnapshot(MetaCore::MetaCoreSceneSnapshot{}); + editorContext.ClearSelection(); + MetaCoreExpect(scene.GetGameObjects().empty(), "清空快照后场景应为空"); + + MetaCoreExpect(scenePersistenceService->LoadScene(editorContext, std::filesystem::path("Scenes") / "Main.mcscene"), "应能重新加载场景"); + MetaCoreExpect(scene.GetGameObjects().size() == 6, "重新加载后对象数量应恢复"); + MetaCoreExpect(editorContext.GetSelectedObjectId() != 0, "重新加载后应恢复选中对象"); + + bool foundRenamedCube = false; + for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) { + if (object.Name == "RoundTripCube") { + foundRenamedCube = true; + break; + } + } + MetaCoreExpect(foundRenamedCube, "重新加载后应保留保存过的对象名称"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + +void MetaCoreTestImportPipelineAndCook() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreImportCookProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Library"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"ImportProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + { + std::ofstream rawAsset(tempProjectRoot / "Assets" / "Texture.png", std::ios::binary | std::ios::trunc); + rawAsset << "PNGDATA_V1"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + const auto assetDatabase = moduleRegistry.ResolveService(); + const auto cookService = moduleRegistry.ResolveService(); + MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); + MetaCoreExpect(cookService != nullptr, "应解析到 CookService"); + + const std::filesystem::path metaPath = tempProjectRoot / "Assets" / "Texture.png.mcmeta"; + const std::filesystem::path packagePath = tempProjectRoot / "Assets" / "Texture.png.mcasset"; + MetaCoreExpect(std::filesystem::exists(metaPath), "导入后应生成 mcmeta"); + MetaCoreExpect(std::filesystem::exists(packagePath), "导入后应生成 mcasset"); + + const auto sourceRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Texture.png"); + MetaCoreExpect(sourceRecord.has_value(), "应能找到原始资源记录"); + MetaCoreExpect(sourceRecord->Guid.IsValid(), "原始资源应有稳定 GUID"); + MetaCoreExpect(sourceRecord->PackagePath == std::filesystem::path("Assets") / "Texture.png.mcasset", "原始资源应关联包路径"); + + const std::filesystem::path cookedPath = tempProjectRoot / cookService->GetCookedPathForAsset(sourceRecord->Guid); + MetaCoreExpect(std::filesystem::exists(cookedPath), "导入后应生成 cooked 结果"); + + const MetaCore::MetaCoreAssetGuid initialGuid = sourceRecord->Guid; + { + std::ofstream rawAsset(tempProjectRoot / "Assets" / "Texture.png", std::ios::binary | std::ios::trunc); + rawAsset << "PNGDATA_V2"; + } + + MetaCoreExpect(assetDatabase->Refresh(), "修改原始资源后应能重新刷新资产数据库"); + const auto refreshedSourceRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Texture.png"); + MetaCoreExpect(refreshedSourceRecord.has_value(), "刷新后应仍能找到原始资源记录"); + MetaCoreExpect(refreshedSourceRecord->Guid == initialGuid, "重新导入后 GUID 应保持稳定"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + +void MetaCoreTestBootstrapStartupSceneCreation() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreBootstrapSceneProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Library"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"BootstrapProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + const auto scenePersistenceService = moduleRegistry.ResolveService(); + const auto assetDatabaseService = moduleRegistry.ResolveService(); + MetaCoreExpect(scenePersistenceService != nullptr, "应解析到 ScenePersistenceService"); + MetaCoreExpect(assetDatabaseService != nullptr, "应解析到 AssetDatabaseService"); + MetaCoreExpect(!scenePersistenceService->LoadStartupScene(editorContext), "空项目初始时不应加载到启动场景"); + + const std::filesystem::path bootstrapScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; + MetaCoreExpect(scenePersistenceService->SaveSceneAs(editorContext, bootstrapScenePath), "应能为默认场景创建二进制启动场景"); + MetaCoreExpect(assetDatabaseService->SetStartupScenePath(bootstrapScenePath), "应能设置 startup scene"); + MetaCoreExpect(assetDatabaseService->Refresh(), "刷新资产数据库应成功"); + + MetaCoreExpect(std::filesystem::exists(tempProjectRoot / bootstrapScenePath), "应生成 Main.mcscene"); + MetaCoreExpect(scenePersistenceService->LoadStartupScene(editorContext), "设置后应能加载 startup scene"); + MetaCoreExpect(scenePersistenceService->GetCurrentScenePath() == bootstrapScenePath, "startup scene 路径应正确"); + MetaCoreExpect(scene.GetGameObjects().size() == 6, "bootstrap scene 应保存默认场景内容"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + +void MetaCoreTestPrefabWorkflow() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCorePrefabWorkflowProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets" / "Prefabs"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Library"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"PrefabProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + MetaCore::MetaCoreGameObject& root = scene.CreateGameObject("PrefabRoot"); + root.MeshRenderer = MetaCore::MetaCoreMeshRendererComponent{}; + root.Transform.Position = glm::vec3(1.0F, 2.0F, 3.0F); + MetaCore::MetaCoreGameObject& child = scene.CreateGameObject("PrefabChild", root.Id); + child.Light = MetaCore::MetaCoreLightComponent{}; + + editorContext.SelectOnly(root.Id); + + const auto prefabService = moduleRegistry.ResolveService(); + const auto assetDatabaseService = moduleRegistry.ResolveService(); + const auto packageService = moduleRegistry.ResolveService(); + const auto reflectionRegistry = moduleRegistry.ResolveService(); + MetaCoreExpect(prefabService != nullptr, "应解析到 PrefabService"); + MetaCoreExpect(prefabService->SupportsPrefabWorkflows(), "PrefabService 应支持工作流"); + MetaCoreExpect(assetDatabaseService != nullptr, "应解析到 AssetDatabaseService"); + MetaCoreExpect(packageService != nullptr, "应解析到 PackageService"); + MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry"); + + const std::filesystem::path prefabPath = std::filesystem::path("Assets") / "Prefabs" / "PrefabRoot.mcprefab"; + const auto createdPrefabPath = prefabService->CreatePrefabFromSelection(editorContext, prefabPath); + MetaCoreExpect(createdPrefabPath.has_value(), "应能从选中对象创建 prefab"); + MetaCoreExpect(std::filesystem::exists(tempProjectRoot / prefabPath), "应写出 mcprefab 文件"); + + MetaCoreExpect(assetDatabaseService->Refresh(), "创建 prefab 后应能刷新资产数据库"); + const auto prefabRecord = assetDatabaseService->FindAssetByRelativePath(prefabPath); + MetaCoreExpect(prefabRecord.has_value(), "应能找到 prefab 资产记录"); + MetaCoreExpect(prefabRecord->Type == "prefab", "prefab 资产类型应正确"); + + const auto instantiatedRootId = prefabService->InstantiatePrefab(editorContext, prefabRecord->Guid, std::nullopt); + MetaCoreExpect(instantiatedRootId.has_value(), "应能实例化 prefab"); + MetaCore::MetaCoreGameObject* instantiatedRoot = scene.FindGameObject(*instantiatedRootId); + MetaCoreExpect(instantiatedRoot != nullptr, "实例化后应能找到根对象"); + MetaCoreExpect(instantiatedRoot->PrefabInstance.has_value(), "实例根应带有 prefab 元数据"); + + instantiatedRoot->Transform.Position = glm::vec3(8.0F, 9.0F, 10.0F); + editorContext.SelectOnly(instantiatedRoot->Id); + MetaCoreExpect(prefabService->ApplySelectedPrefabInstance(editorContext), "应能应用 prefab 实例"); + + const auto prefabPackage = packageService->ReadPackage(tempProjectRoot / prefabPath); + MetaCoreExpect(prefabPackage.has_value(), "Apply 后应能读取 prefab 包"); + MetaCore::MetaCorePrefabDocument appliedPrefabDocument; + MetaCoreExpect( + !prefabPackage->PayloadSections.empty() && + MetaCore::MetaCoreDeserializeFromBytes( + prefabPackage->PayloadSections.front(), + appliedPrefabDocument, + reflectionRegistry->GetTypeRegistry() + ), + "Apply 后 prefab payload 应可反序列化" + ); + MetaCoreExpect(!appliedPrefabDocument.GameObjects.empty(), "Apply 后 prefab 应保留对象数据"); + MetaCoreExpectVec3Near( + appliedPrefabDocument.GameObjects.front().Transform.Position, + glm::vec3(8.0F, 9.0F, 10.0F), + "Apply 后 prefab 资源应写入修改过的 transform" + ); + + const auto secondInstanceRootId = prefabService->InstantiatePrefab(editorContext, prefabRecord->Guid, std::nullopt); + MetaCoreExpect(secondInstanceRootId.has_value(), "应用后应仍能再次实例化 prefab"); + MetaCore::MetaCoreGameObject* secondInstanceRoot = scene.FindGameObject(*secondInstanceRootId); + MetaCoreExpect(secondInstanceRoot != nullptr, "第二个实例应存在"); + MetaCoreExpectVec3Near(secondInstanceRoot->Transform.Position, glm::vec3(8.0F, 9.0F, 10.0F), "Apply 后新实例应继承修改过的 transform"); + + secondInstanceRoot->Transform.Position = glm::vec3(-1.0F, -2.0F, -3.0F); + editorContext.SelectOnly(secondInstanceRoot->Id); + MetaCoreExpect(prefabService->RevertSelectedPrefabInstance(editorContext), "应能还原 prefab 实例"); + + MetaCore::MetaCoreGameObject* revertedRoot = scene.FindGameObject(editorContext.GetActiveObjectId()); + MetaCoreExpect(revertedRoot != nullptr, "还原后应重新选中实例根"); + MetaCoreExpectVec3Near(revertedRoot->Transform.Position, glm::vec3(8.0F, 9.0F, 10.0F), "Revert 后实例应恢复为 prefab 内容"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + +void MetaCoreTestComponentRegistryOperations() { + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + MetaCore::MetaCoreGameObject& object = scene.CreateGameObject("RegistryObject"); + editorContext.SelectOnly(object.Id); + + const auto componentRegistry = moduleRegistry.ResolveService(); + MetaCoreExpect(componentRegistry != nullptr, "应解析到 ComponentTypeRegistry"); + + const auto* cameraDescriptor = componentRegistry->FindDescriptor("Camera"); + const auto* lightDescriptor = componentRegistry->FindDescriptor("Light"); + MetaCoreExpect(cameraDescriptor != nullptr, "应能找到 Camera 描述符"); + MetaCoreExpect(lightDescriptor != nullptr, "应能找到 Light 描述符"); + MetaCoreExpect(!cameraDescriptor->HasComponent(object), "初始时不应有 Camera"); + + const bool addedCamera = editorContext.ExecuteSnapshotCommand("添加 Camera", [&]() { + return cameraDescriptor->AddComponent(object); + }); + MetaCoreExpect(addedCamera, "应能通过注册表添加 Camera"); + MetaCoreExpect(object.Camera.has_value(), "添加后对象应拥有 Camera"); + + const bool addedLight = editorContext.ExecuteSnapshotCommand("添加 Light", [&]() { + return lightDescriptor->AddComponent(object); + }); + MetaCoreExpect(addedLight, "应能通过注册表添加 Light"); + MetaCoreExpect(object.Light.has_value(), "添加后对象应拥有 Light"); + + const bool removedCamera = editorContext.ExecuteSnapshotCommand("移除 Camera", [&]() { + return cameraDescriptor->RemoveComponent(object); + }); + MetaCoreExpect(removedCamera, "应能通过注册表移除 Camera"); + MetaCoreExpect(!object.Camera.has_value(), "移除后对象不应再拥有 Camera"); + + const bool readdedCamera = editorContext.ExecuteSnapshotCommand("重新添加 Camera", [&]() { + return cameraDescriptor->AddComponent(object); + }); + MetaCoreExpect(readdedCamera, "应能重新添加 Camera"); + object.Camera->FieldOfViewDegrees = 77.0F; + object.Camera->NearClip = 0.25F; + object.Camera->FarClip = 250.0F; + object.Camera->IsPrimary = true; + MetaCoreExpect(componentRegistry->CopyComponent("Camera", object), "应能复制 Camera 组件值"); + + MetaCore::MetaCoreGameObject pasteTarget = scene.CreateGameObject("PasteTarget"); + MetaCore::MetaCoreGameObject secondPasteTarget = scene.CreateGameObject("SecondPasteTarget"); + MetaCoreExpect(!pasteTarget.Camera.has_value(), "新对象初始不应拥有 Camera"); + MetaCoreExpect(!secondPasteTarget.Camera.has_value(), "第二个新对象初始不应拥有 Camera"); + MetaCoreExpect(componentRegistry->CanPasteComponent("Camera"), "复制后应可粘贴 Camera"); + MetaCoreExpect(componentRegistry->PasteComponent("Camera", pasteTarget), "应能粘贴 Camera 组件值"); + MetaCoreExpect(componentRegistry->PasteComponent("Camera", secondPasteTarget), "应能向第二个对象粘贴 Camera 组件值"); + MetaCoreExpect(pasteTarget.Camera.has_value(), "粘贴后目标对象应拥有 Camera"); + MetaCoreExpect(secondPasteTarget.Camera.has_value(), "第二个目标对象粘贴后应拥有 Camera"); + MetaCoreExpect(std::abs(pasteTarget.Camera->FieldOfViewDegrees - 77.0F) <= 0.0001F, "粘贴后 FOV 应一致"); + MetaCoreExpect(std::abs(pasteTarget.Camera->NearClip - 0.25F) <= 0.0001F, "粘贴后 NearClip 应一致"); + MetaCoreExpect(std::abs(pasteTarget.Camera->FarClip - 250.0F) <= 0.0001F, "粘贴后 FarClip 应一致"); + MetaCoreExpect(pasteTarget.Camera->IsPrimary, "粘贴后 IsPrimary 应一致"); + MetaCoreExpect(std::abs(secondPasteTarget.Camera->FieldOfViewDegrees - 77.0F) <= 0.0001F, "第二个对象粘贴后 FOV 应一致"); + + MetaCoreExpect(cameraDescriptor->ResetComponent != nullptr, "Camera descriptor 应提供 Reset"); + pasteTarget.Camera->FieldOfViewDegrees = 10.0F; + MetaCoreExpect(cameraDescriptor->ResetComponent(pasteTarget), "应能重置 Camera"); + MetaCoreExpect(std::abs(pasteTarget.Camera->FieldOfViewDegrees - 60.0F) <= 0.0001F, "重置后 Camera 应恢复默认值"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); +} + +void MetaCoreTestRuntimeDataTypeSerialization() { + MetaCore::MetaCoreTypeRegistry registry; + MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + + MetaCore::MetaCoreRuntimeDataValue value; + value.Type = MetaCore::MetaCoreRuntimeValueType::Vec3; + value.Vec3Value = glm::vec3(1.0F, 2.0F, 3.0F); + value.SourceTimestamp = 42; + + const auto serialized = MetaCore::MetaCoreSerializeToBytes(value, registry); + MetaCoreExpect(serialized.has_value(), "RuntimeDataValue 应可序列化"); + + MetaCore::MetaCoreRuntimeDataValue roundTripValue; + MetaCoreExpect( + MetaCore::MetaCoreDeserializeFromBytes(*serialized, roundTripValue, registry), + "RuntimeDataValue 应可反序列化" + ); + MetaCoreExpect(roundTripValue.Type == MetaCore::MetaCoreRuntimeValueType::Vec3, "RuntimeDataValue 类型应保留"); + MetaCoreExpectVec3Near(roundTripValue.Vec3Value, glm::vec3(1.0F, 2.0F, 3.0F), "RuntimeDataValue Vec3 应保留"); + MetaCoreExpect(roundTripValue.SourceTimestamp == 42, "RuntimeDataValue 时间戳应保留"); +} + +void MetaCoreTestRuntimeDataProjectDocumentSerialization() { + MetaCore::MetaCoreTypeRegistry registry; + MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + + MetaCore::MetaCoreRuntimeDataSourcesDocument sourcesDocument; + sourcesDocument.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + "mock-source", + "mock", + "Mock Source", + {MetaCore::MetaCoreDataSourceSetting{"seed", "demo"}}, + true, + 1000 + }); + sourcesDocument.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "pump.position", + "mock-source", + "pump.position", + MetaCore::MetaCoreRuntimeValueType::Vec3 + }); + + const auto serialized = MetaCore::MetaCoreSerializeToBytes(sourcesDocument, registry); + MetaCoreExpect(serialized.has_value(), "RuntimeDataSourcesDocument 应可序列化"); + + MetaCore::MetaCoreRuntimeDataSourcesDocument roundTripDocument; + MetaCoreExpect( + MetaCore::MetaCoreDeserializeFromBytes(*serialized, roundTripDocument, registry), + "RuntimeDataSourcesDocument 应可反序列化" + ); + MetaCoreExpect(roundTripDocument.Sources.size() == 1, "RuntimeDataSourcesDocument 应保留 source"); + MetaCoreExpect(roundTripDocument.DataPoints.size() == 1, "RuntimeDataSourcesDocument 应保留 data point"); + MetaCoreExpect(roundTripDocument.Sources.front().Id == "mock-source", "RuntimeDataSourcesDocument source id 应保留"); +} + +void MetaCoreTestRuntimeDataBinaryDocumentIo() { + MetaCore::MetaCoreTypeRegistry registry; + MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + + const std::filesystem::path tempDirectory = std::filesystem::temp_directory_path() / "MetaCoreRuntimeDataIo"; + std::filesystem::remove_all(tempDirectory); + std::filesystem::create_directories(tempDirectory); + + const std::filesystem::path sourcesPath = tempDirectory / "DataSources.mcruntime"; + const std::filesystem::path bindingsPath = tempDirectory / "Bindings.mcruntime"; + + MetaCore::MetaCoreRuntimeDataSourcesDocument sourcesDocument; + sourcesDocument.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + "mock-source", + "mock", + "Mock Source", + {}, + true, + 1000 + }); + sourcesDocument.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "cube.position", + "mock-source", + "cube.position", + MetaCore::MetaCoreRuntimeValueType::Vec3 + }); + + MetaCore::MetaCoreRuntimeBindingsDocument bindingsDocument; + bindingsDocument.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.cube.position", + "cube.position", + 3, + MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeDataSourcesDocument(sourcesPath, sourcesDocument, registry), + "RuntimeDataSourcesDocument 应可写出二进制文件" + ); + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeBindingsDocument(bindingsPath, bindingsDocument, registry), + "RuntimeBindingsDocument 应可写出二进制文件" + ); + + const auto loadedSources = MetaCore::MetaCoreReadRuntimeDataSourcesDocument(sourcesPath, registry); + const auto loadedBindings = MetaCore::MetaCoreReadRuntimeBindingsDocument(bindingsPath, registry); + MetaCoreExpect(loadedSources.has_value(), "RuntimeDataSourcesDocument 应可读回二进制文件"); + MetaCoreExpect(loadedBindings.has_value(), "RuntimeBindingsDocument 应可读回二进制文件"); + MetaCoreExpect(loadedSources->Sources.size() == 1, "读回的 RuntimeDataSourcesDocument 应保留 source"); + MetaCoreExpect(loadedBindings->Bindings.size() == 1, "读回的 RuntimeBindingsDocument 应保留 binding"); + + std::filesystem::remove_all(tempDirectory); +} + +void MetaCoreTestRuntimeProjectDocumentIo() { + MetaCore::MetaCoreTypeRegistry registry; + MetaCore::MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + + const std::filesystem::path tempPath = std::filesystem::temp_directory_path() / "MetaCoreRuntimeProjectDocument.mcruntimecfg"; + MetaCore::MetaCoreRuntimeProjectDocument writtenDocument; + writtenDocument.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; + writtenDocument.DataSourcesPath = std::filesystem::path("Runtime") / "DataSources.mcruntime"; + writtenDocument.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; + writtenDocument.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; + + MetaCoreExpect(MetaCore::MetaCoreWriteRuntimeProjectDocument(tempPath, writtenDocument, registry), "应能写入 RuntimeProject 文档"); + + const auto loadedDocument = MetaCore::MetaCoreReadRuntimeProjectDocument(tempPath, registry); + MetaCoreExpect(loadedDocument.has_value(), "应能读回 RuntimeProject 文档"); + MetaCoreExpect(loadedDocument->StartupScenePath == writtenDocument.StartupScenePath, "StartupScenePath 应一致"); + MetaCoreExpect(loadedDocument->DiagnosticsPath == writtenDocument.DiagnosticsPath, "DiagnosticsPath 应一致"); + + std::filesystem::remove(tempPath); +} + +void MetaCoreTestRuntimeDiagnosticsSnapshotIo() { + MetaCore::MetaCoreTypeRegistry registry; + MetaCore::MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + + const std::filesystem::path tempPath = std::filesystem::temp_directory_path() / "MetaCoreRuntimeDiagnostics.mcruntimestate"; + MetaCore::MetaCoreRuntimeDiagnosticsSnapshot writtenSnapshot; + writtenSnapshot.SourceStatuses.push_back(MetaCore::MetaCoreRuntimeDataSourceStatus{ + "tcp-source", + MetaCore::MetaCoreRuntimeDataSourceState::Connected, + 10, + 20, + {} + }); + writtenSnapshot.BindingStatuses.push_back(MetaCore::MetaCoreRuntimeBindingStatus{ + "binding.cube.position", + false, + true, + 100, + 1000, + "stale" + }); + writtenSnapshot.HasFaults = true; + + MetaCoreExpect(MetaCore::MetaCoreWriteRuntimeDiagnosticsSnapshot(tempPath, writtenSnapshot, registry), "应能写入 Runtime diagnostics"); + const auto loadedSnapshot = MetaCore::MetaCoreReadRuntimeDiagnosticsSnapshot(tempPath, registry); + MetaCoreExpect(loadedSnapshot.has_value(), "应能读回 Runtime diagnostics"); + MetaCoreExpect(loadedSnapshot->HasFaults, "Runtime diagnostics fault 状态应保留"); + MetaCoreExpect(loadedSnapshot->SourceStatuses.size() == 1, "应包含一个 source status"); + MetaCoreExpect(loadedSnapshot->BindingStatuses.size() == 1, "应包含一个 binding status"); + + std::filesystem::remove(tempPath); +} + +void MetaCoreTestRuntimeDataDispatcherConstruction() { + MetaCore::MetaCoreScene scene; + scene.CreateGameObject("Pump"); + + MetaCore::MetaCoreRuntimeDataDispatcher dispatcher(scene); + dispatcher.SetDataPointDefinitions({ + MetaCore::MetaCoreDataPointDefinition{ + "pump.position", + "mock-source", + "pump.position", + MetaCore::MetaCoreRuntimeValueType::Vec3 + } + }); + dispatcher.SetBindingDefinitions({ + MetaCore::MetaCoreSceneBindingDefinition{ + "binding.pump.position", + "pump.position", + scene.GetGameObjects().front().Id, + MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + } + }); + + MetaCoreExpect(dispatcher.GetBindingStatuses().size() == 1, "Dispatcher 应构建一个 binding status"); + MetaCoreExpect(dispatcher.GetBindingStatuses().front().Healthy, "初始 binding status 应为 healthy"); +} + +void MetaCoreTestRuntimeDataDispatcherAppliesUpdates() { + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreGameObject& cube = scene.CreateGameObject("Cube"); + cube.MeshRenderer = MetaCore::MetaCoreMeshRendererComponent{}; + + MetaCore::MetaCoreRuntimeDataDispatcher dispatcher(scene); + dispatcher.SetDataPointDefinitions({ + MetaCore::MetaCoreDataPointDefinition{ + "cube.position", + "mock-source", + "cube.position", + MetaCore::MetaCoreRuntimeValueType::Vec3 + }, + MetaCore::MetaCoreDataPointDefinition{ + "cube.visible", + "mock-source", + "cube.visible", + MetaCore::MetaCoreRuntimeValueType::Bool + } + }); + dispatcher.SetBindingDefinitions({ + MetaCore::MetaCoreSceneBindingDefinition{ + "binding.position", + "cube.position", + cube.Id, + MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }, + MetaCore::MetaCoreSceneBindingDefinition{ + "binding.visible", + "cube.visible", + cube.Id, + MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + } + }); + + dispatcher.ApplyUpdates({ + MetaCore::MetaCoreRuntimeDataUpdate{ + "cube.position", + MetaCore::MetaCoreRuntimeDataValue{ + MetaCore::MetaCoreRuntimeValueType::Vec3, + false, + 0, + 0.0, + {}, + glm::vec3(4.0F, 5.0F, 6.0F), + 100, + MetaCore::MetaCoreRuntimeDataQuality::Good + }, + 1 + }, + MetaCore::MetaCoreRuntimeDataUpdate{ + "cube.visible", + MetaCore::MetaCoreRuntimeDataValue{ + MetaCore::MetaCoreRuntimeValueType::Bool, + false, + 0, + 0.0, + {}, + glm::vec3(0.0F, 0.0F, 0.0F), + 100, + MetaCore::MetaCoreRuntimeDataQuality::Good + }, + 2 + } + }); + + MetaCoreExpectVec3Near(cube.Transform.Position, glm::vec3(4.0F, 5.0F, 6.0F), "Dispatcher 应更新 Transform.Position"); + MetaCoreExpect(!cube.MeshRenderer->Visible, "Dispatcher 应更新 MeshRenderer.Visible"); +} + +void MetaCoreTestMockRuntimeDataSourceAdapterEmitsUpdates() { + MetaCore::MetaCoreMockRuntimeDataSourceAdapter adapter; + MetaCoreExpect(adapter.Configure(MetaCore::MetaCoreDataSourceDefinition{ + "mock-source", + "mock", + "Mock Source", + {}, + true, + 1000 + }), "Mock adapter 应能配置"); + MetaCoreExpect(adapter.Connect(), "Mock adapter 应能连接"); + adapter.Tick(0.25); + + const auto updates = adapter.PollUpdates(); + MetaCoreExpect(updates.size() >= 3, "Mock adapter 应输出至少三条更新"); + MetaCoreExpect(adapter.GetStatus().State == MetaCore::MetaCoreRuntimeDataSourceState::Connected, "Mock adapter 状态应为 connected"); +} + +void MetaCoreTestRuntimeDataDispatcherMarksStaleBindings() { + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreGameObject& cube = scene.CreateGameObject("Cube"); + cube.MeshRenderer = MetaCore::MetaCoreMeshRendererComponent{}; + + MetaCore::MetaCoreRuntimeDataDispatcher dispatcher(scene); + dispatcher.SetDataPointDefinitions({ + MetaCore::MetaCoreDataPointDefinition{ + "cube.visible", + "mock-source", + "cube.visible", + MetaCore::MetaCoreRuntimeValueType::Bool + } + }); + dispatcher.SetBindingDefinitions({ + MetaCore::MetaCoreSceneBindingDefinition{ + "binding.visible", + "cube.visible", + cube.Id, + MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + } + }); + + dispatcher.ApplyUpdates({ + MetaCore::MetaCoreRuntimeDataUpdate{ + "cube.visible", + MetaCore::MetaCoreRuntimeDataValue{ + MetaCore::MetaCoreRuntimeValueType::Bool, + true, + 0, + 0.0, + {}, + glm::vec3(0.0F, 0.0F, 0.0F), + 100, + MetaCore::MetaCoreRuntimeDataQuality::Good + }, + 1 + } + }); + dispatcher.TickStaleness(1500); + + MetaCoreExpect(dispatcher.GetBindingStatuses().front().Stale, "Binding 应在超时后标记 stale"); + MetaCoreExpect(dispatcher.HasBindingFaults(), "存在 stale binding 时应报告 faults"); +} + +void MetaCoreTestMockRuntimeDataSourceAdapterDegradedState() { + MetaCore::MetaCoreMockRuntimeDataSourceAdapter adapter; + MetaCoreExpect(adapter.Configure(MetaCore::MetaCoreDataSourceDefinition{ + "mock-source", + "mock", + "Mock Source", + {}, + true, + 1000 + }), "Mock adapter 应能配置"); + MetaCoreExpect(adapter.Connect(), "Mock adapter 应能连接"); + adapter.SetEmitUpdates(false); + adapter.Tick(0.25); + + MetaCoreExpect(adapter.GetStatus().State == MetaCore::MetaCoreRuntimeDataSourceState::Degraded, "停止发包后应进入 degraded 状态"); + MetaCoreExpect(adapter.PollUpdates().empty(), "Degraded 状态下不应继续输出更新"); +} + +void MetaCoreTestFileReplayRuntimeDataSourceAdapterEmitsReplayFrames() { + const std::filesystem::path tempDirectory = std::filesystem::temp_directory_path() / "MetaCoreRuntimeReplay"; + std::filesystem::remove_all(tempDirectory); + std::filesystem::create_directories(tempDirectory); + + const std::filesystem::path replayPath = tempDirectory / "RuntimeReplay.mcstream"; + { + std::ofstream replayFile(replayPath, std::ios::trunc); + replayFile << "0.00 cube.visible bool true\n"; + replayFile << "0.25 cube.position vec3 1.0 2.0 3.0\n"; + } + + MetaCore::MetaCoreFileReplayRuntimeDataSourceAdapter adapter; + MetaCoreExpect(adapter.Configure(MetaCore::MetaCoreDataSourceDefinition{ + "replay-source", + "file_replay", + "Replay Source", + {MetaCore::MetaCoreDataSourceSetting{"file_path", replayPath.string()}}, + true, + 1000 + }), "File replay adapter 应能配置"); + MetaCoreExpect(adapter.Connect(), "File replay adapter 应能连接"); + + adapter.Tick(0.10); + auto updates = adapter.PollUpdates(); + MetaCoreExpect(updates.size() == 1, "File replay adapter 首批应输出一条更新"); + MetaCoreExpect(updates.front().DataPointId == "cube.visible", "首批更新应命中第一个数据点"); + + adapter.Tick(0.20); + updates = adapter.PollUpdates(); + MetaCoreExpect(updates.size() == 1, "File replay adapter 第二批应输出一条更新"); + MetaCoreExpect(updates.front().DataPointId == "cube.position", "第二批更新应命中第二个数据点"); + MetaCoreExpect( + adapter.GetStatus().State == MetaCore::MetaCoreRuntimeDataSourceState::Connected, + "File replay adapter 回放期间应保持 connected" + ); + + adapter.Tick(1.0); + MetaCoreExpect( + adapter.PollUpdates().empty(), + "File replay adapter 回放结束后不应继续输出更新" + ); + MetaCoreExpect( + adapter.GetStatus().State == MetaCore::MetaCoreRuntimeDataSourceState::Degraded, + "File replay adapter 回放结束后应进入 degraded 状态" + ); + + std::filesystem::remove_all(tempDirectory); +} + +void MetaCoreTestRuntimeDiagnosticsSnapshotReportsFaults() { + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreGameObject& cube = scene.CreateGameObject("Cube"); + cube.MeshRenderer = MetaCore::MetaCoreMeshRendererComponent{}; + + MetaCore::MetaCoreRuntimeDataDispatcher dispatcher(scene); + dispatcher.SetDataPointDefinitions({ + MetaCore::MetaCoreDataPointDefinition{ + "cube.visible", + "mock-source", + "cube.visible", + MetaCore::MetaCoreRuntimeValueType::Bool + } + }); + dispatcher.SetBindingDefinitions({ + MetaCore::MetaCoreSceneBindingDefinition{ + "binding.visible", + "cube.visible", + cube.Id, + MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + } + }); + dispatcher.TickStaleness(2000); + + const auto diagnostics = dispatcher.BuildDiagnosticsSnapshot({ + MetaCore::MetaCoreRuntimeDataSourceStatus{ + "mock-source", + MetaCore::MetaCoreRuntimeDataSourceState::Degraded, + 1, + 0, + "Degraded source" + } + }); + MetaCoreExpect(diagnostics.HasFaults, "Diagnostics snapshot 应报告 faults"); + MetaCoreExpect(diagnostics.SourceStatuses.size() == 1, "Diagnostics snapshot 应保留 source status"); + MetaCoreExpect(diagnostics.BindingStatuses.size() == 1, "Diagnostics snapshot 应保留 binding status"); +} + +void MetaCoreTestRuntimeDataBinaryDocumentRejectsCorruption() { + MetaCore::MetaCoreTypeRegistry registry; + MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + + const std::filesystem::path tempDirectory = std::filesystem::temp_directory_path() / "MetaCoreRuntimeDataCorruptIo"; + std::filesystem::remove_all(tempDirectory); + std::filesystem::create_directories(tempDirectory); + + const std::filesystem::path corruptSourcesPath = tempDirectory / "CorruptSources.mcruntime"; + { + std::ofstream output(corruptSourcesPath, std::ios::binary | std::ios::trunc); + output << "not-a-valid-runtime-config"; + } + + const auto loadedSources = MetaCore::MetaCoreReadRuntimeDataSourcesDocument(corruptSourcesPath, registry); + MetaCoreExpect(!loadedSources.has_value(), "损坏的 RuntimeDataSourcesDocument 二进制应被拒绝"); + + std::filesystem::remove_all(tempDirectory); +} + +void MetaCoreTestEditorContextRuntimeDataConfigSaveLoad() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorConfigProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Library"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"RuntimeConfigProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + MetaCoreExpect(editorContext.EnsureRuntimeDataConfigLoaded(), "EditorContext 应能加载 Runtime 配置"); + auto& sources = editorContext.AccessRuntimeDataSourcesDocument(); + auto& bindings = editorContext.AccessRuntimeBindingsDocument(); + sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + "replay-source", + "file_replay", + "Replay Source", + {MetaCore::MetaCoreDataSourceSetting{"file_path", "Runtime/RuntimeReplay.mcstream"}}, + true, + 1000 + }); + sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "cube.position", + "replay-source", + "cube.position", + MetaCore::MetaCoreRuntimeValueType::Vec3 + }); + bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.cube.position", + "cube.position", + 3, + MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + + MetaCoreExpect(editorContext.SaveRuntimeDataConfig(), "EditorContext 应能保存 Runtime 配置"); + + MetaCore::MetaCoreTypeRegistry registry; + MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + const auto loadedSources = MetaCore::MetaCoreReadRuntimeDataSourcesDocument( + tempProjectRoot / "Runtime" / "DataSources.mcruntime", + registry + ); + const auto loadedBindings = MetaCore::MetaCoreReadRuntimeBindingsDocument( + tempProjectRoot / "Runtime" / "Bindings.mcruntime", + registry + ); + const auto loadedProjectRuntime = MetaCore::MetaCoreReadRuntimeProjectDocument( + tempProjectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", + registry + ); + MetaCoreExpect(loadedSources.has_value(), "保存后应能读回 Runtime data sources"); + MetaCoreExpect(loadedBindings.has_value(), "保存后应能读回 Runtime bindings"); + MetaCoreExpect(loadedProjectRuntime.has_value(), "保存后应能读回 Runtime project"); + MetaCoreExpect(loadedSources->Sources.size() == 1, "保存后应保留 source"); + MetaCoreExpect(loadedBindings->Bindings.size() == 1, "保存后应保留 binding"); + MetaCoreExpect(loadedProjectRuntime->StartupScenePath == std::filesystem::path("Scenes") / "Main.mcscene", "保存后应写入默认 startup scene 路径"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + +void MetaCoreTestRuntimeDataConfigValidationRejectsBrokenBinding() { + MetaCore::MetaCoreRuntimeDataSourcesDocument sources; + sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + "replay-source", + "file_replay", + "Replay Source", + {MetaCore::MetaCoreDataSourceSetting{"file_path", "Runtime/RuntimeReplay.mcstream"}}, + true, + 1000 + }); + + MetaCore::MetaCoreRuntimeBindingsDocument bindings; + bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.invalid", + "missing.point", + 1, + MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + + const auto issues = MetaCore::MetaCoreValidateRuntimeDataDocuments(sources, bindings); + MetaCoreExpect(!issues.empty(), "坏 Runtime 配置应产生校验问题"); + MetaCoreExpect( + std::any_of(issues.begin(), issues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error; + }), + "坏 Runtime 配置应产生至少一个错误级问题" + ); +} + +void MetaCoreTestRuntimeDataConfigValidationRejectsMissingReplayFilePath() { + MetaCore::MetaCoreRuntimeDataSourcesDocument sources; + sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + "replay-source", + "file_replay", + "Replay Source", + {}, + true, + 1000 + }); + + const auto issues = MetaCore::MetaCoreValidateRuntimeDataDocuments(sources, {}); + MetaCoreExpect( + std::any_of(issues.begin(), issues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && + issue.Message.find("file_path") != std::string::npos; + }), + "缺失 replay file_path 应产生错误" + ); +} + +void MetaCoreTestRuntimeDataConfigValidationRejectsMissingTcpPort() { + MetaCore::MetaCoreRuntimeDataSourcesDocument sources; + sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + "tcp-source", + "tcp", + "Tcp Source", + {MetaCore::MetaCoreDataSourceSetting{"host", "127.0.0.1"}}, + true, + 1000 + }); + + const auto issues = MetaCore::MetaCoreValidateRuntimeDataDocuments(sources, {}); + MetaCoreExpect( + std::any_of(issues.begin(), issues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && + issue.Message.find("port") != std::string::npos; + }), + "缺失 tcp port 应产生错误" + ); +} + +void MetaCoreTestTcpRuntimeDataSourceAdapterReadsSocketStream() { + WSADATA wsaData{}; + MetaCoreExpect(WSAStartup(MAKEWORD(2, 2), &wsaData) == 0, "Smoke test 应能初始化 Winsock"); + + SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + MetaCoreExpect(listenSocket != INVALID_SOCKET, "Smoke test 应能创建监听 socket"); + + sockaddr_in address{}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + address.sin_port = 0; + MetaCoreExpect(bind(listenSocket, reinterpret_cast(&address), sizeof(address)) != SOCKET_ERROR, "Smoke test bind 应成功"); + MetaCoreExpect(listen(listenSocket, 1) != SOCKET_ERROR, "Smoke test listen 应成功"); + + int addressLength = sizeof(address); + MetaCoreExpect( + getsockname(listenSocket, reinterpret_cast(&address), &addressLength) != SOCKET_ERROR, + "Smoke test getsockname 应成功" + ); + const unsigned short port = ntohs(address.sin_port); + + std::thread serverThread([listenSocket]() { + SOCKET clientSocket = accept(listenSocket, nullptr, nullptr); + if (clientSocket != INVALID_SOCKET) { + const char* payload = + "cube.visible bool true\n" + "cube.position vec3 1.0 2.0 3.0\n"; + send(clientSocket, payload, static_cast(std::strlen(payload)), 0); + shutdown(clientSocket, SD_SEND); + closesocket(clientSocket); + } + closesocket(listenSocket); + }); + + MetaCore::MetaCoreTcpRuntimeDataSourceAdapter adapter; + MetaCoreExpect(adapter.Configure(MetaCore::MetaCoreDataSourceDefinition{ + "tcp-source", + "tcp", + "Tcp Source", + { + MetaCore::MetaCoreDataSourceSetting{"host", "127.0.0.1"}, + MetaCore::MetaCoreDataSourceSetting{"port", std::to_string(port)} + }, + true, + 1000 + }), "Tcp adapter 应能配置"); + MetaCoreExpect(adapter.Connect(), "Tcp adapter 应能连接"); + + for (int index = 0; index < 20; ++index) { + adapter.Tick(0.05); + const auto updates = adapter.PollUpdates(); + if (!updates.empty()) { + MetaCoreExpect(updates.size() == 2, "Tcp adapter 应读到两条更新"); + MetaCoreExpect(updates[0].DataPointId == "cube.visible", "Tcp adapter 第一条更新应正确"); + MetaCoreExpect(updates[1].DataPointId == "cube.position", "Tcp adapter 第二条更新应正确"); + MetaCoreExpect(updates[1].Value.Type == MetaCore::MetaCoreRuntimeValueType::Vec3, "Tcp adapter 类型应正确"); + serverThread.join(); + WSACleanup(); + return; + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + serverThread.join(); + WSACleanup(); + MetaCoreExpect(false, "Tcp adapter 应在轮询内收到更新"); +} + +void MetaCoreTestEditorContextRejectsInvalidRuntimeDataSave() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorInvalidConfigProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Library"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"RuntimeInvalidConfigProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + MetaCoreExpect(editorContext.EnsureRuntimeDataConfigLoaded(), "EditorContext 应能加载 Runtime 配置"); + auto& bindings = editorContext.AccessRuntimeBindingsDocument(); + bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.invalid", + "missing.point", + 3, + MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + + MetaCoreExpect(!editorContext.SaveRuntimeDataConfig(), "坏 Runtime 配置应被拒绝保存"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + } // namespace int main() { MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); - MetaCoreExpect(scene.GetGameObjects().size() == 3, "默认场景应包含三个对象"); + MetaCoreExpect(scene.GetGameObjects().size() == 6, "默认场景应包含六个对象"); bool hasCamera = false; bool hasLight = false; @@ -153,6 +1431,33 @@ int main() { MetaCoreTestSceneEditApi(); MetaCoreTestSelectionStateMachine(); MetaCoreTestUndoRedo(); + MetaCoreTestBuiltinModuleComposition(); + MetaCoreTestScenePackageProjectStartupLoad(); + MetaCoreTestComponentRegistryDescriptors(); + MetaCoreTestScenePersistenceRoundTrip(); + MetaCoreTestImportPipelineAndCook(); + MetaCoreTestBootstrapStartupSceneCreation(); + MetaCoreTestPrefabWorkflow(); + MetaCoreTestComponentRegistryOperations(); + MetaCoreTestRuntimeDataTypeSerialization(); + MetaCoreTestRuntimeDataProjectDocumentSerialization(); + MetaCoreTestRuntimeDataBinaryDocumentIo(); + MetaCoreTestRuntimeProjectDocumentIo(); + MetaCoreTestRuntimeDiagnosticsSnapshotIo(); + MetaCoreTestRuntimeDataDispatcherConstruction(); + MetaCoreTestRuntimeDataDispatcherAppliesUpdates(); + MetaCoreTestMockRuntimeDataSourceAdapterEmitsUpdates(); + MetaCoreTestRuntimeDataDispatcherMarksStaleBindings(); + MetaCoreTestMockRuntimeDataSourceAdapterDegradedState(); + MetaCoreTestFileReplayRuntimeDataSourceAdapterEmitsReplayFrames(); + MetaCoreTestRuntimeDiagnosticsSnapshotReportsFaults(); + MetaCoreTestRuntimeDataBinaryDocumentRejectsCorruption(); + MetaCoreTestEditorContextRuntimeDataConfigSaveLoad(); + MetaCoreTestRuntimeDataConfigValidationRejectsBrokenBinding(); + MetaCoreTestRuntimeDataConfigValidationRejectsMissingReplayFilePath(); + MetaCoreTestRuntimeDataConfigValidationRejectsMissingTcpPort(); + MetaCoreTestEditorContextRejectsInvalidRuntimeDataSave(); + MetaCoreTestTcpRuntimeDataSourceAdapterReadsSocketStream(); std::cout << "MetaCoreSmokeTests passed\n"; return 0; diff --git a/tools/MetaCoreHeaderTool/main.cpp b/tools/MetaCoreHeaderTool/main.cpp new file mode 100644 index 0000000..a56f990 --- /dev/null +++ b/tools/MetaCoreHeaderTool/main.cpp @@ -0,0 +1,216 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +enum class MetaCoreReflectedKind { + Struct, + Class, + Enum +}; + +struct MetaCoreReflectedField { + std::string Name{}; +}; + +struct MetaCoreReflectedType { + MetaCoreReflectedKind Kind = MetaCoreReflectedKind::Struct; + std::string Name{}; + std::vector Fields{}; +}; + +[[nodiscard]] std::string MetaCoreTrim(std::string value) { + auto isWhitespace = [](unsigned char character) { + return std::isspace(character) != 0; + }; + + while (!value.empty() && isWhitespace(static_cast(value.front()))) { + value.erase(value.begin()); + } + while (!value.empty() && isWhitespace(static_cast(value.back()))) { + value.pop_back(); + } + return value; +} + +[[nodiscard]] std::string MetaCoreBuildIncludePath(const std::filesystem::path& path) { + const std::string full = path.generic_string(); + constexpr std::string_view marker = "/Public/"; + const std::size_t markerIndex = full.find(marker); + if (markerIndex == std::string::npos) { + return path.filename().generic_string(); + } + return full.substr(markerIndex + marker.size()); +} + +[[nodiscard]] std::vector MetaCoreParseHeader(const std::filesystem::path& path) { + std::ifstream input(path); + if (!input.is_open()) { + throw std::runtime_error("Unable to open header: " + path.string()); + } + + const std::regex typeRegex(R"(^(struct|class|enum\s+class)\s+([A-Za-z_]\w*))"); + const std::regex fieldRegex(R"(^(.+?)\s+([A-Za-z_]\w*)\s*(?:\{.*\}|=.*)?;$)"); + + std::vector reflectedTypes; + std::optional pendingKind; + MetaCoreReflectedType* currentType = nullptr; + bool expectingField = false; + + std::string line; + while (std::getline(input, line)) { + const std::string trimmed = MetaCoreTrim(line); + if (trimmed.empty() || trimmed.starts_with("//")) { + continue; + } + + if (trimmed.starts_with("MC_STRUCT")) { + pendingKind = MetaCoreReflectedKind::Struct; + continue; + } + if (trimmed.starts_with("MC_CLASS")) { + pendingKind = MetaCoreReflectedKind::Class; + continue; + } + if (trimmed.starts_with("MC_ENUM")) { + pendingKind = MetaCoreReflectedKind::Enum; + continue; + } + if (trimmed.starts_with("MC_PROPERTY")) { + expectingField = true; + continue; + } + if (trimmed.starts_with("MC_GENERATED_BODY")) { + continue; + } + + if (pendingKind.has_value()) { + std::smatch match; + if (std::regex_search(trimmed, match, typeRegex)) { + reflectedTypes.push_back(MetaCoreReflectedType{ + pendingKind.value(), + match[2].str(), + {} + }); + currentType = &reflectedTypes.back(); + pendingKind.reset(); + expectingField = false; + continue; + } + } + + if (currentType != nullptr && trimmed == "};") { + currentType = nullptr; + expectingField = false; + continue; + } + + if (currentType == nullptr || currentType->Kind == MetaCoreReflectedKind::Enum || !expectingField) { + continue; + } + + std::smatch match; + if (!std::regex_search(trimmed, match, fieldRegex)) { + throw std::runtime_error("Unable to parse reflected field in " + path.string() + ": " + trimmed); + } + + currentType->Fields.push_back(MetaCoreReflectedField{ + match[2].str() + }); + expectingField = false; + } + + return reflectedTypes; +} + +[[nodiscard]] std::string MetaCoreBuildOutput( + const std::string& functionName, + const std::vector& headers +) { + std::vector reflectedTypes; + for (const auto& header : headers) { + const auto parsed = MetaCoreParseHeader(header); + reflectedTypes.insert(reflectedTypes.end(), parsed.begin(), parsed.end()); + } + + std::ostringstream output; + output << "#include \"MetaCoreFoundation/MetaCoreGeneratedReflection.h\"\n"; + output << "#include \"MetaCoreFoundation/MetaCoreReflection.h\"\n"; + for (const auto& header : headers) { + output << "#include \"" << MetaCoreBuildIncludePath(header) << "\"\n"; + } + output << "\nnamespace MetaCore {\n\n"; + output << "void " << functionName << "(MetaCoreTypeRegistry& registry) {\n"; + + for (const auto& reflectedType : reflectedTypes) { + if (reflectedType.Kind == MetaCoreReflectedKind::Enum) { + output << " MetaCoreRegisterGeneratedEnum<" << reflectedType.Name << ">(registry, \"" << reflectedType.Name << "\");\n"; + continue; + } + + output << " auto " << reflectedType.Name << "Builder = MetaCoreRegisterGeneratedStruct<" + << reflectedType.Name << ">(registry, \"" << reflectedType.Name << "\");\n"; + for (const auto& field : reflectedType.Fields) { + output << " " << reflectedType.Name << "Builder.Field<&" << reflectedType.Name << "::" + << field.Name << ">(\"" << field.Name << "\");\n"; + } + } + + output << "}\n\n} // namespace MetaCore\n"; + return output.str(); +} + +} // namespace + +int main(int argc, char** argv) { + std::filesystem::path outputPath; + std::string functionName; + std::vector headers; + + for (int index = 1; index < argc; ++index) { + const std::string_view argument(argv[index]); + if (argument == "--output" && index + 1 < argc) { + outputPath = argv[++index]; + continue; + } + if (argument == "--function" && index + 1 < argc) { + functionName = argv[++index]; + continue; + } + if (argument == "--module" && index + 1 < argc) { + ++index; + continue; + } + + headers.emplace_back(argv[index]); + } + + if (outputPath.empty() || functionName.empty() || headers.empty()) { + std::cerr << "Usage: MetaCoreHeaderTool --output --function [--module ] \n"; + return 1; + } + + try { + const std::string output = MetaCoreBuildOutput(functionName, headers); + std::ofstream file(outputPath, std::ios::trunc); + if (!file.is_open()) { + std::cerr << "Unable to open output file: " << outputPath << '\n'; + return 1; + } + file << output; + } catch (const std::exception& exception) { + std::cerr << exception.what() << '\n'; + return 1; + } + + return 0; +} diff --git a/tools/MetaCoreRuntimeConfigTool/main.cpp b/tools/MetaCoreRuntimeConfigTool/main.cpp new file mode 100644 index 0000000..de77dc5 --- /dev/null +++ b/tools/MetaCoreRuntimeConfigTool/main.cpp @@ -0,0 +1,187 @@ +#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" +#include "MetaCoreScene/MetaCoreScene.h" +#include "MetaCoreScene/MetaCoreScenePackage.h" + +#include +#include +#include + +namespace { + +[[nodiscard]] MetaCore::MetaCoreSceneDocument MetaCoreBuildPilotSceneDocument() { + MetaCore::MetaCoreSceneDocument document; + document.Name = "Main"; + const MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + document.GameObjects = scene.GetGameObjects(); + return document; +} + +} // namespace + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "Usage: MetaCoreRuntimeConfigTool [--tcp]\n"; + return 1; + } + + const bool generateTcpConfig = argc >= 3 && std::string_view(argv[2]) == "--tcp"; + + MetaCore::MetaCoreTypeRegistry registry; + MetaCore::MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + + MetaCore::MetaCoreRuntimeDataSourcesDocument sources; + sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + generateTcpConfig ? "tcp-source" : "replay-source", + generateTcpConfig ? "tcp" : "file_replay", + generateTcpConfig ? "Tcp Source" : "Replay Source", + generateTcpConfig + ? std::vector{ + MetaCore::MetaCoreDataSourceSetting{"host", "127.0.0.1"}, + MetaCore::MetaCoreDataSourceSetting{"port", "7001"} + } + : std::vector{ + MetaCore::MetaCoreDataSourceSetting{"file_path", "TestProject/Runtime/RuntimeReplay.mcstream"} + }, + true, + 1000 + }); + sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "cube.position", + generateTcpConfig ? "tcp-source" : "replay-source", + "cube.position", + MetaCore::MetaCoreRuntimeValueType::Vec3 + }); + sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "cube.visible", + generateTcpConfig ? "tcp-source" : "replay-source", + "cube.visible", + MetaCore::MetaCoreRuntimeValueType::Bool + }); + sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "cube.base_color", + generateTcpConfig ? "tcp-source" : "replay-source", + "cube.base_color", + MetaCore::MetaCoreRuntimeValueType::Vec3 + }); + sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "valve.visible", + generateTcpConfig ? "tcp-source" : "replay-source", + "valve.visible", + MetaCore::MetaCoreRuntimeValueType::Bool + }); + sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "tank.base_color", + generateTcpConfig ? "tcp-source" : "replay-source", + "tank.base_color", + MetaCore::MetaCoreRuntimeValueType::Vec3 + }); + sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "alarm.intensity", + generateTcpConfig ? "tcp-source" : "replay-source", + "alarm.intensity", + MetaCore::MetaCoreRuntimeValueType::Double + }); + + MetaCore::MetaCoreRuntimeBindingsDocument bindings; + bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.cube.position", + "cube.position", + 3, + MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.cube.visible", + "cube.visible", + 3, + MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.cube.base_color", + "cube.base_color", + 3, + MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererBaseColor, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.valve.visible", + "valve.visible", + 4, + MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.tank.base_color", + "tank.base_color", + 5, + MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererBaseColor, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.alarm.intensity", + "alarm.intensity", + 6, + MetaCore::MetaCoreRuntimeBindingTarget::LightIntensity, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + + const std::filesystem::path runtimeDirectory = argv[1]; + const std::filesystem::path projectRoot = runtimeDirectory.parent_path(); + const std::filesystem::path scenePath = projectRoot / "Scenes" / "Main.mcscene"; + std::filesystem::create_directories(runtimeDirectory); + if (!MetaCore::MetaCoreWriteRuntimeDataSourcesDocument(runtimeDirectory / "DataSources.mcruntime", sources, registry)) { + std::cerr << "Failed to write DataSources.mcruntime\n"; + return 1; + } + if (!MetaCore::MetaCoreWriteRuntimeBindingsDocument(runtimeDirectory / "Bindings.mcruntime", bindings, registry)) { + std::cerr << "Failed to write Bindings.mcruntime\n"; + return 1; + } + MetaCore::MetaCoreRuntimeProjectDocument runtimeProjectDocument; + runtimeProjectDocument.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; + runtimeProjectDocument.DataSourcesPath = std::filesystem::path("Runtime") / "DataSources.mcruntime"; + runtimeProjectDocument.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; + runtimeProjectDocument.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; + if (!MetaCore::MetaCoreWriteRuntimeProjectDocument(runtimeDirectory / "ProjectRuntime.mcruntimecfg", runtimeProjectDocument, registry)) { + std::cerr << "Failed to write ProjectRuntime.mcruntimecfg\n"; + return 1; + } + std::filesystem::create_directories(scenePath.parent_path()); + if (!MetaCore::MetaCoreWriteScenePackage(scenePath, MetaCoreBuildPilotSceneDocument())) { + std::cerr << "Failed to write Main.mcscene\n"; + return 1; + } + + if (!generateTcpConfig) { + std::ofstream replayFile(runtimeDirectory / "RuntimeReplay.mcstream", std::ios::trunc); + if (!replayFile) { + std::cerr << "Failed to write RuntimeReplay.mcstream\n"; + return 1; + } + replayFile + << "# emit_after_seconds data_point_id value_type payload\n" + << "0.00 cube.position vec3 0.0 0.5 0.0\n" + << "0.00 cube.visible bool true\n" + << "0.00 cube.base_color vec3 0.4 0.6 0.9\n" + << "0.00 valve.visible bool true\n" + << "0.00 tank.base_color vec3 0.35 0.65 0.90\n" + << "0.00 alarm.intensity double 0.0\n" + << "0.50 cube.position vec3 1.5 0.5 0.0\n" + << "0.50 cube.base_color vec3 0.8 0.6 0.5\n" + << "0.50 tank.base_color vec3 0.20 0.80 0.25\n" + << "0.50 alarm.intensity double 2.0\n" + << "1.00 cube.visible bool false\n" + << "1.00 valve.visible bool false\n" + << "1.50 cube.position vec3 -1.5 0.5 0.0\n" + << "1.50 cube.visible bool true\n" + << "1.50 valve.visible bool true\n" + << "1.50 alarm.intensity double 0.0\n"; + } + + std::cout << "Runtime config generated at " << runtimeDirectory.string() + << " scene=" << scenePath.string() + << " mode=" << (generateTcpConfig ? "tcp" : "file_replay") << '\n'; + return 0; +} diff --git a/tools/MetaCoreTcpSenderTool/main.cpp b/tools/MetaCoreTcpSenderTool/main.cpp new file mode 100644 index 0000000..2c60714 --- /dev/null +++ b/tools/MetaCoreTcpSenderTool/main.cpp @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include +#include +#include + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include + +namespace { + +bool MetaCoreInitializeWinsock() { + WSADATA wsaData{}; + return WSAStartup(MAKEWORD(2, 2), &wsaData) == 0; +} + +void MetaCoreShutdownWinsock() { + WSACleanup(); +} + +} // namespace + +int main(int argc, char* argv[]) { + const std::string bindHost = argc > 1 ? argv[1] : "127.0.0.1"; + const unsigned short port = argc > 2 ? static_cast(std::strtoul(argv[2], nullptr, 10)) : 7001; + + if (!MetaCoreInitializeWinsock()) { + std::cerr << "MetaCoreTcpSenderTool: WSAStartup failed\n"; + return 1; + } + + addrinfo hints{}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_PASSIVE; + + addrinfo* result = nullptr; + const std::string portString = std::to_string(port); + if (getaddrinfo(bindHost.c_str(), portString.c_str(), &hints, &result) != 0 || result == nullptr) { + std::cerr << "MetaCoreTcpSenderTool: getaddrinfo failed\n"; + MetaCoreShutdownWinsock(); + return 1; + } + + SOCKET listenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol); + if (listenSocket == INVALID_SOCKET) { + std::cerr << "MetaCoreTcpSenderTool: socket creation failed\n"; + freeaddrinfo(result); + MetaCoreShutdownWinsock(); + return 1; + } + + if (bind(listenSocket, result->ai_addr, static_cast(result->ai_addrlen)) == SOCKET_ERROR) { + std::cerr << "MetaCoreTcpSenderTool: bind failed\n"; + closesocket(listenSocket); + freeaddrinfo(result); + MetaCoreShutdownWinsock(); + return 1; + } + freeaddrinfo(result); + + if (listen(listenSocket, 1) == SOCKET_ERROR) { + std::cerr << "MetaCoreTcpSenderTool: listen failed\n"; + closesocket(listenSocket); + MetaCoreShutdownWinsock(); + return 1; + } + + std::cout << "MetaCoreTcpSenderTool: listening on " << bindHost << ":" << port << '\n'; + SOCKET clientSocket = accept(listenSocket, nullptr, nullptr); + closesocket(listenSocket); + if (clientSocket == INVALID_SOCKET) { + std::cerr << "MetaCoreTcpSenderTool: accept failed\n"; + MetaCoreShutdownWinsock(); + return 1; + } + + std::cout << "MetaCoreTcpSenderTool: client connected\n"; + for (int frame = 0; frame < 600; ++frame) { + const double t = static_cast(frame) * 0.05; + const float x = static_cast(std::sin(t) * 2.0); + const bool visible = std::fmod(t, 2.0) < 1.0; + const bool valveVisible = std::fmod(t, 3.0) < 1.5; + const float r = 0.5F + static_cast(std::sin(t) * 0.4); + const float g = 0.6F; + const float b = 0.9F - static_cast(std::sin(t) * 0.2); + const float tankR = 0.25F + static_cast((std::sin(t * 0.5) + 1.0) * 0.25); + const float tankG = 0.55F + static_cast((std::cos(t * 0.5) + 1.0) * 0.15); + const float tankB = 0.35F; + const double alarmIntensity = valveVisible ? 2.5 : 0.0; + + const std::string payload = + "cube.position vec3 " + std::to_string(x) + " 0.5 0.0\n" + + "cube.visible bool " + std::string(visible ? "true" : "false") + "\n" + + "cube.base_color vec3 " + std::to_string(r) + " " + std::to_string(g) + " " + std::to_string(b) + "\n" + + "valve.visible bool " + std::string(valveVisible ? "true" : "false") + "\n" + + "tank.base_color vec3 " + std::to_string(tankR) + " " + std::to_string(tankG) + " " + std::to_string(tankB) + "\n" + + "alarm.intensity double " + std::to_string(alarmIntensity) + "\n"; + + const int sent = send(clientSocket, payload.c_str(), static_cast(payload.size()), 0); + if (sent == SOCKET_ERROR) { + std::cerr << "MetaCoreTcpSenderTool: send failed\n"; + closesocket(clientSocket); + MetaCoreShutdownWinsock(); + return 1; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + + shutdown(clientSocket, SD_SEND); + closesocket(clientSocket); + MetaCoreShutdownWinsock(); + std::cout << "MetaCoreTcpSenderTool: stream completed\n"; + return 0; +}