space-game001/server/server.cpp
Vladislav Khorev 8a374a4231 Merge with main
2026-02-08 15:31:25 +03:00

632 lines
17 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;
}
struct ServerBox {
Eigen::Vector3f position;
Eigen::Matrix3f rotation;
float collisionRadius = 2.0f;
};
struct Projectile {
int shooterId = -1;
uint64_t spawnMs = 0;
Eigen::Vector3f pos;
Eigen::Vector3f vel;
float lifeMs = 5000.0f;
};
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();
});
}
private:
/*
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) {
self->send_message("ID:" + std::to_string(self->id_));
self->do_read();
}
});
}
*/
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:
/*
explicit Session(tcp::socket&& socket, int id)
: ws_(std::move(socket)), id_(id) {
}*/
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) {
self->send_message("ID:" + std::to_string(self->id_));
self->do_read();
}
});
}
/*
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();
// self->send_message("ID:" + std::to_string(self->id_));
// self->do_read();
});
}*/
/*void send_message(std::string msg) {
auto ss = std::make_shared<std::string>(std::move(msg));
ws_.async_write(net::buffer(*ss), [ss](beast::error_code, std::size_t) {});
}
int get_id() const {
return id_;
}*/
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 send_message(std::string msg) {
auto ss = std::make_shared<std::string>(std::move(msg));
if (is_writing_) {
ws_.async_write(net::buffer(*ss),
[self = shared_from_this(), ss](beast::error_code ec, std::size_t) {
if (ec) {
std::cerr << "Write error: " << ec.message() << std::endl;
}
});
}
else {
is_writing_ = true;
ws_.async_write(net::buffer(*ss),
[self = shared_from_this(), ss](beast::error_code ec, std::size_t) {
self->is_writing_ = false;
if (ec) {
std::cerr << "Write error: " << ec.message() << std::endl;
}
});
}
}*/
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) {
auto parts = split(msg, ':');
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(msg);
}
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 update_world(net::steady_timer& timer, net::io_context& ioc) {
static auto last_snapshot_time = std::chrono::steady_clock::now();
auto now = std::chrono::steady_clock::now();
// Рассылка Snapshot раз в 1000мс
/*
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_snapshot_time).count() >= 1000) {
last_snapshot_time = now;
auto system_now = std::chrono::system_clock::now();
std::string snapshot_msg = "SNAPSHOT:" + std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(
system_now.time_since_epoch()).count()
);
std::lock_guard<std::mutex> lock(g_sessions_mutex);
// Формируем общую строку состояний всех игроков
for (auto& session : g_sessions) {
ClientState st = session->get_latest_state(system_now);
snapshot_msg += "|" + std::to_string(session->get_id()) + ":" + st.formPingMessageContent();
}
for (auto& session : g_sessions) {
session->send_message(snapshot_msg);
}
}*/
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 = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count());
std::vector<DeathInfo> deathEvents;
{
std::lock_guard<std::mutex> pl(g_projectiles_mutex);
std::vector<int> indicesToRemove;
float dt = 50.0f / 1000.0f;
for (size_t i = 0; i < g_projectiles.size(); ++i) {
auto& pr = g_projectiles[i];
pr.pos += pr.vel * dt;
if (now_ms > pr.spawnMs + static_cast<uint64_t>(pr.lifeMs)) {
indicesToRemove.push_back(static_cast<int>(i));
continue;
}
bool hitDetected = false;
{
std::lock_guard<std::mutex> lm(g_sessions_mutex);
std::lock_guard<std::mutex> gd(g_dead_mutex);
for (auto& session : g_sessions) {
int targetId = session->get_id();
if (targetId == pr.shooterId) continue;
if (g_dead_players.find(targetId) != g_dead_players.end()) continue;
ClientState targetState;
if (!session->fetchStateAtTime(now, targetState)) continue;
Eigen::Vector3f diff = pr.pos - targetState.position;
const float shipRadius = 15.0f;
const float projectileRadius = 1.5f;
float combinedRadius = shipRadius + projectileRadius;
if (diff.squaredNorm() <= combinedRadius * combinedRadius) {
DeathInfo death;
death.targetId = targetId;
death.serverTime = now_ms;
death.position = pr.pos;
death.killerId = pr.shooterId;
deathEvents.push_back(death);
g_dead_players.insert(targetId);
indicesToRemove.push_back(static_cast<int>(i));
hitDetected = true;
std::cout << "Server: *** HIT DETECTED! ***" << std::endl;
std::cout << "Server: Projectile at ("
<< pr.pos.x() << ", " << pr.pos.y() << ", " << pr.pos.z()
<< ") hit player " << targetId << std::endl;
break;
}
}
}
if (hitDetected) continue;
}
if (!indicesToRemove.empty()) {
std::sort(indicesToRemove.rbegin(), indicesToRemove.rend());
for (int idx : indicesToRemove) {
if (idx >= 0 && idx < (int)g_projectiles.size()) {
g_projectiles.erase(g_projectiles.begin() + idx);
}
}
}
}
if (!deathEvents.empty()) {
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;
}
}
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(), 8080} };
int next_id = 1000;
std::cout << "Server started on port 8080...\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;
}