codex-5.2: синхронизация ресурсов модов, частичная перезагрузка модов

This commit is contained in:
2026-01-01 15:12:27 +06:00
parent 4aa7c6f41a
commit f56b46f669
16 changed files with 692 additions and 84 deletions

View File

@@ -221,11 +221,18 @@ std::tuple<ResourceId, std::optional<AssetsManager::DataEntry>&> AssetsManager::
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())
@@ -233,13 +240,23 @@ std::tuple<ResourceId, std::optional<AssetsManager::DataEntry>&> AssetsManager::
id = index*TableEntry<DataEntry>::ChunkSize + pos;
data = &entry.Entries[pos];
break;
}
if(!data) {
table.emplace_back(std::make_unique<TableEntry<DataEntry>>());
id = (table.size()-1)*TableEntry<DataEntry>::ChunkSize;
data = &table.back()->Entries[0];
table.back()->Empty.reset(0);
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)

View File

@@ -7,7 +7,7 @@ namespace LV::Server {
ContentManager::ContentManager(AssetsManager &am)
: AM(am)
{
std::fill(std::begin(NextId), std::end(NextId), 1);
}
ContentManager::~ContentManager() = default;
@@ -111,6 +111,31 @@ 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);
}
}
}
std::vector<ResourceId> ContentManager::collectProfileIds(EnumDefContent type) const {
std::vector<ResourceId> ids;
const auto &table = ContentKeyToId[(int) type];
for(const auto& domainPair : table) {
for(const auto& keyPair : domainPair.second) {
ids.push_back(keyPair.second);
}
}
std::sort(ids.begin(), ids.end());
auto last = std::unique(ids.begin(), ids.end());
ids.erase(last, ids.end());
return ids;
}
ContentManager::Out_buildEndProfiles ContentManager::buildEndProfiles() {
Out_buildEndProfiles result;
@@ -138,4 +163,4 @@ ContentManager::Out_buildEndProfiles ContentManager::buildEndProfiles() {
return result;
}
}
}

View File

@@ -48,7 +48,7 @@ class ContentManager {
// Следующие идентификаторы регистрации контента
ResourceId NextId[(int) EnumDefContent::MAX_ENUM] = {0};
ResourceId NextId[(int) EnumDefContent::MAX_ENUM] = {};
// Домен -> {ключ -> идентификатор}
std::unordered_map<std::string, std::unordered_map<std::string, ResourceId>> ContentKeyToId[(int) EnumDefContent::MAX_ENUM];
@@ -143,6 +143,10 @@ public:
// Регистрация модификатора предмета модом
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];
@@ -209,4 +213,4 @@ private:
AssetsManager& AM;
};
}
}

View File

@@ -17,6 +17,7 @@
#include <iterator>
#include <memory>
#include <mutex>
#include <optional>
#include <sol/forward.hpp>
#include <sol/protected_function_result.hpp>
#include <sstream>
@@ -1107,7 +1108,7 @@ coro<> GameServer::pushSocketGameProtocol(tcp::socket socket, const std::string
co_await Net::AsyncSocket::write<uint8_t>(socket, 0);
External.NewConnectedPlayers.lock_write()
->push_back(std::make_shared<RemoteClient>(IOC, std::move(socket), username));
->push_back(std::make_shared<RemoteClient>(IOC, std::move(socket), username, this));
}
}
}
@@ -1444,7 +1445,7 @@ void GameServer::init(fs::path worldPath) {
{
sol::table t = LuaMainState.create_table();
Content.CM.registerBase(EnumDefContent::Node, "core", "none", t);
// Content.CM.registerBase(EnumDefContent::Node, "core", "none", t);
Content.CM.registerBase(EnumDefContent::World, "test", "devel_world", t);
}
@@ -1453,11 +1454,11 @@ void GameServer::init(fs::path worldPath) {
// TODO: регистрация контента из mod/content/*
Content.CM.buildEndProfiles();
pushEvent("preInit");
pushEvent("highPreInit");
Content.CM.buildEndProfiles();
LOG.info() << "Инициализация";
initLua();
@@ -1674,6 +1675,13 @@ void GameServer::initLuaPost() {
}
void GameServer::requestModsReload() {
bool expected = false;
if(ModsReloadRequested.compare_exchange_strong(expected, true)) {
LOG.info() << "Запрошена перезагрузка модов";
}
}
void GameServer::stepConnections() {
// Подключить новых игроков
if(!External.NewConnectedPlayers.no_lock_readable().empty()) {
@@ -1715,9 +1723,42 @@ void GameServer::stepConnections() {
}
void GameServer::stepModInitializations() {
if(ModsReloadRequested.exchange(false)) {
reloadMods();
}
BackingChunkPressure.endWithResults();
}
void GameServer::reloadMods() {
LOG.info() << "Перезагрузка модов: ассеты и зависимости";
AssetsManager::ResourceChangeObj changes = Content.AM.recheckResources(AssetsInit);
AssetsManager::Out_applyResourceChange applied = Content.AM.applyResourceChange(changes);
size_t changedCount = 0;
size_t lostCount = 0;
for(int type = 0; type < (int) EnumAssets::MAX_ENUM; type++) {
for(const auto& entry : applied.NewOrChange[type]) {
Content.OnContentChanges.AssetsInfo[type].push_back(entry.first);
changedCount++;
}
lostCount += applied.Lost[type].size();
}
Content.CM.markAllProfilesDirty(EnumDefContent::Node);
Content.CM.buildEndProfiles();
std::vector<ResourceId> nodeIds = Content.CM.collectProfileIds(EnumDefContent::Node);
if(!nodeIds.empty()) {
Content.OnContentChanges.Node.append_range(nodeIds);
}
LOG.info() << "Перезагрузка завершена: обновлено ассетов=" << changedCount
<< " удалено=" << lostCount
<< " нод=" << nodeIds.size();
}
IWorldSaveBackend::TickSyncInfo_Out GameServer::stepDatabaseSync() {
IWorldSaveBackend::TickSyncInfo_In toDB;

View File

@@ -5,6 +5,7 @@
#include <Common/Net.hpp>
#include <Common/Lockable.hpp>
#include <atomic>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/io_context.hpp>
#include <condition_variable>
@@ -58,6 +59,7 @@ class GameServer : public AsyncObject {
bool IsAlive = true, IsGoingShutdown = false;
std::string ShutdownReason;
std::atomic<bool> ModsReloadRequested = false;
static constexpr float
PerTickDuration = 1/30.f, // Минимальная и стартовая длина такта
PerTickAdjustment = 1/60.f; // Подгонка длительности такта в случае провисаний
@@ -283,6 +285,7 @@ public:
void waitShutdown() {
UseLock.wait_no_use();
}
void requestModsReload();
// Подключение tcp сокета
coro<> pushSocketConnect(tcp::socket socket);
@@ -315,6 +318,7 @@ private:
*/
void stepModInitializations();
void reloadMods();
/*
Пересчёт зон видимости игроков, если необходимо
@@ -364,4 +368,4 @@ private:
void stepSyncContent();
};
}
}

View File

@@ -3,8 +3,10 @@
#include "Common/Abstract.hpp"
#include "Common/Net.hpp"
#include "Server/Abstract.hpp"
#include "Server/GameServer.hpp"
#include "Server/World.hpp"
#include <algorithm>
#include <atomic>
#include <boost/asio/error.hpp>
#include <boost/system/system_error.hpp>
#include <exception>
@@ -13,6 +15,23 @@
namespace LV::Server {
namespace {
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";
}
}
}
RemoteClient::~RemoteClient() {
shutdown(EnumDisconnect::ByInterface, "~RemoteClient()");
if(Socket.isAlive()) {
@@ -487,9 +506,13 @@ ResourceRequest RemoteClient::pushPreparedPackets() {
nextRequest = std::move(lock->NextRequest);
}
if(AssetsInWork.AssetsPacket.size()) {
toSend.push_back(std::move(AssetsInWork.AssetsPacket));
if(!AssetsInWork.AssetsPackets.empty()) {
for(Net::Packet& packet : AssetsInWork.AssetsPackets)
toSend.push_back(std::move(packet));
AssetsInWork.AssetsPackets.clear();
}
if(AssetsInWork.AssetsPacket.size())
toSend.push_back(std::move(AssetsInWork.AssetsPacket));
{
Net::Packet p;
@@ -508,6 +531,7 @@ ResourceRequest RemoteClient::pushPreparedPackets() {
void RemoteClient::informateAssets(const std::vector<std::tuple<EnumAssets, ResourceId, const std::string, const std::string, Resource>>& resources)
{
std::vector<std::tuple<EnumAssets, ResourceId, const std::string, const std::string, Hash_t, size_t>> newForClient;
static std::atomic<uint32_t> debugSendLogCount = 0;
for(auto& [type, resId, domain, key, resource] : resources) {
auto hash = resource.hash();
@@ -526,6 +550,22 @@ void RemoteClient::informateAssets(const std::vector<std::tuple<EnumAssets, Reso
if(it == AssetsInWork.OnClient.end() || *it != hash) {
AssetsInWork.OnClient.insert(it, hash);
AssetsInWork.ToSend.emplace_back(type, domain, key, resId, resource, 0);
if(domain == "test"
&& (type == EnumAssets::Nodestate
|| type == EnumAssets::Model
|| type == EnumAssets::Texture))
{
if(debugSendLogCount.fetch_add(1) < 64) {
LOG.debug() << "Queue resource send type=" << assetTypeName(type)
<< " id=" << resId
<< " key=" << domain << ':' << key
<< " size=" << resource.size()
<< " hash=" << int(hash[0]) << '.'
<< int(hash[1]) << '.'
<< int(hash[2]) << '.'
<< int(hash[3]);
}
}
} else {
LOG.warn() << "Клиент повторно запросил имеющийся у него ресурс";
}
@@ -720,6 +760,7 @@ coro<> RemoteClient::rP_System(Net::AsyncSocket &sock) {
}
case ToServer::L2System::ResourceRequest:
{
static std::atomic<uint32_t> debugRequestLogCount = 0;
uint16_t count = co_await sock.read<uint16_t>();
std::vector<Hash_t> hashes;
hashes.reserve(count);
@@ -733,6 +774,29 @@ coro<> RemoteClient::rP_System(Net::AsyncSocket &sock) {
auto lock = NetworkAndResource.lock();
lock->NextRequest.Hashes.append_range(hashes);
lock->ClientRequested.append_range(hashes);
if(debugRequestLogCount.fetch_add(1) < 64) {
if(!hashes.empty()) {
const auto& h = hashes.front();
LOG.debug() << "ResourceRequest count=" << count
<< " first=" << int(h[0]) << '.'
<< int(h[1]) << '.'
<< int(h[2]) << '.'
<< int(h[3]);
} else {
LOG.debug() << "ResourceRequest count=" << count;
}
}
co_return;
}
case ToServer::L2System::ReloadMods:
{
if(Server) {
Server->requestModsReload();
LOG.info() << "Запрос на перезагрузку модов";
} else {
LOG.warn() << "Запрос на перезагрузку модов отклонён: сервер не назначен";
}
co_return;
}
default:
@@ -818,24 +882,59 @@ void RemoteClient::onUpdate() {
// Отправка ресурсов
if(!AssetsInWork.ToSend.empty()) {
auto& toSend = AssetsInWork.ToSend;
constexpr uint16_t kMaxAssetPacketSize = 64000;
const size_t maxChunkPayload = std::max<size_t>(1, kMaxAssetPacketSize - 1 - 1 - 32 - 4);
size_t chunkSize = std::max<size_t>(1'024'000 / toSend.size(), 4096);
chunkSize = std::min(chunkSize, maxChunkPayload);
static std::atomic<uint32_t> debugInitSendLogCount = 0;
Net::Packet& p = AssetsInWork.AssetsPacket;
auto flushAssetsPacket = [&]() {
if(p.size() == 0)
return;
AssetsInWork.AssetsPackets.push_back(std::move(p));
};
bool hasFullSended = false;
for(auto& [type, domain, key, id, res, sended] : toSend) {
if(sended == 0) {
// Оповещаем о начале отправки ресурса
const size_t initSize = 1 + 1 + 4 + 32 + 4 + 1
+ 2 + domain.size()
+ 2 + key.size();
if(p.size() + initSize > kMaxAssetPacketSize)
flushAssetsPacket();
p << (uint8_t) ToClient::L1::Resource
<< (uint8_t) ToClient::L2Resource::InitResSend
<< uint32_t(res.size());
p.write((const std::byte*) res.hash().data(), 32);
p << uint32_t(id) << uint8_t(type) << domain << key;
if(domain == "test"
&& (type == EnumAssets::Nodestate
|| type == EnumAssets::Model
|| type == EnumAssets::Texture))
{
if(debugInitSendLogCount.fetch_add(1) < 64) {
const auto hash = res.hash();
LOG.debug() << "Send InitResSend type=" << assetTypeName(type)
<< " id=" << id
<< " key=" << domain << ':' << key
<< " size=" << res.size()
<< " hash=" << int(hash[0]) << '.'
<< int(hash[1]) << '.'
<< int(hash[2]) << '.'
<< int(hash[3]);
}
}
}
// Отправляем чанк
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::L1::Resource
<< (uint8_t) ToClient::L2Resource::ChunkSend;
p.write((const std::byte*) res.hash().data(), 32);
@@ -862,4 +961,4 @@ std::vector<std::tuple<WorldId_t, Pos::Object, uint8_t>> RemoteClient::getViewPo
return {{0, CameraPos, 1}};
}
}
}

View File

@@ -17,6 +17,7 @@
namespace LV::Server {
class World;
class GameServer;
template<typename ServerKey, typename ClientKey, std::enable_if_t<sizeof(ServerKey) >= sizeof(ClientKey), int> = 0>
class CSChunkedMapper {
@@ -316,6 +317,7 @@ class RemoteClient {
// Тип, домен, ключ, идентификатор, ресурс, количество отправленных байт
std::vector<std::tuple<EnumAssets, std::string, std::string, ResourceId, Resource, size_t>> ToSend;
// Пакет с ресурсами
std::vector<Net::Packet> AssetsPackets;
Net::Packet AssetsPacket;
} AssetsInWork;
@@ -336,8 +338,8 @@ public:
std::queue<Pos::GlobalNode> Build, Break;
public:
RemoteClient(asio::io_context &ioc, tcp::socket socket, const std::string username)
: LOG("RemoteClient " + username), Socket(ioc, std::move(socket)), Username(username)
RemoteClient(asio::io_context &ioc, tcp::socket socket, const std::string username, GameServer* server)
: LOG("RemoteClient " + username), Socket(ioc, std::move(socket)), Username(username), Server(server)
{}
~RemoteClient();
@@ -434,6 +436,7 @@ public:
void onUpdate();
private:
GameServer* Server = nullptr;
void protocolError();
coro<> readPacket(Net::AsyncSocket &sock);
coro<> rP_System(Net::AsyncSocket &sock);
@@ -447,4 +450,4 @@ private:
};
}
}