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:
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
148
network/entities.cpp
Normal 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
98
network/entities.h
Normal 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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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++;
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
119
widgets/multiplayer_lobby.cpp
Normal file
119
widgets/multiplayer_lobby.cpp
Normal 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);
|
||||
}
|
||||
35
widgets/multiplayer_lobby.h
Normal file
35
widgets/multiplayer_lobby.h
Normal 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
|
||||
Reference in New Issue
Block a user