#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Client/AssetsCacheManager.hpp" #include "Client/AssetsHeaderCodec.hpp" #include "Common/Abstract.hpp" #include "Common/IdProvider.hpp" #include "Common/AssetsPreloader.hpp" #include "Common/TexturePipelineProgram.hpp" #include "TOSLib.hpp" #include "assets.hpp" #include "boost/asio/io_context.hpp" #include "png++/image.hpp" #include #include "Abstract.hpp" namespace LV::Client { namespace fs = std::filesystem; class AssetsManager : public IdProvider { public: struct ResourceUpdates { std::vector Models; std::vector Nodestates; std::vector Textures; std::vector Particles; std::vector Animations; std::vector Sounds; std::vector Fonts; }; public: AssetsManager(asio::io_context& ioc, fs::path cachePath) : Cache(AssetsCacheManager::Create(ioc, cachePath)) { for(size_t type = 0; type < static_cast(EnumAssets::MAX_ENUM); type++) { ServerToClientMap[type].push_back(0); } } // Ручные обновления struct Out_checkAndPrepareResourcesUpdate { AssetsPreloader::Out_checkAndPrepareResourcesUpdate RP, ES; std::unordered_map Files; }; Out_checkAndPrepareResourcesUpdate checkAndPrepareResourcesUpdate( const std::vector& resourcePacks, const std::vector& extraSources ) { Out_checkAndPrepareResourcesUpdate result; result.RP = ResourcePacks.checkAndPrepareResourcesUpdate( AssetsPreloader::AssetsRegister{resourcePacks}, [&](EnumAssets type, std::string_view domain, std::string_view key) -> ResourceId { return getId(type, domain, key); }, [&](std::u8string&& data, ResourceFile::Hash_t hash, fs::path path) { result.Files.emplace(hash, std::move(data)); } ); result.ES = ExtraSource.checkAndPrepareResourcesUpdate( AssetsPreloader::AssetsRegister{resourcePacks}, [&](EnumAssets type, std::string_view domain, std::string_view key) -> ResourceId { return getId(type, domain, key); } ); return result; } struct Out_applyResourcesUpdate { }; Out_applyResourcesUpdate applyResourcesUpdate(const Out_checkAndPrepareResourcesUpdate& orr) { Out_applyResourcesUpdate result; for(size_t type = 0; type < static_cast(EnumAssets::MAX_ENUM); ++type) { for(ResourceId id : orr.RP.LostLinks[type]) { std::optional res = ResourcePacks.getResource((EnumAssets) type, id); assert(res); auto hashIter = HashToPath.find(res->Hash); assert(hashIter != HashToPath.end()); auto& entry = hashIter->second; auto iter = std::find(entry.begin(), entry.end(), res->Path); assert(iter != entry.end()); entry.erase(iter); if(entry.empty()) HashToPath.erase(hashIter); } } ResourcePacks.applyResourcesUpdate(orr.RP); ExtraSource.applyResourcesUpdate(orr.ES); std::unordered_set needHashes; for(size_t type = 0; type < static_cast(EnumAssets::MAX_ENUM); ++type) { for(const auto& res : orr.RP.ResourceUpdates[type]) { // Помечаем ресурс для обновления PendingUpdateFromAsync[type].push_back(std::get(res)); HashToPath[std::get(res)].push_back(std::get(res)); } for(ResourceId id : orr.RP.LostLinks[type]) { // Помечаем ресурс для обновления PendingUpdateFromAsync[type].push_back(id); auto& hh = ServerIdToHH[type]; if(id < hh.size()) needHashes.insert(std::get(hh[id])); } } { for(const auto& [hash, data] : orr.Files) { WaitingHashes.insert(hash); } for(const auto& hash : WaitingHashes) needHashes.erase(hash); std::vector> toDisk; std::vector toCache; // Теперь раскидаем хеши по доступным источникам. for(const auto& hash : needHashes) { auto iter = HashToPath.find(hash); if(iter != HashToPath.end()) { // Ставим задачу загрузить с диска. toDisk.emplace_back(hash, iter->second.front()); } else { // Сделаем запрос в кеш. toCache.push_back(hash); } } // Запоминаем, что эти ресурсы уже ожидаются. WaitingHashes.insert_range(needHashes); // Запрос в кеш (если там не найдётся, то запрос уйдёт на сервер). if(!toCache.empty()) Cache->pushReads(std::move(toCache)); // Запрос к диску. if(!toDisk.empty()) NeedToReadFromDisk.append_range(std::move(toDisk)); _onHashLoad(orr.Files); } return result; } // ServerSession // Новые привязки ассетов к Домен+Ключ. void pushAssetsBindDK( const std::vector& domains, const std::array< std::vector>, static_cast(EnumAssets::MAX_ENUM) >& keys ) { LOG.debug() << "BindDK domains=" << domains.size(); for(size_t type = 0; type < static_cast(EnumAssets::MAX_ENUM); ++type) { LOG.info() << type; for(size_t forDomainIter = 0; forDomainIter < keys[type].size(); ++forDomainIter) { LOG.info() << "\t" << domains[forDomainIter]; for(const std::string& key : keys[type][forDomainIter]) { uint32_t id = getId((EnumAssets) type, domains[forDomainIter], key); LOG.info() << "\t\t" << key << " -> " << id; ServerToClientMap[type].push_back(id); } } } } // Новые привязки ассетов к Hash+Header. void pushAssetsBindHH( std::array< std::vector>, static_cast(EnumAssets::MAX_ENUM) >&& hash_and_headers ) { std::unordered_set needHashes; size_t totalBinds = 0; for(size_t type = 0; type < static_cast(EnumAssets::MAX_ENUM); ++type) { size_t maxSize = 0; for(auto& [id, hash, header] : hash_and_headers[type]) { totalBinds++; assert(id < ServerToClientMap[type].size()); id = ServerToClientMap[type][id]; if(id >= maxSize) maxSize = id+1; // Добавляем идентификатор в таблицу ожидающих обновлений. PendingUpdateFromAsync[type].push_back(id); // Поискать есть ли ресурс в ресурспаках. std::optional res = ResourcePacks.getResource((EnumAssets) type, id); if(res) { needHashes.insert(res->Hash); } else { needHashes.insert(hash); } } { // Уберём повторения в идентификаторах. auto& vec = PendingUpdateFromAsync[type]; std::sort(vec.begin(), vec.end()); vec.erase(std::unique(vec.begin(), vec.end()), vec.end()); } if(ServerIdToHH[type].size() < maxSize) ServerIdToHH[type].resize(maxSize); for(auto& [id, hash, header] : hash_and_headers[type]) { ServerIdToHH[type][id] = {hash, std::move(header)}; } } if(totalBinds) LOG.debug() << "BindHH total=" << totalBinds << " wait=" << WaitingHashes.size(); // Нужно убрать хеши, которые уже запрошены // needHashes ^ WaitingHashes. for(const auto& hash : WaitingHashes) needHashes.erase(hash); std::vector> toDisk; std::vector toCache; // Теперь раскидаем хеши по доступным источникам. for(const auto& hash : needHashes) { auto iter = HashToPath.find(hash); if(iter != HashToPath.end()) { // Ставим задачу загрузить с диска. toDisk.emplace_back(hash, iter->second.front()); } else { // Сделаем запрос в кеш. toCache.push_back(hash); } } // Запоминаем, что эти ресурсы уже ожидаются. WaitingHashes.insert_range(needHashes); // Запрос к диску. if(!toDisk.empty()) NeedToReadFromDisk.append_range(std::move(toDisk)); // Запрос в кеш (если там не найдётся, то запрос уйдёт на сервер). if(!toCache.empty()) Cache->pushReads(std::move(toCache)); } // Новые ресурсы, полученные с сервера. void pushNewResources( std::vector> &&resources ) { std::unordered_map files; std::vector vec; files.reserve(resources.size()); vec.reserve(resources.size()); for(auto& [hash, res] : resources) { vec.emplace_back(res); files.emplace(hash, std::move(res)); } _onHashLoad(files); Cache->pushResources(std::move(vec)); } // Для запроса отсутствующих ресурсов с сервера на клиент. std::vector pullNeededResources() { return std::move(NeedToRequestFromServer); } // Получить изменённые ресурсы (для передачи другим модулям). ResourceUpdates pullResourceUpdates() { return std::move(RU); } ResourceId reBind(EnumAssets type, ResourceId server) { return ServerToClientMap[static_cast(type)].at(server); } void tick() { // Проверим кеш std::vector>> resources = Cache->pullReads(); if(!resources.empty()) { std::unordered_map needToProceed; needToProceed.reserve(resources.size()); for(auto& [hash, res] : resources) { if(!res) NeedToRequestFromServer.push_back(hash); else needToProceed.emplace(hash, std::u8string{(const char8_t*) res->data(), res->size()}); } if(!NeedToRequestFromServer.empty()) LOG.debug() << "CacheMiss count=" << NeedToRequestFromServer.size(); if(!needToProceed.empty()) _onHashLoad(needToProceed); } /// Читаем с диска TODO: получилась хрень с определением типа, чтобы получать headless ресурс if(!NeedToReadFromDisk.empty()) { std::unordered_map files; files.reserve(NeedToReadFromDisk.size()); auto detectTypeDomainKey = [&](const fs::path& path, EnumAssets& typeOut, std::string& domainOut, std::string& keyOut) -> bool { fs::path cur = path.parent_path(); for(; !cur.empty(); cur = cur.parent_path()) { std::string name = cur.filename().string(); for(size_t typeIndex = 0; typeIndex < static_cast(EnumAssets::MAX_ENUM); ++typeIndex) { EnumAssets type = static_cast(typeIndex); if(name == ::EnumAssetsToDirectory(type)) { typeOut = type; domainOut = cur.parent_path().filename().string(); keyOut = fs::relative(path, cur).generic_string(); return true; } } } return false; }; for(const auto& [hash, path] : NeedToReadFromDisk) { std::u8string data; std::ifstream file(path, std::ios::binary); if(file) { file.seekg(0, std::ios::end); std::streamoff size = file.tellg(); if(size < 0) size = 0; file.seekg(0, std::ios::beg); data.resize(static_cast(size)); if(size > 0) { file.read(reinterpret_cast(data.data()), size); if(!file) data.clear(); } } else { LOG.warn() << "DiskReadFail " << path.string(); } if(!data.empty()) { EnumAssets type{}; std::string domain; std::string key; if(detectTypeDomainKey(path, type, domain, key)) { if(type == EnumAssets::Nodestate) { std::string_view view(reinterpret_cast(data.data()), data.size()); js::object obj = js::parse(view).as_object(); HeadlessNodeState hns; auto modelResolver = [&](const std::string_view model) -> AssetsModel { auto [mDomain, mKey] = parseDomainKey(model, domain); return getId(EnumAssets::Model, mDomain, mKey); }; hns.parse(obj, modelResolver); data = hns.dump(); } else if(type == EnumAssets::Model) { std::string_view view(reinterpret_cast(data.data()), data.size()); js::object obj = js::parse(view).as_object(); HeadlessModel hm; auto modelResolver = [&](const std::string_view model) -> AssetsModel { auto [mDomain, mKey] = parseDomainKey(model, domain); return getId(EnumAssets::Model, mDomain, mKey); }; auto textureIdResolver = [&](const std::string_view texture) -> std::optional { auto [tDomain, tKey] = parseDomainKey(texture, domain); return getId(EnumAssets::Texture, tDomain, tKey); }; auto textureResolver = [&](const std::string_view texturePipelineSrc) -> std::vector { TexturePipelineProgram tpp; if(!tpp.compile(texturePipelineSrc)) return {}; tpp.link(textureIdResolver); return tpp.toBytes(); }; hm.parse(obj, modelResolver, textureResolver); data = hm.dump(); } } } files.emplace(hash, std::move(data)); } NeedToReadFromDisk.clear(); _onHashLoad(files); } } private: Logger LOG = "Client>AssetsManager"; // Менеджеры учёта дисковых ресурсов AssetsPreloader // В приоритете ищутся ресурсы из ресурспаков по Domain+Key. ResourcePacks, /* Дополнительные источники ресурсов. Используется для поиска ресурса по хешу от сервера (может стоит тот же мод с совпадающими ресурсами), или для временной подгрузки ресурса по Domain+Key пока ресурс не был получен с сервера. */ ExtraSource; // Менеджер файлового кэша. AssetsCacheManager::Ptr Cache; // Указатели на доступные ресурсы std::unordered_map> HashToPath; // Таблица релинковки ассетов с идентификаторов сервера на клиентские. std::array< std::vector, static_cast(EnumAssets::MAX_ENUM) > ServerToClientMap; // Таблица серверных привязок HH (id клиентские) std::array< std::vector>, static_cast(EnumAssets::MAX_ENUM) > ServerIdToHH; // Ресурсы в ожидании данных по хешу для обновления (с диска, кеша, сервера). std::array< std::vector, static_cast(EnumAssets::MAX_ENUM) > PendingUpdateFromAsync; // Хеши, для которых где-то висит задача на загрузку. std::unordered_set WaitingHashes; // Хеши, которые необходимо запросить с сервера. std::vector NeedToRequestFromServer; // Ресурсы, которые нужно считать с диска std::vector> NeedToReadFromDisk; // Обновлённые ресурсы ResourceUpdates RU; // Когда данные были получены с диска, кеша или сервера void _onHashLoad(const std::unordered_map& files) { const auto& rpLinks = ResourcePacks.getResourceLinks(); const auto& esLinks = ExtraSource.getResourceLinks(); auto mapModelId = [&](ResourceId id) -> ResourceId { const auto& map = ServerToClientMap[static_cast(EnumAssets::Model)]; if(id >= map.size()) return 0; return map[id]; }; auto mapTextureId = [&](ResourceId id) -> ResourceId { const auto& map = ServerToClientMap[static_cast(EnumAssets::Texture)]; if(id >= map.size()) return 0; return map[id]; }; auto rebindHeader = [&](EnumAssets type, const ResourceHeader& header) -> ResourceHeader { if(header.empty()) return {}; std::vector bytes; bytes.resize(header.size()); std::memcpy(bytes.data(), header.data(), header.size()); std::vector rebound = AssetsHeaderCodec::rebindHeader( type, bytes, mapModelId, mapTextureId, [](const std::string&) {} ); return ResourceHeader(reinterpret_cast(rebound.data()), rebound.size()); }; for(size_t typeIndex = 0; typeIndex < static_cast(EnumAssets::MAX_ENUM); ++typeIndex) { auto& pending = PendingUpdateFromAsync[typeIndex]; if(pending.empty()) continue; std::vector stillPending; stillPending.reserve(pending.size()); size_t updated = 0; size_t missingSource = 0; size_t missingData = 0; for(ResourceId id : pending) { ResourceFile::Hash_t hash{}; ResourceHeader header; bool hasSource = false; bool localHeader = false; if(id < rpLinks[typeIndex].size() && rpLinks[typeIndex][id].IsExist) { hash = rpLinks[typeIndex][id].Hash; header = rpLinks[typeIndex][id].Header; hasSource = true; localHeader = true; } else if(id < ServerIdToHH[typeIndex].size()) { std::tie(hash, header) = ServerIdToHH[typeIndex][id]; hasSource = true; } if(!hasSource) { missingSource++; stillPending.push_back(id); continue; } auto dataIter = files.find(hash); if(dataIter == files.end()) { missingData++; stillPending.push_back(id); continue; } std::string domain = "core"; std::string key; { auto d = getDK((EnumAssets) typeIndex, id); if(d) { domain = d->Domain; key = d->Key; } } std::u8string data = dataIter->second; EnumAssets type = static_cast(typeIndex); ResourceHeader finalHeader = localHeader ? header : rebindHeader(type, header); if(id == 0) continue; if(type == EnumAssets::Nodestate) { HeadlessNodeState ns; ns.load(data); HeadlessNodeState::Header headerParsed; headerParsed.load(finalHeader); RU.Nodestates.push_back({id, std::move(ns), std::move(headerParsed)}); updated++; } else if(type == EnumAssets::Model) { HeadlessModel hm; hm.load(data); HeadlessModel::Header headerParsed; headerParsed.load(finalHeader); RU.Models.push_back({id, std::move(hm), std::move(headerParsed)}); updated++; } else if(type == EnumAssets::Texture) { AssetsTextureUpdate entry; entry.Id = id; entry.Header = std::move(finalHeader); if(!data.empty()) { iResource sres(reinterpret_cast(data.data()), data.size()); iBinaryStream stream = sres.makeStream(); png::image img(stream.Stream); entry.Width = static_cast(img.get_width()); entry.Height = static_cast(img.get_height()); entry.Pixels.resize(static_cast(entry.Width) * entry.Height); for(uint32_t y = 0; y < entry.Height; ++y) { const auto& row = img.get_pixbuf().operator[](y); for(uint32_t x = 0; x < entry.Width; ++x) { const auto& px = row[x]; uint32_t rgba = (uint32_t(px.alpha) << 24) | (uint32_t(px.red) << 16) | (uint32_t(px.green) << 8) | uint32_t(px.blue); entry.Pixels[x + y * entry.Width] = rgba; } } } RU.Textures.push_back(std::move(entry)); updated++; } else if(type == EnumAssets::Particle) { RU.Particles.push_back({id, std::move(data)}); updated++; } else if(type == EnumAssets::Animation) { RU.Animations.push_back({id, std::move(data)}); updated++; } else if(type == EnumAssets::Sound) { RU.Sounds.push_back({id, std::move(data)}); updated++; } else if(type == EnumAssets::Font) { RU.Fonts.push_back({id, std::move(data)}); updated++; } } if(updated || missingSource || missingData) { LOG.debug() << "HashLoad type=" << int(typeIndex) << " updated=" << updated << " missingSource=" << missingSource << " missingData=" << missingData; } pending = std::move(stillPending); } for(const auto& [hash, res] : files) WaitingHashes.erase(hash); } }; } // namespace LV::Client