17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 13:19:20 +02:00

multiplayer stage 2: peer identity, stable vehicle ids and a session lobby

TTrain::id() numbers cabs with a local counter, in the order each peer happens
to create them, so it cannot address a vehicle across the session. Neither was
there any way for a player to see what is available to drive.

- network/entities: PeerId (the host is peer 1, remote peers are numbered from
  2, so it is a participant rather than a special case) and NetworkEntityId,
  plus a registry that numbers the drivable vehicles of the loaded scenario in
  name order and resolves ids back to TDynamicObject
- the server owns the numbering and publishes it as VEHICLE_LIST - name, crew
  occupancy, crew capacity, AI state, availability - on every change and at
  least every 120 frames, out of band so it stays out of the replay backbuffer;
  a client binds the received ids to its own copy of the scenario by name
- SERVER_HELLO hands the joining peer its PeerId
- a multiplayer lobby listing that roster, with vehicles taken by other players
  shown as such rather than hidden; entering one goes through the existing
  entervehicle command, the vehicle is never moved

The lobby lives inside the driver mode instead of a mode of its own: the world
has to keep running while somebody is choosing (a client stops consuming the
server's frames the moment its mode is not a command processor), and the same
screen is what a player needs to change vehicles later on.

Crew occupancy is still derived from the presence of a cab instance; the real
crew model, claim handshake and AI takeover follow in stage 3.
This commit is contained in:
2026-09-06 17:48:35 +00:00
parent 8460fc531f
commit b9bd0ee33d
17 changed files with 573 additions and 7 deletions

View File

@@ -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"

View File

@@ -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);

View File

@@ -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<driver_ui>(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");

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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<scenarioloader_ui>();
@@ -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::seconds>( std::chrono::system_clock::now() - timestart ).count() ) + " seconds" );
// TODO: implement and use next mode cue

148
network/entities.cpp Normal file
View File

@@ -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<std::string> 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<NetworkEntityId>(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<vehicle_entry> 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

98
network/entities.h Normal file
View File

@@ -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 <cstdint>
#include <string>
#include <vector>
#include <unordered_map>
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<vehicle_entry> 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<vehicle_entry> 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<vehicle_entry> m_entries;
std::unordered_map<std::string, NetworkEntityId> m_byname;
std::unordered_map<NetworkEntityId, size_t> 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

View File

@@ -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();
}

View File

@@ -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<vehicle_entry> last_published_list;
int publish_countdown = 0;
};
class manager

View File

@@ -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::message> network::deserialize_message(std::istream &str
msg = std::make_shared<request_command>();
else if (type == message::SERVER_REJECT)
msg = std::make_shared<server_reject>();
else if (type == message::VEHICLE_LIST)
msg = std::make_shared<vehicle_list>();
if (!msg) {
// unknown message type; hand back a marker the peer handlers treat as a protocol error

View File

@@ -1,5 +1,6 @@
#pragma once
#include "network/message.h"
#include "network/entities.h"
#include "input/command.h"
#include <queue>
@@ -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<vehicle_entry> vehicles;
virtual void serialize(std::ostream &stream) const override;
virtual void deserialize(std::istream &stream) override;

View File

@@ -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<connection> 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<connection> 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<const request_command&>(msg);
@@ -293,6 +304,11 @@ void network::client::handle_message(std::shared_ptr<connection> 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<connection> conn, const mes
if (conn->state != connection::ACTIVE)
return;
if (msg.type == message::VEHICLE_LIST) {
const auto& cmd = dynamic_cast<const vehicle_list&>(msg);
Entities.adopt(cmd.vehicles);
}
if (msg.type == message::FRAME_INFO) {
resume_frame_counter++;

View File

@@ -5,6 +5,7 @@
#include <queue>
#include <chrono>
#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<std::istream> 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();
};

View File

@@ -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<int, std::string> trainset_overrides;

View File

@@ -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<int>(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);
}

View File

@@ -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