#ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include #include #endif #define NOMINMAX #include #include #include #include #include #include #include #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 #include #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 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(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()) { 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()) { handle_key_press(key->code); } if (const auto* mouse = event->getIf()) { 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(MessageType::ClientSetAlgorithm)); buf.write_u8(static_cast(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(MessageType::ClientConnect)); buf.write_u32(msg.protocol_version); buf.write_string(msg.player_name, sizeof(msg.player_name)); buf.write_u8(static_cast(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(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(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(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(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(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(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(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(mouse_pos_i.x), static_cast(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> 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 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 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(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; }