480 lines
16 KiB
C++
480 lines
16 KiB
C++
|
|
// server/sv_main.cpp
|
|||
|
|
#include "common/types.hpp"
|
|||
|
|
#include "common/enet_wrapper.hpp"
|
|||
|
|
#include "common/serialization.hpp"
|
|||
|
|
#include "common/network_messages.hpp"
|
|||
|
|
#include "common/timestamp.hpp"
|
|||
|
|
#include "common/simulation_params.hpp"
|
|||
|
|
#include "sapp/server_world.hpp"
|
|||
|
|
|
|||
|
|
#include <iostream>
|
|||
|
|
#include <unordered_map>
|
|||
|
|
#include <string>
|
|||
|
|
#include <format>
|
|||
|
|
#include <thread>
|
|||
|
|
#include <chrono>
|
|||
|
|
#include <vector>
|
|||
|
|
|
|||
|
|
using namespace netcode;
|
|||
|
|
|
|||
|
|
struct ClientSession {
|
|||
|
|
ENetPeer* peer = nullptr;
|
|||
|
|
ClientId client_id = INVALID_CLIENT_ID;
|
|||
|
|
EntityId entity_id = INVALID_ENTITY_ID;
|
|||
|
|
std::string name;
|
|||
|
|
bool authenticated = false;
|
|||
|
|
|
|||
|
|
uint64_t last_ping_time = 0;
|
|||
|
|
uint64_t estimated_rtt_us = 0;
|
|||
|
|
uint64_t last_activity_time = 0;
|
|||
|
|
|
|||
|
|
CompensationAlgorithm client_algorithm = CompensationAlgorithm::Hybrid;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
class GameServer {
|
|||
|
|
public:
|
|||
|
|
explicit GameServer(uint16_t port = 7777, const SimulationParams& params = DEFAULT_SIMULATION_PARAMS)
|
|||
|
|
: port_(port)
|
|||
|
|
, params_(params)
|
|||
|
|
, world_(params)
|
|||
|
|
{}
|
|||
|
|
|
|||
|
|
bool start() {
|
|||
|
|
if (!network_.create_server(port_, 16)) {
|
|||
|
|
std::cerr << "Failed to create server on port " << port_ << "\n";
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
std::cout << "Server started on port " << port_ << "\n";
|
|||
|
|
std::cout << "Tick rate: " << params_.tick_rate << " Hz\n";
|
|||
|
|
std::cout << "Snapshot rate: " << params_.snapshot_rate << " Hz\n";
|
|||
|
|
|
|||
|
|
running_ = true;
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void run() {
|
|||
|
|
using namespace std::chrono;
|
|||
|
|
|
|||
|
|
auto last_tick_time = steady_clock::now();
|
|||
|
|
auto last_timeout_check = steady_clock::now();
|
|||
|
|
|
|||
|
|
double tick_accumulator = 0.0;
|
|||
|
|
double snapshot_accumulator = 0.0;
|
|||
|
|
double log_timer = 0.0;
|
|||
|
|
|
|||
|
|
while (running_) {
|
|||
|
|
auto now = steady_clock::now();
|
|||
|
|
|
|||
|
|
// Прошедшее время с последнего тика
|
|||
|
|
double dt = duration<double>(now - last_tick_time).count();
|
|||
|
|
|
|||
|
|
// Обработка сети
|
|||
|
|
process_network();
|
|||
|
|
|
|||
|
|
// тики
|
|||
|
|
uint32_t ticks_performed = 0;
|
|||
|
|
tick_accumulator += dt;
|
|||
|
|
while (tick_accumulator >= params_.tick_interval() && ticks_performed < 8) {
|
|||
|
|
world_.tick();
|
|||
|
|
tick_accumulator -= params_.tick_interval();
|
|||
|
|
tick_count_++;
|
|||
|
|
ticks_performed++;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// снапшоты
|
|||
|
|
snapshot_accumulator += dt;
|
|||
|
|
if (snapshot_accumulator >= params_.snapshot_interval()) {
|
|||
|
|
send_snapshots();
|
|||
|
|
snapshot_count_++;
|
|||
|
|
snapshot_accumulator -= params_.snapshot_interval();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// проверка таймаутов
|
|||
|
|
if (duration<double>(now - last_timeout_check).count() >= 5.0) {
|
|||
|
|
check_timeouts();
|
|||
|
|
last_timeout_check = now;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// логирование
|
|||
|
|
log_timer += dt;
|
|||
|
|
if (log_timer >= 1.0) {
|
|||
|
|
print_stats();
|
|||
|
|
log_timer -= 1.0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// вычисление следующего тика
|
|||
|
|
double time_to_next_tick = params_.tick_interval() - tick_accumulator;
|
|||
|
|
double time_to_next_snapshot = params_.snapshot_interval() - snapshot_accumulator;
|
|||
|
|
|
|||
|
|
double time_to_next_event = (std::min)(time_to_next_tick, time_to_next_snapshot);
|
|||
|
|
time_to_next_event = (std::max)(time_to_next_event, 0.0);
|
|||
|
|
|
|||
|
|
// Обновляем время последнего тика
|
|||
|
|
last_tick_time = now;
|
|||
|
|
|
|||
|
|
// Спим до следующего события
|
|||
|
|
if (time_to_next_event > 0.0005) { // > 500 мкс
|
|||
|
|
auto sleep_duration = duration_cast<microseconds>(duration<double>(time_to_next_event));
|
|||
|
|
std::this_thread::sleep_for(sleep_duration);
|
|||
|
|
} else {
|
|||
|
|
std::this_thread::yield(); // если система решит забрать это время то ждем, если нет -- сразу начинаем выполнение
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void stop() {
|
|||
|
|
running_ = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
void process_network() {
|
|||
|
|
ENetEvent event;
|
|||
|
|
while (network_.service(event, 0) > 0) {
|
|||
|
|
switch (event.type) {
|
|||
|
|
case ENET_EVENT_TYPE_CONNECT:
|
|||
|
|
handle_connect(event.peer);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case ENET_EVENT_TYPE_RECEIVE:
|
|||
|
|
handle_packet(event.peer, event.packet);
|
|||
|
|
enet_packet_destroy(event.packet);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case ENET_EVENT_TYPE_DISCONNECT:
|
|||
|
|
handle_disconnect(event.peer);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void handle_connect(ENetPeer* peer) {
|
|||
|
|
char hostStr[46];
|
|||
|
|
if (enet_address_get_host_ip(&peer->address, hostStr, sizeof(hostStr)) == 0) {
|
|||
|
|
std::cout << "New connection from "
|
|||
|
|
<< hostStr << ":"
|
|||
|
|
<< peer->address.port << "\n";
|
|||
|
|
} else {
|
|||
|
|
std::cout << "New connection from [unknown address]:"
|
|||
|
|
<< peer->address.port << "\n";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void handle_disconnect(ENetPeer* peer) {
|
|||
|
|
auto it = sessions_by_peer_.find(peer);
|
|||
|
|
if (it != sessions_by_peer_.end()) {
|
|||
|
|
ClientId client_id = it->second;
|
|||
|
|
auto session_it = sessions_.find(client_id);
|
|||
|
|
if (session_it != sessions_.end()) {
|
|||
|
|
std::cout << "Player disconnected: " << session_it->second.name
|
|||
|
|
<< " (ID: " << client_id << ")\n";
|
|||
|
|
|
|||
|
|
world_.remove_player(client_id);
|
|||
|
|
sessions_.erase(session_it);
|
|||
|
|
}
|
|||
|
|
sessions_by_peer_.erase(it);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void handle_packet(ENetPeer* peer, ENetPacket* packet) {
|
|||
|
|
if (packet->dataLength < 1) return;
|
|||
|
|
|
|||
|
|
ReadBuffer buf(packet->data, packet->dataLength);
|
|||
|
|
uint8_t type_byte;
|
|||
|
|
if (!buf.read_u8(type_byte)) return;
|
|||
|
|
|
|||
|
|
MessageType type = static_cast<MessageType>(type_byte);
|
|||
|
|
|
|||
|
|
switch (type) {
|
|||
|
|
case MessageType::ClientConnect:
|
|||
|
|
handle_client_connect(peer, buf);
|
|||
|
|
break;
|
|||
|
|
case MessageType::ClientInput:
|
|||
|
|
handle_client_input(peer, buf);
|
|||
|
|
break;
|
|||
|
|
case MessageType::ClientSetAlgorithm:
|
|||
|
|
handle_client_set_algorithm(peer, buf);
|
|||
|
|
break;
|
|||
|
|
case MessageType::PingRequest:
|
|||
|
|
handle_ping_request(peer, buf);
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void handle_client_connect(ENetPeer* peer, ReadBuffer& buf) {
|
|||
|
|
uint32_t protocol_version;
|
|||
|
|
if (!buf.read_u32(protocol_version)) return;
|
|||
|
|
|
|||
|
|
uint8_t name_len;
|
|||
|
|
if (!buf.read_u8(name_len)) return;
|
|||
|
|
char name[33] = {};
|
|||
|
|
if (name_len > 0 && name_len <= 32) {
|
|||
|
|
buf.read_bytes(name, name_len);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uint8_t team_pref_byte;
|
|||
|
|
TeamId team_preference = TeamId::None;
|
|||
|
|
if (buf.read_u8(team_pref_byte)) {
|
|||
|
|
team_preference = static_cast<TeamId>(team_pref_byte);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (protocol_version != 1) {
|
|||
|
|
send_reject(peer, ServerRejectMessage::Reason::VersionMismatch);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (sessions_.size() >= 16) {
|
|||
|
|
send_reject(peer, ServerRejectMessage::Reason::ServerFull);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ClientId client_id = next_client_id_++;
|
|||
|
|
EntityId entity_id = world_.create_player(client_id, name, team_preference);
|
|||
|
|
TeamId team = world_.get_player_team(client_id);
|
|||
|
|
|
|||
|
|
ClientSession session;
|
|||
|
|
session.peer = peer;
|
|||
|
|
session.client_id = client_id;
|
|||
|
|
session.entity_id = entity_id;
|
|||
|
|
session.name = name;
|
|||
|
|
session.authenticated = true;
|
|||
|
|
session.last_activity_time = Timestamp::now_us(); // НОВОЕ
|
|||
|
|
|
|||
|
|
sessions_[client_id] = session;
|
|||
|
|
sessions_by_peer_[peer] = client_id;
|
|||
|
|
|
|||
|
|
send_accept(peer, client_id, entity_id, team);
|
|||
|
|
send_config(peer);
|
|||
|
|
|
|||
|
|
std::cout << "Player connected: " << name
|
|||
|
|
<< " (ID: " << client_id
|
|||
|
|
<< ", Entity: " << entity_id
|
|||
|
|
<< ", Team: " << (team == TeamId::Red ? "Red" : "Blue")
|
|||
|
|
<< ", Preferred: " << (team_preference == TeamId::Red ? "Red" :
|
|||
|
|
team_preference == TeamId::Blue ? "Blue" : "Auto") << ")\n";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void handle_client_input(ENetPeer* peer, ReadBuffer& buf) {
|
|||
|
|
auto it = sessions_by_peer_.find(peer);
|
|||
|
|
if (it == sessions_by_peer_.end()) return;
|
|||
|
|
|
|||
|
|
ClientId client_id = it->second;
|
|||
|
|
|
|||
|
|
auto session_it = sessions_.find(client_id);
|
|||
|
|
if (session_it != sessions_.end()) {
|
|||
|
|
session_it->second.last_activity_time = Timestamp::now_us();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uint8_t input_count;
|
|||
|
|
if (!buf.read_u8(input_count)) return;
|
|||
|
|
|
|||
|
|
for (uint8_t i = 0; i < input_count && i < 16; ++i) {
|
|||
|
|
InputCommand cmd;
|
|||
|
|
if (!buf.read_u32(cmd.sequence)) break;
|
|||
|
|
if (!buf.read_u32(cmd.client_tick)) break;
|
|||
|
|
if (!buf.read_u64(cmd.timestamp_us)) break;
|
|||
|
|
|
|||
|
|
uint8_t input_byte;
|
|||
|
|
if (!buf.read_u8(input_byte)) break;
|
|||
|
|
cmd.input = InputState::from_byte(input_byte);
|
|||
|
|
|
|||
|
|
world_.add_player_input(client_id, cmd);
|
|||
|
|
inputs_received_++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void handle_client_set_algorithm(ENetPeer* peer, ReadBuffer& buf) {
|
|||
|
|
auto it = sessions_by_peer_.find(peer);
|
|||
|
|
if (it == sessions_by_peer_.end()) return;
|
|||
|
|
|
|||
|
|
uint8_t algo_byte;
|
|||
|
|
if (!buf.read_u8(algo_byte)) return;
|
|||
|
|
|
|||
|
|
ClientId client_id = it->second;
|
|||
|
|
auto session_it = sessions_.find(client_id);
|
|||
|
|
if (session_it != sessions_.end()) {
|
|||
|
|
session_it->second.client_algorithm = static_cast<CompensationAlgorithm>(algo_byte);
|
|||
|
|
std::cout << "Client " << client_id << " switched to algorithm: "
|
|||
|
|
<< algorithm_name(session_it->second.client_algorithm) << "\n";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void handle_ping_request(ENetPeer* peer, ReadBuffer& buf) {
|
|||
|
|
uint32_t ping_id;
|
|||
|
|
uint64_t client_time;
|
|||
|
|
if (!buf.read_u32(ping_id)) return;
|
|||
|
|
if (!buf.read_u64(client_time)) return;
|
|||
|
|
|
|||
|
|
WriteBuffer response;
|
|||
|
|
response.write_u8(static_cast<uint8_t>(MessageType::PingResponse));
|
|||
|
|
response.write_u32(ping_id);
|
|||
|
|
response.write_u64(client_time);
|
|||
|
|
response.write_u64(Timestamp::now_us());
|
|||
|
|
response.write_u32(world_.current_tick());
|
|||
|
|
|
|||
|
|
network_.send(peer, NetworkChannel::Unreliable, response.data(), response.size(), false);
|
|||
|
|
|
|||
|
|
auto it = sessions_by_peer_.find(peer);
|
|||
|
|
if (it != sessions_by_peer_.end()) {
|
|||
|
|
auto session_it = sessions_.find(it->second);
|
|||
|
|
if (session_it != sessions_.end()) {
|
|||
|
|
uint64_t now = Timestamp::now_us();
|
|||
|
|
if (session_it->second.last_ping_time > 0) {
|
|||
|
|
session_it->second.estimated_rtt_us = now - session_it->second.last_ping_time;
|
|||
|
|
world_.update_player_rtt(it->second, session_it->second.estimated_rtt_us);
|
|||
|
|
}
|
|||
|
|
session_it->second.last_ping_time = now;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void check_timeouts() {
|
|||
|
|
uint64_t now = Timestamp::now_us();
|
|||
|
|
const uint64_t timeout_us = 30'000'000; // 30 секунд
|
|||
|
|
|
|||
|
|
std::vector<ClientId> to_disconnect;
|
|||
|
|
|
|||
|
|
for (const auto& [client_id, session] : sessions_) {
|
|||
|
|
if (session.authenticated &&
|
|||
|
|
session.last_activity_time > 0 &&
|
|||
|
|
now - session.last_activity_time > timeout_us) {
|
|||
|
|
to_disconnect.push_back(client_id);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (ClientId client_id : to_disconnect) {
|
|||
|
|
auto it = sessions_.find(client_id);
|
|||
|
|
if (it != sessions_.end()) {
|
|||
|
|
std::cout << "Client timeout: " << it->second.name << "\n";
|
|||
|
|
if (it->second.peer) {
|
|||
|
|
enet_peer_disconnect(it->second.peer, 0);
|
|||
|
|
}
|
|||
|
|
world_.remove_player(client_id);
|
|||
|
|
sessions_by_peer_.erase(it->second.peer);
|
|||
|
|
sessions_.erase(it);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void send_reject(ENetPeer* peer, ServerRejectMessage::Reason reason) {
|
|||
|
|
WriteBuffer buf;
|
|||
|
|
buf.write_u8(static_cast<uint8_t>(MessageType::ServerReject));
|
|||
|
|
buf.write_u8(static_cast<uint8_t>(reason));
|
|||
|
|
|
|||
|
|
network_.send(peer, NetworkChannel::Reliable, buf.data(), buf.size(), true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void send_accept(ENetPeer* peer, ClientId client_id, EntityId entity_id, TeamId team) {
|
|||
|
|
WriteBuffer buf;
|
|||
|
|
buf.write_u8(static_cast<uint8_t>(MessageType::ServerAccept));
|
|||
|
|
buf.write_u16(client_id);
|
|||
|
|
buf.write_u32(entity_id);
|
|||
|
|
buf.write_u8(static_cast<uint8_t>(team));
|
|||
|
|
buf.write_u32(world_.current_tick());
|
|||
|
|
buf.write_u64(Timestamp::now_us());
|
|||
|
|
|
|||
|
|
network_.send(peer, NetworkChannel::Reliable, buf.data(), buf.size(), true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void send_config(ENetPeer* peer) {
|
|||
|
|
WriteBuffer buf;
|
|||
|
|
buf.write_u8(static_cast<uint8_t>(MessageType::ServerConfig));
|
|||
|
|
buf.write_u32(params_.tick_rate);
|
|||
|
|
buf.write_u32(params_.snapshot_rate);
|
|||
|
|
buf.write_float(params_.arena_width);
|
|||
|
|
buf.write_float(params_.arena_height);
|
|||
|
|
buf.write_float(params_.player_radius);
|
|||
|
|
buf.write_float(params_.player_max_speed);
|
|||
|
|
buf.write_float(params_.ball_radius);
|
|||
|
|
|
|||
|
|
network_.send(peer, NetworkChannel::Reliable, buf.data(), buf.size(), true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void send_snapshots() {
|
|||
|
|
if (sessions_.empty()) return;
|
|||
|
|
|
|||
|
|
WorldSnapshot snapshot = world_.create_snapshot();
|
|||
|
|
|
|||
|
|
WriteBuffer buf;
|
|||
|
|
serialize_snapshot(buf, snapshot);
|
|||
|
|
|
|||
|
|
network_.broadcast(NetworkChannel::Unreliable, buf.data(), buf.size(), false);
|
|||
|
|
snapshots_sent_++;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void print_stats() {
|
|||
|
|
std::cout << std::format("[Tick {}] Players: {} | Inputs/s: {} | Snapshots/s: {} | Red: {:.1f}s | Blue: {:.1f}s\n",
|
|||
|
|
world_.current_tick(),
|
|||
|
|
sessions_.size(),
|
|||
|
|
inputs_received_,
|
|||
|
|
snapshots_sent_,
|
|||
|
|
world_.red_team_time(),
|
|||
|
|
world_.blue_team_time());
|
|||
|
|
|
|||
|
|
inputs_received_ = 0;
|
|||
|
|
snapshots_sent_ = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
uint16_t port_;
|
|||
|
|
SimulationParams params_;
|
|||
|
|
|
|||
|
|
ENetInitializer enet_init_;
|
|||
|
|
NetworkHost network_;
|
|||
|
|
|
|||
|
|
ServerWorld world_;
|
|||
|
|
|
|||
|
|
std::unordered_map<ClientId, ClientSession> sessions_;
|
|||
|
|
std::unordered_map<ENetPeer*, ClientId> sessions_by_peer_;
|
|||
|
|
|
|||
|
|
ClientId next_client_id_ = 1;
|
|||
|
|
|
|||
|
|
bool running_ = false;
|
|||
|
|
|
|||
|
|
uint32_t tick_count_ = 0;
|
|||
|
|
uint32_t snapshot_count_ = 0;
|
|||
|
|
uint32_t inputs_received_ = 0;
|
|||
|
|
uint32_t snapshots_sent_ = 0;
|
|||
|
|
double log_timer_ = 0.0;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
int main(int argc, char* argv[]) {
|
|||
|
|
uint16_t port = 7777;
|
|||
|
|
SimulationParams params = DEFAULT_SIMULATION_PARAMS;
|
|||
|
|
|
|||
|
|
for (int i = 1; i < argc; ++i) {
|
|||
|
|
std::string arg = argv[i];
|
|||
|
|
if (arg == "-p" || arg == "--port") {
|
|||
|
|
if (i + 1 < argc) port = static_cast<uint16_t>(std::stoi(argv[++i]));
|
|||
|
|
} else if (arg == "--tick-rate") {
|
|||
|
|
if (i + 1 < argc) {
|
|||
|
|
params.tick_rate = static_cast<uint32_t>(std::stoi(argv[++i]));
|
|||
|
|
params.fixed_delta_time = 1.0 / params.tick_rate;
|
|||
|
|
}
|
|||
|
|
} else if (arg == "--snapshot-rate") {
|
|||
|
|
if (i + 1 < argc) {
|
|||
|
|
params.snapshot_rate = static_cast<uint32_t>(std::stoi(argv[++i]));
|
|||
|
|
}
|
|||
|
|
} else if (arg == "--help") {
|
|||
|
|
std::cout << "Usage: server [-p port] [--tick-rate N] [--snapshot-rate N]\n";
|
|||
|
|
return 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
GameServer server(port, params);
|
|||
|
|
if (server.start()) {
|
|||
|
|
server.run();
|
|||
|
|
}
|
|||
|
|
} catch (const std::exception& e) {
|
|||
|
|
std::cerr << "Error: " << e.what() << "\n";
|
|||
|
|
return 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return 0;
|
|||
|
|
}
|