17
0
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:
2026-09-06 17:48:35 +00:00
parent 8460fc531f
commit b9bd0ee33d
17 changed files with 573 additions and 7 deletions

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