Рефакторинг кода работы с ресурсами игры на стороне сервера

This commit is contained in:
2026-01-06 18:21:25 +06:00
parent 83530a6c15
commit c13ad06ba9
14 changed files with 656 additions and 578 deletions

View File

@@ -1,5 +1,9 @@
#include "AssetsPreloader.hpp"
#include "Common/Abstract.hpp"
#include "Common/TexturePipelineProgram.hpp"
#include "sha2.hpp"
#include <atomic>
#include <filesystem>
#include <fstream>
#include <unordered_set>
#include <utility>
@@ -42,11 +46,26 @@ static std::u8string readOptionalMeta(const fs::path& path) {
}
AssetsPreloader::AssetsPreloader() {
std::fill(NextId.begin(), NextId.end(), 1);
std::fill(LastSendId.begin(), LastSendId.end(), 1);
for(size_t type = 0; type < static_cast<size_t>(EnumAssets::MAX_ENUM); type++) {
ResourceLinks[type].emplace_back(
ResourceFile::Hash_t{0},
ResourceHeader(),
fs::file_time_type(),
fs::path{""},
false
);
}
}
AssetsPreloader::Out_reloadResources AssetsPreloader::reloadResources(const AssetsRegister& instances, ReloadStatus* status) {
AssetsPreloader::Out_checkAndPrepareResourcesUpdate AssetsPreloader::checkAndPrepareResourcesUpdate(
const AssetsRegister& instances,
const std::function<ResourceId(EnumAssets type, std::string_view domain, std::string_view key)>& idResolver,
const std::function<void(std::u8string&& resource, ResourceFile::Hash_t hash, fs::path resPath)>& onNewResourceParsed,
ReloadStatus* status
) {
assert(idResolver);
assert(onNewResourceParsed);
bool expected = false;
assert(_Reloading.compare_exchange_strong(expected, true) && "Двойной вызов reloadResources");
struct ReloadGuard {
@@ -56,7 +75,7 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::reloadResources(const Asse
try {
ReloadStatus secondStatus;
return _reloadResources(instances, status ? *status : secondStatus);
return _checkAndPrepareResourcesUpdate(instances, idResolver, onNewResourceParsed, status ? *status : secondStatus);
} catch(const std::exception& exc) {
LOG.error() << exc.what();
assert(!"reloadResources: здесь не должно быть ошибок");
@@ -67,9 +86,12 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::reloadResources(const Asse
}
}
AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const AssetsRegister& instances, ReloadStatus& status) {
Out_reloadResources result;
AssetsPreloader::Out_checkAndPrepareResourcesUpdate AssetsPreloader::_checkAndPrepareResourcesUpdate(
const AssetsRegister& instances,
const std::function<ResourceId(EnumAssets type, std::string_view domain, std::string_view key)>& idResolver,
const std::function<void(std::u8string&& resource, ResourceFile::Hash_t hash, fs::path resPath)>& onNewResourceParsed,
ReloadStatus& status
) {
// 1) Поиск всех ресурсов и построение конечной карты ресурсов (timestamps, path, name, size)
// Карта найденных ресурсов
std::array<
@@ -87,12 +109,12 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
static_cast<size_t>(AssetType::MAX_ENUM)
> resourcesFirstStage;
for (const fs::path& instance : instances.Assets) {
for(const fs::path& instance : instances.Assets) {
try {
if (fs::is_regular_file(instance)) {
if(fs::is_regular_file(instance)) {
// Может архив
/// TODO: пока не поддерживается
} else if (fs::is_directory(instance)) {
} else if(fs::is_directory(instance)) {
// Директория
fs::path assetsRoot = instance;
fs::path assetsCandidate = instance / "assets";
@@ -122,20 +144,20 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
>& firstStage = resourcesFirstStage[static_cast<size_t>(assetType)][domain];
// Исследуем все ресурсы одного типа
for (auto begin = fs::recursive_directory_iterator(assetPath), end = fs::recursive_directory_iterator(); begin != end; begin++) {
if (begin->is_directory())
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")
if(assetType == AssetType::Texture && file.extension() == ".meta")
continue;
std::string key = fs::relative(file, assetPath).generic_string();
if (firstStage.contains(key))
if(firstStage.contains(key))
continue;
fs::file_time_type timestamp = fs::last_write_time(file);
if (assetType == AssetType::Texture) {
if(assetType == AssetType::Texture) {
fs::path metaPath = file;
metaPath += ".meta";
if (fs::exists(metaPath) && fs::is_regular_file(metaPath)) {
@@ -148,7 +170,8 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
// Работаем с ресурсом
firstStage[key] = ResourceFindInfo{
.Path = file,
.Timestamp = timestamp
.Timestamp = timestamp,
.Id = idResolver(assetType, domain, key)
};
}
}
@@ -158,7 +181,6 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
}
} catch (const std::exception& exc) {
/// TODO: Логгировать в статусе
}
}
@@ -172,14 +194,14 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
= [&](const std::string_view model) -> uint32_t
{
auto [mDomain, mKey] = parseDomainKey(model, domain);
return getId(AssetType::Model, mDomain, mKey);
return idResolver(AssetType::Model, mDomain, mKey);
};
std::function<std::optional<uint32_t>(std::string_view)> textureIdResolver
= [&](std::string_view texture) -> std::optional<uint32_t>
{
auto [mDomain, mKey] = parseDomainKey(texture, domain);
return getId(AssetType::Texture, mDomain, mKey);
return idResolver(AssetType::Texture, mDomain, mKey);
};
std::function<std::vector<uint8_t>(const std::string_view)> textureResolver
@@ -202,8 +224,8 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
HeadlessNodeState hns;
out.Header = hns.parse(obj, modelResolver);
out.Resource = std::make_shared<std::u8string>(hns.dump());
out.Hash = sha2::sha256((const uint8_t*) out.Resource->data(), out.Resource->size());
out.Resource = 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") {
@@ -213,19 +235,8 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
HeadlessModel hm;
out.Header = hm.parse(obj, modelResolver, textureResolver);
std::u8string compiled = hm.dump();
if(hm.Cuboids.empty()) {
static std::atomic<uint32_t> 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::u8string>(std::move(compiled));
out.Hash = sha2::sha256((const uint8_t*) out.Resource->data(), out.Resource->size());
out.Resource = hm.dump();
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);
@@ -236,239 +247,157 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
}
} else if (type == AssetType::Texture) {
ResourceFile file = readFileBytes(info.Path);
out.Resource = std::make_shared<std::u8string>(std::move(file.Data));
out.Resource = 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::u8string>(std::move(file.Data));
out.Resource = std::move(file.Data);
out.Hash = file.Hash;
}
out.Id = getId(type, domain, key);
out.Id = idResolver(type, domain, key);
return out;
};
};
// 2) Определяем какие ресурсы изменились (новый timestamp) или новые ресурсы
Out_checkAndPrepareResourcesUpdate result;
// Собираем идентификаторы, чтобы потом определить какие ресурсы пропали
std::array<
std::unordered_set<ResourceId>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
> uniqueExists;
// 2) Обрабатываться будут только изменённые (новый timestamp) или новые ресурсы
// Определяем каких ресурсов не стало
for(size_t type = 0; type < static_cast<size_t>(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& uniqueExistsTypes = uniqueExists[type];
const auto& resourceLinksTyped = ResourceLinks[type];
result.MaxNewSize[type] = resourceLinksTyped.size();
auto iterDomain = tableResourcesFirstStage.find(resource.Domain);
if(iterDomain == tableResourcesFirstStage.end()) {
result.Lost[type][resource.Domain].push_back(resource.Key);
continue;
}
{
size_t allIds = 0;
for(const auto& [domain, keys] : resourcesFirstStage[type])
allIds += keys.size();
if(!iterDomain->second.contains(resource.Key)) {
result.Lost[type][resource.Domain].push_back(resource.Key);
}
uniqueExistsTypes.reserve(allIds);
}
}
// Определение новых или изменённых ресурсов
for(size_t type = 0; type < static_cast<size_t>(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<AssetType>(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;
for(const auto& [domain, keys] : resourcesFirstStage[type]) {
for(const auto& [key, res] : keys) {
uniqueExistsTypes.insert(res.Id);
if(res.Id >= resourceLinksTyped.size() || !std::get<bool>(resourceLinksTyped[res.Id]))
{ // Если идентификатора нет в таблице или ресурс не привязан
PendingResource resource = buildResource(static_cast<AssetType>(type), domain, key, res);
onNewResourceParsed(std::move(resource.Resource), resource.Hash, res.Path);
result.HashToPathNew[resource.Hash].push_back(res.Path);
if(res.Id >= result.MaxNewSize[type])
result.MaxNewSize[type] = res.Id+1;
result.ResourceUpdates[type].emplace_back(res.Id, resource.Hash, std::move(resource.Header), resource.Timestamp, res.Path);
} else if(
std::get<fs::path>(resourceLinksTyped[res.Id]) != res.Path
|| std::get<fs::file_time_type>(resourceLinksTyped[res.Id]) != res.Timestamp
) { // Если ресурс теперь берётся с другого места или изменилось время изменения файла
const auto& lastResource = resourceLinksTyped[res.Id];
PendingResource resource = buildResource(static_cast<AssetType>(type), domain, key, res);
if(auto lastHash = std::get<ResourceFile::Hash_t>(lastResource); lastHash != resource.Hash) {
// Хэш изменился
// Сообщаем о новом ресурсе
onNewResourceParsed(std::move(resource.Resource), resource.Hash, res.Path);
// Старый хэш более не доступен по этому расположению.
result.HashToPathLost[lastHash].push_back(std::get<fs::path>(resourceLinksTyped[res.Id]));
// Новый хеш стал доступен по этому расположению.
result.HashToPathNew[resource.Hash].push_back(res.Path);
} else if(std::get<fs::path>(resourceLinksTyped[res.Id]) != res.Path) {
// Изменился конечный путь.
// Хэш более не доступен по этому расположению.
result.HashToPathLost[resource.Hash].push_back(std::get<fs::path>(resourceLinksTyped[res.Id]));
// Хеш теперь доступен по этому расположению.
result.HashToPathNew[resource.Hash].push_back(res.Path);
} else {
// Ресурс без заголовка никак не изменился.
}
if(!needsUpdate)
continue;
PendingResource resource = buildResource(static_cast<AssetType>(type), domain, key, info);
result.NewOrChange[(int) type][domain].push_back(std::move(resource));
// Чтобы там не поменялось, мог поменятся заголовок. Уведомляем о новой привязке.
result.ResourceUpdates[type].emplace_back(res.Id, resource.Hash, std::move(resource.Header), resource.Timestamp, res.Path);
} else {
// Ресурс не изменился
}
}
}
}
return result;
}
AssetsPreloader::Out_applyResourceChange AssetsPreloader::applyResourceChange(const Out_reloadResources& orr) {
Out_applyResourceChange result;
// Удаляем ресурсы
/*
Удаляются только ресурсы, при этом за ними остаётся бронь на идентификатор
Уже скомпилированные зависимости к ресурсам не будут
перекомпилироваться для смены идентификатора.
Если нужный ресурс появится, то привязка останется.
Новые клиенты не получат ресурс которого нет,
но он может использоваться
*/
for(size_t type = 0; type < static_cast<size_t>(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<AssetType>(type), pending.Id};
// Осведомили о новом/изменённом ресурсе
result.NewOrChange[type].emplace_back(pending.Id, resource.Hash, std::move(resource.Header));
}
}
// Не должно быть ресурсов, которые были помечены как потерянные
#ifndef NDEBUG
std::unordered_set<uint32_t> 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;
// 3) Определяем какие ресурсы пропали
for(size_t type = 0; type < static_cast<size_t>(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);
}
}
const auto& resourceLinksTyped = ResourceLinks[type];
lock->clear();
}
size_t counter = 0;
for(const auto& [hash, header, timestamp, path, isExist] : resourceLinksTyped) {
size_t id = counter++;
if(!isExist)
continue;
// id -> домен+ключ
{
auto lock = NewIdToDK[type].lock();
if(uniqueExists[type].contains(id))
continue;
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];
// Ресурс потерян
// Хэш более не доступен по этому расположению.
result.HashToPathLost[hash].push_back(path);
result.LostLinks[type].push_back(id);
}
}
return result;
}
AssetsPreloader::Out_fullSync AssetsPreloader::collectFullSync() const {
Out_fullSync out;
AssetsPreloader::Out_applyResourcesUpdate AssetsPreloader::applyResourcesUpdate(const Out_checkAndPrepareResourcesUpdate& orr) {
Out_applyResourcesUpdate result;
for(size_t type = 0; type < static_cast<size_t>(AssetType::MAX_ENUM); ++type) {
out.IdToDK[type] = IdToDK[type];
}
for(size_t type = 0; type < static_cast<size_t>(EnumAssets::MAX_ENUM); ++type) {
// Затираем потерянные
for(ResourceId id : orr.LostLinks[type]) {
assert(id < ResourceLinks[type].size());
auto& [hash, header, timestamp, path, isExist] = ResourceLinks[type][id];
hash = {0};
header = {};
timestamp = fs::file_time_type();
path.clear();
isExist = false;
for(size_t type = 0; type < static_cast<size_t>(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<AssetType>(type),
id,
&resource
);
result.NewOrUpdates[type].emplace_back(id, hash, header);
}
// Увеличиваем размер, если необходимо
if(orr.MaxNewSize[type] > ResourceLinks[type].size()) {
std::tuple<
ResourceFile::Hash_t,
ResourceHeader,
fs::file_time_type,
fs::path,
bool
> def{
ResourceFile::Hash_t{0},
ResourceHeader(),
fs::file_time_type(),
fs::path{""},
false
};
ResourceLinks[type].resize(orr.MaxNewSize[type], def);
}
// Обновляем / добавляем
for(auto& [id, hash, header, timestamp, path] : orr.ResourceUpdates[type]) {
ResourceLinks[type][id] = {hash, std::move(header), timestamp, std::move(path), true};
result.NewOrUpdates[type].emplace_back(id, hash, header);
}
}
return out;
}
std::tuple<AssetsNodestate, std::vector<AssetsModel>, std::vector<AssetsTexture>>
AssetsPreloader::getNodeDependency(const std::string& domain, const std::string& key) {
(void)domain;
(void)key;
return {0, {}, {}};
return result;
}
}