#include "AssetsPreloader.hpp" #include #include #include #include namespace LV { static TOS::Logger LOG = "AssetsPreloader"; static ResourceFile readFileBytes(const fs::path& path) { std::ifstream file(path, std::ios::binary); if(!file) throw std::runtime_error("Не удалось открыть файл: " + path.string()); file.seekg(0, std::ios::end); std::streamoff size = file.tellg(); if(size < 0) size = 0; file.seekg(0, std::ios::beg); ResourceFile out; out.Data.resize(static_cast(size)); if(size > 0) { file.read(reinterpret_cast(out.Data.data()), size); if (!file) throw std::runtime_error("Не удалось прочитать файл: " + path.string()); } out.calcHash(); return out; } static std::u8string readOptionalMeta(const fs::path& path) { fs::path metaPath = path; metaPath += ".meta"; if(!fs::exists(metaPath) || !fs::is_regular_file(metaPath)) return {}; ResourceFile meta = readFileBytes(metaPath); return std::move(meta.Data); } AssetsPreloader::AssetsPreloader() { std::fill(NextId.begin(), NextId.end(), 1); std::fill(LastSendId.begin(), LastSendId.end(), 1); } AssetsPreloader::Out_reloadResources AssetsPreloader::reloadResources(const AssetsRegister& instances, ReloadStatus* status) { bool expected = false; assert(_Reloading.compare_exchange_strong(expected, true) && "Двойной вызов reloadResources"); struct ReloadGuard { std::atomic& Flag; ~ReloadGuard() { Flag.exchange(false); } } guard{_Reloading}; try { ReloadStatus secondStatus; return _reloadResources(instances, status ? *status : secondStatus); } catch(...) { assert(!"reloadResources: здесь не должно быть ошибок"); std::unreachable(); } } AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const AssetsRegister& instances, ReloadStatus& status) { Out_reloadResources result; // 1) Поиск всех ресурсов и построение конечной карты ресурсов (timestamps, path, name, size) // Карта найденных ресурсов std::array< std::unordered_map< std::string, // Domain std::unordered_map< std::string, ResourceFindInfo, detail::TSVHash, detail::TSVEq >, detail::TSVHash, detail::TSVEq >, static_cast(AssetType::MAX_ENUM) > resourcesFirstStage; for (const fs::path& instance : instances.Assets) { try { if (fs::is_regular_file(instance)) { // Может архив /// TODO: пока не поддерживается } else if (fs::is_directory(instance)) { // Директория fs::path assetsRoot = instance; fs::path assetsCandidate = instance / "assets"; if (fs::exists(assetsCandidate) && fs::is_directory(assetsCandidate)) assetsRoot = assetsCandidate; // Директория assets существует, перебираем домены в ней for(auto begin = fs::directory_iterator(assetsRoot), end = fs::directory_iterator(); begin != end; begin++) { if(!begin->is_directory()) continue; fs::path domainPath = begin->path(); std::string domain = domainPath.filename().string(); // Перебираем по типу ресурса for(size_t type = 0; type < static_cast(AssetType::MAX_ENUM); ++type) { AssetType assetType = static_cast(type); fs::path assetPath = domainPath / EnumAssetsToDirectory(assetType); if (!fs::exists(assetPath) || !fs::is_directory(assetPath)) continue; std::unordered_map< std::string, // Key ResourceFindInfo, // ResourceInfo, detail::TSVHash, detail::TSVEq >& firstStage = resourcesFirstStage[static_cast(assetType)][domain]; // Исследуем все ресурсы одного типа for (auto begin = fs::recursive_directory_iterator(assetPath), end = fs::recursive_directory_iterator(); begin != end; begin++) { if (begin->is_directory()) continue; fs::path file = begin->path(); if (assetType == AssetType::Texture && file.extension() == ".meta") continue; std::string key = fs::relative(file, assetPath).generic_string(); if (firstStage.contains(key)) continue; fs::file_time_type timestamp = fs::last_write_time(file); if (assetType == AssetType::Texture) { fs::path metaPath = file; metaPath += ".meta"; if (fs::exists(metaPath) && fs::is_regular_file(metaPath)) { auto metaTime = fs::last_write_time(metaPath); if (metaTime > timestamp) timestamp = metaTime; } } // Работаем с ресурсом firstStage[key] = ResourceFindInfo{ .Path = file, .Timestamp = timestamp }; } } } } else { throw std::runtime_error("Неизвестный тип инстанса медиаресурсов"); } } catch (const std::exception& exc) { /// TODO: Логгировать в статусе } } auto resolveModelInfo = [&](std::string_view domain, std::string_view key) -> const ResourceFindInfo* { auto& table = resourcesFirstStage[static_cast(AssetType::Model)]; auto iterDomain = table.find(std::string(domain)); if(iterDomain == table.end()) return nullptr; auto iterKey = iterDomain->second.find(std::string(key)); if(iterKey == iterDomain->second.end()) return nullptr; return &iterKey->second; }; std::function(std::string_view, std::string_view, std::unordered_set&)> loadModelProfile; loadModelProfile = [&](std::string_view domain, std::string_view key, std::unordered_set& visiting) -> std::optional { std::string fullKey = std::string(domain) + ':' + std::string(key); if(!visiting.insert(fullKey).second) { LOG.warn() << "Model parent cycle: " << fullKey; return std::nullopt; } const ResourceFindInfo* info = resolveModelInfo(domain, key); if(!info) { LOG.warn() << "Model file not found for parent: " << fullKey; visiting.erase(fullKey); return std::nullopt; } ResourceFile file = readFileBytes(info->Path); std::string_view view(reinterpret_cast(file.Data.data()), file.Data.size()); js::object obj = js::parse(view).as_object(); if(auto parentVal = obj.if_contains("parent")) { if(parentVal->is_string()) { std::string parentStr = std::string(parentVal->as_string()); auto [pDomain, pKeyRaw] = parseDomainKey(parentStr, domain); fs::path pKeyPath = fs::path(pKeyRaw); if(pKeyPath.extension().empty()) pKeyPath += ".json"; std::string pKey = pKeyPath.generic_string(); std::optional parent = loadModelProfile(pDomain, pKey, visiting); if(parent) { auto mergeFieldIfMissing = [&](const char* field) { if(!obj.contains(field) && parent->contains(field)) obj[field] = parent->at(field); }; mergeFieldIfMissing("cuboids"); mergeFieldIfMissing("sub_models"); mergeFieldIfMissing("gui_light"); mergeFieldIfMissing("ambient_occlusion"); if(auto parentTextures = parent->if_contains("textures"); parentTextures && parentTextures->is_object()) { if(auto childTextures = obj.if_contains("textures"); childTextures && childTextures->is_object()) { auto& childObj = childTextures->as_object(); const auto& parentObj = parentTextures->as_object(); for(const auto& [tkey, tval] : parentObj) { if(!childObj.contains(tkey)) childObj.emplace(tkey, tval); } } else if(!obj.contains("textures")) { obj["textures"] = parentTextures->as_object(); } } if(auto parentDisplay = parent->if_contains("display"); parentDisplay && parentDisplay->is_object()) { if(auto childDisplay = obj.if_contains("display"); childDisplay && childDisplay->is_object()) { auto& childObj = childDisplay->as_object(); const auto& parentObj = parentDisplay->as_object(); for(const auto& [dkey, dval] : parentObj) { if(!childObj.contains(dkey)) childObj.emplace(dkey, dval); } } else if(!obj.contains("display")) { obj["display"] = parentDisplay->as_object(); } } } } } visiting.erase(fullKey); return obj; }; // Функция парсинга ресурсов auto buildResource = [&](AssetType type, std::string_view domain, std::string_view key, const ResourceFindInfo& info) -> PendingResource { PendingResource out; out.Key = key; out.Timestamp = info.Timestamp; std::function modelResolver = [&](const std::string_view model) -> uint32_t { auto [mDomain, mKey] = parseDomainKey(model, domain); return getId(AssetType::Model, mDomain, mKey); }; std::function(std::string_view)> textureIdResolver = [&](std::string_view texture) -> std::optional { auto [mDomain, mKey] = parseDomainKey(texture, domain); return getId(AssetType::Texture, mDomain, mKey); }; auto normalizeTexturePipelineSrc = [](std::string_view src) -> std::string { std::string out(src); auto isSpace = [](unsigned char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }; size_t start = 0; while(start < out.size() && isSpace(static_cast(out[start]))) ++start; if(out.compare(start, 3, "tex") != 0) { std::string pref = "tex "; pref += out.substr(start); return pref; } return out; }; std::function(const std::string_view)> textureResolver = [&](const std::string_view texturePipelineSrc) -> std::vector { TexturePipelineProgram tpp; bool flag = tpp.compile(normalizeTexturePipelineSrc(texturePipelineSrc)); if(!flag) return {}; tpp.link(textureIdResolver); return tpp.toBytes(); }; if (type == AssetType::Nodestate) { ResourceFile file = readFileBytes(info.Path); std::string_view view(reinterpret_cast(file.Data.data()), file.Data.size()); js::object obj = js::parse(view).as_object(); HeadlessNodeState hns; out.Header = hns.parse(obj, modelResolver); out.Resource = std::make_shared(hns.dump()); out.Hash = sha2::sha256((const uint8_t*) out.Resource->data(), out.Resource->size()); } else if (type == AssetType::Model) { const std::string ext = info.Path.extension().string(); if (ext == ".json") { std::unordered_set visiting; std::optional objOpt = loadModelProfile(domain, key, visiting); if(!objOpt) { LOG.warn() << "Не удалось загрузить модель: " << info.Path.string(); throw std::runtime_error("Model profile load failed"); } js::object obj = std::move(*objOpt); HeadlessModel hm; out.Header = hm.parse(obj, modelResolver, textureResolver); std::u8string compiled = hm.dump(); if(hm.Cuboids.empty()) { static std::atomic debugEmptyModelLogCount = 0; uint32_t idx = debugEmptyModelLogCount.fetch_add(1); if(idx < 128) { LOG.warn() << "Model compiled with empty cuboids: " << domain << ':' << key << " file=" << info.Path.string() << " size=" << compiled.size(); } } out.Resource = std::make_shared(std::move(compiled)); out.Hash = sha2::sha256((const uint8_t*) out.Resource->data(), out.Resource->size()); // } else if (ext == ".gltf" || ext == ".glb") { // /// TODO: добавить поддержку gltf // ResourceFile file = readFileBytes(info.Path); // out.Resource = std::make_shared>(std::move(file.Data)); // out.Hash = file.Hash; } else { throw std::runtime_error("Не поддерживаемый формат модели: " + info.Path.string()); } } else if (type == AssetType::Texture) { ResourceFile file = readFileBytes(info.Path); out.Resource = std::make_shared(std::move(file.Data)); out.Hash = file.Hash; out.Header = readOptionalMeta(info.Path); } else { ResourceFile file = readFileBytes(info.Path); out.Resource = std::make_shared(std::move(file.Data)); out.Hash = file.Hash; } out.Id = getId(type, domain, key); return out; }; // 2) Обрабатываться будут только изменённые (новый timestamp) или новые ресурсы // Определяем каких ресурсов не стало for(size_t type = 0; type < static_cast(AssetType::MAX_ENUM); ++type) { auto& tableResourcesFirstStage = resourcesFirstStage[type]; for(const auto& [id, resource] : MediaResources[type]) { if(tableResourcesFirstStage.empty()) { result.Lost[type][resource.Domain].push_back(resource.Key); continue; } auto iterDomain = tableResourcesFirstStage.find(resource.Domain); if(iterDomain == tableResourcesFirstStage.end()) { result.Lost[type][resource.Domain].push_back(resource.Key); continue; } if(!iterDomain->second.contains(resource.Key)) { result.Lost[type][resource.Domain].push_back(resource.Key); } } } // Определение новых или изменённых ресурсов for(size_t type = 0; type < static_cast(AssetType::MAX_ENUM); ++type) { for(const auto& [domain, table] : resourcesFirstStage[type]) { auto iterTableDomain = DKToId[type].find(domain); if(iterTableDomain == DKToId[type].end()) { // Домен неизвестен движку, все ресурсы в нём новые for(const auto& [key, info] : table) { PendingResource resource = buildResource(static_cast(type), domain, key, info); result.NewOrChange[type][domain].push_back(std::move(resource)); } } else { for(const auto& [key, info] : table) { bool needsUpdate = true; if(auto iterKey = iterTableDomain->second.find(key); iterKey != iterTableDomain->second.end()) { // Идентификатор найден auto iterRes = MediaResources[type].find(iterKey->second); // Если нашли ресурс по идентификатору и время изменения не поменялось, то он не новый и не изменился if(iterRes != MediaResources[type].end() && iterRes->second.Timestamp == info.Timestamp) needsUpdate = false; } if(!needsUpdate) continue; PendingResource resource = buildResource(static_cast(type), domain, key, info); result.NewOrChange[(int) type][domain].push_back(std::move(resource)); } } } } return result; } AssetsPreloader::Out_applyResourceChange AssetsPreloader::applyResourceChange(const Out_reloadResources& orr) { Out_applyResourceChange result; // Удаляем ресурсы /* Удаляются только ресурсы, при этом за ними остаётся бронь на идентификатор Уже скомпилированные зависимости к ресурсам не будут перекомпилироваться для смены идентификатора. Если нужный ресурс появится, то привязка останется. Новые клиенты не получат ресурс которого нет, но он может использоваться */ for(size_t type = 0; type < static_cast(AssetType::MAX_ENUM); type++) { for(const auto& [domain, keys] : orr.Lost[type]) { auto iterDomain = DKToId[type].find(domain); // Если уже было решено, что ресурсы были, и стали потерянными, то так и должно быть assert(iterDomain != DKToId[type].end()); for(const auto& key : keys) { auto iterKey = iterDomain->second.find(key); // Ресурс был и должен быть assert(iterKey != iterDomain->second.end()); uint32_t id = iterKey->second; auto& resType = MediaResources[type]; auto iterRes = resType.find(id); if(iterRes == resType.end()) continue; // Ресурс был потерян result.Lost[type].push_back(id); // Hash более нам неизвестен HashToId.erase(iterRes->second.Hash); // Затираем ресурс resType.erase(iterRes); } } } // Добавляем for(int type = 0; type < (int) AssetType::MAX_ENUM; type++) { auto& typeTable = DKToId[type]; for(const auto& [domain, resources] : orr.NewOrChange[type]) { auto& domainTable = typeTable[domain]; for(const PendingResource& pending : resources) { MediaResource resource { .Domain = domain, .Key = std::move(pending.Key), .Timestamp = pending.Timestamp, .Resource = std::move(pending.Resource), .Hash = pending.Hash, .Header = std::move(pending.Header) }; auto& table = MediaResources[type]; // Нужно затереть старую ссылку хеша на данный ресурс if(auto iter = table.find(pending.Id); iter != table.end()) HashToId.erase(iter->second.Hash); // Добавили ресурс table[pending.Id] = resource; // Связали с хешем HashToId[resource.Hash] = {static_cast(type), pending.Id}; // Осведомили о новом/изменённом ресурсе result.NewOrChange[type].emplace_back(pending.Id, resource.Hash, std::move(resource.Header)); } } // Не должно быть ресурсов, которые были помечены как потерянные #ifndef NDEBUG std::unordered_set changed; for(const auto& [id, _, _2] : result.NewOrChange[type]) changed.insert(id); auto& lost = result.Lost[type]; for(auto iter : lost) assert(!changed.contains(iter)); #endif } return result; } AssetsPreloader::Out_bakeId AssetsPreloader::bakeIdTables() { #ifndef NDEBUG assert(!DKToIdInBakingMode); DKToIdInBakingMode = true; struct _tempStruct { AssetsPreloader* handler; ~_tempStruct() { handler->DKToIdInBakingMode = false; } } _lock{this}; #endif Out_bakeId result; for(size_t type = 0; type < static_cast(AssetType::MAX_ENUM); ++type) { // домен+ключ -> id { auto lock = NewDKToId[type].lock(); auto& dkToId = DKToId[type]; for(auto& [domain, keys] : *lock) { // Если домен не существует, просто воткнёт новые ключи auto [iterDomain, inserted] = dkToId.try_emplace(domain, std::move(keys)); if(!inserted) { // Домен уже существует, сливаем новые ключи iterDomain->second.merge(keys); } } lock->clear(); } // id -> домен+ключ { auto lock = NewIdToDK[type].lock(); auto& idToDK = IdToDK[type]; result.IdToDK[type] = std::move(*lock); lock->clear(); idToDK.append_range(result.IdToDK[type]); // result.LastSendId[type] = LastSendId[type]; LastSendId[type] = NextId[type]; } } return result; } AssetsPreloader::Out_fullSync AssetsPreloader::collectFullSync() const { Out_fullSync out; for(size_t type = 0; type < static_cast(AssetType::MAX_ENUM); ++type) { out.IdToDK[type] = IdToDK[type]; } for(size_t type = 0; type < static_cast(AssetType::MAX_ENUM); ++type) { for(const auto& [id, resource] : MediaResources[type]) { out.HashHeaders[type].push_back(BindHashHeaderInfo{ .Id = id, .Hash = resource.Hash, .Header = resource.Header }); out.Resources.emplace_back( static_cast(type), id, &resource ); } } return out; } std::tuple, std::vector> AssetsPreloader::getNodeDependency(const std::string& domain, const std::string& key) { (void)domain; (void)key; return {0, {}, {}}; } }