#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace XCEngine { namespace Resources { namespace fs = std::filesystem; namespace { std::string ToStdString(const Containers::String& value) { return std::string(value.CStr()); } bool ShouldTraceAssetPath(const Containers::String& path) { const std::string text = ToStdString(path); return text.rfind("builtin://", 0) == 0 || text.find("backpack") != std::string::npos || text.find("New Material.mat") != std::string::npos; } bool HasVirtualPathScheme(const Containers::String& value) { return ToStdString(value).find("://") != std::string::npos; } Containers::String ToContainersString(const std::string& value) { return Containers::String(value.c_str()); } Containers::String NormalizeArtifactPathString(const Containers::String& path) { if (path.Empty()) { return Containers::String(); } return ToContainersString(fs::path(path.CStr()).lexically_normal().generic_string()); } bool IsProjectRelativePath(const fs::path& path) { const std::string generic = path.generic_string(); return !generic.empty() && generic != "." && generic != ".." && generic.rfind("../", 0) != 0; } void AddUniqueDependencyPath(const fs::path& path, std::unordered_set& seenPaths, std::vector& outPaths) { if (path.empty()) { return; } const fs::path normalizedPath = path.lexically_normal(); const std::string key = normalizedPath.generic_string(); if (key.empty()) { return; } if (seenPaths.insert(key).second) { outPaths.push_back(normalizedPath); } } std::string TrimCopy(const std::string& text) { const auto begin = std::find_if_not(text.begin(), text.end(), [](unsigned char ch) { return std::isspace(ch) != 0; }); if (begin == text.end()) { return std::string(); } const auto end = std::find_if_not(text.rbegin(), text.rend(), [](unsigned char ch) { return std::isspace(ch) != 0; }).base(); return std::string(begin, end); } std::string ToLowerCopy(std::string text) { std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); return text; } std::vector SplitWhitespaceTokens(const std::string& text) { std::vector tokens; std::istringstream stream(text); std::string token; while (stream >> token) { tokens.push_back(token); } return tokens; } std::vector CollectObjDeclaredDependencyPaths(const fs::path& sourcePath) { std::vector dependencies; std::unordered_set seenPaths; std::ifstream input(sourcePath); if (!input.is_open()) { return dependencies; } const fs::path sourceDirectory = sourcePath.parent_path(); std::string line; while (std::getline(input, line)) { const std::string trimmed = TrimCopy(line); if (trimmed.empty() || trimmed[0] == '#') { continue; } const std::string lowered = ToLowerCopy(trimmed); if (lowered.rfind("mtllib", 0) != 0 || (trimmed.size() > 6 && std::isspace(static_cast(trimmed[6])) == 0)) { continue; } const std::string remainder = TrimCopy(trimmed.substr(6)); for (const std::string& token : SplitWhitespaceTokens(remainder)) { AddUniqueDependencyPath(sourceDirectory / token, seenPaths, dependencies); } } return dependencies; } std::vector CollectMtlDeclaredDependencyPaths(const fs::path& mtlPath) { std::vector dependencies; std::unordered_set seenPaths; std::ifstream input(mtlPath); if (!input.is_open()) { return dependencies; } const fs::path sourceDirectory = mtlPath.parent_path(); std::string line; while (std::getline(input, line)) { const std::string trimmed = TrimCopy(line); if (trimmed.empty() || trimmed[0] == '#') { continue; } const std::vector tokens = SplitWhitespaceTokens(trimmed); if (tokens.size() < 2) { continue; } const std::string keyword = ToLowerCopy(tokens.front()); const bool isTextureDirective = keyword.rfind("map_", 0) == 0 || keyword == "bump" || keyword == "disp" || keyword == "decal" || keyword == "refl" || keyword == "norm"; if (!isTextureDirective) { continue; } const std::string& textureToken = tokens.back(); if (!textureToken.empty() && textureToken[0] != '-') { AddUniqueDependencyPath(sourceDirectory / textureToken, seenPaths, dependencies); } } return dependencies; } std::string EscapeField(const std::string& value) { std::string escaped; escaped.reserve(value.size()); for (const char ch : value) { if (ch == '\\' || ch == '\t' || ch == '\n' || ch == '\r') { escaped.push_back('\\'); switch (ch) { case '\t': escaped.push_back('t'); break; case '\n': escaped.push_back('n'); break; case '\r': escaped.push_back('r'); break; default: escaped.push_back(ch); break; } } else { escaped.push_back(ch); } } return escaped; } std::string UnescapeField(const std::string& value) { std::string result; result.reserve(value.size()); for (size_t index = 0; index < value.size(); ++index) { if (value[index] == '\\' && index + 1 < value.size()) { ++index; switch (value[index]) { case 't': result.push_back('\t'); break; case 'n': result.push_back('\n'); break; case 'r': result.push_back('\r'); break; default: result.push_back(value[index]); break; } } else { result.push_back(value[index]); } } return result; } std::vector SplitFields(const std::string& line) { std::vector fields; std::string current; bool escaping = false; for (const char ch : line) { if (escaping) { current.push_back('\\'); current.push_back(ch); escaping = false; continue; } if (ch == '\\') { escaping = true; continue; } if (ch == '\t') { fields.push_back(UnescapeField(current)); current.clear(); continue; } current.push_back(ch); } if (escaping) { current.push_back('\\'); } fields.push_back(UnescapeField(current)); return fields; } void WriteString(std::ofstream& stream, const Containers::String& value) { const Core::uint32 length = static_cast(value.Length()); stream.write(reinterpret_cast(&length), sizeof(length)); if (length > 0) { stream.write(value.CStr(), length); } } Containers::String ReadString(std::ifstream& stream) { Core::uint32 length = 0; stream.read(reinterpret_cast(&length), sizeof(length)); if (!stream || length == 0) { return Containers::String(); } std::string buffer(length, '\0'); stream.read(buffer.data(), length); if (!stream) { return Containers::String(); } return ToContainersString(buffer); } bool WriteTextureArtifactFile(const fs::path& artifactPath, const Texture& texture) { std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc); if (!output.is_open()) { return false; } TextureArtifactHeader header; header.textureType = static_cast(texture.GetTextureType()); header.textureFormat = static_cast(texture.GetFormat()); header.width = texture.GetWidth(); header.height = texture.GetHeight(); header.depth = texture.GetDepth(); header.mipLevels = texture.GetMipLevels(); header.arraySize = texture.GetArraySize(); header.pixelDataSize = static_cast(texture.GetPixelDataSize()); output.write(reinterpret_cast(&header), sizeof(header)); if (texture.GetPixelDataSize() > 0) { output.write(static_cast(texture.GetPixelData()), texture.GetPixelDataSize()); } return static_cast(output); } std::vector GatherMaterialProperties(const Material& material) { return material.GetProperties(); } Containers::String ResolveTextureBindingPath( const Material& material, Core::uint32 bindingIndex, const std::unordered_map& textureArtifactPaths) { const ResourceHandle textureHandle = material.GetTextureBindingTexture(bindingIndex); const Texture* texture = textureHandle.Get(); if (texture != nullptr) { const auto textureIt = textureArtifactPaths.find(texture); if (textureIt != textureArtifactPaths.end()) { return textureIt->second; } if (!texture->GetPath().Empty()) { return NormalizeArtifactPathString(texture->GetPath()); } } return NormalizeArtifactPathString(material.GetTextureBindingPath(bindingIndex)); } bool WriteMaterialArtifactFile( const fs::path& artifactPath, const Material& material, const std::unordered_map& textureArtifactPaths = {}) { std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc); if (!output.is_open()) { return false; } MaterialArtifactFileHeader fileHeader; output.write(reinterpret_cast(&fileHeader), sizeof(fileHeader)); WriteString(output, material.GetName()); WriteString(output, material.GetPath()); WriteString( output, material.GetShader() != nullptr ? material.GetShader()->GetPath() : Containers::String()); WriteString(output, material.GetShaderPass()); MaterialArtifactHeader header; header.renderQueue = material.GetRenderQueue(); header.renderState = material.GetRenderState(); header.tagCount = material.GetTagCount(); const std::vector properties = GatherMaterialProperties(material); std::vector nonTextureProperties; nonTextureProperties.reserve(properties.size()); for (const MaterialProperty& property : properties) { if (property.type == MaterialPropertyType::Texture || property.type == MaterialPropertyType::Cubemap) { continue; } nonTextureProperties.push_back(property); } header.propertyCount = static_cast(nonTextureProperties.size()); header.textureBindingCount = material.GetTextureBindingCount(); output.write(reinterpret_cast(&header), sizeof(header)); for (Core::uint32 tagIndex = 0; tagIndex < material.GetTagCount(); ++tagIndex) { WriteString(output, material.GetTagName(tagIndex)); WriteString(output, material.GetTagValue(tagIndex)); } for (const MaterialProperty& property : nonTextureProperties) { WriteString(output, property.name); MaterialPropertyArtifact propertyArtifact; propertyArtifact.propertyType = static_cast(property.type); propertyArtifact.value = property.value; output.write(reinterpret_cast(&propertyArtifact), sizeof(propertyArtifact)); } for (Core::uint32 bindingIndex = 0; bindingIndex < material.GetTextureBindingCount(); ++bindingIndex) { const Containers::String bindingName = material.GetTextureBindingName(bindingIndex); WriteString(output, bindingName); WriteString(output, ResolveTextureBindingPath(material, bindingIndex, textureArtifactPaths)); } return static_cast(output); } bool WriteShaderArtifactFile(const fs::path& artifactPath, const Shader& shader) { std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc); if (!output.is_open()) { return false; } ShaderArtifactFileHeader fileHeader; output.write(reinterpret_cast(&fileHeader), sizeof(fileHeader)); WriteString(output, shader.GetName()); WriteString(output, NormalizeArtifactPathString(shader.GetPath())); ShaderArtifactHeader header; header.propertyCount = static_cast(shader.GetProperties().Size()); header.passCount = shader.GetPassCount(); output.write(reinterpret_cast(&header), sizeof(header)); for (const ShaderPropertyDesc& property : shader.GetProperties()) { WriteString(output, property.name); WriteString(output, property.displayName); WriteString(output, property.defaultValue); WriteString(output, property.semantic); ShaderPropertyArtifact propertyArtifact; propertyArtifact.propertyType = static_cast(property.type); output.write(reinterpret_cast(&propertyArtifact), sizeof(propertyArtifact)); } for (const ShaderPass& pass : shader.GetPasses()) { WriteString(output, pass.name); ShaderPassArtifactHeader passHeader; passHeader.tagCount = static_cast(pass.tags.Size()); passHeader.resourceCount = static_cast(pass.resources.Size()); passHeader.variantCount = static_cast(pass.variants.Size()); output.write(reinterpret_cast(&passHeader), sizeof(passHeader)); for (const ShaderPassTagEntry& tag : pass.tags) { WriteString(output, tag.name); WriteString(output, tag.value); } for (const ShaderResourceBindingDesc& resource : pass.resources) { WriteString(output, resource.name); WriteString(output, resource.semantic); ShaderResourceArtifact resourceArtifact; resourceArtifact.resourceType = static_cast(resource.type); resourceArtifact.set = resource.set; resourceArtifact.binding = resource.binding; output.write(reinterpret_cast(&resourceArtifact), sizeof(resourceArtifact)); } for (const ShaderStageVariant& variant : pass.variants) { ShaderVariantArtifactHeader variantHeader; variantHeader.stage = static_cast(variant.stage); variantHeader.language = static_cast(variant.language); variantHeader.backend = static_cast(variant.backend); variantHeader.compiledBinarySize = static_cast(variant.compiledBinary.Size()); output.write(reinterpret_cast(&variantHeader), sizeof(variantHeader)); WriteString(output, variant.entryPoint); WriteString(output, variant.profile); WriteString(output, variant.sourceCode); if (!variant.compiledBinary.Empty()) { output.write( reinterpret_cast(variant.compiledBinary.Data()), static_cast(variant.compiledBinary.Size())); } } } return static_cast(output); } bool WriteMeshArtifactFile(const fs::path& artifactPath, const Mesh& mesh, const std::vector& materialArtifactPaths) { std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc); if (!output.is_open()) { return false; } MeshArtifactHeader header; header.vertexCount = mesh.GetVertexCount(); header.vertexStride = mesh.GetVertexStride(); header.vertexAttributes = static_cast(mesh.GetVertexAttributes()); header.indexCount = mesh.GetIndexCount(); header.use32BitIndex = mesh.IsUse32BitIndex() ? 1u : 0u; header.sectionCount = static_cast(mesh.GetSections().Size()); header.materialCount = static_cast(materialArtifactPaths.size()); header.boundsMin = mesh.GetBounds().GetMin(); header.boundsMax = mesh.GetBounds().GetMax(); header.vertexDataSize = static_cast(mesh.GetVertexDataSize()); header.indexDataSize = static_cast(mesh.GetIndexDataSize()); output.write(reinterpret_cast(&header), sizeof(header)); for (const MeshSection& section : mesh.GetSections()) { output.write(reinterpret_cast(§ion), sizeof(section)); } if (mesh.GetVertexDataSize() > 0) { output.write(static_cast(mesh.GetVertexData()), mesh.GetVertexDataSize()); } if (mesh.GetIndexDataSize() > 0) { output.write(static_cast(mesh.GetIndexData()), mesh.GetIndexDataSize()); } for (const Containers::String& materialArtifactPath : materialArtifactPaths) { WriteString(output, NormalizeArtifactPathString(materialArtifactPath)); } return static_cast(output); } void DestroyImportedMesh(Mesh* mesh) { if (mesh == nullptr) { return; } std::vector materials; materials.reserve(mesh->GetMaterials().Size()); for (Material* material : mesh->GetMaterials()) { if (material != nullptr) { materials.push_back(material); } } std::vector textures; textures.reserve(mesh->GetTextures().Size()); for (Texture* texture : mesh->GetTextures()) { if (texture != nullptr) { textures.push_back(texture); } } delete mesh; for (Material* material : materials) { delete material; } for (Texture* texture : textures) { delete texture; } } } // namespace void AssetDatabase::Initialize(const Containers::String& projectRoot) { m_projectRoot = NormalizePathString(projectRoot); m_assetsRoot = NormalizePathString(fs::path(m_projectRoot.CStr()) / "Assets"); m_libraryRoot = NormalizePathString(fs::path(m_projectRoot.CStr()) / "Library"); m_sourceDbPath = NormalizePathString(fs::path(m_libraryRoot.CStr()) / "SourceAssetDB" / "assets.db"); m_artifactDbPath = NormalizePathString(fs::path(m_libraryRoot.CStr()) / "ArtifactDB" / "artifacts.db"); EnsureProjectLayout(); LoadSourceAssetDB(); LoadArtifactDB(); ScanAssets(); SaveArtifactDB(); } void AssetDatabase::Shutdown() { SaveSourceAssetDB(); SaveArtifactDB(); m_projectRoot.Clear(); m_assetsRoot.Clear(); m_libraryRoot.Clear(); m_sourceDbPath.Clear(); m_artifactDbPath.Clear(); m_sourcesByPathKey.clear(); m_sourcesByGuid.clear(); m_artifactsByGuid.clear(); } void AssetDatabase::Refresh() { ScanAssets(); } bool AssetDatabase::ResolvePath(const Containers::String& requestPath, Containers::String& outAbsolutePath, Containers::String& outRelativePath) const { if (requestPath.Empty()) { return false; } if (HasVirtualPathScheme(requestPath)) { return false; } fs::path inputPath(requestPath.CStr()); if (inputPath.is_absolute()) { outAbsolutePath = NormalizePathString(inputPath); std::error_code ec; const fs::path projectRootPath(m_projectRoot.CStr()); const fs::path relativePath = fs::relative(inputPath, projectRootPath, ec); if (!ec) { const Containers::String normalizedRelative = NormalizePathString(relativePath); if (normalizedRelative.StartsWith("Assets/") || normalizedRelative == "Assets") { outRelativePath = normalizedRelative; } else { outRelativePath.Clear(); } } else { outRelativePath.Clear(); } return true; } const Containers::String normalizedRequest = NormalizePathString(requestPath); if (normalizedRequest.StartsWith("Assets/") || normalizedRequest == "Assets") { outRelativePath = normalizedRequest; outAbsolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / normalizedRequest.CStr()); return true; } outAbsolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / requestPath.CStr()); outRelativePath.Clear(); return true; } bool AssetDatabase::TryGetAssetGuid(const Containers::String& requestPath, AssetGUID& outGuid) const { Containers::String absolutePath; Containers::String relativePath; if (!ResolvePath(requestPath, absolutePath, relativePath) || relativePath.Empty()) { return false; } const auto sourceIt = m_sourcesByPathKey.find(ToStdString(MakeKey(relativePath))); if (sourceIt == m_sourcesByPathKey.end()) { return false; } outGuid = sourceIt->second.guid; return outGuid.IsValid(); } bool AssetDatabase::TryGetAssetRef(const Containers::String& requestPath, ResourceType resourceType, AssetRef& outRef) const { AssetGUID guid; if (!TryGetAssetGuid(requestPath, guid)) { return false; } outRef.assetGuid = guid; outRef.localID = kMainAssetLocalID; outRef.resourceType = resourceType; return true; } bool AssetDatabase::TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::String& outRelativePath) const { const auto sourceIt = m_sourcesByGuid.find(guid); if (sourceIt == m_sourcesByGuid.end()) { return false; } outRelativePath = sourceIt->second.relativePath; return true; } void AssetDatabase::BuildLookupSnapshot(std::unordered_map& outPathToGuid, std::unordered_map& outGuidToPath) const { outPathToGuid.clear(); outGuidToPath.clear(); outPathToGuid.reserve(m_sourcesByPathKey.size()); outGuidToPath.reserve(m_sourcesByGuid.size()); for (const auto& [pathKey, record] : m_sourcesByPathKey) { if (!record.guid.IsValid() || record.relativePath.Empty()) { continue; } outPathToGuid[pathKey] = record.guid; } for (const auto& [guid, record] : m_sourcesByGuid) { if (!guid.IsValid() || record.relativePath.Empty()) { continue; } outGuidToPath[guid] = record.relativePath; } } void AssetDatabase::EnsureProjectLayout() { std::error_code ec; fs::create_directories(fs::path(m_assetsRoot.CStr()), ec); ec.clear(); fs::create_directories(fs::path(m_libraryRoot.CStr()) / "SourceAssetDB", ec); ec.clear(); fs::create_directories(fs::path(m_libraryRoot.CStr()) / "ArtifactDB", ec); ec.clear(); fs::create_directories(fs::path(m_libraryRoot.CStr()) / "Artifacts", ec); } void AssetDatabase::LoadSourceAssetDB() { m_sourcesByPathKey.clear(); m_sourcesByGuid.clear(); std::ifstream input(m_sourceDbPath.CStr()); if (!input.is_open()) { return; } std::string line; while (std::getline(input, line)) { if (line.empty() || line[0] == '#') { continue; } const std::vector fields = SplitFields(line); if (fields.size() < 10) { continue; } SourceAssetRecord record; record.guid = AssetGUID::ParseOrDefault(ToContainersString(fields[0])); record.relativePath = ToContainersString(fields[1]); record.metaPath = ToContainersString(fields[2]); record.isFolder = (fields[3] == "1"); record.importerName = ToContainersString(fields[4]); record.importerVersion = static_cast(std::stoul(fields[5])); record.metaHash = ToContainersString(fields[6]); record.sourceHash = ToContainersString(fields[7]); record.sourceFileSize = static_cast(std::stoull(fields[8])); record.sourceWriteTime = static_cast(std::stoull(fields[9])); if (fields.size() > 10) { record.lastKnownArtifactKey = ToContainersString(fields[10]); } if (!record.guid.IsValid() || record.relativePath.Empty()) { continue; } m_sourcesByGuid[record.guid] = record; m_sourcesByPathKey[ToStdString(MakeKey(record.relativePath))] = record; } } void AssetDatabase::SaveSourceAssetDB() const { std::ofstream output(m_sourceDbPath.CStr(), std::ios::out | std::ios::trunc); if (!output.is_open()) { return; } output << "# guid\trelativePath\tmetaPath\tisFolder\timporter\timporterVersion\tmetaHash\tsourceHash\tsize\twriteTime\tartifactKey\n"; for (const auto& [guid, record] : m_sourcesByGuid) { output << EscapeField(ToStdString(record.guid.ToString())) << '\t' << EscapeField(ToStdString(record.relativePath)) << '\t' << EscapeField(ToStdString(record.metaPath)) << '\t' << (record.isFolder ? "1" : "0") << '\t' << EscapeField(ToStdString(record.importerName)) << '\t' << record.importerVersion << '\t' << EscapeField(ToStdString(record.metaHash)) << '\t' << EscapeField(ToStdString(record.sourceHash)) << '\t' << record.sourceFileSize << '\t' << record.sourceWriteTime << '\t' << EscapeField(ToStdString(record.lastKnownArtifactKey)) << '\n'; } } void AssetDatabase::LoadArtifactDB() { m_artifactsByGuid.clear(); std::ifstream input(m_artifactDbPath.CStr()); if (!input.is_open()) { return; } std::string line; while (std::getline(input, line)) { if (line.empty() || line[0] == '#') { continue; } const std::vector fields = SplitFields(line); if (fields.size() < 10) { continue; } ArtifactRecord record; record.artifactKey = ToContainersString(fields[0]); record.assetGuid = AssetGUID::ParseOrDefault(ToContainersString(fields[1])); record.importerName = ToContainersString(fields[2]); record.importerVersion = static_cast(std::stoul(fields[3])); record.resourceType = static_cast(std::stoul(fields[4])); record.artifactDirectory = ToContainersString(fields[5]); record.mainArtifactPath = ToContainersString(fields[6]); record.sourceHash = ToContainersString(fields[7]); record.metaHash = ToContainersString(fields[8]); record.sourceFileSize = static_cast(std::stoull(fields[9])); record.sourceWriteTime = fields.size() > 10 ? static_cast(std::stoull(fields[10])) : 0; record.mainLocalID = fields.size() > 11 ? static_cast(std::stoull(fields[11])) : kMainAssetLocalID; for (size_t index = 12; index + 3 < fields.size(); index += 4) { ArtifactDependencyRecord dependency; dependency.path = ToContainersString(fields[index + 0]); dependency.hash = ToContainersString(fields[index + 1]); dependency.fileSize = static_cast(std::stoull(fields[index + 2])); dependency.writeTime = static_cast(std::stoull(fields[index + 3])); if (!dependency.path.Empty()) { record.dependencies.push_back(dependency); } } if (!record.assetGuid.IsValid() || record.artifactKey.Empty()) { continue; } m_artifactsByGuid[record.assetGuid] = record; } } void AssetDatabase::SaveArtifactDB() const { std::ofstream output(m_artifactDbPath.CStr(), std::ios::out | std::ios::trunc); if (!output.is_open()) { return; } output << "# artifactKey\tassetGuid\timporter\tversion\ttype\tartifactDir\tmainArtifact\tsourceHash\tmetaHash\tsize\twriteTime\tmainLocalID\t(depPath\tdepHash\tdepSize\tdepWriteTime)...\n"; for (const auto& [guid, record] : m_artifactsByGuid) { output << EscapeField(ToStdString(record.artifactKey)) << '\t' << EscapeField(ToStdString(record.assetGuid.ToString())) << '\t' << EscapeField(ToStdString(record.importerName)) << '\t' << record.importerVersion << '\t' << static_cast(record.resourceType) << '\t' << EscapeField(ToStdString(record.artifactDirectory)) << '\t' << EscapeField(ToStdString(record.mainArtifactPath)) << '\t' << EscapeField(ToStdString(record.sourceHash)) << '\t' << EscapeField(ToStdString(record.metaHash)) << '\t' << record.sourceFileSize << '\t' << record.sourceWriteTime << '\t' << record.mainLocalID; for (const ArtifactDependencyRecord& dependency : record.dependencies) { output << '\t' << EscapeField(ToStdString(dependency.path)) << '\t' << EscapeField(ToStdString(dependency.hash)) << '\t' << dependency.fileSize << '\t' << dependency.writeTime; } output << '\n'; } } void AssetDatabase::ScanAssets() { std::unordered_map seenPaths; const fs::path assetsRootPath(m_assetsRoot.CStr()); if (fs::exists(assetsRootPath)) { ScanAssetPath(assetsRootPath, seenPaths); } RemoveMissingRecords(seenPaths); SaveSourceAssetDB(); } void AssetDatabase::ScanAssetPath(const fs::path& path, std::unordered_map& seenPaths) { if (!fs::exists(path)) { return; } if (path.has_extension() && ToLowerCopy(path.extension().string()) == ".meta") { return; } const bool isFolder = fs::is_directory(path); SourceAssetRecord record; if (EnsureMetaForPath(path, isFolder, record)) { seenPaths[ToStdString(MakeKey(record.relativePath))] = true; } if (!isFolder) { return; } for (const auto& entry : fs::directory_iterator(path)) { ScanAssetPath(entry.path(), seenPaths); } } void AssetDatabase::RemoveMissingRecords(const std::unordered_map& seenPaths) { std::vector missingPathKeys; for (const auto& [pathKey, record] : m_sourcesByPathKey) { if (seenPaths.find(pathKey) == seenPaths.end()) { missingPathKeys.push_back(pathKey); } } for (const std::string& pathKey : missingPathKeys) { auto recordIt = m_sourcesByPathKey.find(pathKey); if (recordIt == m_sourcesByPathKey.end()) { continue; } m_artifactsByGuid.erase(recordIt->second.guid); m_sourcesByGuid.erase(recordIt->second.guid); m_sourcesByPathKey.erase(recordIt); } if (!missingPathKeys.empty()) { SaveArtifactDB(); } } bool AssetDatabase::EnsureMetaForPath(const fs::path& sourcePath, bool isFolder, SourceAssetRecord& outRecord) { const Containers::String relativePath = NormalizeRelativePath(sourcePath); if (relativePath.Empty()) { return false; } const std::string pathKey = ToStdString(MakeKey(relativePath)); auto existingIt = m_sourcesByPathKey.find(pathKey); if (existingIt != m_sourcesByPathKey.end()) { outRecord = existingIt->second; } else { outRecord = SourceAssetRecord(); outRecord.relativePath = relativePath; outRecord.importerName = GetImporterNameForPath(relativePath, isFolder); outRecord.importerVersion = kCurrentImporterVersion; } outRecord.relativePath = relativePath; outRecord.isFolder = isFolder; outRecord.importerName = GetImporterNameForPath(relativePath, isFolder); outRecord.importerVersion = kCurrentImporterVersion; const fs::path metaPath(sourcePath.string() + ".meta"); outRecord.metaPath = NormalizeRelativePath(metaPath); bool shouldRewriteMeta = false; if (!fs::exists(metaPath) || !ReadMetaFile(metaPath, outRecord) || !outRecord.guid.IsValid()) { if (!outRecord.guid.IsValid()) { outRecord.guid = AssetGUID::Generate(); } shouldRewriteMeta = true; } if (outRecord.importerVersion != kCurrentImporterVersion) { outRecord.importerVersion = kCurrentImporterVersion; shouldRewriteMeta = true; } const auto duplicateGuidIt = m_sourcesByGuid.find(outRecord.guid); if (duplicateGuidIt != m_sourcesByGuid.end() && duplicateGuidIt->second.relativePath != relativePath) { outRecord.guid = AssetGUID::Generate(); shouldRewriteMeta = true; } if (shouldRewriteMeta) { WriteMetaFile(metaPath, outRecord); } outRecord.metaHash = HashStringToAssetGUID(ReadWholeFileText(metaPath)).ToString(); if (isFolder) { outRecord.sourceHash.Clear(); outRecord.sourceFileSize = 0; outRecord.sourceWriteTime = 0; } else { const Core::uint64 fileSize = GetFileSizeValue(sourcePath); const Core::uint64 writeTime = GetFileWriteTimeValue(sourcePath); if (existingIt != m_sourcesByPathKey.end() && existingIt->second.sourceFileSize == fileSize && existingIt->second.sourceWriteTime == writeTime && !existingIt->second.sourceHash.Empty()) { outRecord.sourceHash = existingIt->second.sourceHash; } else { outRecord.sourceHash = ComputeFileHash(sourcePath); } outRecord.sourceFileSize = fileSize; outRecord.sourceWriteTime = writeTime; } m_sourcesByPathKey[pathKey] = outRecord; m_sourcesByGuid[outRecord.guid] = outRecord; return true; } bool AssetDatabase::ReadMetaFile(const fs::path& metaPath, SourceAssetRecord& inOutRecord) const { std::ifstream input(metaPath); if (!input.is_open()) { return false; } std::string line; while (std::getline(input, line)) { const size_t colonPos = line.find(':'); if (colonPos == std::string::npos) { continue; } const std::string key = TrimCopy(line.substr(0, colonPos)); const std::string value = TrimCopy(line.substr(colonPos + 1)); if (key == "guid") { inOutRecord.guid = AssetGUID::ParseOrDefault(ToContainersString(value)); } else if (key == "folderAsset") { inOutRecord.isFolder = ToLowerCopy(value) == "true"; } else if (key == "importer") { inOutRecord.importerName = ToContainersString(value); } else if (key == "importerVersion") { inOutRecord.importerVersion = static_cast(std::stoul(value)); } } if (inOutRecord.importerName.Empty()) { inOutRecord.importerName = GetImporterNameForPath(inOutRecord.relativePath, inOutRecord.isFolder); } if (inOutRecord.importerVersion == 0) { inOutRecord.importerVersion = kCurrentImporterVersion; } return true; } void AssetDatabase::WriteMetaFile(const fs::path& metaPath, const SourceAssetRecord& record) const { std::ofstream output(metaPath, std::ios::out | std::ios::trunc); if (!output.is_open()) { return; } output << "fileFormatVersion: 1\n"; output << "guid: " << record.guid.ToString().CStr() << "\n"; output << "folderAsset: " << (record.isFolder ? "true" : "false") << "\n"; output << "importer: " << record.importerName.CStr() << "\n"; output << "importerVersion: " << record.importerVersion << "\n"; } Containers::String AssetDatabase::NormalizeRelativePath(const fs::path& sourcePath) const { std::error_code ec; const fs::path projectRootPath(m_projectRoot.CStr()); const fs::path relativePath = fs::relative(sourcePath, projectRootPath, ec); if (ec) { return Containers::String(); } return NormalizePathString(relativePath); } Containers::String AssetDatabase::NormalizePathString(const fs::path& path) { return ToContainersString(path.lexically_normal().generic_string()); } Containers::String AssetDatabase::NormalizePathString(const Containers::String& path) { return NormalizePathString(fs::path(path.CStr())); } Containers::String AssetDatabase::MakeKey(const Containers::String& path) { std::string key = ToStdString(NormalizePathString(path)); std::transform(key.begin(), key.end(), key.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); return ToContainersString(key); } Containers::String AssetDatabase::GetImporterNameForPath(const Containers::String& relativePath, bool isFolder) { if (isFolder) { return Containers::String("FolderImporter"); } const std::string ext = ToLowerCopy(fs::path(relativePath.CStr()).extension().string()); if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp" || ext == ".tga" || ext == ".gif" || ext == ".hdr") { return Containers::String("TextureImporter"); } if (ext == ".obj" || ext == ".fbx" || ext == ".gltf" || ext == ".glb" || ext == ".dae" || ext == ".stl") { return Containers::String("ModelImporter"); } if (ext == ".shader" || ext == ".hlsl" || ext == ".glsl" || ext == ".vert" || ext == ".frag" || ext == ".geom" || ext == ".comp") { return Containers::String("ShaderImporter"); } if (ext == ".mat" || ext == ".material" || ext == ".json") { return Containers::String("MaterialImporter"); } return Containers::String("DefaultImporter"); } ResourceType AssetDatabase::GetPrimaryResourceTypeForImporter(const Containers::String& importerName) { if (importerName == "TextureImporter") { return ResourceType::Texture; } if (importerName == "ModelImporter") { return ResourceType::Mesh; } if (importerName == "MaterialImporter") { return ResourceType::Material; } if (importerName == "ShaderImporter") { return ResourceType::Shader; } return ResourceType::Unknown; } bool AssetDatabase::ShouldReimport(const SourceAssetRecord& sourceRecord, const ArtifactRecord* artifactRecord) const { if (artifactRecord == nullptr) { return true; } if (artifactRecord->artifactKey.Empty() || artifactRecord->mainArtifactPath.Empty()) { return true; } const fs::path artifactMainPath = fs::path(m_projectRoot.CStr()) / artifactRecord->mainArtifactPath.CStr(); if (!fs::exists(artifactMainPath)) { return true; } return artifactRecord->importerVersion != sourceRecord.importerVersion || artifactRecord->sourceHash != sourceRecord.sourceHash || artifactRecord->metaHash != sourceRecord.metaHash || artifactRecord->sourceFileSize != sourceRecord.sourceFileSize || artifactRecord->sourceWriteTime != sourceRecord.sourceWriteTime || !AreDependenciesCurrent(artifactRecord->dependencies); } bool AssetDatabase::ImportAsset(const SourceAssetRecord& sourceRecord, ArtifactRecord& outRecord) { const ResourceType primaryType = GetPrimaryResourceTypeForImporter(sourceRecord.importerName); switch (primaryType) { case ResourceType::Texture: return ImportTextureAsset(sourceRecord, outRecord); case ResourceType::Material: return ImportMaterialAsset(sourceRecord, outRecord); case ResourceType::Mesh: return ImportModelAsset(sourceRecord, outRecord); case ResourceType::Shader: return ImportShaderAsset(sourceRecord, outRecord); default: return false; } } bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath, ResourceType requestedType, ResolvedAsset& outAsset) { outAsset = ResolvedAsset(); Containers::String absolutePath; Containers::String relativePath; if (!ResolvePath(requestPath, absolutePath, relativePath) || relativePath.Empty()) { if (ShouldTraceAssetPath(requestPath)) { Debug::Logger::Get().Info( Debug::LogCategory::FileSystem, Containers::String("[AssetDatabase] EnsureArtifact unresolved path=") + requestPath); } return false; } const fs::path absoluteFsPath(absolutePath.CStr()); if (!fs::exists(absoluteFsPath)) { if (ShouldTraceAssetPath(requestPath)) { Debug::Logger::Get().Info( Debug::LogCategory::FileSystem, Containers::String("[AssetDatabase] EnsureArtifact missing source path=") + requestPath + " absolute=" + absolutePath); } return false; } SourceAssetRecord sourceRecord; if (!EnsureMetaForPath(absoluteFsPath, fs::is_directory(absoluteFsPath), sourceRecord)) { return false; } if (ShouldTraceAssetPath(requestPath)) { Debug::Logger::Get().Info( Debug::LogCategory::FileSystem, Containers::String("[AssetDatabase] EnsureArtifact source path=") + requestPath + " guid=" + sourceRecord.guid.ToString() + " importer=" + sourceRecord.importerName + " relative=" + sourceRecord.relativePath); } const ResourceType primaryType = GetPrimaryResourceTypeForImporter(sourceRecord.importerName); if (primaryType == ResourceType::Unknown || primaryType != requestedType) { if (ShouldTraceAssetPath(requestPath)) { Debug::Logger::Get().Info( Debug::LogCategory::FileSystem, Containers::String("[AssetDatabase] EnsureArtifact type-mismatch path=") + requestPath + " requested=" + GetResourceTypeName(requestedType) + " importerType=" + GetResourceTypeName(primaryType)); } return false; } ArtifactRecord* artifactRecord = nullptr; auto artifactIt = m_artifactsByGuid.find(sourceRecord.guid); if (artifactIt != m_artifactsByGuid.end()) { artifactRecord = &artifactIt->second; } if (ShouldReimport(sourceRecord, artifactRecord)) { if (ShouldTraceAssetPath(requestPath)) { Debug::Logger::Get().Info( Debug::LogCategory::FileSystem, Containers::String("[AssetDatabase] EnsureArtifact reimport path=") + requestPath); } ArtifactRecord rebuiltRecord; if (!ImportAsset(sourceRecord, rebuiltRecord)) { if (ShouldTraceAssetPath(requestPath)) { Debug::Logger::Get().Error( Debug::LogCategory::FileSystem, Containers::String("[AssetDatabase] EnsureArtifact reimport failed path=") + requestPath); } return false; } m_artifactsByGuid[sourceRecord.guid] = rebuiltRecord; m_sourcesByGuid[sourceRecord.guid].lastKnownArtifactKey = rebuiltRecord.artifactKey; m_sourcesByPathKey[ToStdString(MakeKey(sourceRecord.relativePath))].lastKnownArtifactKey = rebuiltRecord.artifactKey; SaveArtifactDB(); SaveSourceAssetDB(); artifactRecord = &m_artifactsByGuid[sourceRecord.guid]; } if (artifactRecord == nullptr) { return false; } outAsset.exists = true; outAsset.artifactReady = true; outAsset.absolutePath = absolutePath; outAsset.relativePath = sourceRecord.relativePath; outAsset.assetGuid = sourceRecord.guid; outAsset.resourceType = artifactRecord->resourceType; outAsset.artifactDirectory = NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactRecord->artifactDirectory.CStr()); outAsset.artifactMainPath = NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactRecord->mainArtifactPath.CStr()); outAsset.mainLocalID = artifactRecord->mainLocalID; if (ShouldTraceAssetPath(requestPath)) { Debug::Logger::Get().Info( Debug::LogCategory::FileSystem, Containers::String("[AssetDatabase] EnsureArtifact ready path=") + requestPath + " artifactKey=" + artifactRecord->artifactKey + " artifact=" + outAsset.artifactMainPath); } return true; } bool AssetDatabase::ImportTextureAsset(const SourceAssetRecord& sourceRecord, ArtifactRecord& outRecord) { TextureLoader loader; const Containers::String absolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr()); LoadResult result = loader.Load(absolutePath); if (!result || result.resource == nullptr) { return false; } Texture* texture = static_cast(result.resource); const Containers::String artifactKey = BuildArtifactKey(sourceRecord); const Containers::String artifactDir = BuildArtifactDirectory(artifactKey); const Containers::String mainArtifactPath = NormalizePathString(fs::path(artifactDir.CStr()) / "main.xctex"); std::error_code ec; fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); ec.clear(); fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); if (ec) { delete texture; return false; } const bool writeOk = WriteTextureArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *texture); delete texture; if (!writeOk) { return false; } outRecord.artifactKey = artifactKey; outRecord.assetGuid = sourceRecord.guid; outRecord.importerName = sourceRecord.importerName; outRecord.importerVersion = sourceRecord.importerVersion; outRecord.resourceType = ResourceType::Texture; outRecord.artifactDirectory = artifactDir; outRecord.mainArtifactPath = mainArtifactPath; outRecord.sourceHash = sourceRecord.sourceHash; outRecord.metaHash = sourceRecord.metaHash; outRecord.sourceFileSize = sourceRecord.sourceFileSize; outRecord.sourceWriteTime = sourceRecord.sourceWriteTime; outRecord.mainLocalID = kMainAssetLocalID; outRecord.dependencies.clear(); return true; } bool AssetDatabase::ImportMaterialAsset(const SourceAssetRecord& sourceRecord, ArtifactRecord& outRecord) { MaterialLoader loader; const Containers::String absolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr()); LoadResult result = loader.Load(absolutePath); if (!result || result.resource == nullptr) { return false; } Material* material = static_cast(result.resource); std::vector dependencies; CollectMaterialDependencies(*material, dependencies); const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies); const Containers::String artifactDir = BuildArtifactDirectory(artifactKey); const Containers::String mainArtifactPath = NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmat"); std::error_code ec; fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); ec.clear(); fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); if (ec) { delete material; return false; } const bool writeOk = WriteMaterialArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *material); delete material; if (!writeOk) { return false; } outRecord.artifactKey = artifactKey; outRecord.assetGuid = sourceRecord.guid; outRecord.importerName = sourceRecord.importerName; outRecord.importerVersion = sourceRecord.importerVersion; outRecord.resourceType = ResourceType::Material; outRecord.artifactDirectory = artifactDir; outRecord.mainArtifactPath = mainArtifactPath; outRecord.sourceHash = sourceRecord.sourceHash; outRecord.metaHash = sourceRecord.metaHash; outRecord.sourceFileSize = sourceRecord.sourceFileSize; outRecord.sourceWriteTime = sourceRecord.sourceWriteTime; outRecord.mainLocalID = kMainAssetLocalID; outRecord.dependencies = std::move(dependencies); return true; } bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord, ArtifactRecord& outRecord) { MeshLoader loader; const Containers::String absolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr()); LoadResult result = loader.Load(absolutePath); if (!result || result.resource == nullptr) { return false; } Mesh* mesh = static_cast(result.resource); std::vector dependencies; CollectModelDependencies(sourceRecord, *mesh, dependencies); const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies); const Containers::String artifactDir = BuildArtifactDirectory(artifactKey); const Containers::String mainArtifactPath = NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmesh"); std::error_code ec; fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); ec.clear(); fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); if (ec) { DestroyImportedMesh(mesh); return false; } bool writeOk = true; std::unordered_map textureArtifactPaths; for (size_t textureIndex = 0; writeOk && textureIndex < mesh->GetTextures().Size(); ++textureIndex) { Texture* texture = mesh->GetTextures()[textureIndex]; if (texture == nullptr) { continue; } const Containers::String textureArtifactPath = NormalizePathString(fs::path(artifactDir.CStr()) / ("texture_" + std::to_string(textureIndex) + ".xctex")); writeOk = WriteTextureArtifactFile( fs::path(m_projectRoot.CStr()) / textureArtifactPath.CStr(), *texture); if (!writeOk) { break; } textureArtifactPaths.emplace(texture, textureArtifactPath); } std::vector materialArtifactPaths; materialArtifactPaths.reserve(mesh->GetMaterials().Size()); for (size_t materialIndex = 0; writeOk && materialIndex < mesh->GetMaterials().Size(); ++materialIndex) { Material* material = mesh->GetMaterials()[materialIndex]; if (material == nullptr) { materialArtifactPaths.emplace_back(); continue; } const Containers::String materialArtifactPath = NormalizePathString(fs::path(artifactDir.CStr()) / ("material_" + std::to_string(materialIndex) + ".xcmat")); writeOk = WriteMaterialArtifactFile( fs::path(m_projectRoot.CStr()) / materialArtifactPath.CStr(), *material, textureArtifactPaths); if (!writeOk) { break; } materialArtifactPaths.push_back(materialArtifactPath); } writeOk = writeOk && WriteMeshArtifactFile( fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *mesh, materialArtifactPaths); DestroyImportedMesh(mesh); if (!writeOk) { return false; } outRecord.artifactKey = artifactKey; outRecord.assetGuid = sourceRecord.guid; outRecord.importerName = sourceRecord.importerName; outRecord.importerVersion = sourceRecord.importerVersion; outRecord.resourceType = ResourceType::Mesh; outRecord.artifactDirectory = artifactDir; outRecord.mainArtifactPath = mainArtifactPath; outRecord.sourceHash = sourceRecord.sourceHash; outRecord.metaHash = sourceRecord.metaHash; outRecord.sourceFileSize = sourceRecord.sourceFileSize; outRecord.sourceWriteTime = sourceRecord.sourceWriteTime; outRecord.mainLocalID = kMainAssetLocalID; outRecord.dependencies = std::move(dependencies); return true; } bool AssetDatabase::ImportShaderAsset(const SourceAssetRecord& sourceRecord, ArtifactRecord& outRecord) { ShaderLoader loader; const Containers::String absolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr()); LoadResult result = loader.Load(absolutePath); if (!result || result.resource == nullptr) { return false; } Shader* shader = static_cast(result.resource); std::vector dependencies; if (!CollectShaderDependencies(sourceRecord, dependencies)) { delete shader; return false; } const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies); const Containers::String artifactDir = BuildArtifactDirectory(artifactKey); const Containers::String mainArtifactPath = NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcshader"); std::error_code ec; fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); ec.clear(); fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); if (ec) { delete shader; return false; } const bool writeOk = WriteShaderArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *shader); delete shader; if (!writeOk) { return false; } outRecord.artifactKey = artifactKey; outRecord.assetGuid = sourceRecord.guid; outRecord.importerName = sourceRecord.importerName; outRecord.importerVersion = sourceRecord.importerVersion; outRecord.resourceType = ResourceType::Shader; outRecord.artifactDirectory = artifactDir; outRecord.mainArtifactPath = mainArtifactPath; outRecord.sourceHash = sourceRecord.sourceHash; outRecord.metaHash = sourceRecord.metaHash; outRecord.sourceFileSize = sourceRecord.sourceFileSize; outRecord.sourceWriteTime = sourceRecord.sourceWriteTime; outRecord.mainLocalID = kMainAssetLocalID; outRecord.dependencies = std::move(dependencies); return true; } Containers::String AssetDatabase::BuildArtifactKey( const AssetDatabase::SourceAssetRecord& sourceRecord, const std::vector& dependencies) const { Containers::String signature = sourceRecord.guid.ToString(); signature += ":"; signature += sourceRecord.importerName; signature += ":"; signature += Containers::String(std::to_string(sourceRecord.importerVersion).c_str()); signature += ":"; signature += sourceRecord.sourceHash; signature += ":"; signature += sourceRecord.metaHash; signature += ":"; signature += Containers::String(std::to_string(sourceRecord.sourceFileSize).c_str()); signature += ":"; signature += Containers::String(std::to_string(sourceRecord.sourceWriteTime).c_str()); for (const ArtifactDependencyRecord& dependency : dependencies) { signature += ":dep:"; signature += dependency.path; signature += ":"; signature += dependency.hash; signature += ":"; signature += Containers::String(std::to_string(dependency.fileSize).c_str()); signature += ":"; signature += Containers::String(std::to_string(dependency.writeTime).c_str()); } return HashStringToAssetGUID(signature).ToString(); } Containers::String AssetDatabase::NormalizeDependencyPath(const fs::path& path) const { const fs::path normalizedPath = path.lexically_normal(); if (normalizedPath.empty()) { return Containers::String(); } if (normalizedPath.is_absolute() && !m_projectRoot.Empty()) { std::error_code ec; const fs::path relativePath = fs::relative(normalizedPath, fs::path(m_projectRoot.CStr()), ec); if (!ec && IsProjectRelativePath(relativePath)) { return ToContainersString(relativePath.generic_string()); } } return ToContainersString(normalizedPath.generic_string()); } fs::path AssetDatabase::ResolveDependencyPath(const Containers::String& path) const { if (path.Empty()) { return fs::path(); } fs::path dependencyPath(path.CStr()); if (dependencyPath.is_absolute()) { return dependencyPath.lexically_normal(); } return (fs::path(m_projectRoot.CStr()) / dependencyPath).lexically_normal(); } bool AssetDatabase::CaptureDependencyRecord(const fs::path& path, AssetDatabase::ArtifactDependencyRecord& outRecord) const { const Containers::String normalizedPath = NormalizeDependencyPath(path); if (normalizedPath.Empty()) { return false; } outRecord = ArtifactDependencyRecord(); outRecord.path = normalizedPath; const fs::path resolvedPath = ResolveDependencyPath(normalizedPath); if (!resolvedPath.empty() && fs::exists(resolvedPath)) { outRecord.hash = ComputeFileHash(resolvedPath); outRecord.fileSize = GetFileSizeValue(resolvedPath); outRecord.writeTime = GetFileWriteTimeValue(resolvedPath); } return true; } bool AssetDatabase::AreDependenciesCurrent( const std::vector& dependencies) const { for (const AssetDatabase::ArtifactDependencyRecord& dependency : dependencies) { if (dependency.path.Empty()) { return false; } const fs::path resolvedPath = ResolveDependencyPath(dependency.path); if (resolvedPath.empty() || !fs::exists(resolvedPath)) { if (!dependency.hash.Empty() || dependency.fileSize != 0 || dependency.writeTime != 0) { return false; } continue; } const Core::uint64 currentFileSize = GetFileSizeValue(resolvedPath); const Core::uint64 currentWriteTime = GetFileWriteTimeValue(resolvedPath); if (currentFileSize != dependency.fileSize || currentWriteTime != dependency.writeTime) { return false; } if (ComputeFileHash(resolvedPath) != dependency.hash) { return false; } } return true; } bool AssetDatabase::CollectModelDependencies(const AssetDatabase::SourceAssetRecord& sourceRecord, const Mesh& mesh, std::vector& outDependencies) const { outDependencies.clear(); std::unordered_set seenDependencyPaths; std::vector candidatePaths; const fs::path sourcePath = fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr(); const fs::path normalizedSourcePath = sourcePath.lexically_normal(); if (ToLowerCopy(normalizedSourcePath.extension().string()) == ".obj") { const std::vector mtlPaths = CollectObjDeclaredDependencyPaths(normalizedSourcePath); for (const fs::path& mtlPath : mtlPaths) { AddUniqueDependencyPath(mtlPath, seenDependencyPaths, candidatePaths); const std::vector texturePaths = CollectMtlDeclaredDependencyPaths(mtlPath); for (const fs::path& texturePath : texturePaths) { AddUniqueDependencyPath(texturePath, seenDependencyPaths, candidatePaths); } } } for (Texture* texture : mesh.GetTextures()) { if (texture == nullptr || texture->GetPath().Empty()) { continue; } const std::string texturePath = ToStdString(texture->GetPath()); if (texturePath.find('#') != std::string::npos) { continue; } AddUniqueDependencyPath(fs::path(texturePath), seenDependencyPaths, candidatePaths); } const std::string sourcePathKey = normalizedSourcePath.generic_string(); for (const fs::path& candidatePath : candidatePaths) { if (candidatePath.generic_string() == sourcePathKey) { continue; } ArtifactDependencyRecord dependency; if (CaptureDependencyRecord(candidatePath, dependency)) { outDependencies.push_back(dependency); } } return true; } bool AssetDatabase::CollectMaterialDependencies( const Material& material, std::vector& outDependencies) const { outDependencies.clear(); std::unordered_set seenDependencyPaths; if (material.GetShader() != nullptr) { const Containers::String shaderPath = material.GetShader()->GetPath(); if (!shaderPath.Empty() && !HasVirtualPathScheme(shaderPath)) { ArtifactDependencyRecord dependency; if (CaptureDependencyRecord(ResolveDependencyPath(shaderPath), dependency)) { const std::string dependencyKey = ToStdString(dependency.path); if (seenDependencyPaths.insert(dependencyKey).second) { outDependencies.push_back(dependency); } } } } for (Core::uint32 bindingIndex = 0; bindingIndex < material.GetTextureBindingCount(); ++bindingIndex) { const Containers::String texturePath = material.GetTextureBindingPath(bindingIndex); if (texturePath.Empty()) { continue; } ArtifactDependencyRecord dependency; if (!CaptureDependencyRecord(ResolveDependencyPath(texturePath), dependency)) { continue; } const std::string dependencyKey = ToStdString(dependency.path); if (seenDependencyPaths.insert(dependencyKey).second) { outDependencies.push_back(dependency); } } return true; } bool AssetDatabase::CollectShaderDependencies( const SourceAssetRecord& sourceRecord, std::vector& outDependencies) const { outDependencies.clear(); ShaderLoader loader; const Containers::String absolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr()); Containers::Array dependencyPaths; if (!loader.CollectSourceDependencies(absolutePath, dependencyPaths)) { return false; } std::unordered_set seenDependencyPaths; for (const Containers::String& dependencyPath : dependencyPaths) { if (dependencyPath.Empty() || HasVirtualPathScheme(dependencyPath)) { continue; } ArtifactDependencyRecord dependency; if (!CaptureDependencyRecord(ResolveDependencyPath(dependencyPath), dependency)) { continue; } const std::string dependencyKey = ToStdString(dependency.path); if (seenDependencyPaths.insert(dependencyKey).second) { outDependencies.push_back(dependency); } } return true; } Containers::String AssetDatabase::BuildArtifactDirectory(const Containers::String& artifactKey) const { if (artifactKey.Length() < 2) { return Containers::String("Library/Artifacts/00/invalid"); } const Containers::String shard = artifactKey.Substring(0, 2); return Containers::String("Library/Artifacts/") + shard + "/" + artifactKey; } Containers::String AssetDatabase::ReadWholeFileText(const fs::path& path) { std::ifstream input(path, std::ios::binary); if (!input.is_open()) { return Containers::String(); } std::ostringstream buffer; buffer << input.rdbuf(); return ToContainersString(buffer.str()); } Containers::String AssetDatabase::ComputeFileHash(const fs::path& path) { std::ifstream input(path, std::ios::binary); if (!input.is_open()) { return Containers::String(); } std::vector bytes( (std::istreambuf_iterator(input)), std::istreambuf_iterator()); if (bytes.empty()) { return HashBytesToAssetGUID(nullptr, 0).ToString(); } return HashBytesToAssetGUID(bytes.data(), bytes.size()).ToString(); } Core::uint64 AssetDatabase::GetFileSizeValue(const fs::path& path) { std::error_code ec; const auto size = fs::file_size(path, ec); return ec ? 0 : static_cast(size); } Core::uint64 AssetDatabase::GetFileWriteTimeValue(const fs::path& path) { std::error_code ec; const auto writeTime = fs::last_write_time(path, ec); if (ec) { return 0; } return static_cast(writeTime.time_since_epoch().count()); } } // namespace Resources } // namespace XCEngine