space-game001/server/server.cpp
2026-02-11 21:48:25 +03:00

697 lines
21 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <mutex>
#include <map>
#include <queue>
#include <sstream>
#include <Eigen/Dense>
#define _USE_MATH_DEFINES
#include <math.h>
#include "../src/network/ClientState.h"
#include <random>
#include <algorithm>
#include <chrono>
#include <unordered_set>
namespace beast = boost::beast;
namespace http = beast::http;
namespace websocket = beast::websocket;
namespace net = boost::asio;
using tcp = net::ip::tcp;
struct DeathInfo {
int targetId = -1;
uint64_t serverTime = 0;
Eigen::Vector3f position = Eigen::Vector3f::Zero();
int killerId = -1;
};
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;
}
// Вспомогательная функция для проверки столкновения снаряда с объектом-сферой
bool checkSegmentSphereCollision(
int x,
const Eigen::Vector3f& pStart,
const Eigen::Vector3f& pEnd,
const Eigen::Vector3f& targetCenter,
float combinedRadius)
{
Eigen::Vector3f segment = pEnd - pStart;
Eigen::Vector3f toTarget = targetCenter - pStart;
float segmentLenSq = segment.squaredNorm();
if (segmentLenSq < 1e-6f) {
return toTarget.norm() <= combinedRadius;
}
// Находим проекцию точки targetCenter на прямую, содержащую отрезок
// t — это нормализованный параметр вдоль отрезка (от 0 до 1)
float t = toTarget.dot(segment) / segmentLenSq;
// Ограничиваем t, чтобы найти ближайшую точку именно на ОТРЕЗКЕ
t = std::max(0.0f, std::min(1.0f, t));
// Ближайшая точка на отрезке к центру цели
Eigen::Vector3f closestPoint = pStart + t * segment;
/*
std::cout << "Collision for box: " << x << " pStart: " << pStart
<< " pEnd: " << pEnd
<< " targetCenter: " << targetCenter
<< " closestPoint: " << closestPoint
<< " t: " << t << std::endl;
*/
// Проверяем расстояние от ближайшей точки до центра цели
return (targetCenter - closestPoint).squaredNorm() <= (combinedRadius * combinedRadius);
}
struct ServerBox {
Eigen::Vector3f position;
Eigen::Matrix3f rotation;
float collisionRadius = 2.0f;
bool destroyed = false;
};
struct Projectile {
int shooterId = -1;
uint64_t spawnMs = 0;
Eigen::Vector3f pos;
Eigen::Vector3f vel;
float lifeMs = 5000.0f;
};
struct BoxDestroyedInfo {
int boxIndex = -1;
uint64_t serverTime = 0;
Eigen::Vector3f position = Eigen::Vector3f::Zero();
int destroyedBy = -1;
};
std::vector<ServerBox> g_serverBoxes;
std::mutex g_boxes_mutex;
std::vector<std::shared_ptr<class Session>> g_sessions;
std::mutex g_sessions_mutex;
std::vector<Projectile> g_projectiles;
std::mutex g_projectiles_mutex;
std::unordered_set<int> g_dead_players;
std::mutex g_dead_mutex;
class Session;
void broadcastToAll(const std::string& message);
class Session : public std::enable_shared_from_this<Session> {
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
int id_;
bool is_writing_ = false;
std::queue<std::shared_ptr<std::string>> writeQueue_;
std::mutex writeMutex_;
public:
ClientStateInterval timedClientStates;
explicit Session(tcp::socket&& socket, int id)
: ws_(std::move(socket)), id_(id) {
}
int get_id() const { return id_; }
bool fetchStateAtTime(std::chrono::system_clock::time_point targetTime, ClientState& outState) const {
if (timedClientStates.canFetchClientStateAtTime(targetTime)) {
outState = timedClientStates.fetchClientStateAtTime(targetTime);
return true;
}
return false;
}
void send_message(const std::string& msg) {
auto ss = std::make_shared<std::string>(msg);
{
std::lock_guard<std::mutex> lock(writeMutex_);
writeQueue_.push(ss);
}
doWrite();
}
void run() {
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
g_sessions.push_back(shared_from_this());
}
ws_.async_accept([self = shared_from_this()](beast::error_code ec) {
if (ec) return;
std::cout << "Client " << self->id_ << " connected\n";
self->init();
});
}
bool IsMessageValid(const std::string& fullMessage) {
#ifdef ENABLE_NETWORK_CHECKSUM
size_t hashPos = fullMessage.find("#hash:");
if (hashPos == std::string::npos) {
return false; // Хеша нет, хотя он ожидался
}
std::string originalContent = fullMessage.substr(0, hashPos);
std::string receivedHashStr = fullMessage.substr(hashPos + 6); // 6 — длина "#hash:"
// Вычисляем ожидаемый хеш от контента
size_t expectedHash = fnv1a_hash(originalContent + NET_SECRET);
std::stringstream ss;
ss << std::hex << expectedHash;
return ss.str() == receivedHashStr;
#else
return true; // В режиме отладки пропускаем всё
#endif
}
private:
void sendBoxesToClient() {
std::lock_guard<std::mutex> lock(g_boxes_mutex);
std::string boxMsg = "BOXES:";
for (const auto& box : g_serverBoxes) {
Eigen::Quaternionf q(box.rotation);
boxMsg += std::to_string(box.position.x()) + ":" +
std::to_string(box.position.y()) + ":" +
std::to_string(box.position.z()) + ":" +
std::to_string(q.w()) + ":" +
std::to_string(q.x()) + ":" +
std::to_string(q.y()) + ":" +
std::to_string(q.z()) + "|";
}
if (!boxMsg.empty() && boxMsg.back() == '|') boxMsg.pop_back();
send_message(boxMsg);
}
public:
void init()
{
sendBoxesToClient();
auto timer = std::make_shared<net::steady_timer>(ws_.get_executor());
timer->expires_after(std::chrono::milliseconds(100));
timer->async_wait([self = shared_from_this(), timer](const boost::system::error_code& ec) {
if (!ec) {
int64_t serverNow = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
self->send_message("ID:" + std::to_string(self->id_) + ":" + std::to_string(serverNow));
self->do_read();
}
});
}
ClientState get_latest_state(std::chrono::system_clock::time_point now) {
if (timedClientStates.timedStates.empty()) {
return {};
}
// 1. Берем самое последнее известное состояние
ClientState latest = timedClientStates.timedStates.back();
// 3. Применяем компенсацию лага (экстраполяцию).
// Функция внутри использует simulate_physics, чтобы переместить объект
// из точки lastUpdateServerTime в точку now.
latest.apply_lag_compensation(now);
return latest;
}
void doWrite() {
std::lock_guard<std::mutex> lock(writeMutex_);
if (is_writing_ || writeQueue_.empty()) {
return;
}
is_writing_ = true;
auto message = writeQueue_.front();
ws_.async_write(net::buffer(*message),
[self = shared_from_this(), message](beast::error_code ec, std::size_t) {
if (ec) {
std::cerr << "Write error: " << ec.message() << std::endl;
return;
}
{
std::lock_guard<std::mutex> lock(self->writeMutex_);
self->writeQueue_.pop();
self->is_writing_ = false;
}
self->doWrite();
});
}
private:
void do_read() {
ws_.async_read(buffer_, [self = shared_from_this()](beast::error_code ec, std::size_t) {
if (ec) {
std::lock_guard<std::mutex> lock(g_sessions_mutex);
g_sessions.erase(std::remove_if(g_sessions.begin(), g_sessions.end(),
[self](const std::shared_ptr<Session>& session) {
return session.get() == self.get();
}), g_sessions.end());
std::cout << "Client " << self->id_ << " disconnected\n";
return;
}
std::string msg = beast::buffers_to_string(self->buffer_.data());
self->process_message(msg);
self->buffer_.consume(self->buffer_.size());
self->do_read();
});
}
void process_message(const std::string& msg) {
if (!IsMessageValid(msg)) {
// Логируем попытку подмены и просто выходим из обработки
std::cout << "[Security] Invalid packet hash. Dropping message: " << msg << std::endl;
return;
}
std::string cleanMessage = msg.substr(0, msg.find("#hash:"));
std::cout << "Received from player " << id_ << ": " << cleanMessage << std::endl;
auto parts = split(cleanMessage, ':');
if (parts.empty()) return;
std::string type = parts[0];
if (type == "UPD") {
{
std::lock_guard<std::mutex> gd(g_dead_mutex);
if (g_dead_players.find(id_) != g_dead_players.end()) {
std::cout << "Server: Ignoring UPD from dead player " << id_ << std::endl;
return;
}
}
if (parts.size() < 16) return;
uint64_t clientTimestamp = std::stoull(parts[1]);
ClientState receivedState;
receivedState.id = id_;
std::chrono::system_clock::time_point uptime_timepoint{
std::chrono::milliseconds(clientTimestamp)
};
receivedState.lastUpdateServerTime = uptime_timepoint;
receivedState.handle_full_sync(parts, 2);
timedClientStates.add_state(receivedState);
retranslateMessage(cleanMessage);
}
else if (parts[0] == "RESPAWN") {
{
std::lock_guard<std::mutex> gd(g_dead_mutex);
g_dead_players.erase(id_);
}
std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_);
broadcastToAll(respawnMsg);
std::cout << "Server: Player " << id_ << " respawned\n";
}
else if (parts[0] == "FIRE") {
if (parts.size() < 10) return;
uint64_t clientTime = std::stoull(parts[1]);
Eigen::Vector3f pos{
std::stof(parts[2]), std::stof(parts[3]), std::stof(parts[4])
};
Eigen::Quaternionf dir(
std::stof(parts[5]), std::stof(parts[6]), std::stof(parts[7]), std::stof(parts[8])
);
float velocity = std::stof(parts[9]);
int shotCount = 2;
if (parts.size() >= 11) {
try { shotCount = std::stoi(parts[10]); }
catch (...) { shotCount = 2; }
}
std::string broadcast = "PROJECTILE:" +
std::to_string(id_) + ":" +
std::to_string(clientTime) + ":" +
std::to_string(pos.x()) + ":" +
std::to_string(pos.y()) + ":" +
std::to_string(pos.z()) + ":" +
std::to_string(dir.w()) + ":" +
std::to_string(dir.x()) + ":" +
std::to_string(dir.y()) + ":" +
std::to_string(dir.z()) + ":" +
std::to_string(velocity);
{
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& session : g_sessions) {
if (session->get_id() != id_) {
session->send_message(broadcast);
}
}
}
{
const std::vector<Eigen::Vector3f> localOffsets = {
Eigen::Vector3f(-1.5f, 0.9f, 5.0f),
Eigen::Vector3f(1.5f, 0.9f, 5.0f)
};
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
for (int i = 0; i < std::min(shotCount, (int)localOffsets.size()); ++i) {
Projectile pr;
pr.shooterId = id_;
pr.spawnMs = now_ms;
Eigen::Vector3f shotPos = pos + dir.toRotationMatrix() * localOffsets[i];
pr.pos = shotPos;
Eigen::Vector3f localForward(0.0f, 0.0f, -1.0f);
Eigen::Vector3f worldForward = dir.toRotationMatrix() * localForward;
float len = worldForward.norm();
if (len > 1e-6f) worldForward /= len;
pr.vel = worldForward * velocity;
pr.lifeMs = 5000.0f;
g_projectiles.push_back(pr);
std::cout << "Server: Created projectile from player " << id_
<< " at pos (" << shotPos.x() << ", " << shotPos.y() << ", " << shotPos.z()
<< ") vel (" << pr.vel.x() << ", " << pr.vel.y() << ", " << pr.vel.z() << ")" << std::endl;
}
}
}
}
void retranslateMessage(const std::string& msg) {
std::string event_msg = "EVENT:" + std::to_string(id_) + ":" + msg;
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (auto& session : g_sessions) {
if (session->get_id() != id_) {
session->send_message(event_msg);
}
}
}
};
void broadcastToAll(const std::string& message) {
std::lock_guard<std::mutex> lock(g_sessions_mutex);
for (const auto& session : g_sessions) {
session->send_message(message);
}
}
void checkShipBoxCollisions(std::chrono::system_clock::time_point now, uint64_t now_ms, std::vector<BoxDestroyedInfo>& boxDestructions) {
// Внимание: Мьютексы g_boxes_mutex и g_sessions_mutex должны быть захвачены
// внешним кодом в update_world перед вызовом этой функции.
const float shipCollisionRadius = 15.0f;
const float boxCollisionRadius = 2.0f;
const float thresh = shipCollisionRadius + boxCollisionRadius;
const float threshSq = thresh * thresh;
for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
if (g_serverBoxes[bi].destroyed) continue;
// Центр ящика в мировых координатах
Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
for (auto& session : g_sessions) {
int playerId = session->get_id();
// Пропускаем мертвых игроков
{
// Если g_dead_mutex не захвачен глобально в update_world, раскомментируйте:
// std::lock_guard<std::mutex> gd(g_dead_mutex);
if (g_dead_players.count(playerId)) continue;
}
ClientState shipState;
// Получаем состояние игрока на текущий момент времени сервера
if (!session->fetchStateAtTime(now, shipState)) continue;
Eigen::Vector3f diff = shipState.position - boxWorld;
// Проверка столкновения сфер
if (diff.squaredNorm() <= threshSq) {
g_serverBoxes[bi].destroyed = true;
// Регистрируем уничтожение ящика
BoxDestroyedInfo destruction;
destruction.boxIndex = static_cast<int>(bi);
destruction.serverTime = now_ms;
destruction.position = boxWorld;
destruction.destroyedBy = playerId;
boxDestructions.push_back(destruction);
std::cout << "Server: Box " << bi << " smashed by player " << playerId << std::endl;
// Один ящик не может быть уничтожен дважды за один проход
break;
}
}
}
}
void dispatchEvents(const std::vector<DeathInfo>& deathEvents, const std::vector<BoxDestroyedInfo>& boxDestructions) {
// 1. Рассылка событий смерти игроков
for (const auto& death : deathEvents) {
std::string deadMsg = "DEAD:" +
std::to_string(death.serverTime) + ":" +
std::to_string(death.targetId) + ":" +
std::to_string(death.position.x()) + ":" +
std::to_string(death.position.y()) + ":" +
std::to_string(death.position.z()) + ":" +
std::to_string(death.killerId);
broadcastToAll(deadMsg);
std::cout << "Server: Sent DEAD event - Player " << death.targetId
<< " killed by " << death.killerId << std::endl;
}
// 2. Рассылка событий разрушения ящиков
for (const auto& destruction : boxDestructions) {
std::string boxMsg = "BOX_DESTROYED:" +
std::to_string(destruction.boxIndex) + ":" +
std::to_string(destruction.serverTime) + ":" +
std::to_string(destruction.position.x()) + ":" +
std::to_string(destruction.position.y()) + ":" +
std::to_string(destruction.position.z()) + ":" +
std::to_string(destruction.destroyedBy);
broadcastToAll(boxMsg);
std::cout << "Server: Broadcasted BOX_DESTROYED for box "
<< destruction.boxIndex << std::endl;
}
}
void update_world(net::steady_timer& timer, net::io_context& ioc) {
const std::chrono::milliseconds interval(50);
timer.expires_after(interval);
timer.async_wait([&](const boost::system::error_code& ec) {
if (ec) return;
auto now = std::chrono::system_clock::now();
uint64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
float dt = 50.0f / 1000.0f;
std::vector<DeathInfo> deathEvents;
std::vector<BoxDestroyedInfo> boxDestructions;
std::vector<int> projectilesToRemove;
{
// Захватываем необходимые данные под мьютексами один раз
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
std::lock_guard<std::mutex> bm(g_boxes_mutex);
std::lock_guard<std::mutex> sm(g_sessions_mutex);
std::lock_guard<std::mutex> gm(g_dead_mutex);
for (size_t i = 0; i < g_projectiles.size(); ++i) {
auto& pr = g_projectiles[i];
Eigen::Vector3f oldPos = pr.pos;
pr.pos += pr.vel * dt;
Eigen::Vector3f newPos = pr.pos;
// 1. Проверка времени жизни снаряда
if (now_ms > pr.spawnMs + static_cast<uint64_t>(pr.lifeMs)) {
projectilesToRemove.push_back(static_cast<int>(i));
continue;
}
bool hitDetected = false;
// 2. Проверка коллизий снаряда с игроками (Ray-cast)
for (auto& session : g_sessions) {
int targetId = session->get_id();
if (targetId == pr.shooterId || g_dead_players.count(targetId)) continue;
ClientState targetState;
if (!session->fetchStateAtTime(now, targetState)) continue;
if (checkSegmentSphereCollision(0, oldPos, newPos, targetState.position, 15.0f + 1.5f)) {
deathEvents.push_back({ targetId, now_ms, newPos, pr.shooterId });
g_dead_players.insert(targetId);
hitDetected = true;
break;
}
}
if (hitDetected) {
projectilesToRemove.push_back(static_cast<int>(i));
continue;
}
// 3. Проверка коллизий снаряда с ящиками (Ray-cast)
for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
if (g_serverBoxes[bi].destroyed) continue;
// Центр ящика с учетом смещения мира
Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
if (checkSegmentSphereCollision(bi, oldPos, newPos, boxWorld, 2.0f + 1.5f)) {
g_serverBoxes[bi].destroyed = true;
boxDestructions.push_back({ static_cast<int>(bi), now_ms, boxWorld, pr.shooterId });
hitDetected = true;
break;
}
}
if (hitDetected) {
projectilesToRemove.push_back(static_cast<int>(i));
}
}
// Удаляем отработавшие снаряды (с конца)
std::sort(projectilesToRemove.rbegin(), projectilesToRemove.rend());
for (int idx : projectilesToRemove) {
g_projectiles.erase(g_projectiles.begin() + idx);
}
}
// 4. Отдельная проверка столкновения кораблей с ящиками (Point-Sphere)
// Эту логику оставляем отдельно, так как она не привязана к снарядам
checkShipBoxCollisions(now, now_ms, boxDestructions);
// Рассылка событий
dispatchEvents(deathEvents, boxDestructions);
update_world(timer, ioc);
});
}
std::vector<ServerBox> generateServerBoxes(int count) {
std::vector<ServerBox> boxes;
std::random_device rd;
std::mt19937 gen(rd());
const float MIN_COORD = -100.0f;
const float MAX_COORD = 100.0f;
const float MIN_DISTANCE = 3.0f;
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
const int MAX_ATTEMPTS = 1000;
std::uniform_real_distribution<> posDistrib(MIN_COORD, MAX_COORD);
std::uniform_real_distribution<> angleDistrib(0.0, M_PI * 2.0);
for (int i = 0; i < count; i++) {
bool accepted = false;
int attempts = 0;
while (!accepted && attempts < MAX_ATTEMPTS) {
ServerBox box;
box.position = Eigen::Vector3f(
(float)posDistrib(gen),
(float)posDistrib(gen),
(float)posDistrib(gen)
);
accepted = true;
for (const auto& existingBox : boxes) {
Eigen::Vector3f diff = box.position - existingBox.position;
if (diff.squaredNorm() < MIN_DISTANCE_SQUARED) {
accepted = false;
break;
}
}
if (accepted) {
float randomAngle = (float)angleDistrib(gen);
Eigen::Vector3f axis = Eigen::Vector3f::Random().normalized();
box.rotation = Eigen::AngleAxisf(randomAngle, axis).toRotationMatrix();
boxes.push_back(box);
}
attempts++;
}
}
return boxes;
}
int main() {
try {
{
std::lock_guard<std::mutex> lock(g_boxes_mutex);
g_serverBoxes = generateServerBoxes(50);
std::cout << "Generated " << g_serverBoxes.size() << " boxes on server\n";
}
net::io_context ioc;
tcp::acceptor acceptor{ ioc, {tcp::v4(), 8081} };
int next_id = 1000;
std::cout << "Server started on port 8081...\n";
auto do_accept = [&](auto& self_fn) -> void {
acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) {
if (!ec) {
std::make_shared<Session>(std::move(socket), next_id++)->run();
}
self_fn(self_fn);
});
};
net::steady_timer timer(ioc);
update_world(timer, ioc);
do_accept(do_accept);
ioc.run();
}
catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}