mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-29 15:07:29 +01: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:
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();
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user