Compare commits

..

2 Commits

9 changed files with 355 additions and 1858 deletions

View File

@@ -1,8 +1,9 @@
#include "AssetsManager.hpp"
#include "AssetsCacheManager.hpp"
#include "Common/Abstract.hpp"
#include "sqlite3.h"
#include <chrono>
#include <cstddef>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <optional>
@@ -13,11 +14,10 @@
namespace LV::Client {
AssetsManager::AssetsManager(boost::asio::io_context &ioc, const fs::path &cachePath,
AssetsCacheManager::AssetsCacheManager(boost::asio::io_context &ioc, const fs::path &cachePath,
size_t maxCacheDirectorySize, size_t maxLifeTime)
: IAsyncDestructible(ioc), CachePath(cachePath)
{
NextId.fill(0);
{
auto lock = Changes.lock();
lock->MaxCacheDatabaseSize = maxCacheDirectorySize;
@@ -107,6 +107,14 @@ AssetsManager::AssetsManager(boost::asio::io_context &ioc, const fs::path &cache
MAKE_ERROR("Не удалось подготовить запрос STMT_DISK_COUNT: " << sqlite3_errmsg(DB));
}
sql = R"(
SELECT sha256, size FROM disk_cache ORDER BY last_used ASC;
)";
if(sqlite3_prepare_v2(DB, sql, -1, &STMT_DISK_OLDEST, nullptr) != SQLITE_OK) {
MAKE_ERROR("Не удалось подготовить запрос STMT_DISK_OLDEST: " << sqlite3_errmsg(DB));
}
sql = R"(
INSERT OR REPLACE INTO inline_cache (sha256, last_used, data)
VALUES (?, ?, ?);
@@ -148,18 +156,35 @@ AssetsManager::AssetsManager(boost::asio::io_context &ioc, const fs::path &cache
if(sqlite3_prepare_v2(DB, sql, -1, &STMT_INLINE_COUNT, nullptr) != SQLITE_OK) {
MAKE_ERROR("Не удалось подготовить запрос STMT_INLINE_COUNT: " << sqlite3_errmsg(DB));
}
sql = R"(
DELETE FROM inline_cache WHERE sha256=?;
)";
if(sqlite3_prepare_v2(DB, sql, -1, &STMT_INLINE_REMOVE, nullptr) != SQLITE_OK) {
MAKE_ERROR("Не удалось подготовить запрос STMT_INLINE_REMOVE: " << sqlite3_errmsg(DB));
}
sql = R"(
SELECT sha256, LENGTH(data) FROM inline_cache ORDER BY last_used ASC;
)";
if(sqlite3_prepare_v2(DB, sql, -1, &STMT_INLINE_OLDEST, nullptr) != SQLITE_OK) {
MAKE_ERROR("Не удалось подготовить запрос STMT_INLINE_OLDEST: " << sqlite3_errmsg(DB));
}
}
LOG.debug() << "Успешно, запускаем поток обработки";
OffThread = std::thread(&AssetsManager::readWriteThread, this, AUC.use());
OffThread = std::thread(&AssetsCacheManager::readWriteThread, this, AUC.use());
LOG.info() << "Инициализировано хранилище кеша: " << CachePath.c_str();
}
AssetsManager::~AssetsManager() {
AssetsCacheManager::~AssetsCacheManager() {
for(sqlite3_stmt* stmt : {
STMT_DISK_INSERT, STMT_DISK_UPDATE_TIME, STMT_DISK_REMOVE, STMT_DISK_CONTAINS,
STMT_DISK_SUM, STMT_DISK_COUNT, STMT_INLINE_INSERT, STMT_INLINE_GET,
STMT_INLINE_UPDATE_TIME, STMT_INLINE_SUM, STMT_INLINE_COUNT
STMT_DISK_SUM, STMT_DISK_COUNT, STMT_DISK_OLDEST, STMT_INLINE_INSERT,
STMT_INLINE_GET, STMT_INLINE_UPDATE_TIME, STMT_INLINE_SUM,
STMT_INLINE_COUNT, STMT_INLINE_REMOVE, STMT_INLINE_OLDEST
}) {
if(stmt)
sqlite3_finalize(stmt);
@@ -173,230 +198,19 @@ AssetsManager::~AssetsManager() {
LOG.info() << "Хранилище кеша закрыто";
}
ResourceId AssetsManager::getId(EnumAssets type, const std::string& domain, const std::string& key) {
std::lock_guard lock(MapMutex);
auto& typeTable = DKToId[type];
auto& domainTable = typeTable[domain];
if(auto iter = domainTable.find(key); iter != domainTable.end())
return iter->second;
ResourceId id = NextId[(int) type]++;
domainTable[key] = id;
return id;
}
std::optional<ResourceId> AssetsManager::getLocalIdFromServer(EnumAssets type, ResourceId serverId) const {
std::lock_guard lock(MapMutex);
auto iterType = ServerToLocal.find(type);
if(iterType == ServerToLocal.end())
return std::nullopt;
auto iter = iterType->second.find(serverId);
if(iter == iterType->second.end())
return std::nullopt;
return iter->second;
}
const AssetsManager::BindInfo* AssetsManager::getBind(EnumAssets type, ResourceId localId) const {
std::lock_guard lock(MapMutex);
auto iterType = LocalBinds.find(type);
if(iterType == LocalBinds.end())
return nullptr;
auto iter = iterType->second.find(localId);
if(iter == iterType->second.end())
return nullptr;
return &iter->second;
}
AssetsManager::BindResult AssetsManager::bindServerResource(EnumAssets type, ResourceId serverId, const std::string& domain,
const std::string& key, const Hash_t& hash, std::vector<uint8_t> header)
{
BindResult result;
result.LocalId = getId(type, domain, key);
std::lock_guard lock(MapMutex);
ServerToLocal[type][serverId] = result.LocalId;
auto& binds = LocalBinds[type];
auto iter = binds.find(result.LocalId);
if(iter == binds.end()) {
result.Changed = true;
binds.emplace(result.LocalId, BindInfo{
.LocalId = result.LocalId,
.ServerId = serverId,
.Domain = domain,
.Key = key,
.Hash = hash,
.Header = std::move(header)
});
return result;
}
BindInfo& info = iter->second;
bool hashChanged = info.Hash != hash;
bool headerChanged = info.Header != header;
result.Changed = hashChanged || headerChanged || info.ServerId != serverId;
info.ServerId = serverId;
info.Domain = domain;
info.Key = key;
info.Hash = hash;
info.Header = std::move(header);
return result;
}
std::optional<ResourceId> AssetsManager::unbindServerResource(EnumAssets type, ResourceId serverId) {
std::lock_guard lock(MapMutex);
auto iterType = ServerToLocal.find(type);
if(iterType == ServerToLocal.end())
return std::nullopt;
auto iter = iterType->second.find(serverId);
if(iter == iterType->second.end())
return std::nullopt;
ResourceId localId = iter->second;
iterType->second.erase(iter);
auto iterBindType = LocalBinds.find(type);
if(iterBindType != LocalBinds.end())
iterBindType->second.erase(localId);
return localId;
}
void AssetsManager::clearServerBindings() {
std::lock_guard lock(MapMutex);
ServerToLocal.clear();
LocalBinds.clear();
}
std::optional<AssetsManager::ParsedHeader> AssetsManager::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<EnumAssets>(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;
}
std::vector<uint8_t> AssetsManager::buildHeader(EnumAssets type, const std::vector<uint32_t>& modelDeps,
const std::vector<uint32_t>& textureDeps, const std::vector<uint8_t>& extra)
{
std::vector<uint8_t> data;
data.reserve(4 + 4 + modelDeps.size() * 4 + 4 + textureDeps.size() * 4 + 4 + extra.size());
data.push_back('a');
data.push_back('h');
data.push_back(1);
data.push_back(static_cast<uint8_t>(type));
auto 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));
};
writeU32(static_cast<uint32_t>(modelDeps.size()));
for(uint32_t id : modelDeps)
writeU32(id);
writeU32(static_cast<uint32_t>(textureDeps.size()));
for(uint32_t id : textureDeps)
writeU32(id);
writeU32(static_cast<uint32_t>(extra.size()));
if(!extra.empty())
data.insert(data.end(), extra.begin(), extra.end());
return data;
}
std::vector<uint8_t> AssetsManager::rebindHeader(const std::vector<uint8_t>& header) const {
auto parsed = parseHeader(header);
if(!parsed)
return header;
std::vector<uint32_t> modelDeps;
modelDeps.reserve(parsed->ModelDeps.size());
for(uint32_t serverId : parsed->ModelDeps) {
auto localId = getLocalIdFromServer(EnumAssets::Model, serverId);
modelDeps.push_back(localId.value_or(0));
}
std::vector<uint32_t> textureDeps;
textureDeps.reserve(parsed->TextureDeps.size());
for(uint32_t serverId : parsed->TextureDeps) {
auto localId = getLocalIdFromServer(EnumAssets::Texture, serverId);
textureDeps.push_back(localId.value_or(0));
}
return buildHeader(parsed->Type, modelDeps, textureDeps, parsed->Extra);
}
coro<> AssetsManager::asyncDestructor() {
coro<> AssetsCacheManager::asyncDestructor() {
NeedShutdown = true;
co_await IAsyncDestructible::asyncDestructor();
}
void AssetsManager::readWriteThread(AsyncUseControl::Lock lock) {
void AssetsCacheManager::readWriteThread(AsyncUseControl::Lock lock) {
try {
std::vector<fs::path> assets;
size_t maxCacheDatabaseSize, maxLifeTime;
[[maybe_unused]] size_t maxCacheDatabaseSize = 0;
[[maybe_unused]] size_t maxLifeTime = 0;
bool databaseSizeKnown = false;
while(!NeedShutdown || !WriteQueue.get_read().empty()) {
// Получить новые данные
if(Changes.get_read().AssetsChange) {
auto lock = Changes.lock();
assets = std::move(lock->Assets);
lock->AssetsChange = false;
}
if(Changes.get_read().MaxChange) {
auto lock = Changes.lock();
maxCacheDatabaseSize = lock->MaxCacheDatabaseSize;
@@ -422,56 +236,25 @@ void AssetsManager::readWriteThread(AsyncUseControl::Lock lock) {
// Чтение
if(!ReadQueue.get_read().empty()) {
ResourceKey rk;
Hash_t hash;
{
auto lock = ReadQueue.lock();
rk = lock->front();
hash = lock->front();
lock->pop();
}
bool finded = false;
// Сначала пробежимся по ресурспакам
{
std::string_view type;
switch(rk.Type) {
case EnumAssets::Nodestate: type = "nodestate"; break;
case EnumAssets::Particle: type = "particle"; break;
case EnumAssets::Animation: type = "animation"; break;
case EnumAssets::Model: type = "model"; break;
case EnumAssets::Texture: type = "texture"; break;
case EnumAssets::Sound: type = "sound"; break;
case EnumAssets::Font: type = "font"; break;
default:
std::unreachable();
}
for(const fs::path& path : assets) {
fs::path end = path / rk.Domain / type / rk.Key;
if(!fs::exists(end))
continue;
// Нашли
finded = true;
Resource res = Resource(end).convertToMem();
ReadyQueue.lock()->emplace_back(rk, res);
break;
}
}
if(!finded) {
// Поищем в малой базе
sqlite3_bind_blob(STMT_INLINE_GET, 1, (const void*) rk.Hash.data(), 32, SQLITE_STATIC);
sqlite3_bind_blob(STMT_INLINE_GET, 1, (const void*) hash.data(), 32, SQLITE_STATIC);
int errc = sqlite3_step(STMT_INLINE_GET);
if(errc == SQLITE_ROW) {
// Есть запись
const uint8_t *hash = (const uint8_t*) sqlite3_column_blob(STMT_INLINE_GET, 0);
const uint8_t *data = (const uint8_t*) sqlite3_column_blob(STMT_INLINE_GET, 0);
int size = sqlite3_column_bytes(STMT_INLINE_GET, 0);
Resource res(hash, size);
Resource res(data, size);
finded = true;
ReadyQueue.lock()->emplace_back(rk, res);
ReadyQueue.lock()->emplace_back(hash, res);
} else if(errc != SQLITE_DONE) {
sqlite3_reset(STMT_INLINE_GET);
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_INLINE_GET: " << sqlite3_errmsg(DB));
@@ -480,7 +263,7 @@ void AssetsManager::readWriteThread(AsyncUseControl::Lock lock) {
sqlite3_reset(STMT_INLINE_GET);
if(finded) {
sqlite3_bind_blob(STMT_INLINE_UPDATE_TIME, 1, (const void*) rk.Hash.data(), 32, SQLITE_STATIC);
sqlite3_bind_blob(STMT_INLINE_UPDATE_TIME, 1, (const void*) hash.data(), 32, SQLITE_STATIC);
sqlite3_bind_int(STMT_INLINE_UPDATE_TIME, 2, time(nullptr));
if(sqlite3_step(STMT_INLINE_UPDATE_TIME) != SQLITE_DONE) {
sqlite3_reset(STMT_INLINE_UPDATE_TIME);
@@ -489,12 +272,11 @@ void AssetsManager::readWriteThread(AsyncUseControl::Lock lock) {
sqlite3_reset(STMT_INLINE_UPDATE_TIME);
}
}
if(!finded) {
// Поищем на диске
sqlite3_bind_blob(STMT_DISK_CONTAINS, 1, (const void*) rk.Hash.data(), 32, SQLITE_STATIC);
int errc = sqlite3_step(STMT_DISK_CONTAINS);
sqlite3_bind_blob(STMT_DISK_CONTAINS, 1, (const void*) hash.data(), 32, SQLITE_STATIC);
errc = sqlite3_step(STMT_DISK_CONTAINS);
if(errc == SQLITE_ROW) {
// Есть запись
std::string hashKey;
@@ -502,13 +284,13 @@ void AssetsManager::readWriteThread(AsyncUseControl::Lock lock) {
std::stringstream ss;
ss << std::hex << std::setfill('0') << std::setw(2);
for (int i = 0; i < 32; ++i)
ss << static_cast<int>(rk.Hash[i]);
ss << static_cast<int>(hash[i]);
hashKey = ss.str();
}
finded = true;
ReadyQueue.lock()->emplace_back(rk, PathFiles / hashKey.substr(0, 2) / hashKey.substr(2));
ReadyQueue.lock()->emplace_back(hash, PathFiles / hashKey.substr(0, 2) / hashKey.substr(2));
} else if(errc != SQLITE_DONE) {
sqlite3_reset(STMT_DISK_CONTAINS);
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_DISK_CONTAINS: " << sqlite3_errmsg(DB));
@@ -518,7 +300,7 @@ void AssetsManager::readWriteThread(AsyncUseControl::Lock lock) {
if(finded) {
sqlite3_bind_int(STMT_DISK_UPDATE_TIME, 1, time(nullptr));
sqlite3_bind_blob(STMT_DISK_UPDATE_TIME, 2, (const void*) rk.Hash.data(), 32, SQLITE_STATIC);
sqlite3_bind_blob(STMT_DISK_UPDATE_TIME, 2, (const void*) hash.data(), 32, SQLITE_STATIC);
if(sqlite3_step(STMT_DISK_UPDATE_TIME) != SQLITE_DONE) {
sqlite3_reset(STMT_DISK_UPDATE_TIME);
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_DISK_UPDATE_TIME: " << sqlite3_errmsg(DB));
@@ -530,7 +312,7 @@ void AssetsManager::readWriteThread(AsyncUseControl::Lock lock) {
if(!finded) {
// Не нашли
ReadyQueue.lock()->emplace_back(rk, std::nullopt);
ReadyQueue.lock()->emplace_back(hash, std::nullopt);
}
continue;
@@ -546,7 +328,111 @@ void AssetsManager::readWriteThread(AsyncUseControl::Lock lock) {
lock->pop();
}
// TODO: добавить вычистку места при нехватке
if(!databaseSizeKnown) {
size_t diskSize = 0;
size_t inlineSize = 0;
int errc = sqlite3_step(STMT_DISK_SUM);
if(errc == SQLITE_ROW) {
if(sqlite3_column_type(STMT_DISK_SUM, 0) != SQLITE_NULL)
diskSize = static_cast<size_t>(sqlite3_column_int64(STMT_DISK_SUM, 0));
} else if(errc != SQLITE_DONE) {
sqlite3_reset(STMT_DISK_SUM);
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_DISK_SUM: " << sqlite3_errmsg(DB));
}
sqlite3_reset(STMT_DISK_SUM);
errc = sqlite3_step(STMT_INLINE_SUM);
if(errc == SQLITE_ROW) {
if(sqlite3_column_type(STMT_INLINE_SUM, 0) != SQLITE_NULL)
inlineSize = static_cast<size_t>(sqlite3_column_int64(STMT_INLINE_SUM, 0));
} else if(errc != SQLITE_DONE) {
sqlite3_reset(STMT_INLINE_SUM);
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_INLINE_SUM: " << sqlite3_errmsg(DB));
}
sqlite3_reset(STMT_INLINE_SUM);
DatabaseSize = diskSize + inlineSize;
databaseSizeKnown = true;
}
if(maxCacheDatabaseSize > 0 && DatabaseSize + res.size() > maxCacheDatabaseSize) {
size_t bytesToFree = DatabaseSize + res.size() - maxCacheDatabaseSize;
sqlite3_reset(STMT_DISK_OLDEST);
int errc = SQLITE_ROW;
while(bytesToFree > 0 && (errc = sqlite3_step(STMT_DISK_OLDEST)) == SQLITE_ROW) {
const void* data = sqlite3_column_blob(STMT_DISK_OLDEST, 0);
int dataSize = sqlite3_column_bytes(STMT_DISK_OLDEST, 0);
if(data && dataSize == 32) {
Hash_t hash;
std::memcpy(hash.data(), data, 32);
size_t entrySize = static_cast<size_t>(sqlite3_column_int64(STMT_DISK_OLDEST, 1));
std::string hashKey = hashToString(hash);
fs::path end = PathFiles / hashKey.substr(0, 2) / hashKey.substr(2);
std::error_code ec;
fs::remove(end, ec);
sqlite3_bind_blob(STMT_DISK_REMOVE, 1, (const void*) hash.data(), 32, SQLITE_STATIC);
if(sqlite3_step(STMT_DISK_REMOVE) != SQLITE_DONE) {
sqlite3_reset(STMT_DISK_REMOVE);
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_DISK_REMOVE: " << sqlite3_errmsg(DB));
}
sqlite3_reset(STMT_DISK_REMOVE);
if(DatabaseSize >= entrySize)
DatabaseSize -= entrySize;
else
DatabaseSize = 0;
if(bytesToFree > entrySize)
bytesToFree -= entrySize;
else
bytesToFree = 0;
}
}
if(errc != SQLITE_DONE && errc != SQLITE_ROW) {
sqlite3_reset(STMT_DISK_OLDEST);
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_DISK_OLDEST: " << sqlite3_errmsg(DB));
}
sqlite3_reset(STMT_DISK_OLDEST);
sqlite3_reset(STMT_INLINE_OLDEST);
errc = SQLITE_ROW;
while(bytesToFree > 0 && (errc = sqlite3_step(STMT_INLINE_OLDEST)) == SQLITE_ROW) {
const void* data = sqlite3_column_blob(STMT_INLINE_OLDEST, 0);
int dataSize = sqlite3_column_bytes(STMT_INLINE_OLDEST, 0);
if(data && dataSize == 32) {
Hash_t hash;
std::memcpy(hash.data(), data, 32);
size_t entrySize = static_cast<size_t>(sqlite3_column_int64(STMT_INLINE_OLDEST, 1));
sqlite3_bind_blob(STMT_INLINE_REMOVE, 1, (const void*) hash.data(), 32, SQLITE_STATIC);
if(sqlite3_step(STMT_INLINE_REMOVE) != SQLITE_DONE) {
sqlite3_reset(STMT_INLINE_REMOVE);
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_INLINE_REMOVE: " << sqlite3_errmsg(DB));
}
sqlite3_reset(STMT_INLINE_REMOVE);
if(DatabaseSize >= entrySize)
DatabaseSize -= entrySize;
else
DatabaseSize = 0;
if(bytesToFree > entrySize)
bytesToFree -= entrySize;
else
bytesToFree = 0;
}
}
if(errc != SQLITE_DONE && errc != SQLITE_ROW) {
sqlite3_reset(STMT_INLINE_OLDEST);
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_INLINE_OLDEST: " << sqlite3_errmsg(DB));
}
sqlite3_reset(STMT_INLINE_OLDEST);
}
if(res.size() <= SMALL_RESOURCE) {
Hash_t hash = res.hash();
@@ -562,6 +448,7 @@ void AssetsManager::readWriteThread(AsyncUseControl::Lock lock) {
}
sqlite3_reset(STMT_INLINE_INSERT);
DatabaseSize += res.size();
} catch(const std::exception& exc) {
LOG.error() << "Произошла ошибка при сохранении " << hashToString(hash);
throw;
@@ -598,6 +485,7 @@ void AssetsManager::readWriteThread(AsyncUseControl::Lock lock) {
}
sqlite3_reset(STMT_DISK_INSERT);
DatabaseSize += res.size();
}
continue;
@@ -611,7 +499,7 @@ void AssetsManager::readWriteThread(AsyncUseControl::Lock lock) {
}
}
std::string AssetsManager::hashToString(const Hash_t& hash) {
std::string AssetsCacheManager::hashToString(const Hash_t& hash) {
std::stringstream ss;
ss << std::hex << std::setfill('0');
for (const auto& byte : hash)

View File

@@ -1,15 +1,12 @@
#pragma once
#include "Common/Abstract.hpp"
#include <array>
#include <cassert>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <queue>
#include <string>
#include <unordered_map>
#include <sqlite3.h>
#include <TOSLib.hpp>
#include <TOSAsync.hpp>
@@ -86,49 +83,18 @@ public:
};
/*
Менеджер предоставления ресурсов. Управляет ресурс паками
и хранением кешированных ресурсов с сервера.
Интерфейс однопоточный.
Обработка файлов в отдельном потоке.
Менеджер кеша ресурсов по хэшу.
Интерфейс однопоточный, обработка файлов в отдельном потоке.
*/
class AssetsManager : public IAsyncDestructible {
class AssetsCacheManager : public IAsyncDestructible {
public:
using Ptr = std::shared_ptr<AssetsManager>;
struct ParsedHeader {
EnumAssets Type = EnumAssets::MAX_ENUM;
std::vector<uint32_t> ModelDeps;
std::vector<uint32_t> TextureDeps;
std::vector<uint8_t> Extra;
};
struct BindInfo {
ResourceId LocalId = 0;
ResourceId ServerId = 0;
std::string Domain;
std::string Key;
Hash_t Hash = {};
std::vector<uint8_t> Header;
};
struct ResourceKey {
Hash_t Hash;
EnumAssets Type;
std::string Domain, Key;
ResourceId Id;
};
struct BindResult {
ResourceId LocalId = 0;
bool Changed = false;
};
using Ptr = std::shared_ptr<AssetsCacheManager>;
public:
virtual ~AssetsManager();
static std::shared_ptr<AssetsManager> Create(asio::io_context &ioc, const fs::path& cachePath,
virtual ~AssetsCacheManager();
static std::shared_ptr<AssetsCacheManager> Create(asio::io_context &ioc, const fs::path& cachePath,
size_t maxCacheDirectorySize = 8*1024*1024*1024ULL, size_t maxLifeTime = 7*24*60*60) {
return createShared(ioc, new AssetsManager(ioc, cachePath, maxCacheDirectorySize, maxLifeTime));
return createShared(ioc, new AssetsCacheManager(ioc, cachePath, maxCacheDirectorySize, maxLifeTime));
}
// Добавить новый полученный с сервера ресурс
@@ -136,13 +102,13 @@ public:
WriteQueue.lock()->push_range(resources);
}
// Добавить задачи на чтение
void pushReads(std::vector<ResourceKey> keys) {
ReadQueue.lock()->push_range(keys);
// Добавить задачи на чтение по хэшу
void pushReads(std::vector<Hash_t> hashes) {
ReadQueue.lock()->push_range(hashes);
}
// Получить считанные данные
std::vector<std::pair<ResourceKey, std::optional<Resource>>> pullReads() {
// Получить считанные данные по хэшу
std::vector<std::pair<Hash_t, std::optional<Resource>>> pullReads() {
return std::move(*ReadyQueue.lock());
}
@@ -159,13 +125,6 @@ public:
lock->MaxChange = true;
}
// Установка путей до папок assets
void setResourcePacks(std::vector<fs::path> packsAssets) {
auto lock = Changes.lock();
lock->Assets = std::move(packsAssets);
lock->AssetsChange = true;
}
// Запуск процедуры проверки хешей всего хранимого кеша
void runFullDatabaseRecheck(std::move_only_function<void(std::string result)>&& func) {
auto lock = Changes.lock();
@@ -177,20 +136,6 @@ public:
return IssuedAnError;
}
// Получить или создать локальный идентификатор ресурса
ResourceId getId(EnumAssets type, const std::string& domain, const std::string& key);
std::optional<ResourceId> getLocalIdFromServer(EnumAssets type, ResourceId serverId) const;
const BindInfo* getBind(EnumAssets type, ResourceId localId) const;
BindResult bindServerResource(EnumAssets type, ResourceId serverId, const std::string& domain, const std::string& key,
const Hash_t& hash, std::vector<uint8_t> header);
std::optional<ResourceId> unbindServerResource(EnumAssets type, ResourceId serverId);
void clearServerBindings();
static std::optional<ParsedHeader> parseHeader(const std::vector<uint8_t>& data);
static std::vector<uint8_t> buildHeader(EnumAssets type, const std::vector<uint32_t>& modelDeps,
const std::vector<uint32_t>& textureDeps, const std::vector<uint8_t>& extra);
std::vector<uint8_t> rebindHeader(const std::vector<uint8_t>& header) const;
private:
Logger LOG = "Client>ResourceHandler";
const fs::path
@@ -207,26 +152,27 @@ private:
*STMT_DISK_CONTAINS = nullptr, // Проверка наличия хеша
*STMT_DISK_SUM = nullptr, // Вычисляет занятое место на диске
*STMT_DISK_COUNT = nullptr, // Возвращает количество записей
*STMT_DISK_OLDEST = nullptr, // Самые старые записи на диске
*STMT_INLINE_INSERT = nullptr, // Вставка ресурса
*STMT_INLINE_GET = nullptr, // Поиск ресурса по хешу
*STMT_INLINE_UPDATE_TIME = nullptr, // Обновить дату последнего использования
*STMT_INLINE_SUM = nullptr, // Размер внутреннего хранилища
*STMT_INLINE_COUNT = nullptr; // Возвращает количество записей
*STMT_INLINE_COUNT = nullptr, // Возвращает количество записей
*STMT_INLINE_REMOVE = nullptr, // Удалить ресурс
*STMT_INLINE_OLDEST = nullptr; // Самые старые записи в базе
// Полный размер данных на диске (насколько известно)
volatile size_t DatabaseSize = 0;
// Очередь задач на чтение
TOS::SpinlockObject<std::queue<ResourceKey>> ReadQueue;
TOS::SpinlockObject<std::queue<Hash_t>> ReadQueue;
// Очередь на запись ресурсов
TOS::SpinlockObject<std::queue<Resource>> WriteQueue;
// Очередь на выдачу результатов чтения
TOS::SpinlockObject<std::vector<std::pair<ResourceKey, std::optional<Resource>>>> ReadyQueue;
TOS::SpinlockObject<std::vector<std::pair<Hash_t, std::optional<Resource>>>> ReadyQueue;
struct Changes_t {
std::vector<fs::path> Assets;
volatile bool AssetsChange = false;
size_t MaxCacheDatabaseSize, MaxLifeTime;
volatile bool MaxChange = false;
std::optional<std::move_only_function<void(std::string)>> OnRecheckEnd;
@@ -237,15 +183,10 @@ private:
bool NeedShutdown = false, IssuedAnError = false;
std::thread OffThread;
mutable std::mutex MapMutex;
std::unordered_map<EnumAssets, std::unordered_map<std::string, std::unordered_map<std::string, ResourceId>>> DKToId;
std::unordered_map<EnumAssets, std::unordered_map<ResourceId, ResourceId>> ServerToLocal;
std::unordered_map<EnumAssets, std::unordered_map<ResourceId, BindInfo>> LocalBinds;
std::array<ResourceId, (int) EnumAssets::MAX_ENUM> NextId = {};
virtual coro<> asyncDestructor();
AssetsManager(boost::asio::io_context &ioc, const fs::path &cachePath,
AssetsCacheManager(boost::asio::io_context &ioc, const fs::path &cachePath,
size_t maxCacheDatabaseSize, size_t maxLifeTime);
void readWriteThread(AsyncUseControl::Lock lock);

View File

@@ -18,6 +18,7 @@
#include <sstream>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <utility>
@@ -26,51 +27,6 @@ namespace LV {
namespace fs = std::filesystem;
PrecompiledTexturePipeline compileTexturePipeline(const std::string &cmd, std::string_view defaultDomain) {
PrecompiledTexturePipeline result;
std::string_view view(cmd);
const size_t trimPos = view.find_first_not_of(" \t\r\n");
if(trimPos == std::string_view::npos)
MAKE_ERROR("Пустая текстурная команда");
view = view.substr(trimPos);
const bool isPipeline = view.size() >= 3
&& view.compare(0, 3, "tex") == 0
&& (view.size() == 3 || std::isspace(static_cast<unsigned char>(view[3])));
if(!isPipeline) {
auto [domain, key] = parseDomainKey(std::string(view), defaultDomain);
result.Assets.emplace_back(std::move(domain), std::move(key));
return result;
}
TexturePipelineProgram program;
std::string err;
if(!program.compile(std::string(view), &err)) {
MAKE_ERROR("Ошибка разбора pipeline: " << err);
}
result.IsSource = true;
result.Pipeline.assign(reinterpret_cast<const char8_t*>(view.data()), view.size());
std::unordered_set<std::string> seen;
for(const auto& patch : program.patches()) {
auto [domain, key] = parseDomainKey(patch.Name, defaultDomain);
std::string token;
token.reserve(domain.size() + key.size() + 1);
token.append(domain);
token.push_back(':');
token.append(key);
if(seen.insert(token).second)
result.Assets.emplace_back(std::move(domain), std::move(key));
}
return result;
}
std::u8string compressVoxels_byte(const std::vector<VoxelCube>& voxels) {
std::u8string compressed;
std::vector<DefVoxelId> defines;
@@ -868,7 +824,21 @@ std::u8string unCompressLinear(std::u8string_view data) {
return *(std::u8string*) &outString;
}
PreparedNodeState::PreparedNodeState(const std::string_view modid, const js::object& profile) {
ResourceHeader HeadlessNodeState::parse(const js::object& profile, const std::function<AssetsModel(const std::string_view model)>& modelResolver) {
std::vector<AssetsModel> headerIds;
std::function<uint16_t(const std::string_view model)> headerResolver =
[&](const std::string_view model) -> uint16_t {
AssetsModel id = modelResolver(model);
auto iter = std::find(headerIds.begin(), headerIds.end(), id);
if(iter == headerIds.end()) {
headerIds.push_back(id);
return headerIds.size()-1;
}
return iter-headerIds.begin();
};
for(auto& [condition, variability] : profile) {
// Распарсить условие
uint16_t node = parseCondition(condition);
@@ -881,39 +851,40 @@ PreparedNodeState::PreparedNodeState(const std::string_view modid, const js::obj
if(variability.is_array()) {
// Варианты условия
for(const js::value& model : variability.as_array()) {
models.push_back(parseModel(modid, model.as_object()));
models.push_back(parseModel(model.as_object(), headerResolver));
}
HasVariability = true;
} else if (variability.is_object()) {
// Один список моделей на условие
models.push_back(parseModel(modid, variability.as_object()));
models.push_back(parseModel(variability.as_object(), headerResolver));
} else {
MAKE_ERROR("Условию должен соответствовать список или объект");
}
Routes.emplace_back(node, std::move(models));
}
ResourceHeader rh;
rh.reserve(headerIds.size()*sizeof(AssetsModel));
for(AssetsModel id : headerIds) {
rh += std::u8string_view((const char8_t*) &id, sizeof(AssetsModel));
}
return rh;
}
PreparedNodeState::PreparedNodeState(const std::string_view modid, const sol::table& profile) {
ResourceHeader HeadlessNodeState::parse(const sol::table& profile, const std::function<AssetsModel(const std::string_view model)>& modelResolver) {
return std::u8string();
}
PreparedNodeState::PreparedNodeState(const std::u8string_view data) {
void HeadlessNodeState::load(const std::u8string_view data) {
Net::LinearReader lr(data);
lr.read<uint16_t>();
uint16_t size;
lr >> size;
LocalToModel.reserve(size);
for(int counter = 0; counter < size; counter++) {
AssetsModel modelId;
lr >> modelId;
LocalToModel.push_back(modelId);
}
lr >> size;
Nodes.reserve(size);
@@ -1026,21 +997,12 @@ PreparedNodeState::PreparedNodeState(const std::u8string_view data) {
lr.checkUnreaded();
}
std::u8string PreparedNodeState::dump() const {
std::u8string HeadlessNodeState::dump() const {
Net::Packet result;
const char magic[] = "bn";
result.write(reinterpret_cast<const std::byte*>(magic), 2);
// ResourceToLocalId
assert(LocalToModelKD.size() < (1 << 16));
assert(LocalToModelKD.size() == LocalToModel.size());
result << uint16_t(LocalToModel.size());
for(AssetsModel modelId : LocalToModel) {
result << modelId;
}
// Nodes
assert(Nodes.size() < (1 << 16));
result << uint16_t(Nodes.size());
@@ -1112,7 +1074,7 @@ std::u8string PreparedNodeState::dump() const {
return result.complite();
}
uint16_t PreparedNodeState::parseCondition(const std::string_view expression) {
uint16_t HeadlessNodeState::parseCondition(const std::string_view expression) {
enum class EnumTokenKind {
LParen, RParen,
Plus, Minus, Star, Slash, Percent,
@@ -1424,55 +1386,11 @@ uint16_t PreparedNodeState::parseCondition(const std::string_view expression) {
};
return lambdaParse(0);
// std::unordered_map<std::string, int> vars;
// std::function<int(uint16_t)> lambdaCalcNode = [&](uint16_t nodeId) -> int {
// const Node& node = Nodes[nodeId];
// if(const Node::Num* value = std::get_if<Node::Num>(&node.v)) {
// return value->v;
// } else if(const Node::Var* value = std::get_if<Node::Var>(&node.v)) {
// auto iter = vars.find(value->name);
// if(iter == vars.end())
// MAKE_ERROR("Неопознанное состояние");
// return iter->second;
// } else if(const Node::Unary* value = std::get_if<Node::Unary>(&node.v)) {
// int rNodeValue = lambdaCalcNode(value->rhs);
// switch(value->op) {
// case Op::Not: return !rNodeValue;
// case Op::Pos: return +rNodeValue;
// case Op::Neg: return -rNodeValue;
// default:
// std::unreachable();
// }
// } else if(const Node::Binary* value = std::get_if<Node::Binary>(&node.v)) {
// int lNodeValue = lambdaCalcNode(value->lhs);
// int rNodeValue = lambdaCalcNode(value->rhs);
// switch(value->op) {
// case Op::Add: return lNodeValue+rNodeValue;
// case Op::Sub: return lNodeValue-rNodeValue;
// case Op::Mul: return lNodeValue*rNodeValue;
// case Op::Div: return lNodeValue/rNodeValue;
// case Op::Mod: return lNodeValue%rNodeValue;
// case Op::LT: return lNodeValue<rNodeValue;
// case Op::LE: return lNodeValue<=rNodeValue;
// case Op::GT: return lNodeValue>rNodeValue;
// case Op::GE: return lNodeValue>=rNodeValue;
// case Op::EQ: return lNodeValue==rNodeValue;
// case Op::NE: return lNodeValue!=rNodeValue;
// case Op::And: return lNodeValue&&rNodeValue;
// case Op::Or: return lNodeValue||rNodeValue;
// default:
// std::unreachable();
// }
// } else {
// std::unreachable();
// }
// };
}
std::pair<float, std::variant<HeadlessNodeState::Model, HeadlessNodeState::VectorModel>> HeadlessNodeState::parseModel(const std::string_view modid, const js::object& obj) {
std::pair<float, std::variant<HeadlessNodeState::Model, HeadlessNodeState::VectorModel>>
HeadlessNodeState::parseModel(const js::object& obj, const std::function<uint16_t(const std::string_view model)>& modelResolver)
{
// ModelToLocalId
bool uvlock;
@@ -1497,22 +1415,7 @@ std::pair<float, std::variant<HeadlessNodeState::Model, HeadlessNodeState::Vecto
Model result;
result.UVLock = false;
result.Transforms = std::move(transforms);
auto [domain, key] = parseDomainKey((std::string) *model_key, modid);
uint16_t resId = 0;
for(auto& [lDomain, lKey] : LocalToModelKD) {
if(lDomain == domain && lKey == key)
break;
resId++;
}
if(resId == LocalToModelKD.size()) {
LocalToModelKD.emplace_back(domain, key);
}
result.Id = resId;
result.Id = modelResolver(*model_key);
return {weight, result};
} else if(model.is_array()) {
@@ -1533,21 +1436,7 @@ std::pair<float, std::variant<HeadlessNodeState::Model, HeadlessNodeState::Vecto
subModel.Transforms = parseTransormations(transformations_val->as_array());
}
auto [domain, key] = parseDomainKey((std::string) js_obj.at("model").as_string(), modid);
uint16_t resId = 0;
for(auto& [lDomain, lKey] : LocalToModelKD) {
if(lDomain == domain && lKey == key)
break;
resId++;
}
if(resId == LocalToModelKD.size()) {
LocalToModelKD.emplace_back(domain, key);
}
subModel.Id = resId;
subModel.Id = modelResolver((std::string) js_obj.at("model").as_string());
result.Models.push_back(std::move(subModel));
}
@@ -1557,7 +1446,7 @@ std::pair<float, std::variant<HeadlessNodeState::Model, HeadlessNodeState::Vecto
}
}
std::vector<Transformation> PreparedNodeState::parseTransormations(const js::array& arr) {
std::vector<Transformation> HeadlessNodeState::parseTransormations(const js::array& arr) {
std::vector<Transformation> result;
for(const js::value& js_value : arr) {
@@ -1596,7 +1485,42 @@ std::vector<Transformation> PreparedNodeState::parseTransormations(const js::arr
}
PreparedModel::PreparedModel(const std::string_view modid, const js::object& profile) {
ResourceHeader HeadlessModel::parse(
const js::object& profile,
const std::function<AssetsModel(const std::string_view model)>& modelResolver,
const std::function<std::vector<uint8_t>(const std::string_view texturePipelineSrc)>& textureResolver
) {
std::vector<AssetsModel> headerIdsModels;
std::function<uint16_t(const std::string_view model)> headerResolverModel =
[&](const std::string_view model) -> uint16_t {
AssetsModel id = modelResolver(model);
auto iter = std::find(headerIdsModels.begin(), headerIdsModels.end(), id);
if(iter == headerIdsModels.end()) {
headerIdsModels.push_back(id);
return headerIdsModels.size()-1;
}
return iter-headerIdsModels.begin();
};
std::vector<std::vector<uint8_t>> headerIdsTextures;
std::unordered_map<std::string, uint32_t, detail::TSVHash, detail::TSVEq> textureToLocal;
std::function<uint16_t(const std::string_view texturePipelineSrc)> headerResolverTexture =
[&](const std::string_view texturePipelineSrc) -> uint16_t {
auto iter = textureToLocal.find(texturePipelineSrc);
if(iter != textureToLocal.end()) {
return iter->second;
}
std::vector<uint8_t> program = textureResolver(texturePipelineSrc);
headerIdsTextures.push_back(program);
uint16_t id = textureToLocal[(std::string) texturePipelineSrc] = headerIdsTextures.size()-1;
return id;
};
if(profile.contains("gui_light")) {
std::string_view gui_light = profile.at("gui_light").as_string();
@@ -1649,7 +1573,7 @@ PreparedModel::PreparedModel(const std::string_view modid, const js::object& pro
const js::object& textures = textures_val->as_object();
for(const auto& [key, value] : textures) {
Textures[key] = compileTexturePipeline((std::string) value.as_string(), modid);
Textures[key] = headerResolverTexture(value.as_string());
}
}
@@ -1802,19 +1726,17 @@ PreparedModel::PreparedModel(const std::string_view modid, const js::object& pro
for(const js::value& value : submodels) {
if(const auto model_key = value.try_as_string()) {
auto [domain, key] = parseDomainKey((std::string) *model_key, modid);
SubModels.push_back({std::move(domain), std::move(key), std::nullopt});
SubModels.emplace_back(headerResolverModel(*model_key), std::nullopt);
} else {
const js::object& obj = value.as_object();
const std::string model_key_str = (std::string) obj.at("model").as_string();
auto [domain, key] = parseDomainKey(model_key_str, modid);
std::optional<uint16_t> scene;
if(const auto scene_val = obj.try_at("scene")) {
scene = static_cast<uint16_t>(scene_val->to_number<int>());
}
SubModels.push_back({std::move(domain), std::move(key), scene});
SubModels.emplace_back(headerResolverModel(model_key_str), scene);
}
}
}
@@ -1826,14 +1748,10 @@ PreparedModel::PreparedModel(const std::string_view modid, const js::object& pro
SubModel result;
if(auto path = sub_val.try_as_string()) {
auto [domain, key] = parseDomainKey((std::string) path.value(), modid);
result.Domain = std::move(domain);
result.Key = std::move(key);
result.Id = headerResolverModel(path.value());
} else {
const js::object& sub = sub_val.as_object();
auto [domain, key] = parseDomainKey((std::string) sub.at("path").as_string(), modid);
result.Domain = std::move(domain);
result.Key = std::move(key);
result.Id = headerResolverModel(sub.at("path").as_string());
if(boost::system::result<const js::value&> scene_val = profile.try_at("scene"))
result.Scene = scene_val->to_number<uint16_t>();
@@ -1842,13 +1760,42 @@ PreparedModel::PreparedModel(const std::string_view modid, const js::object& pro
SubModels.emplace_back(std::move(result));
}
}
// Заголовок
TOS::ByteBuffer rh;
{
uint32_t fullSize = 0;
for(const auto& vector : headerIdsTextures)
fullSize += vector.size();
rh.reserve(2+headerIdsModels.size()*sizeof(AssetsModel)+2+(4)*headerIdsTextures.size()+fullSize);
}
TOS::ByteBuffer::Writer wr;
wr << uint16_t(headerIdsModels.size());
for(AssetsModel id : headerIdsModels)
wr << id;
wr << uint16_t(headerIdsTextures.size());
for(const auto& pipe : headerIdsTextures) {
wr << uint32_t(pipe.size());
wr << pipe;
}
TOS::ByteBuffer buff = wr.complite();
return std::u8string((const char8_t*) buff.data(), buff.size());
}
PreparedModel::PreparedModel(const std::string_view modid, const sol::table& profile) {
ResourceHeader HeadlessModel::parse(
const sol::table& profile,
const std::function<AssetsModel(const std::string_view model)>& modelResolver,
const std::function<std::vector<uint8_t>(const std::string_view texturePipelineSrc)>& textureResolver
) {
std::unreachable();
}
PreparedModel::PreparedModel(const std::u8string& data) {
void HeadlessModel::load(const std::u8string_view data) {
Net::LinearReader lr(data);
lr.read<uint16_t>();
@@ -1895,17 +1842,10 @@ PreparedModel::PreparedModel(const std::u8string& data) {
for(int counter = 0; counter < size; counter++) {
std::string tkey;
lr >> tkey;
TexturePipeline pipe;
uint16_t id;
lr >> id;
uint16_t size;
lr >> size;
pipe.BinTextures.reserve(size);
for(int iter = 0; iter < size; iter++)
pipe.BinTextures.push_back(lr.read<ResourceId>());
lr >> (std::string&) pipe.Pipeline;
CompiledTextures.insert({tkey, std::move(pipe)});
Textures.insert({tkey, id});
}
lr >> size;
@@ -1962,7 +1902,7 @@ PreparedModel::PreparedModel(const std::u8string& data) {
SubModels.reserve(size8);
for(int counter = 0; counter < size8; counter++) {
SubModel sub;
lr >> sub.Domain >> sub.Key;
lr >> sub.Id;
uint16_t val = lr.read<uint16_t>();
if(val != uint16_t(-1)) {
sub.Scene = val;
@@ -1974,7 +1914,7 @@ PreparedModel::PreparedModel(const std::u8string& data) {
lr.checkUnreaded();
}
std::u8string PreparedModel::dump() const {
std::u8string HeadlessModel::dump() const {
Net::Packet result;
result << 'b' << 'm';
@@ -2013,19 +1953,9 @@ std::u8string PreparedModel::dump() const {
assert(Textures.size() < (1 << 16));
result << uint16_t(Textures.size());
assert(CompiledTextures.size() == Textures.size());
for(const auto& [tkey, dk] : CompiledTextures) {
for(const auto& [tkey, id] : Textures) {
assert(tkey.size() < 32);
result << tkey;
assert(dk.BinTextures.size() < 512);
result << uint16_t(dk.BinTextures.size());
for(size_t iter = 0; iter < dk.BinTextures.size(); iter++) {
result << dk.BinTextures[iter];
}
result << (const std::string&) dk.Pipeline;
result << tkey << id;
}
assert(Cuboids.size() < (1 << 16));
@@ -2064,10 +1994,7 @@ std::u8string PreparedModel::dump() const {
assert(SubModels.size() < 256);
result << uint8_t(SubModels.size());
for(const SubModel& model : SubModels) {
assert(model.Domain.size() < 32);
assert(model.Key.size() < 32);
result << model.Domain << model.Key;
result << model.Id;
if(model.Scene)
result << uint16_t(*model.Scene);
else

View File

@@ -19,6 +19,45 @@
namespace LV {
namespace detail {
// Позволяет использовать как std::string так и std::string_view в хэш таблицах
struct TSVHash {
using is_transparent = void;
size_t operator()(std::string_view sv) const noexcept {
return std::hash<std::string_view>{}(sv);
}
size_t operator()(const std::string& s) const noexcept {
return std::hash<std::string_view>{}(s);
}
};
// Позволяет использовать как std::string так и std::string_view в хэш таблицах
struct TSVEq {
using is_transparent = void;
bool operator()(std::string_view a, std::string_view b) const noexcept {
return a == b;
}
bool operator()(const std::string& a, std::string_view b) const noexcept {
return std::string_view(a) == b;
}
bool operator()(std::string_view a, const std::string& b) const noexcept {
return a == std::string_view(b);
}
bool operator()(const std::string& a, const std::string& b) const noexcept {
return a == b;
}
};
}
namespace js = boost::json;
namespace Pos {
@@ -514,29 +553,6 @@ inline std::pair<std::string, std::string> parseDomainKey(const std::string& val
}
}
struct PrecompiledTexturePipeline {
// Локальные идентификаторы пайплайна в домен+ключ
std::vector<std::pair<std::string, std::string>> Assets;
// Чистый код текстурных преобразований, локальные идентификаторы связаны с Assets
std::u8string Pipeline;
// Pipeline содержит исходный текст (tex ...), нужен для компиляции на сервере
bool IsSource = false;
};
struct TexturePipeline {
// Разыменованые идентификаторы
std::vector<AssetsTexture> BinTextures;
// Чистый код текстурных преобразований, локальные идентификаторы связаны с BinTextures
std::u8string Pipeline;
bool operator==(const TexturePipeline& other) const {
return BinTextures == other.BinTextures && Pipeline == other.Pipeline;
}
};
// Компилятор текстурных потоков
PrecompiledTexturePipeline compileTexturePipeline(const std::string &cmd, std::string_view defaultDomain = "core");
struct NodestateEntry {
std::string Name;
int Variability = 0; // Количество возможный значений состояния
@@ -646,8 +662,6 @@ struct HeadlessNodeState {
std::vector<Transformation> Transforms;
};
// Локальный идентификатор в именной ресурс
std::vector<std::string> LocalToModelKD;
// Ноды выражений
std::vector<Node> Nodes;
// Условия -> вариации модели + веса
@@ -865,7 +879,7 @@ private:
bool HasVariability = false;
uint16_t parseCondition(const std::string_view condition);
std::pair<float, std::variant<Model, VectorModel>> parseModel(const std::string_view modid, const js::object& obj);
std::pair<float, std::variant<Model, VectorModel>> parseModel(const js::object& obj, const std::function<uint16_t(const std::string_view model)>& modelResolver);
std::vector<Transformation> parseTransormations(const js::array& arr);
};
@@ -893,8 +907,7 @@ struct HeadlessModel {
};
std::unordered_map<std::string, FullTransformation> Display;
std::unordered_map<std::string, PrecompiledTexturePipeline> Textures;
std::unordered_map<std::string, TexturePipeline> CompiledTextures;
std::unordered_map<std::string, uint16_t> Textures;
struct Cuboid {
bool Shade;
@@ -916,7 +929,7 @@ struct HeadlessModel {
std::vector<Cuboid> Cuboids;
struct SubModel {
std::string Domain, Key;
uint16_t Id;
std::optional<uint16_t> Scene;
};
@@ -962,8 +975,7 @@ struct HeadlessModel {
struct PreparedGLTF {
std::vector<std::string> TextureKey;
std::unordered_map<std::string, PrecompiledTexturePipeline> Textures;
std::unordered_map<std::string, TexturePipeline> CompiledTextures;
std::unordered_map<std::string, uint16_t> Textures;
std::vector<Vertex> Vertices;
@@ -1060,19 +1072,4 @@ struct hash<LV::Hash_t> {
return v;
}
};
template <>
struct hash<LV::TexturePipeline> {
std::size_t operator()(const LV::TexturePipeline& tp) const noexcept {
size_t seed = 0;
for (const auto& tex : tp.BinTextures)
seed ^= std::hash<LV::AssetsTexture>{}(tex) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
std::string_view sv(reinterpret_cast<const char*>(tp.Pipeline.data()), tp.Pipeline.size());
seed ^= std::hash<std::string_view>{}(sv) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
return seed;
}
};
}

View File

@@ -46,44 +46,6 @@ static constexpr const char* EnumAssetsToDirectory(LV::EnumAssets value) {
namespace LV {
namespace detail {
// Позволяет использовать как std::string так и std::string_view в хэш таблицах
struct TSVHash {
using is_transparent = void;
size_t operator()(std::string_view sv) const noexcept {
return std::hash<std::string_view>{}(sv);
}
size_t operator()(const std::string& s) const noexcept {
return std::hash<std::string_view>{}(s);
}
};
// Позволяет использовать как std::string так и std::string_view в хэш таблицах
struct TSVEq {
using is_transparent = void;
bool operator()(std::string_view a, std::string_view b) const noexcept {
return a == b;
}
bool operator()(const std::string& a, std::string_view b) const noexcept {
return std::string_view(a) == b;
}
bool operator()(std::string_view a, const std::string& b) const noexcept {
return a == std::string_view(b);
}
bool operator()(const std::string& a, const std::string& b) const noexcept {
return a == b;
}
};
}
namespace fs = std::filesystem;
using AssetType = EnumAssets;

View File

@@ -1,763 +0,0 @@
#include "AssetsManager.hpp"
#include "Common/Abstract.hpp"
#include "Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp"
#include "boost/json.hpp"
#include "png++/rgb_pixel.hpp"
#include <algorithm>
#include <cstring>
#include <exception>
#include <filesystem>
#include <png.h>
#include <pngconf.h>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include "sol/sol.hpp"
namespace LV::Server {
PreparedModel::PreparedModel(const std::string& domain, const LV::PreparedModel& model) {
Cuboids.reserve(model.Cuboids.size());
for(auto& [key, cmd] : model.Textures) {
for(auto& [domain, key] : cmd.Assets) {
TextureDependencies[domain].push_back(key);
}
}
for(auto& sub : model.SubModels) {
ModelDependencies[sub.Domain].push_back(sub.Key);
}
// for(const PreparedModel::Cuboid& cuboid : model.Cuboids) {
// Cuboid result;
// result.From = cuboid.From;
// result.To = cuboid.To;
// result.Faces = 0;
// for(const auto& [key, _] : cuboid.Faces)
// result.Faces |= (1 << int(key));
// result.Transformations = cuboid.Transformations;
// }
}
PreparedModel::PreparedModel(const std::string& domain, const PreparedGLTF& glTF) {
}
void AssetsManager::loadResourceFromFile(EnumAssets type, ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const {
switch(type) {
case EnumAssets::Nodestate: loadResourceFromFile_Nodestate (out, domain, key, path); return;
case EnumAssets::Particle: loadResourceFromFile_Particle (out, domain, key, path); return;
case EnumAssets::Animation: loadResourceFromFile_Animation (out, domain, key, path); return;
case EnumAssets::Model: loadResourceFromFile_Model (out, domain, key, path); return;
case EnumAssets::Texture: loadResourceFromFile_Texture (out, domain, key, path); return;
case EnumAssets::Sound: loadResourceFromFile_Sound (out, domain, key, path); return;
case EnumAssets::Font: loadResourceFromFile_Font (out, domain, key, path); return;
default:
std::unreachable();
}
}
void AssetsManager::loadResourceFromLua(EnumAssets type, ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const {
switch(type) {
case EnumAssets::Nodestate: loadResourceFromLua_Nodestate(out, domain, key, profile); return;
case EnumAssets::Particle: loadResourceFromLua_Particle(out, domain, key, profile); return;
case EnumAssets::Animation: loadResourceFromLua_Animation(out, domain, key, profile); return;
case EnumAssets::Model: loadResourceFromLua_Model(out, domain, key, profile); return;
case EnumAssets::Texture: loadResourceFromLua_Texture(out, domain, key, profile); return;
case EnumAssets::Sound: loadResourceFromLua_Sound(out, domain, key, profile); return;
case EnumAssets::Font: loadResourceFromLua_Font(out, domain, key, profile); return;
default:
std::unreachable();
}
}
void AssetsManager::loadResourceFromFile_Nodestate(ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const {
Resource res(path);
js::object obj = js::parse(std::string_view((const char*) res.data(), res.size())).as_object();
PreparedNodeState pns(domain, obj);
out.NewOrChange_Nodestates[domain].emplace_back(key, std::move(pns), fs::last_write_time(path));
}
void AssetsManager::loadResourceFromFile_Particle(ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const {
std::unreachable();
}
void AssetsManager::loadResourceFromFile_Animation(ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const {
std::unreachable();
}
void AssetsManager::loadResourceFromFile_Model(ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const {
/*
json, glTF, glB
*/
Resource res(path);
std::filesystem::file_time_type ftt = fs::last_write_time(path);
auto extension = path.extension();
if(extension == ".json") {
js::object obj = js::parse(std::string_view((const char*) res.data(), res.size())).as_object();
LV::PreparedModel pm(domain, obj);
out.NewOrChange_Models[domain].emplace_back(key, std::move(pm), ftt);
} else if(extension == ".gltf") {
js::object obj = js::parse(std::string_view((const char*) res.data(), res.size())).as_object();
PreparedGLTF gltf(domain, obj);
out.NewOrChange_Models[domain].emplace_back(key, std::move(gltf), ftt);
} else if(extension == ".glb") {
PreparedGLTF gltf(domain, res);
out.NewOrChange_Models[domain].emplace_back(key, std::move(gltf), ftt);
} else {
MAKE_ERROR("Не поддерживаемый формат файла");
}
}
void AssetsManager::loadResourceFromFile_Texture(ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const {
Resource res(path);
if(res.size() < 8)
MAKE_ERROR("Файл не является текстурой png или jpeg (недостаточный размер файла)");
if(png_check_sig(reinterpret_cast<png_bytep>((unsigned char*) res.data()), 8)) {
// Это png
fs::file_time_type lwt = fs::last_write_time(path);
out.NewOrChange[(int) EnumAssets::Texture][domain].emplace_back(key, res, lwt);
return;
} else if((int) res.data()[0] == 0xFF && (int) res.data()[1] == 0xD8) {
// Это jpeg
fs::file_time_type lwt = fs::last_write_time(path);
out.NewOrChange[(int) EnumAssets::Texture][domain].emplace_back(key, res, lwt);
return;
} else {
MAKE_ERROR("Файл не является текстурой png или jpeg");
}
std::unreachable();
}
void AssetsManager::loadResourceFromFile_Sound(ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const {
std::unreachable();
}
void AssetsManager::loadResourceFromFile_Font(ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const {
std::unreachable();
}
void AssetsManager::loadResourceFromLua_Nodestate(ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const {
if(std::optional<std::string> path = profile.get<std::optional<std::string>>("path")) {
out.NewOrChange[(int) EnumAssets::Nodestate][domain].emplace_back(key, Resource(*path), fs::file_time_type::min());
return;
}
std::unreachable();
}
void AssetsManager::loadResourceFromLua_Particle(ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const {
if(std::optional<std::string> path = profile.get<std::optional<std::string>>("path")) {
out.NewOrChange[(int) EnumAssets::Particle][domain].emplace_back(key, Resource(*path), fs::file_time_type::min());
return;
}
std::unreachable();
}
void AssetsManager::loadResourceFromLua_Animation(ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const {
if(std::optional<std::string> path = profile.get<std::optional<std::string>>("path")) {
out.NewOrChange[(int) EnumAssets::Animation][domain].emplace_back(key, Resource(*path), fs::file_time_type::min());
return;
}
std::unreachable();
}
void AssetsManager::loadResourceFromLua_Model(ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const {
if(std::optional<std::string> path = profile.get<std::optional<std::string>>("path")) {
out.NewOrChange[(int) EnumAssets::Model][domain].emplace_back(key, Resource(*path), fs::file_time_type::min());
return;
}
std::unreachable();
}
void AssetsManager::loadResourceFromLua_Texture(ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const {
if(std::optional<std::string> path = profile.get<std::optional<std::string>>("path")) {
out.NewOrChange[(int) EnumAssets::Texture][domain].emplace_back(key, Resource(*path), fs::file_time_type::min());
return;
}
std::unreachable();
}
void AssetsManager::loadResourceFromLua_Sound(ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const {
if(std::optional<std::string> path = profile.get<std::optional<std::string>>("path")) {
out.NewOrChange[(int) EnumAssets::Sound][domain].emplace_back(key, Resource(*path), fs::file_time_type::min());
return;
}
std::unreachable();
}
void AssetsManager::loadResourceFromLua_Font(ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const {
if(std::optional<std::string> path = profile.get<std::optional<std::string>>("path")) {
out.NewOrChange[(int) EnumAssets::Font][domain].emplace_back(key, Resource(*path), fs::file_time_type::min());
return;
}
std::unreachable();
}
AssetsManager::AssetsManager(asio::io_context& ioc)
{
}
AssetsManager::~AssetsManager() = default;
std::tuple<ResourceId, std::optional<AssetsManager::DataEntry>&> AssetsManager::Local::nextId(EnumAssets type) {
auto& table = Table[(int) type];
ResourceId id = -1;
std::optional<DataEntry> *data = nullptr;
for(size_t index = 0; index < table.size(); index++) {
auto& entry = *table[index];
if(index == 0 && entry.Empty.test(0)) {
entry.Empty.reset(0);
}
if(entry.IsFull)
continue;
uint32_t pos = entry.Empty._Find_first();
if(pos == entry.Empty.size()) {
entry.IsFull = true;
continue;
}
entry.Empty.reset(pos);
if(entry.Empty._Find_next(pos) == entry.Empty.size())
entry.IsFull = true;
id = index*TableEntry<DataEntry>::ChunkSize + pos;
data = &entry.Entries[pos];
break;
}
if(!data) {
table.emplace_back(std::make_unique<TableEntry<DataEntry>>());
auto& entry = *table.back();
if(table.size() == 1 && entry.Empty.test(0)) {
entry.Empty.reset(0);
}
uint32_t pos = entry.Empty._Find_first();
entry.Empty.reset(pos);
if(entry.Empty._Find_next(pos) == entry.Empty.size())
entry.IsFull = true;
id = (table.size()-1)*TableEntry<DataEntry>::ChunkSize + pos;
data = &entry.Entries[pos];
// Расширяем таблицу с ресурсами, если необходимо
if(type == EnumAssets::Nodestate)
Table_NodeState.emplace_back(std::make_unique<TableEntry<std::vector<AssetsModel>>>());
else if(type == EnumAssets::Model)
Table_Model.emplace_back(std::make_unique<TableEntry<ModelDependency>>());
}
return {id, *data};
}
AssetsManager::ResourceChangeObj AssetsManager::recheckResources(const AssetsRegister& info) {
ResourceChangeObj result;
// Найти пропавшие ресурсы
for(int type = 0; type < (int) EnumAssets::MAX_ENUM; type++) {
auto lock = LocalObj.lock();
for(auto& [domain, resources] : lock->KeyToId[type]) {
for(auto& [key, id] : resources) {
if(!lock->Table[type][id / TableEntry<DataEntry>::ChunkSize]->Entries[id % TableEntry<DataEntry>::ChunkSize])
continue;
bool exists = false;
for(const fs::path& path : info.Assets) {
fs::path file = path / domain;
switch ((EnumAssets) type) {
case EnumAssets::Nodestate: file /= "nodestate"; break;
case EnumAssets::Particle: file /= "particle"; break;
case EnumAssets::Animation: file /= "animation"; break;
case EnumAssets::Model: file /= "model"; break;
case EnumAssets::Texture: file /= "texture"; break;
case EnumAssets::Sound: file /= "sound"; break;
case EnumAssets::Font: file /= "font"; break;
default:
std::unreachable();
}
file /= key;
if(fs::exists(file) && !fs::is_directory(file)) {
exists = true;
break;
}
}
if(exists) continue;
auto iterDomain = info.Custom[type].find(domain);
if(iterDomain == info.Custom[type].end()) {
result.Lost[type][domain].push_back(key);
} else {
auto iterData = iterDomain->second.find(key);
if(iterData == iterDomain->second.end()) {
result.Lost[type][domain].push_back(key);
}
}
}
}
}
// Если ресурс уже был найден более приоритетными директориями, то пропускаем его
std::unordered_map<std::string, std::unordered_set<std::string>> findedResources[(int) EnumAssets::MAX_ENUM];
// Найти новые или изменённые ресурсы
for(int type = 0; type < (int) EnumAssets::MAX_ENUM; type++) {
for(auto& [domain, resources] : info.Custom[type]) {
auto lock = LocalObj.lock();
const auto& keyToId = lock->KeyToId[type];
auto iterDomain = keyToId.find(domain);
auto& findList = findedResources[type][domain];
if(iterDomain == keyToId.end()) {
// Ресурсы данного домена неизвестны
auto& domainList = result.NewOrChange[type][domain];
for(auto& [key, id] : resources) {
// Подобрать идентификатор
// TODO: реализовать регистрации ресурсов из lua
domainList.emplace_back(key, Resource("assets/null"), fs::file_time_type::min());
findList.insert(key);
}
} else {
for(auto& [key, id] : resources) {
if(findList.contains(key))
// Ресурс уже был найден в вышестоящей директории
continue;
else if(iterDomain->second.contains(key)) {
// Ресурс уже есть, TODO: нужно проверить его изменение
loadResourceFromFile((EnumAssets) type, result, domain, key, "assets/null");
} else {
// Ресурс не был известен
loadResourceFromFile((EnumAssets) type, result, domain, key, "assets/null");
}
findList.insert(key);
}
}
}
}
for(const fs::path& path : info.Assets) {
if(!fs::exists(path))
continue;
for(auto begin = fs::directory_iterator(path), end = fs::directory_iterator(); begin != end; begin++) {
if(!begin->is_directory())
continue;
fs::path domainPath = begin->path();
std::string domain = domainPath.filename();
for(int type = 0; type < (int) EnumAssets::MAX_ENUM; type++) {
fs::path resourcesPath = domainPath;
switch ((EnumAssets) type) {
case EnumAssets::Nodestate: resourcesPath /= "nodestate"; break;
case EnumAssets::Particle: resourcesPath /= "particle"; break;
case EnumAssets::Animation: resourcesPath /= "animation"; break;
case EnumAssets::Model: resourcesPath /= "model"; break;
case EnumAssets::Texture: resourcesPath /= "texture"; break;
case EnumAssets::Sound: resourcesPath /= "sound"; break;
case EnumAssets::Font: resourcesPath /= "font"; break;
default:
std::unreachable();
}
auto& findList = findedResources[type][domain];
auto lock = LocalObj.lock();
auto iterDomain = lock->KeyToId[type].find(domain);
if(!fs::exists(resourcesPath) || !fs::is_directory(resourcesPath))
continue;
// Рекурсивно загрузить ресурсы внутри папки resourcesPath
for(auto begin = fs::recursive_directory_iterator(resourcesPath), end = fs::recursive_directory_iterator(); begin != end; begin++) {
if(begin->is_directory())
continue;
fs::path file = begin->path();
std::string key = fs::relative(begin->path(), resourcesPath).string();
if(findList.contains(key))
// Ресурс уже был найден в вышестоящей директории
continue;
else if(iterDomain != lock->KeyToId[type].end() && iterDomain->second.contains(key)) {
// Ресурс уже есть, TODO: нужно проверить его изменение
ResourceId id = iterDomain->second.at(key);
DataEntry& entry = *lock->Table[type][id / TableEntry<DataEntry>::ChunkSize]->Entries[id % TableEntry<DataEntry>::ChunkSize];
fs::file_time_type lwt = fs::last_write_time(file);
if(lwt != entry.FileChangeTime)
// Будем считать что ресурс изменился
loadResourceFromFile((EnumAssets) type, result, domain, key, file);
} else {
// Ресурс не был известен
loadResourceFromFile((EnumAssets) type, result, domain, key, file);
}
findList.insert(key);
}
}
}
}
return result;
}
AssetsManager::Out_applyResourceChange AssetsManager::applyResourceChange(const ResourceChangeObj& orr) {
// Потерянные и обновлённые идентификаторы
Out_applyResourceChange result;
// Удаляем ресурсы
/*
Удаляются только ресурсы, при этом за ними остаётся бронь на идентификатор
Уже скомпилированные зависимости к ресурсам не будут
перекомпилироваться для смены идентификатора. Если нужный ресурс
появится, то привязка останется. Новые клиенты не получат ресурс
которого нет, но он может использоваться
*/
for(int type = 0; type < (int) EnumAssets::MAX_ENUM; type++) {
for(auto& [domain, resources] : orr.Lost[type]) {
auto lock = LocalObj.lock();
auto& keyToIdDomain = lock->KeyToId[type].at(domain);
for(const std::string& key : resources) {
auto iter = keyToIdDomain.find(key);
assert(iter != keyToIdDomain.end());
ResourceId resId = iter->second;
if(type == (int) EnumAssets::Nodestate) {
if(resId / TableEntry<PreparedNodeState>::ChunkSize < lock->Table_NodeState.size()) {
lock->Table_NodeState[resId / TableEntry<PreparedNodeState>::ChunkSize]
->Entries[resId % TableEntry<PreparedNodeState>::ChunkSize].reset();
}
} else if(type == (int) EnumAssets::Model) {
if(resId / TableEntry<ModelDependency>::ChunkSize < lock->Table_Model.size()) {
lock->Table_Model[resId / TableEntry<ModelDependency>::ChunkSize]
->Entries[resId % TableEntry<ModelDependency>::ChunkSize].reset();
}
}
auto& chunk = lock->Table[type][resId / TableEntry<DataEntry>::ChunkSize];
chunk->Entries[resId % TableEntry<DataEntry>::ChunkSize].reset();
}
}
}
// Добавляем
for(int type = 0; type < (int) EnumAssets::MAX_ENUM; type++) {
for(auto& [domain, resources] : orr.NewOrChange[type]) {
auto lock = LocalObj.lock();
auto& keyToIdDomain = lock->KeyToId[type][domain];
for(auto& [key, resource, lwt] : resources) {
ResourceId id = -1;
std::optional<DataEntry>* data = nullptr;
if(auto iterId = keyToIdDomain.find(key); iterId != keyToIdDomain.end()) {
id = iterId->second;
data = &lock->Table[(int) type][id / TableEntry<DataEntry>::ChunkSize]->Entries[id % TableEntry<DataEntry>::ChunkSize];
} else {
auto [_id, _data] = lock->nextId((EnumAssets) type);
id = _id;
data = &_data;
}
result.NewOrChange[type].push_back({id, resource});
keyToIdDomain[key] = id;
data->emplace(lwt, resource, domain, key);
lock->HashToId[resource.hash()] = {(EnumAssets) type, id};
}
}
// Удалённые идентификаторы не считаются удалёнными, если были изменены
std::unordered_set<ResourceId> noc;
for(auto& [id, _] : result.NewOrChange[type])
noc.insert(id);
std::unordered_set<ResourceId> l(result.Lost[type].begin(), result.Lost[type].end());
result.Lost[type].clear();
std::set_difference(l.begin(), l.end(), noc.begin(), noc.end(), std::back_inserter(result.Lost[type]));
}
// Приёмка новых/изменённых описаний состояний нод
if(!orr.NewOrChange_Nodestates.empty())
{
auto lock = LocalObj.lock();
for(auto& [domain, table] : orr.NewOrChange_Nodestates) {
for(auto& [key, _nodestate, ftt] : table) {
ResourceId resId = lock->getId(EnumAssets::Nodestate, domain, key);
std::optional<DataEntry>& data = lock->Table[(int) EnumAssets::Nodestate][resId / TableEntry<DataEntry>::ChunkSize]->Entries[resId % TableEntry<DataEntry>::ChunkSize];
PreparedNodeState nodestate = _nodestate;
// Ресолвим модели
for(const auto& [lDomain, lKey] : nodestate.LocalToModelKD) {
nodestate.LocalToModel.push_back(lock->getId(EnumAssets::Model, lDomain, lKey));
}
// Сдампим для отправки клиенту (Кеш в пролёте?)
Resource res(nodestate.dump());
// На оповещение
result.NewOrChange[(int) EnumAssets::Nodestate].push_back({resId, res});
// Запись в таблице ресурсов
data.emplace(ftt, res, domain, key);
lock->HashToId[res.hash()] = {EnumAssets::Nodestate, resId};
lock->Table_NodeState[resId / TableEntry<DataEntry>::ChunkSize]
->Entries[resId % TableEntry<DataEntry>::ChunkSize] = nodestate.LocalToModel;
}
}
}
// Приёмка новых/изменённых моделей
if(!orr.NewOrChange_Models.empty())
{
auto lock = LocalObj.lock();
for(auto& [domain, table] : orr.NewOrChange_Models) {
auto& keyToIdDomain = lock->KeyToId[(int) EnumAssets::Model][domain];
for(auto& [key, _model, ftt] : table) {
ResourceId resId = -1;
std::optional<DataEntry>* data = nullptr;
if(auto iterId = keyToIdDomain.find(key); iterId != keyToIdDomain.end()) {
resId = iterId->second;
data = &lock->Table[(int) EnumAssets::Model][resId / TableEntry<DataEntry>::ChunkSize]->Entries[resId % TableEntry<DataEntry>::ChunkSize];
} else {
auto [_id, _data] = lock->nextId((EnumAssets) EnumAssets::Model);
resId = _id;
data = &_data;
}
keyToIdDomain[key] = resId;
// Ресолвим текстуры
std::variant<LV::PreparedModel, PreparedGLTF> model = _model;
std::visit([&lock, &domain](auto& val) {
for(const auto& [key, pipeline] : val.Textures) {
TexturePipeline pipe;
if(pipeline.IsSource) {
std::string source(reinterpret_cast<const char*>(pipeline.Pipeline.data()), pipeline.Pipeline.size());
TexturePipelineProgram program;
std::string err;
if(!program.compile(source, &err)) {
MAKE_ERROR("Ошибка компиляции pipeline: " << err);
}
auto resolver = [&](std::string_view name) -> std::optional<uint32_t> {
auto [texDomain, texKey] = parseDomainKey(std::string(name), domain);
return lock->getId(EnumAssets::Texture, texDomain, texKey);
};
if(!program.link(resolver, &err)) {
MAKE_ERROR("Ошибка линковки pipeline: " << err);
}
const std::vector<uint8_t> bytes = program.toBytes();
pipe.Pipeline.resize(bytes.size());
if(!bytes.empty()) {
std::memcpy(pipe.Pipeline.data(), bytes.data(), bytes.size());
}
} else {
pipe.Pipeline = pipeline.Pipeline;
}
for(const auto& [domain, key] : pipeline.Assets) {
ResourceId texId = lock->getId(EnumAssets::Texture, domain, key);
pipe.BinTextures.push_back(texId);
}
val.CompiledTextures[key] = std::move(pipe);
}
}, model);
// Сдампим для отправки клиенту (Кеш в пролёте?)
std::u8string dump = std::visit<std::u8string>([&lock](auto& val) {
return val.dump();
}, model);
Resource res(std::move(dump));
// На оповещение
result.NewOrChange[(int) EnumAssets::Model].push_back({resId, res});
// Запись в таблице ресурсов
data->emplace(ftt, res, domain, key);
lock->HashToId[res.hash()] = {EnumAssets::Model, resId};
// Для нужд сервера, ресолвим зависимости
PreparedModel pm = std::visit<PreparedModel>([&domain](auto& val) {
return PreparedModel(domain, val);
}, model);
ModelDependency deps;
for(auto& [domain2, list] : pm.ModelDependencies) {
for(const std::string& key2 : list) {
ResourceId subResId = lock->getId(EnumAssets::Model, domain2, key2);
deps.ModelDeps.push_back(subResId);
}
}
for(auto& [domain2, list] : pm.TextureDependencies) {
for(const std::string& key2 : list) {
ResourceId subResId = lock->getId(EnumAssets::Texture, domain2, key2);
deps.TextureDeps.push_back(subResId);
}
}
lock->Table_Model[resId / TableEntry<DataEntry>::ChunkSize]
->Entries[resId % TableEntry<DataEntry>::ChunkSize] = std::move(deps);
}
}
}
// Дамп ключей assets
{
std::stringstream result;
auto lock = LocalObj.lock();
for(int type = 0; type < (int) EnumAssets::MAX_ENUM; type++) {
if(type == 0)
result << "Nodestate:\n";
else if(type == 1)
result << "Particle:\n";
else if(type == 2)
result << "Animation:\n";
else if(type == 3)
result << "Model:\n";
else if(type == 4)
result << "Texture:\n";
else if(type == 5)
result << "Sound:\n";
else if(type == 6)
result << "Font:\n";
for(const auto& [domain, list] : lock->KeyToId[type]) {
result << "\t" << domain << ":\n";
for(const auto& [key, id] : list) {
result << "\t\t" << key << " = " << id << '\n';
}
}
}
LOG.debug() << "Дамп ассетов:\n" << result.str();
}
// Вычислить зависимости моделей
{
// Затираем старые данные
auto lock = LocalObj.lock();
for(auto& entriesChunk : lock->Table_Model) {
for(auto& entry : entriesChunk->Entries) {
if(!entry)
continue;
entry->Ready = false;
entry->FullSubTextureDeps.clear();
entry->FullSubModelDeps.clear();
}
}
// Вычисляем зависимости
std::function<void(AssetsModel resId, ModelDependency&)> calcDeps;
calcDeps = [&](AssetsModel resId, ModelDependency& entry) {
for(AssetsModel subResId : entry.ModelDeps) {
auto& model = lock->Table_Model[subResId / TableEntry<ModelDependency>::ChunkSize]
->Entries[subResId % TableEntry<ModelDependency>::ChunkSize];
if(!model)
continue;
if(resId == subResId) {
const auto object1 = lock->getResource(EnumAssets::Model, resId);
const auto object2 = lock->getResource(EnumAssets::Model, subResId);
LOG.warn() << "В моделе " << std::get<1>(*object1) << ':' << std::get<2>(*object1)
<< " обнаружена циклическая зависимость с самой собою";
continue;
}
if(!model->Ready)
calcDeps(subResId, *model);
if(std::binary_search(model->FullSubModelDeps.begin(), model->FullSubModelDeps.end(), resId)) {
// Циклическая зависимость
const auto object1 = lock->getResource(EnumAssets::Model, resId);
const auto object2 = lock->getResource(EnumAssets::Model, subResId);
assert(object1);
LOG.warn() << "В моделе " << std::get<1>(*object1) << ':' << std::get<2>(*object1)
<< " обнаружена циклическая зависимость с " << std::get<1>(*object2) << ':'
<< std::get<2>(*object2);
} else {
entry.FullSubTextureDeps.append_range(model->FullSubTextureDeps);
entry.FullSubModelDeps.push_back(subResId);
entry.FullSubModelDeps.append_range(model->FullSubModelDeps);
}
}
entry.FullSubTextureDeps.append_range(entry.TextureDeps);
{
std::sort(entry.FullSubTextureDeps.begin(), entry.FullSubTextureDeps.end());
auto eraseIter = std::unique(entry.FullSubTextureDeps.begin(), entry.FullSubTextureDeps.end());
entry.FullSubTextureDeps.erase(eraseIter, entry.FullSubTextureDeps.end());
entry.FullSubTextureDeps.shrink_to_fit();
}
{
std::sort(entry.FullSubModelDeps.begin(), entry.FullSubModelDeps.end());
auto eraseIter = std::unique(entry.FullSubModelDeps.begin(), entry.FullSubModelDeps.end());
entry.FullSubModelDeps.erase(eraseIter, entry.FullSubModelDeps.end());
entry.FullSubModelDeps.shrink_to_fit();
}
entry.Ready = true;
};
ssize_t iter = -1;
for(auto& entriesChunk : lock->Table_Model) {
for(auto& entry : entriesChunk->Entries) {
iter++;
if(!entry || entry->Ready)
continue;
// Собираем зависимости
calcDeps(iter, *entry);
}
}
}
return result;
}
}

View File

@@ -1,308 +0,0 @@
#pragma once
#include "Common/Abstract.hpp"
#include "TOSLib.hpp"
#include "Common/Net.hpp"
#include "sha2.hpp"
#include <bitset>
#include <filesystem>
#include <optional>
#include <unordered_map>
#include <unordered_set>
#include <variant>
namespace LV::Server {
namespace fs = std::filesystem;
/*
Используется для расчёта коллизии,
если это необходимо, а также зависимостей к ассетам.
*/
struct PreparedModel {
// Упрощённая коллизия
std::vector<std::pair<glm::vec3, glm::vec3>> Cuboids;
// Зависимости от текстур, которые нужно сообщить клиенту
std::unordered_map<std::string, std::vector<std::string>> TextureDependencies;
// Зависимости от моделей
std::unordered_map<std::string, std::vector<std::string>> ModelDependencies;
PreparedModel(const std::string& domain, const LV::PreparedModel& model);
PreparedModel(const std::string& domain, const PreparedGLTF& glTF);
PreparedModel() = default;
PreparedModel(const PreparedModel&) = default;
PreparedModel(PreparedModel&&) = default;
PreparedModel& operator=(const PreparedModel&) = default;
PreparedModel& operator=(PreparedModel&&) = default;
};
struct ModelDependency {
// Прямые зависимости к тестурам и моделям
std::vector<AssetsTexture> TextureDeps;
std::vector<AssetsModel> ModelDeps;
// Коллизия
std::vector<std::pair<glm::vec3, glm::vec3>> Cuboids;
//
bool Ready = false;
// Полный список зависимостей рекурсивно
std::vector<AssetsTexture> FullSubTextureDeps;
std::vector<AssetsModel> FullSubModelDeps;
};
/*
Работает с ресурсами из папок assets.
Использует папку server_cache/assets для хранения
преобразованных ресурсов
*/
class AssetsManager {
public:
struct ResourceChangeObj {
// Потерянные ресурсы
std::unordered_map<std::string, std::vector<std::string>> Lost[(int) EnumAssets::MAX_ENUM];
// Домен и ключ ресурса
std::unordered_map<std::string, std::vector<std::tuple<std::string, Resource, fs::file_time_type>>> NewOrChange[(int) EnumAssets::MAX_ENUM];
std::unordered_map<std::string, std::vector<std::tuple<std::string, PreparedNodeState, fs::file_time_type>>> NewOrChange_Nodestates;
std::unordered_map<std::string, std::vector<std::tuple<std::string, std::variant<
LV::PreparedModel,
PreparedGLTF
>, fs::file_time_type>>> NewOrChange_Models;
// std::unordered_map<std::string, std::vector<std::pair<std::string, PreparedModel>>> Models;
};
private:
// Данные об отслеживаемых файлах
struct DataEntry {
// Время последнего изменения файла
fs::file_time_type FileChangeTime;
Resource Res;
std::string Domain, Key;
};
template<typename T>
struct TableEntry {
static constexpr size_t ChunkSize = 4096;
bool IsFull = false;
std::bitset<ChunkSize> Empty;
std::array<std::optional<T>, ChunkSize> Entries;
TableEntry() {
Empty.set();
}
};
struct Local {
// Связь ресурсов по идентификаторам
std::vector<std::unique_ptr<TableEntry<DataEntry>>> Table[(int) EnumAssets::MAX_ENUM];
// Распаршенные ресурсы, для использования сервером (сбор зависимостей профиля нод и расчёт коллизии если нужно)
// Первичные зависимости Nodestate к моделям
std::vector<std::unique_ptr<TableEntry<std::vector<AssetsModel>>>> Table_NodeState;
// Упрощённые модели для коллизии
std::vector<std::unique_ptr<TableEntry<ModelDependency>>> Table_Model;
// Связь домены -> {ключ -> идентификатор}
std::unordered_map<std::string, std::unordered_map<std::string, ResourceId>> KeyToId[(int) EnumAssets::MAX_ENUM];
std::unordered_map<Hash_t, std::tuple<EnumAssets, ResourceId>> HashToId;
std::tuple<ResourceId, std::optional<DataEntry>&> nextId(EnumAssets type);
ResourceId getId(EnumAssets type, const std::string& domain, const std::string& key) {
auto& keyToId = KeyToId[(int) type];
if(auto iterKTI = keyToId.find(domain); iterKTI != keyToId.end()) {
if(auto iterKey = iterKTI->second.find(key); iterKey != iterKTI->second.end()) {
return iterKey->second;
}
}
auto [id, entry] = nextId(type);
keyToId[domain][key] = id;
return id;
}
std::optional<std::tuple<Resource, const std::string&, const std::string&>> getResource(EnumAssets type, ResourceId id) {
assert(id < Table[(int) type].size()*TableEntry<DataEntry>::ChunkSize);
auto& value = Table[(int) type][id / TableEntry<DataEntry>::ChunkSize]->Entries[id % TableEntry<DataEntry>::ChunkSize];
if(value)
return {{value->Res, value->Domain, value->Key}};
else
return std::nullopt;
}
std::optional<std::tuple<Resource, const std::string&, const std::string&, EnumAssets, ResourceId>> getResource(const Hash_t& hash) {
auto iter = HashToId.find(hash);
if(iter == HashToId.end())
return std::nullopt;
auto [type, id] = iter->second;
std::optional<std::tuple<Resource, const std::string&, const std::string&>> res = getResource(type, id);
if(!res) {
HashToId.erase(iter);
return std::nullopt;
}
if(std::get<Resource>(*res).hash() == hash) {
auto& [resource, domain, key] = *res;
return std::tuple<Resource, const std::string&, const std::string&, EnumAssets, ResourceId>{resource, domain, key, type, id};
}
HashToId.erase(iter);
return std::nullopt;
}
const std::optional<std::vector<AssetsModel>>& getResourceNodestate(ResourceId id) {
assert(id < Table_NodeState.size()*TableEntry<DataEntry>::ChunkSize);
return Table_NodeState[id / TableEntry<DataEntry>::ChunkSize]
->Entries[id % TableEntry<DataEntry>::ChunkSize];
}
const std::optional<ModelDependency>& getResourceModel(ResourceId id) {
assert(id < Table_Model.size()*TableEntry<DataEntry>::ChunkSize);
return Table_Model[id / TableEntry<DataEntry>::ChunkSize]
->Entries[id % TableEntry<DataEntry>::ChunkSize];
}
};
TOS::SpinlockObject<Local> LocalObj;
/*
Загрузка ресурса с файла. При необходимости приводится
к внутреннему формату и сохраняется в кеше
*/
void loadResourceFromFile (EnumAssets type, ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const;
void loadResourceFromLua (EnumAssets type, ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const;
void loadResourceFromFile_Nodestate (ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const;
void loadResourceFromFile_Particle (ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const;
void loadResourceFromFile_Animation (ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const;
void loadResourceFromFile_Model (ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const;
void loadResourceFromFile_Texture (ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const;
void loadResourceFromFile_Sound (ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const;
void loadResourceFromFile_Font (ResourceChangeObj& out, const std::string& domain, const std::string& key, fs::path path) const;
void loadResourceFromLua_Nodestate (ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const;
void loadResourceFromLua_Particle (ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const;
void loadResourceFromLua_Animation (ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const;
void loadResourceFromLua_Model (ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const;
void loadResourceFromLua_Texture (ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const;
void loadResourceFromLua_Sound (ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const;
void loadResourceFromLua_Font (ResourceChangeObj& out, const std::string& domain, const std::string& key, const sol::table& profile) const;
public:
AssetsManager(asio::io_context& ioc);
~AssetsManager();
/*
Перепроверка изменений ресурсов по дате изменения, пересчёт хешей.
Обнаруженные изменения должны быть отправлены всем клиентам.
Ресурсы будут обработаны в подходящий формат и сохранены в кеше.
Одновременно может выполнятся только одна такая функция
Используется в GameServer
*/
struct AssetsRegister {
/*
Пути до активных папок assets, соответствую порядку загруженным модам.
От последнего мода к первому.
Тот файл, что был загружен раньше и будет использоваться
*/
std::vector<fs::path> Assets;
/*
У этих ресурсов приоритет выше, если их удастся получить,
то использоваться будут именно они
Domain -> {key + data}
*/
std::unordered_map<std::string, std::unordered_map<std::string, void*>> Custom[(int) EnumAssets::MAX_ENUM];
};
ResourceChangeObj recheckResources(const AssetsRegister&);
/*
Применяет расчитанные изменения.
Раздаёт идентификаторы ресурсам и записывает их в таблицу
*/
struct Out_applyResourceChange {
std::vector<ResourceId> Lost[(int) EnumAssets::MAX_ENUM];
std::vector<std::pair<ResourceId, Resource>> NewOrChange[(int) EnumAssets::MAX_ENUM];
};
Out_applyResourceChange applyResourceChange(const ResourceChangeObj& orr);
/*
Выдаёт идентификатор ресурса, даже если он не существует или был удалён.
resource должен содержать домен и путь
*/
ResourceId getId(EnumAssets type, const std::string& domain, const std::string& key) {
return LocalObj.lock()->getId(type, domain, key);
}
// Выдаёт ресурс по идентификатору
std::optional<std::tuple<Resource, const std::string&, const std::string&>> getResource(EnumAssets type, ResourceId id) {
return LocalObj.lock()->getResource(type, id);
}
// Выдаёт ресурс по хешу
std::optional<std::tuple<Resource, const std::string&, const std::string&, EnumAssets, ResourceId>> getResource(const Hash_t& hash) {
return LocalObj.lock()->getResource(hash);
}
// Выдаёт зависимости к ресурсам профиля ноды
std::tuple<AssetsNodestate, std::vector<AssetsModel>, std::vector<AssetsTexture>>
getNodeDependency(const std::string& domain, const std::string& key)
{
if(domain == "core" && key == "none") {
return {0, {}, {}};
}
auto lock = LocalObj.lock();
AssetsNodestate nodestateId = lock->getId(EnumAssets::Nodestate, domain, key+".json");
std::vector<AssetsModel> models;
std::vector<AssetsTexture> textures;
if(auto subModelsPtr = lock->getResourceNodestate(nodestateId)) {
for(AssetsModel resId : *subModelsPtr) {
const auto& subModel = lock->getResourceModel(resId);
if(!subModel)
continue;
models.push_back(resId);
models.append_range(subModel->FullSubModelDeps);
textures.append_range(subModel->FullSubTextureDeps);
}
} else {
LOG.debug() << "Для ноды " << domain << ':' << key << " отсутствует описание Nodestate";
}
{
std::sort(models.begin(), models.end());
auto eraseIter = std::unique(models.begin(), models.end());
models.erase(eraseIter, models.end());
models.shrink_to_fit();
}
{
std::sort(textures.begin(), textures.end());
auto eraseIter = std::unique(textures.begin(), textures.end());
textures.erase(eraseIter, textures.end());
textures.shrink_to_fit();
}
return {nodestateId, std::move(models), std::move(textures)};
}
private:
TOS::Logger LOG = "Server>AssetsManager";
};
}

View File

@@ -1154,150 +1154,6 @@ coro<> GameServer::pushSocketGameProtocol(tcp::socket socket, const std::string
}
}
TexturePipeline GameServer::buildTexturePipeline(const std::string& pl) {
/*
^ объединение текстур, вторая поверх первой.
При наложении текстуры будут автоматически увеличины до размера
самой большой текстуры из участвующих. По умолчанию ближайший соседний
default:dirt.png^our_tech:machine.png
Текстурные команды описываются в [] <- предоставляет текстуру.
Разделитель пробелом
default:dirt.png^[create 2 2 r ffaabbcc]
Если перед командой будет использован $, то первым аргументом будет
предыдущая текстура, если это поддерживает команда
default:dirt.png$[resize 16 16] или [resize default:dirt.png 16 16]
Группировка ()
default:empty^(default:dirt.png^our_tech:machine.png)
*/
std::map<std::string, AssetsTexture> stbt;
std::unordered_set<AssetsTexture> btis;
std::string alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
// Парсер группы. Возвращает позицию на которой закончил и скомпилированный код
std::move_only_function<std::tuple<size_t, std::u8string>(size_t pos)> parse_obj;
std::move_only_function<std::tuple<size_t, std::u8string>(size_t pos, std::u8string maybe)> parse_cmd;
parse_cmd = [&](size_t pos, std::u8string maybe) -> std::tuple<size_t, std::u8string> {
std::string cmd_name;
std::vector<std::u8string> args;
size_t startPos = pos;
for(pos++; pos < pl.size(); pos++) {
if(pl[pos] == ']') {
// Команда завершилась
// return {pos+1, cmd_name};
} else if(pl[pos] == ' ') {
// Аргументы
// Здесь нужно получить либо кастомные значения, либо объект
auto [next_pos, subcmd] = parse_obj(pos+1);
args.push_back(subcmd);
if(next_pos == pl.size())
MAKE_ERROR("Ожидался конец команды объявленной на " << startPos << ", наткнулись на конец потока");
pos = next_pos-1;
} else if(alpha.find(pl[pos]) == std::string::npos) {
MAKE_ERROR("Ошибка в имени команды");
} else {
cmd_name += pl[pos];
}
}
MAKE_ERROR("Ожидался конец команды объявленной на " << startPos << ", наткнулись на конец потока");
};
parse_obj = [&](size_t pos) -> std::pair<size_t, std::u8string> {
std::u8string out;
for(; pos < pl.size(); pos++) {
if(pl[pos] == '[') {
// Начало команды
if(!out.empty()) {
MAKE_ERROR("Отсутствует связь между текстурой и текущей командой " << pos);
}
// out.push_back(TexturePipelineCMD::Combine);
auto [next_size, subcmd] = parse_cmd(pos+1, {});
pos = next_size-1;
out = subcmd;
} else if(pl[pos] == '^') {
// Объединение
if(out.empty()) {
MAKE_ERROR("Отсутствует текстура для комбинирования " << pos);
auto [next_pos, subcmd] = parse_obj(pos+1);
std::u8string cmd;
cmd.push_back(uint8_t(TexturePipelineCMD::Combine));
cmd.insert(cmd.end(), out.begin(), out.end());
cmd.insert(cmd.end(), subcmd.begin(), subcmd.end());
return {next_pos, cmd};
}
} else if(pl[pos] == '$') {
// Готовый набор команд будет использован как аргумент
pos++;
if(pos >= pl.size() || pl[pos] != '[')
MAKE_ERROR("Ожидалось объявление команды " << pos);
auto [next_pos, subcmd] = parse_cmd(pos, out);
pos = next_pos-1;
out = subcmd;
} else if(pl[pos] == '(') {
if(!out.empty()) {
MAKE_ERROR("Начато определение группы после текстуры, вероятно пропущен знак объединения ^ " << pos);
}
// Начало группы
auto [next_pos, subcmd] = parse_obj(pos+1);
pos = next_pos-1;
out = subcmd;
} else if(pl[pos] == ')') {
return {pos+1, out};
} else {
// Это текстура, нужно её имя
if(!out.empty())
MAKE_ERROR("Отсутствует связь между текстурой и текущим объявлением текстуры " << pos);
out.push_back(uint8_t(TexturePipelineCMD::Texture));
std::string texture_name;
for(; pos < pl.size(); pos++) {
if(pl[pos] == '^' || pl[pos] == ')' || pl[pos] == ']')
break;
else if(pl[pos] != '.' && pl[pos] != ':' && alpha.find_first_of(pl[pos]) != std::string::npos)
MAKE_ERROR("Недействительные символы в объявлении текстуры " << pos);
else
texture_name += pl[pos];
}
AssetsTexture id = stbt[texture_name];
btis.insert(id);
for(int iter = 0; iter < 4; iter++)
out.push_back((id >> (iter * 8)) & 0xff);
if(pos < pl.size())
pos--;
}
}
return {pos, out};
};
auto [pos, cmd] = parse_obj(0);
if(pos < pl.size()) {
MAKE_ERROR("Неожиданное продолжение " << pos);
}
return {std::vector<AssetsTexture>(btis.begin(), btis.end()), cmd};
}
std::string GameServer::deBuildTexturePipeline(const TexturePipeline& pipeline) {
return "";
}
int my_exception_handler(lua_State* lua, sol::optional<const std::exception&> maybe_exception, sol::string_view description) {
std::cout << "An exception occurred in a function, here's what it says ";
if (maybe_exception) {

View File

@@ -294,9 +294,6 @@ public:
// Инициализация игрового протокола для сокета (onSocketAuthorized() может передать сокет в onSocketGame())
coro<> pushSocketGameProtocol(tcp::socket socket, const std::string username);
TexturePipeline buildTexturePipeline(const std::string& pipeline);
std::string deBuildTexturePipeline(const TexturePipeline& pipeline);
private:
void init(fs::path worldPath);
void prerun();