diff --git a/CMakeLists.txt b/CMakeLists.txt index 256de9c9..b223f699 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -193,9 +193,11 @@ set(SOURCES "network/network.cpp" "network/message.cpp" "network/manager.cpp" +"network/entities.cpp" "network/backend/asio.cpp" "widgets/vehiclelist.cpp" +"widgets/multiplayer_lobby.cpp" "widgets/map.cpp" "widgets/map_objects.cpp" "widgets/popup.cpp" diff --git a/application/application.cpp b/application/application.cpp index d5cd118c..714a02bc 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -1517,6 +1517,11 @@ bool eu07_application::init_network() { // create network manager m_network.emplace(); + + // the host takes part in the session like any other peer, with an identity of its + // own, so that crew and permission handling needs no special case for it + if (!Global.network_client) + Global.network_peer_id = network::PEER_HOST; } for (auto const &pair : Global.network_servers) @@ -1526,14 +1531,15 @@ bool eu07_application::init_network() m_network->create_server(pair.first, pair.second); } + if (!Global.local_start_vehicle_override && (Global.network_client || !Global.network_servers.empty())) + { + // in a multiplayer session the vehicle is picked in the lobby, so both the host + // and the joining clients start out as observers rather than in a random cab + Global.local_start_vehicle = "ghostview"; + } + if (Global.network_client) { - if (!Global.local_start_vehicle_override) - { - // in a multiplayer session the vehicle is assigned by the server; - // until the lobby exists the client simply starts out as an observer - Global.local_start_vehicle = "ghostview"; - } Global.network_status = "Connecting to " + Global.network_client->second + "..."; WriteLog("net: connecting to " + Global.network_client->second + " (" + Global.network_client->first + ")", logtype::net); diff --git a/application/drivermode.cpp b/application/drivermode.cpp index f9a6dfe2..1ca75c37 100644 --- a/application/drivermode.cpp +++ b/application/drivermode.cpp @@ -266,6 +266,14 @@ bool driver_mode::update() // variable step simulation time routines + 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); @@ -452,6 +460,15 @@ void driver_mode::enter() Error("Bad scenario: failed to locate player train, \"" + Global.local_start_vehicle + "\""); } + if (Application.is_server() || Application.is_client()) + { + // in a multiplayer session nobody is dropped into a random vehicle: the roster + // published by the server is offered instead, and stays available for later switches + auto ui = std::dynamic_pointer_cast(m_userinterface); + if (ui != nullptr) + ui->show_multiplayer_lobby(nPlayerTrain == nullptr); + } + // if (!Global.bMultiplayer) //na razie włączone { // eventy aktywowane z klawiatury tylko dla jednego użytkownika KeyEvents[0] = simulation::Events.FindEvent("keyctrl00"); diff --git a/application/driveruilayer.cpp b/application/driveruilayer.cpp index 0b8463f8..6999be42 100644 --- a/application/driveruilayer.cpp +++ b/application/driveruilayer.cpp @@ -37,6 +37,7 @@ driver_ui::driver_ui() add_external_panel(&m_logpanel); add_external_panel(&m_perfgraphpanel); add_external_panel(&m_cameraviewpanel); + add_external_panel(&m_multiplayerlobby); m_logpanel.is_open = false; m_aidpanel.title = STR("Driving Aid"); @@ -87,10 +88,18 @@ void driver_ui::render_menu_contents() if (ImGui::MenuItem(m_timepanel.name().c_str())) m_timepanel.open(); + if (Application.is_server() || Application.is_client()) + ImGui::MenuItem(m_multiplayerlobby.name().c_str(), nullptr, &m_multiplayerlobby.is_open); + ImGui::EndMenu(); } } +void driver_ui::show_multiplayer_lobby(bool const Show) +{ + m_multiplayerlobby.is_open = Show; +} + void driver_ui::showDebugUI() { m_debugpanel.is_open = !m_debugpanel.is_open; diff --git a/application/driveruilayer.h b/application/driveruilayer.h index 8ad1d775..2c33b2dc 100644 --- a/application/driveruilayer.h +++ b/application/driveruilayer.h @@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/. #include "widgets/trainingcard.h" #include "widgets/perfgraphs.h" #include "widgets/cameraview_extcam.h" +#include "widgets/multiplayer_lobby.h" class driver_ui : public ui_layer { @@ -40,6 +41,9 @@ public: // updates state of UI elements void update() override; + // opens or closes the multiplayer lobby + void + show_multiplayer_lobby( bool const Show ); protected: void render_menu_contents() override; @@ -69,4 +73,5 @@ private: ui::map_panel m_mappanel; ui::time_panel m_timepanel; ui::cameraview_panel m_cameraviewpanel; + ui::multiplayer_lobby_panel m_multiplayerlobby; }; diff --git a/application/scenarioloadermode.cpp b/application/scenarioloadermode.cpp index c514c5d3..098f60d5 100644 --- a/application/scenarioloadermode.cpp +++ b/application/scenarioloadermode.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/renderer.h" #include "utilities/Logs.h" #include "utilities/translation.h" +#include "network/entities.h" scenarioloader_mode::scenarioloader_mode() { m_userinterface = std::make_shared(); @@ -61,6 +62,11 @@ bool scenarioloader_mode::update() { Application.pop_mode(); } + if( Application.is_server() ) { + // the server owns the vehicle numbering for the whole session and publishes it + network::Entities.build(); + } + WriteLog( "Scenario loading time: " + std::to_string( std::chrono::duration_cast( std::chrono::system_clock::now() - timestart ).count() ) + " seconds" ); // TODO: implement and use next mode cue diff --git a/network/entities.cpp b/network/entities.cpp new file mode 100644 index 00000000..3bdd08af --- /dev/null +++ b/network/entities.cpp @@ -0,0 +1,148 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "network/entities.h" + +#include "simulation/simulation.h" +#include "vehicle/DynObj.h" +#include "vehicle/Driver.h" +#include "vehicle/Train.h" +#include "utilities/Logs.h" + +namespace network +{ + +entity_registry Entities; + +PeerId allocate_peer_id() +{ + static PeerId next = PEER_FIRST_REMOTE; + return next++; +} + +bool is_drivable(TDynamicObject const *Vehicle) +{ + if (Vehicle == nullptr || Vehicle->MoverParameters == nullptr) + return false; + + // anything with its own drive, plus anything the scenario already put a driver in + // (this covers driving trailers of multiple units, which carry no engine of their own) + // TODO: a cab count exposed by TMoverParameters would be a more precise test + return Vehicle->MoverParameters->EngineType != TEngineType::None || Vehicle->Mechanik != nullptr; +} + +void entity_registry::clear() +{ + m_entries.clear(); + m_byname.clear(); + m_byid.clear(); +} + +void entity_registry::build() +{ + clear(); + + std::vector names; + for (TDynamicObject *vehicle : simulation::Vehicles.sequence()) + { + if (!is_drivable(vehicle)) + continue; + names.emplace_back(vehicle->name()); + } + + // name order keeps the numbering reproducible regardless of scenario spawn order + std::sort(names.begin(), names.end()); + + for (auto const &name : names) + { + if (m_byname.find(name) != m_byname.end()) + { + // a malformed scenario can hold duplicate names; only the first one is addressable + ErrorLog("net: duplicate vehicle name \"" + name + "\", not assigning a second network id", logtype::net); + continue; + } + + vehicle_entry entry; + entry.id = static_cast(m_entries.size() + 1); + entry.name = name; + m_entries.emplace_back(entry); + m_byname.emplace(name, entry.id); + } + + reindex(); + refresh(); + + WriteLog("net: assigned network ids to " + std::to_string(m_entries.size()) + " drivable vehicles", logtype::net); +} + +void entity_registry::adopt(std::vector const &Entries) +{ + m_entries = Entries; + m_byname.clear(); + for (auto const &entry : m_entries) + m_byname.emplace(entry.name, entry.id); + reindex(); +} + +void entity_registry::reindex() +{ + m_byid.clear(); + for (size_t i = 0; i < m_entries.size(); ++i) + m_byid.emplace(m_entries[i].id, i); +} + +void entity_registry::refresh() +{ + for (auto &entry : m_entries) + { + TDynamicObject const *vehicle = resolve(entry.id); + + entry.ai_active = (vehicle != nullptr && vehicle->Mechanik != nullptr && vehicle->Mechanik->AIControllFlag); + // until the crew model of stage 3 is in place, an existing cab instance is the + // only evidence that somebody took the vehicle over + entry.crew_count = (simulation::Trains.find(entry.name) != nullptr ? 1 : 0); + entry.crew_capacity = CREW_CAPACITY; + entry.claimable = (vehicle != nullptr && entry.crew_count < entry.crew_capacity); + } +} + +NetworkEntityId entity_registry::id_of(std::string const &Name) const +{ + auto const lookup = m_byname.find(Name); + return lookup != m_byname.end() ? lookup->second : ENTITY_NONE; +} + +NetworkEntityId entity_registry::id_of(TDynamicObject const *Vehicle) const +{ + return Vehicle != nullptr ? id_of(Vehicle->name()) : ENTITY_NONE; +} + +std::string entity_registry::name_of(NetworkEntityId Id) const +{ + auto const *entry = find(Id); + return entry != nullptr ? entry->name : std::string(); +} + +vehicle_entry const *entity_registry::find(NetworkEntityId Id) const +{ + auto const lookup = m_byid.find(Id); + return lookup != m_byid.end() ? &m_entries[lookup->second] : nullptr; +} + +TDynamicObject *entity_registry::resolve(NetworkEntityId Id) const +{ + auto const *entry = find(Id); + if (entry == nullptr) + return nullptr; + + return simulation::Vehicles.find(entry->name); +} + +} // namespace network diff --git a/network/entities.h b/network/entities.h new file mode 100644 index 00000000..4b87fdc0 --- /dev/null +++ b/network/entities.h @@ -0,0 +1,98 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include + +class TDynamicObject; + +namespace network +{ + +// identifies a participant of the session. the host is a participant like any other, +// so that peer/crew/permission handling does not need a special case for it +using PeerId = uint32_t; +constexpr PeerId PEER_NONE = 0; +constexpr PeerId PEER_HOST = 1; +// remote peers are numbered from here up +constexpr PeerId PEER_FIRST_REMOTE = 2; + +// identifies a vehicle for the whole session. unlike TTrain::id() this does not depend +// on the order in which a particular peer happened to create its trains +using NetworkEntityId = uint32_t; +constexpr NetworkEntityId ENTITY_NONE = 0; + +// how many people may share one vehicle. the crew itself is a plain container, +// so raising this number does not call for a structural change +constexpr uint8_t CREW_CAPACITY = 2; + +// authoritative description of a single drivable vehicle, as published by the server +struct vehicle_entry +{ + NetworkEntityId id{ENTITY_NONE}; + std::string name; + uint8_t crew_count{0}; + uint8_t crew_capacity{CREW_CAPACITY}; + bool ai_active{false}; + bool claimable{false}; + + bool operator==(vehicle_entry const &Other) const + { + return id == Other.id && name == Other.name && crew_count == Other.crew_count && crew_capacity == Other.crew_capacity && ai_active == Other.ai_active && + claimable == Other.claimable; + } + bool operator!=(vehicle_entry const &Other) const { return !(*this == Other); } +}; + +// maps stable network ids onto the vehicles of the loaded scenario. +// the server owns the numbering and publishes it; a client binds the received ids to +// its own copy of the scenario by vehicle name, which keeps the runtime protocol on ids +class entity_registry +{ +public: + // server side: builds the numbering from the loaded scenario. vehicles are ordered by + // name so that the result does not depend on the order the scenario happened to spawn them + void build(); + // client side: takes over the numbering published by the server + void adopt(std::vector const &Entries); + // server side: refreshes the volatile part of the published entries + void refresh(); + void clear(); + + bool empty() const { return m_entries.empty(); } + std::vector const &entries() const { return m_entries; } + + NetworkEntityId id_of(std::string const &Name) const; + NetworkEntityId id_of(TDynamicObject const *Vehicle) const; + std::string name_of(NetworkEntityId Id) const; + TDynamicObject *resolve(NetworkEntityId Id) const; + vehicle_entry const *find(NetworkEntityId Id) const; + +private: + void reindex(); + + std::vector m_entries; + std::unordered_map m_byname; + std::unordered_map m_byid; +}; + +// session wide vehicle registry. stays empty outside of multiplayer +extern entity_registry Entities; + +// hands out ids for connecting peers; PEER_HOST is reserved for the local participant +PeerId allocate_peer_id(); + +// true for vehicles a player can take over +bool is_drivable(TDynamicObject const *Vehicle); + +} // namespace network diff --git a/network/manager.cpp b/network/manager.cpp index 37d72401..37900d14 100644 --- a/network/manager.cpp +++ b/network/manager.cpp @@ -59,11 +59,37 @@ network::manager::manager() { } +void network::server_manager::publish_vehicle_list() +{ + if (Entities.empty()) + return; + + Entities.refresh(); + + // resend on every change, and once in a while regardless, so that a peer which has + // just finished catching up does not have to wait for somebody to move + bool const changed = (Entities.entries() != last_published_list); + if (!changed && --publish_countdown > 0) + return; + + last_published_list = Entities.entries(); + publish_countdown = PUBLISH_INTERVAL_FRAMES; + + vehicle_list msg; + msg.vehicles = last_published_list; + + for (auto srv : servers) + srv->push_message(msg); +} + void network::manager::update() { for (auto &backend : backend_list()) backend.second->update(); + if (servers && simulation::is_ready) + servers->publish_vehicle_list(); + if (client) client->update(); } diff --git a/network/manager.h b/network/manager.h index 572a8c6b..e5aa01ea 100644 --- a/network/manager.h +++ b/network/manager.h @@ -17,6 +17,15 @@ namespace network void push_delta(double render_dt, double dt, double sync, const command_queue::commands_map &commands); command_queue::commands_map pop_commands(); void create_server(const std::string &backend, const std::string &conf); + // refreshes the authoritative vehicle roster and publishes it when it changed + void publish_vehicle_list(); + + private: + // how many frames may pass between two unconditional broadcasts of the roster + static const int PUBLISH_INTERVAL_FRAMES = 120; + + std::vector last_published_list; + int publish_countdown = 0; }; class manager diff --git a/network/message.cpp b/network/message.cpp index aa93b046..801c8423 100644 --- a/network/message.cpp +++ b/network/message.cpp @@ -33,6 +33,7 @@ void network::server_hello::serialize(std::ostream &stream) const sn_utils::ls_int64(stream, config); sn_utils::s_str(stream, scenario); sn_utils::s_str(stream, app_version); + sn_utils::ls_uint32(stream, peer_id); } void network::server_hello::deserialize(std::istream &stream) @@ -42,6 +43,41 @@ void network::server_hello::deserialize(std::istream &stream) config = sn_utils::ld_int64(stream); scenario = sn_utils::d_str(stream); app_version = sn_utils::d_str(stream); + peer_id = sn_utils::ld_uint32(stream); +} + +void network::vehicle_list::serialize(std::ostream &stream) const +{ + sn_utils::ls_uint32(stream, (uint32_t)vehicles.size()); + for (auto const &entry : vehicles) + { + sn_utils::ls_uint32(stream, entry.id); + sn_utils::s_str(stream, entry.name); + sn_utils::s_uint8(stream, entry.crew_count); + sn_utils::s_uint8(stream, entry.crew_capacity); + sn_utils::s_uint8(stream, (uint8_t)((entry.ai_active ? 1 : 0) | (entry.claimable ? 2 : 0))); + } +} + +void network::vehicle_list::deserialize(std::istream &stream) +{ + vehicles.clear(); + + uint32_t const count = sn_utils::ld_uint32(stream); + vehicles.reserve(count); + + for (uint32_t i = 0; i < count; i++) + { + vehicle_entry entry; + entry.id = sn_utils::ld_uint32(stream); + entry.name = sn_utils::d_str(stream); + entry.crew_count = sn_utils::d_uint8(stream); + entry.crew_capacity = sn_utils::d_uint8(stream); + uint8_t const flags = sn_utils::d_uint8(stream); + entry.ai_active = (flags & 1) != 0; + entry.claimable = (flags & 2) != 0; + vehicles.emplace_back(entry); + } } void ::network::request_command::serialize(std::ostream &stream) const @@ -131,6 +167,8 @@ std::shared_ptr network::deserialize_message(std::istream &str msg = std::make_shared(); else if (type == message::SERVER_REJECT) msg = std::make_shared(); + else if (type == message::VEHICLE_LIST) + msg = std::make_shared(); if (!msg) { // unknown message type; hand back a marker the peer handlers treat as a protocol error diff --git a/network/message.h b/network/message.h index ff198ded..42d62ebb 100644 --- a/network/message.h +++ b/network/message.h @@ -1,5 +1,6 @@ #pragma once #include "network/message.h" +#include "network/entities.h" #include "input/command.h" #include @@ -14,6 +15,7 @@ struct message FRAME_INFO, REQUEST_COMMAND, SERVER_REJECT, + VEHICLE_LIST, TYPE_MAX }; @@ -59,6 +61,20 @@ struct server_hello : public message std::string scenario; // build identification of the server, informational only std::string app_version; + // identity assigned to the joining peer for the rest of the session + uint32_t peer_id{ PEER_NONE }; + + virtual void serialize(std::ostream &stream) const override; + virtual void deserialize(std::istream &stream) override; +}; + +// authoritative roster of the vehicles a player may take over, broadcast by the server +// whenever it changes (and periodically, so that a peer which just went active gets one) +struct vehicle_list : public message +{ + vehicle_list() : message(VEHICLE_LIST) {} + + std::vector vehicles; virtual void serialize(std::ostream &stream) const override; virtual void deserialize(std::istream &stream) override; diff --git a/network/network.cpp b/network/network.cpp index fe022b10..7764ff99 100644 --- a/network/network.cpp +++ b/network/network.cpp @@ -112,6 +112,14 @@ void network::server::push_delta(const frame_info &msg) } } +void network::server::push_message(const message &msg) +{ + for (auto const &client : clients) { + if (client->state == connection::ACTIVE) + client->send_message(msg); + } +} + command_queue::commands_map network::server::pop_commands() { command_queue::commands_map map(client_commands_queue); @@ -157,6 +165,8 @@ void network::server::handle_message(std::shared_ptr conn, const mes reply.config = 0; // TODO: pass bitfield with state of relevant setting switches reply.scenario = Global.SceneryFile; reply.app_version = Global.asVersion; + reply.peer_id = allocate_peer_id(); + conn->peer_id = reply.peer_id; conn->state = connection::CATCHING_UP; conn->backbuffer = backbuffer; conn->backbuffer_pos = 0; @@ -164,7 +174,8 @@ void network::server::handle_message(std::shared_ptr conn, const mes conn->send_message(reply); - WriteLog("net: peer connected, build \"" + cmd.app_version + "\", scenario \"" + Global.SceneryFile + "\"", logtype::net); + WriteLog("net: peer " + std::to_string(conn->peer_id) + " connected, build \"" + cmd.app_version + + "\", scenario \"" + Global.SceneryFile + "\"", logtype::net); } else if (msg.type == message::REQUEST_COMMAND) { const auto& cmd = dynamic_cast(msg); @@ -293,6 +304,11 @@ void network::client::handle_message(std::shared_ptr conn, const mes return; } + if (cmd.peer_id != PEER_NONE) { + Global.network_peer_id = cmd.peer_id; + WriteLog("net: assigned peer id " + std::to_string(cmd.peer_id), logtype::net); + } + Global.network_status.clear(); WriteLog("net: accept received", logtype::net); @@ -301,6 +317,11 @@ void network::client::handle_message(std::shared_ptr conn, const mes if (conn->state != connection::ACTIVE) return; + if (msg.type == message::VEHICLE_LIST) { + const auto& cmd = dynamic_cast(msg); + Entities.adopt(cmd.vehicles); + } + if (msg.type == message::FRAME_INFO) { resume_frame_counter++; diff --git a/network/network.h b/network/network.h index 1ce802be..a737d926 100644 --- a/network/network.h +++ b/network/network.h @@ -5,6 +5,7 @@ #include #include #include "network/message.h" +#include "network/entities.h" #include "input/command.h" namespace network @@ -47,6 +48,9 @@ namespace network DEAD }; peer_state state; + + // session identity of the peer on the other side of this connection + PeerId peer_id { PEER_NONE }; }; class server @@ -64,6 +68,9 @@ namespace network public: server(std::shared_ptr buf); void push_delta(const frame_info &msg); + // sends an out of band message to every active peer. unlike push_delta this is not + // written to the backbuffer, so it does not become part of the replayed history + void push_message(const message &msg); command_queue::commands_map pop_commands(); }; diff --git a/utilities/Globals.h b/utilities/Globals.h index c0745126..03e34290 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -354,6 +354,10 @@ struct global_settings { std::string network_status; // set when the server refused us; keeps the reason for the ui and stops reconnect attempts std::string network_reject_reason; + // identity of this participant within the session; the host keeps network::PEER_HOST + uint32_t network_peer_id = 0; + // vehicle the local player asked to enter from the multiplayer lobby + std::string network_pending_vehicle; std::unordered_map trainset_overrides; diff --git a/widgets/multiplayer_lobby.cpp b/widgets/multiplayer_lobby.cpp new file mode 100644 index 00000000..9c6420fe --- /dev/null +++ b/widgets/multiplayer_lobby.cpp @@ -0,0 +1,119 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "widgets/multiplayer_lobby.h" + +#include "application/application.h" +#include "network/entities.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" +#include "utilities/translation.h" +#include "vehicle/Driver.h" +#include "vehicle/DynObj.h" +#include "vehicle/Train.h" + +ui::multiplayer_lobby_panel::multiplayer_lobby_panel() + : ui_panel(STR_C("Multiplayer lobby"), false) +{ + size_min = {480, 260}; +} + +void ui::multiplayer_lobby_panel::enter_vehicle(std::string const &Name) +{ + TDynamicObject *vehicle = simulation::Vehicles.find(Name); + if (vehicle == nullptr) + return; + + std::string payload{Name}; + // the request travels as an ordinary simulation command, so every peer builds the cab + m_relay.post(user_command::entervehicle, 0.0, simulation::Train ? simulation::Train->id() : 0, GLFW_PRESS, 0, vehicle->GetPosition(), &payload); + // ...and the driver mode moves our own camera in once the cab shows up locally + Global.network_pending_vehicle = Name; + is_open = false; +} + +void ui::multiplayer_lobby_panel::render_contents() +{ + bool const host{Application.is_server()}; + bool const client{Application.is_client()}; + + if (!host && !client) + { + ImGui::TextUnformatted(STR_C("This is not a multiplayer session.")); + return; + } + + ImGui::Text("%s: %s, peer %u", STR_C("Session"), host ? (client ? "host + client" : "host") : "client", Global.network_peer_id); + ImGui::TextUnformatted(Global.SceneryFile.c_str()); + ImGui::Separator(); + + auto const &entries = network::Entities.entries(); + if (entries.empty()) + { + ImGui::TextUnformatted(STR_C("Waiting for the vehicle list from the server...")); + return; + } + + std::string const currentvehicle{simulation::Train != nullptr && simulation::Train->Dynamic() != nullptr ? simulation::Train->Dynamic()->name() : std::string()}; + + if (!FreeFlyModeFlag) + { + ImGui::TextUnformatted(STR_C("Leave the cab to change vehicles.")); + ImGui::Separator(); + } + + ImGui::Columns(4, "mp_vehicles", false); + ImGui::SetColumnWidth(1, 80.0f * Global.ui_scale); + ImGui::SetColumnWidth(2, 90.0f * Global.ui_scale); + ImGui::SetColumnWidth(3, 130.0f * Global.ui_scale); + + ImGui::TextDisabled("%s", STR_C("Vehicle")); + ImGui::NextColumn(); + ImGui::TextDisabled("%s", STR_C("Crew")); + ImGui::NextColumn(); + ImGui::TextDisabled("%s", STR_C("Driver")); + ImGui::NextColumn(); + ImGui::NextColumn(); + ImGui::Separator(); + + for (auto const &entry : entries) + { + ImGui::PushID(static_cast(entry.id)); + + ImGui::TextUnformatted(entry.name.c_str()); + ImGui::NextColumn(); + + ImGui::Text("%u/%u", (unsigned)entry.crew_count, (unsigned)entry.crew_capacity); + ImGui::NextColumn(); + + ImGui::TextUnformatted(entry.crew_count > 0 ? STR_C("player") : entry.ai_active ? STR_C("AI") : STR_C("free")); + ImGui::NextColumn(); + + if (entry.name == currentvehicle) + { + ImGui::TextUnformatted(STR_C("you")); + } + else if (entry.claimable && FreeFlyModeFlag && network::Entities.resolve(entry.id) != nullptr) + { + if (ImGui::Button(STR_C("Enter"), ImVec2(-1, 0))) + enter_vehicle(entry.name); + } + else + { + ImGui::TextDisabled("%s", entry.claimable ? "-" : STR_C("occupied")); + } + ImGui::NextColumn(); + + ImGui::PopID(); + } + + ImGui::Columns(1); +} diff --git a/widgets/multiplayer_lobby.h b/widgets/multiplayer_lobby.h new file mode 100644 index 00000000..a57e58e5 --- /dev/null +++ b/widgets/multiplayer_lobby.h @@ -0,0 +1,35 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "application/uilayer.h" +#include "input/command.h" + +namespace ui +{ + +// lobby of a multiplayer session: shows the vehicle roster published by the server and +// lets the player pick the one to work on. it lives inside the driver mode rather than in +// a mode of its own on purpose - the world has to keep running while somebody is choosing, +// and the very same screen is what a player uses to change vehicles later on +class multiplayer_lobby_panel : public ui_panel +{ +public: + multiplayer_lobby_panel(); + + void render_contents() override; + +private: + void enter_vehicle(std::string const &Name); + + command_relay m_relay; +}; + +} // namespace ui