На стороне сервера теперь используется AssetsPreloader.hpp

This commit is contained in:
2026-01-03 13:11:20 +06:00
parent 776e9bfaca
commit d9e40b4e80
6 changed files with 793 additions and 154 deletions

View File

@@ -1,9 +1,25 @@
#pragma once #pragma once
#include <algorithm>
#include <array>
#include <cstdint> #include <cstdint>
#include <cstring>
#include <exception>
#include <filesystem> #include <filesystem>
#include <functional>
#include <fstream>
#include <memory>
#include <optional>
#include <stdexcept> #include <stdexcept>
#include <string>
#include <string_view>
#include <tuple>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include "Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp"
#include "Common/Abstract.hpp"
#include "Common/Async.hpp" #include "Common/Async.hpp"
#include "TOSAsync.hpp" #include "TOSAsync.hpp"
#include "boost/asio/executor.hpp" #include "boost/asio/executor.hpp"
@@ -17,106 +33,54 @@
Хранит все данные в оперативной памяти. Хранит все данные в оперативной памяти.
*/ */
static constexpr const char* EnumAssetsToDirectory(LV::EnumAssets value) {
enum class EnumAssets : int {
Nodestate, Particle, Animation, Model, Texture, Sound, Font, MAX_ENUM
};
using AssetsNodestate = uint32_t;
using AssetsParticle = uint32_t;
using AssetsAnimation = uint32_t;
using AssetsModel = uint32_t;
using AssetsTexture = uint32_t;
using AssetsSound = uint32_t;
using AssetsFont = uint32_t;
static constexpr const char* EnumAssetsToDirectory(EnumAssets value) {
switch(value) { switch(value) {
case EnumAssets::Nodestate: return "nodestate"; case LV::EnumAssets::Nodestate: return "nodestate";
case EnumAssets::Particle: return "particles"; case LV::EnumAssets::Particle: return "particle";
case EnumAssets::Animation: return "animations"; case LV::EnumAssets::Animation: return "animation";
case EnumAssets::Model: return "models"; case LV::EnumAssets::Model: return "model";
case EnumAssets::Texture: return "textures"; case LV::EnumAssets::Texture: return "texture";
case EnumAssets::Sound: return "sounds"; case LV::EnumAssets::Sound: return "sound";
case EnumAssets::Font: return "fonts"; case LV::EnumAssets::Font: return "font";
default: default:
break;
} }
assert(!"Неизвестный тип медиаресурса"); assert(!"Неизвестный тип медиаресурса");
return "";
} }
namespace LV { namespace LV {
namespace fs = std::filesystem; namespace fs = std::filesystem;
using AssetType = EnumAssets;
struct ResourceFile { struct ResourceFile {
using Hash_t = sha2::sha256_hash; // boost::uuids::detail::sha1::digest_type; using Hash_t = sha2::sha256_hash; // boost::uuids::detail::sha1::digest_type;
Hash_t Hash; Hash_t Hash;
std::vector<std::byte> Data; std::vector<uint8_t> Data;
void calcHash() { void calcHash() {
Hash = sha2::sha256((const uint8_t*) Data.data(), Data.size()); Hash = sha2::sha256(Data.data(), Data.size());
} }
}; };
class AssetsPreloader : public TOS::IAsyncDestructible { class AssetsPreloader : public TOS::IAsyncDestructible {
public: public:
using Ptr = std::shared_ptr<AssetsPreloader>; using Ptr = std::shared_ptr<AssetsPreloader>;
using IdTable = std::unordered_map<
AssetType, // Тип ресурса
std::unordered_map<
std::string, // Domain
std::unordered_map<
std::string, // Key
uint32_t // ResourceId
>
>
>;
// //
struct ReloadResult {
};
struct ReloadStatus {
/// TODO: callback'и для обновления статусов
/// TODO: многоуровневый статус std::vector<std::string>. Этапы/Шаги/Объекты
};
public:
static coro<Ptr> Create(asio::io_context& ioc);
~AssetsPreloader() = default;
AssetsPreloader(const AssetsPreloader&) = delete;
AssetsPreloader(AssetsPreloader&&) = delete;
AssetsPreloader& operator=(const AssetsPreloader&) = delete;
AssetsPreloader& operator=(AssetsPreloader&&) = delete;
// Пересматривает ресурсы и выдаёт изменения.
// Одновременно можно работать только один такой вызов.
// instances -> пути к директории с assets или архивы с assets внутри. От низшего приоритета к высшему.
// status -> обратный отклик о процессе обновления ресурсов.
// ReloadStatus <- новые и потерянные ресурсы.
coro<ReloadResult> reloadResources(const std::vector<fs::path>& instances, ReloadStatus* status = nullptr) {
bool expected = false;
assert(Reloading_.compare_exchange_strong(expected, true) && "Двойной вызов reloadResources");
try {
ReloadStatus secondStatus;
co_return _reloadResources(instances, status ? *status : secondStatus);
} catch(...) {
assert(!"reloadResources: здесь не должно быть ошибок");
}
Reloading_.exchange(false);
}
private:
struct ResourceFirstStageInfo {
// Путь к архиву (если есть), и путь до ресурса
fs::path ArchivePath, Path;
// Время изменения файла
fs::file_time_type Timestamp;
};
struct ResourceSecondStageInfo : public ResourceFirstStageInfo {
// Обезличенный ресурс
std::shared_ptr<std::vector<uint8_t>> Resource;
ResourceFile::Hash_t Hash;
// Сырой заголовок
std::vector<std::string> Dependencies;
};
/* /*
Ресурс имеет бинарную часть, из который вырезаны все зависимости. Ресурс имеет бинарную часть, из который вырезаны все зависимости.
Вторая часть это заголовок, которые всегда динамично передаётся с сервера. Вторая часть это заголовок, которые всегда динамично передаётся с сервера.
@@ -135,21 +99,364 @@ private:
std::vector<uint8_t> Dependencies; std::vector<uint8_t> Dependencies;
}; };
AssetsPreloader(asio::io_context& ioc) struct PendingDependency {
std::string Domain, Key;
};
struct PendingResource {
std::string Domain, Key;
fs::file_time_type Timestamp;
std::shared_ptr<std::vector<uint8_t>> Resource;
ResourceFile::Hash_t Hash;
std::vector<PendingDependency> ModelDeps;
std::vector<PendingDependency> TextureDeps;
std::vector<uint8_t> Extra;
};
struct ResourceChangeObj {
std::unordered_map<std::string, std::vector<std::string>> Lost[(int) AssetType::MAX_ENUM];
std::unordered_map<std::string, std::vector<PendingResource>> NewOrChange[(int) AssetType::MAX_ENUM];
};
struct Out_applyResourceChange {
std::vector<uint32_t> Lost[(int) AssetType::MAX_ENUM];
std::vector<std::pair<uint32_t, MediaResource>> NewOrChange[(int) AssetType::MAX_ENUM];
};
struct ReloadStatus {
/// TODO: callback'и для обновления статусов
/// TODO: многоуровневый статус std::vector<std::string>. Этапы/Шаги/Объекты
};
struct AssetsRegister {
/*
Пути до активных папок assets, соответствую порядку загруженным модам.
От последнего мода к первому.
Тот файл, что был загружен раньше и будет использоваться
*/
std::vector<fs::path> Assets;
/*
У этих ресурсов приоритет выше, если их удастся получить,
то использоваться будут именно они
Domain -> {key + data}
*/
std::unordered_map<std::string, std::unordered_map<std::string, void*>> Custom[(int) AssetType::MAX_ENUM];
};
public:
static coro<Ptr> Create(asio::io_context& ioc);
explicit AssetsPreloader(asio::io_context& ioc)
: TOS::IAsyncDestructible(ioc) : TOS::IAsyncDestructible(ioc)
{ {
} }
~AssetsPreloader() = default;
AssetsPreloader(const AssetsPreloader&) = delete;
AssetsPreloader(AssetsPreloader&&) = delete;
AssetsPreloader& operator=(const AssetsPreloader&) = delete;
AssetsPreloader& operator=(AssetsPreloader&&) = delete;
// Пересматривает ресурсы и выдаёт изменения.
// Одновременно можно работать только один такой вызов.
// instances -> пути к директории с assets или архивы с assets внутри. От низшего приоритета к высшему.
// status -> обратный отклик о процессе обновления ресурсов.
// ReloadStatus <- новые и потерянные ресурсы.
coro<ResourceChangeObj> reloadResources(const std::vector<fs::path>& instances, ReloadStatus* status = nullptr) {
bool expected = false;
assert(Reloading_.compare_exchange_strong(expected, true) && "Двойной вызов reloadResources");
struct ReloadGuard {
std::atomic<bool>& Flag;
~ReloadGuard() { Flag.exchange(false); }
} guard{Reloading_};
try {
ReloadStatus secondStatus;
co_return co_await _reloadResources(instances, status ? *status : secondStatus);
} catch(...) {
assert(!"reloadResources: здесь не должно быть ошибок");
}
}
/*
Перепроверка изменений ресурсов по дате изменения, пересчёт хешей.
Обнаруженные изменения должны быть отправлены всем клиентам.
Ресурсы будут обработаны в подходящий формат и сохранены в кеше.
Одновременно может выполнятся только одна такая функция
Используется в GameServer
*/
coro<ResourceChangeObj> recheckResources(const AssetsRegister& info) {
return reloadResources(info.Assets);
}
// Синхронный вызов reloadResources
ResourceChangeObj reloadResourcesSync(const std::vector<fs::path>& instances, ReloadStatus* status = nullptr) {
asio::io_context ioc;
std::optional<ResourceChangeObj> result;
std::exception_ptr error;
asio::co_spawn(ioc, [this, &instances, status, &result, &error]() -> coro<> {
try {
ReloadStatus localStatus;
result = co_await reloadResources(instances, status ? status : &localStatus);
} catch(...) {
error = std::current_exception();
}
co_return;
}, asio::detached);
ioc.run();
if (error)
std::rethrow_exception(error);
if (!result)
return {};
return std::move(*result);
}
// Синхронный вызов recheckResources
ResourceChangeObj recheckResourcesSync(const AssetsRegister& info, ReloadStatus* status = nullptr) {
return reloadResourcesSync(info.Assets, status);
}
/*
Применяет расчитанные изменения.
Раздаёт идентификаторы ресурсам и записывает их в таблицу
*/
Out_applyResourceChange applyResourceChange(const ResourceChangeObj& orr);
/*
Выдаёт идентификатор ресурса, даже если он не существует или был удалён.
resource должен содержать домен и путь
*/
ResourceId getId(AssetType type, const std::string& domain, const std::string& key);
// Выдаёт ресурс по идентификатору
const MediaResource* getResource(AssetType type, uint32_t id) const;
// Выдаёт ресурс по хешу
std::optional<std::tuple<AssetType, uint32_t, const MediaResource*>> getResource(const ResourceFile::Hash_t& hash);
// Выдаёт зависимости к ресурсам профиля ноды
std::tuple<AssetsNodestate, std::vector<AssetsModel>, std::vector<AssetsTexture>>
getNodeDependency(const std::string& domain, const std::string& key);
private:
struct ResourceFirstStageInfo {
// Путь к архиву (если есть), и путь до ресурса
fs::path ArchivePath, Path;
// Время изменения файла
fs::file_time_type Timestamp;
};
struct ResourceSecondStageInfo : public ResourceFirstStageInfo {
// Обезличенный ресурс
std::shared_ptr<std::vector<uint8_t>> Resource;
ResourceFile::Hash_t Hash;
// Сырой заголовок
std::vector<uint8_t> Dependencies;
};
// Текущее состояние reloadResources // Текущее состояние reloadResources
std::atomic<bool> Reloading_ = false; std::atomic<bool> Reloading_ = false;
struct HeaderWriter {
std::vector<uint8_t> Data;
void writeU8(uint8_t value) {
Data.push_back(value);
}
void writeU32(uint32_t value) {
Data.push_back(uint8_t(value & 0xff));
Data.push_back(uint8_t((value >> 8) & 0xff));
Data.push_back(uint8_t((value >> 16) & 0xff));
Data.push_back(uint8_t((value >> 24) & 0xff));
}
void writeBytes(const uint8_t* data, size_t size) {
if (size == 0)
return;
const uint8_t* ptr = data;
Data.insert(Data.end(), ptr, ptr + size);
}
};
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_t>(size));
if (size > 0) {
file.read(reinterpret_cast<char*>(out.Data.data()), size);
if (!file)
throw std::runtime_error("Не удалось прочитать файл: " + path.string());
}
out.calcHash();
return out;
}
static std::vector<uint8_t> toBytes(std::u8string_view data) {
std::vector<uint8_t> out(data.size());
if (!out.empty())
std::memcpy(out.data(), data.data(), out.size());
return out;
}
// Dependency lists are ordered by placeholder index used in the resource data.
static std::vector<uint8_t> buildHeader(AssetType type, const std::vector<uint32_t>& modelDeps, const std::vector<uint32_t>& textureDeps, const std::vector<uint8_t>& extra) {
HeaderWriter writer;
writer.writeU8('a');
writer.writeU8('h');
writer.writeU8(1);
writer.writeU8(static_cast<uint8_t>(type));
writer.writeU32(static_cast<uint32_t>(modelDeps.size()));
for (uint32_t id : modelDeps)
writer.writeU32(id);
writer.writeU32(static_cast<uint32_t>(textureDeps.size()));
for (uint32_t id : textureDeps)
writer.writeU32(id);
writer.writeU32(static_cast<uint32_t>(extra.size()));
writer.writeBytes(extra.data(), extra.size());
return std::move(writer.Data);
}
static std::vector<uint8_t> 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);
}
static std::optional<uint32_t> findId(const IdTable& table, AssetType type, const std::string& domain, const std::string& key) {
auto iterType = table.find(type);
if (iterType == table.end())
return std::nullopt;
auto iterDomain = iterType->second.find(domain);
if (iterDomain == iterType->second.end())
return std::nullopt;
auto iterKey = iterDomain->second.find(key);
if (iterKey == iterDomain->second.end())
return std::nullopt;
return iterKey->second;
}
void ensureNextId(AssetType type) {
size_t index = static_cast<size_t>(type);
if (NextIdInitialized[index])
return;
uint32_t maxId = 0;
auto iterType = DKToId.find(type);
if (iterType != DKToId.end()) {
for (const auto& [domain, keys] : iterType->second) {
for (const auto& [key, id] : keys) {
maxId = std::max(maxId, id + 1);
}
}
}
NextId[index] = maxId;
NextIdInitialized[index] = true;
}
struct HashHasher {
std::size_t operator()(const ResourceFile::Hash_t& hash) const noexcept {
std::size_t v = 14695981039346656037ULL;
for (const auto& byte : hash) {
v ^= static_cast<std::size_t>(byte);
v *= 1099511628211ULL;
}
return v;
}
};
struct ParsedHeader {
AssetType Type = AssetType::MAX_ENUM;
std::vector<uint32_t> ModelDeps;
std::vector<uint32_t> TextureDeps;
std::vector<uint8_t> Extra;
};
static std::optional<ParsedHeader> parseHeader(const std::vector<uint8_t>& data) {
size_t pos = 0;
auto readU8 = [&](uint8_t& out) -> bool {
if (pos + 1 > data.size())
return false;
out = data[pos++];
return true;
};
auto readU32 = [&](uint32_t& out) -> bool {
if (pos + 4 > data.size())
return false;
out = uint32_t(data[pos]) |
(uint32_t(data[pos + 1]) << 8) |
(uint32_t(data[pos + 2]) << 16) |
(uint32_t(data[pos + 3]) << 24);
pos += 4;
return true;
};
ParsedHeader out;
uint8_t c0, c1, version, type;
if (!readU8(c0) || !readU8(c1) || !readU8(version) || !readU8(type))
return std::nullopt;
if (c0 != 'a' || c1 != 'h' || version != 1)
return std::nullopt;
out.Type = static_cast<AssetType>(type);
uint32_t count = 0;
if (!readU32(count))
return std::nullopt;
out.ModelDeps.reserve(count);
for (uint32_t i = 0; i < count; i++) {
uint32_t id;
if (!readU32(id))
return std::nullopt;
out.ModelDeps.push_back(id);
}
if (!readU32(count))
return std::nullopt;
out.TextureDeps.reserve(count);
for (uint32_t i = 0; i < count; i++) {
uint32_t id;
if (!readU32(id))
return std::nullopt;
out.TextureDeps.push_back(id);
}
uint32_t extraSize = 0;
if (!readU32(extraSize))
return std::nullopt;
if (pos + extraSize > data.size())
return std::nullopt;
out.Extra.assign(data.begin() + pos, data.begin() + pos + extraSize);
return out;
}
// Пересмотр ресурсов // Пересмотр ресурсов
coro<ReloadResult> _reloadResources(const std::vector<fs::path>& instances, ReloadStatus& status) const { coro<ResourceChangeObj> _reloadResources(const std::vector<fs::path>& instances, ReloadStatus& status) const {
ResourceChangeObj result;
// 1) Поиск всех ресурсов и построение конечной карты ресурсов (timestamps, path, name, size) // 1) Поиск всех ресурсов и построение конечной карты ресурсов (timestamps, path, name, size)
// Карта найденных ресурсов // Карта найденных ресурсов
std::unordered_map< std::unordered_map<
EnumAssets, // Тип ресурса AssetType, // Тип ресурса
std::unordered_map< std::unordered_map<
std::string, // Domain std::string, // Domain
std::unordered_map< std::unordered_map<
@@ -166,10 +473,13 @@ private:
/// TODO: пока не поддерживается /// TODO: пока не поддерживается
} else if (fs::is_directory(instance)) { } else if (fs::is_directory(instance)) {
// Директория // Директория
fs::path assets = instance / "assets"; fs::path assetsRoot = instance;
if (fs::exists(assets) && fs::is_directory(assets)) { fs::path assetsCandidate = instance / "assets";
// Директорию assets существует, перебираем домены в ней if (fs::exists(assetsCandidate) && fs::is_directory(assetsCandidate))
for (auto begin = fs::directory_iterator(assets), end = fs::directory_iterator(); begin != end; begin++) { assetsRoot = assetsCandidate;
// Директория assets существует, перебираем домены в ней
for (auto begin = fs::directory_iterator(assetsRoot), end = fs::directory_iterator(); begin != end; begin++) {
if (!begin->is_directory()) if (!begin->is_directory())
continue; continue;
@@ -177,11 +487,14 @@ private:
co_await asio::post(co_await asio::this_coro::executor); co_await asio::post(co_await asio::this_coro::executor);
fs::path domainPath = begin->path(); fs::path domainPath = begin->path();
std::string domain = domainPath.filename(); std::string domain = domainPath.filename().string();
// Перебираем по типу ресурса // Перебираем по типу ресурса
for (EnumAssets assetType = EnumAssets(0); assetType < EnumAssets::MAX_ENUM; ((int&) assetType)++) { for (int type = 0; type < static_cast<int>(AssetType::MAX_ENUM); type++) {
AssetType assetType = static_cast<AssetType>(type);
fs::path assetPath = domainPath / EnumAssetsToDirectory(assetType); fs::path assetPath = domainPath / EnumAssetsToDirectory(assetType);
if (!fs::exists(assetPath) || !fs::is_directory(assetPath))
continue;
std::unordered_map< std::unordered_map<
std::string, // Key std::string, // Key
@@ -194,17 +507,31 @@ private:
continue; continue;
fs::path file = begin->path(); fs::path file = begin->path();
std::string key = fs::relative(file, domainPath).string(); if (assetType == AssetType::Texture && file.extension() == ".meta")
continue;
std::string key = fs::relative(file, assetPath).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] = ResourceFirstStageInfo{ firstStage[key] = ResourceFirstStageInfo{
.Path = file, .Path = file,
.Timestamp = fs::last_write_time(file) .Timestamp = timestamp
}; };
} }
} }
} }
}
} else { } else {
throw std::runtime_error("Неизвестный тип инстанса медиаресурсов"); throw std::runtime_error("Неизвестный тип инстанса медиаресурсов");
} }
@@ -214,40 +541,354 @@ private:
} }
} }
auto buildResource = [&](AssetType type, const std::string& domain, const std::string& key, const ResourceFirstStageInfo& info) -> PendingResource {
PendingResource out;
out.Domain = domain;
out.Key = key;
out.Timestamp = info.Timestamp;
if (type == AssetType::Nodestate) {
ResourceFile file = readFileBytes(info.Path);
std::string_view view(reinterpret_cast<const char*>(file.Data.data()), file.Data.size());
js::object obj = js::parse(view).as_object();
PreparedNodeState pns(domain, obj);
pns.LocalToModel.reserve(pns.LocalToModelKD.size());
uint32_t placeholder = 0;
for (const auto& [subDomain, subKey] : pns.LocalToModelKD) {
pns.LocalToModel.push_back(placeholder++);
out.ModelDeps.push_back({subDomain, subKey});
}
std::vector<uint8_t> data = toBytes(pns.dump());
out.Resource = std::make_shared<std::vector<uint8_t>>(std::move(data));
out.Hash = sha2::sha256(out.Resource->data(), out.Resource->size());
} else if (type == AssetType::Model) {
const std::string ext = info.Path.extension().string();
if (ext == ".json") {
ResourceFile file = readFileBytes(info.Path);
std::string_view view(reinterpret_cast<const char*>(file.Data.data()), file.Data.size());
js::object obj = js::parse(view).as_object();
PreparedModel pm(domain, obj);
pm.CompiledTextures.clear();
pm.CompiledTextures.reserve(pm.Textures.size());
std::unordered_map<std::string, uint32_t> textureIndex;
auto getTexturePlaceholder = [&](const std::string& texDomain, const std::string& texKey) -> uint32_t {
std::string token;
token.reserve(texDomain.size() + texKey.size() + 1);
token.append(texDomain);
token.push_back(':');
token.append(texKey);
auto iter = textureIndex.find(token);
if (iter != textureIndex.end())
return iter->second;
uint32_t placeholderId = static_cast<uint32_t>(out.TextureDeps.size());
textureIndex.emplace(std::move(token), placeholderId);
out.TextureDeps.push_back({texDomain, texKey});
return placeholderId;
};
for (const auto& [tkey, pipeline] : pm.Textures) {
TexturePipeline compiled;
if (pipeline.IsSource) {
TexturePipelineProgram program;
std::string source(reinterpret_cast<const char*>(pipeline.Pipeline.data()), pipeline.Pipeline.size());
std::string err;
if (!program.compile(source, &err)) {
throw std::runtime_error("Ошибка компиляции pipeline: " + err);
}
auto resolver = [&](std::string_view name) -> std::optional<uint32_t> {
auto [texDomain, texKey] = parseDomainKey(std::string(name), domain);
return getTexturePlaceholder(texDomain, texKey);
};
if (!program.link(resolver, &err)) {
throw std::runtime_error("Ошибка линковки pipeline: " + err);
}
const std::vector<uint8_t> bytes = program.toBytes();
compiled.Pipeline.resize(bytes.size());
if (!bytes.empty()) {
std::memcpy(compiled.Pipeline.data(), bytes.data(), bytes.size());
}
} else {
compiled.Pipeline = pipeline.Pipeline;
}
for (const auto& [texDomain, texKey] : pipeline.Assets) {
uint32_t placeholderId = getTexturePlaceholder(texDomain, texKey);
compiled.BinTextures.push_back(placeholderId);
}
pm.CompiledTextures[tkey] = std::move(compiled);
}
for (const auto& sub : pm.SubModels) {
out.ModelDeps.push_back({sub.Domain, sub.Key});
}
std::vector<uint8_t> data = toBytes(pm.dump());
out.Resource = std::make_shared<std::vector<uint8_t>>(std::move(data));
out.Hash = sha2::sha256(out.Resource->data(), out.Resource->size());
} else if (ext == ".gltf" || ext == ".glb") {
ResourceFile file = readFileBytes(info.Path);
out.Resource = std::make_shared<std::vector<uint8_t>>(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::vector<uint8_t>>(std::move(file.Data));
out.Hash = file.Hash;
out.Extra = readOptionalMeta(info.Path);
} else {
ResourceFile file = readFileBytes(info.Path);
out.Resource = std::make_shared<std::vector<uint8_t>>(std::move(file.Data));
out.Hash = file.Hash;
}
return out;
};
// 2) Обрабатываться будут только изменённые (новый timestamp) или новые ресурсы // 2) Обрабатываться будут только изменённые (новый timestamp) или новые ресурсы
// .meta
// Текстуры, шрифты, звуки хранить как есть // Текстуры, шрифты, звуки хранить как есть
// У моделей, состояний нод, анимации, частиц обналичить зависимости // У моделей, состояний нод, анимации, частиц обналичить зависимости
// Мета влияет только на хедер
/// TODO: реализовать реформатирование новых и изменённых ресурсов во внутренний обезличенный формат for (const auto& [type, resources] : MediaResources) {
auto iterType = resourcesFirstStage.find(type);
co_await asio::post(co_await asio::this_coro::executor); for (const auto& [id, resource] : resources) {
if (iterType == resourcesFirstStage.end()) {
asio::experimental::channel<void()> ch(IOC, 8); result.Lost[(int) type][resource.Domain].push_back(resource.Key);
continue;
co_return ReloadResult{};
} }
std::unordered_map< auto iterDomain = iterType->second.find(resource.Domain);
EnumAssets, // Тип ресурса if (iterDomain == iterType->second.end()) {
std::unordered_map< result.Lost[(int) type][resource.Domain].push_back(resource.Key);
std::string, // Domain continue;
std::unordered_map< }
std::string, // Key
uint32_t // ResourceId
>
>
> DKToId;
std::unordered_map< if (!iterDomain->second.contains(resource.Key)) {
EnumAssets, // Тип ресурса result.Lost[(int) type][resource.Domain].push_back(resource.Key);
std::unordered_map< }
uint32_t, }
MediaResource // ResourceInfo }
>
> MediaResources; for (const auto& [type, domains] : resourcesFirstStage) {
for (const auto& [domain, table] : domains) {
for (const auto& [key, info] : table) {
bool needsUpdate = true;
if (auto existingId = findId(DKToId, type, domain, key)) {
auto iterType = MediaResources.find(type);
if (iterType != MediaResources.end()) {
auto iterRes = iterType->second.find(*existingId);
if (iterRes != iterType->second.end() && iterRes->second.Timestamp == info.Timestamp) {
needsUpdate = false;
}
}
}
if (!needsUpdate)
continue;
co_await asio::post(co_await asio::this_coro::executor);
PendingResource resource = buildResource(type, domain, key, info);
result.NewOrChange[(int) type][domain].push_back(std::move(resource));
}
}
}
co_return result;
}
IdTable DKToId;
std::unordered_map<AssetType, std::unordered_map<uint32_t, MediaResource>> MediaResources;
std::unordered_map<ResourceFile::Hash_t, std::pair<AssetType, uint32_t>, HashHasher> HashToId;
std::array<uint32_t, static_cast<size_t>(AssetType::MAX_ENUM)> NextId = {};
std::array<bool, static_cast<size_t>(AssetType::MAX_ENUM)> NextIdInitialized = {};
}; };
inline AssetsPreloader::Out_applyResourceChange AssetsPreloader::applyResourceChange(const ResourceChangeObj& orr) {
Out_applyResourceChange result;
// Удаляем ресурсы
/*
Удаляются только ресурсы, при этом за ними остаётся бронь на идентификатор
Уже скомпилированные зависимости к ресурсам не будут
перекомпилироваться для смены идентификатора. Если нужный ресурс
появится, то привязка останется. Новые клиенты не получат ресурс
которого нет, но он может использоваться
*/
for (int type = 0; type < (int) AssetType::MAX_ENUM; type++) {
for (const auto& [domain, keys] : orr.Lost[type]) {
auto iterType = DKToId.find(static_cast<AssetType>(type));
if (iterType == DKToId.end())
continue;
auto iterDomain = iterType->second.find(domain);
if (iterDomain == iterType->second.end())
continue;
for (const auto& key : keys) {
auto iterKey = iterDomain->second.find(key);
if (iterKey == iterDomain->second.end())
continue;
uint32_t id = iterKey->second;
result.Lost[type].push_back(id);
auto iterResType = MediaResources.find(static_cast<AssetType>(type));
if (iterResType == MediaResources.end())
continue;
auto iterRes = iterResType->second.find(id);
if (iterRes == iterResType->second.end())
continue;
HashToId.erase(iterRes->second.Hash);
iterResType->second.erase(iterRes);
}
}
}
// Добавляем
for (int type = 0; type < (int) AssetType::MAX_ENUM; type++) {
for (const auto& [domain, resources] : orr.NewOrChange[type]) {
for (const PendingResource& pending : resources) {
uint32_t id = getId(static_cast<AssetType>(type), pending.Domain, pending.Key);
std::vector<uint32_t> modelIds;
modelIds.reserve(pending.ModelDeps.size());
for (const auto& dep : pending.ModelDeps)
modelIds.push_back(getId(AssetType::Model, dep.Domain, dep.Key));
std::vector<uint32_t> textureIds;
textureIds.reserve(pending.TextureDeps.size());
for (const auto& dep : pending.TextureDeps)
textureIds.push_back(getId(AssetType::Texture, dep.Domain, dep.Key));
MediaResource resource;
resource.Domain = pending.Domain;
resource.Key = pending.Key;
resource.Timestamp = pending.Timestamp;
resource.Resource = pending.Resource;
resource.Hash = pending.Hash;
resource.Dependencies = buildHeader(static_cast<AssetType>(type), modelIds, textureIds, pending.Extra);
auto& table = MediaResources[static_cast<AssetType>(type)];
if (auto iter = table.find(id); iter != table.end())
HashToId.erase(iter->second.Hash);
table[id] = resource;
HashToId[resource.Hash] = {static_cast<AssetType>(type), id};
result.NewOrChange[type].push_back({id, std::move(resource)});
}
}
std::unordered_set<uint32_t> changed;
for (const auto& [id, _] : result.NewOrChange[type])
changed.insert(id);
auto& lost = result.Lost[type];
lost.erase(std::remove_if(lost.begin(), lost.end(),
[&](uint32_t id) { return changed.contains(id); }), lost.end());
}
return result;
}
inline ResourceId AssetsPreloader::getId(AssetType type, const std::string& domain, const std::string& key) {
auto& typeTable = DKToId[type];
auto& domainTable = typeTable[domain];
if (auto iter = domainTable.find(key); iter != domainTable.end())
return iter->second;
ensureNextId(type);
uint32_t id = NextId[static_cast<size_t>(type)]++;
domainTable[key] = id;
return id;
}
inline const AssetsPreloader::MediaResource* AssetsPreloader::getResource(AssetType type, uint32_t id) const {
auto iterType = MediaResources.find(type);
if (iterType == MediaResources.end())
return nullptr;
auto iterRes = iterType->second.find(id);
if (iterRes == iterType->second.end())
return nullptr;
return &iterRes->second;
}
inline std::optional<std::tuple<AssetType, uint32_t, const AssetsPreloader::MediaResource*>> AssetsPreloader::getResource(const ResourceFile::Hash_t& hash) {
auto iter = HashToId.find(hash);
if (iter == HashToId.end())
return std::nullopt;
auto [type, id] = iter->second;
const MediaResource* res = getResource(type, id);
if (!res) {
HashToId.erase(iter);
return std::nullopt;
}
if (res->Hash != hash) {
HashToId.erase(iter);
return std::nullopt;
}
return std::tuple<AssetType, uint32_t, const MediaResource*>{type, id, res};
}
inline std::tuple<AssetsNodestate, std::vector<AssetsModel>, std::vector<AssetsTexture>>
AssetsPreloader::getNodeDependency(const std::string& domain, const std::string& key) {
if (domain == "core" && key == "none") {
return {0, {}, {}};
}
AssetsNodestate nodestateId = getId(AssetType::Nodestate, domain, key + ".json");
const MediaResource* nodestate = getResource(AssetType::Nodestate, nodestateId);
if (!nodestate)
return {nodestateId, {}, {}};
auto parsed = parseHeader(nodestate->Dependencies);
if (!parsed)
return {nodestateId, {}, {}};
std::vector<AssetsModel> models;
std::vector<AssetsTexture> textures;
std::unordered_set<AssetsModel> visited;
std::unordered_set<AssetsTexture> seenTextures;
std::function<void(AssetsModel)> visitModel = [&](AssetsModel modelId) {
if (!visited.insert(modelId).second)
return;
models.push_back(modelId);
const MediaResource* modelRes = getResource(AssetType::Model, modelId);
if (!modelRes)
return;
auto header = parseHeader(modelRes->Dependencies);
if (!header)
return;
for (uint32_t texId : header->TextureDeps) {
if (seenTextures.insert(texId).second)
textures.push_back(texId);
}
for (uint32_t subId : header->ModelDeps)
visitModel(subId);
};
for (uint32_t modelId : parsed->ModelDeps)
visitModel(modelId);
return {nodestateId, std::move(models), std::move(textures)};
}
} }

View File

@@ -4,7 +4,7 @@
namespace LV::Server { namespace LV::Server {
ContentManager::ContentManager(AssetsManager &am) ContentManager::ContentManager(AssetsPreloader &am)
: AM(am) : AM(am)
{ {
std::fill(std::begin(NextId), std::end(NextId), 1); std::fill(std::begin(NextId), std::end(NextId), 1);

View File

@@ -2,7 +2,7 @@
#include "Common/Abstract.hpp" #include "Common/Abstract.hpp"
#include "Server/Abstract.hpp" #include "Server/Abstract.hpp"
#include "Server/AssetsManager.hpp" #include "Common/AssetsPreloader.hpp"
#include <sol/table.hpp> #include <sol/table.hpp>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
@@ -135,7 +135,7 @@ class ContentManager {
void registerBase_Entity(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile); void registerBase_Entity(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile);
public: public:
ContentManager(AssetsManager &am); ContentManager(AssetsPreloader &am);
~ContentManager(); ~ContentManager();
// Регистрирует определение контента // Регистрирует определение контента
@@ -215,7 +215,7 @@ public:
private: private:
TOS::Logger LOG = "Server>ContentManager"; TOS::Logger LOG = "Server>ContentManager";
AssetsManager& AM; AssetsPreloader& AM;
}; };
} }

View File

@@ -3,7 +3,6 @@
#include "Common/Net.hpp" #include "Common/Net.hpp"
#include "Common/Packets.hpp" #include "Common/Packets.hpp"
#include "Server/Abstract.hpp" #include "Server/Abstract.hpp"
#include "Server/AssetsManager.hpp"
#include "Server/ContentManager.hpp" #include "Server/ContentManager.hpp"
#include "Server/RemoteClient.hpp" #include "Server/RemoteClient.hpp"
#include <algorithm> #include <algorithm>
@@ -1568,7 +1567,7 @@ void GameServer::init(fs::path worldPath) {
AssetsInit.Assets.push_back(mlt.LoadChain[index].Path / "assets"); AssetsInit.Assets.push_back(mlt.LoadChain[index].Path / "assets");
} }
Content.AM.applyResourceChange(Content.AM.recheckResources(AssetsInit)); Content.AM.applyResourceChange(Content.AM.recheckResourcesSync(AssetsInit));
LOG.info() << "Пре Инициализация"; LOG.info() << "Пре Инициализация";
@@ -1883,8 +1882,8 @@ void GameServer::stepModInitializations() {
void GameServer::reloadMods() { void GameServer::reloadMods() {
LOG.info() << "Перезагрузка модов: ассеты и зависимости"; LOG.info() << "Перезагрузка модов: ассеты и зависимости";
AssetsManager::ResourceChangeObj changes = Content.AM.recheckResources(AssetsInit); AssetsPreloader::ResourceChangeObj changes = Content.AM.recheckResourcesSync(AssetsInit);
AssetsManager::Out_applyResourceChange applied = Content.AM.applyResourceChange(changes); AssetsPreloader::Out_applyResourceChange applied = Content.AM.applyResourceChange(changes);
size_t changedCount = 0; size_t changedCount = 0;
size_t lostCount = 0; size_t lostCount = 0;
@@ -2691,22 +2690,23 @@ void GameServer::stepSyncContent() {
std::vector<std::tuple<EnumAssets, ResourceId, const std::string, const std::string, Resource>> resources; std::vector<std::tuple<EnumAssets, ResourceId, const std::string, const std::string, Resource>> resources;
for(int type = 0; type < (int) EnumAssets::MAX_ENUM; type++) { for(int type = 0; type < (int) EnumAssets::MAX_ENUM; type++) {
for(ResourceId resId : full.AssetsInfo[type]) { for(ResourceId resId : full.AssetsInfo[type]) {
std::optional<std::tuple<Resource, const std::string&, const std::string&>> result = Content.AM.getResource((EnumAssets) type, resId); const AssetsPreloader::MediaResource* media = Content.AM.getResource((EnumAssets) type, resId);
if(!result) if(!media)
continue; continue;
auto& [resource, domain, key] = *result; Resource resource(media->Resource->data(), media->Resource->size());
resources.emplace_back((EnumAssets) type, resId, domain, key, resource); resources.emplace_back((EnumAssets) type, resId, media->Domain, media->Key, std::move(resource));
} }
} }
for(const Hash_t& hash : full.Hashes) { for(const Hash_t& hash : full.Hashes) {
std::optional<std::tuple<Resource, const std::string&, const std::string&, EnumAssets, ResourceId>> result = Content.AM.getResource(hash); std::optional<std::tuple<EnumAssets, uint32_t, const AssetsPreloader::MediaResource*>> result = Content.AM.getResource(hash);
if(!result) if(!result)
continue; continue;
auto& [resource, domain, key, type, id] = *result; auto& [type, id, media] = *result;
resources.emplace_back(type, id, domain, key, resource); Resource resource(media->Resource->data(), media->Resource->size());
resources.emplace_back(type, id, media->Domain, media->Key, std::move(resource));
} }
// Информируем о запрошенных профилях // Информируем о запрошенных профилях

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#include "Server/AssetsManager.hpp" #include "Common/AssetsPreloader.hpp"
#define SOL_EXCEPTIONS_SAFE_PROPAGATION 1 #define SOL_EXCEPTIONS_SAFE_PROPAGATION 1
#include <Common/Net.hpp> #include <Common/Net.hpp>
@@ -24,7 +24,6 @@
#include <unordered_map> #include <unordered_map>
#include "WorldDefManager.hpp" #include "WorldDefManager.hpp"
#include "AssetsManager.hpp"
#include "ContentManager.hpp" #include "ContentManager.hpp"
#include "World.hpp" #include "World.hpp"
@@ -74,7 +73,7 @@ class GameServer : public AsyncObject {
struct ContentObj { struct ContentObj {
public: public:
AssetsManager AM; AssetsPreloader AM;
ContentManager CM; ContentManager CM;
// Если контент был перерегистрирован (исключая двоичные ресурсы), то профили будут повторно разосланы // Если контент был перерегистрирован (исключая двоичные ресурсы), то профили будут повторно разосланы
@@ -266,7 +265,7 @@ class GameServer : public AsyncObject {
std::vector<std::pair<std::string, sol::table>> ModInstances; std::vector<std::pair<std::string, sol::table>> ModInstances;
// Идентификатор текущегго мода, находящевося в обработке // Идентификатор текущегго мода, находящевося в обработке
std::string CurrentModId; std::string CurrentModId;
AssetsManager::AssetsRegister AssetsInit; AssetsPreloader::AssetsRegister AssetsInit;
DefEntityId PlayerEntityDefId = 0; DefEntityId PlayerEntityDefId = 0;
public: public:

View File

@@ -5,7 +5,6 @@
#include <Common/Net.hpp> #include <Common/Net.hpp>
#include "Abstract.hpp" #include "Abstract.hpp"
#include "Common/Packets.hpp" #include "Common/Packets.hpp"
#include "Server/AssetsManager.hpp"
#include "Server/ContentManager.hpp" #include "Server/ContentManager.hpp"
#include <Common/Abstract.hpp> #include <Common/Abstract.hpp>
#include <bitset> #include <bitset>