1163 lines
41 KiB
C++
1163 lines
41 KiB
C++
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
|
|
|
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
|
#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());
|
|
}
|
|
|
|
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*>(§ion), 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;
|
|
}
|
|
|
|
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()) {
|
|
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<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
|