17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 12:09:19 +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

@@ -1105,6 +1105,49 @@ void eu07_application::init_files()
}
namespace fs = std::filesystem;
namespace {
// port used when the user did not spell one out in --host / --connect
uint32_t const EU07_DEFAULT_NETWORK_PORT = 7420;
// completes a user supplied endpoint into the "address:port" form expected by the tcp backend.
// note: only the plain ipv4/hostname form is split, anything with more colons is passed through
std::string network_endpoint(std::string const &Argument, std::string const &Defaultaddress)
{
auto address{Argument};
std::string port;
if (std::count(address.begin(), address.end(), ':') == 1)
{
auto const separator{address.find(':')};
port = address.substr(separator + 1);
address = address.substr(0, separator);
}
if (address.empty())
address = Defaultaddress;
if (port.empty())
port = std::to_string(EU07_DEFAULT_NETWORK_PORT);
return address + ":" + port;
}
void print_usage(std::string const &Executable)
{
std::cout
<< "usage: " << Executable << " [options]\n"
<< " -s, --scenario <path> scenario file to load\n"
<< " -v, --vehicle <name> vehicle to start in\n"
<< " --host [address:port] host a multiplayer session (default 0.0.0.0:"
<< EU07_DEFAULT_NETWORK_PORT << ")\n"
<< " --connect <address:port> join a multiplayer session (default port "
<< EU07_DEFAULT_NETWORK_PORT << ")\n"
<< " -h, --help this message"
<< std::endl;
}
} // namespace
int eu07_application::init_settings(int Argc, char *Argv[])
{
Global.asVersion = VERSION_INFO;
@@ -1126,24 +1169,48 @@ int eu07_application::init_settings(int Argc, char *Argv[])
std::string token{Argv[i]};
if (token == "-s")
if (token == "-s" || token == "--scenario")
{
if (i + 1 < Argc)
{
Global.SceneryFile = ToLower(Argv[++i]);
}
}
else if (token == "-v")
else if (token == "-v" || token == "--vehicle")
{
if (i + 1 < Argc)
{
Global.local_start_vehicle = ToLower(Argv[++i]);
Global.local_start_vehicle_override = true;
}
}
else if (token == "--host")
{
// the address is optional, so that a bare --host just listens on every interface
std::string endpoint{std::string("0.0.0.0:") + std::to_string(EU07_DEFAULT_NETWORK_PORT)};
if (i + 1 < Argc && Argv[i + 1][0] != '-')
{
endpoint = network_endpoint(Argv[++i], "0.0.0.0");
}
Global.network_servers.emplace_back("tcp", endpoint);
}
else if (token == "--connect")
{
if (i + 1 >= Argc)
{
std::cout << "--connect requires a server address" << std::endl;
return -1;
}
Global.network_client.emplace("tcp", network_endpoint(Argv[++i], "127.0.0.1"));
}
else if (token == "-h" || token == "--help")
{
print_usage(Argv[0]);
return -1;
}
else
{
std::cout << "usage: " << std::string(Argv[0]) << " [-s sceneryfilepath]"
<< " [-v vehiclename]" << std::endl;
print_usage(Argv[0]);
return -1;
}
}
@@ -1423,17 +1490,23 @@ int eu07_application::init_modes()
{
Global.local_random_engine.seed(std::random_device{}());
if ((!Global.network_servers.empty() || Global.network_client) && Global.SceneryFile.empty())
{
ErrorLog("launcher mode is currently not supported in network mode");
return -1;
}
// activate the default mode
if (Global.SceneryFile.empty())
push_mode(mode::launcher);
else
if (Global.network_client)
{
// a multiplayer client learns the scenario name from the server handshake,
// so it goes straight to the loader and waits there for Global.ready_to_load
push_mode(mode::scenarioloader);
}
else if (Global.SceneryFile.empty())
{
// no scenario given: let the user pick one (a listening server simply
// refuses joins until the scenario is up)
push_mode(mode::launcher);
}
else
{
push_mode(mode::scenarioloader);
}
return 0;
}
@@ -1449,11 +1522,22 @@ bool eu07_application::init_network()
for (auto const &pair : Global.network_servers)
{
// create all servers
WriteLog("net: hosting session on " + pair.second + " (" + pair.first + ")", logtype::net);
m_network->create_server(pair.first, pair.second);
}
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);
// create client
m_network->connect(Global.network_client->first, Global.network_client->second);
}

View File

@@ -32,9 +32,14 @@ bool scenarioloader_mode::init() {
// mode-specific update of simulation data. returns: false on error, true otherwise
bool scenarioloader_mode::update() {
if (!Global.ready_to_load)
// waiting for network connection
if (!Global.ready_to_load) {
// waiting for the network handshake to tell us which scenario to load
m_userinterface->set_progress(
Global.network_status.empty()
? STR("Connecting to server")
: Global.network_status);
return true;
}
if (!state) {
WriteLog("using simulation seed: " + std::to_string(Global.random_seed), logtype::generic);
@@ -76,8 +81,15 @@ void scenarioloader_mode::enter() {
simulation::is_ready = false;
Application.set_title( Global.AppName + " (" + Global.SceneryFile + ")" );
m_userinterface->set_progress(STR("Loading scenery"));
if( Global.SceneryFile.empty() ) {
// multiplayer client: the scenario is not known until the server answers
Application.set_title( Global.AppName );
m_userinterface->set_progress( STR( "Connecting to server" ) );
}
else {
Application.set_title( Global.AppName + " (" + Global.SceneryFile + ")" );
m_userinterface->set_progress(STR("Loading scenery"));
}
}
// maintenance method, called when the mode is deactivated

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

View File

@@ -81,6 +81,9 @@ struct global_settings {
std::string szDefaultExt{ szTexturesDDS };
std::string SceneryFile;
std::string local_start_vehicle{ "EU07-424" };
// set when the starting vehicle was explicitly requested (command line -v),
// so multiplayer clients can tell an intentional choice from the built-in default
bool local_start_vehicle_override{ false };
int iConvertModels{ 0 }; // tworzenie plików binarnych
int iConvertIndexRange{ 1000 }; // range of duplicate vertex scan
bool file_binary_terrain{ true }; // enable binary terrain (de)serialization
@@ -347,6 +350,10 @@ struct global_settings {
std::vector<std::pair<std::string, std::string>> network_servers;
std::optional<std::pair<std::string, std::string>> network_client;
float desync = 0.0f;
// human readable state of the network session, presented on the connecting/loading screen
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;
std::unordered_map<int, std::string> trainset_overrides;