Compare commits

..

26 Commits

Author SHA1 Message Date
0b8326e278 Сохранение мира на фс 2026-01-28 23:05:27 +03:00
07ccd4dd68 Уменьшение размер промежуточного буфера. Отчистка меша чанка, если тот пуст. 2026-01-28 22:46:35 +03:00
da673b0965 Использование профилей по умолчанию для потерянных на стороне клиента 2026-01-18 09:07:25 +03:00
3fb06080db Синхронизация профилей контента 2026-01-17 18:59:36 +03:00
affdc75ebd Добавлена задержка перед выгрузкой чанков 2026-01-16 01:06:55 +06:00
49c4d77c59 Доработка менеджера контента на стороне сервера под новый провайдер идентификаторов 2026-01-16 01:05:20 +06:00
16a0fa5f7a Синхронный IdProvider 2026-01-11 22:28:03 +06:00
a29e772f35 Более нормализованная обработка ресурсов на клиенте 2026-01-07 13:56:10 +06:00
5135aa30a7 codex-5.2: тест новой версии менеджера ассетов 2026-01-07 04:03:17 +06:00
523f9725c0 Переработка менеджера ресурсов на стороне клиентов 2026-01-07 01:58:15 +06:00
c13ad06ba9 Рефакторинг кода работы с ресурсами игры на стороне сервера 2026-01-06 18:21:25 +06:00
83530a6c15 codex-5.2: новая модель объектов взаимодействия с сетью 2026-01-06 18:20:58 +06:00
b61cc9fb03 Разделение TexPipe на cpp и hpp 2026-01-06 18:20:31 +06:00
7224499d14 Документация по текстурным программам 2026-01-05 14:00:31 +06:00
ad4b0d593a Анимация текстур по сетке 2026-01-05 13:47:53 +06:00
d3add82c55 У TexPipe убрано ключевое слово tex 2026-01-05 13:46:55 +06:00
2b2be796e9 codex-5.2: Отладка сети со стороны клиента 2026-01-05 02:25:51 +06:00
6c7a6df8f6 codex-5.2: перестройка Client/AssetsManager 2026-01-05 01:50:17 +06:00
8ce820569a codex-5.2: кое как доведено до почти рабочего состояния 2026-01-05 00:35:52 +06:00
5904fe6853 codex-5.2: отладка передачи ресурсов (assets) 2026-01-04 22:30:06 +06:00
6dd1f93221 Отправка идентификаторов при подключении клиентов 2026-01-04 20:47:48 +06:00
01ea7eee74 ocdex-5.2: Отправка чанков на клиент 2026-01-04 20:23:44 +06:00
dbebf50552 codex-5.2: логгирование передачи ресурсов 2026-01-04 20:18:02 +06:00
51cc68e1b2 codex-5.2: Обработчик ресурсов на стороне клиента 2026-01-04 20:11:14 +06:00
7c54f429ba Изменение кеша ресурсов на клиенте 2026-01-04 18:48:29 +06:00
abe7b987c4 Восстановление архитектуры на стороне сервера 2026-01-04 15:49:40 +06:00
69 changed files with 9158 additions and 7145 deletions

View File

@@ -84,6 +84,16 @@ FetchContent_Declare(
FetchContent_MakeAvailable(Boost)
target_link_libraries(luavox_common INTERFACE Boost::asio Boost::thread Boost::json Boost::iostreams Boost::interprocess Boost::timer Boost::circular_buffer Boost::lockfree Boost::stacktrace Boost::uuid Boost::serialization Boost::nowide)
# unordered_dense
FetchContent_Declare(
unordered_dense
GIT_REPOSITORY https://github.com/martinus/unordered_dense.git
GIT_TAG v4.8.1
)
FetchContent_MakeAvailable(unordered_dense)
target_link_libraries(luavox_common INTERFACE unordered_dense::unordered_dense)
# glm
# find_package(glm REQUIRED)
# target_include_directories(${PROJECT_NAME} PUBLIC ${GLM_INCLUDE_DIR})

View File

@@ -1,9 +1,13 @@
#pragma once
#include "Common/Net.hpp"
#include <cstdint>
#include <functional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <variant>
#include <vector>
#include <Common/Abstract.hpp>
@@ -60,15 +64,40 @@ public:
// states
};
struct AssetsModelUpdate {
ResourceId Id = 0;
HeadlessModel Model;
HeadlessModel::Header Header;
};
struct AssetsNodestateUpdate {
ResourceId Id = 0;
HeadlessNodeState Nodestate;
HeadlessNodeState::Header Header;
};
struct AssetsTextureUpdate {
ResourceId Id = 0;
uint16_t Width = 0;
uint16_t Height = 0;
std::vector<uint32_t> Pixels;
ResourceHeader Header;
};
struct AssetsBinaryUpdate {
ResourceId Id = 0;
std::u8string Data;
};
/* Интерфейс рендера текущего подключения к серверу */
class IRenderSession {
public:
// Объект уведомления об изменениях
struct TickSyncData {
// Новые или изменённые используемые теперь двоичные ресурсы
std::unordered_map<EnumAssets, std::vector<ResourceId>> Assets_ChangeOrAdd;
// Более не используемые ресурсы
std::unordered_map<EnumAssets, std::vector<ResourceId>> Assets_Lost;
// Изменения в ассетах.
std::vector<AssetsModelUpdate> AssetsModels;
std::vector<AssetsNodestateUpdate> AssetsNodestates;
std::vector<AssetsTextureUpdate> AssetsTextures;
// Новые или изменённые профили контента
std::unordered_map<EnumDefContent, std::vector<ResourceId>> Profiles_ChangeOrAdd;
@@ -87,7 +116,7 @@ public:
// Началась стадия изменения данных IServerSession, все должны приостановить работу
virtual void pushStageTickSync() = 0;
// После изменения внутренних данных IServerSession, IRenderSession уведомляется об изменениях
virtual void tickSync(const TickSyncData& data) = 0;
virtual void tickSync(TickSyncData& data) = 0;
// Установить позицию для камеры
virtual void setCameraPos(WorldId_t worldId, Pos::Object pos, glm::quat quat) = 0;
@@ -124,14 +153,6 @@ struct WorldInfo {
std::unordered_map<Pos::GlobalRegion, Region> Regions;
};
struct VoxelInfo {
};
struct NodeInfo {
};
struct PortalInfo {
};
@@ -143,28 +164,96 @@ struct EntityInfo {
glm::quat Quat = glm::quat(1.f, 0.f, 0.f, 0.f);
};
struct FuncEntityInfo {
/*
Конструируются с серверными идентификаторами
*/
struct DefVoxel {
DefVoxel() = default;
DefVoxel(const std::u8string_view view) {
}
void reBind(const std::function<ResourceId(EnumAssets, ResourceId)>& am) {
}
};
struct DefItemInfo {
struct DefNode {
std::variant<AssetsNodestate> RenderStates;
DefNode() = default;
DefNode(const std::u8string_view view) {
Net::LinearReader lr(view);
RenderStates = lr.read<uint32_t>();
}
void reBind(const std::function<ResourceId(EnumAssets, ResourceId)>& am) {
RenderStates = am(EnumAssets::Nodestate, std::get<AssetsNodestate>(RenderStates));
}
};
struct DefVoxel_t {};
struct DefNode_t {
AssetsNodestate NodestateId = 0;
AssetsTexture TexId = 0;
struct DefWorld {
DefWorld() = default;
DefWorld(const std::u8string_view view) {
}
void reBind(const std::function<ResourceId(EnumAssets, ResourceId)>& am) {
}
};
struct DefPortal {
DefPortal() = default;
DefPortal(const std::u8string_view view) {
}
void reBind(const std::function<ResourceId(EnumAssets, ResourceId)>& am) {
}
};
struct DefEntity {
DefEntity() = default;
DefEntity(const std::u8string_view view) {
}
void reBind(const std::function<ResourceId(EnumAssets, ResourceId)>& am) {
}
};
struct DefItem {
DefItem() = default;
DefItem(const std::u8string_view view) {
}
void reBind(const std::function<ResourceId(EnumAssets, ResourceId)>& am) {
}
};
struct AssetEntry {
EnumAssets Type;
ResourceId Id;
std::string Domain, Key;
Resource Res;
Hash_t Hash = {};
std::vector<uint8_t> Dependencies;
ResourceId Id = 0;
std::string Domain;
std::string Key;
HeadlessModel Model;
HeadlessModel::Header ModelHeader;
HeadlessNodeState Nodestate;
HeadlessNodeState::Header NodestateHeader;
uint16_t Width = 0;
uint16_t Height = 0;
std::vector<uint32_t> Pixels;
ResourceHeader Header;
std::u8string Data;
};
/*
@@ -177,17 +266,17 @@ struct AssetEntry {
*/
class IServerSession {
public:
// Используемые двоичные ресурсы
std::unordered_map<EnumAssets, std::unordered_map<ResourceId, AssetEntry>> Assets;
// Включить логирование входящих сетевых пакетов на клиенте.
bool DebugLogPackets = false;
// Используемые профили контента
struct {
std::unordered_map<DefVoxelId, DefVoxel_t> DefVoxel;
std::unordered_map<DefNodeId, DefNode_t> DefNode;
std::unordered_map<DefWorldId, DefWorldInfo> DefWorld;
std::unordered_map<DefPortalId, DefPortalInfo> DefPortal;
std::unordered_map<DefEntityId, DefEntityInfo> DefEntity;
std::unordered_map<DefItemId, DefItemInfo> DefItem;
std::unordered_map<DefVoxelId, DefVoxel> DefVoxels;
std::unordered_map<DefNodeId, DefNode> DefNodes;
std::unordered_map<DefWorldId, DefWorld> DefWorlds;
std::unordered_map<DefPortalId, DefPortal> DefPortals;
std::unordered_map<DefEntityId, DefEntity> DefEntitys;
std::unordered_map<DefItemId, DefItem> DefItems;
} Profiles;
// Видимый контент

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

@@ -0,0 +1,196 @@
#pragma once
#include "Common/Abstract.hpp"
#include <cassert>
#include <functional>
#include <memory>
#include <optional>
#include <queue>
#include <string>
#include <sqlite3.h>
#include <TOSLib.hpp>
#include <TOSAsync.hpp>
#include <filesystem>
#include <string_view>
#include <thread>
#include <vector>
namespace LV::Client {
using namespace TOS;
namespace fs = std::filesystem;
// NOT ThreadSafe
class CacheDatabase {
const fs::path Path;
sqlite3 *DB = nullptr;
sqlite3_stmt *STMT_INSERT = nullptr,
*STMT_UPDATE_TIME = nullptr,
*STMT_REMOVE = nullptr,
*STMT_ALL_HASH = nullptr,
*STMT_SUM = nullptr,
*STMT_OLD = nullptr,
*STMT_TO_FREE = nullptr,
*STMT_COUNT = nullptr;
public:
CacheDatabase(const fs::path &cachePath);
~CacheDatabase();
CacheDatabase(const CacheDatabase&) = delete;
CacheDatabase(CacheDatabase&&) = delete;
CacheDatabase& operator=(const CacheDatabase&) = delete;
CacheDatabase& operator=(CacheDatabase&&) = delete;
/*
Выдаёт размер занимаемый всем хранимым кешем
*/
size_t getCacheSize();
// TODO: добавить ограничения на количество файлов
/*
Создаёт линейный массив в котором подряд указаны все хэш суммы в бинарном виде и возвращает их количество
*/
// std::pair<std::string, size_t> getAllHash();
/*
Обновляет время использования кеша
*/
void updateTimeFor(Hash_t hash);
/*
Добавляет запись
*/
void insert(Hash_t hash, size_t size);
/*
Выдаёт хэши на удаление по размеру в сумме больше bytesToFree.
Сначала удаляется старьё, потом по приоритету дата использования + размер
*/
std::vector<Hash_t> findExcessHashes(size_t bytesToFree, int timeBefore);
/*
Удаление записи
*/
void remove(Hash_t hash);
static std::string hashToString(Hash_t hash);
static int hexCharToInt(char c);
static Hash_t stringToHash(const std::string_view view);
};
/*
Менеджер кеша ресурсов по хэшу.
Интерфейс однопоточный, обработка файлов в отдельном потоке.
*/
class AssetsCacheManager : public IAsyncDestructible {
public:
using Ptr = std::shared_ptr<AssetsCacheManager>;
public:
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 AssetsCacheManager(ioc, cachePath, maxCacheDirectorySize, maxLifeTime));
}
// Добавить новый полученный с сервера ресурс
void pushResources(std::vector<Resource> resources) {
WriteQueue.lock()->push_range(resources);
}
// Добавить задачи на чтение по хэшу
void pushReads(std::vector<Hash_t> hashes) {
ReadQueue.lock()->push_range(hashes);
}
// Получить считанные данные по хэшу
std::vector<std::pair<Hash_t, std::optional<Resource>>> pullReads() {
return std::move(*ReadyQueue.lock());
}
// Размер всего хранимого кеша
size_t getCacheSize() {
return DatabaseSize;
}
// Обновить параметры хранилища кеша
void updateParams(size_t maxLifeTime, size_t maxCacheDirectorySize) {
auto lock = Changes.lock();
lock->MaxLifeTime = maxLifeTime;
lock->MaxCacheDatabaseSize = maxCacheDirectorySize;
lock->MaxChange = true;
}
// Запуск процедуры проверки хешей всего хранимого кеша
void runFullDatabaseRecheck(std::move_only_function<void(std::string result)>&& func) {
auto lock = Changes.lock();
lock->OnRecheckEnd = std::move(func);
lock->FullRecheck = true;
}
bool hasError() {
return IssuedAnError;
}
private:
Logger LOG = "Client>ResourceHandler";
const fs::path
CachePath,
PathDatabase = CachePath / "db.sqlite3",
PathFiles = CachePath / "blobs";
static constexpr size_t SMALL_RESOURCE = 1 << 21;
sqlite3 *DB = nullptr; // База хранения кеша меньше 2мб и информации о кеше на диске
sqlite3_stmt
*STMT_DISK_INSERT = nullptr, // Вставка записи о хеше
*STMT_DISK_UPDATE_TIME = nullptr, // Обновить дату последнего использования
*STMT_DISK_REMOVE = nullptr, // Удалить хеш
*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_REMOVE = nullptr, // Удалить ресурс
*STMT_INLINE_OLDEST = nullptr; // Самые старые записи в базе
// Полный размер данных на диске (насколько известно)
volatile size_t DatabaseSize = 0;
// Очередь задач на чтение
TOS::SpinlockObject<std::queue<Hash_t>> ReadQueue;
// Очередь на запись ресурсов
TOS::SpinlockObject<std::queue<Resource>> WriteQueue;
// Очередь на выдачу результатов чтения
TOS::SpinlockObject<std::vector<std::pair<Hash_t, std::optional<Resource>>>> ReadyQueue;
struct Changes_t {
size_t MaxCacheDatabaseSize, MaxLifeTime;
volatile bool MaxChange = false;
std::optional<std::move_only_function<void(std::string)>> OnRecheckEnd;
volatile bool FullRecheck = false;
};
TOS::SpinlockObject<Changes_t> Changes;
bool NeedShutdown = false, IssuedAnError = false;
std::thread OffThread;
virtual coro<> asyncDestructor();
AssetsCacheManager(boost::asio::io_context &ioc, const fs::path &cachePath,
size_t maxCacheDatabaseSize, size_t maxLifeTime);
void readWriteThread(AsyncUseControl::Lock lock);
std::string hashToString(const Hash_t& hash);
};
}

View File

@@ -0,0 +1,484 @@
#include "Client/AssetsHeaderCodec.hpp"
#include <cstring>
#include <unordered_set>
#include "TOSLib.hpp"
namespace LV::Client::AssetsHeaderCodec {
namespace {
struct ParsedModelHeader {
std::vector<ResourceId> ModelDeps;
std::vector<std::vector<uint8_t>> TexturePipelines;
std::vector<ResourceId> TextureDeps;
};
std::optional<std::vector<ResourceId>> parseNodestateHeaderBytes(const std::vector<uint8_t>& header) {
if(header.empty() || header.size() % sizeof(ResourceId) != 0)
return std::nullopt;
const size_t count = header.size() / sizeof(ResourceId);
std::vector<ResourceId> deps;
deps.resize(count);
for(size_t i = 0; i < count; ++i) {
ResourceId raw = 0;
std::memcpy(&raw, header.data() + i * sizeof(ResourceId), sizeof(ResourceId));
deps[i] = raw;
}
return deps;
}
struct PipelineRemapResult {
bool Ok = true;
std::string Error;
};
PipelineRemapResult remapTexturePipelineIds(std::vector<uint8_t>& code,
const std::function<uint32_t(uint32_t)>& mapId)
{
struct Range {
size_t Start = 0;
size_t End = 0;
};
enum class SrcKind : uint8_t { TexId = 0, Sub = 1 };
enum class Op : uint8_t {
End = 0,
Base_Tex = 1,
Base_Fill = 2,
Base_Anim = 3,
Resize = 10,
Transform = 11,
Opacity = 12,
NoAlpha = 13,
MakeAlpha = 14,
Invert = 15,
Brighten = 16,
Contrast = 17,
Multiply = 18,
Screen = 19,
Colorize = 20,
Anim = 21,
Overlay = 30,
Mask = 31,
LowPart = 32,
Combine = 40
};
struct SrcMeta {
SrcKind Kind = SrcKind::TexId;
uint32_t TexId = 0;
uint32_t Off = 0;
uint32_t Len = 0;
size_t TexIdOffset = 0;
};
const size_t size = code.size();
std::vector<Range> visited;
auto read8 = [&](size_t& ip, uint8_t& out)->bool{
if(ip >= size)
return false;
out = code[ip++];
return true;
};
auto read16 = [&](size_t& ip, uint16_t& out)->bool{
if(ip + 1 >= size)
return false;
out = uint16_t(code[ip]) | (uint16_t(code[ip + 1]) << 8);
ip += 2;
return true;
};
auto read24 = [&](size_t& ip, uint32_t& out)->bool{
if(ip + 2 >= size)
return false;
out = uint32_t(code[ip])
| (uint32_t(code[ip + 1]) << 8)
| (uint32_t(code[ip + 2]) << 16);
ip += 3;
return true;
};
auto read32 = [&](size_t& ip, uint32_t& out)->bool{
if(ip + 3 >= size)
return false;
out = uint32_t(code[ip])
| (uint32_t(code[ip + 1]) << 8)
| (uint32_t(code[ip + 2]) << 16)
| (uint32_t(code[ip + 3]) << 24);
ip += 4;
return true;
};
auto readSrc = [&](size_t& ip, SrcMeta& out)->bool{
uint8_t kind = 0;
if(!read8(ip, kind))
return false;
out.Kind = static_cast<SrcKind>(kind);
if(out.Kind == SrcKind::TexId) {
out.TexIdOffset = ip;
return read24(ip, out.TexId);
}
if(out.Kind == SrcKind::Sub) {
return read24(ip, out.Off) && read24(ip, out.Len);
}
return false;
};
auto patchTexId = [&](const SrcMeta& src)->PipelineRemapResult{
if(src.Kind != SrcKind::TexId)
return {};
uint32_t newId = mapId(src.TexId);
if(newId >= (1u << 24))
return {false, "TexId exceeds u24 range"};
if(src.TexIdOffset + 2 >= code.size())
return {false, "TexId patch outside pipeline"};
code[src.TexIdOffset + 0] = uint8_t(newId & 0xFFu);
code[src.TexIdOffset + 1] = uint8_t((newId >> 8) & 0xFFu);
code[src.TexIdOffset + 2] = uint8_t((newId >> 16) & 0xFFu);
return {};
};
std::function<bool(size_t, size_t)> scan;
scan = [&](size_t start, size_t end) -> bool {
if(start >= end || end > size)
return true;
for(const auto& range : visited) {
if(range.Start == start && range.End == end)
return true;
}
visited.push_back(Range{start, end});
size_t ip = start;
while(ip < end) {
uint8_t opByte = 0;
if(!read8(ip, opByte))
return false;
Op op = static_cast<Op>(opByte);
switch(op) {
case Op::End:
return true;
case Op::Base_Tex: {
SrcMeta src{};
if(!readSrc(ip, src))
return false;
PipelineRemapResult r = patchTexId(src);
if(!r.Ok)
return false;
if(src.Kind == SrcKind::Sub) {
size_t subStart = src.Off;
size_t subEnd = subStart + src.Len;
if(!scan(subStart, subEnd))
return false;
}
} break;
case Op::Base_Fill: {
uint16_t tmp16 = 0;
uint32_t tmp32 = 0;
if(!read16(ip, tmp16)) return false;
if(!read16(ip, tmp16)) return false;
if(!read32(ip, tmp32)) return false;
} break;
case Op::Base_Anim: {
SrcMeta src{};
if(!readSrc(ip, src)) return false;
PipelineRemapResult r = patchTexId(src);
if(!r.Ok) return false;
uint16_t frameW = 0;
uint16_t frameH = 0;
uint16_t frameCount = 0;
uint16_t fpsQ = 0;
uint8_t flags = 0;
if(!read16(ip, frameW)) return false;
if(!read16(ip, frameH)) return false;
if(!read16(ip, frameCount)) return false;
if(!read16(ip, fpsQ)) return false;
if(!read8(ip, flags)) return false;
(void)frameW; (void)frameH; (void)frameCount; (void)fpsQ; (void)flags;
if(src.Kind == SrcKind::Sub) {
size_t subStart = src.Off;
size_t subEnd = subStart + src.Len;
if(!scan(subStart, subEnd)) return false;
}
} break;
case Op::Resize: {
uint16_t tmp16 = 0;
if(!read16(ip, tmp16)) return false;
if(!read16(ip, tmp16)) return false;
} break;
case Op::Transform:
case Op::Opacity:
case Op::Invert:
if(!read8(ip, opByte)) return false;
break;
case Op::NoAlpha:
case Op::Brighten:
break;
case Op::MakeAlpha:
if(ip + 2 >= size) return false;
ip += 3;
break;
case Op::Contrast:
if(ip + 1 >= size) return false;
ip += 2;
break;
case Op::Multiply:
case Op::Screen: {
uint32_t tmp32 = 0;
if(!read32(ip, tmp32)) return false;
} break;
case Op::Colorize: {
uint32_t tmp32 = 0;
if(!read32(ip, tmp32)) return false;
if(!read8(ip, opByte)) return false;
} break;
case Op::Anim: {
uint16_t frameW = 0;
uint16_t frameH = 0;
uint16_t frameCount = 0;
uint16_t fpsQ = 0;
uint8_t flags = 0;
if(!read16(ip, frameW)) return false;
if(!read16(ip, frameH)) return false;
if(!read16(ip, frameCount)) return false;
if(!read16(ip, fpsQ)) return false;
if(!read8(ip, flags)) return false;
(void)frameW; (void)frameH; (void)frameCount; (void)fpsQ; (void)flags;
} break;
case Op::Overlay:
case Op::Mask: {
SrcMeta src{};
if(!readSrc(ip, src)) return false;
PipelineRemapResult r = patchTexId(src);
if(!r.Ok) return false;
if(src.Kind == SrcKind::Sub) {
size_t subStart = src.Off;
size_t subEnd = subStart + src.Len;
if(!scan(subStart, subEnd)) return false;
}
} break;
case Op::LowPart: {
if(!read8(ip, opByte)) return false;
SrcMeta src{};
if(!readSrc(ip, src)) return false;
PipelineRemapResult r = patchTexId(src);
if(!r.Ok) return false;
if(src.Kind == SrcKind::Sub) {
size_t subStart = src.Off;
size_t subEnd = subStart + src.Len;
if(!scan(subStart, subEnd)) return false;
}
} break;
case Op::Combine: {
uint16_t w = 0, h = 0, n = 0;
if(!read16(ip, w)) return false;
if(!read16(ip, h)) return false;
if(!read16(ip, n)) return false;
for(uint16_t i = 0; i < n; ++i) {
uint16_t tmp16 = 0;
if(!read16(ip, tmp16)) return false;
if(!read16(ip, tmp16)) return false;
SrcMeta src{};
if(!readSrc(ip, src)) return false;
PipelineRemapResult r = patchTexId(src);
if(!r.Ok) return false;
if(src.Kind == SrcKind::Sub) {
size_t subStart = src.Off;
size_t subEnd = subStart + src.Len;
if(!scan(subStart, subEnd)) return false;
}
}
(void)w; (void)h;
} break;
default:
return false;
}
}
return true;
};
if(!scan(0, size))
return {false, "Invalid texture pipeline bytecode"};
return {};
}
std::vector<uint32_t> collectTexturePipelineIds(const std::vector<uint8_t>& code) {
std::vector<uint32_t> out;
std::unordered_set<uint32_t> seen;
auto addId = [&](uint32_t id) {
if(seen.insert(id).second)
out.push_back(id);
};
std::vector<uint8_t> copy = code;
auto result = remapTexturePipelineIds(copy, [&](uint32_t id) {
addId(id);
return id;
});
if(!result.Ok)
return {};
return out;
}
std::optional<ParsedModelHeader> parseModelHeaderBytes(const std::vector<uint8_t>& header) {
if(header.empty())
return std::nullopt;
ParsedModelHeader result;
try {
TOS::ByteBuffer buffer(header.size(), header.data());
auto reader = buffer.reader();
uint16_t modelCount = reader.readUInt16();
result.ModelDeps.reserve(modelCount);
for(uint16_t i = 0; i < modelCount; ++i)
result.ModelDeps.push_back(reader.readUInt32());
uint16_t texCount = reader.readUInt16();
result.TexturePipelines.reserve(texCount);
for(uint16_t i = 0; i < texCount; ++i) {
uint32_t size32 = reader.readUInt32();
TOS::ByteBuffer pipe;
reader.readBuffer(pipe);
if(pipe.size() != size32)
return std::nullopt;
result.TexturePipelines.emplace_back(pipe.begin(), pipe.end());
}
std::unordered_set<ResourceId> seen;
for(const auto& pipe : result.TexturePipelines) {
for(uint32_t id : collectTexturePipelineIds(pipe)) {
if(seen.insert(id).second)
result.TextureDeps.push_back(id);
}
}
} catch(const std::exception&) {
return std::nullopt;
}
return result;
}
} // namespace
std::optional<ParsedHeader> parseHeader(EnumAssets type, const std::vector<uint8_t>& header) {
if(header.empty())
return std::nullopt;
ParsedHeader result;
result.Type = type;
if(type == EnumAssets::Nodestate) {
auto deps = parseNodestateHeaderBytes(header);
if(!deps)
return std::nullopt;
result.ModelDeps = std::move(*deps);
return result;
}
if(type == EnumAssets::Model) {
auto parsed = parseModelHeaderBytes(header);
if(!parsed)
return std::nullopt;
result.ModelDeps = std::move(parsed->ModelDeps);
result.TexturePipelines = std::move(parsed->TexturePipelines);
result.TextureDeps = std::move(parsed->TextureDeps);
return result;
}
return std::nullopt;
}
std::vector<uint8_t> rebindHeader(EnumAssets type, const std::vector<uint8_t>& header,
const MapIdFn& mapModelId, const MapIdFn& mapTextureId, const WarnFn& warn)
{
if(header.empty())
return {};
if(type == EnumAssets::Nodestate) {
if(header.size() % sizeof(ResourceId) != 0)
return header;
std::vector<uint8_t> out(header.size());
const size_t count = header.size() / sizeof(ResourceId);
for(size_t i = 0; i < count; ++i) {
ResourceId raw = 0;
std::memcpy(&raw, header.data() + i * sizeof(ResourceId), sizeof(ResourceId));
ResourceId mapped = mapModelId(raw);
std::memcpy(out.data() + i * sizeof(ResourceId), &mapped, sizeof(ResourceId));
}
return out;
}
if(type == EnumAssets::Model) {
try {
TOS::ByteBuffer buffer(header.size(), header.data());
auto reader = buffer.reader();
uint16_t modelCount = reader.readUInt16();
std::vector<ResourceId> models;
models.reserve(modelCount);
for(uint16_t i = 0; i < modelCount; ++i) {
ResourceId id = reader.readUInt32();
models.push_back(mapModelId(id));
}
uint16_t texCount = reader.readUInt16();
std::vector<std::vector<uint8_t>> pipelines;
pipelines.reserve(texCount);
for(uint16_t i = 0; i < texCount; ++i) {
uint32_t size32 = reader.readUInt32();
TOS::ByteBuffer pipe;
reader.readBuffer(pipe);
if(pipe.size() != size32) {
warn("Pipeline size mismatch");
}
std::vector<uint8_t> code(pipe.begin(), pipe.end());
auto result = remapTexturePipelineIds(code, [&](uint32_t id) {
return mapTextureId(static_cast<ResourceId>(id));
});
if(!result.Ok) {
warn(result.Error);
}
pipelines.emplace_back(std::move(code));
}
TOS::ByteBuffer::Writer wr;
wr << uint16_t(models.size());
for(ResourceId id : models)
wr << id;
wr << uint16_t(pipelines.size());
for(const auto& pipe : pipelines) {
wr << uint32_t(pipe.size());
TOS::ByteBuffer pipeBuff(pipe.begin(), pipe.end());
wr << pipeBuff;
}
TOS::ByteBuffer out = wr.complite();
return std::vector<uint8_t>(out.begin(), out.end());
} catch(const std::exception&) {
warn("Failed to rebind model header");
return header;
}
}
return header;
}
} // namespace LV::Client::AssetsHeaderCodec

View File

@@ -0,0 +1,27 @@
#pragma once
#include <cstdint>
#include <functional>
#include <optional>
#include <string>
#include <vector>
#include "Common/Abstract.hpp"
namespace LV::Client::AssetsHeaderCodec {
struct ParsedHeader {
EnumAssets Type{};
std::vector<ResourceId> ModelDeps;
std::vector<ResourceId> TextureDeps;
std::vector<std::vector<uint8_t>> TexturePipelines;
};
using MapIdFn = std::function<ResourceId(ResourceId)>;
using WarnFn = std::function<void(const std::string&)>;
std::optional<ParsedHeader> parseHeader(EnumAssets type, const std::vector<uint8_t>& header);
std::vector<uint8_t> rebindHeader(EnumAssets type, const std::vector<uint8_t>& header,
const MapIdFn& mapModelId, const MapIdFn& mapTextureId, const WarnFn& warn);
} // namespace LV::Client::AssetsHeaderCodec

View File

@@ -0,0 +1,767 @@
#include "AssetsManager.hpp"
#include <algorithm>
#include <cassert>
#include <fstream>
#include "Common/TexturePipelineProgram.hpp"
namespace LV::Client {
namespace {
static const char* assetTypeName(EnumAssets type) {
switch(type) {
case EnumAssets::Nodestate: return "nodestate";
case EnumAssets::Model: return "model";
case EnumAssets::Texture: return "texture";
case EnumAssets::Particle: return "particle";
case EnumAssets::Animation: return "animation";
case EnumAssets::Sound: return "sound";
case EnumAssets::Font: return "font";
default: return "unknown";
}
}
static const char* enumAssetsToDirectory(LV::EnumAssets value) {
switch(value) {
case LV::EnumAssets::Nodestate: return "nodestate";
case LV::EnumAssets::Particle: return "particle";
case LV::EnumAssets::Animation: return "animation";
case LV::EnumAssets::Model: return "model";
case LV::EnumAssets::Texture: return "texture";
case LV::EnumAssets::Sound: return "sound";
case LV::EnumAssets::Font: return "font";
default:
break;
}
assert(!"Unknown asset type");
return "";
}
static std::u8string 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);
std::u8string data;
data.resize(static_cast<size_t>(size));
if(size > 0) {
file.read(reinterpret_cast<char*>(data.data()), size);
if(!file)
throw std::runtime_error("Не удалось прочитать файл: " + path.string());
}
return data;
}
static std::u8string readOptionalMeta(const fs::path& path) {
fs::path metaPath = path;
metaPath += ".meta";
if(!fs::exists(metaPath) || !fs::is_regular_file(metaPath))
return {};
return readFileBytes(metaPath);
}
} // namespace
AssetsManager::AssetsManager(asio::io_context& ioc, const fs::path& cachePath,
size_t maxCacheDirectorySize, size_t maxLifeTime)
: Cache(AssetsCacheManager::Create(ioc, cachePath, maxCacheDirectorySize, maxLifeTime))
{
for(size_t i = 0; i < static_cast<size_t>(AssetType::MAX_ENUM); ++i)
Types[i].NextLocalId = 1;
initSources();
}
void AssetsManager::initSources() {
using SourceResult = AssetsManager::SourceResult;
using SourceStatus = AssetsManager::SourceStatus;
using SourceReady = AssetsManager::SourceReady;
using ResourceKey = AssetsManager::ResourceKey;
using PackResource = AssetsManager::PackResource;
class PackSource final : public IResourceSource {
public:
explicit PackSource(AssetsManager* manager) : Manager(manager) {}
SourceResult tryGet(const ResourceKey& key) override {
std::optional<PackResource> pack = Manager->findPackResource(key.Type, key.Domain, key.Key);
if(pack && pack->Hash == key.Hash)
return {SourceStatus::Hit, pack->Res, 0};
return {SourceStatus::Miss, std::nullopt, 0};
}
void collectReady(std::vector<SourceReady>&) override {}
bool isAsync() const override {
return false;
}
void startPending(std::vector<Hash_t>) override {}
private:
AssetsManager* Manager = nullptr;
};
class MemorySource final : public IResourceSource {
public:
explicit MemorySource(AssetsManager* manager) : Manager(manager) {}
SourceResult tryGet(const ResourceKey& key) override {
auto iter = Manager->MemoryResourcesByHash.find(key.Hash);
if(iter == Manager->MemoryResourcesByHash.end())
return {SourceStatus::Miss, std::nullopt, 0};
return {SourceStatus::Hit, iter->second, 0};
}
void collectReady(std::vector<SourceReady>&) override {}
bool isAsync() const override {
return false;
}
void startPending(std::vector<Hash_t>) override {}
private:
AssetsManager* Manager = nullptr;
};
class CacheSource final : public IResourceSource {
public:
CacheSource(AssetsManager* manager, size_t sourceIndex)
: Manager(manager), SourceIndex(sourceIndex) {}
SourceResult tryGet(const ResourceKey&) override {
return {SourceStatus::Pending, std::nullopt, SourceIndex};
}
void collectReady(std::vector<SourceReady>& out) override {
std::vector<std::pair<Hash_t, std::optional<Resource>>> cached = Manager->Cache->pullReads();
out.reserve(out.size() + cached.size());
for(auto& [hash, res] : cached)
out.push_back(SourceReady{hash, res, SourceIndex});
}
bool isAsync() const override {
return true;
}
void startPending(std::vector<Hash_t> hashes) override {
if(!hashes.empty())
Manager->Cache->pushReads(std::move(hashes));
}
private:
AssetsManager* Manager = nullptr;
size_t SourceIndex = 0;
};
Sources.clear();
PackSourceIndex = Sources.size();
Sources.push_back(SourceEntry{std::make_unique<PackSource>(this), 0});
MemorySourceIndex = Sources.size();
Sources.push_back(SourceEntry{std::make_unique<MemorySource>(this), 0});
CacheSourceIndex = Sources.size();
Sources.push_back(SourceEntry{std::make_unique<CacheSource>(this, CacheSourceIndex), 0});
}
void AssetsManager::collectReadyFromSources() {
std::vector<SourceReady> ready;
for(auto& entry : Sources)
entry.Source->collectReady(ready);
for(SourceReady& item : ready) {
auto iter = PendingReadsByHash.find(item.Hash);
if(iter == PendingReadsByHash.end())
continue;
if(item.Value)
registerSourceHit(item.Hash, item.SourceIndex);
for(ResourceKey& key : iter->second) {
if(item.SourceIndex == CacheSourceIndex) {
if(item.Value) {
LOG.debug() << "Cache hit type=" << assetTypeName(key.Type)
<< " id=" << key.Id
<< " key=" << key.Domain << ':' << key.Key
<< " hash=" << int(item.Hash[0]) << '.'
<< int(item.Hash[1]) << '.'
<< int(item.Hash[2]) << '.'
<< int(item.Hash[3])
<< " size=" << item.Value->size();
} else {
LOG.debug() << "Cache miss type=" << assetTypeName(key.Type)
<< " id=" << key.Id
<< " key=" << key.Domain << ':' << key.Key
<< " hash=" << int(item.Hash[0]) << '.'
<< int(item.Hash[1]) << '.'
<< int(item.Hash[2]) << '.'
<< int(item.Hash[3]);
}
}
ReadyReads.emplace_back(std::move(key), item.Value);
}
PendingReadsByHash.erase(iter);
}
}
AssetsManager::SourceResult AssetsManager::querySources(const ResourceKey& key) {
auto cacheIter = SourceCacheByHash.find(key.Hash);
if(cacheIter != SourceCacheByHash.end()) {
const size_t cachedIndex = cacheIter->second.SourceIndex;
if(cachedIndex < Sources.size()
&& cacheIter->second.Generation == Sources[cachedIndex].Generation)
{
SourceResult cached = Sources[cachedIndex].Source->tryGet(key);
cached.SourceIndex = cachedIndex;
if(cached.Status != SourceStatus::Miss)
return cached;
}
SourceCacheByHash.erase(cacheIter);
}
SourceResult pending;
pending.Status = SourceStatus::Miss;
for(size_t i = 0; i < Sources.size(); ++i) {
SourceResult res = Sources[i].Source->tryGet(key);
res.SourceIndex = i;
if(res.Status == SourceStatus::Hit) {
registerSourceHit(key.Hash, i);
return res;
}
if(res.Status == SourceStatus::Pending && pending.Status == SourceStatus::Miss)
pending = res;
}
return pending;
}
void AssetsManager::registerSourceHit(const Hash_t& hash, size_t sourceIndex) {
if(sourceIndex >= Sources.size())
return;
if(Sources[sourceIndex].Source->isAsync())
return;
SourceCacheByHash[hash] = SourceCacheEntry{
.SourceIndex = sourceIndex,
.Generation = Sources[sourceIndex].Generation
};
}
void AssetsManager::invalidateSourceCache(size_t sourceIndex) {
if(sourceIndex >= Sources.size())
return;
Sources[sourceIndex].Generation++;
for(auto iter = SourceCacheByHash.begin(); iter != SourceCacheByHash.end(); ) {
if(iter->second.SourceIndex == sourceIndex)
iter = SourceCacheByHash.erase(iter);
else
++iter;
}
}
void AssetsManager::invalidateAllSourceCache() {
for(auto& entry : Sources)
entry.Generation++;
SourceCacheByHash.clear();
}
void AssetsManager::tickSources() {
collectReadyFromSources();
}
AssetsManager::PackReloadResult AssetsManager::reloadPacks(const PackRegister& reg) {
PackReloadResult result;
std::array<PackTable, static_cast<size_t>(AssetType::MAX_ENUM)> oldPacks;
for(size_t type = 0; type < static_cast<size_t>(AssetType::MAX_ENUM); ++type) {
oldPacks[type] = Types[type].PackResources;
Types[type].PackResources.clear();
}
for(const fs::path& instance : reg.Packs) {
try {
if(fs::is_regular_file(instance)) {
LOG.warn() << "Архивы ресурспаков пока не поддерживаются: " << instance.string();
continue;
}
if(!fs::is_directory(instance)) {
LOG.warn() << "Неизвестный тип ресурспака: " << instance.string();
continue;
}
fs::path assetsRoot = instance;
fs::path assetsCandidate = instance / "assets";
if(fs::exists(assetsCandidate) && fs::is_directory(assetsCandidate))
assetsRoot = assetsCandidate;
for(auto begin = fs::directory_iterator(assetsRoot), end = fs::directory_iterator(); begin != end; ++begin) {
if(!begin->is_directory())
continue;
fs::path domainPath = begin->path();
std::string domain = domainPath.filename().string();
for(size_t type = 0; type < static_cast<size_t>(AssetType::MAX_ENUM); ++type) {
AssetType assetType = static_cast<AssetType>(type);
fs::path assetPath = domainPath / enumAssetsToDirectory(assetType);
if(!fs::exists(assetPath) || !fs::is_directory(assetPath))
continue;
auto& typeTable = Types[type].PackResources[domain];
for(auto fbegin = fs::recursive_directory_iterator(assetPath),
fend = fs::recursive_directory_iterator();
fbegin != fend; ++fbegin) {
if(fbegin->is_directory())
continue;
fs::path file = fbegin->path();
if(assetType == AssetType::Texture && file.extension() == ".meta")
continue;
std::string key = fs::relative(file, assetPath).generic_string();
if(typeTable.contains(key))
continue;
PackResource entry;
entry.Type = assetType;
entry.Domain = domain;
entry.Key = key;
entry.LocalId = getOrCreateLocalId(assetType, entry.Domain, entry.Key);
try {
if(assetType == AssetType::Nodestate) {
std::u8string data = readFileBytes(file);
std::string_view view(reinterpret_cast<const char*>(data.data()), data.size());
js::object obj = js::parse(view).as_object();
HeadlessNodeState hns;
auto modelResolver = [&](std::string_view model) -> AssetsModel {
auto [mDomain, mKey] = parseDomainKey(model, entry.Domain);
return getOrCreateLocalId(AssetType::Model, mDomain, mKey);
};
entry.Header = hns.parse(obj, modelResolver);
std::u8string compiled = hns.dump();
entry.Res = Resource(std::move(compiled));
entry.Hash = entry.Res.hash();
} else if(assetType == AssetType::Model) {
const std::string ext = file.extension().string();
if(ext == ".json") {
std::u8string data = readFileBytes(file);
std::string_view view(reinterpret_cast<const char*>(data.data()), data.size());
js::object obj = js::parse(view).as_object();
HeadlessModel hm;
auto modelResolver = [&](std::string_view model) -> AssetsModel {
auto [mDomain, mKey] = parseDomainKey(model, entry.Domain);
return getOrCreateLocalId(AssetType::Model, mDomain, mKey);
};
auto normalizeTexturePipelineSrc = [](std::string_view src) -> std::string {
std::string out(src);
auto isSpace = [](unsigned char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; };
size_t start = 0;
while(start < out.size() && isSpace(static_cast<unsigned char>(out[start])))
++start;
if(out.compare(start, 3, "tex") != 0) {
std::string pref = "tex ";
pref += out.substr(start);
return pref;
}
return out;
};
auto textureResolver = [&](std::string_view textureSrc) -> std::vector<uint8_t> {
TexturePipelineProgram tpp;
if(!tpp.compile(normalizeTexturePipelineSrc(textureSrc)))
return {};
auto textureIdResolver = [&](std::string_view name) -> std::optional<uint32_t> {
auto [tDomain, tKey] = parseDomainKey(name, entry.Domain);
return getOrCreateLocalId(AssetType::Texture, tDomain, tKey);
};
if(!tpp.link(textureIdResolver))
return {};
return tpp.toBytes();
};
entry.Header = hm.parse(obj, modelResolver, textureResolver);
std::u8string compiled = hm.dump();
entry.Res = Resource(std::move(compiled));
entry.Hash = entry.Res.hash();
} else {
LOG.warn() << "Не поддерживаемый формат модели: " << file.string();
continue;
}
} else if(assetType == AssetType::Texture) {
std::u8string data = readFileBytes(file);
entry.Res = Resource(std::move(data));
entry.Hash = entry.Res.hash();
entry.Header = readOptionalMeta(file);
} else {
std::u8string data = readFileBytes(file);
entry.Res = Resource(std::move(data));
entry.Hash = entry.Res.hash();
}
} catch(const std::exception& exc) {
LOG.warn() << "Ошибка загрузки ресурса " << file.string() << ": " << exc.what();
continue;
}
typeTable.emplace(entry.Key, entry);
}
}
}
} catch(const std::exception& exc) {
LOG.warn() << "Ошибка загрузки ресурспака " << instance.string() << ": " << exc.what();
}
}
for(size_t type = 0; type < static_cast<size_t>(AssetType::MAX_ENUM); ++type) {
for(const auto& [domain, keyTable] : Types[type].PackResources) {
for(const auto& [key, res] : keyTable) {
bool changed = true;
auto oldDomain = oldPacks[type].find(domain);
if(oldDomain != oldPacks[type].end()) {
auto oldKey = oldDomain->second.find(key);
if(oldKey != oldDomain->second.end()) {
changed = oldKey->second.Hash != res.Hash;
}
}
if(changed)
result.ChangeOrAdd[type].push_back(res.LocalId);
}
}
for(const auto& [domain, keyTable] : oldPacks[type]) {
for(const auto& [key, res] : keyTable) {
auto newDomain = Types[type].PackResources.find(domain);
bool lost = true;
if(newDomain != Types[type].PackResources.end()) {
if(newDomain->second.contains(key))
lost = false;
}
if(lost)
result.Lost[type].push_back(res.LocalId);
}
}
}
invalidateAllSourceCache();
return result;
}
AssetsManager::BindResult AssetsManager::bindServerResource(AssetType type, AssetId serverId,
std::string domain, std::string key, const Hash_t& hash, std::vector<uint8_t> header)
{
BindResult result;
AssetId localFromDK = getOrCreateLocalId(type, domain, key);
auto& map = Types[static_cast<size_t>(type)].ServerToLocal;
AssetId localFromServer = 0;
if(serverId < map.size())
localFromServer = map[serverId];
if(localFromServer != 0)
unionLocalIds(type, localFromServer, localFromDK, &result.ReboundFrom);
AssetId localId = resolveLocalIdMutable(type, localFromDK);
if(serverId >= map.size())
map.resize(serverId + 1, 0);
map[serverId] = localId;
auto& infoList = Types[static_cast<size_t>(type)].BindInfos;
if(localId >= infoList.size())
infoList.resize(localId + 1);
bool hadBinding = infoList[localId].has_value();
bool changed = !hadBinding || infoList[localId]->Hash != hash || infoList[localId]->Header != header;
infoList[localId] = BindInfo{
.Type = type,
.LocalId = localId,
.Domain = std::move(domain),
.Key = std::move(key),
.Hash = hash,
.Header = std::move(header)
};
result.LocalId = localId;
result.Changed = changed;
result.NewBinding = !hadBinding;
return result;
}
std::optional<AssetsManager::AssetId> AssetsManager::unbindServerResource(AssetType type, AssetId serverId) {
auto& map = Types[static_cast<size_t>(type)].ServerToLocal;
if(serverId >= map.size())
return std::nullopt;
AssetId localId = map[serverId];
map[serverId] = 0;
if(localId == 0)
return std::nullopt;
return resolveLocalIdMutable(type, localId);
}
void AssetsManager::clearServerBindings() {
for(auto& typeData : Types) {
typeData.ServerToLocal.clear();
typeData.BindInfos.clear();
}
}
const AssetsManager::BindInfo* AssetsManager::getBind(AssetType type, AssetId localId) const {
localId = resolveLocalId(type, localId);
const auto& table = Types[static_cast<size_t>(type)].BindInfos;
if(localId >= table.size())
return nullptr;
if(!table[localId])
return nullptr;
return &*table[localId];
}
std::vector<uint8_t> AssetsManager::rebindHeader(AssetType type, const std::vector<uint8_t>& header, bool serverIds) {
auto mapModelId = [&](AssetId id) -> AssetId {
if(serverIds) {
auto localId = getLocalIdFromServer(AssetType::Model, id);
if(!localId) {
assert(!"Missing server bind for model id");
MAKE_ERROR("Нет бинда сервера для модели id=" << id);
}
return *localId;
}
return resolveLocalIdMutable(AssetType::Model, id);
};
auto mapTextureId = [&](AssetId id) -> AssetId {
if(serverIds) {
auto localId = getLocalIdFromServer(AssetType::Texture, id);
if(!localId) {
assert(!"Missing server bind for texture id");
MAKE_ERROR("Нет бинда сервера для текстуры id=" << id);
}
return *localId;
}
return resolveLocalIdMutable(AssetType::Texture, id);
};
auto warn = [&](const std::string& msg) {
LOG.warn() << msg;
};
return AssetsHeaderCodec::rebindHeader(type, header, mapModelId, mapTextureId, warn);
}
std::optional<AssetsManager::ParsedHeader> AssetsManager::parseHeader(AssetType type, const std::vector<uint8_t>& header) {
return AssetsHeaderCodec::parseHeader(type, header);
}
void AssetsManager::pushResources(std::vector<Resource> resources) {
for(const Resource& res : resources) {
Hash_t hash = res.hash();
MemoryResourcesByHash[hash] = res;
SourceCacheByHash.erase(hash);
registerSourceHit(hash, MemorySourceIndex);
auto iter = PendingReadsByHash.find(hash);
if(iter != PendingReadsByHash.end()) {
for(ResourceKey& key : iter->second)
ReadyReads.emplace_back(std::move(key), res);
PendingReadsByHash.erase(iter);
}
}
Cache->pushResources(std::move(resources));
}
void AssetsManager::pushReads(std::vector<ResourceKey> reads) {
std::unordered_map<size_t, std::vector<Hash_t>> pendingBySource;
for(ResourceKey& key : reads) {
SourceResult res = querySources(key);
if(res.Status == SourceStatus::Hit) {
if(res.SourceIndex == PackSourceIndex && res.Value) {
LOG.debug() << "Pack hit type=" << assetTypeName(key.Type)
<< " id=" << key.Id
<< " key=" << key.Domain << ':' << key.Key
<< " hash=" << int(key.Hash[0]) << '.'
<< int(key.Hash[1]) << '.'
<< int(key.Hash[2]) << '.'
<< int(key.Hash[3])
<< " size=" << res.Value->size();
}
ReadyReads.emplace_back(std::move(key), res.Value);
continue;
}
if(res.Status == SourceStatus::Pending) {
auto& list = PendingReadsByHash[key.Hash];
bool isFirst = list.empty();
list.push_back(std::move(key));
if(isFirst)
pendingBySource[res.SourceIndex].push_back(list.front().Hash);
continue;
}
ReadyReads.emplace_back(std::move(key), std::nullopt);
}
for(auto& [sourceIndex, hashes] : pendingBySource) {
if(sourceIndex < Sources.size())
Sources[sourceIndex].Source->startPending(std::move(hashes));
}
}
std::vector<std::pair<AssetsManager::ResourceKey, std::optional<Resource>>> AssetsManager::pullReads() {
tickSources();
std::vector<std::pair<ResourceKey, std::optional<Resource>>> out;
out.reserve(ReadyReads.size());
for(auto& entry : ReadyReads)
out.emplace_back(std::move(entry));
ReadyReads.clear();
return out;
}
AssetsManager::AssetId AssetsManager::getOrCreateLocalId(AssetType type, std::string_view domain, std::string_view key) {
auto& table = Types[static_cast<size_t>(type)].DKToLocal;
auto iterDomain = table.find(domain);
if(iterDomain == table.end()) {
iterDomain = table.emplace(
std::string(domain),
std::unordered_map<std::string, AssetId, detail::TSVHash, detail::TSVEq>{}
).first;
}
auto& keyTable = iterDomain->second;
auto iterKey = keyTable.find(key);
if(iterKey != keyTable.end()) {
iterKey->second = resolveLocalIdMutable(type, iterKey->second);
return iterKey->second;
}
AssetId id = allocateLocalId(type);
keyTable.emplace(std::string(key), id);
auto& dk = Types[static_cast<size_t>(type)].LocalToDK;
if(id >= dk.size())
dk.resize(id + 1);
dk[id] = DomainKey{std::string(domain), std::string(key), true};
return id;
}
std::optional<AssetsManager::AssetId> AssetsManager::getLocalIdFromServer(AssetType type, AssetId serverId) const {
const auto& map = Types[static_cast<size_t>(type)].ServerToLocal;
if(serverId >= map.size())
return std::nullopt;
AssetId local = map[serverId];
if(local == 0)
return std::nullopt;
return resolveLocalId(type, local);
}
AssetsManager::AssetId AssetsManager::resolveLocalId(AssetType type, AssetId localId) const {
if(localId == 0)
return 0;
const auto& parents = Types[static_cast<size_t>(type)].LocalParent;
if(localId >= parents.size())
return localId;
AssetId cur = localId;
while(cur < parents.size() && parents[cur] != cur && parents[cur] != 0)
cur = parents[cur];
return cur;
}
AssetsManager::AssetId AssetsManager::allocateLocalId(AssetType type) {
auto& next = Types[static_cast<size_t>(type)].NextLocalId;
AssetId id = next++;
auto& parents = Types[static_cast<size_t>(type)].LocalParent;
if(id >= parents.size())
parents.resize(id + 1, 0);
parents[id] = id;
auto& dk = Types[static_cast<size_t>(type)].LocalToDK;
if(id >= dk.size())
dk.resize(id + 1);
return id;
}
AssetsManager::AssetId AssetsManager::resolveLocalIdMutable(AssetType type, AssetId localId) {
if(localId == 0)
return 0;
auto& parents = Types[static_cast<size_t>(type)].LocalParent;
if(localId >= parents.size())
return localId;
AssetId root = localId;
while(root < parents.size() && parents[root] != root && parents[root] != 0)
root = parents[root];
if(root == localId)
return root;
AssetId cur = localId;
while(cur < parents.size() && parents[cur] != root && parents[cur] != 0) {
AssetId next = parents[cur];
parents[cur] = root;
cur = next;
}
return root;
}
void AssetsManager::unionLocalIds(AssetType type, AssetId fromId, AssetId toId, std::optional<AssetId>* reboundFrom) {
AssetId fromRoot = resolveLocalIdMutable(type, fromId);
AssetId toRoot = resolveLocalIdMutable(type, toId);
if(fromRoot == 0 || toRoot == 0 || fromRoot == toRoot)
return;
auto& parents = Types[static_cast<size_t>(type)].LocalParent;
if(fromRoot >= parents.size() || toRoot >= parents.size())
return;
parents[fromRoot] = toRoot;
if(reboundFrom)
*reboundFrom = fromRoot;
auto& dk = Types[static_cast<size_t>(type)].LocalToDK;
if(fromRoot < dk.size()) {
const DomainKey& fromDK = dk[fromRoot];
if(fromDK.Known) {
if(toRoot >= dk.size())
dk.resize(toRoot + 1);
DomainKey& toDK = dk[toRoot];
if(!toDK.Known) {
toDK = fromDK;
Types[static_cast<size_t>(type)].DKToLocal[toDK.Domain][toDK.Key] = toRoot;
} else if(toDK.Domain != fromDK.Domain || toDK.Key != fromDK.Key) {
LOG.warn() << "Конфликт домен/ключ при ребинде: "
<< fromDK.Domain << ':' << fromDK.Key << " vs "
<< toDK.Domain << ':' << toDK.Key;
}
}
}
auto& binds = Types[static_cast<size_t>(type)].BindInfos;
if(fromRoot < binds.size()) {
if(toRoot >= binds.size())
binds.resize(toRoot + 1);
if(!binds[toRoot] && binds[fromRoot])
binds[toRoot] = std::move(binds[fromRoot]);
}
}
std::optional<AssetsManager::PackResource> AssetsManager::findPackResource(AssetType type,
std::string_view domain, std::string_view key) const
{
const auto& typeTable = Types[static_cast<size_t>(type)].PackResources;
auto iterDomain = typeTable.find(domain);
if(iterDomain == typeTable.end())
return std::nullopt;
auto iterKey = iterDomain->second.find(key);
if(iterKey == iterDomain->second.end())
return std::nullopt;
return iterKey->second;
}
} // namespace LV::Client

View File

@@ -1,255 +1,638 @@
#pragma once
#include "Common/Abstract.hpp"
#include <algorithm>
#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>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include <cstring>
#include "Client/AssetsCacheManager.hpp"
#include "Client/AssetsHeaderCodec.hpp"
#include "Common/Abstract.hpp"
#include "Common/IdProvider.hpp"
#include "Common/AssetsPreloader.hpp"
#include "Common/TexturePipelineProgram.hpp"
#include "TOSLib.hpp"
#include "assets.hpp"
#include "boost/asio/io_context.hpp"
#include "png++/image.hpp"
#include <fstream>
#include "Abstract.hpp"
namespace LV::Client {
using namespace TOS;
namespace fs = std::filesystem;
// NOT ThreadSafe
class CacheDatabase {
const fs::path Path;
sqlite3 *DB = nullptr;
sqlite3_stmt *STMT_INSERT = nullptr,
*STMT_UPDATE_TIME = nullptr,
*STMT_REMOVE = nullptr,
*STMT_ALL_HASH = nullptr,
*STMT_SUM = nullptr,
*STMT_OLD = nullptr,
*STMT_TO_FREE = nullptr,
*STMT_COUNT = nullptr;
class AssetsManager : public IdProvider<EnumAssets> {
public:
CacheDatabase(const fs::path &cachePath);
~CacheDatabase();
struct ResourceUpdates {
CacheDatabase(const CacheDatabase&) = delete;
CacheDatabase(CacheDatabase&&) = delete;
CacheDatabase& operator=(const CacheDatabase&) = delete;
CacheDatabase& operator=(CacheDatabase&&) = delete;
/*
Выдаёт размер занимаемый всем хранимым кешем
*/
size_t getCacheSize();
// TODO: добавить ограничения на количество файлов
/*
Создаёт линейный массив в котором подряд указаны все хэш суммы в бинарном виде и возвращает их количество
*/
// std::pair<std::string, size_t> getAllHash();
/*
Обновляет время использования кеша
*/
void updateTimeFor(Hash_t hash);
/*
Добавляет запись
*/
void insert(Hash_t hash, size_t size);
/*
Выдаёт хэши на удаление по размеру в сумме больше bytesToFree.
Сначала удаляется старьё, потом по приоритету дата использования + размер
*/
std::vector<Hash_t> findExcessHashes(size_t bytesToFree, int timeBefore);
/*
Удаление записи
*/
void remove(Hash_t hash);
static std::string hashToString(Hash_t hash);
static int hexCharToInt(char c);
static Hash_t stringToHash(const std::string_view view);
};
/*
Менеджер предоставления ресурсов. Управляет ресурс паками
и хранением кешированных ресурсов с сервера.
Интерфейс однопоточный.
Обработка файлов в отдельном потоке.
*/
class AssetsManager : 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;
std::vector<AssetsModelUpdate> Models;
std::vector<AssetsNodestateUpdate> Nodestates;
std::vector<AssetsTextureUpdate> Textures;
std::vector<AssetsBinaryUpdate> Particles;
std::vector<AssetsBinaryUpdate> Animations;
std::vector<AssetsBinaryUpdate> Sounds;
std::vector<AssetsBinaryUpdate> Fonts;
};
public:
virtual ~AssetsManager();
static std::shared_ptr<AssetsManager> 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));
AssetsManager(asio::io_context& ioc, fs::path cachePath)
: Cache(AssetsCacheManager::Create(ioc, cachePath))
{
for(size_t type = 0; type < static_cast<size_t>(EnumAssets::MAX_ENUM); type++) {
ServerToClientMap[type].push_back(0);
}
}
// Добавить новый полученный с сервера ресурс
void pushResources(std::vector<Resource> resources) {
WriteQueue.lock()->push_range(resources);
// Ручные обновления
struct Out_checkAndPrepareResourcesUpdate {
AssetsPreloader::Out_checkAndPrepareResourcesUpdate RP, ES;
std::unordered_map<ResourceFile::Hash_t, std::u8string> Files;
};
Out_checkAndPrepareResourcesUpdate checkAndPrepareResourcesUpdate(
const std::vector<fs::path>& resourcePacks,
const std::vector<fs::path>& extraSources
) {
Out_checkAndPrepareResourcesUpdate result;
result.RP = ResourcePacks.checkAndPrepareResourcesUpdate(
AssetsPreloader::AssetsRegister{resourcePacks},
[&](EnumAssets type, std::string_view domain, std::string_view key) -> ResourceId {
return getId(type, domain, key);
},
[&](std::u8string&& data, ResourceFile::Hash_t hash, fs::path path) {
result.Files.emplace(hash, std::move(data));
}
);
result.ES = ExtraSource.checkAndPrepareResourcesUpdate(
AssetsPreloader::AssetsRegister{resourcePacks},
[&](EnumAssets type, std::string_view domain, std::string_view key) -> ResourceId {
return getId(type, domain, key);
}
);
return result;
}
// Добавить задачи на чтение
void pushReads(std::vector<ResourceKey> keys) {
ReadQueue.lock()->push_range(keys);
struct Out_applyResourcesUpdate {
};
Out_applyResourcesUpdate applyResourcesUpdate(const Out_checkAndPrepareResourcesUpdate& orr) {
Out_applyResourcesUpdate result;
for(size_t type = 0; type < static_cast<size_t>(EnumAssets::MAX_ENUM); ++type) {
for(ResourceId id : orr.RP.LostLinks[type]) {
std::optional<AssetsPreloader::Out_Resource> res = ResourcePacks.getResource((EnumAssets) type, id);
assert(res);
auto hashIter = HashToPath.find(res->Hash);
assert(hashIter != HashToPath.end());
auto& entry = hashIter->second;
auto iter = std::find(entry.begin(), entry.end(), res->Path);
assert(iter != entry.end());
entry.erase(iter);
if(entry.empty())
HashToPath.erase(hashIter);
}
}
// Получить считанные данные
std::vector<std::pair<ResourceKey, std::optional<Resource>>> pullReads() {
return std::move(*ReadyQueue.lock());
ResourcePacks.applyResourcesUpdate(orr.RP);
ExtraSource.applyResourcesUpdate(orr.ES);
std::unordered_set<ResourceFile::Hash_t> needHashes;
for(size_t type = 0; type < static_cast<size_t>(EnumAssets::MAX_ENUM); ++type) {
for(const auto& res : orr.RP.ResourceUpdates[type]) {
// Помечаем ресурс для обновления
PendingUpdateFromAsync[type].push_back(std::get<ResourceId>(res));
HashToPath[std::get<ResourceFile::Hash_t>(res)].push_back(std::get<fs::path>(res));
}
// Размер всего хранимого кеша
size_t getCacheSize() {
return DatabaseSize;
for(ResourceId id : orr.RP.LostLinks[type]) {
// Помечаем ресурс для обновления
PendingUpdateFromAsync[type].push_back(id);
auto& hh = ServerIdToHH[type];
if(id < hh.size())
needHashes.insert(std::get<ResourceFile::Hash_t>(hh[id]));
}
}
// Обновить параметры хранилища кеша
void updateParams(size_t maxLifeTime, size_t maxCacheDirectorySize) {
auto lock = Changes.lock();
lock->MaxLifeTime = maxLifeTime;
lock->MaxCacheDatabaseSize = maxCacheDirectorySize;
lock->MaxChange = true;
{
for(const auto& [hash, data] : orr.Files) {
WaitingHashes.insert(hash);
}
// Установка путей до папок assets
void setResourcePacks(std::vector<fs::path> packsAssets) {
auto lock = Changes.lock();
lock->Assets = std::move(packsAssets);
lock->AssetsChange = true;
for(const auto& hash : WaitingHashes)
needHashes.erase(hash);
std::vector<std::tuple<ResourceFile::Hash_t, fs::path>> toDisk;
std::vector<ResourceFile::Hash_t> toCache;
// Теперь раскидаем хеши по доступным источникам.
for(const auto& hash : needHashes) {
auto iter = HashToPath.find(hash);
if(iter != HashToPath.end()) {
// Ставим задачу загрузить с диска.
toDisk.emplace_back(hash, iter->second.front());
} else {
// Сделаем запрос в кеш.
toCache.push_back(hash);
}
}
// Запуск процедуры проверки хешей всего хранимого кеша
void runFullDatabaseRecheck(std::move_only_function<void(std::string result)>&& func) {
auto lock = Changes.lock();
lock->OnRecheckEnd = std::move(func);
lock->FullRecheck = true;
// Запоминаем, что эти ресурсы уже ожидаются.
WaitingHashes.insert_range(needHashes);
// Запрос в кеш (если там не найдётся, то запрос уйдёт на сервер).
if(!toCache.empty())
Cache->pushReads(std::move(toCache));
// Запрос к диску.
if(!toDisk.empty())
NeedToReadFromDisk.append_range(std::move(toDisk));
_onHashLoad(orr.Files);
}
bool hasError() {
return IssuedAnError;
return result;
}
// Получить или создать локальный идентификатор ресурса
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();
// ServerSession
// Новые привязки ассетов к Домен+Ключ.
void pushAssetsBindDK(
const std::vector<std::string>& domains,
const std::array<
std::vector<std::vector<std::string>>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
>& keys
) {
LOG.debug() << "BindDK domains=" << domains.size();
for(size_t type = 0; type < static_cast<size_t>(EnumAssets::MAX_ENUM); ++type) {
LOG.info() << type;
for(size_t forDomainIter = 0; forDomainIter < keys[type].size(); ++forDomainIter) {
LOG.info() << "\t" << domains[forDomainIter];
for(const std::string& key : keys[type][forDomainIter]) {
uint32_t id = getId((EnumAssets) type, domains[forDomainIter], key);
LOG.info() << "\t\t" << key << " -> " << id;
ServerToClientMap[type].push_back(id);
}
}
}
}
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;
// Новые привязки ассетов к Hash+Header.
void pushAssetsBindHH(
std::array<
std::vector<std::tuple<ResourceId, ResourceFile::Hash_t, ResourceHeader>>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
>&& hash_and_headers
) {
std::unordered_set<ResourceFile::Hash_t> needHashes;
size_t totalBinds = 0;
for(size_t type = 0; type < static_cast<size_t>(EnumAssets::MAX_ENUM); ++type) {
size_t maxSize = 0;
for(auto& [id, hash, header] : hash_and_headers[type]) {
totalBinds++;
assert(id < ServerToClientMap[type].size());
id = ServerToClientMap[type][id];
if(id >= maxSize)
maxSize = id+1;
// Добавляем идентификатор в таблицу ожидающих обновлений.
PendingUpdateFromAsync[type].push_back(id);
// Поискать есть ли ресурс в ресурспаках.
std::optional<AssetsPreloader::Out_Resource> res = ResourcePacks.getResource((EnumAssets) type, id);
if(res) {
needHashes.insert(res->Hash);
} else {
needHashes.insert(hash);
}
}
{
// Уберём повторения в идентификаторах.
auto& vec = PendingUpdateFromAsync[type];
std::sort(vec.begin(), vec.end());
vec.erase(std::unique(vec.begin(), vec.end()), vec.end());
}
if(ServerIdToHH[type].size() < maxSize)
ServerIdToHH[type].resize(maxSize);
for(auto& [id, hash, header] : hash_and_headers[type]) {
ServerIdToHH[type][id] = {hash, std::move(header)};
}
}
if(totalBinds)
LOG.debug() << "BindHH total=" << totalBinds << " wait=" << WaitingHashes.size();
// Нужно убрать хеши, которые уже запрошены
// needHashes ^ WaitingHashes.
for(const auto& hash : WaitingHashes)
needHashes.erase(hash);
std::vector<std::tuple<ResourceFile::Hash_t, fs::path>> toDisk;
std::vector<ResourceFile::Hash_t> toCache;
// Теперь раскидаем хеши по доступным источникам.
for(const auto& hash : needHashes) {
auto iter = HashToPath.find(hash);
if(iter != HashToPath.end()) {
// Ставим задачу загрузить с диска.
toDisk.emplace_back(hash, iter->second.front());
} else {
// Сделаем запрос в кеш.
toCache.push_back(hash);
}
}
// Запоминаем, что эти ресурсы уже ожидаются.
WaitingHashes.insert_range(needHashes);
// Запрос к диску.
if(!toDisk.empty())
NeedToReadFromDisk.append_range(std::move(toDisk));
// Запрос в кеш (если там не найдётся, то запрос уйдёт на сервер).
if(!toCache.empty())
Cache->pushReads(std::move(toCache));
}
// Новые ресурсы, полученные с сервера.
void pushNewResources(
std::vector<std::tuple<ResourceFile::Hash_t, std::u8string>> &&resources
) {
std::unordered_map<ResourceFile::Hash_t, std::u8string> files;
std::vector<Resource> vec;
files.reserve(resources.size());
vec.reserve(resources.size());
for(auto& [hash, res] : resources) {
vec.emplace_back(res);
files.emplace(hash, std::move(res));
}
_onHashLoad(files);
Cache->pushResources(std::move(vec));
}
// Для запроса отсутствующих ресурсов с сервера на клиент.
std::vector<ResourceFile::Hash_t> pullNeededResources() {
return std::move(NeedToRequestFromServer);
}
// Получить изменённые ресурсы (для передачи другим модулям).
ResourceUpdates pullResourceUpdates() {
return std::move(RU);
}
ResourceId reBind(EnumAssets type, ResourceId server) {
return ServerToClientMap[static_cast<size_t>(type)].at(server);
}
void tick() {
// Проверим кеш
std::vector<std::pair<Hash_t, std::optional<Resource>>> resources = Cache->pullReads();
if(!resources.empty()) {
std::unordered_map<ResourceFile::Hash_t, std::u8string> needToProceed;
needToProceed.reserve(resources.size());
for(auto& [hash, res] : resources) {
if(!res)
NeedToRequestFromServer.push_back(hash);
else
needToProceed.emplace(hash, std::u8string{(const char8_t*) res->data(), res->size()});
}
if(!NeedToRequestFromServer.empty())
LOG.debug() << "CacheMiss count=" << NeedToRequestFromServer.size();
if(!needToProceed.empty())
_onHashLoad(needToProceed);
}
/// Читаем с диска TODO: получилась хрень с определением типа, чтобы получать headless ресурс
if(!NeedToReadFromDisk.empty()) {
std::unordered_map<ResourceFile::Hash_t, std::u8string> files;
files.reserve(NeedToReadFromDisk.size());
auto detectTypeDomainKey = [&](const fs::path& path, EnumAssets& typeOut, std::string& domainOut, std::string& keyOut) -> bool {
fs::path cur = path.parent_path();
for(; !cur.empty(); cur = cur.parent_path()) {
std::string name = cur.filename().string();
for(size_t typeIndex = 0; typeIndex < static_cast<size_t>(EnumAssets::MAX_ENUM); ++typeIndex) {
EnumAssets type = static_cast<EnumAssets>(typeIndex);
if(name == ::EnumAssetsToDirectory(type)) {
typeOut = type;
domainOut = cur.parent_path().filename().string();
keyOut = fs::relative(path, cur).generic_string();
return true;
}
}
}
return false;
};
for(const auto& [hash, path] : NeedToReadFromDisk) {
std::u8string data;
std::ifstream file(path, std::ios::binary);
if(file) {
file.seekg(0, std::ios::end);
std::streamoff size = file.tellg();
if(size < 0)
size = 0;
file.seekg(0, std::ios::beg);
data.resize(static_cast<size_t>(size));
if(size > 0) {
file.read(reinterpret_cast<char*>(data.data()), size);
if(!file)
data.clear();
}
} else {
LOG.warn() << "DiskReadFail " << path.string();
}
if(!data.empty()) {
EnumAssets type{};
std::string domain;
std::string key;
if(detectTypeDomainKey(path, type, domain, key)) {
if(type == EnumAssets::Nodestate) {
std::string_view view(reinterpret_cast<const char*>(data.data()), data.size());
js::object obj = js::parse(view).as_object();
HeadlessNodeState hns;
auto modelResolver = [&](const std::string_view model) -> AssetsModel {
auto [mDomain, mKey] = parseDomainKey(model, domain);
return getId(EnumAssets::Model, mDomain, mKey);
};
hns.parse(obj, modelResolver);
data = hns.dump();
} else if(type == EnumAssets::Model) {
std::string_view view(reinterpret_cast<const char*>(data.data()), data.size());
js::object obj = js::parse(view).as_object();
HeadlessModel hm;
auto modelResolver = [&](const std::string_view model) -> AssetsModel {
auto [mDomain, mKey] = parseDomainKey(model, domain);
return getId(EnumAssets::Model, mDomain, mKey);
};
auto textureIdResolver = [&](const std::string_view texture) -> std::optional<uint32_t> {
auto [tDomain, tKey] = parseDomainKey(texture, domain);
return getId(EnumAssets::Texture, tDomain, tKey);
};
auto textureResolver = [&](const std::string_view texturePipelineSrc) -> std::vector<uint8_t> {
TexturePipelineProgram tpp;
if(!tpp.compile(texturePipelineSrc))
return {};
tpp.link(textureIdResolver);
return tpp.toBytes();
};
hm.parse(obj, modelResolver, textureResolver);
data = hm.dump();
}
}
}
files.emplace(hash, std::move(data));
}
NeedToReadFromDisk.clear();
_onHashLoad(files);
}
}
private:
Logger LOG = "Client>ResourceHandler";
const fs::path
CachePath,
PathDatabase = CachePath / "db.sqlite3",
PathFiles = CachePath / "blobs";
static constexpr size_t SMALL_RESOURCE = 1 << 21;
Logger LOG = "Client>AssetsManager";
sqlite3 *DB = nullptr; // База хранения кеша меньше 2мб и информации о кеше на диске
sqlite3_stmt
*STMT_DISK_INSERT = nullptr, // Вставка записи о хеше
*STMT_DISK_UPDATE_TIME = nullptr, // Обновить дату последнего использования
*STMT_DISK_REMOVE = nullptr, // Удалить хеш
*STMT_DISK_CONTAINS = nullptr, // Проверка наличия хеша
*STMT_DISK_SUM = nullptr, // Вычисляет занятое место на диске
*STMT_DISK_COUNT = nullptr, // Возвращает количество записей
// Менеджеры учёта дисковых ресурсов
AssetsPreloader
// В приоритете ищутся ресурсы из ресурспаков по Domain+Key.
ResourcePacks,
/*
Дополнительные источники ресурсов.
Используется для поиска ресурса по хешу от сервера (может стоит тот же мод с совпадающими ресурсами),
или для временной подгрузки ресурса по Domain+Key пока ресурс не был получен с сервера.
*/
ExtraSource;
*STMT_INLINE_INSERT = nullptr, // Вставка ресурса
*STMT_INLINE_GET = nullptr, // Поиск ресурса по хешу
*STMT_INLINE_UPDATE_TIME = nullptr, // Обновить дату последнего использования
*STMT_INLINE_SUM = nullptr, // Размер внутреннего хранилища
*STMT_INLINE_COUNT = nullptr; // Возвращает количество записей
// Менеджер файлового кэша.
AssetsCacheManager::Ptr Cache;
// Полный размер данных на диске (насколько известно)
volatile size_t DatabaseSize = 0;
// Указатели на доступные ресурсы
std::unordered_map<ResourceFile::Hash_t, std::vector<fs::path>> HashToPath;
// Очередь задач на чтение
TOS::SpinlockObject<std::queue<ResourceKey>> ReadQueue;
// Очередь на запись ресурсов
TOS::SpinlockObject<std::queue<Resource>> WriteQueue;
// Очередь на выдачу результатов чтения
TOS::SpinlockObject<std::vector<std::pair<ResourceKey, std::optional<Resource>>>> ReadyQueue;
// Таблица релинковки ассетов с идентификаторов сервера на клиентские.
std::array<
std::vector<ResourceId>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
> ServerToClientMap;
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;
volatile bool FullRecheck = false;
// Таблица серверных привязок HH (id клиентские)
std::array<
std::vector<std::tuple<ResourceFile::Hash_t, ResourceHeader>>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
> ServerIdToHH;
// Ресурсы в ожидании данных по хешу для обновления (с диска, кеша, сервера).
std::array<
std::vector<ResourceId>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
> PendingUpdateFromAsync;
// Хеши, для которых где-то висит задача на загрузку.
std::unordered_set<ResourceFile::Hash_t> WaitingHashes;
// Хеши, которые необходимо запросить с сервера.
std::vector<ResourceFile::Hash_t> NeedToRequestFromServer;
// Ресурсы, которые нужно считать с диска
std::vector<std::tuple<ResourceFile::Hash_t, fs::path>> NeedToReadFromDisk;
// Обновлённые ресурсы
ResourceUpdates RU;
// Когда данные были получены с диска, кеша или сервера
void _onHashLoad(const std::unordered_map<ResourceFile::Hash_t, std::u8string>& files) {
const auto& rpLinks = ResourcePacks.getResourceLinks();
const auto& esLinks = ExtraSource.getResourceLinks();
auto mapModelId = [&](ResourceId id) -> ResourceId {
const auto& map = ServerToClientMap[static_cast<size_t>(EnumAssets::Model)];
if(id >= map.size())
return 0;
return map[id];
};
auto mapTextureId = [&](ResourceId id) -> ResourceId {
const auto& map = ServerToClientMap[static_cast<size_t>(EnumAssets::Texture)];
if(id >= map.size())
return 0;
return map[id];
};
auto rebindHeader = [&](EnumAssets type, const ResourceHeader& header) -> ResourceHeader {
if(header.empty())
return {};
std::vector<uint8_t> bytes;
bytes.resize(header.size());
std::memcpy(bytes.data(), header.data(), header.size());
std::vector<uint8_t> rebound = AssetsHeaderCodec::rebindHeader(
type,
bytes,
mapModelId,
mapTextureId,
[](const std::string&) {}
);
return ResourceHeader(reinterpret_cast<const char8_t*>(rebound.data()), rebound.size());
};
TOS::SpinlockObject<Changes_t> Changes;
for(size_t typeIndex = 0; typeIndex < static_cast<size_t>(EnumAssets::MAX_ENUM); ++typeIndex) {
auto& pending = PendingUpdateFromAsync[typeIndex];
if(pending.empty())
continue;
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 = {};
std::vector<ResourceId> stillPending;
stillPending.reserve(pending.size());
size_t updated = 0;
size_t missingSource = 0;
size_t missingData = 0;
for(ResourceId id : pending) {
ResourceFile::Hash_t hash{};
ResourceHeader header;
bool hasSource = false;
bool localHeader = false;
virtual coro<> asyncDestructor();
AssetsManager(boost::asio::io_context &ioc, const fs::path &cachePath,
size_t maxCacheDatabaseSize, size_t maxLifeTime);
if(id < rpLinks[typeIndex].size() && rpLinks[typeIndex][id].IsExist) {
hash = rpLinks[typeIndex][id].Hash;
header = rpLinks[typeIndex][id].Header;
hasSource = true;
localHeader = true;
} else if(id < ServerIdToHH[typeIndex].size()) {
std::tie(hash, header) = ServerIdToHH[typeIndex][id];
hasSource = true;
}
void readWriteThread(AsyncUseControl::Lock lock);
std::string hashToString(const Hash_t& hash);
if(!hasSource) {
missingSource++;
stillPending.push_back(id);
continue;
}
auto dataIter = files.find(hash);
if(dataIter == files.end()) {
missingData++;
stillPending.push_back(id);
continue;
}
std::string domain = "core";
std::string key;
{
auto d = getDK((EnumAssets) typeIndex, id);
if(d) {
domain = d->Domain;
key = d->Key;
}
}
std::u8string data = dataIter->second;
EnumAssets type = static_cast<EnumAssets>(typeIndex);
ResourceHeader finalHeader = localHeader ? header : rebindHeader(type, header);
if(id == 0)
continue;
if(type == EnumAssets::Nodestate) {
HeadlessNodeState ns;
ns.load(data);
HeadlessNodeState::Header headerParsed;
headerParsed.load(finalHeader);
RU.Nodestates.push_back({id, std::move(ns), std::move(headerParsed)});
updated++;
} else if(type == EnumAssets::Model) {
HeadlessModel hm;
hm.load(data);
HeadlessModel::Header headerParsed;
headerParsed.load(finalHeader);
RU.Models.push_back({id, std::move(hm), std::move(headerParsed)});
updated++;
} else if(type == EnumAssets::Texture) {
AssetsTextureUpdate entry;
entry.Id = id;
entry.Header = std::move(finalHeader);
if(!data.empty()) {
iResource sres(reinterpret_cast<const uint8_t*>(data.data()), data.size());
iBinaryStream stream = sres.makeStream();
png::image<png::rgba_pixel> img(stream.Stream);
entry.Width = static_cast<uint16_t>(img.get_width());
entry.Height = static_cast<uint16_t>(img.get_height());
entry.Pixels.resize(static_cast<size_t>(entry.Width) * entry.Height);
for(uint32_t y = 0; y < entry.Height; ++y) {
const auto& row = img.get_pixbuf().operator[](y);
for(uint32_t x = 0; x < entry.Width; ++x) {
const auto& px = row[x];
uint32_t rgba = (uint32_t(px.alpha) << 24)
| (uint32_t(px.red) << 16)
| (uint32_t(px.green) << 8)
| uint32_t(px.blue);
entry.Pixels[x + y * entry.Width] = rgba;
}
}
}
RU.Textures.push_back(std::move(entry));
updated++;
} else if(type == EnumAssets::Particle) {
RU.Particles.push_back({id, std::move(data)});
updated++;
} else if(type == EnumAssets::Animation) {
RU.Animations.push_back({id, std::move(data)});
updated++;
} else if(type == EnumAssets::Sound) {
RU.Sounds.push_back({id, std::move(data)});
updated++;
} else if(type == EnumAssets::Font) {
RU.Fonts.push_back({id, std::move(data)});
updated++;
}
}
if(updated || missingSource || missingData) {
LOG.debug() << "HashLoad type=" << int(typeIndex)
<< " updated=" << updated
<< " missingSource=" << missingSource
<< " missingData=" << missingData;
}
pending = std::move(stillPending);
}
for(const auto& [hash, res] : files)
WaitingHashes.erase(hash);
}
};
}
} // namespace LV::Client

File diff suppressed because it is too large Load Diff

View File

@@ -69,22 +69,26 @@ private:
IRenderSession *RS = nullptr;
// Обработчик кеша ресурсов сервера
AssetsManager::Ptr AM;
AssetsManager AM;
static constexpr uint64_t TIME_BEFORE_UNLOAD_RESOURCE = 180;
struct {
// Существующие привязки ресурсов
std::unordered_set<ResourceId> ExistBinds[(int) EnumAssets::MAX_ENUM];
// std::unordered_set<ResourceId> ExistBinds[(int) EnumAssets::MAX_ENUM];
// Недавно использованные ресурсы, пока хранятся здесь в течении TIME_BEFORE_UNLOAD_RESOURCE секунд
std::unordered_map<std::string, std::pair<AssetEntry, uint64_t>> NotInUse[(int) EnumAssets::MAX_ENUM];
// std::unordered_map<std::string, std::pair<AssetEntry, uint64_t>> NotInUse[(int) EnumAssets::MAX_ENUM];
} MyAssets;
struct AssetLoading {
struct AssetLoadingEntry {
EnumAssets Type;
ResourceId Id;
std::string Domain, Key;
std::string Domain;
std::string Key;
};
struct AssetLoading {
std::u8string Data;
size_t Offset;
size_t Offset = 0;
};
struct AssetBindEntry {
@@ -95,21 +99,41 @@ private:
std::vector<uint8_t> Header;
};
std::array<std::vector<std::pair<std::string, std::string>>, (int) EnumAssets::MAX_ENUM> ServerIdToDK;
std::array<ResourceId, (int) EnumAssets::MAX_ENUM> NextServerId = {};
struct UpdateAssetsBindsDK {
std::vector<std::string> Domains;
std::array<
std::vector<std::vector<std::string>>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
> Keys;
};
struct UpdateAssetsBindsHH {
std::array<
std::vector<std::tuple<ResourceId, ResourceFile::Hash_t, ResourceHeader>>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
> HashAndHeaders;
};
struct TickData {
std::vector<std::pair<DefVoxelId, void*>> Profile_Voxel_AddOrChange;
// Полученные изменения привязок Domain+Key
std::vector<UpdateAssetsBindsDK> BindsDK;
// Полученные изменения привязок Hash+Header
std::vector<UpdateAssetsBindsHH> BindsHH;
// Потерянные привязываются к hash_t(0)
// Полученные с сервера ресурсы
std::vector<std::tuple<ResourceFile::Hash_t, std::u8string>> ReceivedAssets;
std::vector<std::pair<DefVoxelId, DefVoxel>> Profile_Voxel_AddOrChange;
std::vector<DefVoxelId> Profile_Voxel_Lost;
std::vector<std::pair<DefNodeId, DefNode_t>> Profile_Node_AddOrChange;
std::vector<std::pair<DefNodeId, DefNode>> Profile_Node_AddOrChange;
std::vector<DefNodeId> Profile_Node_Lost;
std::vector<std::pair<DefWorldId, void*>> Profile_World_AddOrChange;
std::vector<std::pair<DefWorldId, DefWorld>> Profile_World_AddOrChange;
std::vector<DefWorldId> Profile_World_Lost;
std::vector<std::pair<DefPortalId, void*>> Profile_Portal_AddOrChange;
std::vector<std::pair<DefPortalId, DefPortal>> Profile_Portal_AddOrChange;
std::vector<DefPortalId> Profile_Portal_Lost;
std::vector<std::pair<DefEntityId, DefEntityInfo>> Profile_Entity_AddOrChange;
std::vector<std::pair<DefEntityId, DefEntity>> Profile_Entity_AddOrChange;
std::vector<DefEntityId> Profile_Entity_Lost;
std::vector<std::pair<DefItemId, void*>> Profile_Item_AddOrChange;
std::vector<std::pair<DefItemId, DefItem>> Profile_Item_AddOrChange;
std::vector<DefItemId> Profile_Item_Lost;
std::vector<std::pair<WorldId_t, void*>> Worlds_AddOrChange;
@@ -127,13 +151,6 @@ private:
uint32_t Nodes = 0;
};
struct AssetsBindsChange {
// Новые привязки ресурсов
std::vector<AssetBindEntry> Binds;
// Потерянные из видимости ресурсы
std::vector<ResourceId> Lost[(int) EnumAssets::MAX_ENUM];
};
struct {
// Сюда обращается ветка, обрабатывающая сокет; run()
// Получение ресурсов с сервера
@@ -141,22 +158,7 @@ private:
// Накопление данных за такт сервера
TickData ThisTickEntry;
// Сюда обращается ветка обновления IServerSession, накапливая данные до SyncTick
// Ресурсы, ожидающие ответа от менеджера кеша
std::unordered_map<std::string, std::vector<std::pair<std::string, Hash_t>>> ResourceWait[(int) EnumAssets::MAX_ENUM];
// Полученные изменения связок в ожидании стадии синхронизации такта
std::vector<AssetsBindsChange> Binds;
// Подгруженные или принятые меж тактами ресурсы
std::vector<AssetEntry> LoadedResources;
// Список ресурсов на которые уже был отправлен запрос на загрузку ресурса
std::vector<Hash_t> AlreadyLoading;
// Обменный пункт
// Полученные ресурсы с сервера
TOS::SpinlockObject<std::vector<AssetEntry>> LoadedAssets;
// Изменения в наблюдаемых ресурсах
TOS::SpinlockObject<std::vector<AssetsBindsChange>> AssetsBinds;
// Пакеты обновлений игрового мира
TOS::SpinlockObject<std::vector<TickData>> TickSequence;
} AsyncContext;
@@ -191,10 +193,20 @@ private:
coro<> run(AsyncUseControl::Lock);
void protocolError();
coro<> readPacket(Net::AsyncSocket &sock);
coro<> rP_System(Net::AsyncSocket &sock);
coro<> rP_Resource(Net::AsyncSocket &sock);
coro<> rP_Definition(Net::AsyncSocket &sock);
coro<> rP_Content(Net::AsyncSocket &sock);
coro<> rP_Disconnect(Net::AsyncSocket &sock);
coro<> rP_AssetsBindDK(Net::AsyncSocket &sock);
coro<> rP_AssetsBindHH(Net::AsyncSocket &sock);
coro<> rP_AssetsInitSend(Net::AsyncSocket &sock);
coro<> rP_AssetsNextSend(Net::AsyncSocket &sock);
coro<> rP_DefinitionsFull(Net::AsyncSocket &sock);
coro<> rP_DefinitionsUpdate(Net::AsyncSocket &sock);
coro<> rP_ChunkVoxels(Net::AsyncSocket &sock);
coro<> rP_ChunkNodes(Net::AsyncSocket &sock);
coro<> rP_ChunkLightPrism(Net::AsyncSocket &sock);
coro<> rP_RemoveRegion(Net::AsyncSocket &sock);
coro<> rP_Tick(Net::AsyncSocket &sock);
coro<> rP_TestLinkCameraToEntity(Net::AsyncSocket &sock);
coro<> rP_TestUnlinkCamera(Net::AsyncSocket &sock);
// Нужен сокет, на котором только что был согласован игровой протокол (asyncInitGameProtocol)

View File

@@ -16,7 +16,7 @@
class SharedStagingBuffer {
public:
static constexpr VkDeviceSize kDefaultSize = 64ull * 1024ull * 1024ull;
static constexpr VkDeviceSize kDefaultSize = 18ull * 1024ull * 1024ull;
SharedStagingBuffer(VkDevice device,
VkPhysicalDevice physicalDevice,

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,7 @@
#include <png++/png.hpp>
#include "VulkanRenderSession.hpp"
#include <Server/GameServer.hpp>
#include <malloc.h>
extern void LoadSymbolsVulkan(TOS::DynamicLibrary &library);
@@ -222,6 +223,8 @@ void Vulkan::run()
} catch(const std::exception &exc) {
LOG.error() << "Game.Session->shutdown: " << exc.what();
}
Game.Session = nullptr;
}
if(!NeedShutdown && glfwWindowShouldClose(Graphics.Window)) {
@@ -240,11 +243,12 @@ void Vulkan::run()
try {
if(Game.Session)
Game.Session->shutdown(EnumDisconnect::ByInterface);
Game.Session = nullptr;
} catch(const std::exception &exc) {
LOG.error() << "Game.Session->shutdown: " << exc.what();
}
Game.Session = nullptr;
try {
if(Game.Server)
Game.Server->GS.shutdown("Завершение работы из-за остановки клиента");
@@ -2254,6 +2258,10 @@ void Vulkan::gui_MainMenu() {
}
}
if(ImGui::Button("Memory trim")) {
malloc_trim(0);
}
if(ConnectionProgress.InProgress) {
if(ImGui::Button("Отмена"))
ConnectionProgress.Cancel = true;
@@ -2291,6 +2299,8 @@ void Vulkan::gui_ConnectedToServer() {
double chunksKb = double(Game.Session->getVisibleCompressedChunksBytes()) / 1024.0;
ImGui::Text("chunks compressed: %.1f KB", chunksKb);
ImGui::Checkbox("Логи сетевых пакетов", &Game.Session->DebugLogPackets);
if(ImGui::Button("Delimeter"))
LOG.debug();
@@ -2303,6 +2313,10 @@ void Vulkan::gui_ConnectedToServer() {
Game.ImGuiInterfaces.pop_back();
}
if(ImGui::Button("Memory trim")) {
malloc_trim(0);
}
ImGui::End();
if(Game.Выйти)

View File

@@ -48,7 +48,7 @@ struct DeviceId {
struct Settings {
DeviceId DeviceMain;
uint32_t QueueGraphics = -1, QueueSurface = -1;
bool Debug = true;
bool Debug = false;
bool isValid()
{

View File

@@ -121,15 +121,15 @@ void ChunkMeshGenerator::run(uint8_t id) {
uint8_t fullNodes[18][18][18];
// Профиль, который используется если на стороне клиента отсутствует нужных профиль
DefNode_t defaultProfileNode;
DefNode defaultProfileNode;
// Кеш запросов профилей нод
std::unordered_map<DefNodeId, const DefNode_t*> profilesNodeCache;
auto getNodeProfile = [&](DefNodeId id) -> const DefNode_t* {
std::unordered_map<DefNodeId, const DefNode*> profilesNodeCache;
auto getNodeProfile = [&](DefNodeId id) -> const DefNode* {
auto iterCache = profilesNodeCache.find(id);
if(iterCache == profilesNodeCache.end()) {
// Промах кеша
auto iterSS = SS->Profiles.DefNode.find(id);
if(iterSS != SS->Profiles.DefNode.end()) {
auto iterSS = SS->Profiles.DefNodes.find(id);
if(iterSS != SS->Profiles.DefNodes.end()) {
return (profilesNodeCache[id] = &iterSS->second);
} else {
// Профиль отсутствует на клиенте
@@ -140,6 +140,14 @@ void ChunkMeshGenerator::run(uint8_t id) {
}
};
std::vector<NodeStateInfo> metaStatesInfo;
{
NodeStateInfo info;
info.Name = "meta";
info.Variations = 256;
metaStatesInfo.push_back(std::move(info));
}
// Воксели пока не рендерим
if(auto iterWorld = SS->Content.Worlds.find(wId); iterWorld != SS->Content.Worlds.end()) {
Pos::GlobalRegion rPos = pos >> 2;
@@ -181,14 +189,50 @@ void ChunkMeshGenerator::run(uint8_t id) {
std::unordered_map<DefNodeId, bool> nodeFullCuboidCache;
auto nodeIsFull = [&](Node node) -> bool {
if(node.NodeId == 0)
return false;
auto iterCache = nodeFullCuboidCache.find(node.Data);
if(iterCache == nodeFullCuboidCache.end()) {
const DefNode_t* profile = getNodeProfile(node.NodeId);
if(profile->TexId != 0) {
return (nodeFullCuboidCache[node.Data] = true);
const DefNode* profile = getNodeProfile(node.NodeId);
if(NSP) {
if(const AssetsNodestate* ptr = std::get_if<AssetsNodestate>(&profile->RenderStates)) {
if(NSP->hasNodestate(*ptr)) {
std::unordered_map<std::string, int32_t> states;
int32_t meta = node.Meta;
states.emplace("meta", meta);
const auto routes = NSP->getModelsForNode(*ptr, metaStatesInfo, states);
bool isFull = !routes.empty();
if(isFull) {
for(const auto& variants : routes) {
for(const auto& [weight, faces] : variants) {
(void)weight;
auto hasFace = [&](EnumFace face) -> bool {
auto iterFace = faces.find(face);
return iterFace != faces.end() && !iterFace->second.empty();
};
if(!hasFace(EnumFace::Up)
|| !hasFace(EnumFace::Down)
|| !hasFace(EnumFace::East)
|| !hasFace(EnumFace::West)
|| !hasFace(EnumFace::South)
|| !hasFace(EnumFace::North))
{
isFull = false;
break;
}
}
if(!isFull)
break;
}
}
return (nodeFullCuboidCache[node.Data] = false);
return (nodeFullCuboidCache[node.Data] = isFull);
}
}
}
return (nodeFullCuboidCache[node.Data] = true);
} else {
return iterCache->second;
}
@@ -273,7 +317,7 @@ void ChunkMeshGenerator::run(uint8_t id) {
} else {
for(int y = 0; y < 16; y++)
for(int x = 0; x < 16; x++)
fullNodes[x+0][y+1][0] = 0;
fullNodes[x+1][y+1][0] = 0;
}
} else
goto end;
@@ -325,14 +369,6 @@ void ChunkMeshGenerator::run(uint8_t id) {
std::unordered_map<uint32_t, ModelCacheEntry> modelCache;
std::unordered_map<AssetsTexture, uint32_t> baseTextureCache;
std::vector<NodeStateInfo> metaStatesInfo;
{
NodeStateInfo info;
info.Name = "meta";
info.Variations = 256;
metaStatesInfo.push_back(std::move(info));
}
auto isFaceCovered = [&](EnumFace face, int covered) -> bool {
switch(face) {
case EnumFace::Up: return covered & (1 << 2);
@@ -388,6 +424,10 @@ void ChunkMeshGenerator::run(uint8_t id) {
for(int z = 0; z < 16; z++)
for(int y = 0; y < 16; y++)
for(int x = 0; x < 16; x++) {
const Node& nodeData = (*chunk)[x+y*16+z*16*16];
if(nodeData.NodeId == 0)
continue;
const size_t vertexStart = result.NodeVertexs.size();
int fullCovered = 0;
@@ -401,28 +441,20 @@ void ChunkMeshGenerator::run(uint8_t id) {
if(fullCovered == 0b111111)
continue;
const Node& nodeData = (*chunk)[x+y*16+z*16*16];
const DefNode_t* node = getNodeProfile(nodeData.NodeId);
if(debugMeshEnabled) {
const bool hasRenderable = (node->NodestateId != 0) || (node->TexId != 0);
if(hasRenderable && fullCovered != 0b111111) {
expectedColumnX[x] = 1;
expectedColumnY[y] = 1;
expectedColumnZ[z] = 1;
}
}
const DefNode* node = getNodeProfile(nodeData.NodeId);
bool usedModel = false;
if(NSP && (node->NodestateId != 0 || NSP->hasNodestate(node->NodestateId))) {
if(NSP) {
if(const AssetsNodestate* ptr = std::get_if<AssetsNodestate>(&node->RenderStates)) {
if(NSP->hasNodestate(*ptr)) {
auto iterCache = modelCache.find(nodeData.Data);
if(iterCache == modelCache.end()) {
std::unordered_map<std::string, int32_t> states;
states.emplace("meta", nodeData.Meta);
ModelCacheEntry entry;
entry.Routes = NSP->getModelsForNode(node->NodestateId, metaStatesInfo, states);
entry.Routes = NSP->getModelsForNode(*ptr, metaStatesInfo, states);
iterCache = modelCache.emplace(nodeData.Data, std::move(entry)).first;
}
@@ -442,25 +474,13 @@ void ChunkMeshGenerator::run(uint8_t id) {
usedModel = true;
}
}
}
}
if(usedModel)
goto node_done;
if(NSP && node->TexId != 0) {
auto iterTex = baseTextureCache.find(node->TexId);
if(iterTex != baseTextureCache.end()) {
v.Tex = iterTex->second;
} else {
uint32_t resolvedTex = NSP->getTextureId(node->TexId);
v.Tex = resolvedTex;
baseTextureCache.emplace(node->TexId, resolvedTex);
}
} else {
v.Tex = node->TexId;
}
if(v.Tex == 0)
goto node_done;
v.Tex = 0;
// Рендерим обычный кубоид
// XZ+Y
@@ -668,58 +688,6 @@ void ChunkMeshGenerator::run(uint8_t id) {
}
node_done:
if(debugMeshEnabled) {
const bool emitted = result.NodeVertexs.size() > vertexStart;
if(emitted) {
generatedColumnX[x] = 1;
generatedColumnY[y] = 1;
generatedColumnZ[z] = 1;
} else {
const bool hasRenderable = (node->NodestateId != 0) || (node->TexId != 0);
if(hasRenderable && fullCovered != 0b111111) {
uint32_t warnIndex = debugMeshWarnCount.fetch_add(1);
if(warnIndex < 16) {
LOG.warn() << "Missing node geometry at chunk " << int(pos[0]) << ','
<< int(pos[1]) << ',' << int(pos[2])
<< " local " << x << ',' << y << ',' << z
<< " nodeId " << nodeData.NodeId
<< " meta " << int(nodeData.Meta)
<< " covered " << fullCovered
<< " tex " << node->TexId
<< " nodestate " << node->NodestateId;
}
}
}
}
}
if(debugMeshEnabled) {
auto collectMissing = [](const std::array<uint8_t, 16>& expected,
const std::array<uint8_t, 16>& generated) {
std::string res;
for(int i = 0; i < 16; i++) {
if(expected[i] && !generated[i]) {
if(!res.empty())
res += ',';
res += std::to_string(i);
}
}
return res;
};
std::string missingX = collectMissing(expectedColumnX, generatedColumnX);
std::string missingY = collectMissing(expectedColumnY, generatedColumnY);
std::string missingZ = collectMissing(expectedColumnZ, generatedColumnZ);
if(!missingX.empty() || !missingY.empty() || !missingZ.empty()) {
uint32_t warnIndex = debugMeshWarnCount.fetch_add(1);
if(warnIndex < 16) {
LOG.warn() << "Missing mesh columns at chunk " << int(pos[0]) << ','
<< int(pos[1]) << ',' << int(pos[2])
<< " missingX[" << missingX << "]"
<< " missingY[" << missingY << "]"
<< " missingZ[" << missingZ << "]";
}
}
}
// Вычислить индексы и сократить вершины
@@ -907,6 +875,7 @@ void ChunkPreparator::tickSync(const TickSyncData& data) {
// Получаем готовые чанки
{
std::vector<ChunkMeshGenerator::ChunkObj_t> chunks = std::move(*CMG.Output.lock());
uint8_t frameRetirement = (FrameRoulette+FRAME_COUNT_RESOURCE_LATENCY) % FRAME_COUNT_RESOURCE_LATENCY;
for(auto& chunk : chunks) {
auto iterWorld = Requests.find(chunk.WId);
if(iterWorld == Requests.end())
@@ -921,6 +890,14 @@ void ChunkPreparator::tickSync(const TickSyncData& data) {
// Чанк ожидаем
auto& rChunk = ChunksMesh[chunk.WId][chunk.Pos >> 2][Pos::bvec4u(chunk.Pos & 0x3).pack()];
if(rChunk.VoxelPointer)
VPV_ToFree[frameRetirement].emplace_back(std::move(rChunk.VoxelPointer));
if(rChunk.NodePointer) {
VPN_ToFree[frameRetirement].emplace_back(std::move(rChunk.NodePointer), std::move(rChunk.NodeIndexes));
}
rChunk.VoxelPointer = {};
rChunk.NodePointer = {};
rChunk.NodeIndexes = {};
rChunk.Voxels = std::move(chunk.VoxelDefines);
if(!chunk.VoxelVertexs.empty())
rChunk.VoxelPointer = VertexPool_Voxels.pushVertexs(std::move(chunk.VoxelVertexs));
@@ -1603,6 +1580,8 @@ VulkanRenderSession::VulkanRenderSession(Vulkan *vkInst, IServerSession *serverS
}
VulkanRenderSession::~VulkanRenderSession() {
if(VoxelOpaquePipeline)
vkDestroyPipeline(VkInst->Graphics.Device, VoxelOpaquePipeline, nullptr);
if(VoxelTransparentPipeline)
@@ -1630,7 +1609,7 @@ void VulkanRenderSession::pushStageTickSync() {
CP.pushStageTickSync();
}
void VulkanRenderSession::tickSync(const TickSyncData& data) {
void VulkanRenderSession::tickSync(TickSyncData& data) {
// Изменение ассетов
// Профили
// Чанки
@@ -1648,75 +1627,16 @@ void VulkanRenderSession::tickSync(const TickSyncData& data) {
if(auto iter = data.Profiles_Lost.find(EnumDefContent::Voxel); iter != data.Profiles_Lost.end())
mcpData.ChangedVoxels.insert(mcpData.ChangedVoxels.end(), iter->second.begin(), iter->second.end());
std::vector<std::tuple<AssetsModel, Resource, const std::vector<uint8_t>*>> modelResources;
std::vector<AssetsModel> modelLost;
if(auto iter = data.Assets_ChangeOrAdd.find(EnumAssets::Model); iter != data.Assets_ChangeOrAdd.end()) {
const auto& list = ServerSession->Assets[EnumAssets::Model];
for(ResourceId id : iter->second) {
auto entryIter = list.find(id);
if(entryIter == list.end())
continue;
modelResources.emplace_back(id, entryIter->second.Res, &entryIter->second.Dependencies);
}
}
if(auto iter = data.Assets_Lost.find(EnumAssets::Model); iter != data.Assets_Lost.end())
modelLost.insert(modelLost.end(), iter->second.begin(), iter->second.end());
std::vector<AssetsModel> changedModels;
if(!modelResources.empty() || !modelLost.empty()) {
const auto& modelAssets = ServerSession->Assets[EnumAssets::Model];
changedModels = MP.onModelChanges(std::move(modelResources), std::move(modelLost), &modelAssets);
}
if(!data.AssetsModels.empty())
changedModels = MP.onModelChanges(std::move(data.AssetsModels));
if(TP) {
std::vector<TextureProvider::TextureUpdate> textureResources;
std::vector<AssetsTexture> textureLost;
if(auto iter = data.Assets_ChangeOrAdd.find(EnumAssets::Texture); iter != data.Assets_ChangeOrAdd.end()) {
const auto& list = ServerSession->Assets[EnumAssets::Texture];
for(ResourceId id : iter->second) {
auto entryIter = list.find(id);
if(entryIter == list.end())
continue;
textureResources.push_back({
.Id = id,
.Res = entryIter->second.Res,
.Domain = entryIter->second.Domain,
.Key = entryIter->second.Key
});
}
}
if(auto iter = data.Assets_Lost.find(EnumAssets::Texture); iter != data.Assets_Lost.end())
textureLost.insert(textureLost.end(), iter->second.begin(), iter->second.end());
if(!textureResources.empty() || !textureLost.empty())
TP->onTexturesChanges(std::move(textureResources), std::move(textureLost));
}
if(TP && !data.AssetsTextures.empty())
TP->onTexturesChanges(std::move(data.AssetsTextures));
std::vector<AssetsNodestate> changedNodestates;
if(NSP) {
std::vector<std::tuple<AssetsNodestate, Resource, const std::vector<uint8_t>*>> nodestateResources;
std::vector<AssetsNodestate> nodestateLost;
if(auto iter = data.Assets_ChangeOrAdd.find(EnumAssets::Nodestate); iter != data.Assets_ChangeOrAdd.end()) {
const auto& list = ServerSession->Assets[EnumAssets::Nodestate];
for(ResourceId id : iter->second) {
auto entryIter = list.find(id);
if(entryIter == list.end())
continue;
nodestateResources.emplace_back(id, entryIter->second.Res, &entryIter->second.Dependencies);
}
}
if(auto iter = data.Assets_Lost.find(EnumAssets::Nodestate); iter != data.Assets_Lost.end())
nodestateLost.insert(nodestateLost.end(), iter->second.begin(), iter->second.end());
if(!nodestateResources.empty() || !nodestateLost.empty() || !changedModels.empty())
changedNodestates = NSP->onNodestateChanges(std::move(nodestateResources), std::move(nodestateLost), changedModels);
if(NSP && (!data.AssetsNodestates.empty() || !changedModels.empty())) {
changedNodestates = NSP->onNodestateChanges(std::move(data.AssetsNodestates), std::move(changedModels));
}
if(!changedNodestates.empty()) {
@@ -1725,8 +1645,9 @@ void VulkanRenderSession::tickSync(const TickSyncData& data) {
for(AssetsNodestate id : changedNodestates)
changed.insert(id);
for(const auto& [nodeId, def] : ServerSession->Profiles.DefNode) {
if(changed.contains(def.NodestateId))
for(const auto& [nodeId, def] : ServerSession->Profiles.DefNodes) {
if(const AssetsNodestate* ptr = std::get_if<AssetsNodestate>(&def.RenderStates))
if(changed.contains(*ptr))
mcpData.ChangedNodes.push_back(nodeId);
}
}
@@ -2016,29 +1937,7 @@ void VulkanRenderSession::ensureEntityTexture() {
if(EntityTextureReady || !TP || !NSP)
return;
auto iter = ServerSession->Assets.find(EnumAssets::Texture);
if(iter == ServerSession->Assets.end() || iter->second.empty())
return;
const AssetEntry* picked = nullptr;
for(const auto& [id, entry] : iter->second) {
if(entry.Key == "default.png") {
picked = &entry;
break;
}
}
if(!picked) {
for(const auto& [id, entry] : iter->second) {
if(entry.Key == "grass.png") {
picked = &entry;
break;
}
}
}
if(!picked)
picked = &iter->second.begin()->second;
updateTestQuadTexture(NSP->getTextureId(picked->Id));
}
void VulkanRenderSession::ensureAtlasLayerPreview() {

View File

@@ -5,6 +5,8 @@
#include "Common/Abstract.hpp"
#include <Client/Vulkan/Vulkan.hpp>
#include <algorithm>
#include <array>
#include <atomic>
#include <bitset>
#include <condition_variable>
#include <cstring>
@@ -87,9 +89,7 @@ public:
}
// Применяет изменения, возвращая все затронутые модели
std::vector<AssetsModel> onModelChanges(std::vector<std::tuple<AssetsModel, Resource, const std::vector<uint8_t>*>> newOrChanged,
std::vector<AssetsModel> lost,
const std::unordered_map<ResourceId, AssetEntry>* modelAssets) {
std::vector<AssetsModel> onModelChanges(std::vector<AssetsModelUpdate> entries) {
std::vector<AssetsModel> result;
std::move_only_function<void(ResourceId)> makeUnready;
@@ -122,73 +122,33 @@ public:
iterModel->second.Ready = false;
};
for(ResourceId lostId : lost) {
makeUnready(lostId);
}
for(ResourceId lostId : lost) {
auto iterModel = Models.find(lostId);
if(iterModel == Models.end())
continue;
Models.erase(iterModel);
}
std::unordered_map<std::string, ResourceId> modelKeyToId;
if(modelAssets) {
modelKeyToId.reserve(modelAssets->size());
for(const auto& [id, entry] : *modelAssets) {
modelKeyToId.emplace(entry.Domain + ':' + entry.Key, id);
}
}
for(const auto& [key, resource, deps] : newOrChanged) {
for(const AssetsModelUpdate& entry : entries) {
const AssetsModel key = entry.Id;
result.push_back(key);
makeUnready(key);
ModelObject model;
std::string type = "unknown";
std::optional<AssetsManager::ParsedHeader> header;
if(deps && !deps->empty()) {
header = AssetsManager::parseHeader(*deps);
if(header && header->Type != EnumAssets::Model)
header.reset();
}
const std::vector<uint32_t>* textureDeps = header ? &header->TextureDeps : nullptr;
auto remapTextureId = [&](uint32_t placeholder) -> uint32_t {
if(!textureDeps || placeholder >= textureDeps->size())
return 0;
return (*textureDeps)[placeholder];
};
auto remapPipeline = [&](TexturePipeline pipe) {
if(textureDeps) {
for(auto& texId : pipe.BinTextures)
texId = remapTextureId(texId);
if(!pipe.Pipeline.empty()) {
std::vector<uint8_t> code;
code.resize(pipe.Pipeline.size());
std::memcpy(code.data(), pipe.Pipeline.data(), code.size());
TexturePipelineProgram::remapTexIds(code, *textureDeps, nullptr);
pipe.Pipeline.resize(code.size());
std::memcpy(pipe.Pipeline.data(), code.data(), code.size());
}
}
return pipe;
};
const HeadlessModel& hm = entry.Model;
const HeadlessModel::Header& header = entry.Header;
try {
std::u8string_view data((const char8_t*) resource.data(), resource.size());
if(data.starts_with((const char8_t*) "bm")) {
type = "InternalBinary";
// Компилированная модель внутреннего формата
LV::PreparedModel pm((std::u8string) data);
model.TextureMap.clear();
model.TextureMap.reserve(pm.CompiledTextures.size());
for(auto& [tkey, pipe] : pm.CompiledTextures)
model.TextureMap.emplace(tkey, remapPipeline(std::move(pipe)));
model.TextureMap.reserve(hm.Textures.size());
for(const auto& [tkey, id] : hm.Textures) {
TexturePipeline pipe;
if(id < header.TexturePipelines.size()) {
pipe.Pipeline = header.TexturePipelines[id];
} else {
LOG.warn() << "Model texture pipeline id out of range: model=" << key
<< " local=" << id
<< " pipelines=" << header.TexturePipelines.size();
pipe.BinTextures.push_back(id);
}
model.TextureMap.emplace(tkey, std::move(pipe));
}
model.TextureKeys = {};
for(const PreparedModel::Cuboid& cb : pm.Cuboids) {
for(const HeadlessModel::Cuboid& cb : hm.Cuboids) {
glm::vec3 min = glm::min(cb.From, cb.To), max = glm::max(cb.From, cb.To);
for(const auto& [face, params] : cb.Faces) {
@@ -264,17 +224,21 @@ public:
}
cb.Trs.apply(v);
model.Vertecies[params.Cullface].append_range(v);
const EnumFace cullKey = (params.Cullface == EnumFace::None) ? face : params.Cullface;
model.Vertecies[cullKey].append_range(v);
}
}
if(!pm.SubModels.empty() && modelAssets) {
model.Depends.reserve(pm.SubModels.size());
for(const auto& sub : pm.SubModels) {
auto iter = modelKeyToId.find(sub.Domain + ':' + sub.Key);
if(iter == modelKeyToId.end())
if(!hm.SubModels.empty()) {
model.Depends.reserve(hm.SubModels.size());
for(const auto& sub : hm.SubModels) {
if(sub.Id >= header.Models.size()) {
LOG.warn() << "Model sub-model id out of range: model=" << key
<< " local=" << sub.Id
<< " deps=" << header.Models.size();
continue;
model.Depends.emplace_back(iter->second, Transformations{});
}
model.Depends.emplace_back(header.Models[sub.Id], Transformations{});
}
}
@@ -285,21 +249,21 @@ public:
// std::vector<Transformation> Transformations;
} else if(data.starts_with((const char8_t*) "glTF")) {
type = "glb";
} else if(data.starts_with((const char8_t*) "bgl")) {
type = "InternalGLTF";
} else if(data.starts_with((const char8_t*) "{")) {
type = "InternalJson или glTF";
// Модель внутреннего формата или glTF
}
} catch(const std::exception& exc) {
LOG.warn() << "Не удалось распарсить модель " << type << ":\n\t" << exc.what();
LOG.warn() << "Не удалось собрать модель:\n\t" << exc.what();
continue;
}
{
size_t vertexCount = 0;
for(const auto& [_, verts] : model.Vertecies)
vertexCount += verts.size();
LOG.debug() << "Model loaded id=" << key
<< " verts=" << vertexCount
<< " texKeys=" << model.TextureKeys.size()
<< " pipelines=" << header.TexturePipelines.size();
}
Models.insert_or_assign(key, std::move(model));
}
@@ -452,7 +416,9 @@ class TextureProvider {
public:
struct TextureUpdate {
AssetsTexture Id = 0;
Resource Res;
uint16_t Width = 0;
uint16_t Height = 0;
std::vector<uint32_t> Pixels;
std::string Domain;
std::string Key;
};
@@ -592,56 +558,35 @@ public:
}
// Применяет изменения, возвращая все затронутые модели
std::vector<AssetsTexture> onTexturesChanges(std::vector<TextureUpdate> newOrChanged, std::vector<AssetsTexture> lost) {
std::vector<AssetsTexture> onTexturesChanges(std::vector<AssetsTextureUpdate> entries) {
std::lock_guard lock(Mutex);
std::vector<AssetsTexture> result;
for(const auto& update : newOrChanged) {
const AssetsTexture key = update.Id;
const Resource& res = update.Res;
for(auto& entry : entries) {
const AssetsTexture key = entry.Id;
result.push_back(key);
iResource sres((const uint8_t*) res.data(), res.size());
iBinaryStream stream = sres.makeStream();
png::image<png::rgba_pixel> img(stream.Stream);
uint32_t width = img.get_width();
uint32_t height = img.get_height();
std::vector<uint32_t> pixels;
pixels.resize(width*height);
for(uint32_t y = 0; y < height; y++) {
const auto& row = img.get_pixbuf().operator [](y);
for(uint32_t x = 0; x < width; x++) {
const auto& px = row[x];
uint32_t rgba = (uint32_t(px.alpha) << 24)
| (uint32_t(px.red) << 16)
| (uint32_t(px.green) << 8)
| uint32_t(px.blue);
pixels[x + y * width] = rgba;
}
}
if(entry.Width == 0 || entry.Height == 0 || entry.Pixels.empty())
continue;
Atlas->updateTexture(key, StoredTexture(
static_cast<uint16_t>(width),
static_cast<uint16_t>(height),
std::move(pixels)
entry.Width,
entry.Height,
std::move(entry.Pixels)
));
if(auto anim = getDefaultAnimation(update.Key, width, height)) {
AnimatedSources[key] = *anim;
} else {
bool animated = false;
AnimatedSources.erase(key);
}
NeedsUpload = true;
}
for(AssetsTexture key : lost) {
result.push_back(key);
Atlas->freeTexture(key);
AnimatedSources.erase(key);
NeedsUpload = true;
static std::atomic<uint32_t> debugTextureLogCount = 0;
uint32_t idx = debugTextureLogCount.fetch_add(1);
if(idx < 128) {
LOG.debug() << "Texture loaded id=" << key
<< " size=" << entry.Width << 'x' << entry.Height
<< " animated=" << (animated ? 1 : 0);
}
}
std::sort(result.begin(), result.end());
@@ -857,54 +802,16 @@ public:
{}
// Применяет изменения, возвращает изменённые описания состояний
std::vector<AssetsNodestate> onNodestateChanges(std::vector<std::tuple<AssetsNodestate, Resource, const std::vector<uint8_t>*>> newOrChanged, std::vector<AssetsNodestate> lost, std::vector<AssetsModel> changedModels) {
std::vector<AssetsNodestate> onNodestateChanges(std::vector<AssetsNodestateUpdate> newOrChanged, std::vector<AssetsModel> changedModels) {
std::vector<AssetsNodestate> result;
for(ResourceId lostId : lost) {
auto iterNodestate = Nodestates.find(lostId);
if(iterNodestate == Nodestates.end())
continue;
result.push_back(lostId);
Nodestates.erase(iterNodestate);
}
for(const auto& [key, resource, deps] : newOrChanged) {
for(const AssetsNodestateUpdate& entry : newOrChanged) {
const AssetsNodestate key = entry.Id;
result.push_back(key);
PreparedNodeState nodestate;
std::string type = "unknown";
try {
std::u8string_view data((const char8_t*) resource.data(), resource.size());
if(data.starts_with((const char8_t*) "bn")) {
type = "InternalBinary";
// Компилированный nodestate внутреннего формата
nodestate = PreparedNodeState(data);
} else if(data.starts_with((const char8_t*) "{")) {
type = "InternalJson";
// nodestate в json формате
} else {
type = "InternalBinaryLegacy";
// Старый двоичный формат без заголовка "bn"
std::u8string patched;
patched.reserve(data.size() + 2);
patched.push_back(u8'b');
patched.push_back(u8'n');
patched.append(data);
nodestate = PreparedNodeState(patched);
}
} catch(const std::exception& exc) {
LOG.warn() << "Не удалось распарсить nodestate " << type << ":\n\t" << exc.what();
continue;
}
if(deps && !deps->empty()) {
auto header = AssetsManager::parseHeader(*deps);
if(header && header->Type == EnumAssets::Nodestate) {
nodestate.LocalToModel.assign(header->ModelDeps.begin(), header->ModelDeps.end());
}
}
static_cast<HeadlessNodeState&>(nodestate) = entry.Nodestate;
nodestate.LocalToModel.assign(entry.Header.Models.begin(), entry.Header.Models.end());
Nodestates.insert_or_assign(key, std::move(nodestate));
if(key < 64) {
@@ -971,6 +878,11 @@ public:
auto appendModel = [&](AssetsModel modelId, const std::vector<Transformation>& transforms, std::unordered_map<EnumFace, std::vector<NodeVertexStatic>>& out) {
ModelProvider::Model model = MP.getModel(modelId);
if(model.Vertecies.empty()) {
if(MissingModelGeometryLogged.insert(modelId).second) {
LOG.warn() << "Model has no geometry id=" << modelId;
}
}
Transformations trf{transforms};
for(auto& [l, r] : model.Vertecies) {
@@ -1018,13 +930,28 @@ public:
if(const PreparedNodeState::Model* ptr = std::get_if<PreparedNodeState::Model>(&m)) {
AssetsModel modelId;
if(resolveModelId(ptr->Id, modelId))
if(resolveModelId(ptr->Id, modelId)) {
appendModel(modelId, ptr->Transforms, out);
} else {
uint64_t missKey = (uint64_t(id) << 32) | ptr->Id;
if(MissingLocalModelMapLogged.insert(missKey).second) {
LOG.warn() << "Missing model mapping nodestate=" << id
<< " local=" << ptr->Id
<< " locals=" << nodestate.LocalToModel.size();
}
}
} else if(const PreparedNodeState::VectorModel* ptr = std::get_if<PreparedNodeState::VectorModel>(&m)) {
for(const auto& sub : ptr->Models) {
AssetsModel modelId;
if(!resolveModelId(sub.Id, modelId))
if(!resolveModelId(sub.Id, modelId)) {
uint64_t missKey = (uint64_t(id) << 32) | sub.Id;
if(MissingLocalModelMapLogged.insert(missKey).second) {
LOG.warn() << "Missing model mapping nodestate=" << id
<< " local=" << sub.Id
<< " locals=" << nodestate.LocalToModel.size();
}
continue;
}
std::vector<Transformation> transforms = sub.Transforms;
transforms.insert(transforms.end(), ptr->Transforms.begin(), ptr->Transforms.end());
@@ -1061,6 +988,8 @@ private:
TextureProvider& TP;
std::unordered_map<AssetsNodestate, PreparedNodeState> Nodestates;
std::unordered_set<AssetsNodestate> MissingNodestateLogged;
std::unordered_set<uint64_t> MissingLocalModelMapLogged;
std::unordered_set<AssetsModel> MissingModelGeometryLogged;
std::unordered_set<uint64_t> EmptyRouteLogged;
};
@@ -1170,7 +1099,7 @@ public:
assert(vkInst);
assert(serverSession);
CMG.changeThreadsCount(1);
CMG.changeThreadsCount(3);
}
~ChunkPreparator() {
@@ -1282,10 +1211,10 @@ class VulkanRenderSession : public IRenderSession {
glm::vec3 X64Offset_f, X64Delta; // Смещение мира относительно игрока в матрице вида (0 -> 64)
glm::quat Quat;
ChunkPreparator CP;
ModelProvider MP;
std::unique_ptr<TextureProvider> TP;
ModelProvider MP;
std::unique_ptr<NodestateProvider> NSP;
ChunkPreparator CP;
AtlasImage LightDummy;
Buffer TestQuad;
@@ -1339,7 +1268,7 @@ public:
virtual void prepareTickSync() override;
virtual void pushStageTickSync() override;
virtual void tickSync(const TickSyncData& data) override;
virtual void tickSync(TickSyncData& data) override;
virtual void setCameraPos(WorldId_t worldId, Pos::Object pos, glm::quat quat) override;

View File

@@ -1,5 +1,5 @@
#include "Abstract.hpp"
#include "Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp"
#include "Common/TexturePipelineProgram.hpp"
#include "Common/Net.hpp"
#include "TOSLib.hpp"
#include <boost/interprocess/file_mapping.hpp>
@@ -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;
@@ -827,7 +783,7 @@ void unCompressNodes_bit(const std::u8string& compressed, Node* ptr) {
}
}
void unCompressNodes(const std::u8string& compressed, Node* ptr) {
void unCompressNodes(std::u8string_view compressed, Node* ptr) {
const std::u8string& next = unCompressLinear(compressed);
const Node *lPtr = (const Node*) next.data();
std::copy(lPtr, lPtr+16*16*16, ptr);
@@ -868,7 +824,123 @@ std::u8string unCompressLinear(std::u8string_view data) {
return *(std::u8string*) &outString;
}
PreparedNodeState::PreparedNodeState(const std::string_view modid, const js::object& profile) {
Hash_t ResourceFile::calcHash(const char8_t* data, size_t size) {
return sha2::sha256((const uint8_t*) data, size);
}
uint16_t HeadlessNodeState::Header::addModel(AssetsModel id) {
auto iter = std::find(Models.begin(), Models.end(), id);
if(iter == Models.end()) {
Models.push_back(id);
return Models.size() - 1;
}
return iter - Models.begin();
}
void HeadlessNodeState::Header::load(std::u8string_view data) {
Models.clear();
if(data.empty())
return;
if(data.size() % sizeof(AssetsModel) != 0)
MAKE_ERROR("Invalid nodestate header size");
const size_t count = data.size() / sizeof(AssetsModel);
Models.resize(count);
std::memcpy(Models.data(), data.data(), data.size());
}
ResourceHeader HeadlessNodeState::Header::dump() const {
ResourceHeader rh;
rh.reserve(Models.size() * sizeof(AssetsModel));
for(AssetsModel id : Models) {
rh += std::u8string_view((const char8_t*) &id, sizeof(AssetsModel));
}
return rh;
}
uint16_t HeadlessModel::Header::addModel(AssetsModel id) {
auto iter = std::find(Models.begin(), Models.end(), id);
if(iter == Models.end()) {
Models.push_back(id);
return Models.size() - 1;
}
return iter - Models.begin();
}
uint16_t HeadlessModel::Header::addTexturePipeline(std::vector<uint8_t> pipeline) {
TexturePipelines.push_back(std::move(pipeline));
return TexturePipelines.size() - 1;
}
void HeadlessModel::Header::load(std::u8string_view data) {
Models.clear();
TexturePipelines.clear();
if(data.empty())
return;
try {
TOS::ByteBuffer buffer(data.size(), reinterpret_cast<const uint8_t*>(data.data()));
auto reader = buffer.reader();
uint16_t modelCount = reader.readUInt16();
Models.reserve(modelCount);
for(uint16_t i = 0; i < modelCount; ++i)
Models.push_back(reader.readUInt32());
uint16_t texCount = reader.readUInt16();
TexturePipelines.reserve(texCount);
for(uint16_t i = 0; i < texCount; ++i) {
uint32_t size32 = reader.readUInt32();
TOS::ByteBuffer pipe;
reader.readBuffer(pipe);
if(pipe.size() != size32)
MAKE_ERROR("Invalid model header size");
TexturePipelines.emplace_back(pipe.begin(), pipe.end());
}
} catch(const std::exception&) {
MAKE_ERROR("Invalid model header");
}
}
ResourceHeader HeadlessModel::Header::dump() const {
TOS::ByteBuffer rh;
{
uint32_t fullSize = 0;
for(const auto& vector : TexturePipelines)
fullSize += vector.size();
rh.reserve(2 + Models.size() * sizeof(AssetsModel) + 2 + 4 * TexturePipelines.size() + fullSize);
}
TOS::ByteBuffer::Writer wr;
wr << uint16_t(Models.size());
for(AssetsModel id : Models)
wr << id;
wr << uint16_t(TexturePipelines.size());
for(const auto& pipe : TexturePipelines) {
wr << uint32_t(pipe.size());
wr << pipe;
}
TOS::ByteBuffer buff = wr.complite();
return std::u8string((const char8_t*) buff.data(), buff.size());
}
ResourceHeader HeadlessNodeState::parse(const js::object& profile, const std::function<AssetsModel(const std::string_view model)>& modelResolver) {
Header header;
std::function<uint16_t(const std::string_view model)> headerResolver =
[&](const std::string_view model) -> uint16_t {
AssetsModel id = modelResolver(model);
return header.addModel(id);
};
for(auto& [condition, variability] : profile) {
// Распарсить условие
uint16_t node = parseCondition(condition);
@@ -881,39 +953,33 @@ 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));
}
return header.dump();
}
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 +1092,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 +1169,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 +1481,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 +1510,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 +1531,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 +1541,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 +1580,34 @@ 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
) {
Header header;
std::function<uint16_t(const std::string_view model)> headerResolverModel =
[&](const std::string_view model) -> uint16_t {
AssetsModel id = modelResolver(model);
return header.addModel(id);
};
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;
}
uint16_t id = header.addTexturePipeline(textureResolver(texturePipelineSrc));
textureToLocal[(std::string) texturePipelineSrc] = id;
return id;
};
if(profile.contains("gui_light")) {
std::string_view gui_light = profile.at("gui_light").as_string();
@@ -1649,7 +1660,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,53 +1813,36 @@ 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);
}
}
}
if(boost::system::result<const js::value&> subModels_val = profile.try_at("sub_models")) {
const js::array& subModels = subModels_val->as_array();
// Заголовок
TOS::ByteBuffer rh;
for(const js::value& sub_val : subModels) {
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);
} 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);
if(boost::system::result<const js::value&> scene_val = profile.try_at("scene"))
result.Scene = scene_val->to_number<uint16_t>();
}
SubModels.emplace_back(std::move(result));
}
}
return header.dump();
}
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 +1889,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 +1949,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 +1961,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 +2000,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 +2041,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
@@ -2077,37 +2051,6 @@ std::u8string PreparedModel::dump() const {
return result.complite();
}
PreparedGLTF::PreparedGLTF(const std::string_view modid, const js::object& gltf) {
// gltf
// Сцена по умолчанию
// Сцены -> Ноды
// Ноды -> Ноды, меши, матрицы, translation, rotation
// Меши -> Примитивы
// Примитивы -> Материал, вершинные данные
// Материалы -> текстуры
// Текстуры
// Буферы
}
PreparedGLTF::PreparedGLTF(const std::string_view modid, Resource glb) {
}
PreparedGLTF::PreparedGLTF(std::u8string_view data) {
// lr.checkUnreaded();
}
std::u8string PreparedGLTF::dump() const {
std::unreachable();
}
void PreparedGLTF::load(std::u8string_view data) {
}
struct Resource::InlineMMap {
boost::interprocess::file_mapping MMap;
boost::interprocess::mapped_region Region;

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,19 @@ 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 ResourceFile {
using Hash_t = std::array<uint8_t, 32>;
struct TexturePipeline {
// Разыменованые идентификаторы
std::vector<AssetsTexture> BinTextures;
// Чистый код текстурных преобразований, локальные идентификаторы связаны с BinTextures
std::u8string Pipeline;
Hash_t Hash;
std::u8string Data;
bool operator==(const TexturePipeline& other) const {
return BinTextures == other.BinTextures && Pipeline == other.Pipeline;
static Hash_t calcHash(const char8_t* data, size_t size);
void calcHash() {
Hash = calcHash(Data.data(), Data.size());
}
};
// Компилятор текстурных потоков
PrecompiledTexturePipeline compileTexturePipeline(const std::string &cmd, std::string_view defaultDomain = "core");
struct NodestateEntry {
std::string Name;
int Variability = 0; // Количество возможный значений состояния
@@ -646,8 +675,14 @@ struct HeadlessNodeState {
std::vector<Transformation> Transforms;
};
// Локальный идентификатор в именной ресурс
std::vector<std::string> LocalToModelKD;
struct Header {
std::vector<AssetsModel> Models;
uint16_t addModel(AssetsModel id);
void load(std::u8string_view data);
ResourceHeader dump() const;
};
// Ноды выражений
std::vector<Node> Nodes;
// Условия -> вариации модели + веса
@@ -865,7 +900,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);
};
@@ -885,6 +920,16 @@ struct HeadlessModel {
std::optional<EnumGuiLight> GuiLight = EnumGuiLight::Default;
std::optional<bool> AmbientOcclusion = false;
struct Header {
std::vector<AssetsModel> Models;
std::vector<std::vector<uint8_t>> TexturePipelines;
uint16_t addModel(AssetsModel id);
uint16_t addTexturePipeline(std::vector<uint8_t> pipeline);
void load(std::u8string_view data);
ResourceHeader dump() const;
};
struct FullTransformation {
glm::vec3
Rotation = glm::vec3(0),
@@ -893,8 +938,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 +960,7 @@ struct HeadlessModel {
std::vector<Cuboid> Cuboids;
struct SubModel {
std::string Domain, Key;
uint16_t Id;
std::optional<uint16_t> Scene;
};
@@ -960,29 +1004,24 @@ struct HeadlessModel {
std::u8string dump() const;
};
struct PreparedGLTF {
std::vector<std::string> TextureKey;
std::unordered_map<std::string, PrecompiledTexturePipeline> Textures;
std::unordered_map<std::string, TexturePipeline> CompiledTextures;
std::vector<Vertex> Vertices;
struct TexturePipeline {
std::vector<uint32_t> BinTextures;
std::vector<uint8_t> Pipeline;
bool operator==(const TexturePipeline& other) const {
return BinTextures == other.BinTextures && Pipeline == other.Pipeline;
}
};
PreparedGLTF(const std::string_view modid, const js::object& gltf);
PreparedGLTF(const std::string_view modid, Resource glb);
PreparedGLTF(std::u8string_view data);
struct PreparedNodeState : public HeadlessNodeState {
using HeadlessNodeState::Model;
using HeadlessNodeState::VectorModel;
PreparedGLTF() = default;
PreparedGLTF(const PreparedGLTF&) = default;
PreparedGLTF(PreparedGLTF&&) = default;
std::vector<AssetsModel> LocalToModel;
PreparedGLTF& operator=(const PreparedGLTF&) = default;
PreparedGLTF& operator=(PreparedGLTF&&) = default;
// Пишет в сжатый двоичный формат
std::u8string dump() const;
private:
void load(std::u8string_view data);
PreparedNodeState() = default;
PreparedNodeState(std::u8string_view data) { load(data); }
PreparedNodeState(const std::u8string& data) { load(data); }
};
enum struct TexturePipelineCMD : uint8_t {
@@ -1063,16 +1102,17 @@ struct hash<LV::Hash_t> {
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;
std::size_t operator()(const LV::TexturePipeline& pipe) const noexcept {
std::size_t v = 14695981039346656037ULL;
for (uint32_t id : pipe.BinTextures) {
v ^= static_cast<std::size_t>(id);
v *= 1099511628211ULL;
}
for (uint8_t byte : pipe.Pipeline) {
v ^= static_cast<std::size_t>(byte);
v *= 1099511628211ULL;
}
return v;
}
};
}

View File

@@ -1,10 +1,17 @@
#include "AssetsPreloader.hpp"
#include "Common/Abstract.hpp"
#include "Common/TexturePipelineProgram.hpp"
#include "sha2.hpp"
#include <atomic>
#include <filesystem>
#include <fstream>
#include <unordered_set>
#include <utility>
namespace LV {
static TOS::Logger LOG = "AssetsPreloader";
static ResourceFile readFileBytes(const fs::path& path) {
std::ifstream file(path, std::ios::binary);
if(!file)
@@ -39,30 +46,53 @@ static std::u8string readOptionalMeta(const fs::path& path) {
}
AssetsPreloader::AssetsPreloader() {
std::fill(NextId.begin(), NextId.end(), 1);
std::fill(LastSendId.begin(), LastSendId.end(), 1);
for(size_t type = 0; type < static_cast<size_t>(EnumAssets::MAX_ENUM); type++) {
ResourceLinks[type].emplace_back(
ResourceFile::Hash_t{0},
ResourceHeader(),
fs::file_time_type(),
fs::path{""},
false
);
}
}
AssetsPreloader::Out_reloadResources AssetsPreloader::reloadResources(const AssetsRegister& instances, ReloadStatus* status) {
AssetsPreloader::Out_checkAndPrepareResourcesUpdate AssetsPreloader::checkAndPrepareResourcesUpdate(
const AssetsRegister& instances,
const std::function<ResourceId(EnumAssets type, std::string_view domain, std::string_view key)>& idResolver,
const std::function<void(std::u8string&& resource, ResourceFile::Hash_t hash, fs::path resPath)>& onNewResourceParsed,
ReloadStatus* status
) {
assert(idResolver);
#ifndef NDEBUG
bool expected = false;
assert(_Reloading.compare_exchange_strong(expected, true) && "Двойной вызов reloadResources");
struct ReloadGuard {
std::atomic<bool>& Flag;
~ReloadGuard() { Flag.exchange(false); }
} guard{_Reloading};
#endif
try {
ReloadStatus secondStatus;
return _reloadResources(instances, status ? *status : secondStatus);
return _checkAndPrepareResourcesUpdate(instances, idResolver, onNewResourceParsed, status ? *status : secondStatus);
} catch(const std::exception& exc) {
LOG.error() << exc.what();
assert(!"reloadResources: здесь не должно быть ошибок");
std::unreachable();
} catch(...) {
assert(!"reloadResources: здесь не должно быть ошибок");
std::unreachable();
}
}
AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const AssetsRegister& instances, ReloadStatus& status) {
Out_reloadResources result;
AssetsPreloader::Out_checkAndPrepareResourcesUpdate AssetsPreloader::_checkAndPrepareResourcesUpdate(
const AssetsRegister& instances,
const std::function<ResourceId(EnumAssets type, std::string_view domain, std::string_view key)>& idResolver,
const std::function<void(std::u8string&& resource, ResourceFile::Hash_t hash, fs::path resPath)>& onNewResourceParsed,
ReloadStatus& status
) {
// 1) Поиск всех ресурсов и построение конечной карты ресурсов (timestamps, path, name, size)
// Карта найденных ресурсов
std::array<
@@ -80,12 +110,12 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
static_cast<size_t>(AssetType::MAX_ENUM)
> resourcesFirstStage;
for (const fs::path& instance : instances.Assets) {
for(const fs::path& instance : instances.Assets) {
try {
if (fs::is_regular_file(instance)) {
if(fs::is_regular_file(instance)) {
// Может архив
/// TODO: пока не поддерживается
} else if (fs::is_directory(instance)) {
} else if(fs::is_directory(instance)) {
// Директория
fs::path assetsRoot = instance;
fs::path assetsCandidate = instance / "assets";
@@ -115,20 +145,20 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
>& firstStage = resourcesFirstStage[static_cast<size_t>(assetType)][domain];
// Исследуем все ресурсы одного типа
for (auto begin = fs::recursive_directory_iterator(assetPath), end = fs::recursive_directory_iterator(); begin != end; begin++) {
if (begin->is_directory())
for(auto begin = fs::recursive_directory_iterator(assetPath), end = fs::recursive_directory_iterator(); begin != end; begin++) {
if(begin->is_directory())
continue;
fs::path file = begin->path();
if (assetType == AssetType::Texture && file.extension() == ".meta")
if(assetType == AssetType::Texture && file.extension() == ".meta")
continue;
std::string key = fs::relative(file, assetPath).string();
if (firstStage.contains(key))
std::string key = fs::relative(file, assetPath).generic_string();
if(firstStage.contains(key))
continue;
fs::file_time_type timestamp = fs::last_write_time(file);
if (assetType == AssetType::Texture) {
if(assetType == AssetType::Texture) {
fs::path metaPath = file;
metaPath += ".meta";
if (fs::exists(metaPath) && fs::is_regular_file(metaPath)) {
@@ -141,7 +171,8 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
// Работаем с ресурсом
firstStage[key] = ResourceFindInfo{
.Path = file,
.Timestamp = timestamp
.Timestamp = timestamp,
.Id = idResolver(assetType, domain, key)
};
}
}
@@ -151,7 +182,6 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
}
} catch (const std::exception& exc) {
/// TODO: Логгировать в статусе
}
}
@@ -165,21 +195,21 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
= [&](const std::string_view model) -> uint32_t
{
auto [mDomain, mKey] = parseDomainKey(model, domain);
return getId(AssetType::Model, mDomain, mKey);
return idResolver(AssetType::Model, mDomain, mKey);
};
std::function<std::optional<uint32_t>(std::string_view)> textureIdResolver
= [&](std::string_view texture) -> std::optional<uint32_t>
{
auto [mDomain, mKey] = parseDomainKey(texture, domain);
return getId(AssetType::Texture, mDomain, mKey);
return idResolver(AssetType::Texture, mDomain, mKey);
};
std::function<std::vector<uint8_t>(const std::string_view)> textureResolver
= [&](const std::string_view texturePipelineSrc) -> std::vector<uint8_t>
{
TexturePipelineProgram tpp;
bool flag = tpp.compile((std::string) texturePipelineSrc);
bool flag = tpp.compile(texturePipelineSrc);
if(!flag)
return {};
@@ -195,8 +225,8 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
HeadlessNodeState hns;
out.Header = hns.parse(obj, modelResolver);
out.Resource = std::make_shared<std::u8string>(hns.dump());
out.Hash = sha2::sha256((const uint8_t*) out.Resource->data(), out.Resource->size());
out.Resource = hns.dump();
out.Hash = sha2::sha256((const uint8_t*) out.Resource.data(), out.Resource.size());
} else if (type == AssetType::Model) {
const std::string ext = info.Path.extension().string();
if (ext == ".json") {
@@ -206,8 +236,8 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
HeadlessModel hm;
out.Header = hm.parse(obj, modelResolver, textureResolver);
out.Resource = std::make_shared<std::u8string>(hm.dump());
out.Hash = sha2::sha256((const uint8_t*) out.Resource->data(), out.Resource->size());
out.Resource = hm.dump();
out.Hash = sha2::sha256((const uint8_t*) out.Resource.data(), out.Resource.size());
// } else if (ext == ".gltf" || ext == ".glb") {
// /// TODO: добавить поддержку gltf
// ResourceFile file = readFileBytes(info.Path);
@@ -218,203 +248,148 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
}
} else if (type == AssetType::Texture) {
ResourceFile file = readFileBytes(info.Path);
out.Resource = std::make_shared<std::u8string>(std::move(file.Data));
out.Resource = std::move(file.Data);
out.Hash = file.Hash;
out.Header = readOptionalMeta(info.Path);
} else {
ResourceFile file = readFileBytes(info.Path);
out.Resource = std::make_shared<std::u8string>(std::move(file.Data));
out.Resource = std::move(file.Data);
out.Hash = file.Hash;
}
out.Id = getId(type, domain, key);
out.Id = idResolver(type, domain, key);
return out;
};
// 2) Обрабатываться будут только изменённые (новый timestamp) или новые ресурсы
// Определяем каких ресурсов не стало
// 2) Определяем какие ресурсы изменились (новый timestamp) или новые ресурсы
Out_checkAndPrepareResourcesUpdate result;
// Собираем идентификаторы, чтобы потом определить какие ресурсы пропали
std::array<
std::unordered_set<ResourceId>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
> uniqueExists;
for(size_t type = 0; type < static_cast<size_t>(AssetType::MAX_ENUM); ++type) {
auto& tableResourcesFirstStage = resourcesFirstStage[type];
for(const auto& [id, resource] : MediaResources[type]) {
if(tableResourcesFirstStage.empty()) {
result.Lost[type][resource.Domain].push_back(resource.Key);
continue;
auto& uniqueExistsTypes = uniqueExists[type];
const auto& resourceLinksTyped = ResourceLinks[type];
result.MaxNewSize[type] = resourceLinksTyped.size();
{
size_t allIds = 0;
for(const auto& [domain, keys] : resourcesFirstStage[type])
allIds += keys.size();
uniqueExistsTypes.reserve(allIds);
}
auto iterDomain = tableResourcesFirstStage.find(resource.Domain);
if(iterDomain == tableResourcesFirstStage.end()) {
result.Lost[type][resource.Domain].push_back(resource.Key);
continue;
}
for(const auto& [domain, keys] : resourcesFirstStage[type]) {
for(const auto& [key, res] : keys) {
uniqueExistsTypes.insert(res.Id);
if(!iterDomain->second.contains(resource.Key)) {
result.Lost[type][resource.Domain].push_back(resource.Key);
}
}
}
if(res.Id >= resourceLinksTyped.size() || !resourceLinksTyped[res.Id].IsExist)
{ // Если идентификатора нет в таблице или ресурс не привязан
PendingResource resource = buildResource(static_cast<AssetType>(type), domain, key, res);
if(onNewResourceParsed)
onNewResourceParsed(std::move(resource.Resource), resource.Hash, res.Path);
result.HashToPathNew[resource.Hash].push_back(res.Path);
// Определение новых или изменённых ресурсов
for(size_t type = 0; type < static_cast<size_t>(AssetType::MAX_ENUM); ++type) {
for(const auto& [domain, table] : resourcesFirstStage[type]) {
auto iterTableDomain = DKToId[type].find(domain);
if(iterTableDomain == DKToId[type].end()) {
// Домен неизвестен движку, все ресурсы в нём новые
for(const auto& [key, info] : table) {
PendingResource resource = buildResource(static_cast<AssetType>(type), domain, key, info);
result.NewOrChange[type][domain].push_back(std::move(resource));
}
if(res.Id >= result.MaxNewSize[type])
result.MaxNewSize[type] = res.Id+1;
result.ResourceUpdates[type].emplace_back(res.Id, resource.Hash, std::move(resource.Header), resource.Timestamp, res.Path);
} else if(resourceLinksTyped[res.Id].Path != res.Path
|| resourceLinksTyped[res.Id].LastWrite != res.Timestamp
) { // Если ресурс теперь берётся с другого места или изменилось время изменения файла
const auto& lastResource = resourceLinksTyped[res.Id];
PendingResource resource = buildResource(static_cast<AssetType>(type), domain, key, res);
if(lastResource.Hash != resource.Hash) {
// Хэш изменился
// Сообщаем о новом ресурсе
if(onNewResourceParsed)
onNewResourceParsed(std::move(resource.Resource), resource.Hash, res.Path);
// Старый хэш более не доступен по этому расположению.
result.HashToPathLost[lastResource.Hash].push_back(resourceLinksTyped[res.Id].Path);
// Новый хеш стал доступен по этому расположению.
result.HashToPathNew[resource.Hash].push_back(res.Path);
} else if(resourceLinksTyped[res.Id].Path != res.Path) {
// Изменился конечный путь.
// Хэш более не доступен по этому расположению.
result.HashToPathLost[resource.Hash].push_back(resourceLinksTyped[res.Id].Path);
// Хеш теперь доступен по этому расположению.
result.HashToPathNew[resource.Hash].push_back(res.Path);
} else {
for(const auto& [key, info] : table) {
bool needsUpdate = true;
if(auto iterKey = iterTableDomain->second.find(key); iterKey != iterTableDomain->second.end()) {
// Идентификатор найден
auto iterRes = MediaResources[type].find(iterKey->second);
// Если нашли ресурс по идентификатору и время изменения не поменялось, то он не новый и не изменился
if(iterRes != MediaResources[type].end() && iterRes->second.Timestamp == info.Timestamp)
needsUpdate = false;
// Ресурс без заголовка никак не изменился.
}
if(!needsUpdate)
// Чтобы там не поменялось, мог поменятся заголовок. Уведомляем о новой привязке.
result.ResourceUpdates[type].emplace_back(res.Id, resource.Hash, std::move(resource.Header), resource.Timestamp, res.Path);
} else {
// Ресурс не изменился
}
}
}
}
// 3) Определяем какие ресурсы пропали
for(size_t type = 0; type < static_cast<size_t>(AssetType::MAX_ENUM); ++type) {
const auto& resourceLinksTyped = ResourceLinks[type];
size_t counter = 0;
for(const auto& [hash, header, timestamp, path, isExist] : resourceLinksTyped) {
size_t id = counter++;
if(!isExist)
continue;
PendingResource resource = buildResource(static_cast<AssetType>(type), domain, key, info);
result.NewOrChange[(int) type][domain].push_back(std::move(resource));
}
}
if(uniqueExists[type].contains(id))
continue;
// Ресурс потерян
// Хэш более не доступен по этому расположению.
result.HashToPathLost[hash].push_back(path);
result.LostLinks[type].push_back(id);
}
}
return result;
}
AssetsPreloader::Out_applyResourceChange AssetsPreloader::applyResourceChange(const Out_reloadResources& orr) {
Out_applyResourceChange result;
AssetsPreloader::Out_applyResourcesUpdate AssetsPreloader::applyResourcesUpdate(const Out_checkAndPrepareResourcesUpdate& orr) {
Out_applyResourcesUpdate result;
// Удаляем ресурсы
/*
Удаляются только ресурсы, при этом за ними остаётся бронь на идентификатор
Уже скомпилированные зависимости к ресурсам не будут
перекомпилироваться для смены идентификатора.
Если нужный ресурс появится, то привязка останется.
Новые клиенты не получат ресурс которого нет,
но он может использоваться
*/
for(size_t type = 0; type < static_cast<size_t>(AssetType::MAX_ENUM); type++) {
for(const auto& [domain, keys] : orr.Lost[type]) {
auto iterDomain = DKToId[type].find(domain);
for(size_t type = 0; type < static_cast<size_t>(EnumAssets::MAX_ENUM); ++type) {
// Затираем потерянные
for(ResourceId id : orr.LostLinks[type]) {
assert(id < ResourceLinks[type].size());
auto& [hash, header, timestamp, path, isExist] = ResourceLinks[type][id];
hash = {0};
header = {};
timestamp = fs::file_time_type();
path.clear();
isExist = false;
// Если уже было решено, что ресурсы были, и стали потерянными, то так и должно быть
assert(iterDomain != DKToId[type].end());
for(const auto& key : keys) {
auto iterKey = iterDomain->second.find(key);
// Ресурс был и должен быть
assert(iterKey != iterDomain->second.end());
uint32_t id = iterKey->second;
auto& resType = MediaResources[type];
auto iterRes = resType.find(id);
if(iterRes == resType.end())
continue;
// Ресурс был потерян
result.Lost[type].push_back(id);
// Hash более нам неизвестен
HashToId.erase(iterRes->second.Hash);
// Затираем ресурс
resType.erase(iterRes);
}
}
result.NewOrUpdates[type].emplace_back(id, hash, header);
}
// Добавляем
for(int type = 0; type < (int) AssetType::MAX_ENUM; type++) {
auto& typeTable = DKToId[type];
for(const auto& [domain, resources] : orr.NewOrChange[type]) {
auto& domainTable = typeTable[domain];
for(const PendingResource& pending : resources) {
MediaResource resource {
.Domain = domain,
.Key = std::move(pending.Key),
.Timestamp = pending.Timestamp,
.Resource = std::move(pending.Resource),
.Hash = pending.Hash,
.Header = std::move(pending.Header)
// Увеличиваем размер, если необходимо
if(orr.MaxNewSize[type] > ResourceLinks[type].size()) {
ResourceLink def{
ResourceFile::Hash_t{0},
ResourceHeader(),
fs::file_time_type(),
fs::path{""},
false
};
auto& table = MediaResources[type];
// Нужно затереть старую ссылку хеша на данный ресурс
if(auto iter = table.find(pending.Id); iter != table.end())
HashToId.erase(iter->second.Hash);
// Добавили ресурс
table[pending.Id] = resource;
// Связали с хешем
HashToId[resource.Hash] = {static_cast<AssetType>(type), pending.Id};
// Осведомили о новом/изменённом ресурсе
result.NewOrChange[type].emplace_back(pending.Id, resource.Hash, std::move(resource.Header));
}
ResourceLinks[type].resize(orr.MaxNewSize[type], def);
}
// Не должно быть ресурсов, которые были помечены как потерянные
#ifndef NDEBUG
std::unordered_set<uint32_t> changed;
for(const auto& [id, _, _] : result.NewOrChange[type])
changed.insert(id);
auto& lost = result.Lost[type];
for(auto iter : lost)
assert(!changed.contains(iter));
#endif
}
return result;
}
AssetsPreloader::Out_bakeId AssetsPreloader::bakeIdTables() {
#ifndef NDEBUG
assert(!DKToIdInBakingMode);
DKToIdInBakingMode = true;
struct _tempStruct {
AssetsPreloader* handler;
~_tempStruct() { handler->DKToIdInBakingMode = false; }
} _lock{this};
#endif
Out_bakeId result;
for(size_t type = 0; type < static_cast<size_t>(AssetType::MAX_ENUM); ++type) {
// домен+ключ -> id
{
auto lock = NewDKToId[type].lock();
auto& dkToId = DKToId[type];
for(auto& [domain, keys] : *lock) {
// Если домен не существует, просто воткнёт новые ключи
auto [iterDomain, inserted] = dkToId.try_emplace(domain, std::move(keys));
if(!inserted) {
// Домен уже существует, сливаем новые ключи
iterDomain->second.merge(keys);
}
}
lock->clear();
}
// id -> домен+ключ
{
auto lock = NewIdToDK[type].lock();
auto& idToDK = IdToDK[type];
result.IdToDK[type] = std::move(*lock);
lock->clear();
idToDK.append_range(result.IdToDK[type]);
// result.LastSendId[type] = LastSendId[type];
LastSendId[type] = NextId[type];
// Обновляем / добавляем
for(auto& [id, hash, header, timestamp, path] : orr.ResourceUpdates[type]) {
ResourceLinks[type][id] = {hash, std::move(header), timestamp, std::move(path), true};
result.NewOrUpdates[type].emplace_back(id, hash, header);
}
}

View File

@@ -1,6 +1,5 @@
#pragma once
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
@@ -12,19 +11,13 @@
#include <string_view>
#include <tuple>
#include <unordered_map>
#include <utility>
#include <vector>
#include "Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp"
#include "Abstract.hpp"
#include "Common/Abstract.hpp"
#include "Common/Async.hpp"
#include "TOSAsync.hpp"
#include "TOSLib.hpp"
#include "sha2.hpp"
/*
Класс отвечает за отслеживание изменений и подгрузки медиаресурсов в указанных директориях.
Медиаресурсы, собранные из папки assets или зарегистрированные модами.
Хранит все данные в оперативной памяти.
*/
static constexpr const char* EnumAssetsToDirectory(LV::EnumAssets value) {
@@ -46,75 +39,13 @@ 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;
struct ResourceFile {
using Hash_t = sha2::sha256_hash; // boost::uuids::detail::sha1::digest_type;
Hash_t Hash;
std::u8string Data;
void calcHash() {
Hash = sha2::sha256((const uint8_t*) Data.data(), Data.size());
}
};
class AssetsPreloader {
public:
using Ptr = std::shared_ptr<AssetsPreloader>;
using IdTable =
std::unordered_map<
std::string, // Domain
std::unordered_map<
std::string, // Key
uint32_t, // ResourceId
detail::TSVHash,
detail::TSVEq
>,
detail::TSVHash,
detail::TSVEq
>;
//
/*
Ресурс имеет бинарную часть, из который вырезаны все зависимости.
Вторая часть это заголовок, которые всегда динамично передаётся с сервера.
@@ -138,49 +69,13 @@ public:
std::string Key;
fs::file_time_type Timestamp;
// Обезличенный ресурс
std::shared_ptr<std::u8string> Resource;
std::u8string Resource;
// Его хеш
ResourceFile::Hash_t Hash;
// Заголовок
std::u8string Header;
};
struct BindDomainKeyInfo {
std::string Domain;
std::string Key;
};
struct BindHashHeaderInfo {
ResourceId Id;
Hash_t Hash;
std::u8string Header;
};
struct Out_reloadResources {
std::unordered_map<std::string, std::vector<PendingResource>> NewOrChange[(int) AssetType::MAX_ENUM];
std::unordered_map<std::string, std::vector<std::string>> Lost[(int) AssetType::MAX_ENUM];
};
struct Out_applyResourceChange {
std::array<
std::vector<AssetsPreloader::BindHashHeaderInfo>,
static_cast<size_t>(AssetType::MAX_ENUM)
> NewOrChange;
std::array<
std::vector<ResourceId>,
static_cast<size_t>(AssetType::MAX_ENUM)
> Lost;
};
struct Out_bakeId {
// Новые привязки
std::array<
std::vector<BindDomainKeyInfo>,
static_cast<size_t>(AssetType::MAX_ENUM)
> IdToDK;
};
struct ReloadStatus {
/// TODO: callback'и для обновления статусов
/// TODO: многоуровневый статус std::vector<std::string>. Этапы/Шаги/Объекты
@@ -226,56 +121,125 @@ public:
! Бронирует идентификаторы используя getId();
instances -> пути к директории с assets или архивы с assets внутри. От низшего приоритета к высшему.
idResolver -> функция получения идентификатора по Тип+Домен+Ключ
onNewResourceParsed -> Callback на обработку распаршенных ресурсов без заголовков
(на стороне сервера хранится в другой сущности, на стороне клиента игнорируется).
status -> обратный отклик о процессе обновления ресурсов.
ReloadStatus <- новые и потерянные ресурсы.
*/
Out_reloadResources reloadResources(const AssetsRegister& instances, ReloadStatus* status = nullptr);
struct Out_checkAndPrepareResourcesUpdate {
// Новые связки Id -> Hash + Header + Timestamp + Path (ресурс новый или изменён)
std::array<
std::vector<
std::tuple<
ResourceId, // Ресурс
ResourceFile::Hash_t, // Хэш ресурса на диске
ResourceHeader, // Хедер ресурса (со всеми зависимостями)
fs::file_time_type, // Время изменения ресурса на диске
fs::path // Путь до ресурса
>
>,
static_cast<size_t>(AssetType::MAX_ENUM)
> ResourceUpdates;
// Используется чтобы эффективно увеличить размер таблиц
std::array<
ResourceId,
static_cast<size_t>(AssetType::MAX_ENUM)
> MaxNewSize;
// Потерянные связки Id (ресурс физически потерян)
std::array<
std::vector<ResourceId>,
static_cast<size_t>(AssetType::MAX_ENUM)
> LostLinks;
/*
Новые пути предоставляющие хеш
(по каким путям можно получить ресурс определённого хеша).
*/
std::unordered_map<
ResourceFile::Hash_t,
std::vector<fs::path>
> HashToPathNew;
/*
Потерянные пути, предоставлявшые ресурсы с данным хешем
(пути по которым уже нельзя получить заданных хеш).
*/
std::unordered_map<
ResourceFile::Hash_t,
std::vector<fs::path>
> HashToPathLost;
};
Out_checkAndPrepareResourcesUpdate checkAndPrepareResourcesUpdate(
const AssetsRegister& instances,
const std::function<ResourceId(EnumAssets type, std::string_view domain, std::string_view key)>& idResolver,
const std::function<void(std::u8string&& resource, ResourceFile::Hash_t hash, fs::path resPath)>& onNewResourceParsed = nullptr,
ReloadStatus* status = nullptr
);
/*
Применяет расчитанные изменения.
Out_applyResourceChange <- Нужно отправить клиентам новые привязки ресурсов
Out_applyResourceUpdate <- Нужно отправить клиентам новые привязки ресурсов
id -> hash+header
*/
Out_applyResourceChange applyResourceChange(const Out_reloadResources& orr);
struct BindHashHeaderInfo {
ResourceId Id;
ResourceFile::Hash_t Hash;
ResourceHeader Header;
};
/*
Выдаёт идентификатор ресурса.
Многопоточно.
Иногда нужно вызывать bakeIdTables чтобы оптимизировать таблицы
идентификаторов. При этом никто не должен использовать getId
*/
ResourceId getId(AssetType type, std::string_view domain, std::string_view key);
struct Out_applyResourcesUpdate {
std::array<
std::vector<BindHashHeaderInfo>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
> NewOrUpdates;
};
/*
Оптимизирует таблицы идентификаторов.
Нельзя использовать пока есть вероятность что кто-то использует getId().
Такжке нельзя при выполнении reloadResources().
Out_applyResourcesUpdate applyResourcesUpdate(const Out_checkAndPrepareResourcesUpdate& orr);
Out_bakeId <- Нужно отправить подключенным клиентам новые привязки id -> домен+ключ
*/
Out_bakeId bakeIdTables();
std::array<
std::vector<BindHashHeaderInfo>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
> collectHashBindings() const
{
std::array<
std::vector<BindHashHeaderInfo>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
> result;
/*
Выдаёт пакет со всеми текущими привязками id -> домен+ключ.
Используется при подключении новых клиентов.
*/
void makeGlobalLinkagePacket() {
/// TODO: Собрать пакет с IdToDK и сжать его домены и ключи и id -> hash+header
for(size_t type = 0; type < static_cast<size_t>(EnumAssets::MAX_ENUM); ++type) {
result[type].reserve(ResourceLinks[type].size());
// Тот же пакет для обновления идентификаторов
std::unreachable();
ResourceId counter = 0;
for(const auto& [hash, header, _1, _2, _3] : ResourceLinks[type]) {
ResourceId id = counter++;
result[type].emplace_back(id, hash, header);
}
}
// Выдаёт ресурс по идентификатору
const MediaResource* getResource(AssetType type, uint32_t id) const;
return result;
}
// Выдаёт ресурс по хешу
std::optional<std::tuple<AssetType, uint32_t, const MediaResource*>> getResource(const ResourceFile::Hash_t& hash);
const auto& getResourceLinks() const {
return ResourceLinks;
}
// Выдаёт зависимости к ресурсам профиля ноды
std::tuple<AssetsNodestate, std::vector<AssetsModel>, std::vector<AssetsTexture>>
getNodeDependency(const std::string& domain, const std::string& key);
struct Out_Resource {
ResourceFile::Hash_t Hash;
fs::path Path;
};
std::optional<Out_Resource> getResource(EnumAssets type, ResourceId id) const {
const auto& rl = ResourceLinks[static_cast<size_t>(type)];
if(id >= rl.size() || !rl[id].IsExist)
return std::nullopt;
return Out_Resource{rl[id].Hash, rl[id].Path};
}
private:
struct ResourceFindInfo {
@@ -283,6 +247,8 @@ private:
fs::path ArchivePath, Path;
// Время изменения файла
fs::file_time_type Timestamp;
// Идентификатор ресурса
ResourceId Id;
};
struct HashHasher {
@@ -296,136 +262,32 @@ private:
}
};
#ifndef NDEBUG
// Текущее состояние reloadResources
std::atomic<bool> _Reloading = false;
// Если идентификатор не найден в асинхронной таблице, переходим к работе с синхронной
ResourceId _getIdNew(AssetType type, std::string_view domain, std::string_view key);
Out_reloadResources _reloadResources(const AssetsRegister& instances, ReloadStatus& status);
#ifndef NDEBUG
// Для контроля за режимом слияния ключей
bool DKToIdInBakingMode = false;
#endif
/*
Многопоточная таблица идентификаторов. Новые идентификаторы выделяются в NewDKToId,
и далее вливаются в основную таблицу при вызове bakeIdTables()
*/
std::array<IdTable, static_cast<size_t>(AssetType::MAX_ENUM)> DKToId;
/*
Многопоточная таблица обратного резолва.
Идентификатор -> домен+ключ
*/
std::array<std::vector<BindDomainKeyInfo>, static_cast<size_t>(AssetType::MAX_ENUM)> IdToDK;
struct ResourceLink {
ResourceFile::Hash_t Hash; // Хэш ресурса на диске
/// TODO: клиенту не нужны хедеры
ResourceHeader Header; // Хедер ресурса (со всеми зависимостями)
fs::file_time_type LastWrite; // Время изменения ресурса на диске
fs::path Path; // Путь до ресурса
bool IsExist;
};
/*
Таблица в которой выделяются новые идентификаторы, которых не нашлось в DKToId.
Данный объект одновременно может работать только с одним потоком.
*/
std::array<TOS::SpinlockObject<IdTable>, static_cast<size_t>(AssetType::MAX_ENUM)> NewDKToId;
/*
Конец поля идентификаторов, известный клиентам.
Если NextId продвинулся дальше, нужно уведомить клиентов о новых привязках.
*/
std::array<ResourceId, static_cast<size_t>(AssetType::MAX_ENUM)> LastSendId;
/*
Списки в которых пишутся новые привязки. Начала спиской исходят из LastSendId.
Id + LastSendId -> домен+ключ
*/
std::array<TOS::SpinlockObject<std::vector<BindDomainKeyInfo>>, static_cast<size_t>(AssetType::MAX_ENUM)> NewIdToDK;
Out_checkAndPrepareResourcesUpdate _checkAndPrepareResourcesUpdate(
const AssetsRegister& instances,
const std::function<ResourceId(EnumAssets type, std::string_view domain, std::string_view key)>& idResolver,
const std::function<void(std::u8string&& resource, ResourceFile::Hash_t hash, fs::path resPath)>& onNewResourceParsed,
ReloadStatus& status
);
// Загруженные ресурсы
std::array<std::unordered_map<ResourceId, MediaResource>, static_cast<size_t>(AssetType::MAX_ENUM)> MediaResources;
// Hash -> ресурс
std::unordered_map<ResourceFile::Hash_t, std::pair<AssetType, ResourceId>, HashHasher> HashToId;
// Для последовательного выделения идентификаторов
std::array<ResourceId, static_cast<size_t>(AssetType::MAX_ENUM)> NextId;
// Привязка Id -> Hash + Header + Timestamp + Path
std::array<
std::vector<ResourceLink>,
static_cast<size_t>(AssetType::MAX_ENUM)
> ResourceLinks;
};
inline ResourceId AssetsPreloader::getId(AssetType type, std::string_view domain, std::string_view key) {
#ifndef NDEBUG
assert(!DKToIdInBakingMode);
#endif
const auto& typeTable = DKToId[static_cast<size_t>(type)];
auto domainTable = typeTable.find(domain);
#ifndef NDEBUG
assert(!DKToIdInBakingMode);
#endif
if(domainTable == typeTable.end())
return _getIdNew(type, domain, key);
auto keyTable = domainTable->second.find(key);
if (keyTable == domainTable->second.end())
return _getIdNew(type, domain, key);
return keyTable->second;
return 0;
}
inline ResourceId AssetsPreloader::_getIdNew(AssetType type, std::string_view domain, std::string_view key) {
auto lock = NewDKToId[static_cast<size_t>(type)].lock();
auto iterDomainNewTable = lock->find(domain);
if(iterDomainNewTable == lock->end()) {
iterDomainNewTable = lock->emplace_hint(
iterDomainNewTable,
(std::string) domain,
std::unordered_map<std::string, uint32_t, detail::TSVHash, detail::TSVEq>{}
);
}
auto& domainNewTable = iterDomainNewTable->second;
if(auto iter = domainNewTable.find(key); iter != domainNewTable.end())
return iter->second;
uint32_t id = domainNewTable[(std::string) key] = NextId[static_cast<size_t>(type)]++;
auto lock2 = NewIdToDK[static_cast<size_t>(type)].lock();
lock.unlock();
lock2->emplace_back((std::string) domain, (std::string) key);
return id;
}
inline const AssetsPreloader::MediaResource* AssetsPreloader::getResource(AssetType type, uint32_t id) const {
auto& iterType = MediaResources[static_cast<size_t>(type)];
auto iterRes = iterType.find(id);
if(iterRes == iterType.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};
}
}

272
Src/Common/IdProvider.hpp Normal file
View File

@@ -0,0 +1,272 @@
#pragma once
#include "Common/Abstract.hpp"
#include <ankerl/unordered_dense.h>
#include <array>
#include <atomic>
#include <cassert>
#include <optional>
#include <shared_mutex>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <algorithm>
namespace LV {
template<class Enum = EnumAssets, size_t ShardCount = 64>
class IdProvider {
public:
static constexpr size_t MAX_ENUM = static_cast<size_t>(Enum::MAX_ENUM);
struct BindDomainKeyInfo {
std::string Domain, Key;
};
struct BindDomainKeyViewInfo {
std::string_view Domain, Key;
};
struct KeyHash {
using is_transparent = void;
static inline std::size_t h(std::string_view sv) noexcept {
return std::hash<std::string_view>{}(sv);
}
static inline std::size_t mix(std::size_t a, std::size_t b) noexcept {
a ^= b + 0x9e3779b97f4a7c15ULL + (a << 6) + (a >> 2);
return a;
}
std::size_t operator()(const BindDomainKeyInfo& k) const noexcept {
return mix(h(k.Domain), h(k.Key));
}
std::size_t operator()(const BindDomainKeyViewInfo& kv) const noexcept {
return mix(h(kv.Domain), h(kv.Key));
}
};
struct KeyEq {
using is_transparent = void;
bool operator()(const BindDomainKeyInfo& a, const BindDomainKeyInfo& b) const noexcept {
return a.Domain == b.Domain && a.Key == b.Key;
}
bool operator()(const BindDomainKeyInfo& a, const BindDomainKeyViewInfo& b) const noexcept {
return a.Domain == b.Domain && a.Key == b.Key;
}
bool operator()(const BindDomainKeyViewInfo& a, const BindDomainKeyInfo& b) const noexcept {
return a.Domain == b.Domain && a.Key == b.Key;
}
};
public:
explicit IdProvider() {
for(size_t type = 0; type < MAX_ENUM; ++type) {
_NextId[type].store(1, std::memory_order_relaxed);
_Reverse[type].reserve(1024);
IdToDK[type].push_back({"core", "none"});
auto& sh = _shardFor(static_cast<Enum>(type), "core", "none");
std::unique_lock lk(sh.mutex);
sh.map.emplace(BindDomainKeyInfo{"core", "none"}, 0);
// ensure id 0 has a reverse mapping too
_storeReverse(static_cast<Enum>(type), 0, std::string("core"), std::string("none"));
}
}
/*
Находит или выдаёт идентификатор на запрошенный ресурс.
Функция не требует внешней синхронизации.
*/
inline ResourceId getId(Enum type, std::string_view domain, std::string_view key) {
#ifndef NDEBUG
assert(!DKToIdInBakingMode);
#endif
auto& sh = _shardFor(type, domain, key);
// 1) Поиск в режиме для чтения
{
std::shared_lock lk(sh.mutex);
if(auto it = sh.map.find(BindDomainKeyViewInfo{domain, key}); it != sh.map.end()) {
return it->second;
}
}
// 2) Блокируем и повторно ищем запись (может кто уже успел её добавить)
std::unique_lock lk(sh.mutex);
if (auto it = sh.map.find(BindDomainKeyViewInfo{domain, key}); it != sh.map.end()) {
return it->second;
}
// Выделяем идентификатор
ResourceId id = _NextId[static_cast<size_t>(type)].fetch_add(1, std::memory_order_relaxed);
std::string d(domain);
std::string k(key);
sh.map.emplace(BindDomainKeyInfo{d, k}, id);
sh.newlyInserted.push_back(id);
_storeReverse(type, id, std::move(d), std::move(k));
return id;
}
/*
Переносит все новые идентификаторы в основную таблицу.
В этой реализации "основная таблица" уже основная (forward map обновляется сразу),
а bake() собирает только новые привязки (domain,key) по логам вставок и дополняет IdToDK.
Нельзя использовать пока есть вероятность что кто-то использует getId(), если ты хочешь
строгий debug-контроль как раньше. В релизе это не требуется: bake читает только reverse,
а forward не трогает.
*/
std::array<std::vector<BindDomainKeyInfo>, MAX_ENUM> bake() {
#ifndef NDEBUG
assert(!DKToIdInBakingMode);
DKToIdInBakingMode = true;
struct _tempStruct {
IdProvider* handler;
~_tempStruct() { handler->DKToIdInBakingMode = false; }
} _lock{this};
#endif
std::array<std::vector<BindDomainKeyInfo>, MAX_ENUM> result;
for(size_t t = 0; t < MAX_ENUM; ++t) {
auto type = static_cast<Enum>(t);
// 1) собрать новые id из всех шардов
std::vector<ResourceId> new_ids;
_drainNew(type, new_ids);
if(new_ids.empty())
continue;
// 2) превратить id -> (domain,key) через reverse и вернуть наружу
// + дописать в IdToDK[type] в порядке id (по желанию)
std::sort(new_ids.begin(), new_ids.end());
new_ids.erase(std::unique(new_ids.begin(), new_ids.end()), new_ids.end());
result[t].reserve(new_ids.size());
// reverse читаем под shared lock
std::shared_lock rlk(_ReverseMutex[t]);
for(ResourceId id : new_ids) {
const std::size_t idx = static_cast<std::size_t>(id);
if(idx >= _Reverse[t].size()) {
// теоретически не должно случаться (мы пишем reverse до push в log)
continue;
}
const auto& e = _Reverse[t][idx];
result[t].push_back({e.Domain, e.Key});
}
rlk.unlock();
// 3) дописать в IdToDK (для новых клиентов)
IdToDK[t].append_range(result[t]);
}
return result;
}
// id to DK
std::optional<BindDomainKeyInfo> getDK(Enum type, ResourceId id) {
auto& vec = _Reverse[static_cast<size_t>(type)];
auto& mtx = _ReverseMutex[static_cast<size_t>(type)];
std::unique_lock lk(mtx);
if(id >= vec.size())
return std::nullopt;
return vec[id];
}
// Для отправки новым подключенным клиентам
const std::array<std::vector<BindDomainKeyInfo>, MAX_ENUM>& idToDK() const {
return IdToDK;
}
private:
using Map = ankerl::unordered_dense::map<BindDomainKeyInfo, ResourceId, KeyHash, KeyEq>;
struct Shard {
mutable std::shared_mutex mutex;
Map map;
std::vector<ResourceId> newlyInserted;
};
private:
// Кластер таблиц идентификаторов
std::array<
std::array<Shard, ShardCount>, MAX_ENUM
> _Shards;
// Счётчики идентификаторов
std::array<std::atomic<ResourceId>, MAX_ENUM> _NextId;
// Таблица обратных связок (Id to DK)
std::array<std::vector<BindDomainKeyInfo>, MAX_ENUM> _Reverse;
mutable std::array<std::shared_mutex, MAX_ENUM> _ReverseMutex;
#ifndef NDEBUG
bool DKToIdInBakingMode = false;
#endif
// stable "full sync" table for new clients:
std::array<std::vector<BindDomainKeyInfo>, MAX_ENUM> IdToDK;
private:
Shard& _shardFor(Enum type, const std::string_view domain, const std::string_view key) {
const std::size_t idx = KeyHash{}(BindDomainKeyViewInfo{domain, key}) % ShardCount;
return _Shards[static_cast<size_t>(type)][idx];
}
const Shard& _shardFor(Enum type, const std::string_view domain, const std::string_view key) const {
const std::size_t idx = KeyHash{}(BindDomainKeyViewInfo{domain, key}) % ShardCount;
return _Shards[static_cast<size_t>(type)][idx];
}
void _storeReverse(Enum type, ResourceId id, std::string&& domain, std::string&& key) {
auto& vec = _Reverse[static_cast<size_t>(type)];
auto& mtx = _ReverseMutex[static_cast<size_t>(type)];
const std::size_t idx = static_cast<std::size_t>(id);
std::unique_lock lk(mtx);
if(idx >= vec.size())
vec.resize(idx + 1);
vec[idx] = BindDomainKeyInfo{std::move(domain), std::move(key)};
}
void _drainNew(Enum type, std::vector<ResourceId>& out) {
out.clear();
auto& shards = _Shards[static_cast<size_t>(type)];
// Можно добавить reserve по эвристике
for (auto& sh : shards) {
std::unique_lock lk(sh.mutex);
if (sh.newlyInserted.empty()) continue;
const auto old = out.size();
out.resize(old + sh.newlyInserted.size());
std::copy(sh.newlyInserted.begin(), sh.newlyInserted.end(), out.begin() + old);
sh.newlyInserted.clear();
}
}
};
} // namespace LV

483
Src/Common/Net2.cpp Normal file
View File

@@ -0,0 +1,483 @@
#include "Net2.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/read.hpp>
#include <boost/asio/write.hpp>
#include <boost/system/system_error.hpp>
#include <algorithm>
#include <tuple>
namespace LV::Net2 {
using namespace TOS;
namespace {
struct HeaderFields {
uint32_t size = 0;
uint16_t type = 0;
Priority priority = Priority::Normal;
FrameFlags flags = FrameFlags::None;
uint32_t streamId = 0;
};
std::array<std::byte, AsyncSocket::kHeaderSize> encodeHeader(const HeaderFields &h) {
std::array<std::byte, AsyncSocket::kHeaderSize> out{};
uint32_t sizeNet = detail::toNetwork(h.size);
uint16_t typeNet = detail::toNetwork(h.type);
uint32_t streamNet = detail::toNetwork(h.streamId);
std::memcpy(out.data(), &sizeNet, sizeof(sizeNet));
std::memcpy(out.data() + 4, &typeNet, sizeof(typeNet));
out[6] = std::byte(static_cast<uint8_t>(h.priority));
out[7] = std::byte(static_cast<uint8_t>(h.flags));
std::memcpy(out.data() + 8, &streamNet, sizeof(streamNet));
return out;
}
HeaderFields decodeHeader(const std::array<std::byte, AsyncSocket::kHeaderSize> &in) {
HeaderFields h{};
std::memcpy(&h.size, in.data(), sizeof(h.size));
std::memcpy(&h.type, in.data() + 4, sizeof(h.type));
h.priority = static_cast<Priority>(std::to_integer<uint8_t>(in[6]));
h.flags = static_cast<FrameFlags>(std::to_integer<uint8_t>(in[7]));
std::memcpy(&h.streamId, in.data() + 8, sizeof(h.streamId));
h.size = detail::fromNetwork(h.size);
h.type = detail::fromNetwork(h.type);
h.streamId = detail::fromNetwork(h.streamId);
return h;
}
} // namespace
PacketWriter& PacketWriter::writeBytes(std::span<const std::byte> data) {
Buffer.insert(Buffer.end(), data.begin(), data.end());
return *this;
}
PacketWriter& PacketWriter::writeString(std::string_view str) {
write<uint32_t>(static_cast<uint32_t>(str.size()));
auto bytes = std::as_bytes(std::span<const char>(str.data(), str.size()));
Buffer.insert(Buffer.end(), bytes.begin(), bytes.end());
return *this;
}
std::vector<std::byte> PacketWriter::release() {
std::vector<std::byte> out = std::move(Buffer);
Buffer.clear();
return out;
}
void PacketWriter::clear() {
Buffer.clear();
}
PacketReader::PacketReader(std::span<const std::byte> data)
: Data(data)
{
}
void PacketReader::readBytes(std::span<std::byte> out) {
require(out.size());
std::memcpy(out.data(), Data.data() + Pos, out.size());
Pos += out.size();
}
std::string PacketReader::readString() {
uint32_t size = read<uint32_t>();
require(size);
std::string out(size, '\0');
std::memcpy(out.data(), Data.data() + Pos, size);
Pos += size;
return out;
}
void PacketReader::require(size_t size) {
if(Data.size() - Pos < size)
MAKE_ERROR("Net2::PacketReader: not enough data");
}
SocketServer::SocketServer(asio::io_context &ioc, std::function<coro<>(tcp::socket)> &&onConnect, uint16_t port)
: AsyncObject(ioc), Acceptor(ioc, tcp::endpoint(tcp::v4(), port))
{
assert(onConnect);
co_spawn(run(std::move(onConnect)));
}
bool SocketServer::isStopped() const {
return !Acceptor.is_open();
}
uint16_t SocketServer::getPort() const {
return Acceptor.local_endpoint().port();
}
coro<void> SocketServer::run(std::function<coro<>(tcp::socket)> onConnect) {
while(true) {
try {
co_spawn(onConnect(co_await Acceptor.async_accept()));
} catch(const std::exception &exc) {
if(const boost::system::system_error *errc = dynamic_cast<const boost::system::system_error*>(&exc);
errc && (errc->code() == asio::error::operation_aborted || errc->code() == asio::error::bad_descriptor))
break;
}
}
}
AsyncSocket::SendQueue::SendQueue(asio::io_context &ioc)
: semaphore(ioc)
{
semaphore.expires_at(std::chrono::steady_clock::time_point::max());
}
bool AsyncSocket::SendQueue::empty() const {
for(const auto &queue : queues) {
if(!queue.empty())
return false;
}
return true;
}
AsyncSocket::AsyncSocket(asio::io_context &ioc, tcp::socket &&socket, Limits limits)
: AsyncObject(ioc), LimitsCfg(limits), Socket(std::move(socket)), Outgoing(ioc)
{
Context = std::make_shared<AsyncContext>();
boost::asio::socket_base::linger optionLinger(true, 4);
Socket.set_option(optionLinger);
boost::asio::ip::tcp::no_delay optionNoDelay(true);
Socket.set_option(optionNoDelay);
co_spawn(sendLoop());
}
AsyncSocket::~AsyncSocket() {
if(Context)
Context->needShutdown.store(true);
{
boost::lock_guard lock(Outgoing.mtx);
Outgoing.semaphore.cancel();
WorkDeadline.cancel();
}
if(Socket.is_open())
try { Socket.close(); } catch(...) {}
}
void AsyncSocket::enqueue(OutgoingMessage &&msg) {
if(msg.payload.size() > LimitsCfg.maxMessageSize) {
setError("Net2::AsyncSocket: message too large");
close();
return;
}
boost::unique_lock lock(Outgoing.mtx);
const size_t msgSize = msg.payload.size();
const size_t lowIndex = static_cast<size_t>(Priority::Low);
if(msg.priority == Priority::Low) {
while(Outgoing.bytesInLow + msgSize > LimitsCfg.maxLowPriorityBytes && !Outgoing.queues[lowIndex].empty()) {
Outgoing.bytesInQueue -= Outgoing.queues[lowIndex].front().payload.size();
Outgoing.bytesInLow -= Outgoing.queues[lowIndex].front().payload.size();
Outgoing.queues[lowIndex].pop_front();
}
if(Outgoing.bytesInLow + msgSize > LimitsCfg.maxLowPriorityBytes) {
return;
}
}
if(Outgoing.bytesInQueue + msgSize > LimitsCfg.maxQueueBytes) {
dropLow(msgSize);
if(Outgoing.bytesInQueue + msgSize > LimitsCfg.maxQueueBytes) {
if(msg.dropIfOverloaded)
return;
setError("Net2::AsyncSocket: send queue overflow");
close();
return;
}
}
const size_t idx = static_cast<size_t>(msg.priority);
Outgoing.bytesInQueue += msgSize;
if(msg.priority == Priority::Low)
Outgoing.bytesInLow += msgSize;
Outgoing.queues[idx].push_back(std::move(msg));
if(Outgoing.waiting) {
Outgoing.waiting = false;
Outgoing.semaphore.cancel();
Outgoing.semaphore.expires_at(std::chrono::steady_clock::time_point::max());
}
}
coro<IncomingMessage> AsyncSocket::readMessage() {
while(true) {
std::array<std::byte, kHeaderSize> headerBytes{};
co_await readExact(headerBytes.data(), headerBytes.size());
HeaderFields header = decodeHeader(headerBytes);
if(header.size > LimitsCfg.maxFrameSize)
MAKE_ERROR("Net2::AsyncSocket: frame too large");
std::vector<std::byte> chunk(header.size);
if(header.size)
co_await readExact(chunk.data(), chunk.size());
if(header.streamId != 0) {
if(Fragments.size() >= LimitsCfg.maxOpenStreams && !Fragments.contains(header.streamId))
MAKE_ERROR("Net2::AsyncSocket: too many open streams");
FragmentState &state = Fragments[header.streamId];
if(state.data.empty()) {
state.type = header.type;
state.priority = header.priority;
}
if(state.data.size() + chunk.size() > LimitsCfg.maxMessageSize)
MAKE_ERROR("Net2::AsyncSocket: reassembled message too large");
state.data.insert(state.data.end(), chunk.begin(), chunk.end());
if(!hasFlag(header.flags, FrameFlags::HasMore)) {
IncomingMessage msg{state.type, state.priority, std::move(state.data)};
Fragments.erase(header.streamId);
co_return msg;
}
continue;
}
if(hasFlag(header.flags, FrameFlags::HasMore))
MAKE_ERROR("Net2::AsyncSocket: stream id missing for fragmented frame");
IncomingMessage msg{header.type, header.priority, std::move(chunk)};
co_return msg;
}
}
coro<> AsyncSocket::readLoop(std::function<coro<>(IncomingMessage&&)> onMessage) {
while(isAlive()) {
IncomingMessage msg = co_await readMessage();
co_await onMessage(std::move(msg));
}
}
void AsyncSocket::closeRead() {
if(Socket.is_open() && !Context->readClosed.exchange(true)) {
try { Socket.shutdown(boost::asio::socket_base::shutdown_receive); } catch(...) {}
}
}
void AsyncSocket::close() {
if(Context)
Context->needShutdown.store(true);
if(Socket.is_open())
try { Socket.close(); } catch(...) {}
}
bool AsyncSocket::isAlive() const {
return Context && !Context->needShutdown.load() && !Context->senderStopped.load() && Socket.is_open();
}
std::string AsyncSocket::getError() const {
boost::lock_guard lock(Context->errorMtx);
return Context->error;
}
coro<> AsyncSocket::sendLoop() {
try {
while(!Context->needShutdown.load()) {
OutgoingMessage msg;
{
boost::unique_lock lock(Outgoing.mtx);
if(Outgoing.empty()) {
Outgoing.waiting = true;
auto coroutine = Outgoing.semaphore.async_wait();
lock.unlock();
try { co_await std::move(coroutine); } catch(...) {}
continue;
}
if(!popNext(msg))
continue;
}
co_await sendMessage(std::move(msg));
}
} catch(const std::exception &exc) {
setError(exc.what());
} catch(...) {
setError("Net2::AsyncSocket: send loop stopped");
}
Context->senderStopped.store(true);
}
coro<> AsyncSocket::sendMessage(OutgoingMessage &&msg) {
const size_t total = msg.payload.size();
if(total <= LimitsCfg.maxFrameSize) {
co_await sendFrame(msg.type, msg.priority, FrameFlags::None, 0, msg.payload);
co_return;
}
if(!msg.allowFragment) {
setError("Net2::AsyncSocket: message requires fragmentation");
close();
co_return;
}
uint32_t streamId = NextStreamId++;
if(streamId == 0)
streamId = NextStreamId++;
size_t offset = 0;
while(offset < total) {
const size_t chunk = std::min(LimitsCfg.maxFrameSize, total - offset);
const bool more = (offset + chunk) < total;
FrameFlags flags = more ? FrameFlags::HasMore : FrameFlags::None;
std::span<const std::byte> view(msg.payload.data() + offset, chunk);
co_await sendFrame(msg.type, msg.priority, flags, streamId, view);
offset += chunk;
}
}
coro<> AsyncSocket::sendFrame(uint16_t type, Priority priority, FrameFlags flags, uint32_t streamId,
std::span<const std::byte> payload) {
HeaderFields header{
.size = static_cast<uint32_t>(payload.size()),
.type = type,
.priority = priority,
.flags = flags,
.streamId = streamId
};
auto headerBytes = encodeHeader(header);
std::array<asio::const_buffer, 2> buffers{
asio::buffer(headerBytes),
asio::buffer(payload.data(), payload.size())
};
if(payload.empty())
co_await asio::async_write(Socket, asio::buffer(headerBytes));
else
co_await asio::async_write(Socket, buffers);
}
coro<> AsyncSocket::readExact(std::byte *data, size_t size) {
if(size == 0)
co_return;
co_await asio::async_read(Socket, asio::buffer(data, size));
}
bool AsyncSocket::popNext(OutgoingMessage &out) {
static constexpr int kWeights[4] = {8, 4, 2, 1};
for(int attempt = 0; attempt < 4; ++attempt) {
const uint8_t idx = static_cast<uint8_t>((Outgoing.nextIndex + attempt) % 4);
auto &queue = Outgoing.queues[idx];
if(queue.empty())
continue;
if(Outgoing.credits[idx] <= 0)
Outgoing.credits[idx] = kWeights[idx];
if(Outgoing.credits[idx] <= 0)
continue;
out = std::move(queue.front());
queue.pop_front();
Outgoing.credits[idx]--;
Outgoing.nextIndex = idx;
const size_t msgSize = out.payload.size();
Outgoing.bytesInQueue -= msgSize;
if(idx == static_cast<uint8_t>(Priority::Low))
Outgoing.bytesInLow -= msgSize;
return true;
}
for(int i = 0; i < 4; ++i)
Outgoing.credits[i] = kWeights[i];
return false;
}
void AsyncSocket::dropLow(size_t needBytes) {
const size_t lowIndex = static_cast<size_t>(Priority::Low);
while(Outgoing.bytesInQueue + needBytes > LimitsCfg.maxQueueBytes && !Outgoing.queues[lowIndex].empty()) {
const size_t size = Outgoing.queues[lowIndex].front().payload.size();
Outgoing.bytesInQueue -= size;
Outgoing.bytesInLow -= size;
Outgoing.queues[lowIndex].pop_front();
}
}
void AsyncSocket::setError(const std::string &msg) {
if(!Context)
return;
boost::lock_guard lock(Context->errorMtx);
Context->error = msg;
}
coro<tcp::socket> asyncConnectTo(const std::string &address,
std::function<void(const std::string&)> onProgress) {
std::string progress;
auto addLog = [&](const std::string &msg) {
progress += '\n';
progress += msg;
if(onProgress)
onProgress('\n' + msg);
};
auto ioc = co_await asio::this_coro::executor;
addLog("Parsing address " + address);
auto re = Str::match(address, "((?:\\[[\\d\\w:]+\\])|(?:[\\d\\.]+))(?:\\:(\\d+))?");
std::vector<std::tuple<tcp::endpoint, std::string>> eps;
if(!re) {
re = Str::match(address, "([-_\\.\\w\\d]+)(?:\\:(\\d+))?");
if(!re)
MAKE_ERROR("Failed to parse address");
tcp::resolver resv{ioc};
tcp::resolver::results_type result;
addLog("Resolving name...");
result = co_await resv.async_resolve(*re->at(1), re->at(2) ? *re->at(2) : "7890");
addLog("Got " + std::to_string(result.size()) + " endpoints");
for(auto iter : result) {
std::string addr = iter.endpoint().address().to_string() + ':' + std::to_string(iter.endpoint().port());
std::string hostname = iter.host_name();
if(hostname == addr)
addLog("ep: " + addr);
else
addLog("ep: " + hostname + " (" + addr + ')');
eps.emplace_back(iter.endpoint(), iter.host_name());
}
} else {
eps.emplace_back(tcp::endpoint{asio::ip::make_address(*re->at(1)),
static_cast<uint16_t>(re->at(2) ? Str::toVal<int>(*re->at(2)) : 7890)},
*re->at(1));
}
for(auto [ep, hostname] : eps) {
addLog("Connecting to " + hostname + " (" + ep.address().to_string() + ':'
+ std::to_string(ep.port()) + ")");
try {
tcp::socket sock{ioc};
co_await sock.async_connect(ep);
addLog("Connected");
co_return sock;
} catch(const std::exception &exc) {
addLog(std::string("Connect failed: ") + exc.what());
}
}
MAKE_ERROR("Unable to connect to server");
}
} // namespace LV::Net2

227
Src/Common/Net2.hpp Normal file
View File

@@ -0,0 +1,227 @@
#pragma once
#include "Async.hpp"
#include "TOSLib.hpp"
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <array>
#include <bit>
#include <atomic>
#include <cassert>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <deque>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <type_traits>
#include <unordered_map>
#include <vector>
namespace LV::Net2 {
namespace detail {
constexpr bool kLittleEndian = (std::endian::native == std::endian::little);
template<typename T>
requires std::is_integral_v<T>
inline T toNetwork(T value) {
if constexpr (kLittleEndian && sizeof(T) > 1)
return std::byteswap(value);
return value;
}
template<typename T>
requires std::is_floating_point_v<T>
inline T toNetwork(T value) {
using U = std::conditional_t<sizeof(T) == 4, uint32_t, uint64_t>;
U u = std::bit_cast<U>(value);
u = toNetwork(u);
return std::bit_cast<T>(u);
}
template<typename T>
inline T fromNetwork(T value) {
return toNetwork(value);
}
} // namespace detail
enum class Priority : uint8_t {
Realtime = 0,
High = 1,
Normal = 2,
Low = 3
};
enum class FrameFlags : uint8_t {
None = 0,
HasMore = 1
};
inline FrameFlags operator|(FrameFlags a, FrameFlags b) {
return static_cast<FrameFlags>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
}
inline bool hasFlag(FrameFlags value, FrameFlags flag) {
return (static_cast<uint8_t>(value) & static_cast<uint8_t>(flag)) != 0;
}
struct Limits {
size_t maxFrameSize = 1 << 24;
size_t maxMessageSize = 1 << 26;
size_t maxQueueBytes = 1 << 27;
size_t maxLowPriorityBytes = 1 << 26;
size_t maxOpenStreams = 64;
};
struct OutgoingMessage {
uint16_t type = 0;
Priority priority = Priority::Normal;
bool dropIfOverloaded = false;
bool allowFragment = true;
std::vector<std::byte> payload;
};
struct IncomingMessage {
uint16_t type = 0;
Priority priority = Priority::Normal;
std::vector<std::byte> payload;
};
class PacketWriter {
public:
PacketWriter& writeBytes(std::span<const std::byte> data);
template<typename T>
requires (std::is_integral_v<T> || std::is_floating_point_v<T>)
PacketWriter& write(T value) {
T net = detail::toNetwork(value);
std::array<std::byte, sizeof(T)> bytes{};
std::memcpy(bytes.data(), &net, sizeof(T));
Buffer.insert(Buffer.end(), bytes.begin(), bytes.end());
return *this;
}
PacketWriter& writeString(std::string_view str);
const std::vector<std::byte>& data() const { return Buffer; }
std::vector<std::byte> release();
void clear();
private:
std::vector<std::byte> Buffer;
};
class PacketReader {
public:
explicit PacketReader(std::span<const std::byte> data);
template<typename T>
requires (std::is_integral_v<T> || std::is_floating_point_v<T>)
T read() {
require(sizeof(T));
T net{};
std::memcpy(&net, Data.data() + Pos, sizeof(T));
Pos += sizeof(T);
return detail::fromNetwork(net);
}
void readBytes(std::span<std::byte> out);
std::string readString();
bool empty() const { return Pos >= Data.size(); }
size_t remaining() const { return Data.size() - Pos; }
private:
void require(size_t size);
size_t Pos = 0;
std::span<const std::byte> Data;
};
class SocketServer : public AsyncObject {
public:
SocketServer(asio::io_context &ioc, std::function<coro<>(tcp::socket)> &&onConnect, uint16_t port = 0);
bool isStopped() const;
uint16_t getPort() const;
private:
coro<void> run(std::function<coro<>(tcp::socket)> onConnect);
tcp::acceptor Acceptor;
};
class AsyncSocket : public AsyncObject {
public:
static constexpr size_t kHeaderSize = 12;
AsyncSocket(asio::io_context &ioc, tcp::socket &&socket, Limits limits = {});
~AsyncSocket();
void enqueue(OutgoingMessage &&msg);
coro<IncomingMessage> readMessage();
coro<> readLoop(std::function<coro<>(IncomingMessage&&)> onMessage);
void closeRead();
void close();
bool isAlive() const;
std::string getError() const;
private:
struct FragmentState {
uint16_t type = 0;
Priority priority = Priority::Normal;
std::vector<std::byte> data;
};
struct AsyncContext {
std::atomic_bool needShutdown{false};
std::atomic_bool senderStopped{false};
std::atomic_bool readClosed{false};
boost::mutex errorMtx;
std::string error;
};
struct SendQueue {
boost::mutex mtx;
bool waiting = false;
asio::steady_timer semaphore;
std::deque<OutgoingMessage> queues[4];
size_t bytesInQueue = 0;
size_t bytesInLow = 0;
uint8_t nextIndex = 0;
int credits[4] = {8, 4, 2, 1};
explicit SendQueue(asio::io_context &ioc);
bool empty() const;
};
coro<> sendLoop();
coro<> sendMessage(OutgoingMessage &&msg);
coro<> sendFrame(uint16_t type, Priority priority, FrameFlags flags, uint32_t streamId,
std::span<const std::byte> payload);
coro<> readExact(std::byte *data, size_t size);
bool popNext(OutgoingMessage &out);
void dropLow(size_t needBytes);
void setError(const std::string &msg);
Limits LimitsCfg;
tcp::socket Socket;
SendQueue Outgoing;
std::shared_ptr<AsyncContext> Context;
std::unordered_map<uint32_t, FragmentState> Fragments;
uint32_t NextStreamId = 1;
};
coro<tcp::socket> asyncConnectTo(const std::string &address,
std::function<void(const std::string&)> onProgress = nullptr);
} // namespace LV::Net2

View File

@@ -92,6 +92,7 @@ enum struct ToClient : uint8_t {
AssetsInitSend, // Начало отправки запрошенного клиентом ресурса
AssetsNextSend, // Продолжение отправки ресурса
DefinitionsFull, // Полная информация о профилях контента
DefinitionsUpdate, // Обновление и потеря профилей контента (воксели, ноды, сущности, миры, ...)
ChunkVoxels, // Обновление вокселей чанка

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,406 @@
#pragma once
#include <cstdint>
#include <vector>
#include <string>
#include <string_view>
#include <optional>
#include <functional>
#include <unordered_map>
#include <algorithm>
#include <cmath>
#include <cstring>
// ========================
// External texture view
// ========================
struct Texture {
uint32_t Width, Height;
const uint32_t* Pixels; // assumed 0xAARRGGBB
};
// ========================
// Bytecode words are uint8_t (1 byte machine word)
// TexId is u24 (3 bytes, little-endian)
// Subprogram refs use off24/len24 in BYTES (<=65535)
// ========================
class TexturePipelineProgram {
public:
using Word = uint8_t;
enum AnimFlags : Word {
AnimSmooth = 1u << 0,
AnimHorizontal = 1u << 1,
AnimGrid = 1u << 2
};
static constexpr uint16_t DefaultAnimFpsQ = uint16_t(8u * 256u);
static constexpr size_t MaxCodeBytes = (1u << 16) + 1u; // 65537
struct OwnedTexture {
uint32_t Width = 0, Height = 0;
std::vector<uint32_t> Pixels;
Texture view() const { return Texture{Width, Height, Pixels.data()}; }
};
using IdResolverFunc = std::function<std::optional<uint32_t>(std::string_view)>;
using TextureProviderFunc = std::function<std::optional<Texture>(uint32_t)>;
// Patch point to 3 consecutive bytes where u24 texId lives (b0,b1,b2)
struct Patch {
size_t ByteIndex0 = 0; // Code_[i], Code_[i+1], Code_[i+2]
std::string Name;
};
bool compile(std::string_view src, std::string* err = nullptr);
bool link(const IdResolverFunc& resolver, std::string* err = nullptr);
bool bake(const TextureProviderFunc& provider, OwnedTexture& out, std::string* err = nullptr) const;
bool bake(const TextureProviderFunc& provider, OwnedTexture& out, double timeSeconds, std::string* err = nullptr) const;
const std::vector<Word>& words() const { return Code_; }
const std::vector<Patch>& patches() const { return Patches_; }
std::vector<uint8_t> toBytes() const { return Code_; }
struct AnimSpec {
uint32_t TexId = 0;
bool HasTexId = false;
uint16_t FrameW = 0;
uint16_t FrameH = 0;
uint16_t FrameCount = 0;
uint16_t FpsQ = 0;
uint16_t Flags = 0;
};
static std::vector<AnimSpec> extractAnimationSpecs(const Word* code, size_t size);
static bool remapTexIds(std::vector<uint8_t>& code, const std::vector<uint32_t>& remap, std::string* err = nullptr);
static std::vector<AnimSpec> extractAnimationSpecs(const std::vector<Word>& code) {
return extractAnimationSpecs(code.data(), code.size());
}
void fromBytes(std::vector<uint8_t> bytes);
private:
// ========================
// Byte helpers (little-endian)
// ========================
static inline uint16_t _rd16(const std::vector<uint8_t>& c, size_t& ip) {
uint16_t v = uint16_t(c[ip]) | (uint16_t(c[ip+1]) << 8);
ip += 2;
return v;
}
static inline uint32_t _rd24(const std::vector<uint8_t>& c, size_t& ip) {
uint32_t v = uint32_t(c[ip]) | (uint32_t(c[ip+1]) << 8) | (uint32_t(c[ip+2]) << 16);
ip += 3;
return v;
}
static inline uint32_t _rd32(const std::vector<uint8_t>& c, size_t& ip) {
uint32_t v = uint32_t(c[ip]) |
(uint32_t(c[ip+1]) << 8) |
(uint32_t(c[ip+2]) << 16) |
(uint32_t(c[ip+3]) << 24);
ip += 4;
return v;
}
static inline void _wr8 (std::vector<uint8_t>& o, uint32_t v){ o.push_back(uint8_t(v & 0xFFu)); }
static inline void _wr16(std::vector<uint8_t>& o, uint32_t v){
o.push_back(uint8_t(v & 0xFFu));
o.push_back(uint8_t((v >> 8) & 0xFFu));
}
static inline void _wr24(std::vector<uint8_t>& o, uint32_t v){
o.push_back(uint8_t(v & 0xFFu));
o.push_back(uint8_t((v >> 8) & 0xFFu));
o.push_back(uint8_t((v >> 16) & 0xFFu));
}
static inline void _wr32(std::vector<uint8_t>& o, uint32_t v){
o.push_back(uint8_t(v & 0xFFu));
o.push_back(uint8_t((v >> 8) & 0xFFu));
o.push_back(uint8_t((v >> 16) & 0xFFu));
o.push_back(uint8_t((v >> 24) & 0xFFu));
}
// ========================
// SrcRef encoding in bytes (variable length)
// kind(1) + payload
// TexId: id24(3) => total 4
// Sub : off16(3) + len16(3) => total 7
// ========================
enum class SrcKind : uint8_t { TexId = 0, Sub = 1 };
struct SrcRef {
SrcKind Kind{};
uint32_t TexId24 = 0; // for TexId
uint16_t Off24 = 0; // for Sub
uint16_t Len24 = 0; // for Sub
};
// ========================
// Opcodes (1 byte)
// ========================
enum class Op : uint8_t {
End = 0,
Base_Tex = 1, // SrcRef(TexId)
Base_Fill = 2, // w16, h16, color32
Base_Anim = 3, // SrcRef(TexId), frameW16, frameH16, frames16, fpsQ16, flags8
Resize = 10, // w16, h16
Transform = 11, // t8
Opacity = 12, // a8
NoAlpha = 13, // -
MakeAlpha = 14, // rgb24 (3 bytes) RR,GG,BB
Invert = 15, // mask8
Brighten = 16, // -
Contrast = 17, // cBias8, bBias8 (bias-127)
Multiply = 18, // color32
Screen = 19, // color32
Colorize = 20, // color32, ratio8
Anim = 21, // frameW16, frameH16, frames16, fpsQ16, flags8
Overlay = 30, // SrcRef (var)
Mask = 31, // SrcRef (var)
LowPart = 32, // percent8, SrcRef (var)
Combine = 40 // w16,h16,n16 then n*(x16,y16,SrcRef) (если понадобится — допишем DSL)
};
// ========================
// Pixel helpers (assume 0xAARRGGBB)
// ========================
static inline uint8_t _a(uint32_t c){ return uint8_t((c >> 24) & 0xFF); }
static inline uint8_t _r(uint32_t c){ return uint8_t((c >> 16) & 0xFF); }
static inline uint8_t _g(uint32_t c){ return uint8_t((c >> 8) & 0xFF); }
static inline uint8_t _b(uint32_t c){ return uint8_t((c >> 0) & 0xFF); }
static inline uint32_t _pack(uint8_t a,uint8_t r,uint8_t g,uint8_t b){
return (uint32_t(a)<<24)|(uint32_t(r)<<16)|(uint32_t(g)<<8)|(uint32_t(b));
}
static inline uint8_t _clampu8(int v){ return uint8_t(std::min(255, std::max(0, v))); }
// ========================
// VM (executes bytes)
// ========================
struct Image {
uint32_t W=0,H=0;
std::vector<uint32_t> Px;
};
class VM {
public:
using TextureProvider = TexturePipelineProgram::TextureProviderFunc;
explicit VM(TextureProvider provider);
bool run(const std::vector<uint8_t>& code, OwnedTexture& out, double timeSeconds, std::string* err);
private:
TextureProvider Provider_;
static bool _bad(std::string* err, const char* msg);
static bool _readSrc(const std::vector<uint8_t>& code, size_t& ip, SrcRef& out, std::string* err);
Image _loadTex(uint32_t id, std::unordered_map<uint32_t, Image>& cache, std::string* err);
Image _loadSub(const std::vector<uint8_t>& code,
uint32_t off, uint32_t len,
std::unordered_map<uint32_t, Image>& texCache,
std::unordered_map<uint64_t, Image>& subCache,
double timeSeconds,
std::string* err);
Image _loadSrc(const std::vector<uint8_t>& code,
const SrcRef& src,
std::unordered_map<uint32_t, Image>& texCache,
std::unordered_map<uint64_t, Image>& subCache,
double timeSeconds,
std::string* err);
// ---- image ops (как в исходнике) ----
static Image _makeSolid(uint32_t w, uint32_t h, uint32_t color);
static Image _resizeNN(const Image& src, uint32_t nw, uint32_t nh);
static Image _resizeNN_ifNeeded(Image img, uint32_t w, uint32_t h);
static Image _cropFrame(const Image& sheet, uint32_t index, uint32_t fw, uint32_t fh, bool horizontal);
static Image _cropFrameGrid(const Image& sheet, uint32_t index, uint32_t fw, uint32_t fh);
static void _lerp(Image& base, const Image& over, double t);
static void _alphaOver(Image& base, const Image& over);
static void _applyMask(Image& base, const Image& mask);
static void _opacity(Image& img, uint8_t mul);
static void _noAlpha(Image& img);
static void _makeAlpha(Image& img, uint32_t rgb24);
static void _invert(Image& img, uint32_t maskBits);
static void _brighten(Image& img);
static void _contrast(Image& img, int c, int br);
static void _multiply(Image& img, uint32_t color);
static void _screen(Image& img, uint32_t color);
static void _colorize(Image& img, uint32_t color, uint8_t ratio);
static void _lowpart(Image& base, const Image& over, uint32_t percent);
static Image _transform(const Image& src, uint32_t t);
};
// ========================
// Minimal DSL Lexer/Parser
// now supports:
// name |> op(...)
// 32x32 "#RRGGBBAA"
// optional prefix:
// tex name |> op(...)
// nested only where op expects a texture arg:
// overlay( tex other |> ... )
// Also supports overlay(other) / mask(other) / lowpart(50, other)
// ========================
enum class TokKind { End, Ident, Number, String, Pipe, Comma, LParen, RParen, Eq, X };
struct Tok {
TokKind Kind = TokKind::End;
std::string Text;
uint32_t U32 = 0;
};
struct Lexer {
std::string_view S;
size_t I=0;
bool HasBuf = false;
Tok Buf;
static bool isAlpha(char c){ return (c>='a'&&c<='z')||(c>='A'&&c<='Z')||c=='_'; }
static bool isNum(char c){ return (c>='0'&&c<='9'); }
static bool isAlnum(char c){ return isAlpha(c)||isNum(c); }
void unread(const Tok& t);
Tok peek();
void skipWs();
Tok next();
};
struct ArgVal {
enum class ValueKind { U32, Str, Ident };
ValueKind Kind = ValueKind::U32;
uint32_t U32 = 0;
std::string S;
};
struct ParsedOp {
std::string Name;
std::vector<ArgVal> Pos;
std::unordered_map<std::string, ArgVal> Named;
};
// ========================
// Compiler state
// ========================
std::string Source_;
std::vector<uint8_t> Code_;
std::vector<Patch> Patches_;
// ---- emit helpers (target = arbitrary out vector) ----
static inline void _emitOp(std::vector<uint8_t>& out, Op op) { _wr8(out, uint8_t(op)); }
static inline void _emitU8(std::vector<uint8_t>& out, uint32_t v){ _wr8(out, v); }
static inline void _emitU16(std::vector<uint8_t>& out, uint32_t v){ _wr16(out, v); }
static inline void _emitU24(std::vector<uint8_t>& out, uint32_t v){ _wr24(out, v); }
static inline void _emitU32(std::vector<uint8_t>& out, uint32_t v){ _wr32(out, v); }
// reserve 3 bytes for u24 texId and register patch (absolute or relative)
struct RelPatch { size_t Rel0; std::string Name; };
static void _emitTexPatchU24(std::vector<uint8_t>& out,
std::vector<Patch>* absPatches,
std::vector<RelPatch>* relPatches,
const std::string& name) {
const size_t idx = out.size();
out.push_back(0); out.push_back(0); out.push_back(0);
if(absPatches) absPatches->push_back(Patch{idx, name});
if(relPatches) relPatches->push_back(RelPatch{idx, name});
}
static void _emitSrcTexName(std::vector<uint8_t>& out,
std::vector<Patch>* absPatches,
std::vector<RelPatch>* relPatches,
const std::string& name) {
_emitU8(out, uint8_t(SrcKind::TexId));
_emitTexPatchU24(out, absPatches, relPatches, name);
}
static void _emitSrcSub(std::vector<uint8_t>& out, uint32_t off24, uint32_t len24) {
_emitU8(out, uint8_t(SrcKind::Sub));
_emitU24(out, off24);
_emitU24(out, len24);
}
// ========================
// Color parsing: #RRGGBB or #RRGGBBAA -> 0xAARRGGBB
// ========================
static bool _parseHexColor(std::string_view s, uint32_t& outARGB);
// ========================
// Parsing entry: full program
// ========================
bool _parseProgram(std::string* err);
// ========================
// Base compilation (optionally after 'tex')
// supports:
// 1) name
// 2) "name(.png/.jpg/.jpeg)" (allowed but normalized)
// 3) anim(...)
// 4) 32x32 "#RRGGBBAA"
// optional: all of the above may be prefixed with 'tex'
// ========================
bool _compileBaseAfterTex(Lexer& lx,
std::vector<uint8_t>& out,
std::vector<Patch>* absPatches,
std::vector<RelPatch>* relPatches,
std::string* err);
bool _compileBaseFromToken(Lexer& lx,
const Tok& a,
std::vector<uint8_t>& out,
std::vector<Patch>* absPatches,
std::vector<RelPatch>* relPatches,
std::string* err);
// ========================
// Args parsing:
// - normal args: (a,b,key=v)
// - OR if first token inside '(' is 'tex' => parse nested program until ')'
// ========================
bool _parseArgListOrTextureExpr(Lexer& lx, ParsedOp& op, std::string* err);
bool _parseArgList(Lexer& lx, ParsedOp& op, std::string* err);
bool _tokToVal(const Tok& t, ArgVal& out, std::string* err);
// ========================
// Subprogram compilation:
// we already consumed 'tex'. Parse base + pipeline until next token is ')'
// DO NOT consume ')'
// ========================
struct PendingSubData {
std::vector<uint8_t> Bytes;
std::vector<RelPatch> RelPatches;
};
bool _compileSubProgramFromAlreadySawTex(Lexer& lx, PendingSubData& outSub, std::string* err);
// pending subprogram associated with ParsedOp pointer (created during parsing)
mutable std::unordered_map<const ParsedOp*, PendingSubData> PendingSub_;
// Append subprogram to `out` and emit SrcRef(Sub, off16, len16), migrating patches properly.
static bool _appendSubprogram(std::vector<uint8_t>& out,
PendingSubData&& sub,
std::vector<Patch>* absPatches,
std::vector<RelPatch>* relPatches,
uint32_t& outOff,
uint32_t& outLen,
std::string* err);
// ========================
// Compile operations into arbitrary `out`
// absPatches != nullptr => patches recorded as absolute for this buffer
// relPatches != nullptr => patches recorded as relative for this buffer
// ========================
bool _compileOpInto(Lexer& lx,
const ParsedOp& op,
std::vector<uint8_t>& out,
std::vector<Patch>* absPatches,
std::vector<RelPatch>* relPatches,
std::string* err);
};

View File

@@ -34,24 +34,6 @@ using PlayerId_t = ResourceId;
using DefGeneratorId_t = ResourceId;
/*
Сервер загружает информацию о локальных текстурах
Пересмотр списка текстур?
Динамичные текстуры?
*/
struct ResourceFile {
using Hash_t = sha2::sha256_hash; // boost::uuids::detail::sha1::digest_type;
Hash_t Hash;
std::vector<std::byte> Data;
void calcHash() {
Hash = sha2::sha256((const uint8_t*) Data.data(), Data.size());
}
};
struct ServerTime {
uint32_t Seconds : 24, Sub : 8;
};
@@ -236,6 +218,7 @@ public:
}
DefEntityId getDefId() const { return DefId; }
void setDefId(DefEntityId defId) { DefId = defId; }
};
template<typename Vec>

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 +1,129 @@
#pragma once
#include "Common/Abstract.hpp"
#include "TOSLib.hpp"
#include "Common/Net.hpp"
#include "sha2.hpp"
#include <bitset>
#include <filesystem>
#include <optional>
#include "Common/IdProvider.hpp"
#include "Common/AssetsPreloader.hpp"
#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 {
class AssetsManager : public IdProvider<EnumAssets>, protected AssetsPreloader {
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;
using BindHashHeaderInfo = AssetsManager::BindHashHeaderInfo;
// std::unordered_map<std::string, std::vector<std::pair<std::string, PreparedModel>>> Models;
struct Out_checkAndPrepareResourcesUpdate : public AssetsPreloader::Out_checkAndPrepareResourcesUpdate {
Out_checkAndPrepareResourcesUpdate(AssetsPreloader::Out_checkAndPrepareResourcesUpdate&& obj)
: AssetsPreloader::Out_checkAndPrepareResourcesUpdate(std::move(obj))
{}
std::unordered_map<ResourceFile::Hash_t, std::u8string> NewHeadless;
};
private:
// Данные об отслеживаемых файлах
struct DataEntry {
// Время последнего изменения файла
fs::file_time_type FileChangeTime;
Resource Res;
std::string Domain, Key;
Out_checkAndPrepareResourcesUpdate checkAndPrepareResourcesUpdate(
const AssetsRegister& instances,
ReloadStatus* status = nullptr
) {
std::unordered_map<ResourceFile::Hash_t, std::u8string> newHeadless;
Out_checkAndPrepareResourcesUpdate result = AssetsPreloader::checkAndPrepareResourcesUpdate(
instances,
[&](EnumAssets type, std::string_view domain, std::string_view key) { return getId(type, domain, key); },
[&](std::u8string&& resource, ResourceFile::Hash_t hash, fs::path resPath) { newHeadless.emplace(hash, std::move(resource)); },
status
);
result.NewHeadless = std::move(newHeadless);
return result;
}
struct Out_applyResourcesUpdate : public AssetsPreloader::Out_applyResourcesUpdate {
Out_applyResourcesUpdate(AssetsPreloader::Out_applyResourcesUpdate&& obj)
: AssetsPreloader::Out_applyResourcesUpdate(std::move(obj))
{}
};
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;
Out_applyResourcesUpdate applyResourcesUpdate(Out_checkAndPrepareResourcesUpdate& orr) {
Out_applyResourcesUpdate result = AssetsPreloader::applyResourcesUpdate(orr);
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, {}, {}};
}
static TOS::Logger LOG = "Server>AssetsManager";
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)
for(size_t type = 0; type < static_cast<size_t>(EnumAssets::MAX_ENUM); ++type) {
if(result.NewOrUpdates[type].empty())
continue;
models.push_back(resId);
models.append_range(subModel->FullSubModelDeps);
textures.append_range(subModel->FullSubTextureDeps);
EnumAssets typeEnum = static_cast<EnumAssets>(type);
const char* typeName = ::EnumAssetsToDirectory(typeEnum);
for(const auto& bind : result.NewOrUpdates[type]) {
auto dk = getDK(typeEnum, bind.Id);
if(!dk)
continue;
LOG.debug()
<< typeName << ": "
<< dk->Domain << '+' << dk->Key << " -> " << bind.Id;
}
}
} else {
LOG.debug() << "Для ноды " << domain << ':' << key << " отсутствует описание Nodestate";
}
for(auto& [hash, data] : orr.NewHeadless) {
Resources.emplace(hash, ResourceHashData{0, std::make_shared<std::u8string>(std::move(data))});
}
for(auto& [hash, pathes] : orr.HashToPathNew) {
auto iter = Resources.find(hash);
assert(iter != Resources.end());
iter->second.RefCount += pathes.size();
}
for(auto& [hash, pathes] : orr.HashToPathLost) {
auto iter = Resources.find(hash);
assert(iter != Resources.end());
iter->second.RefCount -= pathes.size();
if(iter->second.RefCount == 0)
Resources.erase(iter);
}
return result;
}
std::vector<std::tuple<ResourceFile::Hash_t, std::shared_ptr<const std::u8string>>>
getResources(const std::vector<ResourceFile::Hash_t>& hashes) const
{
std::sort(models.begin(), models.end());
auto eraseIter = std::unique(models.begin(), models.end());
models.erase(eraseIter, models.end());
models.shrink_to_fit();
std::vector<std::tuple<ResourceFile::Hash_t, std::shared_ptr<const std::u8string>>> result;
result.reserve(hashes.size());
for(const auto& hash : hashes) {
auto iter = Resources.find(hash);
if(iter == Resources.end())
continue;
result.emplace_back(hash, iter->second.Data);
}
{
std::sort(textures.begin(), textures.end());
auto eraseIter = std::unique(textures.begin(), textures.end());
textures.erase(eraseIter, textures.end());
textures.shrink_to_fit();
return result;
}
return {nodestateId, std::move(models), std::move(textures)};
std::array<
std::vector<BindHashHeaderInfo>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
> collectHashBindings() const {
return AssetsPreloader::collectHashBindings();
}
private:
TOS::Logger LOG = "Server>AssetsManager";
struct ResourceHashData {
size_t RefCount;
std::shared_ptr<std::u8string> Data;
};
std::unordered_map<
ResourceFile::Hash_t,
ResourceHashData
> Resources;
};
}

View File

@@ -1,25 +1,35 @@
#include "ContentManager.hpp"
#include "Common/Abstract.hpp"
#include <algorithm>
#include <optional>
#include <utility>
namespace LV::Server {
ContentManager::ContentManager(AssetsPreloader& am)
ContentManager::ContentManager(AssetsManager& am)
: AM(am)
{
std::fill(std::begin(NextId), std::end(NextId), 1);
}
ContentManager::~ContentManager() = default;
void ContentManager::registerBase_Node(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile) {
std::optional<DefNode>& node = getEntry_Node(id);
if(!node)
node.emplace();
std::optional<DefNode_Base>* basePtr;
DefNode& def = *node;
def.Domain = domain;
def.Key = key;
{
size_t entryIndex = id / TableEntry<DefNode_Base>::ChunkSize;
size_t entryId = id % TableEntry<DefNode_Base>::ChunkSize;
size_t need = entryIndex+1-Profiles_Base_Node.size();
for(size_t iter = 0; iter < need; iter++) {
Profiles_Base_Node.emplace_back(std::make_unique<TableEntry<DefNode_Base>>());
}
basePtr = &Profiles_Base_Node[entryIndex]->Entries[entryId];
*basePtr = DefNode_Base();
}
DefNode_Base& def = **basePtr;
{
std::optional<std::variant<std::string, sol::table>> parent = profile.get<std::optional<std::variant<std::string, sol::table>>>("parent");
@@ -93,7 +103,7 @@ void ContentManager::registerBase_Entity(ResourceId id, const std::string& domai
void ContentManager::registerBase(EnumDefContent type, const std::string& domain, const std::string& key, const sol::table& profile)
{
ResourceId id = getId(type, domain, key);
ProfileChanges[(int) type].push_back(id);
ProfileChanges[static_cast<size_t>(type)].push_back(id);
if(type == EnumDefContent::Node)
registerBase_Node(id, domain, key, profile);
@@ -123,56 +133,203 @@ void ContentManager::unRegisterModifier(EnumDefContent type, const std::string&
ProfileChanges[(int) type].push_back(id);
}
void ContentManager::markAllProfilesDirty(EnumDefContent type) {
const auto &table = ContentKeyToId[(int) type];
for(const auto& domainPair : table) {
for(const auto& keyPair : domainPair.second) {
ProfileChanges[(int) type].push_back(keyPair.second);
}
}
}
// void ContentManager::markAllProfilesDirty(EnumDefContent type) {
// const auto &table = this->idToDK()[(int) type];
// size_t counter = 0;
// for(const auto& [domain, key] : table) {
// ProfileChanges[static_cast<size_t>(type)].push_back(counter++);
// }
// }
std::vector<ResourceId> ContentManager::collectProfileIds(EnumDefContent type) const {
std::vector<ResourceId> ids;
const auto &table = ContentKeyToId[(int) type];
template<class type, class modType>
void ContentManager::buildEndProfilesByType(auto& profiles, auto enumType, auto& base, auto& keys, auto& result, auto& modsTable) {
// Расширяем таблицу итоговых профилей до нужного количества
if(!keys.empty()) {
size_t need = keys.back() / TableEntry<type>::ChunkSize;
if(need >= profiles.size()) {
profiles.reserve(need);
for(const auto& domainPair : table) {
for(const auto& keyPair : domainPair.second) {
ids.push_back(keyPair.second);
for(size_t iter = 0; iter <= need-profiles.size(); ++iter)
profiles.emplace_back(std::make_unique<TableEntry<type>>());
}
}
std::sort(ids.begin(), ids.end());
auto last = std::unique(ids.begin(), ids.end());
ids.erase(last, ids.end());
return ids;
TOS::Logger("CM").debug() << "type: " << static_cast<size_t>(enumType);
// Пересчитываем профили
for(size_t id : keys) {
size_t entryIndex = id / TableEntry<type>::ChunkSize;
size_t subIndex = id % TableEntry<type>::ChunkSize;
if(
entryIndex >= base.size()
|| !base[entryIndex]->Entries[subIndex]
) {
// Базовый профиль не существует
profiles[entryIndex]->Entries[subIndex] = std::nullopt;
// Уведомляем о потере профиля
result.LostProfiles[static_cast<size_t>(enumType)].push_back(id);
} else {
// Собираем конечный профиль
std::vector<std::tuple<std::string, modType>> mods_default, *mods = &mods_default;
auto iter = modsTable.find(id);
if(iter != modsTable.end())
mods = &iter->second;
std::optional<BindDomainKeyInfo> dk = getDK(enumType, id);
assert(dk);
TOS::Logger("CM").debug() << "\t" << dk->Domain << ":" << dk->Key << " -> " << id;
profiles[entryIndex]->Entries[subIndex] = base[entryIndex]->Entries[subIndex]->compile(AM, *this, dk->Domain, dk->Key, *mods);
}
}
}
ContentManager::Out_buildEndProfiles ContentManager::buildEndProfiles() {
Out_buildEndProfiles result;
for(int type = 0; type < (int) EnumDefContent::MAX_ENUM; type++) {
std::shared_lock lock(Profiles_Mtx[type]);
auto& keys = ProfileChanges[type];
std::sort(keys.begin(), keys.end());
auto iterErase = std::unique(keys.begin(), keys.end());
keys.erase(iterErase, keys.end());
switch(type) {
case 0: buildEndProfilesByType<DefVoxel, DefVoxel_Mod> (Profiles_Voxel, EnumDefContent::Voxel, Profiles_Base_Voxel, keys, result, Profiles_Mod_Voxel); break;
case 1: buildEndProfilesByType<DefNode, DefNode_Mod> (Profiles_Node, EnumDefContent::Node, Profiles_Base_Node, keys, result, Profiles_Mod_Node); break;
case 2: buildEndProfilesByType<DefWorld, DefWorld_Mod> (Profiles_World, EnumDefContent::World, Profiles_Base_World, keys, result, Profiles_Mod_World); break;
case 3: buildEndProfilesByType<DefPortal, DefPortal_Mod> (Profiles_Portal, EnumDefContent::Portal, Profiles_Base_Portal, keys, result, Profiles_Mod_Portal); break;
case 4: buildEndProfilesByType<DefEntity, DefEntity_Mod> (Profiles_Entity, EnumDefContent::Entity, Profiles_Base_Entity, keys, result, Profiles_Mod_Entity); break;
case 5: buildEndProfilesByType<DefItem, DefItem_Mod> (Profiles_Item, EnumDefContent::Item, Profiles_Base_Item, keys, result, Profiles_Mod_Item); break;
default: std::unreachable();
}
for(ResourceId id : ProfileChanges[(int) EnumDefContent::Node]) {
std::optional<DefNode>& node = getEntry_Node(id);
if(!node) {
continue;
}
auto [nodestateId, assetsModel, assetsTexture]
= AM.getNodeDependency(node->Domain, node->Key);
node->NodestateId = nodestateId;
node->ModelDeps = std::move(assetsModel);
node->TextureDeps = std::move(assetsTexture);
}
return result;
}
ContentManager::Out_getAllProfiles ContentManager::getAllProfiles() {
Out_getAllProfiles result;
size_t counter;
{
std::shared_lock lock(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Voxel)]);
result.ProfilesIds_Voxel.reserve(Profiles_Voxel.size()*TableEntry<DefVoxel>::ChunkSize);
counter = 0;
for(const auto& entry : Profiles_Voxel)
for(const auto& item : entry->Entries) {
size_t id = counter++;
if(item)
result.ProfilesIds_Voxel.push_back(id);
}
result.ProfilesIds_Voxel.shrink_to_fit();
result.Profiles_Voxel.reserve(result.ProfilesIds_Voxel.size());
for(const auto& entry : Profiles_Voxel)
for(const auto& item : entry->Entries)
if(item)
result.Profiles_Voxel.push_back(&item.value());
}
{
std::shared_lock lock(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Node)]);
result.ProfilesIds_Node.reserve(Profiles_Node.size()*TableEntry<DefNode>::ChunkSize);
counter = 0;
for(const auto& entry : Profiles_Node)
for(const auto& item : entry->Entries) {
size_t id = counter++;
if(item)
result.ProfilesIds_Node.push_back(id);
}
result.ProfilesIds_Node.shrink_to_fit();
result.Profiles_Node.reserve(result.ProfilesIds_Node.size());
for(const auto& entry : Profiles_Node)
for(const auto& item : entry->Entries)
if(item)
result.Profiles_Node.push_back(&item.value());
}
{
std::shared_lock lock(Profiles_Mtx[static_cast<size_t>(EnumDefContent::World)]);
result.ProfilesIds_World.reserve(Profiles_World.size()*TableEntry<DefWorld>::ChunkSize);
counter = 0;
for(const auto& entry : Profiles_World)
for(const auto& item : entry->Entries) {
size_t id = counter++;
if(item)
result.ProfilesIds_World.push_back(id);
}
result.ProfilesIds_World.shrink_to_fit();
result.Profiles_World.reserve(result.ProfilesIds_World.size());
for(const auto& entry : Profiles_World)
for(const auto& item : entry->Entries)
if(item)
result.Profiles_World.push_back(&item.value());
}
{
std::shared_lock lock(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Portal)]);
result.ProfilesIds_Portal.reserve(Profiles_Portal.size()*TableEntry<DefPortal>::ChunkSize);
counter = 0;
for(const auto& entry : Profiles_Portal)
for(const auto& item : entry->Entries) {
size_t id = counter++;
if(item)
result.ProfilesIds_Portal.push_back(id);
}
result.ProfilesIds_Portal.shrink_to_fit();
result.Profiles_Portal.reserve(result.ProfilesIds_Portal.size());
for(const auto& entry : Profiles_Portal)
for(const auto& item : entry->Entries)
if(item)
result.Profiles_Portal.push_back(&item.value());
}
{
std::shared_lock lock(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Entity)]);
result.ProfilesIds_Entity.reserve(Profiles_Entity.size()*TableEntry<DefEntity>::ChunkSize);
counter = 0;
for(const auto& entry : Profiles_Entity)
for(const auto& item : entry->Entries) {
size_t id = counter++;
if(item)
result.ProfilesIds_Entity.push_back(id);
}
result.ProfilesIds_Entity.shrink_to_fit();
result.Profiles_Entity.reserve(result.ProfilesIds_Entity.size());
for(const auto& entry : Profiles_Entity)
for(const auto& item : entry->Entries)
if(item)
result.Profiles_Entity.push_back(&item.value());
}
{
std::shared_lock lock(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Item)]);
result.ProfilesIds_Item.reserve(Profiles_Item.size()*TableEntry<DefItem>::ChunkSize);
counter = 0;
for(const auto& entry : Profiles_Item)
for(const auto& item : entry->Entries) {
size_t id = counter++;
if(item)
result.ProfilesIds_Item.push_back(id);
}
result.ProfilesIds_Item.shrink_to_fit();
result.Profiles_Item.reserve(result.ProfilesIds_Item.size());
for(const auto& entry : Profiles_Item)
for(const auto& item : entry->Entries)
if(item)
result.Profiles_Item.push_back(&item.value());
}
result.IdToDK = idToDK();
return result;
}
}

View File

@@ -1,21 +1,64 @@
#pragma once
#include "Common/Abstract.hpp"
#include "Server/Abstract.hpp"
#include "Common/AssetsPreloader.hpp"
#include "AssetsManager.hpp"
#include "Common/IdProvider.hpp"
#include "Common/Net.hpp"
#include "TOSLib.hpp"
#include <array>
#include <mutex>
#include <sol/table.hpp>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
namespace LV::Server {
struct DefVoxel_Base { };
struct DefNode_Base { };
struct DefWorld_Base { };
struct DefPortal_Base { };
struct DefEntity_Base { };
struct DefItem_Base { };
struct ResourceBase {
std::string Domain, Key;
};
class ContentManager;
struct DefVoxel : public ResourceBase {
std::u8string dumpToClient() const {
return {};
}
};
struct DefNode : public ResourceBase {
AssetsNodestate NodestateId;
std::u8string dumpToClient() const {
auto wr = TOS::ByteBuffer::Writer();
wr << uint32_t(NodestateId);
auto buff = wr.complite();
return (std::u8string) std::u8string_view((const char8_t*) buff.data(), buff.size());
}
};
struct DefWorld : public ResourceBase {
std::u8string dumpToClient() const {
return {};
}
};
struct DefPortal : public ResourceBase {
std::u8string dumpToClient() const {
return {};
}
};
struct DefEntity : public ResourceBase {
std::u8string dumpToClient() const {
return {};
}
};
struct DefItem : public ResourceBase {
std::u8string dumpToClient() const {
return {};
}
};
struct DefVoxel_Mod { };
struct DefNode_Mod { };
@@ -24,41 +67,370 @@ struct DefPortal_Mod { };
struct DefEntity_Mod { };
struct DefItem_Mod { };
struct ResourceBase {
std::string Domain, Key;
struct DefVoxel_Base {
private:
friend ContentManager;
DefVoxel compile(AssetsManager& am, ContentManager& cm, const std::string_view domain, const std::string_view key, const std::vector<std::tuple<std::string, DefVoxel_Mod>>& mods) const {
return DefVoxel();
}
};
struct DefVoxel : public ResourceBase { };
struct DefNode : public ResourceBase {
AssetsNodestate NodestateId;
std::vector<AssetsModel> ModelDeps;
std::vector<AssetsTexture> TextureDeps;
struct DefNode_Base {
private:
friend ContentManager;
DefNode compile(AssetsManager& am, ContentManager& cm, const std::string_view domain, const std::string_view key, const std::vector<std::tuple<std::string, DefNode_Mod>>& mods) const {
DefNode profile;
std::string jsonKey = std::string(key)+".json";
profile.NodestateId = am.getId(EnumAssets::Nodestate, domain, jsonKey);
TOS::Logger("Compile").info() << domain << ' ' << key << " -> " << profile.NodestateId;
return profile;
}
};
struct DefWorld : public ResourceBase { };
struct DefPortal : public ResourceBase { };
struct DefEntity : public ResourceBase { };
struct DefItem : public ResourceBase { };
class ContentManager {
struct DefWorld_Base {
private:
friend ContentManager;
DefWorld compile(AssetsManager& am, ContentManager& cm, const std::string_view domain, const std::string_view key, const std::vector<std::tuple<std::string, DefWorld_Mod>>& mods) const {
return DefWorld();
}
};
struct DefPortal_Base {
private:
friend ContentManager;
DefPortal compile(AssetsManager& am, ContentManager& cm, const std::string_view domain, const std::string_view key, const std::vector<std::tuple<std::string, DefPortal_Mod>>& mods) const {
return DefPortal();
}
};
struct DefEntity_Base {
private:
friend ContentManager;
DefEntity compile(AssetsManager& am, ContentManager& cm, const std::string_view domain, const std::string_view key, const std::vector<std::tuple<std::string, DefEntity_Mod>>& mods) const {
return DefEntity();
}
};
struct DefItem_Base {
private:
friend ContentManager;
DefItem compile(AssetsManager& am, ContentManager& cm, const std::string_view domain, const std::string_view key, const std::vector<std::tuple<std::string, DefItem_Mod>>& mods) const {
return DefItem();
}
};
/*
DK to id
id to profile
*/
class ContentManager : public IdProvider<EnumDefContent> {
public:
class LRU {
public:
LRU(ContentManager& cm)
: CM(&cm)
{
}
LRU(const LRU&) = default;
LRU(LRU&&) = default;
LRU& operator=(const LRU&) = default;
LRU& operator=(LRU&&) = default;
ResourceId getId(EnumDefContent type, const std::string_view domain, const std::string_view key) {
auto iter = DKToId[static_cast<size_t>(type)].find(BindDomainKeyViewInfo(domain, key));
if(iter == DKToId[static_cast<size_t>(type)].end()) {
ResourceId id = CM->getId(type, domain, key);
DKToId[static_cast<size_t>(type)].emplace_hint(iter, BindDomainKeyInfo((std::string) domain, (std::string) key), id);
return id;
}
return iter->second;
// switch(type) {
// case EnumDefContent::Voxel:
// case EnumDefContent::Node:
// case EnumDefContent::World:
// case EnumDefContent::Portal:
// case EnumDefContent::Entity:
// case EnumDefContent::Item:
// default:
// std::unreachable();
// }
}
ResourceId getIdVoxel(const std::string_view domain, const std::string_view key) {
return getId(EnumDefContent::Voxel, domain, key);
}
ResourceId getIdNode(const std::string_view domain, const std::string_view key) {
return getId(EnumDefContent::Node, domain, key);
}
ResourceId getIdWorld(const std::string_view domain, const std::string_view key) {
return getId(EnumDefContent::World, domain, key);
}
ResourceId getIdPortal(const std::string_view domain, const std::string_view key) {
return getId(EnumDefContent::Portal, domain, key);
}
ResourceId getIdEntity(const std::string_view domain, const std::string_view key) {
return getId(EnumDefContent::Entity, domain, key);
}
ResourceId getIdItem(const std::string_view domain, const std::string_view key) {
return getId(EnumDefContent::Item, domain, key);
}
private:
ContentManager* CM;
std::array<
ankerl::unordered_dense::map<BindDomainKeyInfo, ResourceId, KeyHash, KeyEq>,
MAX_ENUM
> DKToId;
std::unordered_map<DefVoxelId, std::optional<DefVoxel>*> Profiles_Voxel;
std::unordered_map<DefNodeId, std::optional<DefNode>*> Profiles_Node;
std::unordered_map<DefWorldId, std::optional<DefWorld>*> Profiles_World;
std::unordered_map<DefPortalId, std::optional<DefPortal>*> Profiles_Portal;
std::unordered_map<DefEntityId, std::optional<DefEntity>*> Profiles_Entity;
std::unordered_map<DefItemId, std::optional<DefItem>*> Profiles_Item;
};
public:
ContentManager(AssetsManager &am);
~ContentManager();
// Регистрирует определение контента
void registerBase(EnumDefContent type, const std::string& domain, const std::string& key, const sol::table& profile);
void unRegisterBase(EnumDefContent type, const std::string& domain, const std::string& key);
// Регистрация модификатора предмета модом
void registerModifier(EnumDefContent type, const std::string& mod, const std::string& domain, const std::string& key, const sol::table& profile);
void unRegisterModifier(EnumDefContent type, const std::string& mod, const std::string& domain, const std::string& key);
// Пометить все профили типа как изменённые (например, после перезагрузки ассетов)
// void markAllProfilesDirty(EnumDefContent type);
// Список всех зарегистрированных профилей выбранного типа
std::vector<ResourceId> collectProfileIds(EnumDefContent type) const;
// Компилирует изменённые профили
struct Out_buildEndProfiles {
std::vector<
std::tuple<DefVoxelId, const DefVoxel*>
> ChangedProfiles_Voxel;
std::vector<
std::tuple<DefNodeId, const DefNode*>
> ChangedProfiles_Node;
std::vector<
std::tuple<DefWorldId, const DefWorld*>
> ChangedProfiles_World;
std::vector<
std::tuple<DefPortalId, const DefPortal*>
> ChangedProfiles_Portal;
std::vector<
std::tuple<DefEntityId, const DefEntity*>
> ChangedProfiles_Entity;
std::vector<
std::tuple<DefItemId, const DefItem*>
> ChangedProfiles_Item;
std::array<
std::vector<ResourceId>,
MAX_ENUM
> LostProfiles;
std::array<
std::vector<BindDomainKeyInfo>,
static_cast<size_t>(EnumDefContent::MAX_ENUM)
> IdToDK;
};
// Компилирует конечные профили по базе и модификаторам (предоставляет клиентам изменённые и потерянные)
Out_buildEndProfiles buildEndProfiles();
struct Out_getAllProfiles {
std::vector<DefVoxelId> ProfilesIds_Voxel;
std::vector<const DefVoxel*> Profiles_Voxel;
std::vector<DefNodeId> ProfilesIds_Node;
std::vector<const DefNode*> Profiles_Node;
std::vector<DefWorldId> ProfilesIds_World;
std::vector<const DefWorld*> Profiles_World;
std::vector<DefPortalId> ProfilesIds_Portal;
std::vector<const DefPortal*> Profiles_Portal;
std::vector<DefEntityId> ProfilesIds_Entity;
std::vector<const DefEntity*> Profiles_Entity;
std::vector<DefItemId> ProfilesIds_Item;
std::vector<const DefItem*> Profiles_Item;
std::array<
std::vector<BindDomainKeyInfo>,
static_cast<size_t>(EnumDefContent::MAX_ENUM)
> IdToDK;
};
// Выдаёт все профили (для новых клиентов)
Out_getAllProfiles getAllProfiles();
std::optional<DefVoxel>& getEntry_Voxel(ResourceId resId) {
std::shared_lock mtx(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Voxel)]);
assert(resId / TableEntry<DefVoxel>::ChunkSize <= Profiles_Voxel.size());
return Profiles_Voxel[resId / TableEntry<DefVoxel>::ChunkSize]->Entries[resId % TableEntry<DefVoxel>::ChunkSize];
}
std::optional<DefVoxel>& getEntry_Voxel(const std::string_view domain, const std::string_view key) {
return getEntry_Voxel(getId(EnumDefContent::Voxel, domain, key));
}
std::optional<DefNode>& getEntry_Node(ResourceId resId) {
std::shared_lock mtx(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Node)]);
assert(resId / TableEntry<DefNode>::ChunkSize < Profiles_Node.size());
return Profiles_Node[resId / TableEntry<DefVoxel>::ChunkSize]->Entries[resId % TableEntry<DefVoxel>::ChunkSize];
}
std::optional<DefNode>& getEntry_Node(const std::string_view domain, const std::string_view key) {
return getEntry_Node(getId(EnumDefContent::Node, domain, key));
}
std::optional<DefWorld>& getEntry_World(ResourceId resId) {
std::shared_lock mtx(Profiles_Mtx[static_cast<size_t>(EnumDefContent::World)]);
assert(resId / TableEntry<DefWorld>::ChunkSize < Profiles_World.size());
return Profiles_World[resId / TableEntry<DefVoxel>::ChunkSize]->Entries[resId % TableEntry<DefVoxel>::ChunkSize];
}
std::optional<DefWorld>& getEntry_World(const std::string_view domain, const std::string_view key) {
return getEntry_World(getId(EnumDefContent::World, domain, key));
}
std::optional<DefPortal>& getEntry_Portal(ResourceId resId) {
std::shared_lock mtx(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Portal)]);
assert(resId / TableEntry<DefPortal>::ChunkSize < Profiles_Portal.size());
return Profiles_Portal[resId / TableEntry<DefVoxel>::ChunkSize]->Entries[resId % TableEntry<DefVoxel>::ChunkSize];
}
std::optional<DefPortal>& getEntry_Portal(const std::string_view domain, const std::string_view key) {
return getEntry_Portal(getId(EnumDefContent::Portal, domain, key));
}
std::optional<DefEntity>& getEntry_Entity(ResourceId resId) {
std::shared_lock mtx(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Entity)]);
assert(resId / TableEntry<DefEntity>::ChunkSize < Profiles_Entity.size());
return Profiles_Entity[resId / TableEntry<DefVoxel>::ChunkSize]->Entries[resId % TableEntry<DefVoxel>::ChunkSize];
}
std::optional<DefEntity>& getEntry_Entity(const std::string_view domain, const std::string_view key) {
return getEntry_Entity(getId(EnumDefContent::Entity, domain, key));
}
std::optional<DefItem>& getEntry_Item(ResourceId resId) {
std::shared_lock mtx(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Item)]);
assert(resId / TableEntry<DefItem>::ChunkSize < Profiles_Item.size());
return Profiles_Item[resId / TableEntry<DefVoxel>::ChunkSize]->Entries[resId % TableEntry<DefVoxel>::ChunkSize];
}
std::optional<DefItem>& getEntry_Item(const std::string_view domain, const std::string_view key) {
return getEntry_Item(getId(EnumDefContent::Item, domain, key));
}
ResourceId getId(EnumDefContent type, const std::string_view domain, const std::string_view key) {
ResourceId resId = IdProvider::getId(type, domain, key);
switch(type) {
case EnumDefContent::Voxel:
if(resId >= Profiles_Voxel.size()*TableEntry<DefVoxel>::ChunkSize) {
std::unique_lock mtx(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Voxel)]);
if(resId >= Profiles_Voxel.size()*TableEntry<DefVoxel>::ChunkSize)
Profiles_Voxel.push_back(std::make_unique<TableEntry<DefVoxel>>());
}
break;
case EnumDefContent::Node:
if(resId >= Profiles_Node.size()*TableEntry<DefNode>::ChunkSize) {
std::unique_lock mtx(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Node)]);
if(resId >= Profiles_Node.size()*TableEntry<DefNode>::ChunkSize)
Profiles_Node.push_back(std::make_unique<TableEntry<DefNode>>());
}
break;
case EnumDefContent::World:
if(resId >= Profiles_World.size()*TableEntry<DefWorld>::ChunkSize) {
std::unique_lock mtx(Profiles_Mtx[static_cast<size_t>(EnumDefContent::World)]);
if(resId >= Profiles_World.size()*TableEntry<DefWorld>::ChunkSize)
Profiles_World.push_back(std::make_unique<TableEntry<DefWorld>>());
}
break;
case EnumDefContent::Portal:
if(resId >= Profiles_Portal.size()*TableEntry<DefPortal>::ChunkSize) {
std::unique_lock mtx(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Portal)]);
if(resId >= Profiles_Portal.size()*TableEntry<DefPortal>::ChunkSize)
Profiles_Portal.push_back(std::make_unique<TableEntry<DefPortal>>());
}
break;
case EnumDefContent::Entity:
if(resId >= Profiles_Entity.size()*TableEntry<DefEntity>::ChunkSize) {
std::unique_lock mtx(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Entity)]);
if(resId >= Profiles_Entity.size()*TableEntry<DefEntity>::ChunkSize)
Profiles_Entity.push_back(std::make_unique<TableEntry<DefEntity>>());
}
break;
case EnumDefContent::Item:
if(resId >= Profiles_Item.size()*TableEntry<DefItem>::ChunkSize) {
std::unique_lock mtx(Profiles_Mtx[static_cast<size_t>(EnumDefContent::Item)]);
if(resId >= Profiles_Item.size()*TableEntry<DefItem>::ChunkSize)
Profiles_Item.push_back(std::make_unique<TableEntry<DefItem>>());
}
break;
default:
std::unreachable();
}
return resId;
}
LRU createLRU() {
return {*this};
}
private:
template<typename T>
struct TableEntry {
static constexpr size_t ChunkSize = 4096;
std::array<std::optional<T>, ChunkSize> Entries;
};
void registerBase_Node(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile);
void registerBase_World(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);
// Следующие идентификаторы регистрации контента
ResourceId NextId[(int) EnumDefContent::MAX_ENUM] = {};
// Домен -> {ключ -> идентификатор}
std::unordered_map<std::string, std::unordered_map<std::string, ResourceId>> ContentKeyToId[(int) EnumDefContent::MAX_ENUM];
template<class type, class modType>
void buildEndProfilesByType(auto& profiles, auto enumType, auto& base, auto& keys, auto& result, auto& mods);
TOS::Logger LOG = "Server>ContentManager";
AssetsManager& AM;
// Профили зарегистрированные модами
std::vector<std::unique_ptr<TableEntry<DefVoxel>>> Profiles_Base_Voxel;
std::vector<std::unique_ptr<TableEntry<DefNode>>> Profiles_Base_Node;
std::vector<std::unique_ptr<TableEntry<DefWorld>>> Profiles_Base_World;
std::vector<std::unique_ptr<TableEntry<DefPortal>>> Profiles_Base_Portal;
std::vector<std::unique_ptr<TableEntry<DefEntity>>> Profiles_Base_Entity;
std::vector<std::unique_ptr<TableEntry<DefItem>>> Profiles_Base_Item;
std::vector<std::unique_ptr<TableEntry<DefVoxel_Base>>> Profiles_Base_Voxel;
std::vector<std::unique_ptr<TableEntry<DefNode_Base>>> Profiles_Base_Node;
std::vector<std::unique_ptr<TableEntry<DefWorld_Base>>> Profiles_Base_World;
std::vector<std::unique_ptr<TableEntry<DefPortal_Base>>> Profiles_Base_Portal;
std::vector<std::unique_ptr<TableEntry<DefEntity_Base>>> Profiles_Base_Entity;
std::vector<std::unique_ptr<TableEntry<DefItem_Base>>> Profiles_Base_Item;
// Изменения, накладываемые на профили
// Идентификатор [домен мода модификатора, модификатор]
@@ -71,151 +443,16 @@ class ContentManager {
// Затронутые профили в процессе регистраций
// По ним будут пересобраны профили
std::vector<ResourceId> ProfileChanges[(int) EnumDefContent::MAX_ENUM];
std::vector<ResourceId> ProfileChanges[MAX_ENUM];
// Конечные профили контента
std::array<std::shared_mutex, MAX_ENUM> Profiles_Mtx;
std::vector<std::unique_ptr<TableEntry<DefVoxel>>> Profiles_Voxel;
std::vector<std::unique_ptr<TableEntry<DefNode>>> Profiles_Node;
std::vector<std::unique_ptr<TableEntry<DefWorld>>> Profiles_World;
std::vector<std::unique_ptr<TableEntry<DefPortal>>> Profiles_Portal;
std::vector<std::unique_ptr<TableEntry<DefEntity>>> Profiles_Entity;
std::vector<std::unique_ptr<TableEntry<DefItem>>> Profiles_Item;
std::optional<DefVoxel>& getEntry_Voxel(ResourceId resId) { return Profiles_Voxel[resId / TableEntry<DefVoxel>::ChunkSize]->Entries[resId % TableEntry<DefVoxel>::ChunkSize];}
std::optional<DefNode>& getEntry_Node(ResourceId resId) { return Profiles_Node[resId / TableEntry<DefVoxel>::ChunkSize]->Entries[resId % TableEntry<DefVoxel>::ChunkSize];}
std::optional<DefWorld>& getEntry_World(ResourceId resId) { return Profiles_World[resId / TableEntry<DefVoxel>::ChunkSize]->Entries[resId % TableEntry<DefVoxel>::ChunkSize];}
std::optional<DefPortal>& getEntry_Portal(ResourceId resId) { return Profiles_Portal[resId / TableEntry<DefVoxel>::ChunkSize]->Entries[resId % TableEntry<DefVoxel>::ChunkSize];}
std::optional<DefEntity>& getEntry_Entity(ResourceId resId) { return Profiles_Entity[resId / TableEntry<DefVoxel>::ChunkSize]->Entries[resId % TableEntry<DefVoxel>::ChunkSize];}
std::optional<DefItem>& getEntry_Item(ResourceId resId) { return Profiles_Item[resId / TableEntry<DefVoxel>::ChunkSize]->Entries[resId % TableEntry<DefVoxel>::ChunkSize];}
ResourceId getId(EnumDefContent type, const std::string& domain, const std::string& key) {
if(auto iterCKTI = ContentKeyToId[(int) type].find(domain); iterCKTI != ContentKeyToId[(int) type].end()) {
if(auto iterKey = iterCKTI->second.find(key); iterKey != iterCKTI->second.end()) {
return iterKey->second;
}
}
ResourceId resId = NextId[(int) type]++;
ContentKeyToId[(int) type][domain][key] = resId;
switch(type) {
case EnumDefContent::Voxel:
if(resId >= Profiles_Voxel.size()*TableEntry<DefVoxel>::ChunkSize)
Profiles_Voxel.push_back(std::make_unique<TableEntry<DefVoxel>>());
break;
case EnumDefContent::Node:
if(resId >= Profiles_Node.size()*TableEntry<DefNode>::ChunkSize)
Profiles_Node.push_back(std::make_unique<TableEntry<DefNode>>());
break;
case EnumDefContent::World:
if(resId >= Profiles_World.size()*TableEntry<DefWorld>::ChunkSize)
Profiles_World.push_back(std::make_unique<TableEntry<DefWorld>>());
break;
case EnumDefContent::Portal:
if(resId >= Profiles_Portal.size()*TableEntry<DefPortal>::ChunkSize)
Profiles_Portal.push_back(std::make_unique<TableEntry<DefPortal>>());
break;
case EnumDefContent::Entity:
if(resId >= Profiles_Entity.size()*TableEntry<DefEntity>::ChunkSize)
Profiles_Entity.push_back(std::make_unique<TableEntry<DefEntity>>());
break;
case EnumDefContent::Item:
if(resId >= Profiles_Item.size()*TableEntry<DefItem>::ChunkSize)
Profiles_Item.push_back(std::make_unique<TableEntry<DefItem>>());
break;
default:
std::unreachable();
}
return resId;
}
void registerBase_Node(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile);
void registerBase_World(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:
ContentManager(AssetsPreloader &am);
~ContentManager();
// Регистрирует определение контента
void registerBase(EnumDefContent type, const std::string& domain, const std::string& key, const sol::table& profile);
void unRegisterBase(EnumDefContent type, const std::string& domain, const std::string& key);
// Регистрация модификатора предмета модом
void registerModifier(EnumDefContent type, const std::string& mod, const std::string& domain, const std::string& key, const sol::table& profile);
void unRegisterModifier(EnumDefContent type, const std::string& mod, const std::string& domain, const std::string& key);
// Пометить все профили типа как изменённые (например, после перезагрузки ассетов)
void markAllProfilesDirty(EnumDefContent type);
// Список всех зарегистрированных профилей выбранного типа
std::vector<ResourceId> collectProfileIds(EnumDefContent type) const;
// Компилирует изменённые профили
struct Out_buildEndProfiles {
std::vector<ResourceId> ChangedProfiles[(int) EnumDefContent::MAX_ENUM];
};
Out_buildEndProfiles buildEndProfiles();
std::optional<DefVoxel*> getProfile_Voxel(ResourceId id) {
assert(id < Profiles_Voxel.size()*TableEntry<DefVoxel>::ChunkSize);
auto& value = Profiles_Voxel[id / TableEntry<DefVoxel>::ChunkSize]->Entries[id % TableEntry<DefVoxel>::ChunkSize];
if(value)
return {&*value};
else
return std::nullopt;
}
std::optional<DefNode*> getProfile_Node(ResourceId id) {
assert(id < Profiles_Node.size()*TableEntry<DefNode>::ChunkSize);
auto& value = Profiles_Node[id / TableEntry<DefNode>::ChunkSize]->Entries[id % TableEntry<DefNode>::ChunkSize];
if(value)
return {&*value};
else
return std::nullopt;
}
std::optional<DefWorld*> getProfile_World(ResourceId id) {
assert(id < Profiles_World.size()*TableEntry<DefWorld>::ChunkSize);
auto& value = Profiles_World[id / TableEntry<DefWorld>::ChunkSize]->Entries[id % TableEntry<DefWorld>::ChunkSize];
if(value)
return {&*value};
else
return std::nullopt;
}
std::optional<DefPortal*> getProfile_Portal(ResourceId id) {
assert(id < Profiles_Portal.size()*TableEntry<DefPortal>::ChunkSize);
auto& value = Profiles_Portal[id / TableEntry<DefPortal>::ChunkSize]->Entries[id % TableEntry<DefPortal>::ChunkSize];
if(value)
return {&*value};
else
return std::nullopt;
}
std::optional<DefEntity*> getProfile_Entity(ResourceId id) {
assert(id < Profiles_Entity.size()*TableEntry<DefEntity>::ChunkSize);
auto& value = Profiles_Entity[id / TableEntry<DefEntity>::ChunkSize]->Entries[id % TableEntry<DefEntity>::ChunkSize];
if(value)
return {&*value};
else
return std::nullopt;
}
std::optional<DefItem*> getProfile_Item(ResourceId id) {
assert(id < Profiles_Item.size()*TableEntry<DefItem>::ChunkSize);
auto& value = Profiles_Item[id / TableEntry<DefItem>::ChunkSize]->Entries[id % TableEntry<DefItem>::ChunkSize];
if(value)
return {&*value};
else
return std::nullopt;
}
ResourceId getContentId(EnumDefContent type, const std::string& domain, const std::string& key) {
return getId(type, domain, key);
}
private:
TOS::Logger LOG = "Server>ContentManager";
AssetsPreloader& AM;
};
}

View File

@@ -44,6 +44,15 @@ namespace js = boost::json;
namespace LV::Server {
template <typename T, size_t N>
bool hasAnyBindings(const std::array<std::vector<T>, N>& data) {
for(const auto& list : data) {
if(!list.empty())
return true;
}
return false;
}
std::string ModInfo::dump() const {
js::object obj;
@@ -478,15 +487,14 @@ std::variant<std::vector<ModInfo>, std::vector<std::string>> resolveDepends(cons
GameServer::GameServer(asio::io_context &ioc, fs::path worldPath)
: AsyncObject(ioc),
Content(ioc)
Content(ioc), BackingAsyncLua(Content.CM)
{
init(worldPath);
}
GameServer::~GameServer() {
shutdown("on ~GameServer");
BackingChunkPressure.NeedShutdown = true;
BackingChunkPressure.Symaphore.notify_all();
BackingChunkPressure.NeedShutdown.store(true, std::memory_order_release);
BackingNoiseGenerator.NeedShutdown = true;
BackingAsyncLua.NeedShutdown = true;
@@ -502,27 +510,19 @@ GameServer::~GameServer() {
}
void GameServer::BackingChunkPressure_t::run(int id) {
// static thread_local int local_counter = -1;
int iteration = 0;
LOG.debug() << "Старт потока " << id;
try {
while(true) {
// local_counter++;
// LOG.debug() << "Ожидаю начала " << id << ' ' << local_counter;
{
std::unique_lock<std::mutex> lock(Mutex);
Symaphore.wait(lock, [&](){ return iteration != Iteration || NeedShutdown; });
if(NeedShutdown) {
if(NeedShutdown.load(std::memory_order_acquire)) {
CollectStart->arrive_and_drop();
LOG.debug() << "Завершение выполнения потока " << id;
break;
}
iteration = Iteration;
}
CollectStart->arrive_and_wait();
assert(RunCollect > 0);
assert(RunCompress > 0);
bool shutting = NeedShutdown.load(std::memory_order_acquire);
// Сбор данных
size_t pullSize = Threads.size();
@@ -537,6 +537,8 @@ void GameServer::BackingChunkPressure_t::run(int id) {
std::vector<std::pair<WorldId_t, std::vector<std::pair<Pos::GlobalRegion, Dump>>>> dump;
if(!shutting) {
try {
for(const auto& [worldId, world] : *Worlds) {
const auto &worldObj = *world;
std::vector<std::pair<Pos::GlobalRegion, Dump>> dumpWorld;
@@ -609,16 +611,17 @@ void GameServer::BackingChunkPressure_t::run(int id) {
dump.push_back({worldId, std::move(dumpWorld)});
}
}
// Синхронизация
// LOG.debug() << "Синхронизирую " << id << ' ' << local_counter;
{
std::unique_lock<std::mutex> lock(Mutex);
RunCollect -= 1;
Symaphore.notify_all();
} catch(const std::exception&) {
NeedShutdown.store(true, std::memory_order_release);
shutting = true;
}
}
CollectEnd->arrive_and_wait();
// Сжатие и отправка игрокам
if(!shutting) {
try {
for(auto& [worldId, world] : dump) {
for(auto& [regionPos, region] : world) {
for(auto& [chunkPos, chunk] : region.Voxels) {
@@ -672,22 +675,21 @@ void GameServer::BackingChunkPressure_t::run(int id) {
}
}
}
// Синхронизация
// LOG.debug() << "Конец " << id << ' ' << local_counter;
{
std::unique_lock<std::mutex> lock(Mutex);
RunCompress -= 1;
Symaphore.notify_all();
} catch(const std::exception&) {
NeedShutdown.store(true, std::memory_order_release);
shutting = true;
}
}
CompressEnd->arrive_and_wait();
if(shutting)
continue;
}
} catch(const std::exception& exc) {
std::unique_lock<std::mutex> lock(Mutex);
NeedShutdown = true;
NeedShutdown.store(true, std::memory_order_release);
LOG.error() << "Ошибка выполнения потока " << id << ":\n" << exc.what();
}
Symaphore.notify_all();
}
void GameServer::BackingNoiseGenerator_t::run(int id) {
@@ -769,17 +771,19 @@ void GameServer::BackingAsyncLua_t::run(int id) {
out.Voxels.clear();
out.Entityes.clear();
auto lru = CM.createLRU();
{
constexpr DefNodeId kNodeAir = 0;
constexpr DefNodeId kNodeGrass = 2;
constexpr uint8_t kMetaGrass = 1;
constexpr DefNodeId kNodeDirt = 3;
constexpr DefNodeId kNodeStone = 4;
constexpr DefNodeId kNodeWood = 1;
constexpr DefNodeId kNodeLeaves = 5;
constexpr DefNodeId kNodeLava = 7;
constexpr DefNodeId kNodeWater = 8;
constexpr DefNodeId kNodeFire = 9;
DefNodeId kNodeAir = 0;
DefNodeId kNodeGrass = lru.getIdNode("test", "grass");
uint8_t kMetaGrass = 1;
DefNodeId kNodeDirt = lru.getIdNode("test", "dirt");
DefNodeId kNodeStone = lru.getIdNode("test", "stone");
DefNodeId kNodeWood = lru.getIdNode("test", "log");
DefNodeId kNodeLeaves = lru.getIdNode("test", "leaves");
DefNodeId kNodeLava = lru.getIdNode("test", "lava");
DefNodeId kNodeWater = lru.getIdNode("test", "water");
DefNodeId kNodeFire = lru.getIdNode("test", "fire");
auto hash32 = [](uint32_t x) {
x ^= x >> 16;
@@ -914,9 +918,9 @@ void GameServer::BackingAsyncLua_t::run(int id) {
constexpr int kTestGlobalY = 64;
if(regionBase.y <= kTestGlobalY && (regionBase.y + 63) >= kTestGlobalY) {
int localY = kTestGlobalY - regionBase.y;
setNode(2, localY, 2, kNodeLava, 0, false);
setNode(4, localY, 2, kNodeWater, 0, false);
setNode(6, localY, 2, kNodeFire, 0, false);
setNode(7, localY, 2, kNodeLava, 0, false);
setNode(8, localY, 2, kNodeWater, 0, false);
setNode(9, localY, 2, kNodeFire, 0, false);
}
}
}
@@ -1154,150 +1158,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) {
@@ -1480,7 +1340,8 @@ void GameServer::init(fs::path worldPath) {
AssetsInit.Assets.push_back(mlt.LoadChain[index].Path / "assets");
}
Content.AM.applyResourceChange(Content.AM.reloadResources(AssetsInit));
auto capru = Content.AM.checkAndPrepareResourcesUpdate(AssetsInit);
Content.AM.applyResourcesUpdate(capru);
LOG.info() << "Пре Инициализация";
@@ -1489,7 +1350,7 @@ void GameServer::init(fs::path worldPath) {
// Content.CM.registerBase(EnumDefContent::Node, "core", "none", t);
Content.CM.registerBase(EnumDefContent::World, "test", "devel_world", t);
Content.CM.registerBase(EnumDefContent::Entity, "core", "player", t);
PlayerEntityDefId = Content.CM.getContentId(EnumDefContent::Entity, "core", "player");
// PlayerEntityDefId = Content.CM.getContentId(EnumDefContent::Entity, "core", "player");
}
initLuaPre();
@@ -1502,7 +1363,6 @@ void GameServer::init(fs::path worldPath) {
Content.CM.buildEndProfiles();
LOG.info() << "Инициализация";
initLua();
pushEvent("init");
@@ -1523,6 +1383,7 @@ void GameServer::init(fs::path worldPath) {
LOG.info() << "Загрузка существующих миров...";
BackingChunkPressure.Threads.resize(4);
BackingChunkPressure.Worlds = &Expanse.Worlds;
BackingChunkPressure.init(BackingChunkPressure.Threads.size());
for(size_t iter = 0; iter < BackingChunkPressure.Threads.size(); iter++) {
BackingChunkPressure.Threads[iter] = std::thread(&BackingChunkPressure_t::run, &BackingChunkPressure, iter);
}
@@ -1545,7 +1406,10 @@ void GameServer::prerun() {
auto useLock = UseLock.lock();
run();
} catch(const std::exception& exc) {
LOG.error() << "Исключение в GameServer::run: " << exc.what();
} catch(...) {
LOG.error() << "Неизвестное исключение в GameServer::run";
}
IsAlive = false;
@@ -1564,6 +1428,7 @@ void GameServer::run() {
while(true) {
((uint32_t&) Game.AfterStartTime) += (uint32_t) (CurrentTickDuration*256);
Game.Tick++;
std::chrono::steady_clock::time_point atTickStart = std::chrono::steady_clock::now();
@@ -1595,7 +1460,14 @@ void GameServer::run() {
stepConnections();
stepModInitializations();
IWorldSaveBackend::TickSyncInfo_Out dat1 = stepDatabaseSync();
IWorldSaveBackend::TickSyncInfo_Out dat1;
try {
dat1 = stepDatabaseSync();
} catch(const std::exception& exc) {
LOG.error() << "Ошибка stepDatabaseSync: " << exc.what();
} catch(...) {
LOG.error() << "Неизвестная ошибка stepDatabaseSync";
}
stepGeneratorAndLuaAsync(std::move(dat1));
stepPlayerProceed();
stepWorldPhysic();
@@ -1726,6 +1598,7 @@ void GameServer::requestModsReload() {
}
void GameServer::stepConnections() {
std::vector<std::shared_ptr<RemoteClient>> newClients;
// Подключить новых игроков
if(!External.NewConnectedPlayers.no_lock_readable().empty()) {
auto lock = External.NewConnectedPlayers.lock_write();
@@ -1733,11 +1606,28 @@ void GameServer::stepConnections() {
for(std::shared_ptr<RemoteClient>& client : *lock) {
co_spawn(client->run());
Game.RemoteClients.push_back(client);
newClients.push_back(client);
}
lock->clear();
}
if(!newClients.empty()) {
std::array<std::vector<ResourceId>, static_cast<size_t>(EnumAssets::MAX_ENUM)> lost{};
std::vector<Net::Packet> packets;
packets.push_back(RemoteClient::makePacket_informateAssets_DK(Content.AM.idToDK()));
packets.push_back(RemoteClient::makePacket_informateAssets_HH(Content.AM.collectHashBindings(), lost));
packets.append_range(RemoteClient::makePackets_informateDefContent_Full(Content.CM.getAllProfiles()));
for(const std::shared_ptr<RemoteClient>& client : newClients) {
if(!packets.empty()) {
auto copy = packets;
client->pushPackets(&copy);
}
}
}
BackingChunkPressure.endCollectChanges();
// Отключение игроков
@@ -1799,29 +1689,35 @@ void GameServer::reloadMods() {
{
// TODO: перезагрузка модов
Content.CM.buildEndProfiles();
ContentManager::Out_buildEndProfiles out = Content.CM.buildEndProfiles();
packetsToSend.append_range(RemoteClient::makePackets_informateDefContentUpdate(out));
}
LOG.info() << "Перезагрузка ассетов";
{
{
AssetsPreloader::Out_applyResourceChange applied
= Content.AM.applyResourceChange(Content.AM.reloadResources(AssetsInit));
AssetsManager::Out_checkAndPrepareResourcesUpdate capru = Content.AM.checkAndPrepareResourcesUpdate(AssetsInit);
AssetsManager::Out_applyResourcesUpdate aru = Content.AM.applyResourcesUpdate(capru);
if(!applied.NewOrChange.empty() || !applied.Lost.empty())
if(!capru.ResourceUpdates.empty() || !capru.LostLinks.empty())
packetsToSend.push_back(
RemoteClient::makePacket_informateAssets_HH(
applied.NewOrChange,
applied.Lost
aru.NewOrUpdates,
capru.LostLinks
)
);
}
{
AssetsPreloader::Out_bakeId baked = Content.AM.bakeIdTables();
if(!baked.IdToDK.empty())
packetsToSend.push_back(RemoteClient::makePacket_informateAssets_DK(baked.IdToDK));
std::array<
std::vector<AssetsManager::BindDomainKeyInfo>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
> baked = Content.AM.bake();
if(hasAnyBindings(baked)) {
packetsToSend.push_back(RemoteClient::makePacket_informateAssets_DK(baked));
}
}
}
@@ -1835,35 +1731,76 @@ void GameServer::reloadMods() {
IWorldSaveBackend::TickSyncInfo_Out GameServer::stepDatabaseSync() {
IWorldSaveBackend::TickSyncInfo_In toDB;
for(std::shared_ptr<RemoteClient>& remoteClient : Game.RemoteClients) {
constexpr uint32_t kRegionUnloadDelayTicks = 300;
constexpr uint8_t kUnloadHysteresisExtraRegions = 1;
const uint32_t nowTick = Game.Tick;
for(std::shared_ptr<RemoteClient>& remoteClient : Game.RemoteClients) {
assert(remoteClient);
// Пересчитать зоны наблюдения
// 1) Если игрок пересёк границу региона — пересчитываем области наблюдения.
// Вводим гистерезис: загрузка по "внутренней" границе, выгрузка по "внешней" (+1 регион).
if(remoteClient->CrossedRegion) {
remoteClient->CrossedRegion = false;
// Пересчёт зон наблюдения
std::vector<ContentViewCircle> newCVCs;
std::vector<ContentViewCircle> innerCVCs;
std::vector<ContentViewCircle> outerCVCs;
{
std::vector<std::tuple<WorldId_t, Pos::Object, uint8_t>> points = remoteClient->getViewPoints();
for(auto& [wId, pos, radius] : points) {
assert(radius < 5);
// Внутренняя область (на загрузку)
{
ContentViewCircle cvc;
cvc.WorldId = wId;
cvc.Pos = Pos::Object_t::asRegionsPos(pos);
cvc.Range = radius*radius;
cvc.Range = int16_t(radius * radius);
std::vector<ContentViewCircle> list = Expanse.accumulateContentViewCircles(cvc);
newCVCs.insert(newCVCs.end(), list.begin(), list.end());
innerCVCs.insert(innerCVCs.end(), list.begin(), list.end());
}
// Внешняя область (на удержание/выгрузку) = внутренняя + 1 регион
{
uint8_t outerRadius = radius + kUnloadHysteresisExtraRegions;
if(outerRadius > 5) outerRadius = 5;
ContentViewCircle cvc;
cvc.WorldId = wId;
cvc.Pos = Pos::Object_t::asRegionsPos(pos);
cvc.Range = int16_t(outerRadius * outerRadius);
std::vector<ContentViewCircle> list = Expanse.accumulateContentViewCircles(cvc);
outerCVCs.insert(outerCVCs.end(), list.begin(), list.end());
}
}
}
ContentViewInfo newCbg = Expanse_t::makeContentViewInfo(newCVCs);
ContentViewInfo viewInner = Expanse_t::makeContentViewInfo(innerCVCs);
ContentViewInfo viewOuter = Expanse_t::makeContentViewInfo(outerCVCs);
ContentViewInfo_Diff diff = newCbg.diffWith(remoteClient->ContentViewState);
if(!diff.WorldsNew.empty()) {
// Отменяем отложенную выгрузку для регионов, которые снова попали во внешнюю область
for(const auto& [worldId, regions] : viewOuter.Regions) {
auto itWorld = remoteClient->PendingRegionUnload.find(worldId);
if(itWorld == remoteClient->PendingRegionUnload.end())
continue;
for(const Pos::GlobalRegion& pos : regions) {
itWorld->second.erase(pos);
}
if(itWorld->second.empty())
remoteClient->PendingRegionUnload.erase(itWorld);
}
// Загрузка: только по внутренней границе
ContentViewInfo_Diff diffInner = viewInner.diffWith(remoteClient->ContentViewState);
if(!diffInner.WorldsNew.empty()) {
// Сообщить о новых мирах
for(const WorldId_t id : diff.WorldsNew) {
for(const WorldId_t id : diffInner.WorldsNew) {
auto iter = Expanse.Worlds.find(id);
assert(iter != Expanse.Worlds.end());
@@ -1871,16 +1808,18 @@ IWorldSaveBackend::TickSyncInfo_Out GameServer::stepDatabaseSync() {
}
}
remoteClient->ContentViewState = newCbg;
// Вычистка не наблюдаемых регионов
for(const auto& [worldId, regions] : diff.RegionsLost)
remoteClient->prepareRegionsRemove(worldId, regions);
// и миров
for(const WorldId_t worldId : diff.WorldsLost)
remoteClient->prepareWorldRemove(worldId);
// Подписываем игрока на наблюдение за регионами (внутренняя область)
for(const auto& [worldId, regions] : diffInner.RegionsNew) {
// Добавляем в состояние клиента (слиянием, т.к. там могут быть регионы на удержании)
{
auto& cur = remoteClient->ContentViewState.Regions[worldId];
std::vector<Pos::GlobalRegion> merged;
merged.reserve(cur.size() + regions.size());
std::merge(cur.begin(), cur.end(), regions.begin(), regions.end(), std::back_inserter(merged));
merged.erase(std::unique(merged.begin(), merged.end()), merged.end());
cur = std::move(merged);
}
// Подписываем игрока на наблюдение за регионами
for(const auto& [worldId, regions] : diff.RegionsNew) {
auto iterWorld = Expanse.Worlds.find(worldId);
assert(iterWorld != Expanse.Worlds.end());
@@ -1892,15 +1831,101 @@ IWorldSaveBackend::TickSyncInfo_Out GameServer::stepDatabaseSync() {
}
}
// Отписываем то, что игрок больше не наблюдает
for(const auto& [worldId, regions] : diff.RegionsLost) {
auto iterWorld = Expanse.Worlds.find(worldId);
assert(iterWorld != Expanse.Worlds.end());
// Кандидаты на выгрузку: то, что есть у клиента, но не попадает во внешнюю область (гистерезис)
for(const auto& [worldId, curRegions] : remoteClient->ContentViewState.Regions) {
std::vector<Pos::GlobalRegion> outer;
auto itOuter = viewOuter.Regions.find(worldId);
if(itOuter != viewOuter.Regions.end())
outer = itOuter->second;
iterWorld->second->onRemoteClient_RegionsLost(worldId, remoteClient, regions);
std::vector<Pos::GlobalRegion> toDelay;
toDelay.reserve(curRegions.size());
if(outer.empty()) {
toDelay = curRegions;
} else {
std::set_difference(
curRegions.begin(), curRegions.end(),
outer.begin(), outer.end(),
std::back_inserter(toDelay)
);
}
if(!toDelay.empty()) {
auto& pending = remoteClient->PendingRegionUnload[worldId];
for(const Pos::GlobalRegion& pos : toDelay) {
// если уже ждёт выгрузки — не трогаем
if(pending.find(pos) == pending.end())
pending[pos] = nowTick + kRegionUnloadDelayTicks;
}
}
}
}
// 2) Отложенная выгрузка: если истекла задержка — реально удаляем регион из зоны видимости
if(!remoteClient->PendingRegionUnload.empty()) {
std::unordered_map<WorldId_t, std::vector<Pos::GlobalRegion>> expiredByWorld;
for(auto itWorld = remoteClient->PendingRegionUnload.begin(); itWorld != remoteClient->PendingRegionUnload.end(); ) {
const WorldId_t worldId = itWorld->first;
auto& regMap = itWorld->second;
std::vector<Pos::GlobalRegion> expired;
for(auto itReg = regMap.begin(); itReg != regMap.end(); ) {
if(itReg->second <= nowTick) {
expired.push_back(itReg->first);
itReg = regMap.erase(itReg);
} else {
++itReg;
}
}
if(!expired.empty()) {
std::sort(expired.begin(), expired.end());
expiredByWorld[worldId] = std::move(expired);
}
if(regMap.empty())
itWorld = remoteClient->PendingRegionUnload.erase(itWorld);
else
++itWorld;
}
// Применяем выгрузку: отписка + сообщение клиенту + актуализация ContentViewState
for(auto& [worldId, expired] : expiredByWorld) {
// Удаляем регионы из состояния клиента
auto itCur = remoteClient->ContentViewState.Regions.find(worldId);
if(itCur != remoteClient->ContentViewState.Regions.end()) {
std::vector<Pos::GlobalRegion> kept;
kept.reserve(itCur->second.size());
std::set_difference(
itCur->second.begin(), itCur->second.end(),
expired.begin(), expired.end(),
std::back_inserter(kept)
);
itCur->second = std::move(kept);
}
// Сообщаем клиенту и мирам
remoteClient->prepareRegionsRemove(worldId, expired);
auto iterWorld = Expanse.Worlds.find(worldId);
if(iterWorld != Expanse.Worlds.end()) {
iterWorld->second->onRemoteClient_RegionsLost(worldId, remoteClient, expired);
}
// Если в мире больше нет наблюдаемых регионов — удалить мир у клиента
auto itStateWorld = remoteClient->ContentViewState.Regions.find(worldId);
if(itStateWorld != remoteClient->ContentViewState.Regions.end() && itStateWorld->second.empty()) {
remoteClient->ContentViewState.Regions.erase(itStateWorld);
remoteClient->prepareWorldRemove(worldId);
}
}
}
}
for(auto& [worldId, regions] : toDB.Load) {
std::sort(regions.begin(), regions.end());
@@ -1911,7 +1936,7 @@ IWorldSaveBackend::TickSyncInfo_Out GameServer::stepDatabaseSync() {
// Обзавелись списком на прогрузку регионов
// Теперь узнаем что нужно сохранить и что из регионов было выгружено
for(auto& [worldId, world] : Expanse.Worlds) {
World::SaveUnloadInfo info = world->onStepDatabaseSync();
World::SaveUnloadInfo info = world->onStepDatabaseSync(Content.CM, CurrentTickDuration);
if(!info.ToSave.empty()) {
auto &obj = toDB.ToSave[worldId];
@@ -1925,7 +1950,18 @@ IWorldSaveBackend::TickSyncInfo_Out GameServer::stepDatabaseSync() {
}
// Синхронизируемся с базой
const auto loadFallback = toDB.Load;
try {
return SaveBackend.World->tickSync(std::move(toDB));
} catch(const std::exception& exc) {
LOG.error() << "Ошибка tickSync: " << exc.what();
} catch(...) {
LOG.error() << "Неизвестная ошибка tickSync";
}
IWorldSaveBackend::TickSyncInfo_Out out;
out.NotExisten = loadFallback;
return out;
}
void GameServer::stepGeneratorAndLuaAsync(IWorldSaveBackend::TickSyncInfo_Out db) {
@@ -1965,10 +2001,62 @@ void GameServer::stepGeneratorAndLuaAsync(IWorldSaveBackend::TickSyncInfo_Out db
// Обработка идентификаторов на стороне луа
// Трансформация полученных ключей в профили сервера
auto buildRemap = [&](EnumDefContent type, const std::vector<std::string>& idToKey) {
std::vector<ResourceId> remap;
remap.resize(idToKey.size());
for(size_t i = 0; i < idToKey.size(); ++i) {
if(idToKey[i].empty()) {
remap[i] = static_cast<ResourceId>(i);
continue;
}
auto [domain, key] = parseDomainKey(std::string_view(idToKey[i]));
remap[i] = Content.CM.getId(type, domain, key);
}
return remap;
};
auto remapRegion = [&](DB_Region_Out& region) {
std::vector<ResourceId> voxelRemap;
std::vector<ResourceId> nodeRemap;
std::vector<ResourceId> entityRemap;
if(!region.VoxelIdToKey.empty())
voxelRemap = buildRemap(EnumDefContent::Voxel, region.VoxelIdToKey);
if(!region.NodeIdToKey.empty())
nodeRemap = buildRemap(EnumDefContent::Node, region.NodeIdToKey);
if(!region.EntityToKey.empty())
entityRemap = buildRemap(EnumDefContent::Entity, region.EntityToKey);
if(!voxelRemap.empty()) {
for(auto& voxel : region.Voxels) {
if(voxel.VoxelId < voxelRemap.size())
voxel.VoxelId = voxelRemap[voxel.VoxelId];
}
}
if(!nodeRemap.empty()) {
for(auto& chunk : region.Nodes) {
for(auto& node : chunk) {
if(node.NodeId < nodeRemap.size())
node.NodeId = nodeRemap[node.NodeId];
}
}
}
if(!entityRemap.empty()) {
for(auto& entity : region.Entityes) {
auto id = entity.getDefId();
if(id < entityRemap.size())
entity.setDefId(entityRemap[id]);
}
}
};
for(auto& [WorldId_t, regions] : db.LoadedRegions) {
auto &list = toLoadRegions[WorldId_t];
for(auto& [pos, region] : regions) {
remapRegion(region);
auto &obj = list.emplace_back(pos, World::RegionIn()).second;
convertRegionVoxelsToChunks(region.Voxels, obj.Voxels);
obj.Nodes = std::move(region.Nodes);
@@ -2579,6 +2667,7 @@ void GameServer::stepSyncContent() {
n.NodeId = 4;
n.Meta = uint8_t((int(nPos.x) + int(nPos.y) + int(nPos.z)) & 0x3);
region->second->IsChunkChanged_Nodes |= 1ull << cPos.pack();
region->second->IsChanged = true;
}
}
@@ -2596,6 +2685,7 @@ void GameServer::stepSyncContent() {
n.NodeId = 0;
n.Meta = 0;
region->second->IsChunkChanged_Nodes |= 1ull << cPos.pack();
region->second->IsChanged = true;
}
}
}
@@ -2611,29 +2701,32 @@ void GameServer::stepSyncContent() {
std::vector<Net::Packet> packetsToAll;
{
AssetsPreloader::Out_bakeId baked = Content.AM.bakeIdTables();
if(!baked.IdToDK.empty()) {
packetsToAll.push_back(RemoteClient::makePacket_informateAssets_DK(baked.IdToDK));
std::array<
std::vector<AssetsManager::BindDomainKeyInfo>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
> baked = Content.AM.bake();
if(hasAnyBindings(baked)) {
packetsToAll.push_back(RemoteClient::makePacket_informateAssets_DK(baked));
}
}
// Оповещаем о двоичных ресурсах по запросу
std::vector<AssetBinaryInfo> binaryResources;
for(const Hash_t& hash : full.Hashes) {
std::optional<
std::tuple<EnumAssets, uint32_t, const AssetsPreloader::MediaResource*>
> result = Content.AM.getResource(hash);
if(!result)
continue;
auto& [type, id, media] = *result;
Resource resource(*media->Resource);
binaryResources.push_back(AssetBinaryInfo{
.Data = std::move(resource),
.Hash = media->Hash
});
auto assetTypeName = [](EnumAssets type) {
switch(type) {
case EnumAssets::Nodestate: return "nodestate";
case EnumAssets::Model: return "model";
case EnumAssets::Texture: return "texture";
case EnumAssets::Particle: return "particle";
case EnumAssets::Animation: return "animation";
case EnumAssets::Sound: return "sound";
case EnumAssets::Font: return "font";
default: return "unknown";
}
};
std::vector<std::tuple<ResourceFile::Hash_t, std::shared_ptr<const std::u8string>>> binaryResources
= Content.AM.getResources(full.Hashes);
for(std::shared_ptr<RemoteClient>& remoteClient : Game.RemoteClients) {
if(!binaryResources.empty())

View File

@@ -6,15 +6,14 @@
#include <Common/Net.hpp>
#include <Common/Lockable.hpp>
#include <atomic>
#include <barrier>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/io_context.hpp>
#include <condition_variable>
#include <filesystem>
#include "Common/Abstract.hpp"
#include "RemoteClient.hpp"
#include "Server/Abstract.hpp"
#include <TOSLib.hpp>
#include <functional>
#include <memory>
#include <queue>
#include <set>
@@ -25,6 +24,7 @@
#include "WorldDefManager.hpp"
#include "ContentManager.hpp"
#include "AssetsManager.hpp"
#include "World.hpp"
#include "SaveBackend.hpp"
@@ -73,7 +73,7 @@ class GameServer : public AsyncObject {
struct ContentObj {
public:
AssetsPreloader AM;
AssetsManager AM;
ContentManager CM;
// Если контент был перерегистрирован (исключая двоичные ресурсы), то профили будут повторно разосланы
@@ -88,6 +88,8 @@ class GameServer : public AsyncObject {
struct {
std::vector<std::shared_ptr<RemoteClient>> RemoteClients;
ServerTime AfterStartTime = {0, 0};
// Счётчик тактов (увеличивается на 1 каждый тик в GameServer::run)
uint32_t Tick = 0;
} Game;
@@ -156,38 +158,56 @@ class GameServer : public AsyncObject {
*/
struct BackingChunkPressure_t {
TOS::Logger LOG = "BackingChunkPressure";
volatile bool NeedShutdown = false;
std::atomic<bool> NeedShutdown = false;
std::vector<std::thread> Threads;
std::mutex Mutex;
volatile int RunCollect = 0, RunCompress = 0, Iteration = 0;
std::condition_variable Symaphore;
std::unique_ptr<std::barrier<>> CollectStart;
std::unique_ptr<std::barrier<>> CollectEnd;
std::unique_ptr<std::barrier<>> CompressEnd;
std::unordered_map<WorldId_t, std::unique_ptr<World>> *Worlds;
bool HasStarted = false;
void init(size_t threadCount) {
if(threadCount == 0)
return;
const ptrdiff_t participants = static_cast<ptrdiff_t>(threadCount + 1);
CollectStart = std::make_unique<std::barrier<>>(participants);
CollectEnd = std::make_unique<std::barrier<>>(participants);
CompressEnd = std::make_unique<std::barrier<>>(participants);
}
void startCollectChanges() {
std::lock_guard<std::mutex> lock(Mutex);
RunCollect = Threads.size();
RunCompress = Threads.size();
Iteration += 1;
assert(RunCollect != 0);
Symaphore.notify_all();
if(!CollectStart)
return;
HasStarted = true;
CollectStart->arrive_and_wait();
}
void endCollectChanges() {
std::unique_lock<std::mutex> lock(Mutex);
Symaphore.wait(lock, [&](){ return RunCollect == 0 || NeedShutdown; });
if(!CollectEnd)
return;
if(!HasStarted)
return;
CollectEnd->arrive_and_wait();
}
void endWithResults() {
std::unique_lock<std::mutex> lock(Mutex);
Symaphore.wait(lock, [&](){ return RunCompress == 0 || NeedShutdown; });
if(!CompressEnd)
return;
if(!HasStarted)
return;
CompressEnd->arrive_and_wait();
}
void stop() {
{
std::unique_lock<std::mutex> lock(Mutex);
NeedShutdown = true;
Symaphore.notify_all();
}
NeedShutdown.store(true, std::memory_order_release);
if(CollectStart)
CollectStart->arrive_and_drop();
if(CollectEnd)
CollectEnd->arrive_and_drop();
if(CompressEnd)
CompressEnd->arrive_and_drop();
for(std::thread& thread : Threads)
thread.join();
@@ -234,7 +254,7 @@ class GameServer : public AsyncObject {
auto lock = Output.lock();
std::vector<std::pair<NoiseKey, std::array<float, 64*64*64>>> out = std::move(*lock);
lock->reserve(8000);
lock->reserve(25);
return std::move(out);
}
@@ -249,6 +269,13 @@ class GameServer : public AsyncObject {
std::vector<std::thread> Threads;
TOS::SpinlockObject<std::queue<std::pair<BackingNoiseGenerator_t::NoiseKey, std::array<float, 64*64*64>>>> NoiseIn;
TOS::SpinlockObject<std::vector<std::pair<BackingNoiseGenerator_t::NoiseKey, World::RegionIn>>> RegionOut;
ContentManager &CM;
BackingAsyncLua_t(ContentManager& cm)
: CM(cm)
{
}
void stop() {
NeedShutdown = true;
@@ -294,9 +321,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();

View File

@@ -11,13 +11,14 @@
#include <boost/system/system_error.hpp>
#include <exception>
#include <Common/Packets.hpp>
#include <unordered_set>
namespace LV::Server {
Net::Packet RemoteClient::makePacket_informateAssets_DK(
const std::array<
std::vector<AssetsPreloader::BindDomainKeyInfo>,
std::vector<AssetsManager::BindDomainKeyInfo>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
>& dkVector
) {
@@ -46,7 +47,7 @@ Net::Packet RemoteClient::makePacket_informateAssets_DK(
// Запись связок домен+ключ
for(size_t type = 0; type < static_cast<size_t>(EnumAssets::MAX_ENUM); type++) {
const std::vector<AssetsPreloader::BindDomainKeyInfo>& binds = dkVector[type];
const std::vector<AssetsManager::BindDomainKeyInfo>& binds = dkVector[type];
pack << uint32_t(binds.size());
for(const auto& bind : binds) {
@@ -59,6 +60,7 @@ Net::Packet RemoteClient::makePacket_informateAssets_DK(
// Сжатие
std::u8string compressed = compressLinear(pack.complite());
pack.clear();
pack << uint8_t(ToClient::AssetsBindDK) << (const std::string&) compressed;
return pack;
@@ -66,7 +68,7 @@ Net::Packet RemoteClient::makePacket_informateAssets_DK(
Net::Packet RemoteClient::makePacket_informateAssets_HH(
const std::array<
std::vector<AssetsPreloader::BindHashHeaderInfo>,
std::vector<AssetsManager::BindHashHeaderInfo>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
>& hhVector,
const std::array<
@@ -92,24 +94,122 @@ Net::Packet RemoteClient::makePacket_informateAssets_HH(
return pack;
}
std::vector<Net::Packet> RemoteClient::makePackets_sendDefContentUpdate(
std::array<
std::vector<
std::pair<
ResourceId, // Идентификатор профиля
std::u8string // Двоичный формат профиля
>
>,
static_cast<size_t>(EnumDefContent::MAX_ENUM)
> newOrUpdate, // Новые или изменённые
std::array<
std::vector<ResourceId>,
static_cast<size_t>(EnumDefContent::MAX_ENUM)
> lost, // Потерянные профили
std::array<
std::vector<std::pair<std::string, std::string>>,
static_cast<size_t>(EnumDefContent::MAX_ENUM)
> idToDK // Новые привязки
std::vector<Net::Packet> RemoteClient::makePackets_informateDefContent_Full(
const ContentManager::Out_getAllProfiles& profiles
) {
std::vector<Net::Packet> packets;
Net::Packet pack;
auto check = [&](size_t needSize) {
if(pack.size()+needSize > 65500) {
packets.emplace_back(std::move(pack));
pack.clear();
}
};
pack << (uint8_t) ToClient::DefinitionsFull;
{
pack << (uint32_t) profiles.ProfilesIds_Voxel.size();
for(uint32_t id : profiles.ProfilesIds_Voxel) {
check(4);
pack << id;
}
for(const auto& profile : profiles.Profiles_Voxel) {
std::u8string data = profile->dumpToClient();
check(data.size());
pack << std::string_view((const char*) data.data(), data.size());
}
}
{
pack << (uint32_t) profiles.ProfilesIds_Node.size();
for(uint32_t id : profiles.ProfilesIds_Node) {
check(4);
pack << id;
}
for(const auto& profile : profiles.Profiles_Node) {
std::u8string data = profile->dumpToClient();
check(data.size());
pack << std::string_view((const char*) data.data(), data.size());
}
}
{
pack << (uint32_t) profiles.ProfilesIds_World.size();
for(uint32_t id : profiles.ProfilesIds_World) {
check(4);
pack << id;
}
for(const auto& profile : profiles.Profiles_World) {
std::u8string data = profile->dumpToClient();
check(data.size());
pack << std::string_view((const char*) data.data(), data.size());
}
}
{
pack << (uint32_t) profiles.ProfilesIds_Portal.size();
for(uint32_t id : profiles.ProfilesIds_Portal) {
check(4);
pack << id;
}
for(const auto& profile : profiles.Profiles_Portal) {
std::u8string data = profile->dumpToClient();
check(data.size());
pack << std::string_view((const char*) data.data(), data.size());
}
}
{
pack << (uint32_t) profiles.ProfilesIds_Entity.size();
for(uint32_t id : profiles.ProfilesIds_Entity) {
check(4);
pack << id;
}
for(const auto& profile : profiles.Profiles_Entity) {
std::u8string data = profile->dumpToClient();
check(data.size());
pack << std::string_view((const char*) data.data(), data.size());
}
}
{
pack << (uint32_t) profiles.ProfilesIds_Item.size();
for(uint32_t id : profiles.ProfilesIds_Item) {
check(4);
pack << id;
}
for(const auto& profile : profiles.Profiles_Item) {
std::u8string data = profile->dumpToClient();
check(data.size());
pack << std::string_view((const char*) data.data(), data.size());
}
}
for(size_t type = 0; type < static_cast<size_t>(EnumDefContent::MAX_ENUM); type++) {
pack << uint32_t(profiles.IdToDK[type].size());
for(const auto& [domain, key] : profiles.IdToDK[type]) {
check(domain.size() + key.size() + 8);
pack << domain << key;
}
}
if(pack.size())
packets.emplace_back(std::move(pack));
return packets;
}
std::vector<Net::Packet> RemoteClient::makePackets_informateDefContentUpdate(
const ContentManager::Out_buildEndProfiles& profiles
) {
std::vector<Net::Packet> packets;
Net::Packet pack;
@@ -122,35 +222,88 @@ std::vector<Net::Packet> RemoteClient::makePackets_sendDefContentUpdate(
};
pack << (uint8_t) ToClient::DefinitionsUpdate;
pack << uint32_t(newOrUpdate.size());
for(size_t type = 0; type < static_cast<size_t>(EnumDefContent::MAX_ENUM); type++) {
pack << uint32_t(newOrUpdate[type].size());
for(const auto& [id, data] : newOrUpdate[type]) {
{
pack << uint32_t(profiles.ChangedProfiles_Voxel.size());
for(const auto& [id, profile] : profiles.ChangedProfiles_Voxel) {
pack << id;
std::u8string data = profile->dumpToClient();
check(data.size());
pack << id << (const std::string&) data;
pack << std::string_view((const char*) data.data(), data.size());
}
}
pack << uint32_t(lost.size());
{
pack << uint32_t(profiles.ChangedProfiles_Node.size());
for(const auto& [id, profile] : profiles.ChangedProfiles_Node) {
pack << id;
std::u8string data = profile->dumpToClient();
check(data.size());
pack << std::string_view((const char*) data.data(), data.size());
}
}
{
pack << uint32_t(profiles.ChangedProfiles_World.size());
for(const auto& [id, profile] : profiles.ChangedProfiles_World) {
pack << id;
std::u8string data = profile->dumpToClient();
check(data.size());
pack << std::string_view((const char*) data.data(), data.size());
}
}
{
pack << uint32_t(profiles.ChangedProfiles_Portal.size());
for(const auto& [id, profile] : profiles.ChangedProfiles_Portal) {
pack << id;
std::u8string data = profile->dumpToClient();
check(data.size());
pack << std::string_view((const char*) data.data(), data.size());
}
}
{
pack << uint32_t(profiles.ChangedProfiles_Entity.size());
for(const auto& [id, profile] : profiles.ChangedProfiles_Entity) {
pack << id;
std::u8string data = profile->dumpToClient();
check(data.size());
pack << std::string_view((const char*) data.data(), data.size());
}
}
{
pack << uint32_t(profiles.ChangedProfiles_Item.size());
for(const auto& [id, profile] : profiles.ChangedProfiles_Item) {
pack << id;
std::u8string data = profile->dumpToClient();
check(data.size());
pack << std::string_view((const char*) data.data(), data.size());
}
}
{
for(size_t type = 0; type < static_cast<size_t>(EnumDefContent::MAX_ENUM); type++) {
pack << uint32_t(lost[type].size());
pack << uint32_t(profiles.LostProfiles[type].size());
for(ResourceId id : lost[type]) {
check(4);
for(const ResourceId id : profiles.LostProfiles[type]) {
check(sizeof(ResourceId));
pack << id;
}
}
}
pack << uint32_t(idToDK.size());
{
for(size_t type = 0; type < static_cast<size_t>(EnumDefContent::MAX_ENUM); type++) {
pack << uint32_t(idToDK[type].size());
pack << uint32_t(profiles.IdToDK[type].size());
for(const auto& [domain, key] : idToDK[type]) {
for(const auto& [domain, key] : profiles.IdToDK[type]) {
check(domain.size() + key.size() + 8);
pack << key << domain;
}
}
}
if(pack.size())
packets.emplace_back(std::move(pack));
@@ -258,6 +411,15 @@ void RemoteClient::shutdown(EnumDisconnect type, const std::string reason) {
void RemoteClient::NetworkAndResource_t::prepareRegionsRemove(WorldId_t worldId, std::vector<Pos::GlobalRegion> regionPoses)
{
auto worldIter = ChunksToSend.find(worldId);
if(worldIter != ChunksToSend.end()) {
for(const Pos::GlobalRegion &regionPos : regionPoses)
worldIter->second.erase(regionPos);
if(worldIter->second.empty())
ChunksToSend.erase(worldIter);
}
for(Pos::GlobalRegion regionPos : regionPoses) {
checkPacketBorder(16);
NextPacket << (uint8_t) ToClient::RemoveRegion
@@ -265,6 +427,60 @@ void RemoteClient::NetworkAndResource_t::prepareRegionsRemove(WorldId_t worldId,
}
}
void RemoteClient::NetworkAndResource_t::flushChunksToPackets()
{
for(auto &worldPair : ChunksToSend) {
const WorldId_t worldId = worldPair.first;
auto &regions = worldPair.second;
for(auto &regionPair : regions) {
const Pos::GlobalRegion regionPos = regionPair.first;
auto &voxels = regionPair.second.first;
auto &nodes = regionPair.second.second;
for(auto &chunkPair : voxels) {
const Pos::bvec4u chunkPos = chunkPair.first;
const std::u8string &compressed = chunkPair.second;
Pos::GlobalChunk globalPos = (Pos::GlobalChunk) regionPos;
globalPos <<= 2;
globalPos += (Pos::GlobalChunk) chunkPos;
const size_t size = 1 + sizeof(WorldId_t)
+ sizeof(Pos::GlobalChunk::Pack)
+ sizeof(uint32_t)
+ compressed.size();
checkPacketBorder(static_cast<uint16_t>(std::min<size_t>(size, 64000)));
NextPacket << (uint8_t) ToClient::ChunkVoxels
<< worldId << globalPos.pack() << uint32_t(compressed.size());
NextPacket.write((const std::byte*) compressed.data(), compressed.size());
}
for(auto &chunkPair : nodes) {
const Pos::bvec4u chunkPos = chunkPair.first;
const std::u8string &compressed = chunkPair.second;
Pos::GlobalChunk globalPos = (Pos::GlobalChunk) regionPos;
globalPos <<= 2;
globalPos += (Pos::GlobalChunk) chunkPos;
const size_t size = 1 + sizeof(WorldId_t)
+ sizeof(Pos::GlobalChunk::Pack)
+ sizeof(uint32_t)
+ compressed.size();
checkPacketBorder(static_cast<uint16_t>(std::min<size_t>(size, 64000)));
NextPacket << (uint8_t) ToClient::ChunkNodes
<< worldId << globalPos.pack() << uint32_t(compressed.size());
NextPacket.write((const std::byte*) compressed.data(), compressed.size());
}
}
}
ChunksToSend.clear();
}
void RemoteClient::NetworkAndResource_t::prepareEntitiesUpdate(const std::vector<std::tuple<ServerEntityId_t, const Entity*>>& entities)
{
// for(auto& [entityId, entity] : entities) {
@@ -325,10 +541,8 @@ void RemoteClient::NetworkAndResource_t::prepareWorldRemove(WorldId_t worldId)
void RemoteClient::prepareCameraSetEntity(ServerEntityId_t entityId) {
auto lock = NetworkAndResource.lock();
ClientEntityId_t cId = lock->ReMapEntities.toClient(entityId);
lock->checkPacketBorder(8);
lock->NextPacket << (uint8_t) ToClient::TestLinkCameraToEntity
<< cId;
lock->checkPacketBorder(4);
lock->NextPacket << (uint8_t) ToClient::TestLinkCameraToEntity;
}
ResourceRequest RemoteClient::pushPreparedPackets() {
@@ -337,6 +551,7 @@ ResourceRequest RemoteClient::pushPreparedPackets() {
{
auto lock = NetworkAndResource.lock();
lock->flushChunksToPackets();
if(lock->NextPacket.size())
lock->SimplePackets.push_back(std::move(lock->NextPacket));
@@ -367,21 +582,21 @@ ResourceRequest RemoteClient::pushPreparedPackets() {
return std::move(nextRequest);
}
void RemoteClient::informateBinaryAssets(const std::vector<AssetBinaryInfo>& resources)
void RemoteClient::informateBinaryAssets(const std::vector<std::tuple<ResourceFile::Hash_t, std::shared_ptr<const std::u8string>>>& resources)
{
for(const AssetBinaryInfo& resource : resources) {
for(const auto& [hash, resource] : resources) {
auto lock = NetworkAndResource.lock();
auto iter = std::find(lock->ClientRequested.begin(), lock->ClientRequested.end(), resource.Hash);
auto iter = std::find(lock->ClientRequested.begin(), lock->ClientRequested.end(), hash);
if(iter == lock->ClientRequested.end())
continue;
lock->ClientRequested.erase(iter);
lock.unlock();
auto it = std::lower_bound(AssetsInWork.OnClient.begin(), AssetsInWork.OnClient.end(), resource.Hash);
if(it == AssetsInWork.OnClient.end() || *it != resource.Hash) {
AssetsInWork.OnClient.insert(it, resource.Hash);
AssetsInWork.ToSend.emplace_back(resource.Data, 0);
auto it = std::lower_bound(AssetsInWork.OnClient.begin(), AssetsInWork.OnClient.end(), hash);
if(it == AssetsInWork.OnClient.end() || *it != hash) {
AssetsInWork.OnClient.insert(it, hash);
AssetsInWork.ToSend.emplace_back(hash, resource, 0);
} else {
LOG.warn() << "Клиент повторно запросил имеющийся у него ресурс";
}
@@ -472,6 +687,13 @@ coro<> RemoteClient::rP_System(Net::AsyncSocket &sock) {
LOG.debug() << "ResourceRequest count=" << count;
}
}
for(const auto& hash : hashes) {
LOG.debug() << "Client requested hash="
<< int(hash[0]) << '.'
<< int(hash[1]) << '.'
<< int(hash[2]) << '.'
<< int(hash[3]);
}
co_return;
}
case ToServer::L2System::ReloadMods:
@@ -541,36 +763,36 @@ void RemoteClient::onUpdate() {
bool hasFullSended = false;
for(auto& [res, sended] : toSend) {
for(auto& [hash, res, sended] : toSend) {
if(sended == 0) {
// Оповещаем о начале отправки ресурса
const size_t initSize = 1 + 1 + 4 + 32 + 4 + 1;
if(p.size() + initSize > kMaxAssetPacketSize)
flushAssetsPacket();
p << (uint8_t) ToClient::AssetsInitSend
<< uint32_t(res.size());
p.write((const std::byte*) res.hash().data(), 32);
<< uint32_t(res->size());
p.write((const std::byte*) hash.data(), 32);
}
// Отправляем чанк
size_t willSend = std::min(chunkSize, res.size()-sended);
size_t willSend = std::min(chunkSize, res->size()-sended);
const size_t chunkMsgSize = 1 + 1 + 32 + 4 + willSend;
if(p.size() + chunkMsgSize > kMaxAssetPacketSize)
flushAssetsPacket();
p << (uint8_t) ToClient::AssetsNextSend;
p.write((const std::byte*) res.hash().data(), 32);
p.write((const std::byte*) hash.data(), 32);
p << uint32_t(willSend);
p.write(res.data() + sended, willSend);
p.write((const std::byte*) res->data() + sended, willSend);
sended += willSend;
if(sended == res.size()) {
if(sended == res->size()) {
hasFullSended = true;
}
}
if(hasFullSended) {
for(ssize_t iter = toSend.size()-1; iter >= 0; iter--) {
if(std::get<0>(toSend[iter]).size() == std::get<1>(toSend[iter])) {
if(std::get<1>(toSend[iter])->size() == std::get<2>(toSend[iter])) {
toSend.erase(toSend.begin()+iter);
}
}

View File

@@ -5,6 +5,7 @@
#include <Common/Net.hpp>
#include "Abstract.hpp"
#include "Common/Packets.hpp"
#include "Server/AssetsManager.hpp"
#include "Server/ContentManager.hpp"
#include <Common/Abstract.hpp>
#include <bitset>
@@ -233,6 +234,8 @@ class RemoteClient {
ChunksToSend[worldId][regionPos].second[chunkPos] = compressed_nodes;
}
void flushChunksToPackets();
void prepareEntitiesRemove(const std::vector<ServerEntityId_t>& entityId);
void prepareRegionsRemove(WorldId_t worldId, std::vector<Pos::GlobalRegion> regionPoses);
void prepareWorldRemove(WorldId_t worldId);
@@ -254,7 +257,7 @@ class RemoteClient {
std::vector<Hash_t> OnClient;
// Отправляемые на клиент ресурсы
// Ресурс, количество отправленных байт
std::vector<std::tuple<Resource, size_t>> ToSend;
std::vector<std::tuple<ResourceFile::Hash_t, std::shared_ptr<const std::u8string>, size_t>> ToSend;
// Пакет с ресурсами
std::vector<Net::Packet> AssetsPackets;
Net::Packet AssetsPacket;
@@ -274,6 +277,10 @@ public:
ContentViewInfo ContentViewState;
// Если игрок пересекал границы региона (для перерасчёта ContentViewState)
bool CrossedRegion = true;
// Отложенная выгрузка регионов (гистерезис + задержка)
// worldId -> (regionPos -> tick_deadline)
std::unordered_map<WorldId_t, std::unordered_map<Pos::GlobalRegion, uint32_t>> PendingRegionUnload;
std::queue<Pos::GlobalNode> Build, Break;
std::optional<ServerEntityId_t> PlayerEntity;
@@ -359,7 +366,7 @@ public:
// Создаёт пакет для всех игроков с оповещением о новых идентификаторах (id -> domain+key)
static Net::Packet makePacket_informateAssets_DK(
const std::array<
std::vector<AssetsPreloader::BindDomainKeyInfo>,
std::vector<AssetsManager::BindDomainKeyInfo>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
>& dkVector
);
@@ -367,7 +374,7 @@ public:
// Создаёт пакет для всех игроков с оповещением об изменении файлов ресурсов (id -> hash+header)
static Net::Packet makePacket_informateAssets_HH(
const std::array<
std::vector<AssetsPreloader::BindHashHeaderInfo>,
std::vector<AssetsManager::BindHashHeaderInfo>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
>& hhVector,
const std::array<
@@ -378,28 +385,18 @@ public:
// Оповещение о двоичных ресурсах (стриминг по запросу)
void informateBinaryAssets(
const std::vector<AssetBinaryInfo>& resources
const std::vector<std::tuple<ResourceFile::Hash_t, std::shared_ptr<const std::u8string>>>& resources
);
// Создаёт пакет со всеми данными об игровых профилях
static std::vector<Net::Packet> makePackets_informateDefContent_Full(
const ContentManager::Out_getAllProfiles& profiles
);
// Создаёт пакет об обновлении игровых профилей
static std::vector<Net::Packet> makePackets_sendDefContentUpdate(
std::array<
std::vector<
std::pair<
ResourceId, // Идентификатор профиля
std::u8string // Двоичный формат профиля
>
>,
static_cast<size_t>(EnumDefContent::MAX_ENUM)
> newOrUpdate, // Новые или изменённые
std::array<
std::vector<ResourceId>,
static_cast<size_t>(EnumDefContent::MAX_ENUM)
> lost, // Потерянные профили
std::array<
std::vector<std::pair<std::string, std::string>>,
static_cast<size_t>(EnumDefContent::MAX_ENUM)
> idToDK // Новые привязки
static std::vector<Net::Packet> makePackets_informateDefContentUpdate(
const ContentManager::Out_buildEndProfiles& profiles
);
void onUpdate();
@@ -409,13 +406,6 @@ private:
void protocolError();
coro<> readPacket(Net::AsyncSocket &sock);
coro<> rP_System(Net::AsyncSocket &sock);
// void incrementProfile(const std::vector<TextureId_t> &textures, const std::vector<ModelId_t> &model,
// const std::vector<SoundId_t> &sounds, const std::vector<FontId_t> &font
// );
// void decrementProfile(std::vector<TextureId_t> &&textures, std::vector<ModelId_t> &&model,
// std::vector<SoundId_t> &&sounds, std::vector<FontId_t> &&font
// );
};

View File

@@ -16,7 +16,7 @@ namespace LV::Server {
*/
struct SB_Region_In {
// Список вокселей всех чанков
std::unordered_map<Pos::bvec4u, VoxelCube> Voxels;
std::unordered_map<Pos::bvec4u, std::vector<VoxelCube>> Voxels;
// Привязка вокселей к ключу профиля
std::vector<std::pair<DefVoxelId, std::string>> VoxelsMap;
// Ноды всех чанков

View File

@@ -1,6 +1,7 @@
#include "Filesystem.hpp"
#include "Server/Abstract.hpp"
#include "Server/SaveBackend.hpp"
#include "TOSLib.hpp"
#include <boost/json/array.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
@@ -11,12 +12,277 @@
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>
#include <cstring>
namespace LV::Server::SaveBackends {
namespace fs = std::filesystem;
namespace js = boost::json;
namespace {
constexpr uint32_t kRegionVersion = 1;
constexpr size_t kRegionNodeCount = 4 * 4 * 4 * 16 * 16 * 16;
template<typename T>
js::object packIdMap(const std::vector<std::pair<T, std::string>>& map) {
js::object out;
for(const auto& [id, key] : map) {
out[std::to_string(id)] = key;
}
return out;
}
void unpackIdMap(const js::object& obj, std::vector<std::string>& out) {
size_t maxId = 0;
for(const auto& kvp : obj) {
try {
maxId = std::max(maxId, static_cast<size_t>(std::stoul(kvp.key())));
} catch(...) {
continue;
}
}
out.assign(maxId + 1, {});
for(const auto& kvp : obj) {
try {
size_t id = std::stoul(kvp.key());
out[id] = std::string(kvp.value().as_string());
} catch(...) {
continue;
}
}
}
std::string encodeCompressed(const uint8_t* data, size_t size) {
std::u8string compressed = compressLinear(std::u8string_view(reinterpret_cast<const char8_t*>(data), size));
return TOS::Enc::toBase64(reinterpret_cast<const uint8_t*>(compressed.data()), compressed.size());
}
std::u8string decodeCompressed(const std::string& base64) {
if(base64.empty())
return {};
TOS::ByteBuffer buffer = TOS::Enc::fromBase64(base64);
return unCompressLinear(std::u8string_view(reinterpret_cast<const char8_t*>(buffer.data()), buffer.size()));
}
bool writeRegionFile(const fs::path& path, const SB_Region_In& data) {
js::object jobj;
jobj["version"] = kRegionVersion;
{
std::vector<VoxelCube_Region> voxels;
convertChunkVoxelsToRegion(data.Voxels, voxels);
js::object jvoxels;
jvoxels["count"] = static_cast<uint64_t>(voxels.size());
if(!voxels.empty()) {
const uint8_t* raw = reinterpret_cast<const uint8_t*>(voxels.data());
size_t rawSize = sizeof(VoxelCube_Region) * voxels.size();
jvoxels["data"] = encodeCompressed(raw, rawSize);
} else {
jvoxels["data"] = "";
}
jobj["voxels"] = std::move(jvoxels);
jobj["voxels_map"] = packIdMap(data.VoxelsMap);
}
{
js::object jnodes;
const Node* nodePtr = data.Nodes[0].data();
const uint8_t* raw = reinterpret_cast<const uint8_t*>(nodePtr);
size_t rawSize = sizeof(Node) * kRegionNodeCount;
jnodes["data"] = encodeCompressed(raw, rawSize);
jobj["nodes"] = std::move(jnodes);
jobj["nodes_map"] = packIdMap(data.NodeMap);
}
{
js::array ents;
for(const Entity& entity : data.Entityes) {
js::object je;
je["def"] = static_cast<uint64_t>(entity.getDefId());
je["world"] = static_cast<uint64_t>(entity.WorldId);
je["pos"] = js::array{entity.Pos.x, entity.Pos.y, entity.Pos.z};
je["speed"] = js::array{entity.Speed.x, entity.Speed.y, entity.Speed.z};
je["accel"] = js::array{entity.Acceleration.x, entity.Acceleration.y, entity.Acceleration.z};
je["quat"] = js::array{entity.Quat.x, entity.Quat.y, entity.Quat.z, entity.Quat.w};
je["hp"] = static_cast<uint64_t>(entity.HP);
je["abbox"] = js::array{entity.ABBOX.x, entity.ABBOX.y, entity.ABBOX.z};
je["in_region"] = js::array{entity.InRegionPos.x, entity.InRegionPos.y, entity.InRegionPos.z};
js::object tags;
for(const auto& [key, value] : entity.Tags) {
tags[key] = value;
}
je["tags"] = std::move(tags);
ents.push_back(std::move(je));
}
jobj["entities"] = std::move(ents);
jobj["entities_map"] = packIdMap(data.EntityMap);
}
fs::create_directories(path.parent_path());
std::ofstream fd(path, std::ios::binary);
if(!fd)
return false;
fd << js::serialize(jobj);
return true;
}
bool readRegionFile(const fs::path& path, DB_Region_Out& out) {
try {
std::ifstream fd(path, std::ios::binary);
if(!fd)
return false;
out = {};
js::object jobj = js::parse(fd).as_object();
if(auto it = jobj.find("voxels"); it != jobj.end()) {
const js::object& jvoxels = it->value().as_object();
size_t count = 0;
if(auto itCount = jvoxels.find("count"); itCount != jvoxels.end())
count = static_cast<size_t>(itCount->value().to_number<uint64_t>());
std::string base64;
if(auto itData = jvoxels.find("data"); itData != jvoxels.end())
base64 = std::string(itData->value().as_string());
if(count > 0 && !base64.empty()) {
std::u8string raw = decodeCompressed(base64);
if(raw.size() != sizeof(VoxelCube_Region) * count)
return false;
out.Voxels.resize(count);
std::memcpy(out.Voxels.data(), raw.data(), raw.size());
}
}
if(auto it = jobj.find("voxels_map"); it != jobj.end()) {
unpackIdMap(it->value().as_object(), out.VoxelIdToKey);
}
if(auto it = jobj.find("nodes"); it != jobj.end()) {
const js::object& jnodes = it->value().as_object();
std::string base64;
if(auto itData = jnodes.find("data"); itData != jnodes.end())
base64 = std::string(itData->value().as_string());
if(!base64.empty()) {
std::u8string raw = decodeCompressed(base64);
if(raw.size() != sizeof(Node) * kRegionNodeCount)
return false;
std::memcpy(out.Nodes[0].data(), raw.data(), raw.size());
}
}
if(auto it = jobj.find("nodes_map"); it != jobj.end()) {
unpackIdMap(it->value().as_object(), out.NodeIdToKey);
}
if(auto it = jobj.find("entities"); it != jobj.end()) {
const js::array& ents = it->value().as_array();
out.Entityes.reserve(ents.size());
for(const js::value& val : ents) {
const js::object& je = val.as_object();
DefEntityId defId = static_cast<DefEntityId>(je.at("def").to_number<uint64_t>());
Entity entity(defId);
if(auto itWorld = je.find("world"); itWorld != je.end())
entity.WorldId = static_cast<DefWorldId>(itWorld->value().to_number<uint64_t>());
if(auto itPos = je.find("pos"); itPos != je.end()) {
const js::array& arr = itPos->value().as_array();
entity.Pos = Pos::Object(
static_cast<int32_t>(arr.at(0).to_number<int64_t>()),
static_cast<int32_t>(arr.at(1).to_number<int64_t>()),
static_cast<int32_t>(arr.at(2).to_number<int64_t>())
);
}
if(auto itSpeed = je.find("speed"); itSpeed != je.end()) {
const js::array& arr = itSpeed->value().as_array();
entity.Speed = Pos::Object(
static_cast<int32_t>(arr.at(0).to_number<int64_t>()),
static_cast<int32_t>(arr.at(1).to_number<int64_t>()),
static_cast<int32_t>(arr.at(2).to_number<int64_t>())
);
}
if(auto itAccel = je.find("accel"); itAccel != je.end()) {
const js::array& arr = itAccel->value().as_array();
entity.Acceleration = Pos::Object(
static_cast<int32_t>(arr.at(0).to_number<int64_t>()),
static_cast<int32_t>(arr.at(1).to_number<int64_t>()),
static_cast<int32_t>(arr.at(2).to_number<int64_t>())
);
}
if(auto itQuat = je.find("quat"); itQuat != je.end()) {
const js::array& arr = itQuat->value().as_array();
entity.Quat = glm::quat(
static_cast<float>(arr.at(3).to_number<double>()),
static_cast<float>(arr.at(0).to_number<double>()),
static_cast<float>(arr.at(1).to_number<double>()),
static_cast<float>(arr.at(2).to_number<double>())
);
}
if(auto itHp = je.find("hp"); itHp != je.end())
entity.HP = static_cast<uint32_t>(itHp->value().to_number<uint64_t>());
if(auto itAabb = je.find("abbox"); itAabb != je.end()) {
const js::array& arr = itAabb->value().as_array();
entity.ABBOX.x = static_cast<uint64_t>(arr.at(0).to_number<uint64_t>());
entity.ABBOX.y = static_cast<uint64_t>(arr.at(1).to_number<uint64_t>());
entity.ABBOX.z = static_cast<uint64_t>(arr.at(2).to_number<uint64_t>());
}
if(auto itRegion = je.find("in_region"); itRegion != je.end()) {
const js::array& arr = itRegion->value().as_array();
entity.InRegionPos = Pos::GlobalRegion(
static_cast<int16_t>(arr.at(0).to_number<int64_t>()),
static_cast<int16_t>(arr.at(1).to_number<int64_t>()),
static_cast<int16_t>(arr.at(2).to_number<int64_t>())
);
}
if(auto itTags = je.find("tags"); itTags != je.end()) {
const js::object& tags = itTags->value().as_object();
for(const auto& kvp : tags) {
entity.Tags[std::string(kvp.key())] = static_cast<float>(kvp.value().to_number<double>());
}
}
out.Entityes.push_back(std::move(entity));
}
}
if(auto it = jobj.find("entities_map"); it != jobj.end()) {
unpackIdMap(it->value().as_object(), out.EntityToKey);
}
return true;
} catch(const std::exception& exc) {
TOS::Logger("RegionLoader::Filesystem").warn() << "Не удалось загрузить регион " << path << "\n\t" << exc.what();
return false;
}
}
}
class WSB_Filesystem : public IWorldSaveBackend {
fs::path Dir;
@@ -35,7 +301,32 @@ public:
virtual TickSyncInfo_Out tickSync(TickSyncInfo_In &&data) override {
TickSyncInfo_Out out;
out.NotExisten = std::move(data.Load);
// Сохранение регионов
for(auto& [worldId, regions] : data.ToSave) {
for(auto& [regionPos, region] : regions) {
writeRegionFile(getPath(std::to_string(worldId), regionPos), region);
}
}
// Загрузка регионов
for(auto& [worldId, regions] : data.Load) {
for(const Pos::GlobalRegion& regionPos : regions) {
const fs::path path = getPath(std::to_string(worldId), regionPos);
if(!fs::exists(path)) {
out.NotExisten[worldId].push_back(regionPos);
continue;
}
DB_Region_Out regionOut;
if(!readRegionFile(path, regionOut)) {
out.NotExisten[worldId].push_back(regionPos);
continue;
}
out.LoadedRegions[worldId].push_back({regionPos, std::move(regionOut)});
}
}
return out;
}

View File

@@ -1,6 +1,8 @@
#include "World.hpp"
#include "ContentManager.hpp"
#include "TOSLib.hpp"
#include <memory>
#include <unordered_set>
namespace LV::Server {
@@ -96,8 +98,94 @@ void World::onRemoteClient_RegionsLost(WorldId_t worldId, std::shared_ptr<Remote
}
}
World::SaveUnloadInfo World::onStepDatabaseSync() {
return {};
World::SaveUnloadInfo World::onStepDatabaseSync(ContentManager& cm, float dtime) {
SaveUnloadInfo out;
constexpr float kSaveDelay = 15.0f;
constexpr float kUnloadDelay = 15.0f;
std::vector<Pos::GlobalRegion> toErase;
toErase.reserve(16);
for(auto& [pos, regionPtr] : Regions) {
Region& region = *regionPtr;
region.LastSaveTime += dtime;
const bool hasChanges = region.IsChanged || region.IsChunkChanged_Voxels || region.IsChunkChanged_Nodes;
const bool needToSave = hasChanges && region.LastSaveTime > kSaveDelay;
const bool needToUnload = region.RMs.empty() && region.LastSaveTime > kUnloadDelay;
if(needToSave || needToUnload) {
SB_Region_In data;
data.Voxels = region.Voxels;
data.Nodes = region.Nodes;
data.Entityes.reserve(region.Entityes.size());
for(const Entity& entity : region.Entityes) {
if(entity.IsRemoved || entity.NeedRemove)
continue;
data.Entityes.push_back(entity);
}
std::unordered_set<DefVoxelId> voxelIds;
for(const auto& [chunkPos, voxels] : region.Voxels) {
(void) chunkPos;
for(const VoxelCube& cube : voxels)
voxelIds.insert(cube.VoxelId);
}
std::unordered_set<DefNodeId> nodeIds;
for(const auto& chunk : region.Nodes) {
for(const Node& node : chunk)
nodeIds.insert(node.NodeId);
}
std::unordered_set<DefEntityId> entityIds;
for(const Entity& entity : data.Entityes)
entityIds.insert(entity.getDefId());
data.VoxelsMap.reserve(voxelIds.size());
for(DefVoxelId id : voxelIds) {
auto dk = cm.getDK(EnumDefContent::Voxel, id);
if(!dk)
continue;
data.VoxelsMap.emplace_back(id, dk->Domain + ":" + dk->Key);
}
data.NodeMap.reserve(nodeIds.size());
for(DefNodeId id : nodeIds) {
auto dk = cm.getDK(EnumDefContent::Node, id);
if(!dk)
continue;
data.NodeMap.emplace_back(id, dk->Domain + ":" + dk->Key);
}
data.EntityMap.reserve(entityIds.size());
for(DefEntityId id : entityIds) {
auto dk = cm.getDK(EnumDefContent::Entity, id);
if(!dk)
continue;
data.EntityMap.emplace_back(id, dk->Domain + ":" + dk->Key);
}
out.ToSave.push_back({pos, std::move(data)});
region.LastSaveTime = 0.0f;
region.IsChanged = false;
}
if(needToUnload) {
out.ToUnload.push_back(pos);
toErase.push_back(pos);
}
}
for(const Pos::GlobalRegion& pos : toErase) {
Regions.erase(pos);
}
return out;
}
void World::pushRegions(std::vector<std::pair<Pos::GlobalRegion, RegionIn>> regions) {

View File

@@ -12,6 +12,7 @@
namespace LV::Server {
class GameServer;
class ContentManager;
class Region {
public:
@@ -152,7 +153,7 @@ public:
std::vector<Pos::GlobalRegion> ToUnload;
std::vector<std::pair<Pos::GlobalRegion, SB_Region_In>> ToSave;
};
SaveUnloadInfo onStepDatabaseSync();
SaveUnloadInfo onStepDatabaseSync(ContentManager& cm, float dtime);
struct RegionIn {
std::unordered_map<Pos::bvec4u, std::vector<VoxelCube>> Voxels;

View File

@@ -338,9 +338,10 @@ class ByteBuffer : public std::vector<uint8_t> {
if(Index + sizeof(T) > Obj->size())
throw std::runtime_error("Вышли за пределы буфера");
const uint8_t *ptr = Obj->data()+Index;
T value{};
std::memcpy(&value, Obj->data() + Index, sizeof(T));
Index += sizeof(T);
return swapEndian(*(const T*) ptr);
return swapEndian(value);
}
public:
@@ -362,8 +363,8 @@ class ByteBuffer : public std::vector<uint8_t> {
inline Reader& operator>>(int64_t &value) { value = readOffset<int64_t>(); return *this; }
inline Reader& operator>>(uint64_t &value) { value = readOffset<uint64_t>(); return *this; }
inline Reader& operator>>(bool &value) { value = readOffset<uint8_t>(); return *this; }
inline Reader& operator>>(float &value) { return operator>>(*(uint32_t*) &value); }
inline Reader& operator>>(double &value) { return operator>>(*(uint64_t*) &value); }
inline Reader& operator>>(float &value) { uint32_t raw = readOffset<uint32_t>(); std::memcpy(&value, &raw, sizeof(raw)); return *this; }
inline Reader& operator>>(double &value) { uint64_t raw = readOffset<uint64_t>(); std::memcpy(&value, &raw, sizeof(raw)); return *this; }
inline int8_t readInt8() { int8_t value; this->operator>>(value); return value; }
inline uint8_t readUInt8() { uint8_t value; this->operator>>(value); return value; }
@@ -449,6 +450,17 @@ class ByteBuffer : public std::vector<uint8_t> {
size_t Index = 0;
uint16_t BlockSize = 256;
template<typename T> inline void writeRaw(const T &value)
{
uint8_t *ptr = checkBorder(sizeof(T));
std::memcpy(ptr, &value, sizeof(T));
}
template<typename T> inline void writeSwapped(const T &value)
{
T temp = swapEndian(value);
writeRaw(temp);
}
inline uint8_t* checkBorder(size_t count)
{
@@ -469,17 +481,17 @@ class ByteBuffer : public std::vector<uint8_t> {
Writer& operator=(const Writer&) = default;
Writer& operator=(Writer&&) = default;
inline Writer& operator<<(const int8_t &value) { *(int8_t*) checkBorder(sizeof(value)) = value; return *this; }
inline Writer& operator<<(const uint8_t &value) { *(uint8_t*) checkBorder(sizeof(value)) = value; return *this; }
inline Writer& operator<<(const int16_t &value) { *(int16_t*) checkBorder(sizeof(value)) = swapEndian(value); return *this; }
inline Writer& operator<<(const uint16_t &value) { *(uint16_t*) checkBorder(sizeof(value)) = swapEndian(value); return *this; }
inline Writer& operator<<(const int32_t &value) { *(int32_t*) checkBorder(sizeof(value)) = swapEndian(value); return *this; }
inline Writer& operator<<(const uint32_t &value) { *(uint32_t*) checkBorder(sizeof(value)) = swapEndian(value); return *this; }
inline Writer& operator<<(const int64_t &value) { *(int64_t*) checkBorder(sizeof(value)) = swapEndian(value); return *this; }
inline Writer& operator<<(const uint64_t &value) { *(uint64_t*) checkBorder(sizeof(value)) = swapEndian(value); return *this; }
inline Writer& operator<<(const bool &value) { *(uint8_t*) checkBorder(sizeof(value)) = uint8_t(value ? 1 : 0); return *this; }
inline Writer& operator<<(const float &value) { *(uint32_t*) checkBorder(sizeof(value)) = swapEndian(*(uint32_t*) &value); return *this; }
inline Writer& operator<<(const double &value) { *(uint64_t*) checkBorder(sizeof(value)) = swapEndian(*(uint64_t*) &value); return *this; }
inline Writer& operator<<(const int8_t &value) { writeRaw(value); return *this; }
inline Writer& operator<<(const uint8_t &value) { writeRaw(value); return *this; }
inline Writer& operator<<(const int16_t &value) { writeSwapped(value); return *this; }
inline Writer& operator<<(const uint16_t &value) { writeSwapped(value); return *this; }
inline Writer& operator<<(const int32_t &value) { writeSwapped(value); return *this; }
inline Writer& operator<<(const uint32_t &value) { writeSwapped(value); return *this; }
inline Writer& operator<<(const int64_t &value) { writeSwapped(value); return *this; }
inline Writer& operator<<(const uint64_t &value) { writeSwapped(value); return *this; }
inline Writer& operator<<(const bool &value) { uint8_t temp = value ? 1 : 0; writeRaw(temp); return *this; }
inline Writer& operator<<(const float &value) { uint32_t raw; std::memcpy(&raw, &value, sizeof(raw)); writeSwapped(raw); return *this; }
inline Writer& operator<<(const double &value) { uint64_t raw; std::memcpy(&raw, &value, sizeof(raw)); writeSwapped(raw); return *this; }
inline void writeInt8(const int8_t &value) { this->operator<<(value); }
inline void writeUInt8(const uint8_t &value) { this->operator<<(value); }

66
docs/assets_manager.md Normal file
View File

@@ -0,0 +1,66 @@
# AssetsManager
Документ описывает реализацию `AssetsManager` на стороне клиента.
## Назначение
`AssetsManager` объединяет в одном объекте:
- таблицы привязок ресурсов (domain/key -> localId, serverId -> localId);
- загрузку и хранение ресурсов из ресурспаков;
- систему источников данных (packs/memory/cache) с асинхронной выдачей;
- перестройку заголовков ресурсов под локальные идентификаторы.
## Основные структуры данных
- `PerType` — набор таблиц на каждый тип ресурса:
- `DKToLocal` и `LocalToDK` — двунаправленное отображение domain/key <-> localId.
- `LocalParent` — union-find для ребиндинга локальных id.
- `ServerToLocal` и `BindInfos` — привязки серверных id и их метаданные.
- `PackResources` — набор ресурсов, собранных из паков.
- `Sources` — список источников ресурсов (pack, memory, cache).
- `SourceCacheByHash` — кэш успешного источника для хеша.
- `PendingReadsByHash` и `ReadyReads` — очередь ожидания и готовые ответы.
## Источники ресурсов
Источники реализованы через интерфейс `IResourceSource`:
- pack source (sync) — ищет ресурсы в `PackResources`.
- memory source (sync) — ищет в `MemoryResourcesByHash`.
- cache source (async) — делает чтения через `AssetsCacheManager`.
Алгоритм поиска:
1) Сначала проверяется `SourceCacheByHash` (если не протух по поколению).
2) Источники опрашиваются по порядку, первый `Hit` возвращается сразу.
3) Если источник вернул `Pending`, запрос попадает в ожидание.
4) `tickSources()` опрашивает асинхронные источники и переводит ответы в `ReadyReads`.
## Привязка идентификаторов
- `getOrCreateLocalId()` создаёт локальный id для domain/key.
- `bindServerResource()` связывает serverId с localId и записывает `BindInfo`.
- `unionLocalIds()` объединяет локальные id при конфликте, используя union-find.
## Ресурспаки
`reloadPacks()` сканирует директории, собирает ресурсы в `PackResources`,
а затем возвращает список изменений и потерь по типам.
Важно: ключи ресурсов всегда хранятся с разделителем `/`.
Для нормализации пути используется `fs::path::generic_string()`.
## Заголовки
- `rebindHeader()` заменяет id зависимостей в заголовках ресурса.
- `parseHeader()` парсит заголовок без модификаций.
## Поток данных чтения
1) `pushReads()` принимает список `ResourceKey` и пытается получить ресурс.
2) `pullReads()` возвращает готовые ответы, включая промахи.
3) `pushResources()` добавляет ресурсы в память и прокидывает их в кэш.
## Ограничения
- Класс не предназначен для внешнего многопоточного использования.
- Политика приоритета ресурсов в паке фиксированная: первый найденный ключ побеждает.
- Коллизии хешей не обрабатываются отдельно.

105
docs/texture_pipeline.md Normal file
View File

@@ -0,0 +1,105 @@
# Текстурные программы (TexturePipelineProgram)
Текстурная программа — это строка, описывающая источник текстуры и цепочку операций
над ней. Такие строки используются в `textures` моделей и компилируются
`TexturePipelineProgram`.
## Общая форма
```
[tex] <base> [|> op(...)]*
```
`tex` в начале необязателен.
## Базовые выражения
- `name` или `"name.png"` — ссылка на текстуру из assets. Расширение .png/.jpg/.jpeg допустимо.
- `anim(...)` — анимация из спрайт-листа (см. ниже).
- `<w>x<h> <#RRGGBB|#RRGGBBAA>` — заливка цветом.
Примеры:
```
stone
tex "core:stone.png"
32x32 "#FF00FF"
```
## Аргументы операций
- Позиционные: `op(1, 2, "str")`
- Именованные: `op(w=16, h=16)`
- Значения: числа (uint32), строки в кавычках, либо идентификаторы.
Цвета задаются `#RRGGBB` или `#RRGGBBAA`.
## Операции пайплайна
Операции без аргументов можно писать без `()`: `brighten` и т.п.
В подвыражениях текстур (см. ниже) операции без аргументов нужно писать со скобками:
`brighten()`.
### Операции, принимающие текстуру
- `overlay(tex)` — наложение с альфой.
- `mask(tex)` — применение альфа-маски.
- `lowpart(percent, tex)` — смешивание нижней части (percent 1..100).
`tex` может быть:
- именем текстуры: `overlay("core:stone")`
- именованным аргументом: `overlay(tex="core:stone")`
- вложенной программой: `overlay( tex stone |> invert("rgb") )`
### Геометрия и альфа
- `resize(w, h)` — ресайз до размеров.
- `transform(t)` — трансформация (значение 0..7).
- `opacity(a)` — прозрачность 0..255.
- `remove_alpha` или `noalpha` — убрать альфа-канал.
- `make_alpha(color)` — сделать альфу по цвету (цвет в `#RRGGBB`).
### Цвет и яркость
- `invert(channels="rgb")` — инверсия каналов (`r`, `g`, `b`, `a`).
- `brighten()` — лёгкое осветление.
- `contrast(value, brightness)` — контраст и яркость (-127..127).
- `multiply(color)` — умножение на цвет.
- `screen(color)` — экранный режим.
- `colorize(color, ratio=255)` — тонирование цветом.
### Анимация
`anim` можно использовать в базе (с указанием текстуры) или в пайплайне
над текущим изображением.
База:
```
anim(tex, frame_w, frame_h, frames, fps, smooth, axis)
```
Пайплайн:
```
... |> anim(frame_w, frame_h, frames, fps, smooth, axis)
```
Именованные аргументы:
- `tex` — имя текстуры (только для базового `anim`).
- `frame_w` или `w`
- `frame_h` или `h`
- `frames` или `count`
- `fps`
- `smooth` (0/1)
- `axis` — режим нарезки:
- `g` или пусто: по сетке (слева направо, сверху вниз)
- `x`/`h`: по горизонтали
- `y`/`v`: по вертикали
Если `frames` не задан, количество кадров вычисляется автоматически:
- сетка: `(sheet.W / frame_w) * (sheet.H / frame_h)`
- ось X/Y: `sheet.W / frame_w` или `sheet.H / frame_h`
Примеры:
```
anim("core:sheet", 16, 16, fps=8) # сетка по умолчанию
anim("core:sheet", 16, 16, axis="x") # по горизонтали
stone |> anim(16, 16, fps=10, smooth=1) # анимировать текущую текстуру
```
## Вложенные текстурные выражения
Некоторые операции принимают текстуру в аргументах. Чтобы передать не только имя,
но и полноценную программу, используйте префикс `tex`:
```
overlay( tex "core:stone" |> resize(16,16) |> brighten() )
```

View File

@@ -1,81 +0,0 @@
{
"textures": {
"default": "acacia_planks.png"
},
"cuboids": [
{
"from": [
-0.5,
-0.5,
-0.5
],
"to": [
0.5,
0.5,
0.5
],
"faces": {
"down": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "down"
},
"up": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "up"
},
"north": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "north"
},
"south": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "south"
},
"west": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "west"
},
"east": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "east"
}
}
}
]
}

View File

@@ -1,81 +0,0 @@
{
"textures": {
"default": "frame.png"
},
"cuboids": [
{
"from": [
-0.5,
-0.5,
-0.5
],
"to": [
0.5,
0.5,
0.5
],
"faces": {
"down": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "down"
},
"up": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "up"
},
"north": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "north"
},
"south": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "south"
},
"west": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "west"
},
"east": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "east"
}
}
}
]
}

View File

@@ -1,81 +0,0 @@
{
"textures": {
"default": "grass.png"
},
"cuboids": [
{
"from": [
-0.5,
-0.5,
-0.5
],
"to": [
0.5,
0.5,
0.5
],
"faces": {
"down": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "down"
},
"up": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "up"
},
"north": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "north"
},
"south": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "south"
},
"west": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "west"
},
"east": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "east"
}
}
}
]
}

View File

@@ -1,81 +0,0 @@
{
"textures": {
"default": "jungle_planks.png"
},
"cuboids": [
{
"from": [
-0.5,
-0.5,
-0.5
],
"to": [
0.5,
0.5,
0.5
],
"faces": {
"down": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "down"
},
"up": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "up"
},
"north": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "north"
},
"south": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "south"
},
"west": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "west"
},
"east": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "east"
}
}
}
]
}

View File

@@ -1,81 +0,0 @@
{
"textures": {
"default": "oak_planks.png"
},
"cuboids": [
{
"from": [
-0.5,
-0.5,
-0.5
],
"to": [
0.5,
0.5,
0.5
],
"faces": {
"down": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "down"
},
"up": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "up"
},
"north": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "north"
},
"south": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "south"
},
"west": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "west"
},
"east": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "east"
}
}
}
]
}

View File

@@ -1,81 +0,0 @@
{
"textures": {
"default": "tropical_rainforest_wood.png"
},
"cuboids": [
{
"from": [
-0.5,
-0.5,
-0.5
],
"to": [
0.5,
0.5,
0.5
],
"faces": {
"down": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "down"
},
"up": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "up"
},
"north": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "north"
},
"south": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "south"
},
"west": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "west"
},
"east": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "east"
}
}
}
]
}

View File

@@ -1,81 +0,0 @@
{
"textures": {
"default": "willow_wood.png"
},
"cuboids": [
{
"from": [
-0.5,
-0.5,
-0.5
],
"to": [
0.5,
0.5,
0.5
],
"faces": {
"down": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "down"
},
"up": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "up"
},
"north": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "north"
},
"south": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "south"
},
"west": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "west"
},
"east": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "east"
}
}
}
]
}

View File

@@ -1,81 +0,0 @@
{
"textures": {
"default": "xnether_blue_wood.png"
},
"cuboids": [
{
"from": [
-0.5,
-0.5,
-0.5
],
"to": [
0.5,
0.5,
0.5
],
"faces": {
"down": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "down"
},
"up": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "up"
},
"north": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "north"
},
"south": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "south"
},
"west": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "west"
},
"east": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "east"
}
}
}
]
}

View File

@@ -1,81 +0,0 @@
{
"textures": {
"default": "xnether_purple_wood.png"
},
"cuboids": [
{
"from": [
-0.5,
-0.5,
-0.5
],
"to": [
0.5,
0.5,
0.5
],
"faces": {
"down": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "down"
},
"up": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "up"
},
"north": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "north"
},
"south": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "south"
},
"west": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "west"
},
"east": {
"uv": [
0,
0,
1,
1
],
"texture": "default",
"cullface": "east"
}
}
}
]
}

View File

@@ -1,14 +0,0 @@
{
"meta==0": {
"model": "node/grass.json"
},
"meta==1": {
"model": "node/oak_planks.json"
},
"meta==2": {
"model": "node/jungle_planks.json"
},
"meta==3": {
"model": "node/acacia_planks.json"
}
}

View File

@@ -1,14 +0,0 @@
{
"meta==0": {
"model": "node/tropical_rainforest_wood.json"
},
"meta==1": {
"model": "node/willow_wood.json"
},
"meta==2": {
"model": "node/xnether_blue_wood.json"
},
"meta==3": {
"model": "node/xnether_purple_wood.json"
}
}

View File

@@ -1,14 +0,0 @@
{
"meta==0": {
"model": "node/frame.json"
},
"meta==1": {
"model": "node/grass.json"
},
"meta==2": {
"model": "node/oak_planks.json"
},
"meta==3": {
"model": "node/acacia_planks.json"
}
}

View File

@@ -1,14 +0,0 @@
{
"meta==0": {
"model": "node/jungle_planks.json"
},
"meta==1": {
"model": "node/tropical_rainforest_wood.json"
},
"meta==2": {
"model": "node/willow_wood.json"
},
"meta==3": {
"model": "node/xnether_blue_wood.json"
}
}

View File

@@ -1,14 +0,0 @@
{
"meta==0": {
"model": "node/oak_planks.json"
},
"meta==1": {
"model": "node/jungle_planks.json"
},
"meta==2": {
"model": "node/acacia_planks.json"
},
"meta==3": {
"model": "node/willow_wood.json"
}
}

View File

@@ -1,14 +0,0 @@
{
"meta==0": {
"model": "node/grass.json"
},
"meta==1": {
"model": "node/frame.json"
},
"meta==2": {
"model": "node/xnether_purple_wood.json"
},
"meta==3": {
"model": "node/tropical_rainforest_wood.json"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -1,98 +0,0 @@
-- parent = default:air
--
-- hasHalfTransparency
-- collideBox = {}
-- plantLike = {}
-- nodebox = {}
local node_template = {
parent = "default:normal" or node_template,
render = {
has_half_transparency = false
},
collision = {
},
events = {
},
node_advancement_factory = function(world_id, node_pos)
local node_advancement = {
onLoad = function(data)
end,
onSave = function()
return {}
end
}
return node_advancement
end
}
local instance = {}
--[[
Движок автоматически подгружает ассеты из папки assets
В этом методе можно зарегистрировать ассеты из иных источников
Состояния нод, частицы, анимации, модели, текстуры, звуки, шрифты
]]--
function instance.assetsInit()
end
--[[
*preInit. События для регистрации определений игрового контента
Ноды, воксели, миры, порталы, сущности, предметы
]]--
function instance.lowPreInit()
end
--[[
До вызова preInit будет выполнена регистрация
контента из файлов в папке content
]]--
function instance.preInit()
local node_air = {}
node_air.hasHalfTransparency = false
node_air.collideBox = nil
node_air.render = nil
core.register_node('test0', {})
core.register_node('test1', {})
core.register_node('test2', {})
core.register_node('test3', {})
core.register_node('test4', {})
core.register_node('test5', {})
end
function instance.highPreInit()
end
--[[
На этом этапе можно наложить изменения
на зарегистрированный другими модами контент
]]--
function instance.init()
end
function instance.postInit()
end
function instance.preDeInit()
end
function instance.deInit()
end
function instance.postDeInit()
core.unregister_node('test0')
core.unregister_node('test1')
core.unregister_node('test2')
core.unregister_node('test3')
core.unregister_node('test4')
core.unregister_node('test5')
end
return instance

View File

@@ -1,9 +0,0 @@
{
"id": "test",
"name": "Test Mod",
"description": "Это тестовый мод",
"depends": [],
"optional_depends": [],
"author": "DrSocalkwe3n",
"version": [0, 0, 0, 1]
}