diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index 858fa53..03f9d11 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -12,6 +12,7 @@ #include "MetaCorePlatform/MetaCoreInput.h" #include "MetaCoreFoundation/MetaCoreAssetRegistry.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" +#include "MetaCoreScene/MetaCoreRuntimeLifecycle.h" #include "MetaCoreScene/MetaCoreScenePackage.h" #include "MetaCoreScene/MetaCoreSceneSerializer.h" #include @@ -47,7 +48,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -2791,6 +2794,19 @@ bool MetaCoreDeserializeReflectedComponentValue( return path.extension() == ".mcprefab"; } +[[nodiscard]] std::filesystem::path MetaCoreNormalizePrefabJsonPath(const std::filesystem::path& path) { + std::filesystem::path normalizedPath = path.lexically_normal(); + const std::string filename = normalizedPath.filename().string(); + if (filename.length() > 14 && filename.compare(filename.length() - 14, 14, ".mcprefab.json") == 0) { + return normalizedPath; + } + if (normalizedPath.extension() == ".mcprefab") { + normalizedPath.replace_extension(".mcprefab.json"); + return normalizedPath; + } + return normalizedPath.parent_path() / (normalizedPath.stem().string() + ".mcprefab.json"); +} + [[nodiscard]] bool MetaCoreIsMaterialPath(const std::filesystem::path& path) { const std::string filename = path.filename().string(); if (filename.length() > 16 && filename.compare(filename.length() - 16, 16, ".mcmaterial.json") == 0) { @@ -2827,6 +2843,66 @@ bool MetaCoreDeserializeReflectedComponentValue( return MetaCoreIsScenePath(path) || MetaCoreIsPrefabPath(path) || MetaCoreIsMaterialPath(path) || MetaCoreIsUiPath(path); } +void MetaCoreRewriteMovedAssetMetadata( + const std::filesystem::path& absoluteAssetPath, + const std::filesystem::path& absoluteMetaPath, + const std::filesystem::path& relativeAssetPath +) { + MetaCoreAssetMetadataDocument metadata; + if (!MetaCoreReadMetaJson(absoluteMetaPath, metadata)) { + return; + } + + metadata.SourcePath = relativeAssetPath.lexically_normal(); + metadata.PackagePath = MetaCoreIsJsonSourceAssetPath(relativeAssetPath) + ? metadata.SourcePath + : MetaCoreBuildPackagePathFromSource(metadata.SourcePath); + metadata.SourceHash = MetaCoreHashFile(absoluteAssetPath).value_or(metadata.SourceHash); + (void)MetaCoreWriteMetaJson(absoluteMetaPath, metadata); +} + +void MetaCoreRewriteMovedAssetMetadataTree( + const std::filesystem::path& projectRoot, + const std::filesystem::path& absoluteMovedPath, + const std::filesystem::path& relativeMovedPath +) { + if (!std::filesystem::exists(absoluteMovedPath)) { + return; + } + + if (std::filesystem::is_regular_file(absoluteMovedPath)) { + const std::filesystem::path metaPath = MetaCoreBuildMetaPath(absoluteMovedPath); + if (std::filesystem::exists(metaPath)) { + MetaCoreRewriteMovedAssetMetadata(absoluteMovedPath, metaPath, relativeMovedPath); + } + return; + } + + if (!std::filesystem::is_directory(absoluteMovedPath)) { + return; + } + + for (const auto& entry : std::filesystem::recursive_directory_iterator(absoluteMovedPath)) { + if (!entry.is_regular_file() || + MetaCoreIsMetaPath(entry.path()) || + MetaCoreIsLegacyMetaPath(entry.path()) || + MetaCoreIsPackagePath(entry.path())) { + continue; + } + + const std::filesystem::path metaPath = MetaCoreBuildMetaPath(entry.path()); + if (!std::filesystem::exists(metaPath)) { + continue; + } + + MetaCoreRewriteMovedAssetMetadata( + entry.path(), + metaPath, + entry.path().lexically_relative(projectRoot) + ); + } +} + [[nodiscard]] bool MetaCoreIsGeneratedProjectPath(const std::filesystem::path& relativePath) { const auto normalized = relativePath.lexically_normal(); const auto iterator = normalized.begin(); @@ -2836,6 +2912,16 @@ bool MetaCoreDeserializeReflectedComponentValue( return *iterator == "Library" || *iterator == "Build" || *iterator == "Runtime"; } +[[nodiscard]] bool MetaCorePathIsUnderOrEqual( + const std::filesystem::path& path, + const std::filesystem::path& parent +) { + const std::string normalizedPath = path.lexically_normal().generic_string(); + const std::string normalizedParent = parent.lexically_normal().generic_string(); + return normalizedPath == normalizedParent || + normalizedPath.rfind(normalizedParent + "/", 0) == 0; +} + [[nodiscard]] std::filesystem::path MetaCoreBuildModelRuntimePackagePath( const std::filesystem::path& projectRoot, const MetaCoreAssetGuid& modelAssetGuid @@ -3365,12 +3451,106 @@ template return subtreeObjects; } -void MetaCoreNormalizePrefabSubtree(std::vector& gameObjects, MetaCoreId rootId) { - for (MetaCoreGameObject& gameObject : gameObjects) { - if (gameObject.GetId() == rootId) { - gameObject.SetParentId(0); +[[nodiscard]] bool MetaCoreIsNativePrefabComponentType(std::string_view typeId) { + return typeId == "Transform" || + typeId == "Camera" || + typeId == "Light" || + typeId == "MeshRenderer"; +} + +void MetaCoreCapturePrefabSerializedComponents( + const MetaCoreGameObject& gameObject, + MetaCoreGameObjectData& data, + const std::shared_ptr& componentRegistry, + const MetaCoreTypeRegistry& typeRegistry +) { + data.SerializedComponents.clear(); + if (componentRegistry == nullptr) { + return; + } + + for (const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor : componentRegistry->GetComponentDescriptors()) { + if (MetaCoreIsNativePrefabComponentType(descriptor.TypeId) || + !descriptor.HasComponent || + !descriptor.CopyComponentPayload || + !descriptor.HasComponent(gameObject)) { + continue; + } + + const auto payload = descriptor.CopyComponentPayload(gameObject, typeRegistry); + if (!payload.has_value() || payload->empty()) { + continue; + } + + data.SerializedComponents.push_back(MetaCoreSerializedComponentData{ + descriptor.TypeId, + *payload + }); + } + + std::sort( + data.SerializedComponents.begin(), + data.SerializedComponents.end(), + [](const MetaCoreSerializedComponentData& lhs, const MetaCoreSerializedComponentData& rhs) { + return lhs.TypeId < rhs.TypeId; + } + ); +} + +[[nodiscard]] MetaCoreGameObjectData MetaCoreCapturePrefabObjectData( + const MetaCoreGameObject& gameObject, + MetaCoreId rootId, + const std::shared_ptr& componentRegistry, + const MetaCoreTypeRegistry& typeRegistry +) { + MetaCoreGameObjectData data; + data.Id = gameObject.GetId(); + data.Name = gameObject.GetName(); + data.ParentId = gameObject.GetId() == rootId ? 0 : gameObject.GetParentId(); + data.Transform = gameObject.GetComponent(); + if (gameObject.HasComponent()) data.Camera = gameObject.GetComponent(); + if (gameObject.HasComponent()) data.Light = gameObject.GetComponent(); + if (gameObject.HasComponent()) data.MeshRenderer = gameObject.GetComponent(); + MetaCoreCapturePrefabSerializedComponents(gameObject, data, componentRegistry, typeRegistry); + return data; +} + +void MetaCoreRestorePrefabSerializedComponents( + MetaCoreEditorContext& editorContext, + MetaCoreGameObject& gameObject, + const MetaCoreGameObjectData& data, + const std::shared_ptr& componentRegistry, + const MetaCoreTypeRegistry& typeRegistry +) { + if (componentRegistry == nullptr || data.SerializedComponents.empty()) { + return; + } + + for (const MetaCoreSerializedComponentData& componentData : data.SerializedComponents) { + if (MetaCoreIsNativePrefabComponentType(componentData.TypeId) || componentData.Payload.empty()) { + continue; + } + + const auto* descriptor = componentRegistry->FindDescriptor(componentData.TypeId); + if (descriptor == nullptr || !descriptor->PasteComponentPayload) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Warning, + "Prefab", + "Skipped serialized component without registered C++ type: " + componentData.TypeId + ); + continue; + } + + if (!descriptor->PasteComponentPayload( + gameObject, + std::span(componentData.Payload.data(), componentData.Payload.size()), + typeRegistry)) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Warning, + "Prefab", + "Failed to restore serialized component: " + componentData.TypeId + ); } - gameObject.RemoveComponent(); } } @@ -3532,11 +3712,26 @@ public: [[nodiscard]] bool ReimportAsset(const MetaCoreAssetGuid& assetGuid) override; bool Refresh() override; bool CreateFolder(const std::filesystem::path& relativeDirectory) override; + std::optional CreateSceneAsset( + const std::filesystem::path& relativeScenePath, + bool makeStartupScene + ) override; + std::optional CreatePrefabAsset(const std::filesystem::path& relativePrefabPath) override; + std::optional CreateMaterialAsset(const std::filesystem::path& relativeMaterialPath) override; + std::optional CreateUiDocumentAsset(const std::filesystem::path& relativeUiPath) 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; + [[nodiscard]] std::optional RefreshAndFindCreatedAsset(const std::filesystem::path& relativePath); + [[nodiscard]] bool WriteJsonAssetMetadata( + const std::filesystem::path& absolutePath, + const std::filesystem::path& relativePath, + const MetaCoreAssetGuid& assetGuid, + std::string_view assetType, + std::string_view importerId + ) const; void ApplyProjectDescriptorRoot(const std::filesystem::path& projectRoot); void LoadProjectDescriptor(); void SaveProjectDescriptor() const; @@ -3712,6 +3907,19 @@ bool MetaCoreBuiltinAssetDatabaseService::RenamePath( return false; } } + if (std::filesystem::exists(legacyMetaTarget)) { + MetaCoreRewriteMovedAssetMetadata(absoluteTargetPath, legacyMetaTarget, targetPath); + } + MetaCoreRewriteMovedAssetMetadataTree(Project_.RootPath, absoluteTargetPath, targetPath); + + const std::filesystem::path packageSource = Project_.RootPath / MetaCoreBuildPackagePathFromSource(normalizedPath); + const std::filesystem::path packageTarget = Project_.RootPath / MetaCoreBuildPackagePathFromSource(targetPath); + if (std::filesystem::exists(packageSource) && !std::filesystem::exists(packageTarget)) { + std::filesystem::rename(packageSource, packageTarget, errorCode); + if (errorCode) { + return false; + } + } const auto remapPath = [&](std::filesystem::path& pathValue) { if (pathValue == normalizedPath) { @@ -3793,6 +4001,19 @@ bool MetaCoreBuiltinAssetDatabaseService::MovePath( return false; } } + if (std::filesystem::exists(legacyMetaTarget)) { + MetaCoreRewriteMovedAssetMetadata(Project_.RootPath / targetPath, legacyMetaTarget, targetPath); + } + MetaCoreRewriteMovedAssetMetadataTree(Project_.RootPath, Project_.RootPath / targetPath, targetPath); + + const std::filesystem::path packageSource = Project_.RootPath / MetaCoreBuildPackagePathFromSource(normalizedSourcePath); + const std::filesystem::path packageTarget = Project_.RootPath / MetaCoreBuildPackagePathFromSource(targetPath); + if (std::filesystem::exists(packageSource) && !std::filesystem::exists(packageTarget)) { + std::filesystem::rename(packageSource, packageTarget, errorCode); + if (errorCode) { + return false; + } + } remapPath(Project_.StartupScenePath); for (auto& scenePath : Project_.ScenePaths) { @@ -4053,6 +4274,51 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { } } + if (std::filesystem::exists(Project_.UiPath) && + !MetaCorePathIsUnderOrEqual(Project_.UiPath, Project_.AssetsPath)) { + for (const auto& entry : std::filesystem::recursive_directory_iterator(Project_.UiPath)) { + if (!entry.is_regular_file() || + MetaCoreIsMetaPath(entry.path()) || + MetaCoreIsLegacyMetaPath(entry.path()) || + MetaCoreIsPackagePath(entry.path())) { + continue; + } + + const std::filesystem::path relativePath = entry.path().lexically_relative(Project_.RootPath); + MetaCoreAssetMetadataDocument metadata; + bool hasMetadata = false; + const std::filesystem::path metaPath = MetaCoreBuildMetaPath(entry.path()); + if (std::filesystem::exists(metaPath)) { + hasMetadata = MetaCoreReadMetaJson(metaPath, metadata); + } + if (!hasMetadata) { + metadata.AssetGuid = MetaCoreAssetGuid::Generate(); + metadata.AssetType = MetaCoreDetectAssetType(entry.path()); + metadata.ImporterId = MetaCoreDetectImporterId(entry.path()); + metadata.SourcePath = relativePath; + metadata.PackagePath = MetaCoreIsJsonSourceAssetPath(relativePath) + ? relativePath + : MetaCoreBuildPackagePathFromSource(relativePath); + metadata.SourceHash = MetaCoreHashFile(entry.path()).value_or(0); + hasMetadata = MetaCoreWriteMetaJson(metaPath, metadata); + } + + AssetRecords_.push_back(MetaCoreAssetRecord{ + hasMetadata ? metadata.AssetGuid : MetaCoreAssetGuid{}, + relativePath, + hasMetadata ? metadata.AssetType : MetaCoreDetectAssetType(entry.path()), + MetaCoreAssetStorageKind::SourceFile, + relativePath, + hasMetadata + ? metadata.PackagePath + : (MetaCoreIsJsonSourceAssetPath(relativePath) ? relativePath : 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); }); @@ -4136,7 +4402,199 @@ bool MetaCoreBuiltinAssetDatabaseService::CreateFolder(const std::filesystem::pa if (!HasProject() || MetaCoreIsUnsafeRelativePath(relativeDirectory)) { return false; } - return std::filesystem::create_directories(Project_.RootPath / relativeDirectory.lexically_normal()); + const bool created = std::filesystem::create_directories(Project_.RootPath / relativeDirectory.lexically_normal()); + (void)Refresh(); + return created; +} + +std::optional MetaCoreBuiltinAssetDatabaseService::RefreshAndFindCreatedAsset( + const std::filesystem::path& relativePath +) { + if (!Refresh()) { + return std::nullopt; + } + return FindAssetByRelativePath(relativePath.lexically_normal()); +} + +bool MetaCoreBuiltinAssetDatabaseService::WriteJsonAssetMetadata( + const std::filesystem::path& absolutePath, + const std::filesystem::path& relativePath, + const MetaCoreAssetGuid& assetGuid, + std::string_view assetType, + std::string_view importerId +) const { + if (!assetGuid.IsValid()) { + return false; + } + + MetaCoreAssetMetadataDocument metadata; + metadata.AssetGuid = assetGuid; + metadata.AssetType = std::string(assetType); + metadata.ImporterId = std::string(importerId); + metadata.SourcePath = relativePath.lexically_normal(); + metadata.PackagePath = metadata.SourcePath; + metadata.SourceHash = MetaCoreHashFile(absolutePath).value_or(0); + return MetaCoreWriteMetaJson(MetaCoreBuildMetaPath(absolutePath), metadata); +} + +std::optional MetaCoreBuiltinAssetDatabaseService::CreateSceneAsset( + const std::filesystem::path& relativeScenePath, + bool makeStartupScene +) { + if (!HasProject() || ReflectionRegistry_ == nullptr || relativeScenePath.empty() || + MetaCoreIsUnsafeRelativePath(relativeScenePath)) { + return std::nullopt; + } + + std::filesystem::path normalizedPath = relativeScenePath.lexically_normal(); + if (normalizedPath.extension() == ".mcscene") { + normalizedPath.replace_extension(".mcscene.json"); + } else if (!MetaCoreIsScenePath(normalizedPath)) { + normalizedPath = normalizedPath.parent_path() / (normalizedPath.stem().string() + ".mcscene.json"); + } + if (MetaCoreIsUnsafeRelativePath(normalizedPath)) { + return std::nullopt; + } + + const std::filesystem::path absolutePath = Project_.RootPath / normalizedPath; + if (std::filesystem::exists(absolutePath)) { + return std::nullopt; + } + + (void)std::filesystem::create_directories(absolutePath.parent_path()); + MetaCoreSceneDocument document; + document.Name = absolutePath.stem().stem().string(); + document.GameObjects = MetaCoreCreateDefaultScene().CaptureSnapshot().GameObjects; + if (!MetaCoreSceneSerializer::SaveSceneToJson(absolutePath, document, ReflectionRegistry_->GetTypeRegistry())) { + return std::nullopt; + } + + const MetaCoreAssetGuid sceneGuid = MetaCoreAssetGuid::Generate(); + if (!WriteJsonAssetMetadata(absolutePath, normalizedPath, sceneGuid, "scene", "SceneImporter")) { + return std::nullopt; + } + (void)RegisterScenePath(normalizedPath, makeStartupScene); + return RefreshAndFindCreatedAsset(normalizedPath); +} + +std::optional MetaCoreBuiltinAssetDatabaseService::CreatePrefabAsset( + const std::filesystem::path& relativePrefabPath +) { + if (!HasProject() || ReflectionRegistry_ == nullptr || relativePrefabPath.empty() || + MetaCoreIsUnsafeRelativePath(relativePrefabPath)) { + return std::nullopt; + } + + std::filesystem::path normalizedPath = relativePrefabPath.lexically_normal(); + if (normalizedPath.extension() == ".mcprefab") { + normalizedPath.replace_extension(".mcprefab.json"); + } else if (!MetaCoreIsPrefabPath(normalizedPath)) { + normalizedPath = normalizedPath.parent_path() / (normalizedPath.stem().string() + ".mcprefab.json"); + } + if (MetaCoreIsUnsafeRelativePath(normalizedPath)) { + return std::nullopt; + } + + const std::filesystem::path absolutePath = Project_.RootPath / normalizedPath; + if (std::filesystem::exists(absolutePath)) { + return std::nullopt; + } + + (void)std::filesystem::create_directories(absolutePath.parent_path()); + MetaCorePrefabDocument document; + document.Name = absolutePath.stem().stem().string(); + MetaCoreGameObjectData rootObject; + rootObject.Id = 1; + rootObject.Name = document.Name.empty() ? "PrefabRoot" : document.Name; + document.GameObjects.push_back(std::move(rootObject)); + if (!MetaCoreSceneSerializer::SavePrefabToJson(absolutePath, document, ReflectionRegistry_->GetTypeRegistry())) { + return std::nullopt; + } + + const MetaCoreAssetGuid prefabGuid = MetaCoreAssetGuid::Generate(); + if (!WriteJsonAssetMetadata(absolutePath, normalizedPath, prefabGuid, "prefab", "PrefabImporter")) { + return std::nullopt; + } + return RefreshAndFindCreatedAsset(normalizedPath); +} + +std::optional MetaCoreBuiltinAssetDatabaseService::CreateMaterialAsset( + const std::filesystem::path& relativeMaterialPath +) { + if (!HasProject() || ReflectionRegistry_ == nullptr || relativeMaterialPath.empty() || + MetaCoreIsUnsafeRelativePath(relativeMaterialPath)) { + return std::nullopt; + } + + std::filesystem::path normalizedPath = relativeMaterialPath.lexically_normal(); + if (!MetaCoreIsMaterialPath(normalizedPath)) { + normalizedPath = normalizedPath.parent_path() / (normalizedPath.stem().string() + ".mcmaterial.json"); + } + if (MetaCoreIsUnsafeRelativePath(normalizedPath)) { + return std::nullopt; + } + + const std::filesystem::path absolutePath = Project_.RootPath / normalizedPath; + if (std::filesystem::exists(absolutePath)) { + return std::nullopt; + } + + (void)std::filesystem::create_directories(absolutePath.parent_path()); + MetaCoreMaterialAssetDocument document; + document.AssetGuid = MetaCoreAssetGuid::Generate(); + document.Name = absolutePath.stem().stem().string(); + if (!MetaCoreSceneSerializer::SaveMaterialToJson(absolutePath, document, ReflectionRegistry_->GetTypeRegistry())) { + return std::nullopt; + } + + if (!WriteJsonAssetMetadata(absolutePath, normalizedPath, document.AssetGuid, "material", "MaterialImporter")) { + return std::nullopt; + } + return RefreshAndFindCreatedAsset(normalizedPath); +} + +std::optional MetaCoreBuiltinAssetDatabaseService::CreateUiDocumentAsset( + const std::filesystem::path& relativeUiPath +) { + if (!HasProject() || ReflectionRegistry_ == nullptr || relativeUiPath.empty() || + MetaCoreIsUnsafeRelativePath(relativeUiPath)) { + return std::nullopt; + } + + std::filesystem::path normalizedPath = relativeUiPath.lexically_normal(); + if (normalizedPath.extension() == ".mcui") { + normalizedPath.replace_extension(".mcui.json"); + } else if (!MetaCoreIsUiPath(normalizedPath)) { + normalizedPath = normalizedPath.parent_path() / (normalizedPath.stem().string() + ".mcui.json"); + } + if (MetaCoreIsUnsafeRelativePath(normalizedPath)) { + return std::nullopt; + } + + const std::filesystem::path absolutePath = Project_.RootPath / normalizedPath; + if (std::filesystem::exists(absolutePath)) { + return std::nullopt; + } + + (void)std::filesystem::create_directories(absolutePath.parent_path()); + MetaCoreUiDocument document; + document.Name = absolutePath.stem().stem().string(); + MetaCoreUiNodeDocument rootNode; + rootNode.Id = "root"; + rootNode.Name = "Root"; + rootNode.Type = MetaCoreUiNodeType::Panel; + rootNode.RectTransform.Size = glm::vec3(1920.0F, 1080.0F, 0.0F); + document.RootNodeIds.push_back(rootNode.Id); + document.Nodes.push_back(std::move(rootNode)); + if (!MetaCoreSceneSerializer::SaveUiToJson(absolutePath, document, ReflectionRegistry_->GetTypeRegistry())) { + return std::nullopt; + } + + const MetaCoreAssetGuid uiGuid = MetaCoreAssetGuid::Generate(); + if (!WriteJsonAssetMetadata(absolutePath, normalizedPath, uiGuid, "ui_document", "UiDocumentImporter")) { + return std::nullopt; + } + return RefreshAndFindCreatedAsset(normalizedPath); } bool MetaCoreBuiltinAssetDatabaseService::RegisterScenePath( @@ -6143,6 +6601,7 @@ public: AssetDatabaseService_ = moduleRegistry.ResolveService(); PackageService_ = moduleRegistry.ResolveService(); ReflectionRegistry_ = moduleRegistry.ResolveService(); + ComponentRegistry_ = moduleRegistry.ResolveService(); } void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { @@ -6150,6 +6609,7 @@ public: AssetDatabaseService_.reset(); PackageService_.reset(); ReflectionRegistry_.reset(); + ComponentRegistry_.reset(); } [[nodiscard]] bool SupportsPrefabWorkflows() const override { return true; } @@ -6180,6 +6640,7 @@ private: std::shared_ptr AssetDatabaseService_{}; std::shared_ptr PackageService_{}; std::shared_ptr ReflectionRegistry_{}; + std::shared_ptr ComponentRegistry_{}; }; class MetaCoreBuiltinComponentTypeRegistry final : public MetaCoreIComponentTypeRegistry { @@ -6514,10 +6975,38 @@ public: } if (descriptor.Lifecycle.OnDrawGizmos) { - descriptor.Lifecycle.OnDrawGizmos(editorContext, gameObject); + try { + descriptor.Lifecycle.OnDrawGizmos(editorContext, gameObject); + } catch (const std::exception& exception) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Error, + "Gizmos", + descriptor.DisplayName + " OnDrawGizmos failed: " + exception.what() + ); + } catch (...) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Error, + "Gizmos", + descriptor.DisplayName + " OnDrawGizmos failed with an unknown exception" + ); + } } if (descriptor.Lifecycle.OnDrawGizmosSelected && editorContext.IsObjectSelected(objectId)) { - descriptor.Lifecycle.OnDrawGizmosSelected(editorContext, gameObject); + try { + descriptor.Lifecycle.OnDrawGizmosSelected(editorContext, gameObject); + } catch (const std::exception& exception) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Error, + "Gizmos", + descriptor.DisplayName + " OnDrawGizmosSelected failed: " + exception.what() + ); + } catch (...) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Error, + "Gizmos", + descriptor.DisplayName + " OnDrawGizmosSelected failed with an unknown exception" + ); + } } } } @@ -6540,21 +7029,23 @@ public: (void)moduleRegistry; State_ = MetaCorePlayModeState::Edit; EditStateSnapshot_.reset(); - StartedLifecycleComponents_.clear(); + RuntimeLifecycleExecutor_ = MetaCoreRuntimeLifecycleExecutor{}; ComponentRegistry_.reset(); } [[nodiscard]] MetaCorePlayModeState GetState() const override { return State_; } + [[nodiscard]] bool IsRuntimeSceneActive() const override { return State_ != MetaCorePlayModeState::Edit; } [[nodiscard]] bool CanEnterPlayMode() const override { return State_ == MetaCorePlayModeState::Edit; } [[nodiscard]] bool EnterPlayMode(MetaCoreEditorContext& editorContext) override { if (!CanEnterPlayMode()) { return false; } EditStateSnapshot_ = editorContext.CaptureStateSnapshot(); - StartedLifecycleComponents_.clear(); - ElapsedPlayTimeSeconds_ = 0.0F; + editorContext.RestoreStateSnapshot(*EditStateSnapshot_); + editorContext.SetRuntimeSceneActive(true); + ConfigureRuntimeLifecycleExecutor(editorContext); State_ = MetaCorePlayModeState::Playing; - InvokeLifecycleStart(editorContext); + RuntimeLifecycleExecutor_.EnterScene(editorContext.GetScene(), BuildLifecycleErrorSink(editorContext)); editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "PlayMode", "Entered Play Mode"); return true; } @@ -6562,14 +7053,15 @@ public: if (State_ == MetaCorePlayModeState::Edit) { return false; } - InvokeLifecycleDestroy(editorContext); + RuntimeLifecycleExecutor_.ExitScene(editorContext.GetScene(), BuildLifecycleErrorSink(editorContext)); + editorContext.SetRuntimeSceneActive(false); if (EditStateSnapshot_.has_value()) { editorContext.RestoreStateSnapshot(*EditStateSnapshot_); } EditStateSnapshot_.reset(); - StartedLifecycleComponents_.clear(); - ElapsedPlayTimeSeconds_ = 0.0F; + RuntimeLifecycleExecutor_ = MetaCoreRuntimeLifecycleExecutor{}; State_ = MetaCorePlayModeState::Edit; + editorContext.SetRuntimeSceneActive(false); editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "PlayMode", "Exited Play Mode"); return true; } @@ -6593,91 +7085,110 @@ public: if (State_ != MetaCorePlayModeState::Paused) { return false; } - const float sanitizedDeltaSeconds = std::max(deltaSeconds, 0.0F); - InvokeLifecycleStart(editorContext); - InvokeLifecycleUpdate(editorContext, sanitizedDeltaSeconds); - ElapsedPlayTimeSeconds_ += sanitizedDeltaSeconds; + RuntimeLifecycleExecutor_.TickScene(editorContext.GetScene(), deltaSeconds, BuildLifecycleErrorSink(editorContext)); return true; } - [[nodiscard]] float GetElapsedPlayTimeSeconds() const override { return ElapsedPlayTimeSeconds_; } + [[nodiscard]] float GetElapsedPlayTimeSeconds() const override { return RuntimeLifecycleExecutor_.GetElapsedTimeSeconds(); } + void NotifyGameObjectsWillBeDestroyed( + MetaCoreEditorContext& editorContext, + const std::vector& objectIds + ) override { + if (State_ == MetaCorePlayModeState::Edit) { + return; + } + + RuntimeLifecycleExecutor_.NotifyGameObjectsWillBeDestroyed( + editorContext.GetScene(), + objectIds, + BuildLifecycleErrorSink(editorContext) + ); + } + void NotifyComponentWillBeRemoved( + MetaCoreEditorContext& editorContext, + MetaCoreId objectId, + std::string_view componentTypeId + ) override { + if (State_ == MetaCorePlayModeState::Edit) { + return; + } + + RuntimeLifecycleExecutor_.NotifyComponentWillBeRemoved( + editorContext.GetScene(), + objectId, + componentTypeId, + BuildLifecycleErrorSink(editorContext) + ); + } void TickPlayMode(MetaCoreEditorContext& editorContext, float deltaSeconds) override { if (State_ != MetaCorePlayModeState::Playing) { return; } - const float sanitizedDeltaSeconds = std::max(deltaSeconds, 0.0F); - InvokeLifecycleStart(editorContext); - InvokeLifecycleUpdate(editorContext, sanitizedDeltaSeconds); - ElapsedPlayTimeSeconds_ += sanitizedDeltaSeconds; + RuntimeLifecycleExecutor_.TickScene(editorContext.GetScene(), deltaSeconds, BuildLifecycleErrorSink(editorContext)); } private: - [[nodiscard]] std::string BuildLifecycleKey(MetaCoreId objectId, std::string_view componentTypeId) const { - return std::to_string(objectId) + ":" + std::string(componentTypeId); + [[nodiscard]] MetaCoreRuntimeLifecycleExecutor::ErrorSink BuildLifecycleErrorSink(MetaCoreEditorContext& editorContext) { + return [&editorContext](const MetaCoreRuntimeLifecycleError& error) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Error, + "PlayMode", + "Lifecycle " + error.Phase + + " failed for " + error.ComponentTypeId + + " on object " + std::to_string(error.ObjectId) + + ": " + error.Message + ); + }; } - template - void ForEachLifecycleComponent(MetaCoreEditorContext& editorContext, TCallback&& callback) { + void ConfigureRuntimeLifecycleExecutor(MetaCoreEditorContext& editorContext) { + RuntimeLifecycleExecutor_ = MetaCoreRuntimeLifecycleExecutor{}; if (ComponentRegistry_ == nullptr) { return; } - const std::vector objectIds = editorContext.GetScene().BuildHierarchyPreorder(); - for (const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor : ComponentRegistry_->GetComponentDescriptors()) { - for (MetaCoreId objectId : objectIds) { - MetaCoreGameObject gameObject = editorContext.GetScene().FindGameObject(objectId); - if (!gameObject || !descriptor.HasComponent || !descriptor.HasComponent(gameObject)) { - continue; - } - callback(descriptor, gameObject); + for (const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor : + ComponentRegistry_->GetComponentDescriptors()) { + if (!descriptor.HasComponent || + (!descriptor.Lifecycle.OnStart && + !descriptor.Lifecycle.OnUpdate && + !descriptor.Lifecycle.OnDestroy)) { + continue; } + + MetaCoreRuntimeLifecycleDescriptor runtimeDescriptor; + runtimeDescriptor.TypeId = descriptor.TypeId; + runtimeDescriptor.HasComponent = descriptor.HasComponent; + if (descriptor.Lifecycle.OnStart) { + runtimeDescriptor.OnStart = + [&editorContext, onStart = descriptor.Lifecycle.OnStart](MetaCoreScene&, MetaCoreGameObject& gameObject) { + onStart(editorContext, gameObject); + }; + } + if (descriptor.Lifecycle.OnUpdate) { + runtimeDescriptor.OnUpdate = + [&editorContext, onUpdate = descriptor.Lifecycle.OnUpdate]( + MetaCoreScene&, + MetaCoreGameObject& gameObject, + float deltaSeconds + ) { + onUpdate(editorContext, gameObject, deltaSeconds); + }; + } + if (descriptor.Lifecycle.OnDestroy) { + runtimeDescriptor.OnDestroy = + [&editorContext, onDestroy = descriptor.Lifecycle.OnDestroy](MetaCoreScene&, MetaCoreGameObject& gameObject) { + onDestroy(editorContext, gameObject); + }; + } + + (void)RuntimeLifecycleExecutor_.RegisterDescriptor(std::move(runtimeDescriptor)); } } - void InvokeLifecycleStart(MetaCoreEditorContext& editorContext) { - ForEachLifecycleComponent(editorContext, [&](const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, MetaCoreGameObject& gameObject) { - const std::string key = BuildLifecycleKey(gameObject.GetId(), descriptor.TypeId); - if (StartedLifecycleComponents_.contains(key)) { - return; - } - - StartedLifecycleComponents_.insert(key); - if (descriptor.Lifecycle.OnStart) { - descriptor.Lifecycle.OnStart(editorContext, gameObject); - } - }); - } - - void InvokeLifecycleUpdate(MetaCoreEditorContext& editorContext, float deltaSeconds) { - ForEachLifecycleComponent(editorContext, [&](const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, MetaCoreGameObject& gameObject) { - if (!descriptor.Lifecycle.OnUpdate) { - return; - } - const std::string key = BuildLifecycleKey(gameObject.GetId(), descriptor.TypeId); - if (!StartedLifecycleComponents_.contains(key)) { - return; - } - descriptor.Lifecycle.OnUpdate(editorContext, gameObject, deltaSeconds); - }); - } - - void InvokeLifecycleDestroy(MetaCoreEditorContext& editorContext) { - ForEachLifecycleComponent(editorContext, [&](const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, MetaCoreGameObject& gameObject) { - if (!descriptor.Lifecycle.OnDestroy) { - return; - } - const std::string key = BuildLifecycleKey(gameObject.GetId(), descriptor.TypeId); - if (!StartedLifecycleComponents_.contains(key)) { - return; - } - descriptor.Lifecycle.OnDestroy(editorContext, gameObject); - }); - } - MetaCorePlayModeState State_ = MetaCorePlayModeState::Edit; - float ElapsedPlayTimeSeconds_ = 0.0F; std::optional EditStateSnapshot_{}; - std::unordered_set StartedLifecycleComponents_{}; + MetaCoreRuntimeLifecycleExecutor RuntimeLifecycleExecutor_{}; std::shared_ptr ComponentRegistry_{}; }; @@ -6756,15 +7267,13 @@ std::optional MetaCoreBuiltinPrefabService::CreatePrefabF return std::nullopt; } - std::vector prefabObjects = MetaCoreCaptureSubtree(editorContext.GetScene(), activeObjectId); + const 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 normalizedPrefabPath = MetaCoreNormalizePrefabJsonPath(relativePrefabPath); const std::filesystem::path absolutePrefabPath = AssetDatabaseService_->GetProjectDescriptor().RootPath / normalizedPrefabPath; MetaCoreAssetGuid prefabGuid; @@ -6788,15 +7297,12 @@ std::optional MetaCoreBuiltinPrefabService::CreatePrefabF std::vector prefabDataObjects; prefabDataObjects.reserve(prefabObjects.size()); for (const MetaCoreGameObject& gameObject : prefabObjects) { - MetaCoreGameObjectData data; - data.Id = gameObject.GetId(); - data.Name = gameObject.GetName(); - data.ParentId = gameObject.GetParentId(); - data.Transform = gameObject.GetComponent(); - if (gameObject.HasComponent()) data.Camera = gameObject.GetComponent(); - if (gameObject.HasComponent()) data.Light = gameObject.GetComponent(); - if (gameObject.HasComponent()) data.MeshRenderer = gameObject.GetComponent(); - prefabDataObjects.push_back(std::move(data)); + prefabDataObjects.push_back(MetaCoreCapturePrefabObjectData( + gameObject, + activeObjectId, + ComponentRegistry_, + ReflectionRegistry_->GetTypeRegistry() + )); } prefabDocument.GameObjects = std::move(prefabDataObjects); @@ -6816,6 +7322,27 @@ std::optional MetaCoreBuiltinPrefabService::CreatePrefabF (void)MetaCoreWriteMetaJson(MetaCoreBuildMetaPath(absolutePrefabPath), metadata); (void)AssetDatabaseService_->Refresh(); + (void)editorContext.ExecuteSnapshotCommand("Create Prefab Instance Link", [&]() { + bool updatedAnyMetadata = false; + for (MetaCoreGameObject gameObject : prefabObjects) { + if (!gameObject) { + continue; + } + + const MetaCorePrefabInstanceMetadata prefabMetadata{ + prefabGuid, + gameObject.GetId(), + activeObjectId + }; + if (gameObject.HasComponent()) { + gameObject.GetComponent() = prefabMetadata; + } else { + gameObject.AddComponent(prefabMetadata); + } + updatedAnyMetadata = true; + } + return updatedAnyMetadata; + }); editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Prefab", "已创建 Prefab: " + normalizedPrefabPath.generic_string()); return normalizedPrefabPath; } @@ -6857,6 +7384,13 @@ std::optional MetaCoreBuiltinPrefabService::InstantiatePrefabDocumen if (sourceObject.Camera.has_value()) clonedObject.AddComponent(*sourceObject.Camera); if (sourceObject.Light.has_value()) clonedObject.AddComponent(*sourceObject.Light); if (sourceObject.MeshRenderer.has_value()) clonedObject.AddComponent(*sourceObject.MeshRenderer); + MetaCoreRestorePrefabSerializedComponents( + editorContext, + clonedObject, + sourceObject, + ComponentRegistry_, + ReflectionRegistry_->GetTypeRegistry() + ); clonedObject.AddComponent(MetaCorePrefabInstanceMetadata{ prefabAssetGuid, @@ -6967,6 +7501,12 @@ bool MetaCoreBuiltinPrefabService::ApplySelectedPrefabInstance(MetaCoreEditorCon if (gameObject.HasComponent()) data.Camera = gameObject.GetComponent(); if (gameObject.HasComponent()) data.Light = gameObject.GetComponent(); if (gameObject.HasComponent()) data.MeshRenderer = gameObject.GetComponent(); + MetaCoreCapturePrefabSerializedComponents( + gameObject, + data, + ComponentRegistry_, + ReflectionRegistry_->GetTypeRegistry() + ); prefabDataObjects.push_back(std::move(data)); } @@ -6982,9 +7522,7 @@ bool MetaCoreBuiltinPrefabService::ApplySelectedPrefabInstance(MetaCoreEditorCon std::filesystem::path normalizedPrefabPath = !prefabAsset->PackagePath.empty() ? prefabAsset->PackagePath : prefabAsset->RelativePath; - if (normalizedPrefabPath.extension() == ".mcprefab") { - normalizedPrefabPath.replace_extension(".mcprefab.json"); - } + normalizedPrefabPath = MetaCoreNormalizePrefabJsonPath(normalizedPrefabPath); const std::filesystem::path absolutePrefabPath = AssetDatabaseService_->GetProjectDescriptor().RootPath / normalizedPrefabPath; bool saveSuccess = MetaCoreSceneSerializer::SavePrefabToJson(absolutePrefabPath, prefabDocument, ReflectionRegistry_->GetTypeRegistry()); @@ -7021,6 +7559,10 @@ bool MetaCoreBuiltinPrefabService::RevertSelectedPrefabInstance(MetaCoreEditorCo const MetaCoreId parentId = rootObject.GetParentId(); const bool reverted = editorContext.ExecuteSnapshotCommand("还原 Prefab 实例", [&]() { + if (const auto playModeService = editorContext.GetModuleRegistry().ResolveService(); + playModeService != nullptr) { + playModeService->NotifyGameObjectsWillBeDestroyed(editorContext, {*instanceRootId}); + } std::vector deletedIds = editorContext.GetScene().DeleteGameObjects({*instanceRootId}); if (deletedIds.empty()) { return false; @@ -7098,6 +7640,10 @@ bool MetaCoreBuiltinClipboardService::DeleteSelection(MetaCoreEditorContext& edi std::vector deletedIds; const bool deleted = editorContext.ExecuteSnapshotCommand("删除对象", [&]() { + if (const auto playModeService = editorContext.GetModuleRegistry().ResolveService(); + playModeService != nullptr) { + playModeService->NotifyGameObjectsWillBeDestroyed(editorContext, selectedIds); + } deletedIds = editorContext.GetScene().DeleteGameObjects(selectedIds); if (deletedIds.empty()) { return false; @@ -7451,8 +7997,8 @@ public: 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(); diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index 74213f2..f025886 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -4249,6 +4249,165 @@ void MetaCorePollPlayerProcess(MetaCoreEditorContext& editorContext) { } } +// 辅助函数:绘制 Unity/Infernux 风格的贴图插槽控件 (ObjectField) +// 支持拖放接收 texture 资产、X 键清空和 ◎ 弹出 Picker 列表 +void DrawTextureField( + const char* label, + MetaCoreAssetGuid& currentGuid, + MetaCoreIAssetDatabaseService& assetDatabaseService, + bool& changed +) { + ImGui::PushID(label); + + // 1. 绘制标签 + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(label); + + // 将贴图插槽控件对齐到大约 40% 的宽度,保留与 Unity 面板一致的比例 + ImGui::SameLine(ImGui::GetContentRegionMax().x * 0.40f); + + // 2. 解析当前绑定的贴图名称 + std::string previewName = "None (Texture)"; + std::string fullPathStr = "None"; + if (currentGuid.IsValid()) { + const auto record = assetDatabaseService.FindAssetByGuid(currentGuid); + if (record.has_value()) { + previewName = record->RelativePath.filename().string(); + fullPathStr = record->RelativePath.string(); + } else { + previewName = "Missing (" + currentGuid.ToString() + ")"; + fullPathStr = "Missing Guid: " + currentGuid.ToString(); + } + } + + // 3. 贴图插槽按钮,宽度减去清除按钮、选择按钮以及额外空隙 + float buttonWidth = ImGui::GetContentRegionAvail().x - 56.0f; + if (buttonWidth < 50.0f) { + buttonWidth = 50.0f; + } + + // 未绑定贴图时文字呈现灰色以示区分 + if (!currentGuid.IsValid()) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); + } + + if (ImGui::Button(previewName.c_str(), ImVec2(buttonWidth, 0.0f))) { + ImGui::OpenPopup("TexturePickerPopup"); + } + + if (!currentGuid.IsValid()) { + ImGui::PopStyleColor(); + } + + // 悬停显示完整相对路径 + if (ImGui::IsItemHovered() && currentGuid.IsValid()) { + ImGui::SetTooltip("%s", fullPathStr.c_str()); + } + + // 支持拖拽放置 + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(MetaCoreProjectAssetDragDropPayloadType)) { + if (payload->DataSize == sizeof(MetaCoreProjectAssetDragDropPayload)) { + const auto* assetPayload = static_cast(payload->Data); + if (assetPayload != nullptr && std::string(assetPayload->AssetType) == "texture") { + if (currentGuid != assetPayload->AssetGuid) { + currentGuid = assetPayload->AssetGuid; + changed = true; + } + } + } + } + ImGui::EndDragDropTarget(); + } + + ImGui::SameLine(0, 4); + + // 4. 清除按钮 (X) + if (ImGui::Button("X", ImVec2(22.0f, 0.0f))) { + if (currentGuid.IsValid()) { + currentGuid = MetaCoreAssetGuid{}; + changed = true; + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("清除绑定的贴图"); + } + + ImGui::SameLine(0, 4); + + // 5. Picker 弹出列表按钮 (◎) + if (ImGui::Button("◎", ImVec2(22.0f, 0.0f))) { + ImGui::OpenPopup("TexturePickerPopup"); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("选择贴图"); + } + + // 6. Picker 弹出列表 + if (ImGui::BeginPopup("TexturePickerPopup")) { + ImGui::TextDisabled("选择贴图资源"); + ImGui::Separator(); + + // 搜索过滤栏 + static char filterBuffer[128] = ""; + ImGui::InputText("##Filter", filterBuffer, IM_ARRAYSIZE(filterBuffer)); + ImGui::SameLine(); + if (ImGui::Button("清除")) { + filterBuffer[0] = '\0'; + } + + ImGui::Spacing(); + if (ImGui::BeginChild("TextureList", ImVec2(250.0f, 200.0f), true)) { + // "None" 选项 + const bool isNoneSelected = !currentGuid.IsValid(); + if (ImGui::Selectable("None", isNoneSelected)) { + if (currentGuid.IsValid()) { + currentGuid = MetaCoreAssetGuid{}; + changed = true; + } + ImGui::CloseCurrentPopup(); + } + + for (const auto& a : assetDatabaseService.GetAssetRegistry()) { + if (a.Type == "texture") { + std::string filename = a.RelativePath.filename().string(); + + // 忽略大小写进行搜索过滤 + if (filterBuffer[0] != '\0') { + std::string lowerFilename = filename; + for (char& c : lowerFilename) { + c = static_cast(std::tolower(static_cast(c))); + } + std::string lowerFilter = filterBuffer; + for (char& c : lowerFilter) { + c = static_cast(std::tolower(static_cast(c))); + } + if (lowerFilename.find(lowerFilter) == std::string::npos) { + continue; + } + } + + const bool selected = (currentGuid == a.Guid); + if (ImGui::Selectable(filename.c_str(), selected)) { + if (currentGuid != a.Guid) { + currentGuid = a.Guid; + changed = true; + } + ImGui::CloseCurrentPopup(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", a.RelativePath.string().c_str()); + } + } + } + ImGui::EndChild(); + } + ImGui::EndPopup(); + } + + ImGui::PopID(); +} + void DrawMaterialAssetDetails( MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, @@ -4282,7 +4441,7 @@ void DrawMaterialAssetDetails( bool changed = false; float color[3] = { materialDoc.BaseColor.r, materialDoc.BaseColor.g, materialDoc.BaseColor.b }; - if (ImGui::ColorEdit3("基础颜色 (Base Color)", color)) { + if (ImGui::ColorEdit3("基础颜色 (Albedo)", color)) { materialDoc.BaseColor = glm::vec3(color[0], color[1], color[2]); changed = true; } @@ -4302,46 +4461,16 @@ void DrawMaterialAssetDetails( ImGui::Spacing(); ImGui::Separator(); ImGui::Text("贴图绑定 (Textures)"); + ImGui::Spacing(); - auto drawTextureCombo = [&](const char* label, MetaCoreAssetGuid& currentGuid) { - std::string previewName = "None"; - if (currentGuid.IsValid()) { - const auto record = assetDatabaseService.FindAssetByGuid(currentGuid); - if (record.has_value()) { - previewName = record->RelativePath.filename().string(); - } else { - previewName = "Missing (" + currentGuid.ToString() + ")"; - } - } - - if (ImGui::BeginCombo(label, previewName.c_str())) { - if (ImGui::Selectable("None", !currentGuid.IsValid())) { - if (currentGuid.IsValid()) { - currentGuid = MetaCoreAssetGuid{}; - changed = true; - } - } - - for (const auto& a : assetDatabaseService.GetAssetRegistry()) { - if (a.Type == "texture") { - const bool selected = (currentGuid == a.Guid); - const std::string name = a.RelativePath.filename().string(); - if (ImGui::Selectable(name.c_str(), selected)) { - if (currentGuid != a.Guid) { - currentGuid = a.Guid; - changed = true; - } - } - } - } - ImGui::EndCombo(); - } - }; - - drawTextureCombo("基础颜色贴图 (Base Color Texture)", materialDoc.BaseColorTexture); - drawTextureCombo("法线贴图 (Normal Texture)", materialDoc.NormalTexture); - drawTextureCombo("金属粗糙度贴图 (Metallic Roughness)", materialDoc.MetallicRoughnessTexture); - drawTextureCombo("遮蔽贴图 (Occlusion/Ao Texture)", materialDoc.AoTexture); + // 使用全新的 Unity/Infernux 风格贴图字段控件进行渲染 + DrawTextureField("基础贴图 (Albedo Map)", materialDoc.BaseColorTexture, assetDatabaseService, changed); + ImGui::Spacing(); + DrawTextureField("法线贴图 (Normal Map)", materialDoc.NormalTexture, assetDatabaseService, changed); + ImGui::Spacing(); + DrawTextureField("金属粗糙度 (Metallic Roughness Map)", materialDoc.MetallicRoughnessTexture, assetDatabaseService, changed); + ImGui::Spacing(); + DrawTextureField("遮蔽贴图 (Occlusion Map)", materialDoc.AoTexture, assetDatabaseService, changed); if (changed) { if (assetEditingService->SaveMaterialAsset(selectedAsset.Guid, materialDoc)) { diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp index 832a1cb..0d3e08c 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp @@ -459,6 +459,20 @@ bool MetaCoreEditorContext::IsObjectSelected(MetaCoreId objectId) const { return std::find(SelectedObjectIds_.begin(), SelectedObjectIds_.end(), objectId) != SelectedObjectIds_.end(); } +MetaCoreId MetaCoreEditorContext::GetHoveredObjectId() const { return HoveredObjectId_; } + +bool MetaCoreEditorContext::IsObjectHovered(MetaCoreId objectId) const { + return objectId != 0 && HoveredObjectId_ == objectId; +} + +void MetaCoreEditorContext::SetHoveredObjectId(MetaCoreId objectId) { + HoveredObjectId_ = (objectId != 0 && (bool)Scene_.FindGameObject(objectId)) ? objectId : 0; +} + +void MetaCoreEditorContext::ClearHoveredObject() { + HoveredObjectId_ = 0; +} + void MetaCoreEditorContext::ClearSelection() { SelectedObjectIds_.clear(); ActiveObjectId_ = 0; @@ -586,20 +600,100 @@ MetaCoreGizmoOperation MetaCoreEditorContext::GetGizmoOperation() const { return void MetaCoreEditorContext::SetGizmoOperation(MetaCoreGizmoOperation operation) { GizmoOperation_ = operation; } MetaCoreGizmoMode MetaCoreEditorContext::GetGizmoMode() const { return GizmoMode_; } void MetaCoreEditorContext::SetGizmoMode(MetaCoreGizmoMode mode) { GizmoMode_ = mode; } +MetaCoreGizmoPivotMode MetaCoreEditorContext::GetGizmoPivotMode() const { return GizmoPivotMode_; } +void MetaCoreEditorContext::SetGizmoPivotMode(MetaCoreGizmoPivotMode mode) { GizmoPivotMode_ = mode; } const MetaCoreGizmoSnapSettings& MetaCoreEditorContext::GetGizmoSnapSettings() const { return GizmoSnapSettings_; } -void MetaCoreEditorContext::SetGizmoSnapSettings(const MetaCoreGizmoSnapSettings& settings) { GizmoSnapSettings_ = settings; } +void MetaCoreEditorContext::SetGizmoSnapSettings(const MetaCoreGizmoSnapSettings& settings) { + GizmoSnapSettings_ = settings; + const auto sanitizeStep = [](float value, float fallback, float minimum, float maximum) { + return std::clamp(std::isfinite(value) ? value : fallback, minimum, maximum); + }; + GizmoSnapSettings_.TranslationStep = sanitizeStep(settings.TranslationStep, 0.5F, 0.001F, 10000.0F); + GizmoSnapSettings_.RotationStepDegrees = sanitizeStep(settings.RotationStepDegrees, 15.0F, 0.1F, 360.0F); + GizmoSnapSettings_.ScaleStep = sanitizeStep(settings.ScaleStep, 0.1F, 0.001F, 10.0F); +} void MetaCoreEditorContext::SetGizmoSnapEnabled(bool enabled) { GizmoSnapSettings_.Enabled = enabled; } bool MetaCoreEditorContext::GetShowViewportGrid() const { return ShowViewportGrid_; } void MetaCoreEditorContext::SetShowViewportGrid(bool show) { ShowViewportGrid_ = show; } +bool MetaCoreEditorContext::GetShowViewportSelectionOverlay() const { return ShowViewportSelectionOverlay_; } +void MetaCoreEditorContext::SetShowViewportSelectionOverlay(bool show) { ShowViewportSelectionOverlay_ = show; } bool MetaCoreEditorContext::GetShowWorldOrigin() const { return ShowWorldOrigin_; } void MetaCoreEditorContext::SetShowWorldOrigin(bool show) { ShowWorldOrigin_ = show; } +void MetaCoreEditorContext::ClearComponentGizmoDrawList() { + ComponentGizmoLines_.clear(); + ComponentGizmoIcons_.clear(); +} +void MetaCoreEditorContext::DrawGizmoLine( + const glm::vec3& start, + const glm::vec3& end, + const glm::vec4& color, + float thickness +) { + ComponentGizmoLines_.push_back(MetaCoreComponentGizmoLine{ + start, + end, + color, + std::clamp(thickness, 0.5F, 16.0F) + }); +} +void MetaCoreEditorContext::DrawGizmoWireBox( + const glm::vec3& center, + const glm::vec3& size, + const glm::vec4& color, + float thickness +) { + const glm::vec3 halfSize = glm::abs(size) * 0.5F; + const glm::vec3 corners[8] = { + center + glm::vec3(-halfSize.x, -halfSize.y, -halfSize.z), + center + glm::vec3( halfSize.x, -halfSize.y, -halfSize.z), + center + glm::vec3(-halfSize.x, halfSize.y, -halfSize.z), + center + glm::vec3( halfSize.x, halfSize.y, -halfSize.z), + center + glm::vec3(-halfSize.x, -halfSize.y, halfSize.z), + center + glm::vec3( halfSize.x, -halfSize.y, halfSize.z), + center + glm::vec3(-halfSize.x, halfSize.y, halfSize.z), + center + glm::vec3( halfSize.x, halfSize.y, halfSize.z) + }; + constexpr int edges[12][2] = { + {0, 1}, {1, 3}, {3, 2}, {2, 0}, + {4, 5}, {5, 7}, {7, 6}, {6, 4}, + {0, 4}, {1, 5}, {2, 6}, {3, 7} + }; + for (const auto& edge : edges) { + DrawGizmoLine(corners[edge[0]], corners[edge[1]], color, thickness); + } +} +void MetaCoreEditorContext::DrawGizmoIcon( + const glm::vec3& worldPosition, + std::string label, + const glm::vec4& color, + float size +) { + ComponentGizmoIcons_.push_back(MetaCoreComponentGizmoIcon{ + worldPosition, + color, + std::move(label), + std::clamp(size, 3.0F, 32.0F) + }); +} +const std::vector& MetaCoreEditorContext::GetComponentGizmoLines() const { + return ComponentGizmoLines_; +} +const std::vector& MetaCoreEditorContext::GetComponentGizmoIcons() const { + return ComponentGizmoIcons_; +} MetaCoreReparentTransformRule MetaCoreEditorContext::GetReparentTransformRule() const { return ReparentTransformRule_; } void MetaCoreEditorContext::SetReparentTransformRule(MetaCoreReparentTransformRule rule) { ReparentTransformRule_ = rule; } MetaCoreEditorCommandService& MetaCoreEditorContext::GetCommandService() { return CommandService_; } const MetaCoreEditorCommandService& MetaCoreEditorContext::GetCommandService() const { return CommandService_; } +bool MetaCoreEditorContext::IsRuntimeSceneActive() const { return RuntimeSceneActive_; } +void MetaCoreEditorContext::SetRuntimeSceneActive(bool active) { RuntimeSceneActive_ = active; } bool MetaCoreEditorContext::ExecuteCommand(std::unique_ptr command) { + if (RuntimeSceneActive_) { + return command != nullptr && command->Execute(*this); + } + if (!CommandService_.Execute(std::move(command), *this)) { return false; } @@ -610,6 +704,10 @@ bool MetaCoreEditorContext::ExecuteCommand(std::unique_ptr& mutator) { + if (RuntimeSceneActive_) { + (void)label; + if (!mutator()) { + return false; + } + NormalizeSelection(); + return true; + } + const MetaCoreEditorStateSnapshot beforeSnapshot = CaptureStateSnapshot(); if (!mutator()) { return false; @@ -1095,6 +1211,9 @@ void MetaCoreEditorContext::NormalizeSelection() { if (SelectionAnchorId_ == 0 || !deduplicatedIds.contains(SelectionAnchorId_)) { SelectionAnchorId_ = ActiveObjectId_; } + if (HoveredObjectId_ != 0 && !Scene_.FindGameObject(HoveredObjectId_)) { + HoveredObjectId_ = 0; + } } void MetaCoreEditorContext::NotifySelectionChanged() { diff --git a/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp b/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp index 15bfaa4..75037c1 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp @@ -101,6 +101,42 @@ std::vector MetaCoreBuildSelectionTransformRoots( return rootIds; } +glm::mat4 MetaCoreBuildSelectionGizmoWorldMatrix( + const MetaCoreEditorContext& editorContext, + MetaCoreId activeObjectId +) { + glm::mat4 gizmoWorldMatrix = MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), activeObjectId); + if (editorContext.GetGizmoPivotMode() != MetaCoreGizmoPivotMode::Center) { + return gizmoWorldMatrix; + } + + const std::vector rootIds = MetaCoreBuildSelectionTransformRoots( + editorContext.GetScene(), + editorContext.GetSelectedObjectIds(), + activeObjectId + ); + if (rootIds.size() < 2) { + return gizmoWorldMatrix; + } + + glm::vec3 center(0.0F); + std::size_t validRootCount = 0; + for (const MetaCoreId rootId : rootIds) { + const MetaCoreGameObject rootObject = editorContext.GetScene().FindGameObject(rootId); + if (!rootObject) { + continue; + } + center += glm::vec3(MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), rootId)[3]); + ++validRootCount; + } + if (validRootCount == 0) { + return gizmoWorldMatrix; + } + + gizmoWorldMatrix[3] = glm::vec4(center / static_cast(validRootCount), 1.0F); + return gizmoWorldMatrix; +} + ImGuizmo::OPERATION MetaCoreToImGuizmoOperation(MetaCoreGizmoOperation operation) { switch (operation) { case MetaCoreGizmoOperation::Translate: return ImGuizmo::TRANSLATE; @@ -128,6 +164,40 @@ const char* MetaCoreGizmoModeLabel(MetaCoreGizmoMode mode) { } } +void MetaCoreExpandFocusBounds( + const MetaCoreScene& scene, + MetaCoreId objectId, + bool& initialized, + glm::vec3& boundsMin, + glm::vec3& boundsMax +) { + const MetaCoreGameObject object = scene.FindGameObject(objectId); + if (!object) { + return; + } + + const glm::mat4 worldMatrix = MetaCoreBuildWorldTransformMatrix(scene, objectId); + const glm::vec3 worldPosition(worldMatrix[3]); + const float extent = std::max({ + glm::length(glm::vec3(worldMatrix[0])), + glm::length(glm::vec3(worldMatrix[1])), + glm::length(glm::vec3(worldMatrix[2])), + 0.5F + }); + const glm::vec3 objectMin = worldPosition - glm::vec3(extent); + const glm::vec3 objectMax = worldPosition + glm::vec3(extent); + + if (!initialized) { + boundsMin = objectMin; + boundsMax = objectMax; + initialized = true; + return; + } + + boundsMin = glm::min(boundsMin, objectMin); + boundsMax = glm::max(boundsMax, objectMax); +} + } // namespace void MetaCoreSceneInteractionService::ResetFrameState() { @@ -265,6 +335,26 @@ MetaCoreId MetaCoreSceneInteractionService::PickGameObjectFromViewport( return bestObjectId; } +void MetaCoreSceneInteractionService::UpdateViewportHover( + MetaCoreEditorContext& editorContext, + const MetaCoreSceneView& sceneView, + bool allowPicking +) const { + const MetaCoreSceneViewportState& viewportState = editorContext.GetSceneViewportState(); + if (!allowPicking || !viewportState.Hovered) { + editorContext.ClearHoveredObject(); + return; + } + + const MetaCoreId hoveredObjectId = PickGameObjectFromViewport( + editorContext.GetScene(), + sceneView, + viewportState, + editorContext.GetInput().GetCursorPosition() + ); + editorContext.SetHoveredObjectId(hoveredObjectId); +} + void MetaCoreSceneInteractionService::ApplyViewportSelection(MetaCoreEditorContext& editorContext, MetaCoreId pickedObjectId) const { if (const auto selectionService = editorContext.GetModuleRegistry().ResolveService(); selectionService != nullptr) { @@ -293,7 +383,7 @@ void MetaCoreSceneInteractionService::HandleGizmoEndUse(MetaCoreEditorContext& e if (!gizmoUsing && GizmoWasUsing_) { if (HasGizmoBeforeSnapshot_) { const MetaCoreEditorStateSnapshot afterSnapshot = editorContext.CaptureStateSnapshot(); - editorContext.CommitStateTransition("Gizmo Transform", GizmoBeforeSnapshot_, afterSnapshot, true); + (void)editorContext.CommitStateTransition("Gizmo Transform", GizmoBeforeSnapshot_, afterSnapshot, false); } GizmoWasUsing_ = false; HasGizmoBeforeSnapshot_ = false; @@ -307,41 +397,11 @@ void MetaCoreSceneInteractionService::FocusSelectedObject(MetaCoreEditorContext& return; } - if (selectedObjectIds.size() == 1) { - MetaCoreGameObject selectedObject = editorContext.GetSelectedGameObject(); - if (selectedObject) { - editorContext.GetCameraController().FocusGameObject(selectedObject); - } - return; - } - bool initialized = false; glm::vec3 boundsMin(0.0F); glm::vec3 boundsMax(0.0F); for (const MetaCoreId selectedObjectId : selectedObjectIds) { - const MetaCoreGameObject selectedObject = editorContext.GetScene().FindGameObject(selectedObjectId); - if (!selectedObject) { - continue; - } - - const float extent = std::max({ - std::abs(selectedObject.GetComponent().Scale.x), - std::abs(selectedObject.GetComponent().Scale.y), - std::abs(selectedObject.GetComponent().Scale.z), - 0.5F - }); - const glm::vec3 objectMin = selectedObject.GetComponent().Position - glm::vec3(extent); - const glm::vec3 objectMax = selectedObject.GetComponent().Position + glm::vec3(extent); - - if (!initialized) { - boundsMin = objectMin; - boundsMax = objectMax; - initialized = true; - continue; - } - - boundsMin = glm::min(boundsMin, objectMin); - boundsMax = glm::max(boundsMax, objectMax); + MetaCoreExpandFocusBounds(editorContext.GetScene(), selectedObjectId, initialized, boundsMin, boundsMax); } if (!initialized) { @@ -365,24 +425,7 @@ void MetaCoreSceneInteractionService::FocusVisibleScene(MetaCoreEditorContext& e continue; } - const float extent = std::max({ - std::abs(gameObject.GetComponent().Scale.x), - std::abs(gameObject.GetComponent().Scale.y), - std::abs(gameObject.GetComponent().Scale.z), - 0.5F - }); - const glm::vec3 objectMin = gameObject.GetComponent().Position - glm::vec3(extent); - const glm::vec3 objectMax = gameObject.GetComponent().Position + glm::vec3(extent); - - if (!initialized) { - boundsMin = objectMin; - boundsMax = objectMax; - initialized = true; - continue; - } - - boundsMin = glm::min(boundsMin, objectMin); - boundsMax = glm::max(boundsMax, objectMax); + MetaCoreExpandFocusBounds(editorContext.GetScene(), gameObject.GetId(), initialized, boundsMin, boundsMax); } if (!initialized) { @@ -400,7 +443,8 @@ bool MetaCoreSceneInteractionService::ApplyWorldTransformDeltaToSelection( MetaCoreEditorContext& editorContext, MetaCoreId manipulatedObjectId, const glm::mat4& originalWorldMatrix, - const glm::mat4& newWorldMatrix + const glm::mat4& newWorldMatrix, + MetaCoreGizmoPivotMode pivotMode ) const { if (manipulatedObjectId == 0 || !editorContext.GetScene().FindGameObject(manipulatedObjectId)) { return false; @@ -429,7 +473,9 @@ bool MetaCoreSceneInteractionService::ApplyWorldTransformDeltaToSelection( const glm::mat4 currentWorldMatrix = MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), objectId); const glm::mat4 targetWorldMatrix = - objectId == manipulatedObjectId ? newWorldMatrix : deltaMatrix * currentWorldMatrix; + pivotMode == MetaCoreGizmoPivotMode::Pivot && objectId == manipulatedObjectId + ? newWorldMatrix + : deltaMatrix * currentWorldMatrix; glm::mat4 targetLocalMatrix = targetWorldMatrix; if (object.GetParentId() != 0) { @@ -468,7 +514,7 @@ void MetaCoreSceneInteractionService::HandleGizmoManipulation(MetaCoreEditorCont gizmoAspect, 0.1F, 1000.0F ); - glm::mat4 worldMatrixMetaCore = MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), selectedObject.GetId()); + glm::mat4 worldMatrixMetaCore = MetaCoreBuildSelectionGizmoWorldMatrix(editorContext, selectedObject.GetId()); const glm::mat4 originalWorldMatrixMetaCore = worldMatrixMetaCore; ImGuizmo::SetOrthographic(false); @@ -514,7 +560,8 @@ void MetaCoreSceneInteractionService::HandleGizmoManipulation(MetaCoreEditorCont editorContext, selectedObject.GetId(), originalWorldMatrixMetaCore, - gizmoMatrix + gizmoMatrix, + editorContext.GetGizmoPivotMode() ); } HandleGizmoEndUse(editorContext, gizmoUsing); @@ -609,6 +656,15 @@ void MetaCoreSceneInteractionService::DrawViewportToolbar(MetaCoreEditorContext& editorContext.SetGizmoMode(MetaCoreGizmoMode::Global); } + ImGui::SameLine(0, 12); + if (drawToolbarToggle(" Pivot ", editorContext.GetGizmoPivotMode() == MetaCoreGizmoPivotMode::Pivot)) { + editorContext.SetGizmoPivotMode(MetaCoreGizmoPivotMode::Pivot); + } + ImGui::SameLine(0, 2); + if (drawToolbarToggle(" Center ", editorContext.GetGizmoPivotMode() == MetaCoreGizmoPivotMode::Center)) { + editorContext.SetGizmoPivotMode(MetaCoreGizmoPivotMode::Center); + } + ImGui::SameLine(0, 12); const MetaCoreGizmoSnapSettings& snapSettings = editorContext.GetGizmoSnapSettings(); if (drawToolbarToggle(" Snap ", snapSettings.Enabled)) { @@ -637,6 +693,11 @@ void MetaCoreSceneInteractionService::DrawViewportToolbar(MetaCoreEditorContext& editorContext.SetShowViewportGrid(!showGrid); } ImGui::SameLine(); + bool showSelectionOverlay = editorContext.GetShowViewportSelectionOverlay(); + if (drawToolbarToggle(" Outline ", showSelectionOverlay)) { + editorContext.SetShowViewportSelectionOverlay(!showSelectionOverlay); + } + ImGui::SameLine(); bool showOrigin = editorContext.GetShowWorldOrigin(); if (drawToolbarToggle(" 原点 ", showOrigin)) { editorContext.SetShowWorldOrigin(!showOrigin); @@ -669,6 +730,13 @@ void MetaCoreSceneInteractionService::DrawViewportToolbar(MetaCoreEditorContext& activeObject ? activeObject.GetName().c_str() : "<缺失>" ); } + ImGui::SameLine(0, 12); + const MetaCoreGameObject hoveredObject = + editorContext.GetScene().FindGameObject(editorContext.GetHoveredObjectId()); + ImGui::TextDisabled( + "Hover: %s", + hoveredObject ? hoveredObject.GetName().c_str() : "None" + ); } ImGui::End(); ImGui::PopStyleColor(); diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h index 67d09aa..cd8eddf 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h @@ -11,6 +11,8 @@ #include #include +#include +#include #include #include #include @@ -38,6 +40,11 @@ enum class MetaCoreGizmoMode { Global }; +enum class MetaCoreGizmoPivotMode { + Pivot = 0, + Center +}; + enum class MetaCoreReparentTransformRule { KeepWorld = 0, KeepLocal @@ -82,6 +89,20 @@ struct MetaCoreSceneViewportState { bool Focused = false; }; +struct MetaCoreComponentGizmoLine { + glm::vec3 Start{0.0F}; + glm::vec3 End{0.0F}; + glm::vec4 Color{1.0F}; + float Thickness = 1.5F; +}; + +struct MetaCoreComponentGizmoIcon { + glm::vec3 WorldPosition{0.0F}; + glm::vec4 Color{1.0F}; + std::string Label{}; + float Size = 7.0F; +}; + struct MetaCoreSelectedAssetState { MetaCoreAssetGuid Guid{}; std::filesystem::path RelativePath{}; @@ -144,6 +165,10 @@ public: [[nodiscard]] const std::vector& GetSelectedObjectIds() const; [[nodiscard]] MetaCoreId GetActiveObjectId() const; [[nodiscard]] bool IsObjectSelected(MetaCoreId objectId) const; + [[nodiscard]] MetaCoreId GetHoveredObjectId() const; + [[nodiscard]] bool IsObjectHovered(MetaCoreId objectId) const; + void SetHoveredObjectId(MetaCoreId objectId); + void ClearHoveredObject(); void ClearSelection(); void SelectOnly(MetaCoreId objectId); void ToggleSelection(MetaCoreId objectId); @@ -160,17 +185,44 @@ public: void SetGizmoOperation(MetaCoreGizmoOperation operation); [[nodiscard]] MetaCoreGizmoMode GetGizmoMode() const; void SetGizmoMode(MetaCoreGizmoMode mode); + [[nodiscard]] MetaCoreGizmoPivotMode GetGizmoPivotMode() const; + void SetGizmoPivotMode(MetaCoreGizmoPivotMode mode); [[nodiscard]] const MetaCoreGizmoSnapSettings& GetGizmoSnapSettings() const; void SetGizmoSnapSettings(const MetaCoreGizmoSnapSettings& settings); void SetGizmoSnapEnabled(bool enabled); [[nodiscard]] bool GetShowViewportGrid() const; void SetShowViewportGrid(bool show); + [[nodiscard]] bool GetShowViewportSelectionOverlay() const; + void SetShowViewportSelectionOverlay(bool show); [[nodiscard]] bool GetShowWorldOrigin() const; void SetShowWorldOrigin(bool show); + void ClearComponentGizmoDrawList(); + void DrawGizmoLine( + const glm::vec3& start, + const glm::vec3& end, + const glm::vec4& color = glm::vec4(1.0F), + float thickness = 1.5F + ); + void DrawGizmoWireBox( + const glm::vec3& center, + const glm::vec3& size, + const glm::vec4& color = glm::vec4(1.0F), + float thickness = 1.5F + ); + void DrawGizmoIcon( + const glm::vec3& worldPosition, + std::string label, + const glm::vec4& color = glm::vec4(1.0F), + float size = 7.0F + ); + [[nodiscard]] const std::vector& GetComponentGizmoLines() const; + [[nodiscard]] const std::vector& GetComponentGizmoIcons() const; [[nodiscard]] MetaCoreReparentTransformRule GetReparentTransformRule() const; void SetReparentTransformRule(MetaCoreReparentTransformRule rule); [[nodiscard]] MetaCoreEditorCommandService& GetCommandService(); [[nodiscard]] const MetaCoreEditorCommandService& GetCommandService() const; + [[nodiscard]] bool IsRuntimeSceneActive() const; + void SetRuntimeSceneActive(bool active); bool ExecuteCommand(std::unique_ptr command); bool UndoCommand(); bool RedoCommand(); @@ -225,13 +277,19 @@ private: std::vector SelectedObjectIds_{}; MetaCoreId ActiveObjectId_ = 0; MetaCoreId SelectionAnchorId_ = 0; + MetaCoreId HoveredObjectId_ = 0; MetaCoreGizmoOperation GizmoOperation_ = MetaCoreGizmoOperation::Translate; MetaCoreGizmoMode GizmoMode_ = MetaCoreGizmoMode::Local; + MetaCoreGizmoPivotMode GizmoPivotMode_ = MetaCoreGizmoPivotMode::Pivot; MetaCoreGizmoSnapSettings GizmoSnapSettings_{}; bool ShowViewportGrid_ = true; + bool ShowViewportSelectionOverlay_ = true; bool ShowWorldOrigin_ = true; + std::vector ComponentGizmoLines_{}; + std::vector ComponentGizmoIcons_{}; MetaCoreReparentTransformRule ReparentTransformRule_ = MetaCoreReparentTransformRule::KeepWorld; MetaCoreEditorCommandService CommandService_{}; + bool RuntimeSceneActive_ = false; MetaCoreId PendingRenameObjectId_ = 0; bool DockLayoutBuilt_ = false; bool RuntimeDataConfigLoaded_ = false; diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h index b063f19..7e075ae 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h @@ -14,6 +14,7 @@ #include #include #include +#include #include namespace MetaCore { @@ -147,6 +148,13 @@ public: [[nodiscard]] virtual bool ReimportAsset(const MetaCoreAssetGuid& assetGuid) = 0; virtual bool Refresh() = 0; virtual bool CreateFolder(const std::filesystem::path& relativeDirectory) = 0; + virtual std::optional CreateSceneAsset( + const std::filesystem::path& relativeScenePath, + bool makeStartupScene + ) = 0; + virtual std::optional CreatePrefabAsset(const std::filesystem::path& relativePrefabPath) = 0; + virtual std::optional CreateMaterialAsset(const std::filesystem::path& relativeMaterialPath) = 0; + virtual std::optional CreateUiDocumentAsset(const std::filesystem::path& relativeUiPath) = 0; virtual bool RegisterScenePath(const std::filesystem::path& relativeScenePath, bool makeStartupScene) = 0; virtual bool SetStartupScenePath(const std::filesystem::path& relativeScenePath) = 0; }; @@ -363,6 +371,7 @@ enum class MetaCorePlayModeState { class MetaCoreIPlayModeService : public MetaCoreIEditorService { public: [[nodiscard]] virtual MetaCorePlayModeState GetState() const = 0; + [[nodiscard]] virtual bool IsRuntimeSceneActive() const = 0; [[nodiscard]] virtual bool CanEnterPlayMode() const = 0; [[nodiscard]] virtual bool EnterPlayMode(MetaCoreEditorContext& editorContext) = 0; [[nodiscard]] virtual bool ExitPlayMode(MetaCoreEditorContext& editorContext) = 0; @@ -370,6 +379,15 @@ public: [[nodiscard]] virtual bool ResumePlayMode(MetaCoreEditorContext& editorContext) = 0; [[nodiscard]] virtual bool StepPlayMode(MetaCoreEditorContext& editorContext, float deltaSeconds) = 0; [[nodiscard]] virtual float GetElapsedPlayTimeSeconds() const = 0; + virtual void NotifyGameObjectsWillBeDestroyed( + MetaCoreEditorContext& editorContext, + const std::vector& objectIds + ) = 0; + virtual void NotifyComponentWillBeRemoved( + MetaCoreEditorContext& editorContext, + MetaCoreId objectId, + std::string_view componentTypeId + ) = 0; virtual void TickPlayMode(MetaCoreEditorContext& editorContext, float deltaSeconds) = 0; }; diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h index 00e6d70..af128ad 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h @@ -21,6 +21,11 @@ public: const glm::vec2& cursorPosition ) const; + void UpdateViewportHover( + MetaCoreEditorContext& editorContext, + const MetaCoreSceneView& sceneView, + bool allowPicking + ) const; void ApplyViewportSelection(MetaCoreEditorContext& editorContext, MetaCoreId pickedObjectId) const; void HandleGizmoBeginUse(MetaCoreEditorContext& editorContext, bool gizmoUsing); void HandleGizmoEndUse(MetaCoreEditorContext& editorContext, bool gizmoUsing); @@ -30,7 +35,8 @@ public: MetaCoreEditorContext& editorContext, MetaCoreId manipulatedObjectId, const glm::mat4& originalWorldMatrix, - const glm::mat4& newWorldMatrix + const glm::mat4& newWorldMatrix, + MetaCoreGizmoPivotMode pivotMode = MetaCoreGizmoPivotMode::Pivot ) const; void HandleGizmoManipulation(MetaCoreEditorContext& editorContext); void DrawViewportToolbar(MetaCoreEditorContext& editorContext); diff --git a/Source/MetaCoreScene/Private/MetaCoreRuntimeLifecycle.cpp b/Source/MetaCoreScene/Private/MetaCoreRuntimeLifecycle.cpp new file mode 100644 index 0000000..aa56e41 --- /dev/null +++ b/Source/MetaCoreScene/Private/MetaCoreRuntimeLifecycle.cpp @@ -0,0 +1,326 @@ +#include "MetaCoreScene/MetaCoreRuntimeLifecycle.h" + +#include +#include +#include + +namespace MetaCore { + +void MetaCoreRuntimeLifecycleExecutor::ClearDescriptors() { + Descriptors_.clear(); + StartedComponents_.clear(); + Running_ = false; + ElapsedTimeSeconds_ = 0.0F; +} + +bool MetaCoreRuntimeLifecycleExecutor::RegisterDescriptor(MetaCoreRuntimeLifecycleDescriptor descriptor) { + if (descriptor.TypeId.empty() || !descriptor.HasComponent) { + return false; + } + + const auto iterator = std::find_if( + Descriptors_.begin(), + Descriptors_.end(), + [&](const MetaCoreRuntimeLifecycleDescriptor& registeredDescriptor) { + return registeredDescriptor.TypeId == descriptor.TypeId; + } + ); + if (iterator != Descriptors_.end()) { + return false; + } + + Descriptors_.push_back(std::move(descriptor)); + return true; +} + +std::size_t MetaCoreRuntimeLifecycleExecutor::RegisterDescriptors( + const std::vector& descriptors +) { + std::size_t registeredCount = 0; + for (const MetaCoreRuntimeLifecycleDescriptor& descriptor : descriptors) { + if (RegisterDescriptor(descriptor)) { + ++registeredCount; + } + } + return registeredCount; +} + +const std::vector& MetaCoreRuntimeLifecycleExecutor::GetDescriptors() const { + return Descriptors_; +} + +void MetaCoreRuntimeLifecycleExecutor::EnterScene(MetaCoreScene& scene, ErrorSink errorSink) { + StartedComponents_.clear(); + ElapsedTimeSeconds_ = 0.0F; + Running_ = true; + InvokeStartForNewComponents(scene, errorSink); +} + +void MetaCoreRuntimeLifecycleExecutor::TickScene(MetaCoreScene& scene, float deltaSeconds, ErrorSink errorSink) { + if (!Running_) { + return; + } + + const float sanitizedDeltaSeconds = std::max(deltaSeconds, 0.0F); + InvokeStartForNewComponents(scene, errorSink); + + const std::vector objectIds = scene.BuildHierarchyPreorder(); + for (const MetaCoreRuntimeLifecycleDescriptor& descriptor : Descriptors_) { + if (!descriptor.OnUpdate) { + continue; + } + + for (const MetaCoreId objectId : objectIds) { + MetaCoreGameObject gameObject = scene.FindGameObject(objectId); + if (!gameObject || !descriptor.HasComponent(gameObject)) { + continue; + } + + const std::string key = BuildLifecycleKey(objectId, descriptor.TypeId); + if (!StartedComponents_.contains(key)) { + continue; + } + + try { + descriptor.OnUpdate(scene, gameObject, sanitizedDeltaSeconds); + } catch (const std::exception& exception) { + ReportException(errorSink, "OnUpdate", descriptor, objectId, exception); + } catch (...) { + ReportUnknownException(errorSink, "OnUpdate", descriptor, objectId); + } + } + } + + ElapsedTimeSeconds_ += sanitizedDeltaSeconds; +} + +void MetaCoreRuntimeLifecycleExecutor::NotifyGameObjectsWillBeDestroyed( + MetaCoreScene& scene, + const std::vector& objectIds, + ErrorSink errorSink +) { + if (!Running_ || objectIds.empty()) { + return; + } + + std::vector affectedObjectIds; + std::unordered_set deduplicatedObjectIds; + for (const MetaCoreId objectId : objectIds) { + if (objectId == 0 || deduplicatedObjectIds.contains(objectId)) { + continue; + } + + const std::vector subtreeObjectIds = scene.GetSubtreeObjectIds(objectId); + if (subtreeObjectIds.empty()) { + continue; + } + + for (const MetaCoreId subtreeObjectId : subtreeObjectIds) { + if (deduplicatedObjectIds.insert(subtreeObjectId).second) { + affectedObjectIds.push_back(subtreeObjectId); + } + } + } + + for (const MetaCoreId objectId : affectedObjectIds) { + for (const MetaCoreRuntimeLifecycleDescriptor& descriptor : Descriptors_) { + (void)InvokeDestroyForStartedComponent(scene, descriptor, objectId, errorSink); + } + } +} + +void MetaCoreRuntimeLifecycleExecutor::NotifyComponentWillBeRemoved( + MetaCoreScene& scene, + MetaCoreId objectId, + std::string_view componentTypeId, + ErrorSink errorSink +) { + if (!Running_ || objectId == 0 || componentTypeId.empty()) { + return; + } + + const auto iterator = std::find_if( + Descriptors_.begin(), + Descriptors_.end(), + [&](const MetaCoreRuntimeLifecycleDescriptor& descriptor) { + return descriptor.TypeId == componentTypeId; + } + ); + if (iterator == Descriptors_.end()) { + return; + } + + (void)InvokeDestroyForStartedComponent(scene, *iterator, objectId, errorSink); +} + +void MetaCoreRuntimeLifecycleExecutor::ExitScene(MetaCoreScene& scene, ErrorSink errorSink) { + if (!Running_) { + return; + } + + const std::vector objectIds = scene.BuildHierarchyPreorder(); + for (const MetaCoreRuntimeLifecycleDescriptor& descriptor : Descriptors_) { + for (const MetaCoreId objectId : objectIds) { + (void)InvokeDestroyForStartedComponent(scene, descriptor, objectId, errorSink); + } + } + + StartedComponents_.clear(); + Running_ = false; + ElapsedTimeSeconds_ = 0.0F; +} + +bool MetaCoreRuntimeLifecycleExecutor::IsRunning() const { + return Running_; +} + +float MetaCoreRuntimeLifecycleExecutor::GetElapsedTimeSeconds() const { + return ElapsedTimeSeconds_; +} + +std::string MetaCoreRuntimeLifecycleExecutor::BuildLifecycleKey( + MetaCoreId objectId, + std::string_view componentTypeId +) const { + return std::to_string(objectId) + ":" + std::string(componentTypeId); +} + +void MetaCoreRuntimeLifecycleExecutor::InvokeStartForNewComponents( + MetaCoreScene& scene, + const ErrorSink& errorSink +) { + const std::vector objectIds = scene.BuildHierarchyPreorder(); + for (const MetaCoreRuntimeLifecycleDescriptor& descriptor : Descriptors_) { + for (const MetaCoreId objectId : objectIds) { + MetaCoreGameObject gameObject = scene.FindGameObject(objectId); + if (!gameObject || !descriptor.HasComponent(gameObject)) { + continue; + } + + const std::string key = BuildLifecycleKey(objectId, descriptor.TypeId); + if (StartedComponents_.contains(key)) { + continue; + } + + StartedComponents_.insert(key); + if (!descriptor.OnStart) { + continue; + } + + try { + descriptor.OnStart(scene, gameObject); + } catch (const std::exception& exception) { + ReportException(errorSink, "OnStart", descriptor, objectId, exception); + } catch (...) { + ReportUnknownException(errorSink, "OnStart", descriptor, objectId); + } + } + } +} + +bool MetaCoreRuntimeLifecycleExecutor::InvokeDestroyForStartedComponent( + MetaCoreScene& scene, + const MetaCoreRuntimeLifecycleDescriptor& descriptor, + MetaCoreId objectId, + const ErrorSink& errorSink +) { + if (descriptor.TypeId.empty() || objectId == 0) { + return false; + } + + const std::string key = BuildLifecycleKey(objectId, descriptor.TypeId); + if (!StartedComponents_.contains(key)) { + return false; + } + + MetaCoreGameObject gameObject = scene.FindGameObject(objectId); + if (!gameObject || !descriptor.HasComponent(gameObject)) { + StartedComponents_.erase(key); + return false; + } + + if (descriptor.OnDestroy) { + try { + descriptor.OnDestroy(scene, gameObject); + } catch (const std::exception& exception) { + ReportException(errorSink, "OnDestroy", descriptor, objectId, exception); + } catch (...) { + ReportUnknownException(errorSink, "OnDestroy", descriptor, objectId); + } + } + + StartedComponents_.erase(key); + return true; +} + +void MetaCoreRuntimeLifecycleExecutor::ReportException( + const ErrorSink& errorSink, + std::string_view phase, + const MetaCoreRuntimeLifecycleDescriptor& descriptor, + MetaCoreId objectId, + const std::exception& exception +) const { + if (!errorSink) { + return; + } + + errorSink(MetaCoreRuntimeLifecycleError{ + std::string(phase), + descriptor.TypeId, + objectId, + exception.what() + }); +} + +void MetaCoreRuntimeLifecycleExecutor::ReportUnknownException( + const ErrorSink& errorSink, + std::string_view phase, + const MetaCoreRuntimeLifecycleDescriptor& descriptor, + MetaCoreId objectId +) const { + if (!errorSink) { + return; + } + + errorSink(MetaCoreRuntimeLifecycleError{ + std::string(phase), + descriptor.TypeId, + objectId, + "unknown exception" + }); +} + +void MetaCoreRuntimeLifecycleRegistry::Clear() { + Descriptors_.clear(); +} + +bool MetaCoreRuntimeLifecycleRegistry::RegisterDescriptor(MetaCoreRuntimeLifecycleDescriptor descriptor) { + if (descriptor.TypeId.empty() || !descriptor.HasComponent) { + return false; + } + + const auto iterator = std::find_if( + Descriptors_.begin(), + Descriptors_.end(), + [&](const MetaCoreRuntimeLifecycleDescriptor& registeredDescriptor) { + return registeredDescriptor.TypeId == descriptor.TypeId; + } + ); + if (iterator != Descriptors_.end()) { + return false; + } + + Descriptors_.push_back(std::move(descriptor)); + return true; +} + +const std::vector& MetaCoreRuntimeLifecycleRegistry::GetDescriptors() const { + return Descriptors_; +} + +MetaCoreRuntimeLifecycleRegistry& MetaCoreGetRuntimeLifecycleRegistry() { + static MetaCoreRuntimeLifecycleRegistry registry; + return registry; +} + +} // namespace MetaCore diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreRuntimeLifecycle.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreRuntimeLifecycle.h new file mode 100644 index 0000000..693d3fb --- /dev/null +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreRuntimeLifecycle.h @@ -0,0 +1,99 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreId.h" +#include "MetaCoreScene/MetaCoreGameObject.h" +#include "MetaCoreScene/MetaCoreScene.h" + +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +struct MetaCoreRuntimeLifecycleError { + std::string Phase{}; + std::string ComponentTypeId{}; + MetaCoreId ObjectId = 0; + std::string Message{}; +}; + +struct MetaCoreRuntimeLifecycleDescriptor { + std::string TypeId{}; + std::function HasComponent{}; + std::function OnStart{}; + std::function OnUpdate{}; + std::function OnDestroy{}; +}; + +class MetaCoreRuntimeLifecycleExecutor { +public: + using ErrorSink = std::function; + + void ClearDescriptors(); + bool RegisterDescriptor(MetaCoreRuntimeLifecycleDescriptor descriptor); + std::size_t RegisterDescriptors(const std::vector& descriptors); + [[nodiscard]] const std::vector& GetDescriptors() const; + + void EnterScene(MetaCoreScene& scene, ErrorSink errorSink = {}); + void TickScene(MetaCoreScene& scene, float deltaSeconds, ErrorSink errorSink = {}); + void NotifyGameObjectsWillBeDestroyed( + MetaCoreScene& scene, + const std::vector& objectIds, + ErrorSink errorSink = {} + ); + void NotifyComponentWillBeRemoved( + MetaCoreScene& scene, + MetaCoreId objectId, + std::string_view componentTypeId, + ErrorSink errorSink = {} + ); + void ExitScene(MetaCoreScene& scene, ErrorSink errorSink = {}); + + [[nodiscard]] bool IsRunning() const; + [[nodiscard]] float GetElapsedTimeSeconds() const; + +private: + [[nodiscard]] std::string BuildLifecycleKey(MetaCoreId objectId, std::string_view componentTypeId) const; + void InvokeStartForNewComponents(MetaCoreScene& scene, const ErrorSink& errorSink); + bool InvokeDestroyForStartedComponent( + MetaCoreScene& scene, + const MetaCoreRuntimeLifecycleDescriptor& descriptor, + MetaCoreId objectId, + const ErrorSink& errorSink + ); + void ReportException( + const ErrorSink& errorSink, + std::string_view phase, + const MetaCoreRuntimeLifecycleDescriptor& descriptor, + MetaCoreId objectId, + const std::exception& exception + ) const; + void ReportUnknownException( + const ErrorSink& errorSink, + std::string_view phase, + const MetaCoreRuntimeLifecycleDescriptor& descriptor, + MetaCoreId objectId + ) const; + + std::vector Descriptors_{}; + std::unordered_set StartedComponents_{}; + bool Running_ = false; + float ElapsedTimeSeconds_ = 0.0F; +}; + +class MetaCoreRuntimeLifecycleRegistry { +public: + void Clear(); + bool RegisterDescriptor(MetaCoreRuntimeLifecycleDescriptor descriptor); + [[nodiscard]] const std::vector& GetDescriptors() const; + +private: + std::vector Descriptors_{}; +}; + +MetaCoreRuntimeLifecycleRegistry& MetaCoreGetRuntimeLifecycleRegistry(); + +} // namespace MetaCore diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index dfe8c99..c5eaea1 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -19,6 +19,7 @@ #include "MetaCoreRuntimeData/MetaCoreRuntimeDataSource.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" #include "MetaCoreScene/MetaCoreScenePackage.h" +#include "MetaCoreScene/MetaCoreRuntimeLifecycle.h" #include "MetaCoreScene/MetaCoreScene.h" #include "MetaCoreScene/MetaCoreSceneSerializer.h" #include "MetaCoreScene/MetaCoreUiRmlCompiler.h" @@ -34,6 +35,8 @@ #include #include #include +#include +#include #include #include @@ -480,6 +483,121 @@ void MetaCoreTestAssetDatabaseCreateAndOpenProject() { std::filesystem::remove_all(tempProjectRoot); } +void MetaCoreTestAssetDatabaseCreatesFirstClassProjectAssets() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreP2CreateProjectAssets"; + std::filesystem::remove_all(tempProjectRoot); + + _putenv_s("METACORE_PROJECT_PATH", ""); + + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + const auto assetDatabase = moduleRegistry.ResolveService(); + const auto reflectionRegistry = moduleRegistry.ResolveService(); + MetaCoreExpect(assetDatabase != nullptr, "AssetDatabaseService should be available"); + MetaCoreExpect(reflectionRegistry != nullptr, "ReflectionRegistry should be available"); + MetaCoreExpect(assetDatabase->CreateProject(tempProjectRoot, "P2CreateAssets"), "Project creation should succeed"); + + const auto sceneRecord = assetDatabase->CreateSceneAsset(std::filesystem::path("Scenes") / "P2Scene.mcscene", true); + MetaCoreExpect(sceneRecord.has_value(), "CreateSceneAsset should create a scene record"); + MetaCoreExpect(sceneRecord->Type == "scene", "Created scene should be registered as scene"); + MetaCoreExpect(sceneRecord->RelativePath == std::filesystem::path("Scenes") / "P2Scene.mcscene.json", "Scene extension should normalize to .mcscene.json"); + MetaCoreExpect(assetDatabase->GetProjectDescriptor().StartupScenePath == sceneRecord->RelativePath, "Created startup scene should update project descriptor"); + MetaCoreExpect(std::filesystem::exists(tempProjectRoot / sceneRecord->RelativePath), "Created scene file should exist"); + MetaCoreExpect(std::filesystem::exists(tempProjectRoot / (sceneRecord->RelativePath.string() + ".mcmeta")), "Created scene should have mcmeta"); + + const auto prefabRecord = assetDatabase->CreatePrefabAsset(std::filesystem::path("Assets") / "Prefabs" / "P2Prefab.mcprefab"); + MetaCoreExpect(prefabRecord.has_value(), "CreatePrefabAsset should create a prefab record"); + MetaCoreExpect(prefabRecord->Type == "prefab", "Created prefab should be registered as prefab"); + MetaCoreExpect(prefabRecord->RelativePath == std::filesystem::path("Assets") / "Prefabs" / "P2Prefab.mcprefab.json", "Prefab extension should normalize to .mcprefab.json"); + const auto prefabDocument = MetaCore::MetaCoreSceneSerializer::LoadPrefabFromJson( + tempProjectRoot / prefabRecord->RelativePath, + reflectionRegistry->GetTypeRegistry() + ); + MetaCoreExpect(prefabDocument.has_value(), "Created prefab should be loadable"); + MetaCoreExpect(prefabDocument->GameObjects.size() == 1, "Created prefab should contain a root object"); + + const auto materialRecord = assetDatabase->CreateMaterialAsset(std::filesystem::path("Assets") / "Materials" / "P2Material"); + MetaCoreExpect(materialRecord.has_value(), "CreateMaterialAsset should create a material record"); + MetaCoreExpect(materialRecord->Type == "material", "Created material should be registered as material"); + const auto materialDocument = MetaCore::MetaCoreSceneSerializer::LoadMaterialFromJson( + tempProjectRoot / materialRecord->RelativePath, + reflectionRegistry->GetTypeRegistry() + ); + MetaCoreExpect(materialDocument.has_value(), "Created material should be loadable"); + MetaCoreExpect(materialDocument->AssetGuid == materialRecord->Guid, "Material document guid should match AssetDatabase guid"); + + const auto uiRecord = assetDatabase->CreateUiDocumentAsset(std::filesystem::path("Ui") / "P2Hud.mcui"); + MetaCoreExpect(uiRecord.has_value(), "CreateUiDocumentAsset should create a UI record"); + MetaCoreExpect(uiRecord->Type == "ui_document", "Created UI document should be registered as ui_document"); + MetaCoreExpect(uiRecord->RelativePath == std::filesystem::path("Ui") / "P2Hud.mcui.json", "UI extension should normalize to .mcui.json"); + const auto uiDocument = MetaCore::MetaCoreSceneSerializer::LoadUiFromJson( + tempProjectRoot / uiRecord->RelativePath, + reflectionRegistry->GetTypeRegistry() + ); + MetaCoreExpect(uiDocument.has_value(), "Created UI document should be loadable"); + MetaCoreExpect(uiDocument->RootNodeIds.size() == 1 && uiDocument->Nodes.size() == 1, "Created UI document should contain one root panel"); + + MetaCoreExpect(assetDatabase->CreateFolder(std::filesystem::path("Assets") / "Moved"), "CreateFolder should create a project folder"); + const MetaCore::MetaCoreAssetGuid materialGuid = materialRecord->Guid; + MetaCoreExpect(assetDatabase->MovePath(materialRecord->RelativePath, std::filesystem::path("Assets") / "Moved"), "MovePath should move created material"); + const std::filesystem::path movedMaterialPath = std::filesystem::path("Assets") / "Moved" / materialRecord->RelativePath.filename(); + const auto movedMaterialRecord = assetDatabase->FindAssetByRelativePath(movedMaterialPath); + MetaCoreExpect(movedMaterialRecord.has_value(), "Moved material should be registered at new path"); + MetaCoreExpect(movedMaterialRecord->Guid == materialGuid, "Moved material should keep GUID"); + MetaCoreExpect(movedMaterialRecord->PackagePath == movedMaterialPath, "Moved JSON material package path should follow source path"); + + const MetaCore::MetaCoreAssetGuid uiGuid = uiRecord->Guid; + MetaCoreExpect(assetDatabase->RenamePath(uiRecord->RelativePath, "P2HudRenamed.mcui.json"), "RenamePath should rename created UI document"); + const std::filesystem::path renamedUiPath = std::filesystem::path("Ui") / "P2HudRenamed.mcui.json"; + const auto renamedUiRecord = assetDatabase->FindAssetByRelativePath(renamedUiPath); + MetaCoreExpect(renamedUiRecord.has_value(), "Renamed UI should be registered at new path"); + MetaCoreExpect(renamedUiRecord->Guid == uiGuid, "Renamed UI should keep GUID"); + MetaCoreExpect(renamedUiRecord->PackagePath == renamedUiPath, "Renamed JSON UI package path should follow source path"); + + MetaCoreExpect(assetDatabase->CreateFolder(std::filesystem::path("Assets") / "FolderMoveSource"), "CreateFolder should create folder move source"); + const auto folderMaterialRecord = + assetDatabase->CreateMaterialAsset(std::filesystem::path("Assets") / "FolderMoveSource" / "FolderMaterial"); + MetaCoreExpect(folderMaterialRecord.has_value(), "CreateMaterialAsset should create material inside folder"); + const MetaCore::MetaCoreAssetGuid folderMaterialGuid = folderMaterialRecord->Guid; + + MetaCoreExpect(assetDatabase->CreateFolder(std::filesystem::path("Assets") / "FolderMoveTarget"), "CreateFolder should create folder move target"); + MetaCoreExpect( + assetDatabase->MovePath(std::filesystem::path("Assets") / "FolderMoveSource", std::filesystem::path("Assets") / "FolderMoveTarget"), + "MovePath should move a folder containing assets" + ); + const std::filesystem::path movedFolderMaterialPath = + std::filesystem::path("Assets") / "FolderMoveTarget" / "FolderMoveSource" / "FolderMaterial.mcmaterial.json"; + const auto movedFolderMaterialRecord = assetDatabase->FindAssetByRelativePath(movedFolderMaterialPath); + MetaCoreExpect(movedFolderMaterialRecord.has_value(), "Asset inside moved folder should be registered at new path"); + MetaCoreExpect(movedFolderMaterialRecord->Guid == folderMaterialGuid, "Asset inside moved folder should keep GUID"); + MetaCoreExpect( + movedFolderMaterialRecord->PackagePath == movedFolderMaterialPath, + "Asset inside moved folder should rewrite package path metadata" + ); + + MetaCoreExpect( + assetDatabase->RenamePath(std::filesystem::path("Assets") / "FolderMoveTarget" / "FolderMoveSource", "FolderRenamed"), + "RenamePath should rename a folder containing assets" + ); + const std::filesystem::path renamedFolderMaterialPath = + std::filesystem::path("Assets") / "FolderMoveTarget" / "FolderRenamed" / "FolderMaterial.mcmaterial.json"; + const auto renamedFolderMaterialRecord = assetDatabase->FindAssetByRelativePath(renamedFolderMaterialPath); + MetaCoreExpect(renamedFolderMaterialRecord.has_value(), "Asset inside renamed folder should be registered at new path"); + MetaCoreExpect(renamedFolderMaterialRecord->Guid == folderMaterialGuid, "Asset inside renamed folder should keep GUID"); + MetaCoreExpect( + renamedFolderMaterialRecord->PackagePath == renamedFolderMaterialPath, + "Asset inside renamed folder should rewrite package path metadata" + ); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + void MetaCoreTestAssetDatabaseRenamePathUpdatesProjectDescriptor() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreRenamePathProject"; @@ -980,14 +1098,124 @@ void MetaCoreTestPlayModeLifecycleAndSceneIsolation() { const auto componentRegistry = moduleRegistry.ResolveService(); const auto reflectionRegistry = moduleRegistry.ResolveService(); const auto playModeService = moduleRegistry.ResolveService(); + const auto clipboardService = moduleRegistry.ResolveService(); MetaCoreExpect(componentRegistry != nullptr, "Component registry should be available for play mode lifecycle test"); MetaCoreExpect(reflectionRegistry != nullptr, "Reflection registry should be available for play mode lifecycle test"); MetaCoreExpect(playModeService != nullptr, "Play mode service should be available"); + MetaCoreExpect(clipboardService != nullptr, "Clipboard service should be available for runtime deletion test"); MetaCore::MetaCoreGameObject object = scene.CreateGameObject("LifecycleObject"); object.GetComponent().Position = glm::vec3(1.0F, 2.0F, 3.0F); object.AddComponent().SourceModelPath = "Assets/Models/Source.glb"; const MetaCore::MetaCoreId objectId = object.GetId(); + MetaCore::MetaCoreGameObject runtimeDeletedObject = scene.CreateGameObject("RuntimeDeletedObject"); + runtimeDeletedObject.AddComponent(MetaCore::MetaCoreCameraComponent{}); + const MetaCore::MetaCoreId runtimeDeletedObjectId = runtimeDeletedObject.GetId(); + MetaCore::MetaCoreGameObject runtimeComponentRemovedObject = scene.CreateGameObject("RuntimeComponentRemovedObject"); + runtimeComponentRemovedObject.AddComponent(MetaCore::MetaCoreCameraComponent{}); + const MetaCore::MetaCoreId runtimeComponentRemovedObjectId = runtimeComponentRemovedObject.GetId(); + + int runtimeStartCount = 0; + int runtimeUpdateCount = 0; + int runtimeDestroyCount = 0; + std::vector runtimeLifecycleErrors; + MetaCore::MetaCoreRuntimeLifecycleExecutor runtimeLifecycleExecutor; + MetaCore::MetaCoreRuntimeLifecycleDescriptor runtimeLifecycleDescriptor{}; + runtimeLifecycleDescriptor.TypeId = "RuntimeModelRootTag"; + runtimeLifecycleDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) { + return gameObject.HasComponent(); + }; + runtimeLifecycleDescriptor.OnStart = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&) { + ++runtimeStartCount; + }; + runtimeLifecycleDescriptor.OnUpdate = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&, float) { + ++runtimeUpdateCount; + throw std::runtime_error("runtime executor fault"); + }; + runtimeLifecycleDescriptor.OnDestroy = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&) { + ++runtimeDestroyCount; + }; + MetaCoreExpect( + runtimeLifecycleExecutor.RegisterDescriptor(std::move(runtimeLifecycleDescriptor)), + "Runtime lifecycle executor should register a descriptor" + ); + const auto runtimeErrorSink = [&](const MetaCore::MetaCoreRuntimeLifecycleError& error) { + runtimeLifecycleErrors.push_back(error); + }; + runtimeLifecycleExecutor.EnterScene(scene, runtimeErrorSink); + runtimeLifecycleExecutor.TickScene(scene, 0.25F, runtimeErrorSink); + runtimeLifecycleExecutor.ExitScene(scene, runtimeErrorSink); + MetaCoreExpect(runtimeStartCount == 1, "Runtime lifecycle executor should run OnStart"); + MetaCoreExpect(runtimeUpdateCount == 1, "Runtime lifecycle executor should run OnUpdate"); + MetaCoreExpect(runtimeDestroyCount == 1, "Runtime lifecycle executor should run OnDestroy"); + MetaCoreExpect(!runtimeLifecycleExecutor.IsRunning(), "Runtime lifecycle executor should stop after ExitScene"); + MetaCoreExpect(!runtimeLifecycleErrors.empty(), "Runtime lifecycle executor should report callback exceptions"); + MetaCoreExpect( + runtimeLifecycleErrors.front().Message.find("runtime executor fault") != std::string::npos, + "Runtime lifecycle executor should preserve exception messages" + ); + + MetaCore::MetaCoreGameObject transientObject = scene.CreateGameObject("TransientLifecycleObject"); + transientObject.AddComponent().SourceModelPath = "Assets/Models/Transient.glb"; + const MetaCore::MetaCoreId transientObjectId = transientObject.GetId(); + int transientDestroyCount = 0; + MetaCore::MetaCoreRuntimeLifecycleExecutor transientLifecycleExecutor; + MetaCore::MetaCoreRuntimeLifecycleDescriptor transientLifecycleDescriptor{}; + transientLifecycleDescriptor.TypeId = "TransientRuntimeModelRootTag"; + transientLifecycleDescriptor.HasComponent = [transientObjectId](const MetaCore::MetaCoreGameObject& gameObject) { + return gameObject.GetId() == transientObjectId && + gameObject.HasComponent(); + }; + transientLifecycleDescriptor.OnDestroy = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&) { + ++transientDestroyCount; + }; + MetaCoreExpect( + transientLifecycleExecutor.RegisterDescriptor(std::move(transientLifecycleDescriptor)), + "Runtime lifecycle executor should register transient descriptor" + ); + transientLifecycleExecutor.EnterScene(scene, runtimeErrorSink); + transientLifecycleExecutor.NotifyGameObjectsWillBeDestroyed(scene, {transientObjectId}, runtimeErrorSink); + MetaCoreExpect(transientDestroyCount == 1, "Runtime lifecycle executor should destroy before runtime object delete"); + const std::vector transientDeletedIds = scene.DeleteGameObjects({transientObjectId}); + MetaCoreExpect(!transientDeletedIds.empty(), "Transient runtime object should be deleted after destroy notification"); + transientLifecycleExecutor.ExitScene(scene, runtimeErrorSink); + MetaCoreExpect( + transientDestroyCount == 1, + "Runtime lifecycle executor should not destroy a notified object again on ExitScene" + ); + + int registryStartCount = 0; + auto& runtimeLifecycleRegistry = MetaCore::MetaCoreGetRuntimeLifecycleRegistry(); + runtimeLifecycleRegistry.Clear(); + MetaCore::MetaCoreRuntimeLifecycleDescriptor registryLifecycleDescriptor{}; + registryLifecycleDescriptor.TypeId = "RegisteredRuntimeModelRootTag"; + registryLifecycleDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) { + return gameObject.HasComponent(); + }; + registryLifecycleDescriptor.OnStart = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&) { + ++registryStartCount; + }; + MetaCoreExpect( + runtimeLifecycleRegistry.RegisterDescriptor(std::move(registryLifecycleDescriptor)), + "Runtime lifecycle registry should accept a valid static descriptor" + ); + MetaCoreExpect( + !runtimeLifecycleRegistry.RegisterDescriptor(MetaCore::MetaCoreRuntimeLifecycleDescriptor{}), + "Runtime lifecycle registry should reject invalid descriptors" + ); + MetaCore::MetaCoreRuntimeLifecycleExecutor registeredRuntimeLifecycleExecutor; + MetaCoreExpect( + registeredRuntimeLifecycleExecutor.RegisterDescriptors(runtimeLifecycleRegistry.GetDescriptors()) == 1, + "Runtime lifecycle executor should import descriptors from the runtime registry" + ); + MetaCoreExpect( + registeredRuntimeLifecycleExecutor.RegisterDescriptors(runtimeLifecycleRegistry.GetDescriptors()) == 0, + "Runtime lifecycle executor should reject duplicate registry descriptors" + ); + registeredRuntimeLifecycleExecutor.EnterScene(scene, runtimeErrorSink); + registeredRuntimeLifecycleExecutor.ExitScene(scene, runtimeErrorSink); + MetaCoreExpect(registryStartCount == 1, "Registered runtime lifecycle descriptor should execute OnStart"); + runtimeLifecycleRegistry.Clear(); MetaCore::MetaCoreEditorContext editorContext( window, @@ -1004,6 +1232,9 @@ void MetaCoreTestPlayModeLifecycleAndSceneIsolation() { int destroyCount = 0; int drawGizmosCount = 0; int drawSelectedGizmosCount = 0; + int faultyDrawGizmosCount = 0; + int runtimeDeleteStartCount = 0; + int runtimeDeleteDestroyCount = 0; MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor lifecycleDescriptor{}; lifecycleDescriptor.TypeId = "LifecycleModelRootTag"; @@ -1044,11 +1275,26 @@ void MetaCoreTestPlayModeLifecycleAndSceneIsolation() { lifecycleDescriptor.Lifecycle.OnDestroy = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) { ++destroyCount; }; - lifecycleDescriptor.Lifecycle.OnDrawGizmos = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) { + lifecycleDescriptor.Lifecycle.OnDrawGizmos = [&](MetaCore::MetaCoreEditorContext& context, MetaCore::MetaCoreGameObject&) { ++drawGizmosCount; + context.DrawGizmoLine( + glm::vec3(0.0F), + glm::vec3(1.0F, 0.0F, 0.0F), + glm::vec4(1.0F, 0.0F, 0.0F, 1.0F) + ); }; - lifecycleDescriptor.Lifecycle.OnDrawGizmosSelected = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) { + lifecycleDescriptor.Lifecycle.OnDrawGizmosSelected = [&](MetaCore::MetaCoreEditorContext& context, MetaCore::MetaCoreGameObject&) { ++drawSelectedGizmosCount; + context.DrawGizmoWireBox( + glm::vec3(0.0F), + glm::vec3(2.0F), + glm::vec4(0.0F, 1.0F, 0.0F, 1.0F) + ); + context.DrawGizmoIcon( + glm::vec3(0.0F, 1.0F, 0.0F), + "Lifecycle", + glm::vec4(0.0F, 0.5F, 1.0F, 1.0F) + ); }; MetaCoreExpect( @@ -1056,44 +1302,220 @@ void MetaCoreTestPlayModeLifecycleAndSceneIsolation() { "Play mode lifecycle component descriptor should register" ); + MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor faultyLifecycleDescriptor{}; + faultyLifecycleDescriptor.TypeId = "FaultyLifecycleModelRootTag"; + faultyLifecycleDescriptor.DisplayName = "Faulty Lifecycle Model Root Tag"; + faultyLifecycleDescriptor.Category = "Scripts"; + faultyLifecycleDescriptor.ReflectedType = reflectionRegistry->GetTypeRegistry().FindStruct(); + faultyLifecycleDescriptor.MutableComponent = [](MetaCore::MetaCoreGameObject& gameObject) -> void* { + return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; + }; + faultyLifecycleDescriptor.ConstComponent = [](const MetaCore::MetaCoreGameObject& gameObject) -> const void* { + return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; + }; + faultyLifecycleDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) { + return gameObject.HasComponent(); + }; + faultyLifecycleDescriptor.Lifecycle.OnUpdate = []( + MetaCore::MetaCoreEditorContext&, + MetaCore::MetaCoreGameObject&, + float + ) { + throw std::runtime_error("intentional lifecycle fault"); + }; + faultyLifecycleDescriptor.Lifecycle.OnDrawGizmos = [&]( + MetaCore::MetaCoreEditorContext&, + MetaCore::MetaCoreGameObject& + ) { + ++faultyDrawGizmosCount; + throw std::runtime_error("intentional gizmo fault"); + }; + MetaCoreExpect( + componentRegistry->RegisterComponentDescriptor(std::move(faultyLifecycleDescriptor)), + "Faulty play mode lifecycle descriptor should register" + ); + + MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor runtimeDeleteDescriptor{}; + runtimeDeleteDescriptor.TypeId = "LifecycleCamera"; + runtimeDeleteDescriptor.DisplayName = "Lifecycle Camera"; + runtimeDeleteDescriptor.Category = "Scripts"; + runtimeDeleteDescriptor.ReflectedType = reflectionRegistry->GetTypeRegistry().FindStruct(); + runtimeDeleteDescriptor.MutableComponent = [](MetaCore::MetaCoreGameObject& gameObject) -> void* { + return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; + }; + runtimeDeleteDescriptor.ConstComponent = [](const MetaCore::MetaCoreGameObject& gameObject) -> const void* { + return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; + }; + runtimeDeleteDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) { + return gameObject.HasComponent(); + }; + runtimeDeleteDescriptor.Lifecycle.OnStart = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) { + ++runtimeDeleteStartCount; + }; + runtimeDeleteDescriptor.Lifecycle.OnDestroy = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) { + ++runtimeDeleteDestroyCount; + }; + MetaCoreExpect( + componentRegistry->RegisterComponentDescriptor(std::move(runtimeDeleteDescriptor)), + "Runtime delete lifecycle descriptor should register" + ); + MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Edit, "Initial play mode state should be Edit"); + MetaCoreExpect(!playModeService->IsRuntimeSceneActive(), "Runtime scene should be inactive in edit mode"); + MetaCoreExpect(!editorContext.IsRuntimeSceneActive(), "Editor context should start in edit scene mode"); componentRegistry->DrawComponentGizmos(editorContext); MetaCoreExpect(drawGizmosCount == 1, "OnDrawGizmos should run for registered static C++ components"); MetaCoreExpect(drawSelectedGizmosCount == 1, "OnDrawGizmosSelected should run for selected registered static C++ components"); + MetaCoreExpect( + faultyDrawGizmosCount == 1, + "Component gizmo failures should be isolated without stopping other gizmo callbacks" + ); + MetaCoreExpect( + editorContext.GetComponentGizmoLines().size() == 13, + "Component gizmos should queue lines and wire boxes for the Scene View" + ); + MetaCoreExpect( + editorContext.GetComponentGizmoIcons().size() == 1 && + editorContext.GetComponentGizmoIcons().front().Label == "Lifecycle", + "Component gizmos should queue labeled Scene View icons" + ); + editorContext.ClearComponentGizmoDrawList(); + MetaCoreExpect( + editorContext.GetComponentGizmoLines().empty() && editorContext.GetComponentGizmoIcons().empty(), + "Component gizmo draw list should clear between Scene View frames" + ); + + MetaCoreExpect( + editorContext.ExecuteSnapshotCommand("Pre-play edit command", [&]() { + scene.FindGameObject(objectId).GetComponent().Scale = glm::vec3(2.0F); + return true; + }), + "Edit mode commands should be recorded before entering play mode" + ); + const std::size_t undoCountBeforePlay = editorContext.GetCommandService().GetUndoCount(); + MetaCoreExpect(undoCountBeforePlay > 0, "Pre-play edit command should be present in undo stack"); MetaCoreExpect(playModeService->EnterPlayMode(editorContext), "EnterPlayMode should succeed"); MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Playing, "Play mode state should be Playing"); + MetaCoreExpect(playModeService->IsRuntimeSceneActive(), "Runtime scene should be active in play mode"); + MetaCoreExpect(editorContext.IsRuntimeSceneActive(), "Editor context should expose runtime scene mode while playing"); MetaCoreExpect(startCount == 1, "OnStart should run once on enter play mode"); + MetaCoreExpect(runtimeDeleteStartCount == 2, "Runtime delete lifecycle objects should start on enter play mode"); MetaCoreExpectVec3Near( scene.FindGameObject(objectId).GetComponent().Position, glm::vec3(10.0F, 2.0F, 3.0F), "OnStart should be able to mutate runtime scene state" ); + MetaCoreExpect( + editorContext.ExecuteSnapshotCommand("Runtime-only edit command", [&]() { + scene.FindGameObject(objectId).GetComponent().Position.z = 99.0F; + return true; + }), + "Runtime scene commands should still mutate the runtime scene" + ); + MetaCoreExpect( + editorContext.GetCommandService().GetUndoCount() == undoCountBeforePlay, + "Runtime scene commands should not be pushed into the edit undo stack" + ); + MetaCoreExpect(!editorContext.UndoCommand(), "Undo should be disabled while runtime scene is active"); + MetaCoreExpectVec3Near( + scene.FindGameObject(objectId).GetComponent().Position, + glm::vec3(10.0F, 2.0F, 99.0F), + "Runtime-only edit should affect only the runtime scene" + ); playModeService->TickPlayMode(editorContext, 0.5F); MetaCoreExpect(startCount == 1, "OnStart should not repeat during tick"); MetaCoreExpect(updateCount == 1, "OnUpdate should run during play tick"); + const auto hasLifecycleErrorLog = [&]() { + return std::any_of( + logService.GetEntries().begin(), + logService.GetEntries().end(), + [](const MetaCore::MetaCoreLogEntry& entry) { + return entry.Level == MetaCore::MetaCoreLogLevel::Error && + entry.Category == "PlayMode" && + entry.Message.find("intentional lifecycle fault") != std::string::npos; + } + ); + }; + MetaCoreExpect(hasLifecycleErrorLog(), "Lifecycle exceptions should be reported to the PlayMode console log"); MetaCoreExpectVec3Near( scene.FindGameObject(objectId).GetComponent().Position, - glm::vec3(10.0F, 2.5F, 3.0F), + glm::vec3(10.0F, 2.5F, 99.0F), "OnUpdate should mutate runtime scene state" ); + MetaCoreExpect(std::abs(playModeService->GetElapsedPlayTimeSeconds() - 0.5F) <= 0.0001F, "Play time should advance during play tick"); + + MetaCore::MetaCoreGameObject runtimeComponentRemovedGameObject = + scene.FindGameObject(runtimeComponentRemovedObjectId); + MetaCoreExpect( + runtimeComponentRemovedGameObject && + runtimeComponentRemovedGameObject.HasComponent(), + "Runtime component removal test object should have a Camera component" + ); + playModeService->NotifyComponentWillBeRemoved( + editorContext, + runtimeComponentRemovedObjectId, + "LifecycleCamera" + ); + runtimeComponentRemovedGameObject.RemoveComponent(); + MetaCoreExpect( + runtimeDeleteDestroyCount == 1, + "Runtime component removal should notify lifecycle OnDestroy before removing the component" + ); + + editorContext.SelectOnly(runtimeDeletedObjectId); + MetaCoreExpect(clipboardService->DeleteSelection(editorContext), "Runtime DeleteSelection should delete the selected runtime object"); + MetaCoreExpect(!scene.FindGameObject(runtimeDeletedObjectId), "Deleted runtime object should be removed from the runtime scene"); + MetaCoreExpect( + runtimeDeleteDestroyCount == 2, + "Runtime DeleteSelection should notify lifecycle OnDestroy before deleting the object" + ); + editorContext.SelectOnly(objectId); MetaCoreExpect(playModeService->PausePlayMode(editorContext), "PausePlayMode should succeed"); + MetaCoreExpect(playModeService->IsRuntimeSceneActive(), "Runtime scene should remain active while paused"); playModeService->TickPlayMode(editorContext, 0.25F); MetaCoreExpect(updateCount == 1, "OnUpdate should not run while paused"); + MetaCoreExpect(std::abs(playModeService->GetElapsedPlayTimeSeconds() - 0.5F) <= 0.0001F, "Play time should not advance during paused tick"); + MetaCoreExpect(playModeService->StepPlayMode(editorContext, 0.125F), "StepPlayMode should succeed while paused"); + MetaCoreExpect(updateCount == 2, "OnUpdate should run for a paused step"); + MetaCoreExpect(std::abs(playModeService->GetElapsedPlayTimeSeconds() - 0.625F) <= 0.0001F, "Play time should advance by step delta"); MetaCoreExpect(playModeService->ResumePlayMode(editorContext), "ResumePlayMode should succeed"); playModeService->TickPlayMode(editorContext, 0.25F); - MetaCoreExpect(updateCount == 2, "OnUpdate should resume after pause"); + MetaCoreExpect(updateCount == 3, "OnUpdate should resume after pause"); MetaCoreExpect(playModeService->ExitPlayMode(editorContext), "ExitPlayMode should succeed"); MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Edit, "Play mode state should return to Edit"); + MetaCoreExpect(!playModeService->IsRuntimeSceneActive(), "Runtime scene should be inactive after exiting play mode"); + MetaCoreExpect(!editorContext.IsRuntimeSceneActive(), "Editor context should return to edit scene mode after exiting play mode"); MetaCoreExpect(destroyCount == 1, "OnDestroy should run on exit play mode"); + MetaCoreExpect( + runtimeDeleteDestroyCount == 2, + "Runtime removed components and deleted objects should not receive duplicate OnDestroy on exit play mode" + ); + MetaCoreExpect( + scene.FindGameObject(runtimeDeletedObjectId), + "ExitPlayMode should restore runtime-deleted objects from the edit scene snapshot" + ); + MetaCoreExpect( + scene.FindGameObject(runtimeComponentRemovedObjectId).HasComponent(), + "ExitPlayMode should restore runtime-removed components from the edit scene snapshot" + ); MetaCoreExpectVec3Near( scene.FindGameObject(objectId).GetComponent().Position, glm::vec3(1.0F, 2.0F, 3.0F), "ExitPlayMode should restore edit scene snapshot" ); + MetaCoreExpectVec3Near( + scene.FindGameObject(objectId).GetComponent().Scale, + glm::vec3(2.0F), + "ExitPlayMode should keep pre-play edit scene changes" + ); + MetaCoreExpect( + editorContext.GetCommandService().GetUndoCount() == undoCountBeforePlay, + "ExitPlayMode should preserve the pre-play edit undo stack without runtime commands" + ); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); @@ -1130,9 +1552,9 @@ void MetaCoreTestSceneGizmoMultiSelectDeltaAndUndo() { logService, moduleRegistry ); - editorContext.SetSelection({parentId, childId, siblingId}, parentId); MetaCore::MetaCoreSceneInteractionService sceneInteractionService; + editorContext.SetSelection({parentId, childId, siblingId}, parentId); const glm::mat4 originalParentWorld = MetaCore::MetaCoreBuildTransformMatrix(scene.FindGameObject(parentId).GetComponent()); const glm::mat4 movedParentWorld = glm::translate(glm::mat4(1.0F), glm::vec3(3.0F, 0.0F, 0.0F)); @@ -1186,10 +1608,137 @@ void MetaCoreTestSceneGizmoMultiSelectDeltaAndUndo() { "Redo gizmo drag should reapply other selected root transform" ); + MetaCoreExpect( + editorContext.GetGizmoPivotMode() == MetaCore::MetaCoreGizmoPivotMode::Pivot, + "Gizmo pivot mode should default to Pivot" + ); + editorContext.SetGizmoPivotMode(MetaCore::MetaCoreGizmoPivotMode::Center); + MetaCoreExpect( + editorContext.GetGizmoPivotMode() == MetaCore::MetaCoreGizmoPivotMode::Center, + "Gizmo pivot mode should switch to Center" + ); + + const glm::mat4 originalCenterWorld = glm::translate(glm::mat4(1.0F), glm::vec3(5.0F, 0.0F, 0.0F)); + const glm::mat4 rotatedCenterWorld = + originalCenterWorld * glm::rotate(glm::mat4(1.0F), glm::radians(90.0F), glm::vec3(0.0F, 0.0F, 1.0F)); + sceneInteractionService.HandleGizmoBeginUse(editorContext, true); + MetaCoreExpect( + sceneInteractionService.ApplyWorldTransformDeltaToSelection( + editorContext, + parentId, + originalCenterWorld, + rotatedCenterWorld, + MetaCore::MetaCoreGizmoPivotMode::Center + ), + "Center gizmo delta should apply around the shared selection center" + ); + sceneInteractionService.HandleGizmoEndUse(editorContext, true); + sceneInteractionService.HandleGizmoEndUse(editorContext, false); + + MetaCoreExpectVec3Near( + scene.FindGameObject(parentId).GetComponent().Position, + glm::vec3(5.0F, -2.0F, 0.0F), + "Center rotation should rotate active root around shared center" + ); + MetaCoreExpectVec3Near( + scene.FindGameObject(siblingId).GetComponent().Position, + glm::vec3(5.0F, 2.0F, 0.0F), + "Center rotation should rotate other selected root around shared center" + ); + MetaCoreExpectVec3Near( + scene.FindGameObject(childId).GetComponent().Position, + glm::vec3(0.0F, 2.0F, 0.0F), + "Center rotation should not double-apply selected child local transform" + ); + + MetaCoreExpect(editorContext.UndoCommand(), "Undo center gizmo rotation should succeed"); + MetaCoreExpectVec3Near( + scene.FindGameObject(parentId).GetComponent().Position, + glm::vec3(3.0F, 0.0F, 0.0F), + "Undo center rotation should restore active root" + ); + MetaCoreExpectVec3Near( + scene.FindGameObject(siblingId).GetComponent().Position, + glm::vec3(7.0F, 0.0F, 0.0F), + "Undo center rotation should restore other root" + ); + coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); } +void MetaCoreTestSceneViewportHoverPicking() { + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + MetaCore::MetaCoreGameObject hoveredObject = scene.CreateGameObject("HoverTarget"); + hoveredObject.AddComponent(MetaCore::MetaCoreMeshRendererComponent{}); + const MetaCore::MetaCoreId hoveredObjectId = hoveredObject.GetId(); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + MetaCore::MetaCoreSceneViewportState& viewportState = editorContext.GetSceneViewportState(); + viewportState.Left = 0.0F; + viewportState.Top = 0.0F; + viewportState.Width = 128.0F; + viewportState.Height = 128.0F; + viewportState.Hovered = true; + viewportState.Focused = true; + editorContext.GetInput().SetCursorPosition(glm::vec2(64.0F, 64.0F)); + + MetaCore::MetaCoreSceneView sceneView; + sceneView.CameraPosition = glm::vec3(0.0F, 0.0F, 5.0F); + sceneView.CameraTarget = glm::vec3(0.0F, 0.0F, 0.0F); + sceneView.CameraUp = glm::vec3(0.0F, 1.0F, 0.0F); + sceneView.VerticalFieldOfViewDegrees = 60.0F; + + MetaCore::MetaCoreSceneInteractionService sceneInteractionService; + sceneInteractionService.UpdateViewportHover(editorContext, sceneView, true); + MetaCoreExpect(editorContext.GetHoveredObjectId() == hoveredObjectId, "Scene viewport hover should track picked object"); + MetaCoreExpect(editorContext.IsObjectHovered(hoveredObjectId), "Hovered object predicate should match picked object"); + MetaCoreExpect( + editorContext.GetShowViewportSelectionOverlay(), + "Viewport selection overlay should be enabled by default" + ); + + const MetaCore::MetaCoreEditorStateSnapshot snapshot = editorContext.CaptureStateSnapshot(); + editorContext.ClearHoveredObject(); + editorContext.SetShowViewportSelectionOverlay(false); + editorContext.RestoreStateSnapshot(snapshot); + MetaCoreExpect( + editorContext.GetHoveredObjectId() == 0, + "Editor state snapshot restore should not restore transient hover state" + ); + MetaCoreExpect( + !editorContext.GetShowViewportSelectionOverlay(), + "Editor state snapshot restore should not restore transient viewport overlay state" + ); + editorContext.SetShowViewportSelectionOverlay(true); + MetaCoreExpect( + editorContext.GetShowViewportSelectionOverlay(), + "Viewport selection overlay flag should be mutable" + ); + + editorContext.SetHoveredObjectId(hoveredObjectId); + sceneInteractionService.UpdateViewportHover(editorContext, sceneView, false); + MetaCoreExpect(editorContext.GetHoveredObjectId() == 0, "Scene viewport hover should clear when picking is blocked"); + + editorContext.SetHoveredObjectId(hoveredObjectId); + viewportState.Hovered = false; + sceneInteractionService.UpdateViewportHover(editorContext, sceneView, true); + MetaCoreExpect(editorContext.GetHoveredObjectId() == 0, "Scene viewport hover should clear outside the viewport"); +} + void MetaCoreTestGizmoSnapSettings() { MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; @@ -1242,6 +1791,25 @@ void MetaCoreTestGizmoSnapSettings() { std::abs(editorContext.GetGizmoSnapSettings().StepForOperation(MetaCore::MetaCoreGizmoOperation::Scale) - 0.2F) < 0.0001F, "Custom scale snap step should be used" ); + + MetaCore::MetaCoreGizmoSnapSettings invalidSnap; + invalidSnap.Enabled = true; + invalidSnap.TranslationStep = -1.0F; + invalidSnap.RotationStepDegrees = 900.0F; + invalidSnap.ScaleStep = std::numeric_limits::quiet_NaN(); + editorContext.SetGizmoSnapSettings(invalidSnap); + MetaCoreExpect( + std::abs(editorContext.GetGizmoSnapSettings().TranslationStep - 0.001F) < 0.0001F, + "Negative translate snap step should clamp to a positive minimum" + ); + MetaCoreExpect( + std::abs(editorContext.GetGizmoSnapSettings().RotationStepDegrees - 360.0F) < 0.0001F, + "Oversized rotation snap step should clamp to 360 degrees" + ); + MetaCoreExpect( + std::abs(editorContext.GetGizmoSnapSettings().ScaleStep - 0.1F) < 0.0001F, + "Non-finite scale snap step should fall back to the default" + ); } void MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot() { @@ -2458,9 +3026,14 @@ void MetaCoreTestPrefabWorkflow() { moduleRegistry ); - MetaCore::MetaCoreGameObject root = scene.CreateGameObject("PrefabRoot"); + MetaCore::MetaCoreGameObject sceneParent = scene.CreateGameObject("SceneParent"); + const MetaCore::MetaCoreId sceneParentId = sceneParent.GetId(); + MetaCore::MetaCoreGameObject root = scene.CreateGameObject("PrefabRoot", sceneParentId); root.AddComponent(); + root.AddComponent(); root.GetComponent().Position = glm::vec3(1.0F, 2.0F, 3.0F); + root.GetComponent().Speed = 4.25F; + root.GetComponent().Target = "PrefabTarget"; const MetaCore::MetaCoreId rootId = root.GetId(); MetaCore::MetaCoreGameObject child = scene.CreateGameObject("PrefabChild", rootId); child.AddComponent(); @@ -2471,35 +3044,63 @@ void MetaCoreTestPrefabWorkflow() { const auto assetDatabaseService = moduleRegistry.ResolveService(); const auto packageService = moduleRegistry.ResolveService(); const auto reflectionRegistry = moduleRegistry.ResolveService(); + const auto componentRegistry = moduleRegistry.ResolveService(); MetaCoreExpect(prefabService != nullptr, "应解析到 PrefabService"); MetaCoreExpect(prefabService->SupportsPrefabWorkflows(), "PrefabService 应支持工作流"); MetaCoreExpect(assetDatabaseService != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(packageService != nullptr, "应解析到 PackageService"); MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry"); + MetaCoreExpect(componentRegistry != nullptr, "Prefab workflow should resolve ComponentRegistry"); + MetaCoreRegisterSmokeScriptComponentForTesting(*componentRegistry, *reflectionRegistry, "PrefabSmokeScript"); + + const std::filesystem::path prefabRequestPath = std::filesystem::path("Assets") / "Prefabs" / "PrefabRoot.mcprefab"; const std::filesystem::path prefabPath = std::filesystem::path("Assets") / "Prefabs" / "PrefabRoot.mcprefab.json"; - const auto createdPrefabPath = prefabService->CreatePrefabFromSelection(editorContext, prefabPath); + const auto createdPrefabPath = prefabService->CreatePrefabFromSelection(editorContext, prefabRequestPath); MetaCoreExpect(createdPrefabPath.has_value(), "应能从选中对象创建 prefab"); MetaCoreExpect(std::filesystem::exists(tempProjectRoot / prefabPath), "应写出 mcprefab.json 文件"); MetaCoreExpect(assetDatabaseService->Refresh(), "创建 prefab 后应能刷新资产数据库"); + MetaCoreExpect(*createdPrefabPath == prefabPath, "CreatePrefabFromSelection should normalize .mcprefab to .mcprefab.json"); + MetaCoreExpect(root.GetParentId() == sceneParentId, "Create Prefab should preserve the selected object's scene parent"); + MetaCoreExpect(root.HasComponent(), "Create Prefab should link the original root as a prefab instance"); + MetaCoreExpect(child.HasComponent(), "Create Prefab should link the original child as a prefab instance"); + const auto prefabRecord = assetDatabaseService->FindAssetByRelativePath(prefabPath); MetaCoreExpect(prefabRecord.has_value(), "应能找到 prefab 资产记录"); MetaCoreExpect(prefabRecord->Type == "prefab", "prefab 资产类型应正确"); + MetaCoreExpect(root.GetComponent().PrefabAssetGuid == prefabRecord->Guid, "Original root should store the prefab asset guid"); + MetaCoreExpect(root.GetComponent().PrefabObjectId == rootId, "Original root should map to its source prefab object id"); + MetaCoreExpect(root.GetComponent().PrefabInstanceRootId == rootId, "Original root should store itself as the prefab instance root"); + const auto instantiatedRootId = prefabService->InstantiatePrefab(editorContext, prefabRecord->Guid, std::nullopt); MetaCoreExpect(instantiatedRootId.has_value(), "应能实例化 prefab"); MetaCore::MetaCoreGameObject instantiatedRoot = scene.FindGameObject(*instantiatedRootId); MetaCoreExpect(instantiatedRoot, "实例化后应能找到根对象"); MetaCoreExpect(instantiatedRoot.HasComponent(), "实例根应带有 prefab 元数据"); + MetaCoreExpect(instantiatedRoot.HasComponent(), "Prefab instance should restore static C++ components"); + MetaCoreExpect(std::abs(instantiatedRoot.GetComponent().Speed - 4.25F) <= 0.0001F, "Prefab instance should restore static C++ component field values"); + MetaCoreExpect(instantiatedRoot.GetComponent().Target == "PrefabTarget", "Prefab instance should restore static C++ component string fields"); + instantiatedRoot.GetComponent().Position = glm::vec3(8.0F, 9.0F, 10.0F); + instantiatedRoot.GetComponent().Speed = 6.75F; + instantiatedRoot.GetComponent().Target = "AppliedPrefabTarget"; editorContext.SelectOnly(instantiatedRoot.GetId()); MetaCoreExpect(prefabService->ApplySelectedPrefabInstance(editorContext), "应能应用 prefab 实例"); const auto appliedPrefabDocument = MetaCore::MetaCoreSceneSerializer::LoadPrefabFromJson(tempProjectRoot / prefabPath, reflectionRegistry->GetTypeRegistry()); MetaCoreExpect(appliedPrefabDocument.has_value(), "Apply 后 prefab 应能被 JSON 解析"); MetaCoreExpect(!appliedPrefabDocument->GameObjects.empty(), "Apply 后 prefab 应保留对象数据"); + MetaCoreExpect( + !appliedPrefabDocument->GameObjects.front().SerializedComponents.empty(), + "Apply should write static C++ components into the prefab document" + ); + MetaCoreExpect( + appliedPrefabDocument->GameObjects.front().SerializedComponents.front().TypeId == "PrefabSmokeScript", + "Apply should preserve the static C++ component TypeId in the prefab document" + ); MetaCoreExpectVec3Near( appliedPrefabDocument->GameObjects.front().Transform.Position, glm::vec3(8.0F, 9.0F, 10.0F), @@ -2512,7 +3113,12 @@ void MetaCoreTestPrefabWorkflow() { MetaCoreExpect(secondInstanceRoot, "第二个实例应存在"); MetaCoreExpectVec3Near(secondInstanceRoot.GetComponent().Position, glm::vec3(8.0F, 9.0F, 10.0F), "Apply 后新实例应继承修改过的 transform"); + MetaCoreExpect(secondInstanceRoot.HasComponent(), "Apply should make new prefab instances restore static C++ components"); + MetaCoreExpect(std::abs(secondInstanceRoot.GetComponent().Speed - 6.75F) <= 0.0001F, "Apply should make new prefab instances inherit static C++ component fields"); + MetaCoreExpect(secondInstanceRoot.GetComponent().Target == "AppliedPrefabTarget", "Apply should make new prefab instances inherit static C++ string fields"); + secondInstanceRoot.GetComponent().Position = glm::vec3(-1.0F, -2.0F, -3.0F); + secondInstanceRoot.GetComponent().Speed = 1.0F; editorContext.SelectOnly(secondInstanceRoot.GetId()); MetaCoreExpect(prefabService->RevertSelectedPrefabInstance(editorContext), "应能还原 prefab 实例"); @@ -2520,6 +3126,9 @@ void MetaCoreTestPrefabWorkflow() { MetaCoreExpect(revertedRoot, "还原后应重新选中实例根"); MetaCoreExpectVec3Near(revertedRoot.GetComponent().Position, glm::vec3(8.0F, 9.0F, 10.0F), "Revert 后实例应恢复为 prefab 内容"); + MetaCoreExpect(revertedRoot.HasComponent(), "Revert should restore static C++ components"); + MetaCoreExpect(std::abs(revertedRoot.GetComponent().Speed - 6.75F) <= 0.0001F, "Revert should restore static C++ component fields"); + const MetaCore::MetaCoreId revertedRootId = revertedRoot.GetId(); const std::vector revertedSubtreeIds = scene.GetSubtreeObjectIds(revertedRootId); @@ -5512,6 +6121,8 @@ int main() { MetaCoreTestProjectFileRoundTripAndDiscovery(); std::cout << "[RUN] MetaCoreTestAssetDatabaseCreateAndOpenProject..." << std::endl; MetaCoreTestAssetDatabaseCreateAndOpenProject(); + std::cout << "[RUN] MetaCoreTestAssetDatabaseCreatesFirstClassProjectAssets..." << std::endl; + MetaCoreTestAssetDatabaseCreatesFirstClassProjectAssets(); std::cout << "[RUN] MetaCoreTestAssetDatabaseRenamePathUpdatesProjectDescriptor..." << std::endl; MetaCoreTestAssetDatabaseRenamePathUpdatesProjectDescriptor(); std::cout << "[RUN] MetaCoreTestAssetDatabaseMovePathUpdatesProjectDescriptor..." << std::endl; @@ -5524,6 +6135,8 @@ int main() { MetaCoreTestPlayModeLifecycleAndSceneIsolation(); std::cout << "[RUN] MetaCoreTestSceneGizmoMultiSelectDeltaAndUndo..." << std::endl; MetaCoreTestSceneGizmoMultiSelectDeltaAndUndo(); + std::cout << "[RUN] MetaCoreTestSceneViewportHoverPicking..." << std::endl; + MetaCoreTestSceneViewportHoverPicking(); std::cout << "[RUN] MetaCoreTestGizmoSnapSettings..." << std::endl; MetaCoreTestGizmoSnapSettings(); std::cout << "[RUN] MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot..." << std::endl;