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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user