423 lines
17 KiB
C++
423 lines
17 KiB
C++
#pragma once
|
||
|
||
#include <TOSLib.hpp>
|
||
#include <Common/Lockable.hpp>
|
||
#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>
|
||
#include <initializer_list>
|
||
#include <optional>
|
||
#include <queue>
|
||
#include <string>
|
||
#include <type_traits>
|
||
#include <unordered_map>
|
||
|
||
namespace LV::Server {
|
||
|
||
class World;
|
||
class GameServer;
|
||
|
||
template<typename ServerKey, typename ClientKey, std::enable_if_t<sizeof(ServerKey) >= sizeof(ClientKey), int> = 0>
|
||
class CSChunkedMapper {
|
||
std::unordered_map<uint32_t, std::tuple<std::bitset<64>, std::array<ServerKey, 64>>> Chunks;
|
||
|
||
public:
|
||
ServerKey toServer(ClientKey cKey) {
|
||
int chunkIndex = cKey >> 6;
|
||
int subIndex = cKey & 0x3f;
|
||
|
||
auto iChunk = Chunks.find(chunkIndex);
|
||
assert(iChunk != Chunks.end() && "Идентификатор уже занят");
|
||
|
||
std::bitset<64> &bits = std::get<0>(iChunk.second);
|
||
std::array<ServerKey, 64> &keys = std::get<1>(iChunk.second);
|
||
|
||
assert(bits.test(subIndex) && "Идентификатор уже занят");
|
||
|
||
return keys[subIndex];
|
||
}
|
||
|
||
void erase(ClientKey cKey) {
|
||
int chunkIndex = cKey >> 6;
|
||
int subIndex = cKey & 0x3f;
|
||
|
||
auto iChunk = Chunks.find(chunkIndex);
|
||
if(iChunk == Chunks.end())
|
||
MAKE_ERROR("Идентификатор не привязан");
|
||
|
||
std::bitset<64> &bits = std::get<0>(iChunk->second);
|
||
std::array<ServerKey, 64> &keys = std::get<1>(iChunk->second);
|
||
|
||
assert(bits.test(subIndex) && "Идентификатор уже занят");
|
||
|
||
bits.reset(subIndex);
|
||
}
|
||
|
||
void link(ClientKey cKey, ServerKey sKey) {
|
||
int chunkIndex = cKey >> 6;
|
||
int subIndex = cKey & 0x3f;
|
||
|
||
std::tuple<std::bitset<64>, std::array<ServerKey, 64>> &chunk = Chunks[chunkIndex];
|
||
std::bitset<64> &bits = std::get<0>(chunk);
|
||
std::array<ServerKey, 64> &keys = std::get<1>(chunk);
|
||
|
||
assert(!bits.test(subIndex) && "Идентификатор уже занят");
|
||
|
||
bits.set(subIndex);
|
||
keys[subIndex] = sKey;
|
||
}
|
||
};
|
||
|
||
template<typename ServerKey, typename ClientKey, std::enable_if_t<sizeof(ServerKey) >= sizeof(ClientKey), int> = 0>
|
||
class SCSKeyRemapper {
|
||
std::bitset<sizeof(ClientKey)*8-1> FreeClientKeys;
|
||
std::map<ServerKey, ClientKey> Map;
|
||
CSChunkedMapper<ServerKey, ClientKey> CSmapper;
|
||
|
||
public:
|
||
SCSKeyRemapper() {
|
||
FreeClientKeys.set();
|
||
}
|
||
|
||
// Соотнести идентификатор на стороне сервера с идентификатором на стороне клиента
|
||
ClientKey toClient(ServerKey skey) {
|
||
auto iter = Map.find(skey);
|
||
if(iter == Map.end()) {
|
||
// Идентификатор отсутствует, нужно его занять
|
||
// Ищет позицию ближайшего бита 1
|
||
size_t pos = FreeClientKeys._Find_first();
|
||
if(pos == FreeClientKeys.size())
|
||
return ClientKey(0); // Свободные идентификаторы отсутствуют
|
||
|
||
ClientKey ckey = ClientKey(pos+1);
|
||
Map[skey] = ckey;
|
||
CSmapper.link(ckey, skey);
|
||
FreeClientKeys.reset(pos);
|
||
return ClientKey(pos);
|
||
}
|
||
|
||
return iter->second;
|
||
}
|
||
|
||
// Соотнести идентификатор на стороне клиента с идентификатором на стороне сервера
|
||
ServerKey toServer(ClientKey ckey) {
|
||
return CSmapper.toServer(ckey);
|
||
}
|
||
|
||
// Удаляет серверный идентификатор, освобождая идентификатор клиента
|
||
ClientKey erase(ServerKey skey) {
|
||
auto iter = Map.find(skey);
|
||
|
||
if(iter == Map.end())
|
||
return 0;
|
||
|
||
ClientKey ckey = iter->second;
|
||
CSmapper.erase(ckey);
|
||
Map.erase(iter);
|
||
FreeClientKeys.set(ckey-1);
|
||
|
||
return ckey;
|
||
}
|
||
|
||
void rebindClientKey(ServerKey prev, ServerKey next) {
|
||
auto iter = Map.find(prev);
|
||
|
||
assert(iter != Map.end() && "Идентификатор не найден");
|
||
ClientKey ckey = iter->second;
|
||
CSmapper.erase(ckey);
|
||
CSmapper.link(ckey, next);
|
||
Map.erase(iter);
|
||
Map[next] = ckey;
|
||
}
|
||
};
|
||
|
||
/*
|
||
Шаблоны игрового контента, которые необходимо поддерживать в актуальном
|
||
состоянии для клиента и шаблоны, которые клиенту уже не нужны.
|
||
Соответствующие менеджеры ресурсов будут следить за изменениями
|
||
этих ресурсов и переотправлять их клиенту
|
||
|
||
Информация о двоичных ресурсах будет получена сразу же при их запросе.
|
||
Действительная отправка ресурсов будет только по запросу клиента.
|
||
*/
|
||
struct ResourceRequest {
|
||
std::vector<Hash_t> Hashes;
|
||
|
||
void merge(const ResourceRequest &obj) {
|
||
Hashes.insert(Hashes.end(), obj.Hashes.begin(), obj.Hashes.end());
|
||
}
|
||
|
||
void uniq() {
|
||
std::sort(Hashes.begin(), Hashes.end());
|
||
auto last = std::unique(Hashes.begin(), Hashes.end());
|
||
Hashes.erase(last, Hashes.end());
|
||
}
|
||
};
|
||
|
||
struct AssetBinaryInfo {
|
||
Resource Data;
|
||
Hash_t Hash;
|
||
};
|
||
|
||
// using EntityKey = std::tuple<WorldId_c, Pos::GlobalRegion>;
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/*
|
||
Обработчик сокета клиента.
|
||
Подписывает клиента на отслеживание необходимых ресурсов
|
||
на основе передаваемых клиенту данных
|
||
*/
|
||
class RemoteClient {
|
||
TOS::Logger LOG;
|
||
DestroyLock UseLock;
|
||
Net::AsyncSocket Socket;
|
||
bool IsConnected = true, IsGoingShutdown = false;
|
||
|
||
struct NetworkAndResource_t {
|
||
// Смена идентификаторов сервера на клиентские
|
||
SCSKeyRemapper<ServerEntityId_t, ClientEntityId_t> ReMapEntities;
|
||
// Накопленные чанки для отправки
|
||
std::unordered_map<
|
||
WorldId_t, // Миры
|
||
std::unordered_map<
|
||
Pos::GlobalRegion, // Регионы
|
||
std::pair<
|
||
std::unordered_map< // Воксели
|
||
Pos::bvec4u, // Чанки
|
||
std::u8string
|
||
>,
|
||
std::unordered_map< // Ноды
|
||
Pos::bvec4u, // Чанки
|
||
std::u8string
|
||
>
|
||
>
|
||
>
|
||
> ChunksToSend;
|
||
|
||
// Запрос информации об ассетах и профилях контента
|
||
ResourceRequest NextRequest;
|
||
// Запрошенные клиентом ресурсы
|
||
/// TODO: здесь может быть засор
|
||
std::vector<Hash_t> ClientRequested;
|
||
|
||
Net::Packet NextPacket;
|
||
std::vector<Net::Packet> SimplePackets;
|
||
void checkPacketBorder(uint16_t size) {
|
||
if(64000-NextPacket.size() < size || (NextPacket.size() != 0 && size == 0)) {
|
||
SimplePackets.push_back(std::move(NextPacket));
|
||
}
|
||
}
|
||
|
||
void prepareChunkUpdate_Voxels(
|
||
WorldId_t worldId,
|
||
Pos::GlobalRegion regionPos,
|
||
Pos::bvec4u chunkPos,
|
||
const std::u8string& compressed_voxels
|
||
) {
|
||
ChunksToSend[worldId][regionPos].first[chunkPos] = compressed_voxels;
|
||
}
|
||
|
||
void prepareChunkUpdate_Nodes(
|
||
WorldId_t worldId,
|
||
Pos::GlobalRegion regionPos,
|
||
Pos::bvec4u chunkPos,
|
||
const std::u8string& compressed_nodes
|
||
) {
|
||
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);
|
||
void prepareEntitiesUpdate(const std::vector<std::tuple<ServerEntityId_t, const Entity*>>& entities);
|
||
void prepareEntitiesUpdate_Dynamic(const std::vector<std::tuple<ServerEntityId_t, const Entity*>>& entities);
|
||
void prepareEntitySwap(ServerEntityId_t prevEntityId, ServerEntityId_t nextEntityId);
|
||
void prepareWorldUpdate(WorldId_t worldId, World* world);
|
||
};
|
||
|
||
struct {
|
||
/*
|
||
К концу такта собираются необходимые идентификаторы ресурсов
|
||
В конце такта сервер забирает запросы и возвращает информацию
|
||
о ресурсах. Отправляем связку Идентификатор + домен:ключ
|
||
+ хеш. Если у клиента не окажется этого ресурса, он может его запросить
|
||
*/
|
||
|
||
// Ресурсы, отправленные на клиент в этой сессии
|
||
std::vector<Hash_t> OnClient;
|
||
// Отправляемые на клиент ресурсы
|
||
// Ресурс, количество отправленных байт
|
||
std::vector<std::tuple<ResourceFile::Hash_t, std::shared_ptr<const std::u8string>, size_t>> ToSend;
|
||
// Пакет с ресурсами
|
||
std::vector<Net::Packet> AssetsPackets;
|
||
Net::Packet AssetsPacket;
|
||
} AssetsInWork;
|
||
|
||
TOS::SpinlockObject<NetworkAndResource_t> NetworkAndResource;
|
||
|
||
public:
|
||
const std::string Username;
|
||
Pos::Object CameraPos = {0, 0, 0};
|
||
Pos::Object LastPos = CameraPos;
|
||
ToServer::PacketQuat CameraQuat = {0};
|
||
TOS::SpinlockObject<std::queue<uint8_t>> Actions;
|
||
ResourceId RecievedAssets[(int) EnumAssets::MAX_ENUM] = {0};
|
||
|
||
// Регионы, наблюдаемые клиентом
|
||
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;
|
||
|
||
public:
|
||
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();
|
||
|
||
coro<> run();
|
||
void shutdown(EnumDisconnect type, const std::string reason);
|
||
bool isConnected() { return IsConnected; }
|
||
void setPlayerEntity(ServerEntityId_t id) { PlayerEntity = id; }
|
||
std::optional<ServerEntityId_t> getPlayerEntity() const { return PlayerEntity; }
|
||
void clearPlayerEntity() { PlayerEntity.reset(); }
|
||
|
||
void pushPackets(std::vector<Net::Packet> *simplePackets, std::vector<Net::SmartPacket> *smartPackets = nullptr) {
|
||
if(IsGoingShutdown)
|
||
return;
|
||
|
||
Socket.pushPackets(simplePackets, smartPackets);
|
||
}
|
||
|
||
// Возвращает список точек наблюдений клиентом с радиусом в регионах
|
||
std::vector<std::tuple<WorldId_t, Pos::Object, uint8_t>> getViewPoints();
|
||
|
||
/*
|
||
Сервер собирает изменения миров, сжимает их и раздаёт на отправку игрокам
|
||
*/
|
||
|
||
// Все функции prepare потокобезопасные
|
||
// maybe используются в BackingChunkPressure_t в GameServer в пуле потоков.
|
||
// если возвращает false, то блокировка сейчас находится у другого потока
|
||
// и запрос не был обработан.
|
||
|
||
// Создаёт пакет отправки вокселей чанка
|
||
void prepareChunkUpdate_Voxels(
|
||
WorldId_t worldId,
|
||
Pos::GlobalRegion regionPos,
|
||
Pos::bvec4u chunkPos,
|
||
const std::u8string& compressed_voxels
|
||
) {
|
||
NetworkAndResource.lock()->prepareChunkUpdate_Voxels(worldId, regionPos, chunkPos, compressed_voxels);
|
||
}
|
||
|
||
// Создаёт пакет отправки нод чанка
|
||
void prepareChunkUpdate_Nodes(
|
||
WorldId_t worldId,
|
||
Pos::GlobalRegion regionPos,
|
||
Pos::bvec4u chunkPos,
|
||
const std::u8string& compressed_nodes
|
||
) {
|
||
NetworkAndResource.lock()->prepareChunkUpdate_Nodes(worldId, regionPos, chunkPos, compressed_nodes);
|
||
}
|
||
|
||
// Клиент перестал наблюдать за сущностями
|
||
void prepareEntitiesRemove(const std::vector<ServerEntityId_t>& entityId) { NetworkAndResource.lock()->prepareEntitiesRemove(entityId); }
|
||
// Регион удалён из зоны видимости
|
||
void prepareRegionsRemove(WorldId_t worldId, std::vector<Pos::GlobalRegion> regionPoses) { NetworkAndResource.lock()->prepareRegionsRemove(worldId, regionPoses); }
|
||
// Мир удалён из зоны видимости
|
||
void prepareWorldRemove(WorldId_t worldId) { NetworkAndResource.lock()->prepareWorldRemove(worldId); }
|
||
|
||
// В зоне видимости добавилась новая сущность или она изменилась
|
||
void prepareEntitiesUpdate(const std::vector<std::tuple<ServerEntityId_t, const Entity*>>& entities) { NetworkAndResource.lock()->prepareEntitiesUpdate(entities); }
|
||
void prepareEntitiesUpdate_Dynamic(const std::vector<std::tuple<ServerEntityId_t, const Entity*>>& entities) { NetworkAndResource.lock()->prepareEntitiesUpdate_Dynamic(entities); }
|
||
// Наблюдаемая сущность пересекла границы региона, у неё изменился серверный идентификатор
|
||
void prepareEntitySwap(ServerEntityId_t prevEntityId, ServerEntityId_t nextEntityId) { NetworkAndResource.lock()->prepareEntitySwap(prevEntityId, nextEntityId); }
|
||
// Мир появился в зоне видимости или изменился
|
||
void prepareWorldUpdate(WorldId_t worldId, World* world) { NetworkAndResource.lock()->prepareWorldUpdate(worldId, world); }
|
||
|
||
// В зоне видимости добавился порта или он изменился
|
||
// void preparePortalUpdate(PortalId_t portalId, void* portal);
|
||
// Клиент перестал наблюдать за порталом
|
||
// void preparePortalRemove(PortalId_t portalId);
|
||
|
||
// Прочие моменты
|
||
void prepareCameraSetEntity(ServerEntityId_t entityId);
|
||
|
||
// Отправка подготовленных пакетов
|
||
ResourceRequest pushPreparedPackets();
|
||
|
||
// Создаёт пакет для всех игроков с оповещением о новых идентификаторах (id -> domain+key)
|
||
static Net::Packet makePacket_informateAssets_DK(
|
||
const std::array<
|
||
std::vector<AssetsManager::BindDomainKeyInfo>,
|
||
static_cast<size_t>(EnumAssets::MAX_ENUM)
|
||
>& dkVector
|
||
);
|
||
|
||
// Создаёт пакет для всех игроков с оповещением об изменении файлов ресурсов (id -> hash+header)
|
||
static Net::Packet makePacket_informateAssets_HH(
|
||
const std::array<
|
||
std::vector<AssetsManager::BindHashHeaderInfo>,
|
||
static_cast<size_t>(EnumAssets::MAX_ENUM)
|
||
>& hhVector,
|
||
const std::array<
|
||
std::vector<ResourceId>,
|
||
static_cast<size_t>(EnumAssets::MAX_ENUM)
|
||
>& lost
|
||
);
|
||
|
||
// Оповещение о двоичных ресурсах (стриминг по запросу)
|
||
void informateBinaryAssets(
|
||
const std::vector<std::tuple<ResourceFile::Hash_t, std::shared_ptr<const std::u8string>>>& resources
|
||
);
|
||
|
||
// Создаёт пакет об обновлении игровых профилей
|
||
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 // Новые привязки
|
||
);
|
||
|
||
void onUpdate();
|
||
|
||
private:
|
||
GameServer* Server = nullptr;
|
||
void protocolError();
|
||
coro<> readPacket(Net::AsyncSocket &sock);
|
||
coro<> rP_System(Net::AsyncSocket &sock);
|
||
};
|
||
|
||
|
||
}
|