Files
XCEngine/engine/src/Core/Asset/AssetDatabase.cpp

1163 lines
41 KiB
C++
Raw Normal View History

#include <XCEngine/Core/Asset/AssetDatabase.h>
#include <XCEngine/Core/Asset/ArtifactFormats.h>
2026-04-02 18:50:41 +08:00
#include <XCEngine/Debug/Logger.h>
#include <XCEngine/Resources/Mesh/MeshLoader.h>
#include <XCEngine/Resources/Texture/TextureLoader.h>
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <unordered_map>
#include <vector>
namespace XCEngine {
namespace Resources {
namespace fs = std::filesystem;
namespace {
std::string ToStdString(const Containers::String& value) {
return std::string(value.CStr());
}
2026-04-02 18:50:41 +08:00
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());
}
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<char>(std::tolower(ch));
});
return text;
}
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<std::string> SplitFields(const std::string& line) {
std::vector<std::string> 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<Core::uint32>(value.Length());
stream.write(reinterpret_cast<const char*>(&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<char*>(&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<Core::uint32>(texture.GetTextureType());
header.textureFormat = static_cast<Core::uint32>(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<Core::uint64>(texture.GetPixelDataSize());
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
if (texture.GetPixelDataSize() > 0) {
output.write(static_cast<const char*>(texture.GetPixelData()), texture.GetPixelDataSize());
}
return static_cast<bool>(output);
}
std::vector<MaterialProperty> GatherMaterialProperties(const Material& material) {
return material.GetProperties();
}
void WriteMaterialBlock(std::ofstream& output,
const Material& material,
const std::unordered_map<const Texture*, std::string>& textureFileNames) {
WriteString(output, material.GetName());
WriteString(output, material.GetPath());
WriteString(output, material.GetShaderPass());
MaterialArtifactHeader header;
header.renderQueue = material.GetRenderQueue();
header.renderState = material.GetRenderState();
header.tagCount = material.GetTagCount();
const std::vector<MaterialProperty> properties = GatherMaterialProperties(material);
std::vector<MaterialProperty> 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<Core::uint32>(nonTextureProperties.size());
header.textureBindingCount = material.GetTextureBindingCount();
output.write(reinterpret_cast<const char*>(&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<Core::uint32>(property.type);
propertyArtifact.value = property.value;
output.write(reinterpret_cast<const char*>(&propertyArtifact), sizeof(propertyArtifact));
}
for (Core::uint32 bindingIndex = 0; bindingIndex < material.GetTextureBindingCount(); ++bindingIndex) {
const Containers::String bindingName = material.GetTextureBindingName(bindingIndex);
const Texture* texture = material.GetTextureBindingTexture(bindingIndex).Get();
auto fileIt = texture != nullptr ? textureFileNames.find(texture) : textureFileNames.end();
WriteString(output, bindingName);
WriteString(output,
fileIt != textureFileNames.end()
? ToContainersString(fileIt->second)
: Containers::String());
}
}
bool WriteMeshArtifactFile(const fs::path& artifactPath, const Mesh& mesh) {
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<Core::uint32>(mesh.GetVertexAttributes());
header.indexCount = mesh.GetIndexCount();
header.use32BitIndex = mesh.IsUse32BitIndex() ? 1u : 0u;
header.sectionCount = static_cast<Core::uint32>(mesh.GetSections().Size());
header.materialCount = static_cast<Core::uint32>(mesh.GetMaterials().Size());
header.textureCount = static_cast<Core::uint32>(mesh.GetTextures().Size());
header.boundsMin = mesh.GetBounds().GetMin();
header.boundsMax = mesh.GetBounds().GetMax();
header.vertexDataSize = static_cast<Core::uint64>(mesh.GetVertexDataSize());
header.indexDataSize = static_cast<Core::uint64>(mesh.GetIndexDataSize());
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
for (const MeshSection& section : mesh.GetSections()) {
output.write(reinterpret_cast<const char*>(&section), sizeof(section));
}
if (mesh.GetVertexDataSize() > 0) {
output.write(static_cast<const char*>(mesh.GetVertexData()), mesh.GetVertexDataSize());
}
if (mesh.GetIndexDataSize() > 0) {
output.write(static_cast<const char*>(mesh.GetIndexData()), mesh.GetIndexDataSize());
}
std::unordered_map<const Texture*, std::string> textureFileNames;
for (size_t textureIndex = 0; textureIndex < mesh.GetTextures().Size(); ++textureIndex) {
const Texture* texture = mesh.GetTextures()[textureIndex];
if (texture == nullptr) {
continue;
}
const std::string fileName = "texture_" + std::to_string(textureIndex) + ".xctex";
textureFileNames.emplace(texture, fileName);
WriteString(output, ToContainersString(fileName));
}
for (size_t materialIndex = 0; materialIndex < mesh.GetMaterials().Size(); ++materialIndex) {
const Material* material = mesh.GetMaterials()[materialIndex];
const Core::uint32 materialPresent = material != nullptr ? 1u : 0u;
output.write(reinterpret_cast<const char*>(&materialPresent), sizeof(materialPresent));
if (material != nullptr) {
WriteMaterialBlock(output, *material, textureFileNames);
}
}
return static_cast<bool>(output);
}
void DestroyImportedMesh(Mesh* mesh) {
if (mesh == nullptr) {
return;
}
std::vector<Material*> materials;
materials.reserve(mesh->GetMaterials().Size());
for (Material* material : mesh->GetMaterials()) {
if (material != nullptr) {
materials.push_back(material);
}
}
std::vector<Texture*> 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();
}
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;
}
2026-04-02 18:50:41 +08:00
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::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<std::string> 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<Core::uint32>(std::stoul(fields[5]));
record.metaHash = ToContainersString(fields[6]);
record.sourceHash = ToContainersString(fields[7]);
record.sourceFileSize = static_cast<Core::uint64>(std::stoull(fields[8]));
record.sourceWriteTime = static_cast<Core::uint64>(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<std::string> 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<Core::uint32>(std::stoul(fields[3]));
record.resourceType = static_cast<ResourceType>(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<Core::uint64>(std::stoull(fields[9]));
record.sourceWriteTime = fields.size() > 10 ? static_cast<Core::uint64>(std::stoull(fields[10])) : 0;
record.mainLocalID = fields.size() > 11 ? static_cast<LocalID>(std::stoull(fields[11])) : kMainAssetLocalID;
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\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<Core::uint32>(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 << '\n';
}
}
void AssetDatabase::ScanAssets() {
std::unordered_map<std::string, bool> 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<std::string, bool>& 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<std::string, bool>& seenPaths) {
std::vector<std::string> 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;
}
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<Core::uint32>(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<char>(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 == ".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;
}
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->sourceHash != sourceRecord.sourceHash ||
artifactRecord->metaHash != sourceRecord.metaHash ||
artifactRecord->sourceFileSize != sourceRecord.sourceFileSize ||
artifactRecord->sourceWriteTime != sourceRecord.sourceWriteTime;
}
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::Mesh:
return ImportModelAsset(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()) {
2026-04-02 18:50:41 +08:00
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)) {
2026-04-02 18:50:41 +08:00
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;
}
2026-04-02 18:50:41 +08:00
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) {
2026-04-02 18:50:41 +08:00
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)) {
2026-04-02 18:50:41 +08:00
if (ShouldTraceAssetPath(requestPath)) {
Debug::Logger::Get().Info(
Debug::LogCategory::FileSystem,
Containers::String("[AssetDatabase] EnsureArtifact reimport path=") + requestPath);
}
ArtifactRecord rebuiltRecord;
if (!ImportAsset(sourceRecord, rebuiltRecord)) {
2026-04-02 18:50:41 +08:00
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;
2026-04-02 18:50:41 +08:00
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<Texture*>(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;
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<Mesh*>(result.resource);
const Containers::String artifactKey = BuildArtifactKey(sourceRecord);
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 = WriteMeshArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *mesh);
for (size_t textureIndex = 0; writeOk && textureIndex < mesh->GetTextures().Size(); ++textureIndex) {
Texture* texture = mesh->GetTextures()[textureIndex];
if (texture == nullptr) {
continue;
}
const fs::path textureArtifactPath =
fs::path(m_projectRoot.CStr()) / artifactDir.CStr() / ("texture_" + std::to_string(textureIndex) + ".xctex");
writeOk = WriteTextureArtifactFile(textureArtifactPath, *texture);
}
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;
return true;
}
Containers::String AssetDatabase::BuildArtifactKey(const SourceAssetRecord& sourceRecord) 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());
return HashStringToAssetGUID(signature).ToString();
}
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<Core::uint8> bytes(
(std::istreambuf_iterator<char>(input)),
std::istreambuf_iterator<char>());
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<Core::uint64>(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<Core::uint64>(writeTime.time_since_epoch().count());
}
} // namespace Resources
} // namespace XCEngine