diff --git a/application/application.cpp b/application/application.cpp index 13d8afbf..ffcf1e61 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -594,6 +594,7 @@ int eu07_application::run() if (statehash != authoritative.state_hash) { ++m_statemismatches; + Global.network_digest_mismatches = m_statemismatches; if (m_statemismatches == 1 || (m_statemismatches % NETWORK_DIGEST_LOG_INTERVAL) == 0) { WriteLog("net: state digest differs at tick " + std::to_string(authoritative.tick) + " (" + std::to_string(m_statemismatches) + @@ -605,6 +606,7 @@ int eu07_application::run() { WriteLog("net: state digest back in step at tick " + std::to_string(authoritative.tick), logtype::net); m_statemismatches = 0; + Global.network_digest_mismatches = 0; } } diff --git a/application/drivermode.cpp b/application/drivermode.cpp index daacf7a0..e252f4c6 100644 --- a/application/drivermode.cpp +++ b/application/drivermode.cpp @@ -183,6 +183,54 @@ bool driver_mode::update() simulation::State.update_scripting_interface(); simulation::Environment.update(); + // taking a seat has to work whether the world is running or not: a player sitting + // in the lobby with the game paused still expects the button to put them in the cab + if (Global.network_leave_pending) + { + // we were taken off the crew (by our own request or by the server), so the + // camera goes back to observing. the cab instance itself stays put, it may + // still be occupied by somebody else or handed back to the AI + Global.network_leave_pending = false; + if (simulation::Train != nullptr) + { + if (!FreeFlyModeFlag) + InOutKey(); + simulation::Train = nullptr; + Camera.m_owner = nullptr; + Global.local_start_vehicle = "ghostview"; + } + } + + if (change_train.empty() && !Global.network_pending_vehicle.empty()) + { + // vehicle picked in the multiplayer lobby; the cab itself is built by the + // replicated entervehicle command, we only have to move in once it exists + change_train = Global.network_pending_vehicle; + Global.network_pending_vehicle.clear(); + } + + if (!change_train.empty()) + { + TTrain *train = simulation::Trains.find(change_train); + if (train) + { + Global.local_start_vehicle = change_train; + simulation::Train = train; + InOutKey(); + if (!Application.is_server() && !Application.is_client()) + { + // in a multiplayer session the AI handover belongs to the server, which + // also knows whether the vehicle had an AI driver in the first place + m_relay.post(user_command::aidriverdisable, 0.0, 0.0, GLFW_PRESS, 0); + } + change_train.clear(); + + auto ui = std::dynamic_pointer_cast(m_userinterface); + if (ui != nullptr) + ui->show_multiplayer_lobby(false); + } + } + if (deltatime != 0.0 || false == simulation::is_ready) { // jak pauza, to nie ma po co tego przeliczać @@ -267,52 +315,6 @@ bool driver_mode::update() // variable step simulation time routines - if (Global.network_leave_pending) - { - // we were taken off the crew (by our own request or by the server), so the - // camera goes back to observing. the cab instance itself stays put, it may - // still be occupied by somebody else or handed back to the AI - Global.network_leave_pending = false; - if (simulation::Train != nullptr) - { - if (!FreeFlyModeFlag) - InOutKey(); - simulation::Train = nullptr; - Camera.m_owner = nullptr; - Global.local_start_vehicle = "ghostview"; - } - } - - if (change_train.empty() && !Global.network_pending_vehicle.empty()) - { - // vehicle picked in the multiplayer lobby; the cab itself is built by the - // replicated entervehicle command, we only have to move in once it exists - change_train = Global.network_pending_vehicle; - Global.network_pending_vehicle.clear(); - } - - if (!change_train.empty()) - { - TTrain *train = simulation::Trains.find(change_train); - if (train) - { - Global.local_start_vehicle = change_train; - simulation::Train = train; - InOutKey(); - if (!Application.is_server() && !Application.is_client()) - { - // in a multiplayer session the AI handover belongs to the server, which - // also knows whether the vehicle had an AI driver in the first place - m_relay.post(user_command::aidriverdisable, 0.0, 0.0, GLFW_PRESS, 0); - } - change_train.clear(); - - auto ui = std::dynamic_pointer_cast(m_userinterface); - if (ui != nullptr) - ui->show_multiplayer_lobby(false); - } - } - if (simulation::Train == nullptr && false == FreeFlyModeFlag) { // intercept cases when the driven train got removed after entering portal diff --git a/application/driveruilayer.cpp b/application/driveruilayer.cpp index 67cfbfa0..197a6ceb 100644 --- a/application/driveruilayer.cpp +++ b/application/driveruilayer.cpp @@ -282,11 +282,4 @@ void driver_ui::render_() ImGui::OpenPopup(popupheader); } - if (Global.network_position_error > 0.5f) - { - ImGui::SetNextWindowSize(ImVec2(-1, -1)); - if (ImGui::Begin("network", nullptr, ImGuiWindowFlags_NoCollapse)) - ImGui::Text("%.1f m behind the server (tick %llu)", Global.network_position_error, (unsigned long long)Global.simulation_tick); - ImGui::End(); - } } diff --git a/application/driveruipanels.cpp b/application/driveruipanels.cpp index b84b1086..ca29bf29 100644 --- a/application/driveruipanels.cpp +++ b/application/driveruipanels.cpp @@ -545,6 +545,7 @@ void debug_panel::update() m_powergridlines.clear(); m_cameralines.clear(); m_rendererlines.clear(); + m_networklines.clear(); #ifdef WITH_UART m_uartlines.clear(); #endif @@ -561,6 +562,11 @@ void debug_panel::update() update_section_powergrid( m_powergridlines ); update_section_camera( m_cameralines ); update_section_renderer( m_rendererlines ); + if( Application.is_server() || Application.is_client() ) { + update_section_network( m_networklines ); + net_sent_graph.update( (float)( network::Traffic.sent_rate / 1024.0 ) ); + net_received_graph.update( (float)( network::Traffic.received_rate / 1024.0 ) ); + } #ifdef WITH_UART update_section_uart(m_uartlines); #endif @@ -621,6 +627,13 @@ debug_panel::render() { } render_section( "Camera", m_cameralines ); render_section( "Gfx Renderer / Statistics", m_rendererlines ); + if( ( Application.is_server() || Application.is_client() ) + && ( true == render_section( "Network", m_networklines ) ) ) { + ImGui::TextUnformatted( "sent kB/s" ); + net_sent_graph.render(); + ImGui::TextUnformatted( "received kB/s" ); + net_received_graph.render(); + } render_section_settings(); render_section_developer(); // Developer tools #ifdef WITH_UART @@ -1025,6 +1038,70 @@ void debug_panel::graph_data::render() { ImGui::PopID(); } +namespace { + +std::string format_bytes( uint64_t const Bytes ) { + + if( Bytes < 1024ull ) { return std::to_string( Bytes ) + " B"; } + if( Bytes < 1024ull * 1024ull ) { return to_string( Bytes / 1024.0, 1 ) + " kB"; } + if( Bytes < 1024ull * 1024ull * 1024ull ) { return to_string( Bytes / ( 1024.0 * 1024.0 ), 2 ) + " MB"; } + return to_string( Bytes / ( 1024.0 * 1024.0 * 1024.0 ), 2 ) + " GB"; +} + +} // namespace + +void +debug_panel::update_section_network( std::vector &Output ) { + + auto const host { Application.is_server() }; + auto const client { Application.is_client() }; + + Output.emplace_back( + "role: " + std::string( host ? ( client ? "host + client" : "host" ) : "client" ) + + ", peer " + std::to_string( Global.network_peer_id ) + + ", tick " + std::to_string( Global.simulation_tick ), + Global.UITextColor ); + + auto const &traffic { network::Traffic }; + + Output.emplace_back( + "sent: " + format_bytes( traffic.sent_bytes ) + " in " + std::to_string( traffic.sent_messages ) + " messages" + + " (" + to_string( traffic.sent_rate / 1024.0, 2 ) + " kB/s)", + Global.UITextColor ); + + Output.emplace_back( + "received: " + format_bytes( traffic.received_bytes ) + " in " + std::to_string( traffic.received_messages ) + " messages" + + " (" + to_string( traffic.received_rate / 1024.0, 2 ) + " kB/s)", + Global.UITextColor ); + + Output.emplace_back( + "total: " + format_bytes( traffic.sent_bytes + traffic.received_bytes ) + + " (" + to_string( ( traffic.sent_rate + traffic.received_rate ) / 1024.0, 2 ) + " kB/s)", + Global.UITextColor ); + + if( client ) { + Output.emplace_back( + "worst vehicle out of place: " + to_string( Global.network_position_error, 2 ) + " m", + ( Global.network_position_error > 5.0f ? + glm::vec4( 1.f, 0.6f, 0.3f, 1.f ) : + Global.UITextColor ) ); + + Output.emplace_back( + "state digest: " + std::string( + Global.network_digest_mismatches == 0 ? + "in step with the server" : + "differing for " + std::to_string( Global.network_digest_mismatches ) + " steps" ), + Global.UITextColor ); + } + + if( !Global.network_lobby_message.empty() ) { + Output.emplace_back( "lobby: " + Global.network_lobby_message, glm::vec4( 1.f, 0.6f, 0.3f, 1.f ) ); + } + if( !Global.network_reject_reason.empty() ) { + Output.emplace_back( "refused: " + Global.network_reject_reason, glm::vec4( 1.f, 0.4f, 0.3f, 1.f ) ); + } +} + std::string debug_panel::update_vehicle_coupler( int const Side ) { // NOTE: mover and vehicle are guaranteed to be valid by the caller diff --git a/application/driveruipanels.h b/application/driveruipanels.h index 9df8acc3..f9cd1406 100644 --- a/application/driveruipanels.h +++ b/application/driveruipanels.h @@ -89,6 +89,7 @@ private: void update_section_powergrid( std::vector &Output ); void update_section_camera( std::vector &Output ); void update_section_renderer( std::vector &Output ); + void update_section_network( std::vector &Output ); #ifdef WITH_UART void update_section_uart( std::vector &Output ); #endif @@ -123,6 +124,7 @@ private: m_eventqueuelines, m_powergridlines, m_rendererlines, + m_networklines, m_uartlines, m_hardwarelines; @@ -149,6 +151,9 @@ private: }; graph_data AccN_jerk_graph; graph_data AccN_acc_graph; + // kilobytes per second in and out, so the cost of a session can be read off a chart + graph_data net_sent_graph; + graph_data net_received_graph; float last_AccN; std::array queue_event_buf = { 0 }; diff --git a/network/backend/asio.cpp b/network/backend/asio.cpp index 958b084e..f4781596 100644 --- a/network/backend/asio.cpp +++ b/network/backend/asio.cpp @@ -23,6 +23,8 @@ void network::tcp::connection::disconnect() void network::tcp::connection::send_data(std::shared_ptr buffer) { + Traffic.sent(buffer->size()); + asio::async_write(m_socket, asio::buffer(*buffer.get()), std::bind(&connection::send_complete, this, buffer)); } @@ -52,6 +54,8 @@ void network::tcp::connection::handle_header(const asio::error_code &err, size_t return; } + Traffic.received(bytes_transferred); + uint32_t sig = sn_utils::ld_uint32(header); if (sig != NETWORK_MAGIC) { disconnect(); @@ -81,6 +85,8 @@ void network::tcp::connection::handle_data(const asio::error_code &err, size_t b return; } + Traffic.received(bytes_transferred); + std::istringstream stream(m_body_buffer); std::shared_ptr msg = deserialize_message(stream); if (message_handler) diff --git a/network/manager.cpp b/network/manager.cpp index 4624463d..e12c996d 100644 --- a/network/manager.cpp +++ b/network/manager.cpp @@ -199,6 +199,8 @@ void network::manager::request_resync(uint64_t tick, uint64_t state_hash) void network::manager::update() { + Traffic.update(); + for (auto &backend : backend_list()) backend.second->update(); diff --git a/network/network.cpp b/network/network.cpp index 5398c978..d9ef1c53 100644 --- a/network/network.cpp +++ b/network/network.cpp @@ -19,6 +19,32 @@ std::uint32_t const EU07_NETWORK_VERSION = 4; namespace network { +traffic_counters Traffic; + +void traffic_counters::update() +{ + auto const now = Timer::GetTime(); + + if (rate_time_mark < 0.0) + { + rate_time_mark = now; + rate_sent_mark = sent_bytes; + rate_received_mark = received_bytes; + return; + } + + auto const elapsed = now - rate_time_mark; + if (elapsed < 1.0) + return; + + sent_rate = (double)(sent_bytes - rate_sent_mark) / elapsed; + received_rate = (double)(received_bytes - rate_received_mark) / elapsed; + + rate_time_mark = now; + rate_sent_mark = sent_bytes; + rate_received_mark = received_bytes; +} + backend_list_t& backend_list() { // HACK: static initialization order fiasco fix // NOTE: potential static deinitialization order fiasco diff --git a/network/network.h b/network/network.h index 62a12b50..bcd1332b 100644 --- a/network/network.h +++ b/network/network.h @@ -11,6 +11,31 @@ namespace network { + // what has gone over the wire so far, and how fast it is going now. the transport + // layer feeds it; the debug panel reads it + struct traffic_counters + { + uint64_t sent_bytes { 0 }; + uint64_t received_bytes { 0 }; + uint64_t sent_messages { 0 }; + uint64_t received_messages { 0 }; + // bytes per second, averaged over the last second + double sent_rate { 0.0 }; + double received_rate { 0.0 }; + + void sent(size_t Bytes) { sent_bytes += Bytes; ++sent_messages; } + void received(size_t Bytes) { received_bytes += Bytes; ++received_messages; } + // recomputes the rates; called once per frame + void update(); + + private: + uint64_t rate_sent_mark { 0 }; + uint64_t rate_received_mark { 0 }; + double rate_time_mark { -1.0 }; + }; + + extern traffic_counters Traffic; + //m7todo: separate client/server connection class? class connection { diff --git a/network/snapshot.cpp b/network/snapshot.cpp index ca4e0ecc..d2af4ed1 100644 --- a/network/snapshot.cpp +++ b/network/snapshot.cpp @@ -416,29 +416,54 @@ void apply_controls(TDynamicObject &Vehicle, vehicle_state const &State) Vehicle.Mechanik->TakeControl(State.aiactive); } -// puts a vehicle back where the authority says it is. place_on_track() wants the distance -// of the vehicle's nose along the track, while what travels in the update is the offset of -// its leading bogie; this inverts the arithmetic that function itself does -void reposition(TDynamicObject &Vehicle, vehicle_state const &State) +// puts a vehicle where the authority says it is. +// +// there are two ways to do that and they are not interchangeable. place_on_track() sets +// the axles from scratch, which is right for a peer that is joining and has nothing to go +// on - but doing it to a running train turns the whole set around, because the placement +// works from the vehicle's own idea of which way it faces and that is not what a moving +// consist looks like from outside. So a routine correction slides the vehicle along the +// track it is already on, by the difference between the two bogie offsets. That is the +// very same operation the physics performs on every step, so nothing gets reoriented and +// nothing ends up beside the rails +void correct_position(TDynamicObject &Vehicle, vehicle_state const &State, bool const Absolute) { - TTrack *track = track_by_id(State.track); - if (track == nullptr) + if (Absolute) { - static bool reported{false}; - if (!reported) + TTrack *track = track_by_id(State.track); + if (track == nullptr) { - reported = true; - ErrorLog("net: cannot place " + Vehicle.name() + ", track " + std::to_string(State.track) + " is unknown here", logtype::net); + static bool reported{false}; + if (!reported) + { + reported = true; + ErrorLog("net: cannot place " + Vehicle.name() + ", track " + std::to_string(State.track) + " is unknown here", logtype::net); + } + return; } + + double const half = Vehicle.fAxleDist * 0.5; + double const axleoffset = (State.axlefirst ? -half : half); + double const sign = (State.direction ? 1.0 : -1.0); + double const nose = (State.translation - axleoffset) * sign + 0.5 * Vehicle.MoverParameters->Dim.L; + + Vehicle.place_on_track(track, nose, false); return; } - double const half = Vehicle.fAxleDist * 0.5; - double const axleoffset = (State.axlefirst ? -half : half); - double const sign = (State.direction ? 1.0 : -1.0); - double const nose = (State.translation - axleoffset) * sign + 0.5 * Vehicle.MoverParameters->Dim.L; + // both sides have to be talking about the same piece of track for the difference of + // the offsets to mean anything. when they are not, the vehicle is left alone until it + // gets there - a wrong guess here is what puts a locomotive next to the rails + if (track_id_of(Vehicle.RaTrackGet()) != State.track) + return; - Vehicle.place_on_track(track, nose, false); + double const delta = State.translation - Vehicle.RaTranslationGet(); + if (std::abs(delta) < 0.01) + return; + + // the offset runs from the track's first point, the move runs along the vehicle, and + // the axle's own sense of direction is what converts between the two + Vehicle.Move(delta * Vehicle.RaDirectionGet()); } std::string write_vehicles(bool const Full, bool const OwnedOnly) @@ -590,7 +615,7 @@ void read_vehicles(std::istream &Stream, network::snapshot_mode const Mode, netw for (TDynamicObject *member : group) { - reposition(*member, states.at(member)); + correct_position(*member, states.at(member), Mode == network::snapshot_mode::join); ++Result.repositioned; } } diff --git a/utilities/Globals.h b/utilities/Globals.h index 23394a94..d5b2ef77 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -375,6 +375,8 @@ struct global_settings { // the scenario has finished loading and the world can be worked with. this is not the // same as simulation::is_ready, which only goes up once the player has a cab bool simulation_loaded = false; + // consecutive steps whose state digest disagreed with the server's; diagnostics only + uint64_t network_digest_mismatches = 0; std::unordered_map trainset_overrides;