1302 lines
48 KiB
C++
1302 lines
48 KiB
C++
#ifdef _WIN32
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <winsock2.h>
|
|
#include <ws2tcpip.h>
|
|
#endif
|
|
|
|
|
|
#define NOMINMAX
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <format>
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <random>
|
|
#include <thread>
|
|
|
|
#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 "app/client_world.hpp"
|
|
#include "app/network_simulation.hpp"
|
|
|
|
#include <SFML/Graphics.hpp>
|
|
#include <SFML/Window.hpp>
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
const std::string defaultFontPath = "C:/Windows/Fonts/arial.ttf";
|
|
#else
|
|
const std::string defaultFontPath = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf";
|
|
#endif
|
|
|
|
using namespace netcode;
|
|
|
|
|
|
// Состояния клиента
|
|
enum class ClientState {
|
|
TeamSelection,
|
|
Connecting,
|
|
Connected,
|
|
Disconnected
|
|
};
|
|
|
|
class GameClient {
|
|
public:
|
|
sf::Clock bot_clock_;
|
|
|
|
GameClient() : window_(sf::VideoMode({1760, 990}), "Netcode Demo - Client") {
|
|
window_.setFramerateLimit(120);
|
|
auto_test_mode_ = false;
|
|
if (!font_.openFromFile(defaultFontPath)) {
|
|
std::cerr << "Warning: Could not load font\n";
|
|
}
|
|
|
|
state_ = ClientState::TeamSelection;
|
|
|
|
network_presets_ = {
|
|
{"Perfect (No Sim)", NetworkPresets::Perfect()},
|
|
{"LAN (2ms)", NetworkPresets::LAN()},
|
|
{"Good Broadband (25ms)", NetworkPresets::GoodBroadband()},
|
|
{"Average Broadband (50ms)", NetworkPresets::AverageBroadband()},
|
|
{"Poor Broadband (100ms)", NetworkPresets::PoorBroadband()},
|
|
{"Mobile 4G (75ms)", NetworkPresets::Mobile4G()},
|
|
{"Mobile 3G (150ms)", NetworkPresets::Mobile3G()},
|
|
{"Satellite (300ms)", NetworkPresets::Satellite()}
|
|
};
|
|
|
|
network_sim_.update_config(network_presets_[0].second);
|
|
}
|
|
|
|
void enable_auto_test() {
|
|
auto_test_mode_ = true;
|
|
bot_enabled_ = false;
|
|
|
|
bot_config_.test_duration_per_preset_sec = 240.0f;
|
|
bot_config_.stabilization_time_sec = 5.0f;
|
|
bot_config_.movement_change_interval_sec = 0.7f;
|
|
bot_config_.random_movements = true;
|
|
bot_config_.circle_radius = 15.0f;
|
|
bot_config_.circle_speed = 2.0f;
|
|
|
|
std::cout << "AUTO TEST MODE ENABLED\n";
|
|
}
|
|
|
|
void direct_select_team(TeamId team) {
|
|
selected_team_ = team;
|
|
state_ = ClientState::Connecting;
|
|
if (!connect_to_server()) {
|
|
state_ = ClientState::Disconnected;
|
|
}
|
|
std::cout << "Auto-selected team: " << (team == TeamId::Red ? "Red" : team == TeamId::Blue ? "Blue" : "Auto") << "\n";
|
|
}
|
|
|
|
void direct_set_algorithm(CompensationAlgorithm algo) {
|
|
set_algorithm(algo);
|
|
std::cout << "Direct switched to algorithm: " << algorithm_name(algo) << "\n";
|
|
}
|
|
|
|
void direct_set_preset(size_t index) {
|
|
if (index >= network_presets_.size()) return;
|
|
current_preset_index_ = index;
|
|
network_sim_.update_config(network_presets_[index].second);
|
|
std::cout << "Direct switched to preset: " << network_presets_[index].first << "\n";
|
|
}
|
|
|
|
InputState get_bot_input() {
|
|
InputState input;
|
|
if (!bot_enabled_) return input;
|
|
|
|
float dt = bot_clock_.restart().asSeconds();
|
|
|
|
bot_timer_ += dt;
|
|
direction_timer_ += dt;
|
|
|
|
if (direction_timer_ >= bot_config_.movement_change_interval_sec) {
|
|
direction_timer_ = 0.0f;
|
|
|
|
if (bot_config_.random_movements) {
|
|
std::random_device rd;
|
|
std::mt19937 gen(rd());
|
|
std::uniform_real_distribution<float> dist(-1.0f, 1.0f);
|
|
|
|
current_bot_direction_.x = dist(gen);
|
|
current_bot_direction_.y = dist(gen);
|
|
|
|
if (current_bot_direction_.length_squared() > 0.01f) {
|
|
current_bot_direction_ = current_bot_direction_.normalized();
|
|
}
|
|
} else {
|
|
circle_angle_ += bot_config_.circle_speed * dt;
|
|
current_bot_direction_.x = std::cos(circle_angle_);
|
|
current_bot_direction_.y = std::sin(circle_angle_);
|
|
}
|
|
}
|
|
|
|
input.move_right = current_bot_direction_.x > 0.3f;
|
|
input.move_left = current_bot_direction_.x < -0.3f;
|
|
input.move_down = current_bot_direction_.y > 0.3f;
|
|
input.move_up = current_bot_direction_.y < -0.3f;
|
|
|
|
return input;
|
|
}
|
|
|
|
void run_auto_test() {
|
|
if (!auto_test_mode_) return;
|
|
|
|
std::cout << "Starting auto test...\n";
|
|
direct_select_team(TeamId::None);
|
|
}
|
|
|
|
bool connect(const std::string& host, uint16_t port) {
|
|
host_ = host;
|
|
port_ = port;
|
|
return true;
|
|
}
|
|
|
|
// Жестко багуется ибо microsoft
|
|
bool connect_to_server() {
|
|
std::cout << "Creating client...\n";
|
|
if (!network_.create_client()) {
|
|
std::cerr << "Failed to create client\n";
|
|
#ifdef _WIN32
|
|
std::cerr << "WSA Error: " << WSAGetLastError() << "\n";
|
|
#endif
|
|
return false;
|
|
}
|
|
std::cout << "Client created successfully\n";
|
|
|
|
std::cout << "Connecting to " << host_ << ":" << port_ << "...\n";
|
|
server_peer_ = network_.connect(host_, port_);
|
|
if (!server_peer_) {
|
|
std::cerr << "Failed to initiate connection\n";
|
|
#ifdef _WIN32
|
|
std::cerr << "WSA Error: " << WSAGetLastError() << "\n";
|
|
#endif
|
|
return false;
|
|
}
|
|
std::cout << "Connection initiated, waiting for response...\n";
|
|
|
|
ENetHost* raw_host = network_.raw();
|
|
if (raw_host) {
|
|
std::cout << "Host socket: " << raw_host->socket << "\n";
|
|
}
|
|
|
|
ENetEvent event;
|
|
int result = network_.service(event, 5000);
|
|
|
|
std::cout << "Service returned: " << result << "\n";
|
|
#ifdef _WIN32
|
|
if (result < 0) {
|
|
std::cerr << "WSA Error after service: " << WSAGetLastError() << "\n";
|
|
}
|
|
#endif
|
|
|
|
if (result > 0 && event.type == ENET_EVENT_TYPE_CONNECT) {
|
|
std::cout << "Connected to server\n";
|
|
send_connect_request();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void send_delayed_packets() {
|
|
if (!network_sim_.is_enabled()) return;
|
|
|
|
auto packets = network_sim_.receive_packets();
|
|
|
|
for (const auto& packet_data : packets) {
|
|
if (packet_data.empty()) continue;
|
|
|
|
NetworkChannel channel = NetworkChannel::Unreliable;
|
|
if (!packet_data.empty()) {
|
|
MessageType type = static_cast<MessageType>(packet_data[0]);
|
|
if (type == MessageType::ClientConnect ||
|
|
type == MessageType::ClientSetAlgorithm) {
|
|
channel = NetworkChannel::Reliable;
|
|
}
|
|
}
|
|
|
|
network_.send(server_peer_, channel,
|
|
packet_data.data(), packet_data.size(),
|
|
channel == NetworkChannel::Reliable);
|
|
}
|
|
}
|
|
|
|
void set_custom_network_config(float latency_ms, float jitter_ms,
|
|
float packet_loss = 0.0f, float duplication = 0.0f) {
|
|
auto cfg = NetworkPresets::Custom(latency_ms, jitter_ms, packet_loss, duplication);
|
|
network_sim_.update_config(cfg);
|
|
std::cout << std::format("Custom network: {}ms ±{}ms, loss {:.1f}%\n",
|
|
latency_ms, jitter_ms, packet_loss * 100.0f);
|
|
}
|
|
|
|
void run() {
|
|
sf::Clock clock;
|
|
sf::Clock metrics_clock;
|
|
sf::Clock compensation_metrics_clock;
|
|
|
|
size_t current_algo_index = 0;
|
|
size_t current_preset_index = 0;
|
|
enum class AutoTestPhase { WaitingConnect, StabilizeAlgo, TestPreset, Finished } phase = AutoTestPhase::WaitingConnect;
|
|
float phase_timer = 0.0f;
|
|
|
|
run_auto_test();
|
|
|
|
while (window_.isOpen()) {
|
|
float dt = clock.restart().asSeconds();
|
|
|
|
while (const auto event = window_.pollEvent()) {
|
|
if (event->is<sf::Event::Closed>()) {
|
|
if (state_ == ClientState::Connected && server_peer_) {
|
|
enet_peer_disconnect(server_peer_, 0);
|
|
|
|
ENetEvent event;
|
|
while (network_.service(event, 100) > 0) {
|
|
if (event.type == ENET_EVENT_TYPE_DISCONNECT) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
window_.close();
|
|
}
|
|
|
|
if (const auto* key = event->getIf<sf::Event::KeyPressed>()) {
|
|
handle_key_press(key->code);
|
|
}
|
|
|
|
if (const auto* mouse = event->getIf<sf::Event::MouseButtonPressed>()) {
|
|
handle_mouse_click(mouse->position);
|
|
}
|
|
}
|
|
|
|
switch (state_) {
|
|
case ClientState::TeamSelection:
|
|
render_team_selection();
|
|
break;
|
|
|
|
case ClientState::Connecting:
|
|
process_network();
|
|
render_connecting();
|
|
break;
|
|
|
|
case ClientState::Connected:
|
|
process_network();
|
|
process_input();
|
|
|
|
{
|
|
double current_time_ms = Timestamp::now_ms();
|
|
world_.update(current_time_ms);
|
|
}
|
|
|
|
ping_timer_ += dt;
|
|
if (ping_timer_ >= 0.5f) {
|
|
send_ping();
|
|
ping_timer_ = 0.0f;
|
|
}
|
|
|
|
if (metrics_clock.getElapsedTime().asSeconds() >= 1.0f) {
|
|
update_metrics();
|
|
metrics_clock.restart();
|
|
}
|
|
|
|
if (compensation_metrics_clock.getElapsedTime().asSeconds() >= 1.0f) {
|
|
update_compensation_metrics();
|
|
compensation_metrics_clock.restart();
|
|
}
|
|
|
|
render();
|
|
|
|
if (auto_test_mode_) {
|
|
if (!bot_enabled_) {
|
|
bot_enabled_ = true;
|
|
bot_clock_.restart();
|
|
std::cout << "Bot enabled after connection\n";
|
|
phase = AutoTestPhase::StabilizeAlgo;
|
|
phase_timer = 0.0f;
|
|
current_algo_index = 0;
|
|
current_preset_index = 0;
|
|
|
|
direct_set_algorithm(algorithms_to_test_[0]);
|
|
std::cout << "[AutoTest] Starting with algorithm: " << algorithm_name(algorithms_to_test_[0]) << "\n";
|
|
}
|
|
|
|
phase_timer += dt;
|
|
|
|
if (phase == AutoTestPhase::StabilizeAlgo) {
|
|
if (phase_timer >= bot_config_.stabilization_time_sec) {
|
|
std::cout << "[AutoTest] Stabilization complete, starting test phase\n";
|
|
phase = AutoTestPhase::TestPreset;
|
|
phase_timer = 0.0f;
|
|
}
|
|
} else if (phase == AutoTestPhase::TestPreset) {
|
|
if (phase_timer >= bot_config_.test_duration_per_preset_sec) {
|
|
current_preset_index++;
|
|
|
|
if (current_preset_index >= network_presets_.size()) {
|
|
current_preset_index = 0;
|
|
current_algo_index++;
|
|
|
|
if (current_algo_index >= algorithms_to_test_.size()) {
|
|
phase = AutoTestPhase::Finished;
|
|
std::cout << "AUTO TEST COMPLETED!\n";
|
|
std::this_thread::sleep_for(std::chrono::seconds(5));
|
|
window_.close();
|
|
return;
|
|
}
|
|
|
|
CompensationAlgorithm next_algo = algorithms_to_test_[current_algo_index];
|
|
direct_set_algorithm(next_algo);
|
|
direct_set_preset(0);
|
|
std::cout << "[AutoTest] Switched to algorithm: " << algorithm_name(next_algo) << "\n";
|
|
std::cout << "[AutoTest] Reset to preset: " << network_presets_[0].first << "\n";
|
|
|
|
phase = AutoTestPhase::StabilizeAlgo;
|
|
phase_timer = 0.0f;
|
|
} else {
|
|
direct_set_preset(current_preset_index);
|
|
std::cout << "[AutoTest] Switched to preset: " << network_presets_[current_preset_index].first << "\n";
|
|
|
|
phase = AutoTestPhase::StabilizeAlgo;
|
|
phase_timer = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case ClientState::Disconnected:
|
|
render_disconnected();
|
|
if (auto_test_mode_) {
|
|
window_.close();
|
|
}
|
|
break;
|
|
}
|
|
|
|
window_.display();
|
|
}
|
|
}
|
|
|
|
void handle_key_press(sf::Keyboard::Key key) {
|
|
if (state_ != ClientState::Connected) {
|
|
if (key == sf::Keyboard::Key::Escape) {
|
|
window_.close();
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (key) {
|
|
case sf::Keyboard::Key::Num1:
|
|
set_algorithm(CompensationAlgorithm::None);
|
|
break;
|
|
case sf::Keyboard::Key::Num2:
|
|
set_algorithm(CompensationAlgorithm::ClientPrediction);
|
|
break;
|
|
case sf::Keyboard::Key::Num3:
|
|
set_algorithm(CompensationAlgorithm::PredictionReconciliation);
|
|
break;
|
|
case sf::Keyboard::Key::Num4:
|
|
set_algorithm(CompensationAlgorithm::EntityInterpolation);
|
|
break;
|
|
case sf::Keyboard::Key::Num5:
|
|
set_algorithm(CompensationAlgorithm::DeadReckoning);
|
|
break;
|
|
case sf::Keyboard::Key::Num6:
|
|
set_algorithm(CompensationAlgorithm::Hybrid);
|
|
break;
|
|
case sf::Keyboard::Key::Num7:
|
|
set_algorithm(CompensationAlgorithm::ServerLagCompensation);
|
|
break;
|
|
|
|
case sf::Keyboard::Key::F1:
|
|
show_server_positions_ = !show_server_positions_;
|
|
break;
|
|
case sf::Keyboard::Key::F2:
|
|
show_predicted_positions_ = !show_predicted_positions_;
|
|
break;
|
|
case sf::Keyboard::Key::F3:
|
|
show_interpolated_positions_ = !show_interpolated_positions_;
|
|
break;
|
|
case sf::Keyboard::Key::F4:
|
|
show_velocity_vectors_ = !show_velocity_vectors_;
|
|
break;
|
|
case sf::Keyboard::Key::F5:
|
|
show_debug_info_ = !show_debug_info_;
|
|
break;
|
|
|
|
case sf::Keyboard::Key::Add:
|
|
interpolation_delay_ms_ = (std::min)(interpolation_delay_ms_ + 10.0f, 500.0f);
|
|
world_.set_interpolation_delay(interpolation_delay_ms_);
|
|
break;
|
|
case sf::Keyboard::Key::Subtract:
|
|
interpolation_delay_ms_ = (std::max)(interpolation_delay_ms_ - 10.0f, 0.0f);
|
|
world_.set_interpolation_delay(interpolation_delay_ms_);
|
|
break;
|
|
|
|
case sf::Keyboard::Key::Escape:
|
|
window_.close();
|
|
break;
|
|
|
|
case sf::Keyboard::Key::N:
|
|
current_preset_index_ = (current_preset_index_ + 1) % network_presets_.size();
|
|
network_sim_.update_config(network_presets_[current_preset_index_].second);
|
|
std::cout << "Network preset: " << network_presets_[current_preset_index_].first << "\n";
|
|
break;
|
|
|
|
case sf::Keyboard::Key::M:
|
|
if (current_preset_index_ == 0) {
|
|
current_preset_index_ = network_presets_.size() - 1;
|
|
} else {
|
|
current_preset_index_--;
|
|
}
|
|
network_sim_.update_config(network_presets_[current_preset_index_].second);
|
|
std::cout << "Network preset: " << network_presets_[current_preset_index_].first << "\n";
|
|
break;
|
|
|
|
case sf::Keyboard::Key::F6:
|
|
show_network_stats_ = !show_network_stats_;
|
|
break;
|
|
|
|
case sf::Keyboard::Key::L:
|
|
network_sim_.trigger_lag_spike(1000.0f, 500.0f);
|
|
std::cout << "Lag spike triggered (1s duration, +500ms delay)\n";
|
|
break;
|
|
|
|
case sf::Keyboard::Key::PageUp: {
|
|
auto cfg = network_sim_.config();
|
|
cfg.enabled = true;
|
|
cfg.base_latency_ms += 10.0f;
|
|
network_sim_.update_config(cfg);
|
|
std::cout << "Latency: " << cfg.base_latency_ms << "ms\n";
|
|
break;
|
|
}
|
|
case sf::Keyboard::Key::PageDown: {
|
|
auto cfg = network_sim_.config();
|
|
cfg.base_latency_ms = (std::max)(0.0f, cfg.base_latency_ms - 10.0f);
|
|
if (cfg.base_latency_ms == 0.0f) {
|
|
cfg.enabled = false;
|
|
}
|
|
network_sim_.update_config(cfg);
|
|
std::cout << "Latency: " << cfg.base_latency_ms << "ms\n";
|
|
break;
|
|
}
|
|
|
|
case sf::Keyboard::Key::Home: {
|
|
auto cfg = network_sim_.config();
|
|
cfg.enabled = true;
|
|
cfg.jitter_ms += 5.0f;
|
|
network_sim_.update_config(cfg);
|
|
std::cout << "Jitter: " << cfg.jitter_ms << "ms\n";
|
|
break;
|
|
}
|
|
case sf::Keyboard::Key::End: {
|
|
auto cfg = network_sim_.config();
|
|
cfg.jitter_ms = (std::max)(0.0f, cfg.jitter_ms - 5.0f);
|
|
network_sim_.update_config(cfg);
|
|
std::cout << "Jitter: " << cfg.jitter_ms << "ms\n";
|
|
break;
|
|
}
|
|
|
|
case sf::Keyboard::Key::P: {
|
|
auto cfg = network_sim_.config();
|
|
cfg.enabled = true;
|
|
cfg.packet_loss += 0.01f;
|
|
if (cfg.packet_loss > 0.2f) cfg.packet_loss = 0.0f;
|
|
network_sim_.update_config(cfg);
|
|
std::cout << "Packet loss: " << (cfg.packet_loss * 100.0f) << "%\n";
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void handle_mouse_click(sf::Vector2i mouse_pos) {
|
|
if (state_ != ClientState::TeamSelection) return;
|
|
|
|
if (red_team_button_.contains(sf::Vector2f(mouse_pos))) {
|
|
selected_team_ = TeamId::Red;
|
|
state_ = ClientState::Connecting;
|
|
if (!connect_to_server()) {
|
|
state_ = ClientState::Disconnected;
|
|
}
|
|
} else if (blue_team_button_.contains(sf::Vector2f(mouse_pos))) {
|
|
selected_team_ = TeamId::Blue;
|
|
state_ = ClientState::Connecting;
|
|
if (!connect_to_server()) {
|
|
state_ = ClientState::Disconnected;
|
|
}
|
|
} else if (auto_team_button_.contains(sf::Vector2f(mouse_pos))) {
|
|
selected_team_ = TeamId::None; // Авто-выбор
|
|
state_ = ClientState::Connecting;
|
|
if (!connect_to_server()) {
|
|
state_ = ClientState::Disconnected;
|
|
}
|
|
}
|
|
}
|
|
|
|
void set_algorithm(CompensationAlgorithm algo) {
|
|
world_.set_algorithm(algo);
|
|
current_algorithm_ = algo;
|
|
|
|
ClientSetAlgorithmMessage msg;
|
|
msg.algorithm = algo;
|
|
|
|
WriteBuffer buf;
|
|
buf.write_u8(static_cast<uint8_t>(MessageType::ClientSetAlgorithm));
|
|
buf.write_u8(static_cast<uint8_t>(algo));
|
|
|
|
network_.send(server_peer_, NetworkChannel::Reliable, buf.data(), buf.size(), true);
|
|
|
|
std::cout << "Algorithm changed to: " << algorithm_name(algo) << "\n";
|
|
}
|
|
|
|
void send_connect_request() {
|
|
ClientConnectMessage msg;
|
|
msg.protocol_version = 1;
|
|
msg.team_preference = selected_team_;
|
|
std::strncpy(msg.player_name, player_name_.c_str(), sizeof(msg.player_name) - 1);
|
|
|
|
WriteBuffer buf;
|
|
buf.write_u8(static_cast<uint8_t>(MessageType::ClientConnect));
|
|
buf.write_u32(msg.protocol_version);
|
|
buf.write_string(msg.player_name, sizeof(msg.player_name));
|
|
buf.write_u8(static_cast<uint8_t>(msg.team_preference));
|
|
|
|
if (network_sim_.is_enabled()) {
|
|
network_sim_.send_packet(buf.data(), buf.size());
|
|
} else {
|
|
network_.send(server_peer_, NetworkChannel::Reliable, buf.data(), buf.size(), true);
|
|
}
|
|
}
|
|
|
|
void process_network() {
|
|
send_delayed_packets();
|
|
|
|
ENetEvent event;
|
|
while (network_.service(event, 0) > 0) {
|
|
switch (event.type) {
|
|
case ENET_EVENT_TYPE_RECEIVE:
|
|
handle_packet(event.packet);
|
|
enet_packet_destroy(event.packet);
|
|
break;
|
|
|
|
case ENET_EVENT_TYPE_DISCONNECT:
|
|
std::cout << "Disconnected from server\n";
|
|
state_ = ClientState::Disconnected;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void handle_packet(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::ServerAccept:
|
|
handle_server_accept(buf);
|
|
break;
|
|
case MessageType::ServerReject:
|
|
handle_server_reject(buf);
|
|
break;
|
|
case MessageType::ServerSnapshot:
|
|
handle_snapshot(packet->data, packet->dataLength);
|
|
break;
|
|
case MessageType::ServerConfig:
|
|
handle_server_config(buf);
|
|
break;
|
|
case MessageType::PingResponse:
|
|
handle_ping_response(buf);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void handle_server_accept(ReadBuffer& buf) {
|
|
uint16_t client_id;
|
|
uint32_t entity_id;
|
|
uint8_t team;
|
|
uint32_t tick;
|
|
uint64_t server_time;
|
|
|
|
if (!buf.read_u16(client_id)) return;
|
|
if (!buf.read_u32(entity_id)) return;
|
|
if (!buf.read_u8(team)) return;
|
|
if (!buf.read_u32(tick)) return;
|
|
if (!buf.read_u64(server_time)) return;
|
|
|
|
local_client_id_ = client_id;
|
|
local_entity_id_ = entity_id;
|
|
local_team_ = static_cast<TeamId>(team);
|
|
|
|
world_.set_local_player(local_client_id_, local_entity_id_);
|
|
state_ = ClientState::Connected;
|
|
|
|
std::cout << "Connected! Client ID: " << client_id
|
|
<< ", Entity ID: " << entity_id
|
|
<< ", Team: " << (local_team_ == TeamId::Red ? "Red" : "Blue") << "\n";
|
|
}
|
|
|
|
void handle_server_reject(ReadBuffer& buf) {
|
|
uint8_t reason;
|
|
buf.read_u8(reason);
|
|
std::cerr << "Connection rejected. Reason: " << static_cast<int>(reason) << "\n";
|
|
state_ = ClientState::Disconnected;
|
|
}
|
|
|
|
void handle_snapshot(const uint8_t* data, size_t size) {
|
|
ReadBuffer buf(data, size);
|
|
WorldSnapshot snapshot;
|
|
|
|
if (deserialize_snapshot(buf, snapshot)) {
|
|
world_.receive_snapshot(snapshot);
|
|
last_server_tick_ = snapshot.server_tick;
|
|
snapshots_received_++;
|
|
}
|
|
}
|
|
|
|
void handle_server_config(ReadBuffer& buf) {
|
|
SimulationParams new_params = DEFAULT_SIMULATION_PARAMS;
|
|
|
|
if (!buf.read_u32(new_params.tick_rate)) return;
|
|
if (!buf.read_u32(new_params.snapshot_rate)) return;
|
|
if (!buf.read_float(new_params.arena_width)) return;
|
|
if (!buf.read_float(new_params.arena_height)) return;
|
|
if (!buf.read_float(new_params.player_radius)) return;
|
|
if (!buf.read_float(new_params.player_max_speed)) return;
|
|
if (!buf.read_float(new_params.ball_radius)) return;
|
|
|
|
new_params.fixed_delta_time = 1.0 / static_cast<double>(new_params.tick_rate);
|
|
world_.set_simulation_params(new_params);
|
|
|
|
|
|
std::cout << "Server config: tick_rate=" << new_params.tick_rate
|
|
<< ", snapshot_rate=" << new_params.snapshot_rate << "\n";
|
|
}
|
|
|
|
void handle_ping_response(ReadBuffer& buf) {
|
|
uint32_t ping_id;
|
|
uint64_t client_time, server_time;
|
|
uint32_t server_tick;
|
|
|
|
if (!buf.read_u32(ping_id)) return;
|
|
if (!buf.read_u64(client_time)) return;
|
|
if (!buf.read_u64(server_time)) return;
|
|
if (!buf.read_u32(server_tick)) return;
|
|
|
|
uint64_t now = Timestamp::now_us();
|
|
time_sync_.add_sample(client_time, server_time, now);
|
|
|
|
current_rtt_ms_ = time_sync_.get_rtt_ms();
|
|
current_jitter_ms_ = time_sync_.get_jitter_ms();
|
|
}
|
|
|
|
void process_input() {
|
|
InputState input;
|
|
|
|
if (auto_test_mode_ && bot_enabled_) {
|
|
input = get_bot_input();
|
|
} else {
|
|
input.move_up = sf::Keyboard::isKeyPressed(sf::Keyboard::Key::W);
|
|
input.move_down = sf::Keyboard::isKeyPressed(sf::Keyboard::Key::S);
|
|
input.move_left = sf::Keyboard::isKeyPressed(sf::Keyboard::Key::A);
|
|
input.move_right = sf::Keyboard::isKeyPressed(sf::Keyboard::Key::D);
|
|
}
|
|
|
|
InputCommand cmd;
|
|
cmd.sequence = ++input_sequence_;
|
|
cmd.client_tick = client_tick_++;
|
|
cmd.timestamp_us = Timestamp::now_us();
|
|
cmd.input = input;
|
|
|
|
world_.process_local_input(cmd);
|
|
send_input(cmd);
|
|
recent_inputs_.push(cmd);
|
|
}
|
|
|
|
void send_input(const InputCommand& cmd) {
|
|
WriteBuffer buf;
|
|
buf.write_u8(static_cast<uint8_t>(MessageType::ClientInput));
|
|
buf.write_u8(1);
|
|
|
|
buf.write_u32(cmd.sequence);
|
|
buf.write_u32(cmd.client_tick);
|
|
buf.write_u64(cmd.timestamp_us);
|
|
buf.write_u8(cmd.input.to_byte());
|
|
|
|
if (network_sim_.is_enabled()) {
|
|
network_sim_.send_packet(buf.data(), buf.size());
|
|
} else {
|
|
network_.send(server_peer_, NetworkChannel::Unreliable, buf.data(), buf.size(), false);
|
|
}
|
|
|
|
inputs_sent_++;
|
|
}
|
|
|
|
|
|
void send_ping() {
|
|
WriteBuffer buf;
|
|
buf.write_u8(static_cast<uint8_t>(MessageType::PingRequest));
|
|
buf.write_u32(++ping_id_);
|
|
buf.write_u64(Timestamp::now_us());
|
|
|
|
if (network_sim_.is_enabled()) {
|
|
network_sim_.send_packet(buf.data(), buf.size());
|
|
} else {
|
|
network_.send(server_peer_, NetworkChannel::Unreliable, buf.data(), buf.size(), false);
|
|
}
|
|
}
|
|
|
|
void update_metrics() {
|
|
fps_ = static_cast<float>(frame_count_);
|
|
frame_count_ = 0;
|
|
inputs_per_second_ = inputs_sent_;
|
|
inputs_sent_ = 0;
|
|
snapshots_per_second_ = snapshots_received_;
|
|
snapshots_received_ = 0;
|
|
|
|
uint64_t now_ms = Timestamp::now_ms();
|
|
|
|
if (state_ == ClientState::Connected && now_ms - last_log_time_ms_ >= 1000) {
|
|
auto pos_metrics = world_.metrics().get_position_metrics();
|
|
auto comp_metrics = world_.metrics().get_compensation_metrics(1.0); // за последнюю секунду
|
|
|
|
auto cfg = network_sim_.config();
|
|
auto sim_stats = network_sim_.get_stats();
|
|
|
|
uint64_t packets_sent_ps = sim_stats.packets_sent - prev_packets_sent_;
|
|
uint64_t packets_delivered_ps = sim_stats.packets_delivered - prev_packets_delivered_;
|
|
uint64_t packets_lost_ps = sim_stats.packets_lost - prev_packets_lost_;
|
|
uint64_t packets_duplicated_ps = sim_stats.packets_duplicated - prev_packets_duplicated_;
|
|
|
|
prev_packets_sent_ = sim_stats.packets_sent;
|
|
prev_packets_delivered_ = sim_stats.packets_delivered;
|
|
prev_packets_lost_ = sim_stats.packets_lost;
|
|
prev_packets_duplicated_ = sim_stats.packets_duplicated;
|
|
|
|
logger_.log(
|
|
now_ms,
|
|
network_presets_[current_preset_index_].first,
|
|
algorithm_name(current_algorithm_),
|
|
current_rtt_ms_,
|
|
current_jitter_ms_,
|
|
cfg.packet_loss * 100.0f,
|
|
packets_sent_ps,
|
|
packets_delivered_ps,
|
|
packets_lost_ps,
|
|
packets_duplicated_ps,
|
|
fps_,
|
|
inputs_per_second_,
|
|
snapshots_per_second_,
|
|
pos_metrics,
|
|
comp_metrics
|
|
);
|
|
|
|
world_.metrics().reset_compensation_metrics();
|
|
world_.metrics().reset();
|
|
|
|
last_log_time_ms_ = now_ms;
|
|
}
|
|
}
|
|
|
|
void render_team_selection() {
|
|
frame_count_++;
|
|
window_.clear(sf::Color(20, 20, 30));
|
|
|
|
sf::Text title(font_, "SELECT YOUR TEAM", 48);
|
|
title.setPosition(sf::Vector2f(window_.getSize().x / 2 - 250.0f, 100.0f));
|
|
title.setFillColor(sf::Color::White);
|
|
window_.draw(title);
|
|
|
|
float button_width = 300.0f;
|
|
float button_height = 150.0f;
|
|
float center_x = window_.getSize().x / 2;
|
|
float center_y = window_.getSize().y / 2;
|
|
|
|
float red_x = center_x - button_width - 50.0f;
|
|
float red_y = center_y - button_height / 2.0f;
|
|
red_team_button_.position = { red_x, red_y };
|
|
red_team_button_.size = { button_width, button_height };
|
|
|
|
sf::RectangleShape red_button(sf::Vector2f(button_width, button_height));
|
|
red_button.setPosition(sf::Vector2f(red_x, red_y));
|
|
red_button.setFillColor(sf::Color(150, 30, 30));
|
|
red_button.setOutlineColor(sf::Color(255, 100, 100));
|
|
red_button.setOutlineThickness(3.0f);
|
|
|
|
sf::Vector2i mouse_pos_i = sf::Mouse::getPosition(window_);
|
|
sf::Vector2f mouse_pos_f(static_cast<float>(mouse_pos_i.x), static_cast<float>(mouse_pos_i.y));
|
|
if (red_team_button_.contains(mouse_pos_f)) {
|
|
red_button.setFillColor(sf::Color(200, 50, 50));
|
|
}
|
|
window_.draw(red_button);
|
|
|
|
sf::Text red_text(font_, "RED TEAM", 32);
|
|
red_text.setPosition(sf::Vector2f(red_x + 50.0f, red_y + 60.0f));
|
|
red_text.setFillColor(sf::Color::White);
|
|
window_.draw(red_text);
|
|
|
|
float blue_x = center_x + 50.0f;
|
|
float blue_y = center_y - button_height / 2.0f;
|
|
blue_team_button_.position = { blue_x, blue_y };
|
|
blue_team_button_.size = { button_width, button_height };
|
|
sf::RectangleShape blue_button(sf::Vector2f(button_width, button_height));
|
|
blue_button.setPosition(sf::Vector2f(blue_x, blue_y));
|
|
blue_button.setFillColor(sf::Color(30, 30, 150));
|
|
blue_button.setOutlineColor(sf::Color(100, 100, 255));
|
|
blue_button.setOutlineThickness(3.0f);
|
|
|
|
if (blue_team_button_.contains(mouse_pos_f)) {
|
|
blue_button.setFillColor(sf::Color(50, 50, 200));
|
|
}
|
|
window_.draw(blue_button);
|
|
|
|
sf::Text blue_text(font_, "BLUE TEAM", 32);
|
|
blue_text.setPosition(sf::Vector2f(blue_x + 40.0f, blue_y + 60.0f));
|
|
blue_text.setFillColor(sf::Color::White);
|
|
window_.draw(blue_text);
|
|
|
|
float auto_x = center_x - button_width / 2.0f;
|
|
float auto_y = center_y + button_height;
|
|
auto_team_button_.position = { auto_x, auto_y };
|
|
auto_team_button_.size = { button_width, button_height * 0.6f };
|
|
sf::RectangleShape auto_button(sf::Vector2f(button_width, button_height * 0.6f));
|
|
auto_button.setPosition(sf::Vector2f(auto_x, auto_y));
|
|
auto_button.setFillColor(sf::Color(60, 60, 60));
|
|
auto_button.setOutlineColor(sf::Color(150, 150, 150));
|
|
auto_button.setOutlineThickness(3.0f);
|
|
|
|
if (auto_team_button_.contains(mouse_pos_f)) {
|
|
auto_button.setFillColor(sf::Color(80, 80, 80));
|
|
}
|
|
window_.draw(auto_button);
|
|
|
|
sf::Text auto_text(font_, "AUTO BALANCE", 24);
|
|
auto_text.setPosition(sf::Vector2f(auto_x + 60.0f, auto_y + 30.0f));
|
|
auto_text.setFillColor(sf::Color::White);
|
|
window_.draw(auto_text);
|
|
|
|
sf::Text instruction(font_, "Click to select your team", 20);
|
|
instruction.setPosition(sf::Vector2f(center_x - 120.0f, window_.getSize().y - 100.0f));
|
|
instruction.setFillColor(sf::Color(150, 150, 150));
|
|
window_.draw(instruction);
|
|
}
|
|
|
|
|
|
void render_connecting() {
|
|
frame_count_++;
|
|
window_.clear(sf::Color(20, 20, 30));
|
|
|
|
sf::Text text(font_, "Connecting to server...", 36);
|
|
text.setPosition(sf::Vector2f(window_.getSize().x / 2 - 200.0f,
|
|
window_.getSize().y / 2));
|
|
text.setFillColor(sf::Color::White);
|
|
window_.draw(text);
|
|
}
|
|
|
|
void render_disconnected() {
|
|
frame_count_++;
|
|
window_.clear(sf::Color(20, 20, 30));
|
|
|
|
sf::Text text(font_, "Disconnected from server", 36);
|
|
text.setPosition(sf::Vector2f(window_.getSize().x / 2 - 230.0f,
|
|
window_.getSize().y / 2 - 50.0f));
|
|
text.setFillColor(sf::Color::Red);
|
|
window_.draw(text);
|
|
|
|
sf::Text instruction(font_, "Press ESC to exit", 24);
|
|
instruction.setPosition(sf::Vector2f(window_.getSize().x / 2 - 100.0f,
|
|
window_.getSize().y / 2 + 50.0f));
|
|
instruction.setFillColor(sf::Color(150, 150, 150));
|
|
window_.draw(instruction);
|
|
}
|
|
|
|
void render() {
|
|
frame_count_++;
|
|
window_.clear(sf::Color(30, 30, 40));
|
|
|
|
const auto& params = world_.params();
|
|
|
|
float arena_display_width = netcode::DEFAULT_SIMULATION_PARAMS.arena_width;
|
|
float arena_display_height = netcode::DEFAULT_SIMULATION_PARAMS.arena_height;
|
|
float scale_x = arena_display_width / params.arena_width;
|
|
float scale_y = arena_display_height / params.arena_height;
|
|
float scale = (std::min)(scale_x, scale_y);
|
|
float offset_x = 60.0f;
|
|
float offset_y = 20.0f;
|
|
|
|
auto to_screen = [&](const Vec2& pos) -> sf::Vector2f {
|
|
return sf::Vector2f(offset_x + pos.x * scale, offset_y + pos.y * scale);
|
|
};
|
|
|
|
// Арена
|
|
sf::RectangleShape arena(sf::Vector2f(params.arena_width * scale, params.arena_height * scale));
|
|
arena.setPosition(sf::Vector2f(offset_x, offset_y));
|
|
arena.setFillColor(sf::Color(50, 50, 60));
|
|
arena.setOutlineColor(sf::Color::White);
|
|
arena.setOutlineThickness(2.0f);
|
|
window_.draw(arena);
|
|
|
|
// Зоны команд
|
|
sf::RectangleShape red_zone(sf::Vector2f(params.red_zone.width * scale, params.red_zone.height * scale));
|
|
red_zone.setPosition(to_screen(Vec2(params.red_zone.x, params.red_zone.y)));
|
|
red_zone.setFillColor(sf::Color(255, 0, 0, 50));
|
|
red_zone.setOutlineColor(sf::Color(255, 0, 0, 150));
|
|
red_zone.setOutlineThickness(2.0f);
|
|
window_.draw(red_zone);
|
|
|
|
sf::RectangleShape blue_zone(sf::Vector2f(params.blue_zone.width * scale, params.blue_zone.height * scale));
|
|
blue_zone.setPosition(to_screen(Vec2(params.blue_zone.x, params.blue_zone.y)));
|
|
blue_zone.setFillColor(sf::Color(0, 0, 255, 50));
|
|
blue_zone.setOutlineColor(sf::Color(0, 0, 255, 150));
|
|
blue_zone.setOutlineThickness(2.0f);
|
|
window_.draw(blue_zone);
|
|
|
|
// Сущности
|
|
for (const auto& [id, entity] : world_.entities()) {
|
|
render_entity(entity, scale, offset_x, offset_y);
|
|
}
|
|
|
|
// UI
|
|
render_ui();
|
|
}
|
|
|
|
|
|
void render_entity(const ClientEntity& entity, float scale, float offset_x, float offset_y) {
|
|
auto to_screen = [&](const Vec2& pos) -> sf::Vector2f {
|
|
return sf::Vector2f(offset_x + pos.x * scale, offset_y + pos.y * scale);
|
|
};
|
|
|
|
float radius = entity.radius * scale;
|
|
|
|
// Серверная позиция (полупрозрачная)
|
|
if (show_server_positions_) {
|
|
sf::CircleShape server_circle(radius);
|
|
server_circle.setOrigin(sf::Vector2f(radius, radius));
|
|
server_circle.setPosition(to_screen(entity.server_position));
|
|
server_circle.setFillColor(sf::Color(100, 100, 100, 80));
|
|
server_circle.setOutlineColor(sf::Color(150, 150, 150, 150));
|
|
server_circle.setOutlineThickness(1.0f);
|
|
window_.draw(server_circle);
|
|
}
|
|
|
|
// Предсказанная позиция
|
|
if (show_predicted_positions_ && entity.is_local_player) {
|
|
sf::CircleShape pred_circle(radius);
|
|
pred_circle.setOrigin(sf::Vector2f(radius, radius));
|
|
pred_circle.setPosition(to_screen(entity.predicted_position));
|
|
pred_circle.setFillColor(sf::Color(0, 255, 0, 60));
|
|
pred_circle.setOutlineColor(sf::Color(0, 255, 0, 200));
|
|
pred_circle.setOutlineThickness(1.0f);
|
|
window_.draw(pred_circle);
|
|
}
|
|
|
|
// Интерполированная позиция
|
|
if (show_interpolated_positions_ && !entity.is_local_player) {
|
|
sf::CircleShape interp_circle(radius);
|
|
interp_circle.setOrigin(sf::Vector2f(radius, radius));
|
|
interp_circle.setPosition(to_screen(entity.interpolated_position));
|
|
interp_circle.setFillColor(sf::Color(255, 255, 0, 60));
|
|
interp_circle.setOutlineColor(sf::Color(255, 255, 0, 200));
|
|
interp_circle.setOutlineThickness(1.0f);
|
|
window_.draw(interp_circle);
|
|
}
|
|
|
|
// рендер позиция
|
|
sf::CircleShape main_circle(radius);
|
|
main_circle.setOrigin(sf::Vector2f(radius, radius));
|
|
main_circle.setPosition(to_screen(entity.render_position));
|
|
|
|
if (entity.type == EntityType::Player) {
|
|
if (entity.is_local_player) {
|
|
main_circle.setFillColor(sf::Color(0, 200, 0));
|
|
main_circle.setOutlineColor(sf::Color::White);
|
|
} else if (entity.team == TeamId::Red) {
|
|
main_circle.setFillColor(sf::Color(200, 50, 50));
|
|
main_circle.setOutlineColor(sf::Color(255, 100, 100));
|
|
} else {
|
|
main_circle.setFillColor(sf::Color(50, 50, 200));
|
|
main_circle.setOutlineColor(sf::Color(100, 100, 255));
|
|
}
|
|
} else if (entity.type == EntityType::Ball) {
|
|
main_circle.setFillColor(sf::Color(255, 200, 50));
|
|
main_circle.setOutlineColor(sf::Color::White);
|
|
}
|
|
|
|
main_circle.setOutlineThickness(2.0f);
|
|
window_.draw(main_circle);
|
|
|
|
// Вектор скорости
|
|
if (show_velocity_vectors_) {
|
|
Vec2 vel_end = entity.render_position + entity.server_velocity * 0.1f;
|
|
sf::Vertex line[] = {
|
|
sf::Vertex(to_screen(entity.render_position), sf::Color::Yellow),
|
|
sf::Vertex(to_screen(vel_end), sf::Color::Red)
|
|
};
|
|
window_.draw(line, 2, sf::PrimitiveType::Lines);
|
|
}
|
|
|
|
// Линия коррекции
|
|
if (entity.is_local_player && entity.correction_offset.length_squared() > 0.1f) {
|
|
Vec2 correction_end = entity.render_position - entity.correction_offset;
|
|
sf::Vertex line[] = {
|
|
sf::Vertex(to_screen(entity.render_position), sf::Color::Magenta),
|
|
sf::Vertex(to_screen(correction_end), sf::Color::Cyan)
|
|
};
|
|
window_.draw(line, 2, sf::PrimitiveType::Lines);
|
|
}
|
|
}
|
|
void update_compensation_metrics() {
|
|
world_.metrics().reset_compensation_metrics();
|
|
}
|
|
void render_ui() {
|
|
float y = 10.0f;
|
|
float line_height = 20.0f;
|
|
|
|
auto draw_text = [&](const std::string& str, sf::Color color = sf::Color::White) {
|
|
sf::Text text(font_, str, 14);
|
|
text.setPosition(sf::Vector2f(10.0f, y));
|
|
text.setFillColor(color);
|
|
window_.draw(text);
|
|
y += line_height;
|
|
};
|
|
|
|
// Команда
|
|
std::string team_str = (local_team_ == TeamId::Red) ? "RED" :
|
|
(local_team_ == TeamId::Blue) ? "BLUE" : "NONE";
|
|
sf::Color team_color = (local_team_ == TeamId::Red) ? sf::Color(255, 100, 100) :
|
|
sf::Color(100, 100, 255);
|
|
draw_text(std::format("Team: {}", team_str), team_color);
|
|
|
|
// Алгоритм
|
|
draw_text(std::format("Algorithm: {} [1-7 to change]", algorithm_name(current_algorithm_)),
|
|
sf::Color::Yellow);
|
|
|
|
// Сеть
|
|
draw_text(std::format("RTT: {:.1f} ms | Jitter: {:.1f} ms", current_rtt_ms_, current_jitter_ms_));
|
|
draw_text(std::format("Server Tick: {} | FPS: {:.0f}", last_server_tick_, fps_));
|
|
draw_text(std::format("Inputs/s: {} | Snapshots/s: {}", inputs_per_second_, snapshots_per_second_));
|
|
|
|
draw_text(std::format("Interp Delay: {:.0f} ms [+/- to adjust] (Auto: {:.0f} ms)",
|
|
interpolation_delay_ms_,
|
|
current_rtt_ms_ / 2.0 + current_jitter_ms_ * 2.0));
|
|
|
|
// Счёт
|
|
y = 30.0f;
|
|
sf::Text score_text(font_, std::format("Red: {:.1f}s | Blue: {:.1f}s",
|
|
world_.red_team_time(), world_.blue_team_time()), 18);
|
|
score_text.setPosition(sf::Vector2f(window_.getSize().x - 260.0f, y));
|
|
score_text.setFillColor(sf::Color::White);
|
|
window_.draw(score_text);
|
|
|
|
if (show_debug_info_) {
|
|
y = 150.0f;
|
|
auto pos_metrics = world_.metrics().get_position_metrics();
|
|
draw_text(std::format("Position MAE: {:.2f} | MSE: {:.2f}", pos_metrics.mae, pos_metrics.mse));
|
|
draw_text(std::format("Max Error: {:.2f} | Samples: {}", pos_metrics.max_error, pos_metrics.sample_count));
|
|
|
|
auto comp_metrics = world_.metrics().get_compensation_metrics(1.0);
|
|
draw_text(std::format("Predictions/s: {} | Reconciliations/s: {}",
|
|
comp_metrics.predictions_per_second, comp_metrics.reconciliations_per_second));
|
|
draw_text(std::format("Avg Correction: {:.2f} | Max: {:.2f}",
|
|
comp_metrics.avg_correction_distance, comp_metrics.max_correction_distance));
|
|
}
|
|
if (show_network_stats_ && network_sim_.is_enabled()) {
|
|
y = 310.0f;
|
|
|
|
auto sim_stats = network_sim_.get_stats();
|
|
auto cfg = network_sim_.config();
|
|
|
|
draw_text("Network Simulation", sf::Color::Cyan);
|
|
draw_text(std::format("Preset: {}", network_presets_[current_preset_index_].first),
|
|
sf::Color::Yellow);
|
|
draw_text(std::format("Base Latency: {:.1f}ms (RTT: {:.1f}ms)",
|
|
cfg.base_latency_ms, cfg.base_latency_ms * 2.0f));
|
|
draw_text(std::format("Jitter: {:.1f}ms", cfg.jitter_ms));
|
|
draw_text(std::format("Packet Loss: {:.1f}%", cfg.packet_loss * 100.0f));
|
|
|
|
if (cfg.packet_duplication > 0.0f) {
|
|
draw_text(std::format("Duplication: {:.1f}%", cfg.packet_duplication * 100.0f));
|
|
}
|
|
|
|
draw_text(std::format("Sent: {} | Delivered: {} | Lost: {}",
|
|
sim_stats.packets_sent, sim_stats.packets_delivered, sim_stats.packets_lost));
|
|
|
|
if (sim_stats.packets_duplicated > 0) {
|
|
draw_text(std::format("Duplicated: {}", sim_stats.packets_duplicated));
|
|
}
|
|
|
|
draw_text(std::format("Queue: {} packets", sim_stats.packets_in_queue));
|
|
}
|
|
else {
|
|
y = 300;
|
|
draw_text("No simulation", sf::Color::Yellow);
|
|
}
|
|
|
|
y = window_.getSize().y - 120.0f;
|
|
draw_text("F1-F5: Visualization | F6: Network Stats", sf::Color(150, 150, 150));
|
|
draw_text("N/M: Network Preset | L: Lag Spike | P: Packet Loss", sf::Color(150, 150, 150));
|
|
draw_text("PgUp/PgDn: Latency | Home/End: Jitter", sf::Color(150, 150, 150));
|
|
draw_text("WASD: Move | 1-7: Algorithm | ESC: Quit", sf::Color(150, 150, 150));
|
|
}
|
|
|
|
private:
|
|
// Окно и рендер
|
|
sf::RenderWindow window_;
|
|
sf::Font font_;
|
|
|
|
// Состояние
|
|
ClientState state_;
|
|
std::string host_;
|
|
uint16_t port_;
|
|
|
|
// Выбор команды
|
|
TeamId selected_team_ = TeamId::None;
|
|
sf::FloatRect red_team_button_;
|
|
sf::FloatRect blue_team_button_;
|
|
sf::FloatRect auto_team_button_;
|
|
|
|
// Сеть
|
|
ENetInitializer enet_init_;
|
|
NetworkHost network_;
|
|
ENetPeer* server_peer_ = nullptr;
|
|
TimeSynchronizer time_sync_;
|
|
|
|
NetworkSimulator network_sim_;
|
|
bool show_network_stats_ = true;
|
|
|
|
std::vector<std::pair<std::string, NetworkSimulator::Config>> network_presets_;
|
|
size_t current_preset_index_ = 0;
|
|
|
|
// Мир
|
|
ClientWorld world_;
|
|
|
|
// Состояние подключения
|
|
ClientId local_client_id_ = INVALID_CLIENT_ID;
|
|
EntityId local_entity_id_ = INVALID_ENTITY_ID;
|
|
TeamId local_team_ = TeamId::None;
|
|
std::string player_name_ = "Player";
|
|
|
|
// Ввод
|
|
SequenceNumber input_sequence_ = 0;
|
|
TickNumber client_tick_ = 0;
|
|
RingBuffer<InputCommand, 64> recent_inputs_;
|
|
|
|
// Алгоритм
|
|
CompensationAlgorithm current_algorithm_ = CompensationAlgorithm::Hybrid;
|
|
float interpolation_delay_ms_ = 100.0f;
|
|
|
|
// Визуализация
|
|
bool show_server_positions_ = true;
|
|
bool show_predicted_positions_ = true;
|
|
bool show_interpolated_positions_ = true;
|
|
bool show_velocity_vectors_ = false;
|
|
bool show_debug_info_ = true;
|
|
|
|
// Метрики
|
|
float ping_timer_ = 0.0f;
|
|
uint32_t ping_id_ = 0;
|
|
double current_rtt_ms_ = 0.0;
|
|
double current_jitter_ms_ = 0.0;
|
|
TickNumber last_server_tick_ = 0;
|
|
MetricsLogger logger_{"client_metrics.csv"};
|
|
uint64_t last_log_time_ms_ = 0;
|
|
|
|
uint64_t prev_packets_sent_ = 0;
|
|
uint64_t prev_packets_delivered_ = 0;
|
|
uint64_t prev_packets_lost_ = 0;
|
|
uint64_t prev_packets_duplicated_ = 0;
|
|
|
|
float fps_ = 0.0f;
|
|
uint32_t frame_count_ = 0;
|
|
uint32_t inputs_sent_ = 0;
|
|
uint32_t inputs_per_second_ = 0;
|
|
uint32_t snapshots_received_ = 0;
|
|
uint32_t snapshots_per_second_ = 0;
|
|
|
|
// Тесты
|
|
bool auto_test_mode_ = false;
|
|
bool bot_enabled_ = false;
|
|
|
|
struct BotConfig {
|
|
float test_duration_per_preset_sec = 10.0f;
|
|
float stabilization_time_sec = 10.0f;
|
|
float movement_change_interval_sec = 4.0f;
|
|
float speed_factor = 1.0f;
|
|
bool random_movements = true;
|
|
float circle_radius = 15.0f;
|
|
float circle_speed = 2.0f;
|
|
};
|
|
|
|
BotConfig bot_config_;
|
|
|
|
float bot_timer_ = 0.0f;
|
|
float direction_timer_ = 0.0f;
|
|
Vec2 current_bot_direction_ = {0.0f, 0.0f};
|
|
float circle_angle_ = 0.0f;
|
|
|
|
std::vector<CompensationAlgorithm> algorithms_to_test_ = {
|
|
|
|
CompensationAlgorithm::ClientPrediction,
|
|
CompensationAlgorithm::PredictionReconciliation,
|
|
CompensationAlgorithm::EntityInterpolation,
|
|
CompensationAlgorithm::DeadReckoning,
|
|
CompensationAlgorithm::Hybrid,
|
|
};
|
|
};
|
|
|
|
int main(int argc, char* argv[]) {
|
|
std::string host = "::1";
|
|
uint16_t port = 7777;
|
|
|
|
bool auto_test = false;
|
|
for (int i = 1; i < argc; ++i) {
|
|
std::string arg = argv[i];
|
|
if (arg == "--auto-test") {
|
|
auto_test = true;
|
|
} else if (arg == "-h" || arg == "--host") {
|
|
if (i + 1 < argc) host = argv[++i];
|
|
} else if (arg == "-p" || arg == "--port") {
|
|
if (i + 1 < argc) port = static_cast<uint16_t>(std::stoi(argv[++i]));
|
|
} else if (arg == "--help") {
|
|
std::cout << "Usage: client [-h host] [-p port]\n";
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
std::cout << "Will connect to " << host << ":" << port << "...\n";
|
|
|
|
try {
|
|
GameClient client;
|
|
if (auto_test) {
|
|
client.enable_auto_test();
|
|
}
|
|
|
|
if (client.connect(host, port)) {
|
|
client.run();
|
|
}
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Error: " << e.what() << "\n";
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
} |