17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-18 06:49:18 +02:00

multiplayer stage 1: --host/--connect startup, client without a local scenario

Networking used to be reachable only through the eu07.ini network.server /
network.client entries, and init_modes() refused to start at all unless a
scenery file was already known. That made it impossible to join a session
without knowing (and typing) the host's scenario up front, even though
SERVER_HELLO already carries the scenario name.

- add --host [address:port] and --connect address:port to the command line
  (default port 7420, --host defaults to 0.0.0.0), plus --scenario/--vehicle
  aliases and -h/--help; the existing ini settings keep working
- drop the init_modes() restriction: a client goes straight to the scenario
  loader and waits there for the handshake, a host without -s still gets the
  launcher and simply refuses joins until a scenario is up
- the loader screen reports the network state instead of sitting on a blank
  progress bar
- a client that did not ask for a vehicle with -v starts as an observer
  rather than failing to find the default EU07-424; the lobby of stage 2 will
  take over the choice
- handshake carries build identification and the server answers an
  impossible join with SERVER_REJECT and a readable reason instead of
  dropping the socket silently; protocol version bumped to 3
- deserialize_message() no longer dereferences a null message on an unknown
  message type
This commit is contained in:
2026-09-06 17:28:44 +00:00
parent eed44dc858
commit 8460fc531f
6 changed files with 200 additions and 21 deletions

View File

@@ -6,12 +6,24 @@ void network::client_hello::serialize(std::ostream &stream) const
{
sn_utils::ls_int32(stream, version);
sn_utils::ls_uint32(stream, start_packet);
sn_utils::s_str(stream, app_version);
}
void network::client_hello::deserialize(std::istream &stream)
{
version = sn_utils::ld_int32(stream);
start_packet = sn_utils::ld_uint32(stream);
app_version = sn_utils::d_str(stream);
}
void network::server_reject::serialize(std::ostream &stream) const
{
sn_utils::s_str(stream, reason);
}
void network::server_reject::deserialize(std::istream &stream)
{
reason = sn_utils::d_str(stream);
}
void network::server_hello::serialize(std::ostream &stream) const
@@ -20,6 +32,7 @@ void network::server_hello::serialize(std::ostream &stream) const
sn_utils::ls_int64(stream, timestamp);
sn_utils::ls_int64(stream, config);
sn_utils::s_str(stream, scenario);
sn_utils::s_str(stream, app_version);
}
void network::server_hello::deserialize(std::istream &stream)
@@ -28,6 +41,7 @@ void network::server_hello::deserialize(std::istream &stream)
timestamp = sn_utils::ld_int64(stream);
config = sn_utils::ld_int64(stream);
scenario = sn_utils::d_str(stream);
app_version = sn_utils::d_str(stream);
}
void ::network::request_command::serialize(std::ostream &stream) const
@@ -115,6 +129,13 @@ std::shared_ptr<network::message> network::deserialize_message(std::istream &str
msg = std::make_shared<frame_info>();
else if (type == message::REQUEST_COMMAND)
msg = std::make_shared<request_command>();
else if (type == message::SERVER_REJECT)
msg = std::make_shared<server_reject>();
if (!msg) {
// unknown message type; hand back a marker the peer handlers treat as a protocol error
return std::make_shared<message>(message::TYPE_MAX);
}
msg->deserialize(stream);

View File

@@ -13,6 +13,7 @@ struct message
SERVER_HELLO,
FRAME_INFO,
REQUEST_COMMAND,
SERVER_REJECT,
TYPE_MAX
};
@@ -32,6 +33,20 @@ struct client_hello : public message
int32_t version;
uint32_t start_packet;
// build identification of the connecting simulator, informational only
std::string app_version;
};
// sent by the server instead of SERVER_HELLO when the client cannot join;
// carries a human readable reason so the player learns what went wrong
struct server_reject : public message
{
server_reject() : message(SERVER_REJECT) {}
std::string reason;
virtual void serialize(std::ostream &stream) const override;
virtual void deserialize(std::istream &stream) override;
};
struct server_hello : public message
@@ -42,6 +57,8 @@ struct server_hello : public message
int64_t timestamp;
int64_t config;
std::string scenario;
// build identification of the server, informational only
std::string app_version;
virtual void serialize(std::ostream &stream) const override;
virtual void deserialize(std::istream &stream) override;

View File

@@ -7,7 +7,9 @@
#include "application/application.h"
#include "utilities/Globals.h"
std::uint32_t const EU07_NETWORK_VERSION = 2;
// 2 - legacy lockstep protocol
// 3 - handshake carries build identification and an explicit rejection message
std::uint32_t const EU07_NETWORK_VERSION = 3;
namespace network {
@@ -44,6 +46,7 @@ void network::connection::connected()
client_hello msg;
msg.version = EU07_NETWORK_VERSION;
msg.start_packet = packet_counter;
msg.app_version = Global.asVersion;
send_message(msg);
}
}
@@ -127,8 +130,23 @@ void network::server::handle_message(std::shared_ptr<connection> conn, const mes
if (msg.type == message::CLIENT_HELLO) {
const auto& cmd = dynamic_cast<const client_hello&>(msg);
if (cmd.version != EU07_NETWORK_VERSION // wrong version
|| !Global.ready_to_load) { // not ready yet
std::string rejection;
if (cmd.version != (int32_t)EU07_NETWORK_VERSION) {
rejection = "incompatible protocol version: server speaks "
+ std::to_string(EU07_NETWORK_VERSION)
+ ", client speaks " + std::to_string(cmd.version);
}
else if (!Global.ready_to_load || Global.SceneryFile.empty()) {
// the host has not picked a scenario yet, there is nothing to join
rejection = "the server has no scenario running yet";
}
if (!rejection.empty()) {
WriteLog("net: rejecting peer: " + rejection, logtype::net);
server_reject reply;
reply.reason = rejection;
conn->send_message(reply);
conn->disconnect();
return;
}
@@ -138,6 +156,7 @@ void network::server::handle_message(std::shared_ptr<connection> conn, const mes
reply.timestamp = Global.starting_timestamp;
reply.config = 0; // TODO: pass bitfield with state of relevant setting switches
reply.scenario = Global.SceneryFile;
reply.app_version = Global.asVersion;
conn->state = connection::CATCHING_UP;
conn->backbuffer = backbuffer;
conn->backbuffer_pos = 0;
@@ -145,7 +164,7 @@ void network::server::handle_message(std::shared_ptr<connection> conn, const mes
conn->send_message(reply);
WriteLog("net: client accepted", logtype::net);
WriteLog("net: peer 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);
@@ -163,6 +182,11 @@ void network::client::update()
conn.reset();
}
if (!Global.network_reject_reason.empty()) {
// the server explicitly refused us; retrying would only spam it
return;
}
if (!conn) {
if (!reconnect_delay) {
connect();
@@ -240,6 +264,16 @@ void network::client::handle_message(std::shared_ptr<connection> conn, const mes
return;
}
if (msg.type == message::SERVER_REJECT) {
const auto& cmd = dynamic_cast<const server_reject&>(msg);
ErrorLog("net: connection refused by the server: " + cmd.reason, logtype::net);
Global.network_reject_reason = cmd.reason;
Global.network_status = "Connection refused: " + cmd.reason;
conn->disconnect();
return;
}
if (msg.type == message::SERVER_HELLO) {
const auto& cmd = dynamic_cast<const server_hello&>(msg);
conn->state = connection::ACTIVE;
@@ -251,12 +285,16 @@ void network::client::handle_message(std::shared_ptr<connection> conn, const mes
// TODO: configure simulation settings according to received cmd.config
Global.SceneryFile = cmd.scenario;
Global.ready_to_load = true;
WriteLog("net: scenario handshake: \"" + cmd.scenario + "\", server build \"" + cmd.app_version + "\"", logtype::net);
} else if (Global.random_seed != cmd.seed) {
ErrorLog("net: seed mismatch", logtype::net);
conn->disconnect();
return;
}
Global.network_status.clear();
WriteLog("net: accept received", logtype::net);
}