332 lines
9.6 KiB
C++
332 lines
9.6 KiB
C++
#ifdef NETWORK
|
||
|
||
#include "WebSocketClient.h"
|
||
#include <iostream>
|
||
#include <SDL2/SDL.h>
|
||
|
||
// Вспомогательный split
|
||
std::vector<std::string> split(const std::string& s, char delimiter) {
|
||
std::vector<std::string> tokens;
|
||
std::string token;
|
||
std::istringstream tokenStream(s);
|
||
while (std::getline(tokenStream, token, delimiter)) {
|
||
tokens.push_back(token);
|
||
}
|
||
return tokens;
|
||
}
|
||
|
||
namespace ZL {
|
||
|
||
void WebSocketClient::Connect(const std::string& host, uint16_t port) {
|
||
try {
|
||
boost::asio::ip::tcp::resolver resolver(ioc_);
|
||
auto const results = resolver.resolve(host, std::to_string(port));
|
||
|
||
ws_ = std::make_unique<boost::beast::websocket::stream<boost::beast::tcp_stream>>(ioc_);
|
||
|
||
// Выполняем синхронный коннект и handshake для простоты старта
|
||
boost::beast::get_lowest_layer(*ws_).connect(results);
|
||
ws_->handshake(host, "/");
|
||
|
||
connected = true;
|
||
|
||
// Запускаем асинхронное чтение в пуле потоков TaskManager
|
||
startAsyncRead();
|
||
|
||
}
|
||
catch (std::exception& e) {
|
||
std::cerr << "Network Error: " << e.what() << std::endl;
|
||
}
|
||
}
|
||
|
||
void WebSocketClient::startAsyncRead() {
|
||
ws_->async_read(buffer_, [this](boost::beast::error_code ec, std::size_t bytes) {
|
||
if (!ec) {
|
||
std::string msg = boost::beast::buffers_to_string(buffer_.data());
|
||
buffer_.consume(bytes);
|
||
processIncomingMessage(msg);
|
||
startAsyncRead();
|
||
}
|
||
else {
|
||
connected = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
void WebSocketClient::processIncomingMessage(const std::string& msg) {
|
||
// Логика парсинга...
|
||
if (msg.rfind("ID:", 0) == 0) {
|
||
clientId = std::stoi(msg.substr(3));
|
||
}
|
||
|
||
// Безопасно кладем в очередь для главного потока
|
||
std::lock_guard<std::mutex> lock(queueMutex);
|
||
messageQueue.push(msg);
|
||
}
|
||
|
||
std::vector<ProjectileInfo> WebSocketClient::getPendingProjectiles() {
|
||
std::lock_guard<std::mutex> lock(projMutex_);
|
||
auto copy = pendingProjectiles_;
|
||
pendingProjectiles_.clear();
|
||
return copy;
|
||
}
|
||
|
||
std::vector<DeathInfo> WebSocketClient::getPendingDeaths() {
|
||
std::lock_guard<std::mutex> lock(deathsMutex_);
|
||
auto copy = pendingDeaths_;
|
||
pendingDeaths_.clear();
|
||
return copy;
|
||
}
|
||
|
||
std::vector<int> WebSocketClient::getPendingRespawns() {
|
||
std::lock_guard<std::mutex> lock(respawnMutex_);
|
||
auto copy = pendingRespawns_;
|
||
pendingRespawns_.clear();
|
||
return copy;
|
||
}
|
||
|
||
void WebSocketClient::Poll() {
|
||
std::lock_guard<std::mutex> lock(queueMutex);
|
||
|
||
while (!messageQueue.empty()) {
|
||
|
||
auto nowTime = std::chrono::system_clock::now();
|
||
|
||
//Apply server delay:
|
||
nowTime -= std::chrono::milliseconds(CLIENT_DELAY);
|
||
|
||
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||
nowTime.time_since_epoch()
|
||
).count();
|
||
|
||
|
||
|
||
std::string msg = messageQueue.front();
|
||
messageQueue.pop();
|
||
|
||
// Обработка списка коробок от сервера
|
||
if (msg.rfind("BOXES:", 0) == 0) {
|
||
std::string payload = msg.substr(6); // после "BOXES:"
|
||
std::vector<std::pair<Eigen::Vector3f, Eigen::Matrix3f>> parsedBoxes;
|
||
if (!payload.empty()) {
|
||
auto items = split(payload, '|');
|
||
for (auto& item : items) {
|
||
if (item.empty()) continue;
|
||
auto parts = split(item, ':');
|
||
if (parts.size() < 7) continue;
|
||
try {
|
||
float px = std::stof(parts[0]);
|
||
float py = std::stof(parts[1]);
|
||
float pz = std::stof(parts[2]);
|
||
Eigen::Quaternionf q(
|
||
std::stof(parts[3]),
|
||
std::stof(parts[4]),
|
||
std::stof(parts[5]),
|
||
std::stof(parts[6])
|
||
);
|
||
Eigen::Matrix3f rot = q.toRotationMatrix();
|
||
parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot);
|
||
}
|
||
catch (...) {
|
||
// пропускаем некорректную запись
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
{
|
||
std::lock_guard<std::mutex> bLock(boxesMutex);
|
||
serverBoxes_ = std::move(parsedBoxes);
|
||
}
|
||
continue;
|
||
}
|
||
if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
|
||
auto parts = split(msg, ':');
|
||
if (parts.size() >= 2) {
|
||
try {
|
||
int respawnedPlayerId = std::stoi(parts[1]);
|
||
{
|
||
std::lock_guard<std::mutex> rLock(respawnMutex_);
|
||
pendingRespawns_.push_back(respawnedPlayerId);
|
||
}
|
||
{
|
||
std::lock_guard<std::mutex> pLock(playersMutex);
|
||
remotePlayers.erase(respawnedPlayerId);
|
||
}
|
||
std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl;
|
||
}
|
||
catch (...) {}
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (msg.rfind("PROJECTILE:", 0) == 0) {
|
||
auto parts = split(msg, ':');
|
||
if (parts.size() >= 10) {
|
||
try {
|
||
ProjectileInfo pi;
|
||
pi.shooterId = std::stoi(parts[1]);
|
||
pi.clientTime = std::stoull(parts[2]);
|
||
pi.position = Eigen::Vector3f(
|
||
std::stof(parts[3]),
|
||
std::stof(parts[4]),
|
||
std::stof(parts[5])
|
||
);
|
||
Eigen::Quaternionf q(
|
||
std::stof(parts[6]),
|
||
std::stof(parts[7]),
|
||
std::stof(parts[8]),
|
||
std::stof(parts[9])
|
||
);
|
||
pi.rotation = q.toRotationMatrix();
|
||
|
||
pi.velocity = std::stof(parts[10]);
|
||
std::lock_guard<std::mutex> pl(projMutex_);
|
||
pendingProjectiles_.push_back(pi);
|
||
}
|
||
catch (...) {}
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (msg.rfind("DEAD:", 0) == 0) {
|
||
auto parts = split(msg, ':');
|
||
if (parts.size() >= 7) {
|
||
try {
|
||
DeathInfo di;
|
||
di.serverTime = std::stoull(parts[1]);
|
||
di.targetId = std::stoi(parts[2]);
|
||
di.position = Eigen::Vector3f(
|
||
std::stof(parts[3]),
|
||
std::stof(parts[4]),
|
||
std::stof(parts[5])
|
||
);
|
||
di.killerId = std::stoi(parts[6]);
|
||
|
||
std::lock_guard<std::mutex> dl(deathsMutex_);
|
||
pendingDeaths_.push_back(di);
|
||
}
|
||
catch (...) {}
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (msg.rfind("EVENT:", 0) == 0) {
|
||
auto parts = split(msg, ':');
|
||
if (parts.size() < 5) continue; // EVENT:ID:TYPE:TIME:DATA...
|
||
|
||
int remoteId = std::stoi(parts[1]);
|
||
std::string subType = parts[2];
|
||
uint64_t sentTime = std::stoull(parts[3]);
|
||
|
||
ClientState remoteState;
|
||
remoteState.id = remoteId;
|
||
|
||
std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast<std::chrono::system_clock::time_point::duration>(std::chrono::milliseconds(sentTime)) };
|
||
remoteState.lastUpdateServerTime = uptime_timepoint;
|
||
|
||
if (subType == "UPD") {
|
||
int startFrom = 4;
|
||
remoteState.position = { std::stof(parts[startFrom]), std::stof(parts[startFrom + 1]), std::stof(parts[startFrom + 2]) };
|
||
Eigen::Quaternionf q(
|
||
std::stof(parts[startFrom + 3]),
|
||
std::stof(parts[startFrom + 4]),
|
||
std::stof(parts[startFrom + 5]),
|
||
std::stof(parts[startFrom + 6]));
|
||
remoteState.rotation = q.toRotationMatrix();
|
||
|
||
remoteState.currentAngularVelocity = Eigen::Vector3f{
|
||
std::stof(parts[startFrom + 7]),
|
||
std::stof(parts[startFrom + 8]),
|
||
std::stof(parts[startFrom + 9]) };
|
||
remoteState.velocity = std::stof(parts[startFrom + 10]);
|
||
remoteState.selectedVelocity = std::stoi(parts[startFrom + 11]);
|
||
remoteState.discreteMag = std::stof(parts[startFrom + 12]);
|
||
remoteState.discreteAngle = std::stoi(parts[startFrom + 13]);
|
||
}
|
||
else
|
||
{
|
||
throw std::runtime_error("Unknown EVENT subtype: " + subType);
|
||
}
|
||
|
||
{
|
||
std::lock_guard<std::mutex> pLock(playersMutex);
|
||
auto& rp = remotePlayers[remoteId];
|
||
|
||
rp.add_state(remoteState);
|
||
}
|
||
}
|
||
|
||
if (msg.rfind("SNAPSHOT:", 0) == 0) {
|
||
auto mainParts = split(msg.substr(9), '|'); // Отсекаем "SNAPSHOT:" и делим по игрокам
|
||
if (mainParts.empty()) continue;
|
||
|
||
uint64_t serverTimestamp = std::stoull(mainParts[0]);
|
||
std::chrono::system_clock::time_point serverTime{ std::chrono::milliseconds(serverTimestamp) };
|
||
|
||
for (size_t i = 1; i < mainParts.size(); ++i) {
|
||
auto playerParts = split(mainParts[i], ':');
|
||
if (playerParts.size() < 15) continue; // ID + 14 полей ClientState
|
||
|
||
int rId = std::stoi(playerParts[0]);
|
||
if (rId == clientId) continue; // Свое состояние игрок знает лучше всех (Client Side Prediction)
|
||
|
||
ClientState remoteState;
|
||
remoteState.id = rId;
|
||
remoteState.lastUpdateServerTime = serverTime;
|
||
|
||
// Используем твой handle_full_sync, начиная со 2-го индекса (пропускаем ID в playerParts)
|
||
remoteState.handle_full_sync(playerParts, 1);
|
||
|
||
{
|
||
std::lock_guard<std::mutex> pLock(playersMutex);
|
||
remotePlayers[rId].add_state(remoteState);
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
|
||
}
|
||
}
|
||
void WebSocketClient::Send(const std::string& message) {
|
||
if (!connected) return;
|
||
|
||
auto ss = std::make_shared<std::string>(message);
|
||
|
||
std::lock_guard<std::mutex> lock(writeMutex_);
|
||
writeQueue_.push(ss);
|
||
|
||
// Если сейчас ничего не записывается, инициируем первую запись
|
||
if (!isWriting_) {
|
||
doWrite();
|
||
}
|
||
}
|
||
|
||
void WebSocketClient::doWrite() {
|
||
// Эта функция всегда вызывается под мьютексом или из колбэка
|
||
if (writeQueue_.empty()) {
|
||
isWriting_ = false;
|
||
return;
|
||
}
|
||
|
||
isWriting_ = true;
|
||
auto message = writeQueue_.front();
|
||
|
||
// Захватываем self (shared_from_this), чтобы объект не удалился во время записи
|
||
ws_->async_write(
|
||
boost::asio::buffer(*message),
|
||
[this, message](boost::beast::error_code ec, std::size_t) {
|
||
if (ec) {
|
||
connected = false;
|
||
return;
|
||
}
|
||
|
||
std::lock_guard<std::mutex> lock(writeMutex_);
|
||
writeQueue_.pop(); // Удаляем отправленное сообщение
|
||
doWrite(); // Проверяем следующее
|
||
}
|
||
);
|
||
}
|
||
}
|
||
|
||
#endif
|