diff --git a/CMakeLists.txt b/CMakeLists.txt index 48ccc30..3c80509 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,8 +33,35 @@ endif() list(PREPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") -if(EXISTS "${CMAKE_SOURCE_DIR}/vcpkg_installed/x64-windows/share/imgui/imgui-config.cmake") - list(PREPEND CMAKE_PREFIX_PATH "${CMAKE_SOURCE_DIR}/vcpkg_installed/x64-windows") +set(METACORE_VCPKG_TRIPLET "") +set(METACORE_VCPKG_PREFIX "") +if(WIN32) + set(METACORE_VCPKG_TRIPLET "x64-windows") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$") + set(METACORE_VCPKG_TRIPLET "x64-linux") +endif() + +if(METACORE_VCPKG_TRIPLET) + foreach(METACORE_VCPKG_PREFIX_CANDIDATE IN ITEMS + "${CMAKE_SOURCE_DIR}/vcpkg_installed/${METACORE_VCPKG_TRIPLET}" + "$ENV{VCPKG_ROOT}/installed/${METACORE_VCPKG_TRIPLET}" + "$ENV{HOME}/vcpkg/installed/${METACORE_VCPKG_TRIPLET}" + ) + if(EXISTS "${METACORE_VCPKG_PREFIX_CANDIDATE}/share/imgui/imgui-config.cmake") + set(METACORE_VCPKG_PREFIX "${METACORE_VCPKG_PREFIX_CANDIDATE}") + list(PREPEND CMAKE_PREFIX_PATH "${METACORE_VCPKG_PREFIX_CANDIDATE}") + break() + endif() + endforeach() +endif() + +if(METACORE_VCPKG_PREFIX) + foreach(METACORE_CACHED_PACKAGE_DIR IN ITEMS imgui_DIR glfw3_DIR) + if(DEFINED ${METACORE_CACHED_PACKAGE_DIR} AND + NOT "${${METACORE_CACHED_PACKAGE_DIR}}" MATCHES "^${METACORE_VCPKG_PREFIX}(/|$)") + unset(${METACORE_CACHED_PACKAGE_DIR} CACHE) + endif() + endforeach() endif() @@ -177,6 +204,8 @@ endfunction() set(METACORE_FOUNDATION_HEADERS Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreArchive.h Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetGuid.h + Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetChange.h + Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetDependencyGraph.h Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetRegistry.h Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreGeneratedReflection.h Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreHash.h @@ -185,11 +214,13 @@ set(METACORE_FOUNDATION_HEADERS Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCorePackage.h Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreProject.h Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreReflection.h + Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreRuntimeAssetRegistry.h ) set(METACORE_FOUNDATION_SOURCES Source/MetaCoreFoundation/Private/MetaCoreArchive.cpp Source/MetaCoreFoundation/Private/MetaCoreAssetGuid.cpp + Source/MetaCoreFoundation/Private/MetaCoreAssetDependencyGraph.cpp Source/MetaCoreFoundation/Private/MetaCoreAssetRegistry.cpp Source/MetaCoreFoundation/Private/MetaCoreHash.cpp Source/MetaCoreFoundation/Private/MetaCoreId.cpp @@ -197,6 +228,7 @@ set(METACORE_FOUNDATION_SOURCES Source/MetaCoreFoundation/Private/MetaCorePackage.cpp Source/MetaCoreFoundation/Private/MetaCoreProject.cpp Source/MetaCoreFoundation/Private/MetaCoreReflection.cpp + Source/MetaCoreFoundation/Private/MetaCoreRuntimeAssetRegistry.cpp ) metacore_generate_reflection( diff --git a/SandboxProject/Assets/Materials/default_pbr.material.json.mcmeta b/SandboxProject/Assets/Materials/default_pbr.material.json.mcmeta index bbffce8..d7e9b21 100644 Binary files a/SandboxProject/Assets/Materials/default_pbr.material.json.mcmeta and b/SandboxProject/Assets/Materials/default_pbr.material.json.mcmeta differ diff --git a/SandboxProject/Assets/Models/Cube.glb.mcmeta b/SandboxProject/Assets/Models/Cube.glb.mcmeta index 2fce38a..c87cb25 100644 --- a/SandboxProject/Assets/Models/Cube.glb.mcmeta +++ b/SandboxProject/Assets/Models/Cube.glb.mcmeta @@ -10,12 +10,14 @@ "guid": "9cd50d41-df30-26ff-da7e-e01bb57f7135", "index": 0, "name": "Cube", + "stable_import_key": "", "type": "mesh" }, { "guid": "20a8eca7-ae5b-8ed9-c7a8-b64b59567283", "index": 0, "name": "Cube_Material", + "stable_import_key": "", "type": "material" } ] diff --git a/SandboxProject/Assets/Models/Plane.glb.mcmeta b/SandboxProject/Assets/Models/Plane.glb.mcmeta index d9b1975..714f3e6 100644 --- a/SandboxProject/Assets/Models/Plane.glb.mcmeta +++ b/SandboxProject/Assets/Models/Plane.glb.mcmeta @@ -10,12 +10,14 @@ "guid": "804bca6d-b9ea-f7af-3f0a-f078d5de0d45", "index": 0, "name": "Plane", + "stable_import_key": "", "type": "mesh" }, { "guid": "6c337003-dc9a-da7c-d685-6d27c673046a", "index": 0, "name": "Plane_Material", + "stable_import_key": "", "type": "material" } ] diff --git a/SandboxProject/Assets/Models/box1.glb.mcmeta b/SandboxProject/Assets/Models/box1.glb.mcmeta index 668468a..8c1c3f1 100644 --- a/SandboxProject/Assets/Models/box1.glb.mcmeta +++ b/SandboxProject/Assets/Models/box1.glb.mcmeta @@ -10,12 +10,14 @@ "guid": "34a6ed73-d4a5-83a3-5863-742c204c4869", "index": 0, "name": "box1", + "stable_import_key": "", "type": "mesh" }, { "guid": "9833a707-d635-426e-5db2-4cb58fbb5d03", "index": 0, "name": "box1_Material", + "stable_import_key": "", "type": "material" } ] diff --git a/SandboxProject/Assets/Models/jyc.glb.mcmeta b/SandboxProject/Assets/Models/jyc.glb.mcmeta index 2fbe0a0..ced5f4c 100644 --- a/SandboxProject/Assets/Models/jyc.glb.mcmeta +++ b/SandboxProject/Assets/Models/jyc.glb.mcmeta @@ -10,12 +10,14 @@ "guid": "50f3a009-11f7-c225-e32c-79f879faf21e", "index": 0, "name": "jyc", + "stable_import_key": "", "type": "mesh" }, { "guid": "64cad172-c4ea-929e-1c10-8a6af41df213", "index": 0, "name": "jyc_Material", + "stable_import_key": "", "type": "material" } ] diff --git a/SandboxProject/Assets/UI/main_menu.rcss.mcmeta b/SandboxProject/Assets/UI/main_menu.rcss.mcmeta index 7bedd69..ec51ec9 100644 Binary files a/SandboxProject/Assets/UI/main_menu.rcss.mcmeta and b/SandboxProject/Assets/UI/main_menu.rcss.mcmeta differ diff --git a/SandboxProject/Assets/UI/main_menu.rml.mcmeta b/SandboxProject/Assets/UI/main_menu.rml.mcmeta index 5e478cb..558ffbf 100644 Binary files a/SandboxProject/Assets/UI/main_menu.rml.mcmeta and b/SandboxProject/Assets/UI/main_menu.rml.mcmeta differ diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index d03846c..63e298d 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -11,6 +11,8 @@ #include "MetaCoreFoundation/MetaCoreProject.h" #include "MetaCorePlatform/MetaCoreInput.h" #include "MetaCoreFoundation/MetaCoreAssetRegistry.h" +#include "MetaCoreFoundation/MetaCoreAssetDependencyGraph.h" +#include "MetaCoreFoundation/MetaCoreRuntimeAssetRegistry.h" #include "MetaCoreScene/MetaCoreScenePackage.h" #include "MetaCoreScene/MetaCoreSceneSerializer.h" #include @@ -28,13 +30,18 @@ #include "stb_dxt.h" #include +#include #include +#include +#include #include #include #include #include #include #include +#include +#include #include #include #include @@ -48,6 +55,39 @@ namespace MetaCore { namespace { +void MetaCoreWriteGuidArray(nlohmann::json& json, const char* key, const std::vector& guids) { + if (guids.empty()) { + return; + } + + auto values = nlohmann::json::array(); + for (const MetaCoreAssetGuid& guid : guids) { + if (guid.IsValid()) { + values.push_back(guid.ToString()); + } + } + if (!values.empty()) { + json[key] = std::move(values); + } +} + +void MetaCoreReadGuidArray(const nlohmann::json& json, const char* key, std::vector& outGuids) { + outGuids.clear(); + if (!json.contains(key) || !json[key].is_array()) { + return; + } + + for (const auto& value : json[key]) { + if (!value.is_string()) { + continue; + } + const auto guid = MetaCoreAssetGuid::Parse(value.get()); + if (guid.has_value() && guid->IsValid()) { + outGuids.push_back(*guid); + } + } +} + bool MetaCoreWriteMetaJson( const std::filesystem::path& absoluteMetaPath, const MetaCoreAssetMetadataDocument& metadata @@ -70,10 +110,13 @@ bool MetaCoreWriteMetaJson( subAssetJson["type"] = subAsset.Type; subAssetJson["name"] = subAsset.Name; subAssetJson["index"] = subAsset.Index; + subAssetJson["stable_import_key"] = subAsset.StableImportKey; + MetaCoreWriteGuidArray(subAssetJson, "dependencies", subAsset.Dependencies); subAssetsJson.push_back(subAssetJson); } json["sub_assets"] = subAssetsJson; } + MetaCoreWriteGuidArray(json, "dependencies", metadata.Dependencies); // 将 JSON 漂亮格式化并写入文件 std::ofstream output(absoluteMetaPath); @@ -139,11 +182,16 @@ bool MetaCoreReadMetaJson( if (subAssetJson.contains("index") && subAssetJson["index"].is_number_integer()) { subAsset.Index = subAssetJson["index"].get(); } + if (subAssetJson.contains("stable_import_key") && subAssetJson["stable_import_key"].is_string()) { + subAsset.StableImportKey = subAssetJson["stable_import_key"].get(); + } + MetaCoreReadGuidArray(subAssetJson, "dependencies", subAsset.Dependencies); if (subAsset.Guid.IsValid()) { outMetadata.SubAssets.push_back(subAsset); } } } + MetaCoreReadGuidArray(json, "dependencies", outMetadata.Dependencies); return true; } catch (...) { return false; @@ -3069,6 +3117,340 @@ void MetaCoreWriteEmbeddedTextureRuntimePackages( return "asset"; } +void MetaCoreAppendUniqueGuid(std::vector& guids, const MetaCoreAssetGuid& guid) { + if (guid.IsValid() && std::find(guids.begin(), guids.end(), guid) == guids.end()) { + guids.push_back(guid); + } +} + +void MetaCoreCollectJsonGuidReferences(const nlohmann::json& json, std::vector& outGuids) { + if (json.is_string()) { + const auto guid = MetaCoreAssetGuid::Parse(json.get()); + if (guid.has_value()) { + MetaCoreAppendUniqueGuid(outGuids, *guid); + } + return; + } + if (json.is_array()) { + for (const auto& value : json) { + MetaCoreCollectJsonGuidReferences(value, outGuids); + } + return; + } + if (json.is_object()) { + for (const auto& [key, value] : json.items()) { + (void)key; + MetaCoreCollectJsonGuidReferences(value, outGuids); + } + } +} + +[[nodiscard]] std::vector MetaCoreReadJsonGuidDependencies( + const std::filesystem::path& absolutePath, + const MetaCoreAssetGuid& selfGuid = {} +) { + std::vector dependencies; + std::ifstream input(absolutePath); + if (!input.is_open()) { + return dependencies; + } + + try { + nlohmann::json json; + input >> json; + MetaCoreCollectJsonGuidReferences(json, dependencies); + } catch (...) { + return {}; + } + + std::erase(dependencies, selfGuid); + return dependencies; +} + +[[nodiscard]] std::filesystem::path MetaCoreRemapRelativePath( + const std::filesystem::path& value, + const std::filesystem::path& oldPath, + const std::filesystem::path& newPath +) { + const std::filesystem::path normalizedValue = value.lexically_normal(); + const std::filesystem::path normalizedOldPath = oldPath.lexically_normal(); + if (normalizedValue == normalizedOldPath) { + return newPath.lexically_normal(); + } + + const std::string valueString = normalizedValue.generic_string(); + const std::string oldPrefix = normalizedOldPath.generic_string() + "/"; + if (valueString.rfind(oldPrefix, 0) == 0) { + return (newPath / normalizedValue.lexically_relative(normalizedOldPath)).lexically_normal(); + } + return value; +} + +void MetaCoreUpdateMovedMetadata( + const std::filesystem::path& projectRoot, + const std::filesystem::path& oldRelativePath, + const std::filesystem::path& newRelativePath +) { + const std::filesystem::path absoluteNewPath = projectRoot / newRelativePath; + std::vector metaPaths; + const std::filesystem::path sidecarPath = MetaCoreBuildMetaPath(absoluteNewPath); + if (std::filesystem::exists(sidecarPath)) { + metaPaths.push_back(sidecarPath); + } + if (std::filesystem::is_directory(absoluteNewPath)) { + for (const auto& entry : std::filesystem::recursive_directory_iterator(absoluteNewPath)) { + if (entry.is_regular_file() && MetaCoreIsMetaPath(entry.path())) { + metaPaths.push_back(entry.path()); + } + } + } + + for (const std::filesystem::path& metaPath : metaPaths) { + MetaCoreAssetMetadataDocument metadata; + if (!MetaCoreReadMetaJson(metaPath, metadata)) { + continue; + } + metadata.SourcePath = MetaCoreRemapRelativePath(metadata.SourcePath, oldRelativePath, newRelativePath); + metadata.PackagePath = MetaCoreRemapRelativePath(metadata.PackagePath, oldRelativePath, newRelativePath); + (void)MetaCoreWriteMetaJson(metaPath, metadata); + } +} + +void MetaCoreMoveSidecarIfPresent( + const std::filesystem::path& projectRoot, + const std::filesystem::path& oldRelativePath, + const std::filesystem::path& newRelativePath +) { + std::error_code errorCode; + const std::filesystem::path oldMetaPath = projectRoot / MetaCoreBuildMetaPath(oldRelativePath); + const std::filesystem::path newMetaPath = projectRoot / MetaCoreBuildMetaPath(newRelativePath); + if (std::filesystem::exists(oldMetaPath) && !std::filesystem::exists(newMetaPath)) { + std::filesystem::rename(oldMetaPath, newMetaPath, errorCode); + } + + const std::filesystem::path oldPackagePath = projectRoot / MetaCoreBuildPackagePathFromSource(oldRelativePath); + const std::filesystem::path newPackagePath = projectRoot / MetaCoreBuildPackagePathFromSource(newRelativePath); + if (std::filesystem::exists(oldPackagePath) && !std::filesystem::exists(newPackagePath)) { + errorCode.clear(); + std::filesystem::rename(oldPackagePath, newPackagePath, errorCode); + } +} + +void MetaCoreApplyStableSubAssetGuids( + const std::vector& previousSubAssets, + MetaCoreModelAssetDocument& document +) { + std::unordered_map previousGuids; + for (const MetaCoreSubAssetMetadata& subAsset : previousSubAssets) { + if (subAsset.Guid.IsValid() && !subAsset.StableImportKey.empty()) { + previousGuids[subAsset.Type + "|" + subAsset.StableImportKey] = subAsset.Guid; + } + } + + std::unordered_map remappedGuids; + const auto reuseGuids = [&](auto& generatedAssets, std::string_view type) { + for (auto& generatedAsset : generatedAssets) { + const auto previous = previousGuids.find(std::string(type) + "|" + generatedAsset.StableImportKey); + if (previous == previousGuids.end() || previous->second == generatedAsset.AssetGuid) { + continue; + } + remappedGuids[generatedAsset.AssetGuid] = previous->second; + generatedAsset.AssetGuid = previous->second; + } + }; + + reuseGuids(document.GeneratedTextureAssets, "texture"); + reuseGuids(document.GeneratedMaterialAssets, "material"); + reuseGuids(document.GeneratedMeshAssets, "mesh"); + + const auto remapGuid = [&](MetaCoreAssetGuid& guid) { + if (const auto iterator = remappedGuids.find(guid); iterator != remappedGuids.end()) { + guid = iterator->second; + } + }; + for (MetaCoreMaterialAssetDocument& material : document.GeneratedMaterialAssets) { + remapGuid(material.BaseColorTexture); + remapGuid(material.NormalTexture); + remapGuid(material.MetallicRoughnessTexture); + remapGuid(material.AoTexture); + remapGuid(material.EmissiveTexture); + } +} + +void MetaCoreUpdateModelSubAssetMetadata( + const MetaCoreModelAssetDocument& document, + MetaCoreAssetMetadataDocument& metadata +) { + metadata.SubAssets.clear(); + metadata.Dependencies.clear(); + const auto addSubAsset = [&](const auto& generatedAsset, std::string type, std::int32_t index) { + if (!generatedAsset.AssetGuid.IsValid()) { + return; + } + + MetaCoreSubAssetMetadata subAsset; + subAsset.Guid = generatedAsset.AssetGuid; + subAsset.Type = std::move(type); + subAsset.Name = generatedAsset.Name; + subAsset.Index = index; + subAsset.StableImportKey = generatedAsset.StableImportKey; + metadata.SubAssets.push_back(std::move(subAsset)); + MetaCoreAppendUniqueGuid(metadata.Dependencies, generatedAsset.AssetGuid); + }; + + for (std::size_t index = 0; index < document.GeneratedMeshAssets.size(); ++index) { + addSubAsset(document.GeneratedMeshAssets[index], "mesh", static_cast(index)); + } + for (std::size_t index = 0; index < document.GeneratedMaterialAssets.size(); ++index) { + if (!document.GeneratedMaterialAssets[index].AssetGuid.IsValid()) { + continue; + } + addSubAsset(document.GeneratedMaterialAssets[index], "material", static_cast(index)); + auto& dependencies = metadata.SubAssets.back().Dependencies; + const auto& material = document.GeneratedMaterialAssets[index]; + MetaCoreAppendUniqueGuid(dependencies, material.BaseColorTexture); + MetaCoreAppendUniqueGuid(dependencies, material.NormalTexture); + MetaCoreAppendUniqueGuid(dependencies, material.MetallicRoughnessTexture); + MetaCoreAppendUniqueGuid(dependencies, material.AoTexture); + MetaCoreAppendUniqueGuid(dependencies, material.EmissiveTexture); + } + for (std::size_t index = 0; index < document.GeneratedTextureAssets.size(); ++index) { + addSubAsset(document.GeneratedTextureAssets[index], "texture", static_cast(index)); + } +} + +void MetaCoreRebuildAssetDependencyGraph(const MetaCoreProjectDescriptor& project) { + auto& graph = MetaCoreAssetDependencyGraph::Get(); + graph.Clear(); + + const std::array roots{project.AssetsPath, project.ScenesPath}; + for (const std::filesystem::path& root : roots) { + if (!std::filesystem::exists(root)) { + continue; + } + for (const auto& entry : std::filesystem::recursive_directory_iterator(root)) { + if (!entry.is_regular_file() || !MetaCoreIsMetaPath(entry.path())) { + continue; + } + + MetaCoreAssetMetadataDocument metadata; + if (!MetaCoreReadMetaJson(entry.path(), metadata) || !metadata.AssetGuid.IsValid()) { + continue; + } + + std::vector dependencies = metadata.Dependencies; + const std::filesystem::path absoluteSourcePath = project.RootPath / metadata.SourcePath; + if (MetaCoreIsJsonSourceAssetPath(absoluteSourcePath) && std::filesystem::exists(absoluteSourcePath)) { + for (const MetaCoreAssetGuid& dependency : MetaCoreReadJsonGuidDependencies(absoluteSourcePath, metadata.AssetGuid)) { + MetaCoreAppendUniqueGuid(dependencies, dependency); + } + } + + graph.SetDependencies( + metadata.AssetGuid, + MetaCoreAssetDependencyGraph::AssetGuidSet(dependencies.begin(), dependencies.end()) + ); + for (const MetaCoreSubAssetMetadata& subAsset : metadata.SubAssets) { + graph.SetDependencies( + subAsset.Guid, + MetaCoreAssetDependencyGraph::AssetGuidSet( + subAsset.Dependencies.begin(), + subAsset.Dependencies.end() + ) + ); + } + } + } +} + +void MetaCoreQueueAssetChangeWithDependents(const MetaCoreAssetChangeEvent& rootEvent, bool propagate) { + auto& runtimeRegistry = MetaCoreRuntimeAssetRegistry::Get(); + runtimeRegistry.QueueEvent(rootEvent); + if (!propagate || !rootEvent.AssetGuid.IsValid()) { + return; + } + + std::vector pending{rootEvent.AssetGuid}; + std::unordered_set visited{rootEvent.AssetGuid}; + while (!pending.empty()) { + const MetaCoreAssetGuid dependencyGuid = pending.back(); + pending.pop_back(); + for (const MetaCoreAssetGuid& dependentGuid : MetaCoreAssetDependencyGraph::Get().GetDependents(dependencyGuid)) { + if (!visited.insert(dependentGuid).second) { + continue; + } + MetaCoreAssetChangeEvent dependentEvent = rootEvent; + dependentEvent.AssetGuid = dependentGuid; + dependentEvent.SourceAssetGuid = rootEvent.AssetGuid; + dependentEvent.OldRelativePath.clear(); + dependentEvent.NewRelativePath.clear(); + runtimeRegistry.QueueEvent(std::move(dependentEvent)); + pending.push_back(dependentGuid); + } + } +} + +void MetaCorePublishAssetRecordChanges( + const std::vector& previousRecords, + const std::vector& currentRecords +) { + std::unordered_map previousByGuid; + std::unordered_map currentByGuid; + for (const MetaCoreAssetRecord& record : previousRecords) { + if (record.Guid.IsValid()) { + previousByGuid[record.Guid] = record; + } + } + for (const MetaCoreAssetRecord& record : currentRecords) { + if (record.Guid.IsValid()) { + currentByGuid[record.Guid] = record; + } + } + + for (const auto& [guid, previous] : previousByGuid) { + const auto current = currentByGuid.find(guid); + if (current == currentByGuid.end()) { + MetaCoreQueueAssetChangeWithDependents( + MetaCoreAssetChangeEvent{MetaCoreAssetChangeType::Deleted, guid, guid, previous.RelativePath, {}}, + true + ); + continue; + } + if (previous.RelativePath != current->second.RelativePath) { + MetaCoreQueueAssetChangeWithDependents( + MetaCoreAssetChangeEvent{ + MetaCoreAssetChangeType::Moved, + guid, + guid, + previous.RelativePath, + current->second.RelativePath + }, + false + ); + } else if (previous.SourceHash != current->second.SourceHash) { + MetaCoreQueueAssetChangeWithDependents( + MetaCoreAssetChangeEvent{ + MetaCoreAssetChangeType::Modified, + guid, + guid, + previous.RelativePath, + current->second.RelativePath + }, + true + ); + } + } + + for (const auto& [guid, current] : currentByGuid) { + if (!previousByGuid.contains(guid)) { + MetaCoreQueueAssetChangeWithDependents( + MetaCoreAssetChangeEvent{MetaCoreAssetChangeType::Created, guid, guid, {}, current.RelativePath}, + false + ); + } + } +} + [[nodiscard]] MetaCoreTextureAssetDocument MetaCoreBuildImportedTextureDocument( const std::filesystem::path& absoluteSourcePath, const std::filesystem::path& relativeSourcePath, @@ -3405,6 +3787,7 @@ public: [[nodiscard]] bool RenamePath(const std::filesystem::path& relativePath, const std::filesystem::path& newName) override; [[nodiscard]] bool MovePath(const std::filesystem::path& relativePath, const std::filesystem::path& targetDirectory) override; [[nodiscard]] bool DeletePath(const std::filesystem::path& relativePath) override; + bool ApplyExternalFileChanges(const std::vector& changes) override; [[nodiscard]] const std::vector& GetAssetRegistry() const override { return AssetRecords_; } [[nodiscard]] std::optional FindAssetByGuid(const MetaCoreAssetGuid& assetGuid) const override { @@ -3466,6 +3849,7 @@ private: void SaveProjectDescriptor() const; void RefreshScenePathsFromDisk(); void RefreshAssetRecordsFromDisk(); + void RescanHotReloadBaseline() const; MetaCoreEditorModuleRegistry* ModuleRegistry_ = nullptr; MetaCoreProjectDescriptor Project_{}; @@ -3594,6 +3978,12 @@ bool MetaCoreBuiltinAssetDatabaseService::OpenProject(const std::filesystem::pat #endif LoadProjectDescriptor(); const bool refreshed = Refresh(); + if (ModuleRegistry_ != nullptr) { + if (const auto hotReloadService = ModuleRegistry_->ResolveService(); + hotReloadService != nullptr) { + hotReloadService->SetProjectRoot(Project_.RootPath); + } + } if (ModuleRegistry_ != nullptr && refreshed && HasProject()) { ModuleRegistry_->AccessEventBus().Publish(MetaCoreEditorEvent{ MetaCoreEditorEventType::ProjectLoaded, @@ -3632,14 +4022,8 @@ bool MetaCoreBuiltinAssetDatabaseService::RenamePath( return false; } - const std::filesystem::path legacyMetaSource = Project_.RootPath / MetaCoreBuildMetaPath(normalizedPath); - const std::filesystem::path legacyMetaTarget = Project_.RootPath / MetaCoreBuildMetaPath(targetPath); - if (std::filesystem::exists(legacyMetaSource) && !std::filesystem::exists(legacyMetaTarget)) { - std::filesystem::rename(legacyMetaSource, legacyMetaTarget, errorCode); - if (errorCode) { - return false; - } - } + MetaCoreMoveSidecarIfPresent(Project_.RootPath, normalizedPath, targetPath); + MetaCoreUpdateMovedMetadata(Project_.RootPath, normalizedPath, targetPath); const auto remapPath = [&](std::filesystem::path& pathValue) { if (pathValue == normalizedPath) { @@ -3662,7 +4046,9 @@ bool MetaCoreBuiltinAssetDatabaseService::RenamePath( Project_.ScenePaths.erase(std::unique(Project_.ScenePaths.begin(), Project_.ScenePaths.end()), Project_.ScenePaths.end()); SaveProjectDescriptor(); - return Refresh(); + const bool refreshed = Refresh(); + RescanHotReloadBaseline(); + return refreshed; } bool MetaCoreBuiltinAssetDatabaseService::MovePath( @@ -3713,14 +4099,8 @@ bool MetaCoreBuiltinAssetDatabaseService::MovePath( return false; } - const std::filesystem::path legacyMetaSource = Project_.RootPath / MetaCoreBuildMetaPath(normalizedSourcePath); - const std::filesystem::path legacyMetaTarget = Project_.RootPath / MetaCoreBuildMetaPath(targetPath); - if (std::filesystem::exists(legacyMetaSource) && !std::filesystem::exists(legacyMetaTarget)) { - std::filesystem::rename(legacyMetaSource, legacyMetaTarget, errorCode); - if (errorCode) { - return false; - } - } + MetaCoreMoveSidecarIfPresent(Project_.RootPath, normalizedSourcePath, targetPath); + MetaCoreUpdateMovedMetadata(Project_.RootPath, normalizedSourcePath, targetPath); remapPath(Project_.StartupScenePath); for (auto& scenePath : Project_.ScenePaths) { @@ -3729,7 +4109,9 @@ bool MetaCoreBuiltinAssetDatabaseService::MovePath( std::sort(Project_.ScenePaths.begin(), Project_.ScenePaths.end()); Project_.ScenePaths.erase(std::unique(Project_.ScenePaths.begin(), Project_.ScenePaths.end()), Project_.ScenePaths.end()); SaveProjectDescriptor(); - return Refresh(); + const bool refreshed = Refresh(); + RescanHotReloadBaseline(); + return refreshed; } bool MetaCoreBuiltinAssetDatabaseService::DeletePath(const std::filesystem::path& relativePath) { @@ -3778,9 +4160,44 @@ bool MetaCoreBuiltinAssetDatabaseService::DeletePath(const std::filesystem::path ); SaveProjectDescriptor(); + const bool refreshed = Refresh(); + RescanHotReloadBaseline(); + return refreshed; +} + +bool MetaCoreBuiltinAssetDatabaseService::ApplyExternalFileChanges( + const std::vector& changes +) { + if (!HasProject() || changes.empty()) { + return false; + } + + for (const MetaCoreAssetChangeEvent& change : changes) { + if (change.Type == MetaCoreAssetChangeType::Moved && + !change.OldRelativePath.empty() && + !change.NewRelativePath.empty()) { + MetaCoreMoveSidecarIfPresent(Project_.RootPath, change.OldRelativePath, change.NewRelativePath); + MetaCoreUpdateMovedMetadata(Project_.RootPath, change.OldRelativePath, change.NewRelativePath); + } else if (change.Type == MetaCoreAssetChangeType::Deleted && !change.OldRelativePath.empty()) { + std::error_code errorCode; + std::filesystem::remove(Project_.RootPath / MetaCoreBuildMetaPath(change.OldRelativePath), errorCode); + errorCode.clear(); + std::filesystem::remove(Project_.RootPath / MetaCoreBuildPackagePathFromSource(change.OldRelativePath), errorCode); + } + } return Refresh(); } +void MetaCoreBuiltinAssetDatabaseService::RescanHotReloadBaseline() const { + if (ModuleRegistry_ == nullptr) { + return; + } + if (const auto hotReloadService = ModuleRegistry_->ResolveService(); + hotReloadService != nullptr) { + hotReloadService->RescanBaseline(); + } +} + void MetaCoreBuiltinAssetDatabaseService::SaveProjectDescriptor() const { if (!HasProject()) { return; @@ -3991,6 +4408,13 @@ bool MetaCoreBuiltinAssetDatabaseService::Refresh() { return false; } + const std::vector previousRecords = AssetRecords_; + + // Pass 1: make all existing GUID/path mappings available before importers + // scan references. A second scan below picks up newly-created metadata. + MetaCoreAssetRegistry::Get().Clear(); + MetaCoreAssetRegistry::Get().ScanDirectory(Project_.AssetsPath); + if (ModuleRegistry_ != nullptr) { if (const auto importPipeline = ModuleRegistry_->ResolveService(); importPipeline != nullptr) { @@ -4004,6 +4428,8 @@ bool MetaCoreBuiltinAssetDatabaseService::Refresh() { // 扫描并重置全局资产 GUID 注册表 (对标 Unity 升级) MetaCoreAssetRegistry::Get().Clear(); MetaCoreAssetRegistry::Get().ScanDirectory(Project_.AssetsPath); + MetaCorePublishAssetRecordChanges(previousRecords, AssetRecords_); + MetaCoreRebuildAssetDependencyGraph(Project_); if (ModuleRegistry_ != nullptr) { if (const auto cookService = ModuleRegistry_->ResolveService(); cookService != nullptr) { @@ -4690,9 +5116,13 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste const std::filesystem::path absoluteMetaPath = MetaCoreBuildMetaPath(absoluteSourcePath); MetaCoreAssetMetadataDocument metadata; + bool hasMetadata = false; if (std::filesystem::exists(absoluteMetaPath)) { - (void)MetaCoreReadMetaJson(absoluteMetaPath, metadata); + hasMetadata = MetaCoreReadMetaJson(absoluteMetaPath, metadata); } + const std::uint64_t previousSourceHash = metadata.SourceHash; + const std::string previousImporterId = metadata.ImporterId; + const std::vector previousSubAssets = metadata.SubAssets; if (!metadata.AssetGuid.IsValid()) { metadata.AssetGuid = MetaCoreAssetGuid::Generate(); @@ -4703,17 +5133,20 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste metadata.SourcePath = relativeSourcePath; metadata.PackagePath = relativePackagePath; metadata.SourceHash = *sourceHash; + if (jsonSourceAsset) { + metadata.Dependencies = MetaCoreReadJsonGuidDependencies(absoluteSourcePath, metadata.AssetGuid); + } const MetaCoreModelImportSettingsDocument existingModelImportSettings = (metadata.AssetType == "model") ? MetaCoreReadExistingModelImportSettings(*PackageService_, registry, absolutePackagePath) : MetaCoreBuildDefaultModelImportSettings(); - bool needsPackageWrite = forcePackageWrite || !std::filesystem::exists(absolutePackagePath); - if (!needsPackageWrite) { - const auto package = PackageService_->ReadPackage(absolutePackagePath); - needsPackageWrite = !package.has_value() || package->Header.SourceHash != *sourceHash; - } + const bool needsPackageWrite = + forcePackageWrite || + !hasMetadata || + previousSourceHash != *sourceHash || + previousImporterId != metadata.ImporterId; if (needsPackageWrite) { std::optional> importedAssetBytes; @@ -4728,42 +5161,8 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste meshPayloads, existingModelImportSettings ); - - // 收集生成的子资产信息到元数据中,以便落盘和注册表在下次启动时扫描解析 - metadata.SubAssets.clear(); - for (std::size_t index = 0; index < importedAsset.GeneratedMeshAssets.size(); ++index) { - const auto& mesh = importedAsset.GeneratedMeshAssets[index]; - if (mesh.AssetGuid.IsValid()) { - MetaCoreSubAssetMetadata sub; - sub.Guid = mesh.AssetGuid; - sub.Type = "mesh"; - sub.Name = mesh.Name; - sub.Index = static_cast(index); - metadata.SubAssets.push_back(sub); - } - } - for (std::size_t index = 0; index < importedAsset.GeneratedMaterialAssets.size(); ++index) { - const auto& mat = importedAsset.GeneratedMaterialAssets[index]; - if (mat.AssetGuid.IsValid()) { - MetaCoreSubAssetMetadata sub; - sub.Guid = mat.AssetGuid; - sub.Type = "material"; - sub.Name = mat.Name; - sub.Index = static_cast(index); - metadata.SubAssets.push_back(sub); - } - } - for (std::size_t index = 0; index < importedAsset.GeneratedTextureAssets.size(); ++index) { - const auto& tex = importedAsset.GeneratedTextureAssets[index]; - if (tex.AssetGuid.IsValid()) { - MetaCoreSubAssetMetadata sub; - sub.Guid = tex.AssetGuid; - sub.Type = "texture"; - sub.Name = tex.Name; - sub.Index = static_cast(index); - metadata.SubAssets.push_back(sub); - } - } + MetaCoreApplyStableSubAssetGuids(previousSubAssets, importedAsset); + MetaCoreUpdateModelSubAssetMetadata(importedAsset, metadata); importedAssetBytes = MetaCoreSerializeToBytes(importedAsset, registry); importedAssetTypeName = "MetaCoreModelAssetDocument"; @@ -4861,6 +5260,8 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste metadata.SourceHash, existingModelImportSettings ); + MetaCoreApplyStableSubAssetGuids(previousSubAssets, importedAsset); + MetaCoreUpdateModelSubAssetMetadata(importedAsset, metadata); importedAssetBytes = MetaCoreSerializeToBytes(importedAsset, registry); importedAssetTypeName = "MetaCoreModelAssetDocument"; } else { @@ -4903,6 +5304,10 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste } } + if (!MetaCoreWriteMetaJson(absoluteMetaPath, metadata)) { + return false; + } + if (CookService_ != nullptr) { (void)CookService_->CookAsset(metadata.AssetGuid); } @@ -6235,6 +6640,312 @@ bool MetaCoreBuiltinSceneEditingService::ReparentSelection(MetaCoreEditorContext return reparented; } +class MetaCoreBuiltinAssetHotReloadService final : public MetaCoreIAssetHotReloadService { +public: + ~MetaCoreBuiltinAssetHotReloadService() override { StopWorker(); } + + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.AssetHotReload"; } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + ModuleRegistry_ = &moduleRegistry; + AssetDatabaseService_ = moduleRegistry.ResolveService(); + if (AssetDatabaseService_ != nullptr && AssetDatabaseService_->HasProject()) { + SetProjectRoot(AssetDatabaseService_->GetProjectDescriptor().RootPath); + } + } + + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + StopWorker(); + AssetDatabaseService_.reset(); + ModuleRegistry_ = nullptr; + } + + void Tick() override { + std::vector queuedChanges; + const auto now = std::chrono::steady_clock::now(); + { + std::scoped_lock lock(Mutex_); + for (auto iterator = QueuedChanges_.begin(); iterator != QueuedChanges_.end();) { + if (iterator->NextAttemptAt <= now) { + queuedChanges.push_back(std::move(*iterator)); + iterator = QueuedChanges_.erase(iterator); + } else { + ++iterator; + } + } + } + + if (!queuedChanges.empty() && AssetDatabaseService_ != nullptr) { + std::vector changes; + changes.reserve(queuedChanges.size()); + for (const QueuedChange& queuedChange : queuedChanges) { + changes.push_back(queuedChange.Event); + const std::filesystem::path scenePath = + queuedChange.Event.NewRelativePath.empty() + ? queuedChange.Event.OldRelativePath + : queuedChange.Event.NewRelativePath; + if (ModuleRegistry_ != nullptr && MetaCoreIsScenePath(scenePath)) { + ModuleRegistry_->AccessEventBus().Publish(MetaCoreEditorEvent{ + MetaCoreEditorEventType::ExternalSceneChanged, + "场景文件已在外部修改,索引已刷新;当前打开场景未自动覆盖", + scenePath, + 0, + true + }); + } + } + + if (!AssetDatabaseService_->ApplyExternalFileChanges(changes)) { + std::scoped_lock lock(Mutex_); + for (QueuedChange& queuedChange : queuedChanges) { + if (queuedChange.RetryCount < 3) { + ++queuedChange.RetryCount; + queuedChange.NextAttemptAt = now + std::chrono::milliseconds(250); + QueuedChanges_.push_back(std::move(queuedChange)); + } + } + } + } + + MetaCoreRuntimeAssetRegistry::Get().DispatchQueuedEvents(); + } + + void SetProjectRoot(const std::filesystem::path& projectRoot) override { + { + std::scoped_lock lock(Mutex_); + ProjectRoot_ = projectRoot.lexically_normal(); + } + RescanBaseline(); + StartWorker(); + } + + void RescanBaseline() override { + const SnapshotMap snapshot = BuildSnapshot(); + std::scoped_lock lock(Mutex_); + Baseline_ = snapshot; + PendingCandidates_.clear(); + QueuedChanges_.clear(); + } + +private: + struct FileSnapshot { + std::uintmax_t Size = 0; + std::uint64_t Hash = 0; + + bool operator==(const FileSnapshot&) const = default; + }; + + struct PendingCandidate { + MetaCoreAssetChangeEvent Event{}; + FileSnapshot Snapshot{}; + std::chrono::steady_clock::time_point FirstSeen{}; + }; + + struct QueuedChange { + MetaCoreAssetChangeEvent Event{}; + int RetryCount = 0; + std::chrono::steady_clock::time_point NextAttemptAt{}; + }; + + using SnapshotMap = std::unordered_map; + + [[nodiscard]] static bool ShouldIgnoreFile(const std::filesystem::path& path) { + const std::string extension = path.extension().string(); + const std::string filename = path.filename().string(); + return extension == ".mcmeta" || + extension == ".meta" || + extension == ".mcasset" || + extension == ".tmp" || + (!filename.empty() && (filename.back() == '~' || filename.front() == '.')); + } + + [[nodiscard]] SnapshotMap BuildSnapshot() const { + std::filesystem::path projectRoot; + { + std::scoped_lock lock(Mutex_); + projectRoot = ProjectRoot_; + } + + SnapshotMap snapshot; + for (const std::filesystem::path& root : std::array{ + projectRoot / "Assets", + projectRoot / "Scenes" + }) { + if (!std::filesystem::exists(root)) { + continue; + } + for (const auto& entry : std::filesystem::recursive_directory_iterator(root)) { + if (!entry.is_regular_file() || ShouldIgnoreFile(entry.path())) { + continue; + } + const auto hash = MetaCoreHashFile(entry.path()); + if (!hash.has_value()) { + continue; + } + snapshot[entry.path().lexically_relative(projectRoot).generic_string()] = FileSnapshot{ + entry.file_size(), + *hash + }; + } + } + return snapshot; + } + + [[nodiscard]] static std::string BuildCandidateKey(const MetaCoreAssetChangeEvent& event) { + return std::to_string(static_cast(event.Type)) + "|" + + event.OldRelativePath.generic_string() + "|" + + event.NewRelativePath.generic_string(); + } + + [[nodiscard]] static bool IsCandidateCurrent( + const PendingCandidate& candidate, + const SnapshotMap& snapshot + ) { + const std::string oldPath = candidate.Event.OldRelativePath.generic_string(); + const std::string newPath = candidate.Event.NewRelativePath.generic_string(); + if (candidate.Event.Type == MetaCoreAssetChangeType::Deleted) { + return !snapshot.contains(oldPath); + } + if (candidate.Event.Type == MetaCoreAssetChangeType::Moved) { + const auto iterator = snapshot.find(newPath); + return !snapshot.contains(oldPath) && iterator != snapshot.end() && iterator->second == candidate.Snapshot; + } + const auto iterator = snapshot.find(newPath); + return iterator != snapshot.end() && iterator->second == candidate.Snapshot; + } + + void PollOnce() { + const SnapshotMap current = BuildSnapshot(); + const auto now = std::chrono::steady_clock::now(); + + std::scoped_lock lock(Mutex_); + std::vector> created; + std::vector> deleted; + for (const auto& [path, file] : current) { + const auto previous = Baseline_.find(path); + if (previous == Baseline_.end()) { + created.push_back({path, file}); + } else if (!(previous->second == file)) { + AddCandidate( + MetaCoreAssetChangeEvent{MetaCoreAssetChangeType::Modified, {}, {}, path, path}, + file, + now + ); + } + } + for (const auto& [path, file] : Baseline_) { + if (!current.contains(path)) { + deleted.push_back({path, file}); + } + } + + std::unordered_set pairedCreates; + for (const auto& [oldPath, oldFile] : deleted) { + bool paired = false; + for (std::size_t index = 0; index < created.size(); ++index) { + if (!pairedCreates.contains(index) && created[index].second == oldFile) { + pairedCreates.insert(index); + AddCandidate( + MetaCoreAssetChangeEvent{ + MetaCoreAssetChangeType::Moved, + {}, + {}, + oldPath, + created[index].first + }, + created[index].second, + now + ); + paired = true; + break; + } + } + if (!paired) { + AddCandidate( + MetaCoreAssetChangeEvent{MetaCoreAssetChangeType::Deleted, {}, {}, oldPath, {}}, + oldFile, + now + ); + } + } + for (std::size_t index = 0; index < created.size(); ++index) { + if (!pairedCreates.contains(index)) { + AddCandidate( + MetaCoreAssetChangeEvent{MetaCoreAssetChangeType::Created, {}, {}, {}, created[index].first}, + created[index].second, + now + ); + } + } + + Baseline_ = current; + for (auto iterator = PendingCandidates_.begin(); iterator != PendingCandidates_.end();) { + if (!IsCandidateCurrent(iterator->second, current)) { + iterator = PendingCandidates_.erase(iterator); + continue; + } + if (now - iterator->second.FirstSeen >= std::chrono::milliseconds(250)) { + QueuedChanges_.push_back(QueuedChange{iterator->second.Event, 0, now}); + iterator = PendingCandidates_.erase(iterator); + continue; + } + ++iterator; + } + } + + void AddCandidate( + MetaCoreAssetChangeEvent event, + const FileSnapshot& snapshot, + std::chrono::steady_clock::time_point now + ) { + const std::string key = BuildCandidateKey(event); + const auto iterator = PendingCandidates_.find(key); + if (iterator == PendingCandidates_.end() || !(iterator->second.Snapshot == snapshot)) { + PendingCandidates_[key] = PendingCandidate{std::move(event), snapshot, now}; + } + } + + void StartWorker() { + if (Worker_.joinable()) { + return; + } + StopRequested_ = false; + Worker_ = std::thread([this]() { + while (!StopRequested_) { + std::unique_lock lock(WorkerMutex_); + if (WorkerCondition_.wait_for(lock, std::chrono::milliseconds(500), [this]() { + return StopRequested_.load(); + })) { + break; + } + lock.unlock(); + PollOnce(); + } + }); + } + + void StopWorker() { + StopRequested_ = true; + WorkerCondition_.notify_all(); + if (Worker_.joinable()) { + Worker_.join(); + } + } + + MetaCoreEditorModuleRegistry* ModuleRegistry_ = nullptr; + std::shared_ptr AssetDatabaseService_{}; + mutable std::mutex Mutex_{}; + std::filesystem::path ProjectRoot_{}; + SnapshotMap Baseline_{}; + std::unordered_map PendingCandidates_{}; + std::vector QueuedChanges_{}; + std::atomic_bool StopRequested_ = false; + std::mutex WorkerMutex_{}; + std::condition_variable WorkerCondition_{}; + std::thread Worker_{}; +}; + class MetaCoreBuiltinCoreServicesModule final : public MetaCoreIModule { public: [[nodiscard]] std::string GetModuleName() const override { return "MetaCoreBuiltinCoreServicesModule"; } @@ -6245,6 +6956,7 @@ 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()); diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index 274f5ad..bb7f42a 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -17,6 +17,7 @@ #include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h" #include "MetaCorePlatform/MetaCoreWindow.h" #include "MetaCoreFoundation/MetaCoreAssetRegistry.h" +#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include #include @@ -640,6 +641,9 @@ void MetaCoreOpenProject(MetaCoreEditorContext& editorContext, const std::filesy } if (assetDatabaseService->OpenProject(projectRoot)) { + editorContext.GetViewportRenderer().SetProjectRootPath(assetDatabaseService->GetProjectDescriptor().RootPath); + editorContext.SetSelectedProjectDirectory("Assets"); + editorContext.ClearSelectedAsset(); MetaCoreEnsureProjectStartupScene(editorContext); editorContext.AddConsoleMessage( MetaCoreLogLevel::Info, @@ -658,6 +662,9 @@ void MetaCoreCreateProject(MetaCoreEditorContext& editorContext, const std::file } if (assetDatabaseService->CreateProject(projectRoot, projectName)) { + editorContext.GetViewportRenderer().SetProjectRootPath(assetDatabaseService->GetProjectDescriptor().RootPath); + editorContext.SetSelectedProjectDirectory("Assets"); + editorContext.ClearSelectedAsset(); MetaCoreEnsureProjectStartupScene(editorContext); editorContext.AddConsoleMessage( MetaCoreLogLevel::Info, @@ -669,6 +676,100 @@ void MetaCoreCreateProject(MetaCoreEditorContext& editorContext, const std::file } } +[[nodiscard]] bool MetaCoreIsAssetsRelativePath(const std::filesystem::path& relativePath) { + const std::filesystem::path normalizedPath = relativePath.lexically_normal(); + const auto begin = normalizedPath.begin(); + return begin != normalizedPath.end() && *begin == "Assets"; +} + +void MetaCoreImportExternalAssetPath( + MetaCoreEditorContext& editorContext, + const std::filesystem::path& sourcePath, + const std::filesystem::path& targetRelativeDirectory +) { + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || !assetDatabaseService->HasProject()) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Asset", "导入失败:当前没有打开项目"); + return; + } + + if (sourcePath.empty()) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Asset", "导入失败:源路径为空"); + return; + } + + const std::filesystem::path absoluteSourcePath = sourcePath.is_absolute() + ? sourcePath.lexically_normal() + : (std::filesystem::current_path() / sourcePath).lexically_normal(); + std::error_code errorCode; + if (!std::filesystem::exists(absoluteSourcePath, errorCode)) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Error, + "Asset", + "导入失败:源路径不存在: " + absoluteSourcePath.string() + ); + return; + } + + std::filesystem::path normalizedTargetDirectory = targetRelativeDirectory.empty() + ? std::filesystem::path("Assets") + : targetRelativeDirectory.lexically_normal(); + if (!MetaCoreIsAssetsRelativePath(normalizedTargetDirectory)) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Asset", "导入失败:目标目录必须位于 Assets 下"); + return; + } + + const std::filesystem::path absoluteTargetDirectory = + assetDatabaseService->GetProjectDescriptor().RootPath / normalizedTargetDirectory; + std::filesystem::create_directories(absoluteTargetDirectory, errorCode); + if (errorCode) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Error, + "Asset", + "导入失败:无法创建目标目录: " + absoluteTargetDirectory.string() + ); + return; + } + + const std::filesystem::path absoluteTargetPath = absoluteTargetDirectory / absoluteSourcePath.filename(); + errorCode.clear(); + if (std::filesystem::is_directory(absoluteSourcePath, errorCode)) { + std::filesystem::copy( + absoluteSourcePath, + absoluteTargetPath, + std::filesystem::copy_options::recursive | std::filesystem::copy_options::overwrite_existing, + errorCode + ); + } else { + std::filesystem::copy_file( + absoluteSourcePath, + absoluteTargetPath, + std::filesystem::copy_options::overwrite_existing, + errorCode + ); + } + + if (errorCode) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Error, + "Asset", + "导入失败:" + errorCode.message() + ); + return; + } + + (void)assetDatabaseService->Refresh(); + if (const auto hotReloadService = editorContext.GetModuleRegistry().ResolveService(); + hotReloadService != nullptr) { + hotReloadService->RescanBaseline(); + } + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Info, + "Asset", + "已导入资源: " + absoluteTargetPath.lexically_relative(assetDatabaseService->GetProjectDescriptor().RootPath).generic_string() + ); +} + void MetaCoreHandleGlobalEditorShortcuts(MetaCoreEditorContext& editorContext) { const ImGuiIO& io = ImGui::GetIO(); if (io.WantTextInput) { @@ -1819,7 +1920,7 @@ public: std::snprintf(CreateProjectPathBuffer_, sizeof(CreateProjectPathBuffer_), "%s", std::filesystem::current_path().string().c_str()); } std::snprintf(CreateProjectNameBuffer_, sizeof(CreateProjectNameBuffer_), "%s", "NewMetaCoreProject"); - ImGui::OpenPopup("MetaCoreCreateProjectPopup"); + OpenCreateProjectPopup_ = true; } if (ImGui::MenuItem("打开项目...")) { if (assetDatabaseService != nullptr && assetDatabaseService->HasProject()) { @@ -1827,7 +1928,7 @@ public: } else { std::snprintf(OpenProjectPathBuffer_, sizeof(OpenProjectPathBuffer_), "%s", std::filesystem::current_path().string().c_str()); } - ImGui::OpenPopup("MetaCoreOpenProjectPopup"); + OpenProjectPopup_ = true; } ImGui::Separator(); if (ImGui::MenuItem("保存场景", "Ctrl+S", false, scenePersistenceService != nullptr)) { @@ -1849,6 +1950,19 @@ public: ImGui::EndMenu(); } + if (OpenCreateProjectPopup_) { + ImGui::OpenPopup("MetaCoreCreateProjectPopup"); + OpenCreateProjectPopup_ = false; + } + if (OpenProjectPopup_) { + ImGui::OpenPopup("MetaCoreOpenProjectPopup"); + OpenProjectPopup_ = false; + } + if (OpenImportResourcePopup_) { + ImGui::OpenPopup("MetaCoreImportResourcePopup"); + OpenImportResourcePopup_ = false; + } + if (ImGui::BeginPopupModal("MetaCoreCreateProjectPopup", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::InputText("项目目录", CreateProjectPathBuffer_, sizeof(CreateProjectPathBuffer_)); ImGui::InputText("项目名称", CreateProjectNameBuffer_, sizeof(CreateProjectNameBuffer_)); @@ -1877,6 +1991,25 @@ public: ImGui::EndPopup(); } + if (ImGui::BeginPopupModal("MetaCoreImportResourcePopup", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::TextUnformatted("输入外部文件或目录路径,导入到项目 Assets 目录。"); + ImGui::InputText("源路径", ImportSourcePathBuffer_, sizeof(ImportSourcePathBuffer_)); + ImGui::InputText("目标目录", ImportTargetDirectoryBuffer_, sizeof(ImportTargetDirectoryBuffer_)); + if (ImGui::Button("导入")) { + MetaCoreImportExternalAssetPath( + editorContext, + std::filesystem::path(ImportSourcePathBuffer_), + std::filesystem::path(ImportTargetDirectoryBuffer_) + ); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("取消")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + if (ImGui::BeginMenu("编辑")) { if (ImGui::MenuItem("撤销", "Ctrl+Z", false, editorContext.GetCommandService().CanUndo())) { MetaCoreHandleUndo(editorContext); @@ -1898,7 +2031,22 @@ public: } if (ImGui::BeginMenu("资源")) { - ImGui::MenuItem("导入资源", nullptr, false, false); + const bool canImportResource = + assetDatabaseService != nullptr && assetDatabaseService->HasProject(); + if (ImGui::MenuItem("导入资源...", nullptr, false, canImportResource)) { + ImportSourcePathBuffer_[0] = '\0'; + std::filesystem::path targetDirectory = editorContext.GetSelectedProjectDirectory(); + if (targetDirectory.empty() || !MetaCoreIsAssetsRelativePath(targetDirectory)) { + targetDirectory = "Assets"; + } + std::snprintf( + ImportTargetDirectoryBuffer_, + sizeof(ImportTargetDirectoryBuffer_), + "%s", + targetDirectory.generic_string().c_str() + ); + OpenImportResourcePopup_ = true; + } if (ImGui::MenuItem("从选中对象创建 Prefab", nullptr, false, editorContext.GetActiveObjectId() != 0)) { (void)MetaCoreCreatePrefabFromSelection(editorContext); } @@ -1953,9 +2101,14 @@ public: } private: + bool OpenCreateProjectPopup_ = false; + bool OpenProjectPopup_ = false; + bool OpenImportResourcePopup_ = false; char CreateProjectPathBuffer_[512]{}; char CreateProjectNameBuffer_[256]{}; char OpenProjectPathBuffer_[512]{}; + char ImportSourcePathBuffer_[1024]{}; + char ImportTargetDirectoryBuffer_[512]{}; }; class MetaCoreHierarchyPanelProvider final : public MetaCoreIEditorPanelProvider { diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index af2d91e..776b6df 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -738,6 +738,10 @@ bool MetaCoreEditorApp::Initialize(int argc, char* argv[]) { int MetaCoreEditorApp::Run() { while (!Window_.ShouldClose()) { Window_.BeginFrame(); + if (const auto hotReloadService = ModuleRegistry_.ResolveService(); + hotReloadService != nullptr) { + hotReloadService->Tick(); + } #if defined(_WIN32) ImGui_ImplWin32_NewFrame(); diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h index 1c8fd44..2751e7d 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h @@ -29,6 +29,12 @@ struct MetaCoreSubAssetMetadata { MC_PROPERTY() std::int32_t Index = 0; + + MC_PROPERTY() + std::string StableImportKey{}; + + MC_PROPERTY() + std::vector Dependencies{}; }; MC_STRUCT() @@ -55,6 +61,9 @@ struct MetaCoreAssetMetadataDocument { MC_PROPERTY() std::vector SubAssets{}; + + MC_PROPERTY() + std::vector Dependencies{}; }; MC_STRUCT() diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h index c898b4d..81d5e2a 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h @@ -1,6 +1,7 @@ #pragma once #include "MetaCoreFoundation/MetaCoreAssetGuid.h" +#include "MetaCoreFoundation/MetaCoreAssetChange.h" #include "MetaCoreFoundation/MetaCoreAssetTypes.h" #include "MetaCoreFoundation/MetaCoreId.h" #include "MetaCoreFoundation/MetaCorePackage.h" @@ -28,7 +29,8 @@ enum class MetaCoreEditorEventType { SceneLoaded, SceneSaved, SceneDirtyStateChanged, - SelectionChanged + SelectionChanged, + ExternalSceneChanged }; struct MetaCoreEditorEvent { @@ -144,12 +146,20 @@ public: [[nodiscard]] virtual std::vector GetDirectoriesUnder(const std::filesystem::path& relativeDirectory) const = 0; [[nodiscard]] virtual std::vector GetAssetsUnder(const std::filesystem::path& relativeDirectory) const = 0; [[nodiscard]] virtual std::optional FindAssetByRelativePath(const std::filesystem::path& relativePath) const = 0; + virtual bool ApplyExternalFileChanges(const std::vector& changes) = 0; virtual bool Refresh() = 0; virtual bool CreateFolder(const std::filesystem::path& relativeDirectory) = 0; virtual bool RegisterScenePath(const std::filesystem::path& relativeScenePath, bool makeStartupScene) = 0; virtual bool SetStartupScenePath(const std::filesystem::path& relativeScenePath) = 0; }; +class MetaCoreIAssetHotReloadService : public MetaCoreIEditorService { +public: + virtual void Tick() = 0; + virtual void SetProjectRoot(const std::filesystem::path& projectRoot) = 0; + virtual void RescanBaseline() = 0; +}; + class MetaCoreIImportPipelineService : public MetaCoreIEditorService { public: [[nodiscard]] virtual bool RefreshImports() = 0; diff --git a/Source/MetaCoreFoundation/Private/MetaCoreAssetDependencyGraph.cpp b/Source/MetaCoreFoundation/Private/MetaCoreAssetDependencyGraph.cpp new file mode 100644 index 0000000..330a5d0 --- /dev/null +++ b/Source/MetaCoreFoundation/Private/MetaCoreAssetDependencyGraph.cpp @@ -0,0 +1,99 @@ +#include "MetaCoreFoundation/MetaCoreAssetDependencyGraph.h" + +namespace MetaCore { + +MetaCoreAssetDependencyGraph& MetaCoreAssetDependencyGraph::Get() { + static MetaCoreAssetDependencyGraph graph; + return graph; +} + +void MetaCoreAssetDependencyGraph::SetDependencies( + const MetaCoreAssetGuid& assetGuid, + const AssetGuidSet& dependencies +) { + if (!assetGuid.IsValid()) { + return; + } + + std::scoped_lock lock(Mutex_); + if (const auto previous = Dependencies_.find(assetGuid); previous != Dependencies_.end()) { + for (const MetaCoreAssetGuid& dependency : previous->second) { + if (auto dependents = Dependents_.find(dependency); dependents != Dependents_.end()) { + dependents->second.erase(assetGuid); + if (dependents->second.empty()) { + Dependents_.erase(dependents); + } + } + } + } + + AssetGuidSet validDependencies; + for (const MetaCoreAssetGuid& dependency : dependencies) { + if (!dependency.IsValid() || dependency == assetGuid) { + continue; + } + validDependencies.insert(dependency); + Dependents_[dependency].insert(assetGuid); + } + + if (validDependencies.empty()) { + Dependencies_.erase(assetGuid); + } else { + Dependencies_[assetGuid] = std::move(validDependencies); + } +} + +MetaCoreAssetDependencyGraph::AssetGuidSet MetaCoreAssetDependencyGraph::GetDependencies( + const MetaCoreAssetGuid& assetGuid +) const { + std::scoped_lock lock(Mutex_); + const auto iterator = Dependencies_.find(assetGuid); + return iterator == Dependencies_.end() ? AssetGuidSet{} : iterator->second; +} + +MetaCoreAssetDependencyGraph::AssetGuidSet MetaCoreAssetDependencyGraph::GetDependents( + const MetaCoreAssetGuid& assetGuid +) const { + std::scoped_lock lock(Mutex_); + const auto iterator = Dependents_.find(assetGuid); + return iterator == Dependents_.end() ? AssetGuidSet{} : iterator->second; +} + +void MetaCoreAssetDependencyGraph::RemoveAsset(const MetaCoreAssetGuid& assetGuid) { + if (!assetGuid.IsValid()) { + return; + } + + std::scoped_lock lock(Mutex_); + if (const auto dependencies = Dependencies_.find(assetGuid); dependencies != Dependencies_.end()) { + for (const MetaCoreAssetGuid& dependency : dependencies->second) { + if (auto dependents = Dependents_.find(dependency); dependents != Dependents_.end()) { + dependents->second.erase(assetGuid); + if (dependents->second.empty()) { + Dependents_.erase(dependents); + } + } + } + Dependencies_.erase(dependencies); + } + + if (const auto dependents = Dependents_.find(assetGuid); dependents != Dependents_.end()) { + for (const MetaCoreAssetGuid& dependent : dependents->second) { + if (auto dependencies = Dependencies_.find(dependent); dependencies != Dependencies_.end()) { + dependencies->second.erase(assetGuid); + if (dependencies->second.empty()) { + Dependencies_.erase(dependencies); + } + } + } + Dependents_.erase(dependents); + } +} + +void MetaCoreAssetDependencyGraph::Clear() { + std::scoped_lock lock(Mutex_); + Dependencies_.clear(); + Dependents_.clear(); +} + +} // namespace MetaCore diff --git a/Source/MetaCoreFoundation/Private/MetaCoreAssetRegistry.cpp b/Source/MetaCoreFoundation/Private/MetaCoreAssetRegistry.cpp index 3587740..55f166f 100644 --- a/Source/MetaCoreFoundation/Private/MetaCoreAssetRegistry.cpp +++ b/Source/MetaCoreFoundation/Private/MetaCoreAssetRegistry.cpp @@ -50,7 +50,7 @@ void MetaCoreAssetRegistry::ScanDirectory(const std::filesystem::path& rootPath) const std::string subGuidStr = subAssetJson["guid"].get(); auto parsedSubGuid = MetaCoreAssetGuid::Parse(subGuidStr); if (parsedSubGuid.has_value()) { - RegisterAsset(*parsedSubGuid, portablePath); + RegisterSubAsset(*parsedSubGuid, portablePath); } } } @@ -93,18 +93,50 @@ bool MetaCoreAssetRegistry::RegisterAsset(const MetaCoreAssetGuid& guid, const s return false; } - // 将路径规范化为通用字符串(统一斜杠) - std::string keyPath = relativePath.generic_string(); - std::transform(keyPath.begin(), keyPath.end(), keyPath.begin(), ::tolower); - + UnregisterAsset(guid); + const std::string keyPath = NormalizePathKey(relativePath); GuidToPath_[guid] = relativePath; PathToGuid_[keyPath] = guid; + PrimaryAssetGuids_.insert(guid); return true; } +bool MetaCoreAssetRegistry::RegisterSubAsset(const MetaCoreAssetGuid& guid, const std::filesystem::path& relativePath) { + if (!guid.IsValid()) { + return false; + } + UnregisterAsset(guid); + GuidToPath_[guid] = relativePath; + return true; +} + +void MetaCoreAssetRegistry::UnregisterAsset(const MetaCoreAssetGuid& guid) { + const auto iterator = GuidToPath_.find(guid); + if (iterator == GuidToPath_.end()) { + return; + } + + const std::string keyPath = NormalizePathKey(iterator->second); + if (const auto pathIterator = PathToGuid_.find(keyPath); + pathIterator != PathToGuid_.end() && pathIterator->second == guid) { + PathToGuid_.erase(pathIterator); + } + GuidToPath_.erase(iterator); + PrimaryAssetGuids_.erase(guid); +} + +bool MetaCoreAssetRegistry::MoveAsset(const MetaCoreAssetGuid& guid, const std::filesystem::path& newRelativePath) { + if (!guid.IsValid() || !GuidToPath_.contains(guid)) { + return false; + } + const bool isPrimaryAsset = PrimaryAssetGuids_.contains(guid); + return isPrimaryAsset ? RegisterAsset(guid, newRelativePath) : RegisterSubAsset(guid, newRelativePath); +} + void MetaCoreAssetRegistry::Clear() { GuidToPath_.clear(); PathToGuid_.clear(); + PrimaryAssetGuids_.clear(); } std::filesystem::path MetaCoreAssetRegistry::ResolveGuidToPath(const MetaCoreAssetGuid& guid) const { @@ -116,9 +148,7 @@ std::filesystem::path MetaCoreAssetRegistry::ResolveGuidToPath(const MetaCoreAss } MetaCoreAssetGuid MetaCoreAssetRegistry::ResolvePathToGuid(const std::filesystem::path& relativePath) const { - std::string keyPath = relativePath.generic_string(); - std::transform(keyPath.begin(), keyPath.end(), keyPath.begin(), ::tolower); - + const std::string keyPath = NormalizePathKey(relativePath); const auto it = PathToGuid_.find(keyPath); if (it != PathToGuid_.end()) { return it->second; @@ -126,6 +156,16 @@ MetaCoreAssetGuid MetaCoreAssetRegistry::ResolvePathToGuid(const std::filesystem return {}; } +std::string MetaCoreAssetRegistry::NormalizePathKey(const std::filesystem::path& path) { + std::string keyPath = path.lexically_normal().generic_string(); +#if defined(_WIN32) + std::transform(keyPath.begin(), keyPath.end(), keyPath.begin(), [](unsigned char value) { + return value >= 'A' && value <= 'Z' ? static_cast(value - 'A' + 'a') : static_cast(value); + }); +#endif + return keyPath; +} + bool MetaCoreAssetRegistry::HasAsset(const MetaCoreAssetGuid& guid) const { return GuidToPath_.count(guid) > 0; } diff --git a/Source/MetaCoreFoundation/Private/MetaCoreRuntimeAssetRegistry.cpp b/Source/MetaCoreFoundation/Private/MetaCoreRuntimeAssetRegistry.cpp new file mode 100644 index 0000000..eff0c26 --- /dev/null +++ b/Source/MetaCoreFoundation/Private/MetaCoreRuntimeAssetRegistry.cpp @@ -0,0 +1,57 @@ +#include "MetaCoreFoundation/MetaCoreRuntimeAssetRegistry.h" + +#include + +namespace MetaCore { + +MetaCoreRuntimeAssetRegistry& MetaCoreRuntimeAssetRegistry::Get() { + static MetaCoreRuntimeAssetRegistry registry; + return registry; +} + +MetaCoreRuntimeAssetRegistry::SubscriptionId MetaCoreRuntimeAssetRegistry::Subscribe(AssetChangeHandler handler) { + if (!handler) { + return 0; + } + + std::scoped_lock lock(Mutex_); + const SubscriptionId subscriptionId = NextSubscriptionId_++; + Subscriptions_.emplace(subscriptionId, std::move(handler)); + return subscriptionId; +} + +void MetaCoreRuntimeAssetRegistry::Unsubscribe(SubscriptionId subscriptionId) { + std::scoped_lock lock(Mutex_); + Subscriptions_.erase(subscriptionId); +} + +void MetaCoreRuntimeAssetRegistry::QueueEvent(MetaCoreAssetChangeEvent event) { + std::scoped_lock lock(Mutex_); + QueuedEvents_.push_back(std::move(event)); +} + +void MetaCoreRuntimeAssetRegistry::DispatchQueuedEvents() { + std::vector events; + std::unordered_map subscriptions; + { + std::scoped_lock lock(Mutex_); + events.swap(QueuedEvents_); + subscriptions = Subscriptions_; + } + + for (const MetaCoreAssetChangeEvent& event : events) { + for (const auto& [subscriptionId, handler] : subscriptions) { + (void)subscriptionId; + if (handler) { + handler(event); + } + } + } +} + +void MetaCoreRuntimeAssetRegistry::Clear() { + std::scoped_lock lock(Mutex_); + QueuedEvents_.clear(); +} + +} // namespace MetaCore diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetChange.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetChange.h new file mode 100644 index 0000000..7294be9 --- /dev/null +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetChange.h @@ -0,0 +1,24 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreAssetGuid.h" + +#include + +namespace MetaCore { + +enum class MetaCoreAssetChangeType { + Created = 0, + Modified, + Moved, + Deleted +}; + +struct MetaCoreAssetChangeEvent { + MetaCoreAssetChangeType Type = MetaCoreAssetChangeType::Modified; + MetaCoreAssetGuid AssetGuid{}; + MetaCoreAssetGuid SourceAssetGuid{}; + std::filesystem::path OldRelativePath{}; + std::filesystem::path NewRelativePath{}; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetDependencyGraph.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetDependencyGraph.h new file mode 100644 index 0000000..8a5fe51 --- /dev/null +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetDependencyGraph.h @@ -0,0 +1,31 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreAssetGuid.h" + +#include +#include +#include + +namespace MetaCore { + +class MetaCoreAssetDependencyGraph { +public: + using AssetGuidSet = std::unordered_set; + + [[nodiscard]] static MetaCoreAssetDependencyGraph& Get(); + + void SetDependencies(const MetaCoreAssetGuid& assetGuid, const AssetGuidSet& dependencies); + [[nodiscard]] AssetGuidSet GetDependencies(const MetaCoreAssetGuid& assetGuid) const; + [[nodiscard]] AssetGuidSet GetDependents(const MetaCoreAssetGuid& assetGuid) const; + void RemoveAsset(const MetaCoreAssetGuid& assetGuid); + void Clear(); + +private: + MetaCoreAssetDependencyGraph() = default; + + mutable std::mutex Mutex_{}; + std::unordered_map Dependencies_{}; + std::unordered_map Dependents_{}; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetRegistry.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetRegistry.h index 14115ae..2d8541f 100644 --- a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetRegistry.h +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetRegistry.h @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -20,6 +21,12 @@ public: // 手动注册单个资产的双向映射关系 bool RegisterAsset(const MetaCoreAssetGuid& guid, const std::filesystem::path& relativePath); + // 子资产共享源文件路径,但不参与路径到 GUID 的反查 + bool RegisterSubAsset(const MetaCoreAssetGuid& guid, const std::filesystem::path& relativePath); + + void UnregisterAsset(const MetaCoreAssetGuid& guid); + bool MoveAsset(const MetaCoreAssetGuid& guid, const std::filesystem::path& newRelativePath); + // 清空注册表缓存 void Clear(); @@ -33,6 +40,8 @@ public: [[nodiscard]] bool HasAsset(const MetaCoreAssetGuid& guid) const; private: + [[nodiscard]] static std::string NormalizePathKey(const std::filesystem::path& path); + MetaCoreAssetRegistry() = default; ~MetaCoreAssetRegistry() = default; @@ -41,6 +50,7 @@ private: std::unordered_map GuidToPath_; std::unordered_map PathToGuid_; + std::unordered_set PrimaryAssetGuids_; }; } // namespace MetaCore diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreRuntimeAssetRegistry.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreRuntimeAssetRegistry.h new file mode 100644 index 0000000..71be86e --- /dev/null +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreRuntimeAssetRegistry.h @@ -0,0 +1,35 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreAssetChange.h" + +#include +#include +#include +#include +#include + +namespace MetaCore { + +class MetaCoreRuntimeAssetRegistry { +public: + using SubscriptionId = std::size_t; + using AssetChangeHandler = std::function; + + [[nodiscard]] static MetaCoreRuntimeAssetRegistry& Get(); + + [[nodiscard]] SubscriptionId Subscribe(AssetChangeHandler handler); + void Unsubscribe(SubscriptionId subscriptionId); + void QueueEvent(MetaCoreAssetChangeEvent event); + void DispatchQueuedEvents(); + void Clear(); + +private: + MetaCoreRuntimeAssetRegistry() = default; + + std::mutex Mutex_{}; + std::unordered_map Subscriptions_{}; + std::vector QueuedEvents_{}; + SubscriptionId NextSubscriptionId_ = 1; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp index a31b034..c85a2e3 100644 --- a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp @@ -7,6 +7,7 @@ #include "MetaCoreRender/MetaCoreSceneRenderSync.h" #include #include "MetaCoreFoundation/MetaCoreAssetRegistry.h" +#include "MetaCoreFoundation/MetaCoreRuntimeAssetRegistry.h" #include #include @@ -204,12 +205,26 @@ public: MaterialProvider_ = filament::gltfio::createJitShaderProvider(Engine_); NameManager_ = new utils::NameComponentManager(utils::EntityManager::get()); AssetLoader_ = filament::gltfio::AssetLoader::create({ Engine_, MaterialProvider_, NameManager_ }); + RuntimeAssetSubscriptionId_ = MetaCoreRuntimeAssetRegistry::Get().Subscribe( + [this](const MetaCoreAssetChangeEvent& event) { + if (event.AssetGuid.IsValid()) { + InvalidatedModelGuids_.insert(event.AssetGuid); + } + if (event.SourceAssetGuid.IsValid()) { + InvalidatedModelGuids_.insert(event.SourceAssetGuid); + } + } + ); std::cout << "FilamentSceneBridge: Initialized with Offscreen=" << (offscreen ? "true" : "false") << std::endl; return true; } void Shutdown() { + if (RuntimeAssetSubscriptionId_ != 0) { + MetaCoreRuntimeAssetRegistry::Get().Unsubscribe(RuntimeAssetSubscriptionId_); + RuntimeAssetSubscriptionId_ = 0; + } if (Engine_) { std::cout << "FilamentSceneBridge: Shutting down..." << std::endl; @@ -223,6 +238,8 @@ public: std::cout << "[DEBUG] Destroyed asset for ID: " << id << std::endl; } LoadedAssets_.clear(); + HostRootSourceModelGuids_.clear(); + InvalidatedModelGuids_.clear(); // 2. 销毁所有 pivotEntity 中间辅助坐标变换实体 for (auto& [objectId, assetEntity] : ObjectToFilamentEntity_) { @@ -506,8 +523,12 @@ public: for (auto it = LoadedAssets_.begin(); it != LoadedAssets_.end(); ) { MetaCoreId hostRootId = it->first; filament::gltfio::FilamentAsset* asset = it->second; + const auto sourceGuid = HostRootSourceModelGuids_.find(hostRootId); + const bool invalidated = + sourceGuid != HostRootSourceModelGuids_.end() && + InvalidatedModelGuids_.contains(sourceGuid->second); - if (!activeHostRootIds.contains(hostRootId)) { + if (!activeHostRootIds.contains(hostRootId) || invalidated) { std::cout << "FilamentSceneBridge: Unloading deleted asset: " << hostRootId << std::endl; // 从场景中移除该资产关联的所有实体 @@ -565,11 +586,13 @@ public: } // 从 LoadedAssets_ 中移除 + HostRootSourceModelGuids_.erase(hostRootId); it = LoadedAssets_.erase(it); } else { ++it; } } + InvalidatedModelGuids_.clear(); for (const auto& renderable : snapshot.Renderables) { if (renderable.MeshSource != MetaCoreMeshSourceKind::Asset && @@ -641,6 +664,9 @@ public: } LoadedAssets_[hostRootId] = asset; + if (renderable.SourceModelAssetGuid.IsValid()) { + HostRootSourceModelGuids_[hostRootId] = renderable.SourceModelAssetGuid; + } // 复制材质实例防止共享干扰,放在加载好资源后 { @@ -1328,6 +1354,8 @@ private: std::unordered_map ObjectWorldMatrices_{}; std::unordered_map> ObjectToFilamentEntity_; std::unordered_map LoadedAssets_; + std::unordered_map HostRootSourceModelGuids_; + std::unordered_set InvalidatedModelGuids_; std::unordered_map> AssetDuplicatedMaterials_; std::unordered_map SceneLightEntities_; std::unordered_set EntitiesInScene_; @@ -1344,6 +1372,7 @@ private: std::filesystem::path ProjectRootPath_{}; std::string EmptyString_{}; + MetaCoreRuntimeAssetRegistry::SubscriptionId RuntimeAssetSubscriptionId_ = 0; }; MetaCoreFilamentSceneBridge::MetaCoreFilamentSceneBridge() diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index 4f7ad23..d7eac31 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -7,6 +7,8 @@ #include "MetaCoreFoundation/MetaCoreLogService.h" #include "MetaCoreFoundation/MetaCoreProject.h" #include "MetaCoreFoundation/MetaCoreAssetRegistry.h" +#include "MetaCoreFoundation/MetaCoreAssetDependencyGraph.h" +#include "MetaCoreFoundation/MetaCoreRuntimeAssetRegistry.h" #include "MetaCorePlatform/MetaCoreWindow.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" @@ -31,6 +33,7 @@ #include #include #include +#include #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN @@ -197,6 +200,7 @@ void MetaCoreTestBuiltinModuleComposition() { MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 PackageService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 AssetDatabaseService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ImportPipelineService"); + MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 AssetHotReloadService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 CookService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ScenePersistenceService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 SelectionService"); @@ -3094,6 +3098,167 @@ void MetaCoreTestP3ProductivityStage() { std::filesystem::remove_all(tempProjectRoot); } +void MetaCoreTestAssetDependencyGraphAndRuntimeRegistry() { + auto& graph = MetaCore::MetaCoreAssetDependencyGraph::Get(); + graph.Clear(); + + const MetaCore::MetaCoreAssetGuid modelGuid = MetaCore::MetaCoreAssetGuid::Generate(); + const MetaCore::MetaCoreAssetGuid materialGuid = MetaCore::MetaCoreAssetGuid::Generate(); + const MetaCore::MetaCoreAssetGuid textureGuid = MetaCore::MetaCoreAssetGuid::Generate(); + graph.SetDependencies(modelGuid, {materialGuid}); + graph.SetDependencies(materialGuid, {textureGuid}); + MetaCoreExpect(graph.GetDependencies(modelGuid).contains(materialGuid), "依赖图应记录正向依赖"); + MetaCoreExpect(graph.GetDependents(textureGuid).contains(materialGuid), "依赖图应记录反向依赖"); + + graph.SetDependencies(modelGuid, {textureGuid}); + MetaCoreExpect(!graph.GetDependents(materialGuid).contains(modelGuid), "替换依赖后旧反向边应移除"); + MetaCoreExpect(graph.GetDependents(textureGuid).contains(modelGuid), "替换依赖后新反向边应建立"); + graph.RemoveAsset(textureGuid); + MetaCoreExpect(graph.GetDependencies(modelGuid).empty(), "删除资源后引用它的边应移除"); + + auto& runtimeRegistry = MetaCore::MetaCoreRuntimeAssetRegistry::Get(); + runtimeRegistry.Clear(); + int callbackCount = 0; + const auto subscription = runtimeRegistry.Subscribe([&](const MetaCore::MetaCoreAssetChangeEvent& event) { + if (event.AssetGuid == modelGuid) { + ++callbackCount; + } + }); + runtimeRegistry.QueueEvent(MetaCore::MetaCoreAssetChangeEvent{ + MetaCore::MetaCoreAssetChangeType::Modified, + modelGuid, + modelGuid, + "Assets/Model.glb", + "Assets/Model.glb" + }); + MetaCoreExpect(callbackCount == 0, "运行时失效事件应先进入队列"); + runtimeRegistry.DispatchQueuedEvents(); + MetaCoreExpect(callbackCount == 1, "主线程分发后运行时失效订阅者应收到事件"); + runtimeRegistry.Unsubscribe(subscription); + graph.Clear(); +} + +void MetaCoreTestAssetRegistryPathIdentity() { + auto& registry = MetaCore::MetaCoreAssetRegistry::Get(); + registry.Clear(); + + const MetaCore::MetaCoreAssetGuid modelGuid = MetaCore::MetaCoreAssetGuid::Generate(); + const MetaCore::MetaCoreAssetGuid meshGuid = MetaCore::MetaCoreAssetGuid::Generate(); + MetaCoreExpect(registry.RegisterAsset(modelGuid, "Assets/Models/CaseModel.glb"), "主资源应能注册"); + MetaCoreExpect(registry.RegisterSubAsset(meshGuid, "Assets/Models/CaseModel.glb"), "子资源应能注册"); + MetaCoreExpect(registry.ResolvePathToGuid("Assets/Models/CaseModel.glb") == modelGuid, "子资源不应覆盖主资源路径反查"); + MetaCoreExpect(registry.ResolveGuidToPath(meshGuid) == std::filesystem::path("Assets/Models/CaseModel.glb"), "子资源 GUID 应解析到源文件"); +#if defined(_WIN32) + MetaCoreExpect(registry.ResolvePathToGuid("assets/models/casemodel.glb") == modelGuid, "Windows 路径反查应忽略 ASCII 大小写"); +#else + MetaCoreExpect(!registry.ResolvePathToGuid("assets/models/casemodel.glb").IsValid(), "Linux 路径反查应保留大小写"); +#endif + MetaCoreExpect(registry.MoveAsset(modelGuid, "Assets/Models/Moved.glb"), "主资源路径应能移动"); + MetaCoreExpect(!registry.ResolvePathToGuid("Assets/Models/CaseModel.glb").IsValid(), "移动后旧路径应失效"); + MetaCoreExpect(registry.ResolvePathToGuid("Assets/Models/Moved.glb") == modelGuid, "移动后新路径应生效"); + registry.Clear(); +} + +void MetaCoreTestModelMetadataKeepsSubAssetGuidsAfterMove() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreStableSubAssetMoveProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets" / "Moved"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\"name\":\"StableMove\",\"version\":\"0.1.0\",\"scenes\":[],\"startup_scene\":\"\"}"; + } + { + std::ofstream modelFile(tempProjectRoot / "Assets" / "Pump.gltf", std::ios::trunc); + modelFile << "{\"meshes\":[{\"name\":\"PumpMesh\"}],\"materials\":[{\"name\":\"PumpMaterial\"}],\"images\":[{\"uri\":\"Textures/PumpBaseColor.png\"}]}"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + const auto assetDatabase = moduleRegistry.ResolveService(); + const auto importPipeline = moduleRegistry.ResolveService(); + MetaCoreExpect(assetDatabase != nullptr && importPipeline != nullptr, "应解析到资源服务"); + + const auto sourceRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Pump.gltf"); + MetaCoreExpect(sourceRecord.has_value(), "应导入 Pump 模型"); + nlohmann::json beforeJson; + { + std::ifstream input(tempProjectRoot / "Assets" / "Pump.gltf.mcmeta"); + input >> beforeJson; + } + MetaCoreExpect(beforeJson.contains("sub_assets") && !beforeJson["sub_assets"].empty(), "模型元数据应包含子资源"); + std::unordered_map guidByStableKey; + for (const auto& subAsset : beforeJson["sub_assets"]) { + MetaCoreExpect(subAsset.contains("stable_import_key"), "子资源元数据应写入 stable_import_key"); + guidByStableKey[subAsset["stable_import_key"].get()] = subAsset["guid"].get(); + } + + MetaCoreExpect(assetDatabase->MovePath(std::filesystem::path("Assets") / "Pump.gltf", std::filesystem::path("Assets") / "Moved"), "应移动模型资源"); + MetaCoreExpect(importPipeline->ReimportAsset(sourceRecord->Guid), "移动后应能强制重导模型"); + MetaCoreExpect(assetDatabase->Refresh(), "重导后应能刷新数据库"); + + nlohmann::json afterJson; + { + std::ifstream input(tempProjectRoot / "Assets" / "Moved" / "Pump.gltf.mcmeta"); + input >> afterJson; + } + MetaCoreExpect(afterJson["source_path"] == "Assets/Moved/Pump.gltf", "移动后 mcmeta source_path 应更新"); + for (const auto& subAsset : afterJson["sub_assets"]) { + const std::string stableKey = subAsset["stable_import_key"].get(); + MetaCoreExpect(guidByStableKey.at(stableKey) == subAsset["guid"].get(), "移动并重导后子资源 GUID 应保持"); + } + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + +void MetaCoreTestHotReloadQueuesExternalMoveUntilTick() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreHotReloadMoveProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\"name\":\"HotReloadMove\",\"version\":\"0.1.0\",\"scenes\":[],\"startup_scene\":\"\"}"; + } + { + std::ofstream sourceFile(tempProjectRoot / "Assets" / "Original.dat", std::ios::trunc); + sourceFile << "stable move payload"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + const auto assetDatabase = moduleRegistry.ResolveService(); + const auto hotReload = moduleRegistry.ResolveService(); + MetaCoreExpect(assetDatabase != nullptr && hotReload != nullptr, "应解析到热更新服务"); + const auto originalRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Original.dat"); + MetaCoreExpect(originalRecord.has_value(), "应导入原始文件"); + + std::filesystem::rename(tempProjectRoot / "Assets" / "Original.dat", tempProjectRoot / "Assets" / "Renamed.dat"); + std::this_thread::sleep_for(std::chrono::milliseconds(1300)); + MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / "Assets" / "Renamed.dat.mcmeta"), "后台监听线程不应直接移动 mcmeta"); + hotReload->Tick(); + + const auto renamedRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Renamed.dat"); + MetaCoreExpect(renamedRecord.has_value(), "主线程 Tick 后应注册外部移动后的文件"); + MetaCoreExpect(renamedRecord->Guid == originalRecord->Guid, "外部移动后应保留资源 GUID"); + MetaCoreExpect(std::filesystem::exists(tempProjectRoot / "Assets" / "Renamed.dat.mcmeta"), "主线程 Tick 后应移动 mcmeta"); + MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / "Assets" / "Original.dat.mcmeta"), "外部移动后旧 mcmeta 应消失"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + } // namespace int main() { @@ -3125,6 +3290,15 @@ int main() { logService.AddEntry(MetaCore::MetaCoreLogLevel::Info, "Test", "Smoke"); MetaCoreExpect(logService.GetEntries().size() == 1, "日志服务应记录一条日志"); + std::cout << "[RUN] MetaCoreTestAssetDependencyGraphAndRuntimeRegistry..." << std::endl; + MetaCoreTestAssetDependencyGraphAndRuntimeRegistry(); + std::cout << "[RUN] MetaCoreTestAssetRegistryPathIdentity..." << std::endl; + MetaCoreTestAssetRegistryPathIdentity(); + std::cout << "[RUN] MetaCoreTestModelMetadataKeepsSubAssetGuidsAfterMove..." << std::endl; + MetaCoreTestModelMetadataKeepsSubAssetGuidsAfterMove(); + std::cout << "[RUN] MetaCoreTestHotReloadQueuesExternalMoveUntilTick..." << std::endl; + MetaCoreTestHotReloadQueuesExternalMoveUntilTick(); + std::cout << "[RUN] MetaCoreTestSceneEditApi..." << std::endl; MetaCoreTestSceneEditApi(); std::cout << "[RUN] MetaCoreTestSelectionStateMachine..." << std::endl;