Multiplayer fixes: a late network message from one race can no longer stop a player's car responding in the next, a host's 25-character password can be typed by joiners, and changing the shared traffic settings clears the joiners' ready.
diff --git a/src/app/frontend/PagesMulti.cpp b/src/app/frontend/PagesMulti.cpp
index aa5545e..a31ff7a 100644
--- a/src/app/frontend/PagesMulti.cpp
+++ b/src/app/frontend/PagesMulti.cpp
@@ -190,30 +190,24 @@ class SessionsPage final : public Page {
}
// NetSelectMenu::NetSelectMenu: the NET NAME field takes 12 characters.
auto& name = menu.add(Box{kBoxX, 66, kBoxWide, kBoxH}, &m_netName, 12);
- // NetSelectMenu::NetNameCB -> mmInterface::NetNameCB: the driver's net
- // name.
- name.onCommit = [this, &fe] {
- const auto trimmed = std::string(str::trim(m_netName));
- m_netName = trimmed.empty() ? netName(fe) : trimmed;
- if (fe.profile) {
- fe.profile->netName = m_netName;
- fe.saveProfile();
- }
- // The name is fixed per NetGame: start a fresh one with it.
- if (fe.ctx.netGame && !fe.ctx.netGame->inSession()) {
- fe.ctx.netGame.reset();
- ensureNetGame(fe).startLanScan();
- }
- };
+ // The field edits the menu's own name buffer (NetSelectMenu +0x8c),
+ // which hosting and joining read; NetNameCB only logs it. So the name
+ // in the field counts whether or not Enter was pressed: it is applied
+ // on Enter, before HOST and JOIN, and on leaving the page.
+ name.onCommit = [this, &fe] { applyNetName(fe); };
// HOST (NetSelectMenu::HostCB): the host options dialog.
auto& host = menu.add(SpriteSheet{"texture/sess_hst.tga", 4}, kColumnX, 98,
- [&fe] { fe.push(makeHostOptionsDialog(fe)); });
+ [this, &fe] {
+ applyNetName(fe);
+ fe.push(makeHostOptionsDialog(fe));
+ });
// NetSelectMenu::FocusDescription has pictures for the provider lamps
// only.
menu.add(SpriteSheet{"texture/sess_jn.tga", 4}, kColumnX, 167, [this, &fe] {
// JOIN (NetSelectMenu::JoinCB): a session picked in the list joins
// directly; otherwise ask for an address ("leave blank to search
// for available sessions").
+ applyNetName(fe);
const auto list = fe.ctx.netGame ? fe.ctx.netGame->lanSessions() : std::vector{};
if (m_selected >= 0 && m_selected < static_cast(list.size()))
joinListed(fe, list[static_cast(m_selected)]);
@@ -229,13 +223,20 @@ class SessionsPage final : public Page {
// A double click joins the session (NetSelectMenu::JoinCallback joins
// on the pick).
m_list->onDoubleClick = [this, &fe] {
+ applyNetName(fe);
const auto list = fe.ctx.netGame ? fe.ctx.netGame->lanSessions() : std::vector{};
if (m_selected >= 0 && m_selected < static_cast(list.size()))
joinListed(fe, list[static_cast(m_selected)]);
};
auto& back = addBack(fe, *this);
- back.onClick = [&fe] { leave(fe); };
- menu.onBack = [&fe] { leave(fe); };
+ back.onClick = [this, &fe] {
+ applyNetName(fe);
+ leave(fe);
+ };
+ menu.onBack = [this, &fe] {
+ applyNetName(fe);
+ leave(fe);
+ };
addNavStrip(fe, *this);
menu.focus(&host);
}
@@ -272,6 +273,21 @@ class SessionsPage final : public Page {
fe.pop();
}
+ // The name in the field as the driver's net name (empty: the profile's),
+ // saved with the profile when it changed and given to the next host or
+ // join (no new NetGame: tearing down its port forwarding stalled the
+ // menus).
+ void applyNetName(Frontend& fe) {
+ const auto trimmed = std::string(str::trim(m_netName));
+ m_netName = trimmed.empty() ? netName(fe) : trimmed;
+ if (fe.profile && m_netName != netName(fe)) {
+ fe.profile->netName = m_netName;
+ fe.saveProfile();
+ }
+ if (fe.ctx.netGame && !fe.ctx.netGame->inSession())
+ fe.ctx.netGame->setPlayerName(m_netName);
+ }
+
std::vector rows(Frontend& fe) const {
std::vector out;
if (!fe.ctx.netGame)
diff --git a/src/game/net/NetGame.h b/src/game/net/NetGame.h
index 770bc39..089d53d 100644
--- a/src/game/net/NetGame.h
+++ b/src/game/net/NetGame.h
@@ -167,6 +167,9 @@ class NetGame {
std::string* error = nullptr);
// Leaves the session (as host: ends it for everyone). Stops port forwarding.
void leave();
+ // The name the next host() or join() uses (a session in progress keeps
+ // the one it started with).
+ void setPlayerName(std::string name) { m_options.playerName = std::move(name); }
// Services the network. Call every frame while a NetGame exists.
void update();
diff --git a/tests/NetTestPorts.h b/tests/NetTestPorts.h
new file mode 100644
index 0000000..bdd99f0
--- /dev/null
+++ b/tests/NetTestPorts.h
@@ -0,0 +1,31 @@
+#pragma once
+
+// The slot within a network test file's port block (tests/TestData.h lists
+// the blocks) for this test process. ctest runs every test as a process of
+// its own, several at a time; a slot from the clock let two of them pick the
+// same port now and then (a bind failure in NetGameLobby on CI). Process ids
+// of processes running at the same time differ, and on Linux they are handed
+// out in order, so a slot from the id is distinct for them. Neighbouring
+// processes get neighbouring slots: a block's slot spacing must cover every
+// port one process uses.
+
+#include
+
+#ifdef _WIN32
+#include
+#else
+#include
+#endif
+
+namespace mm2::test {
+
+inline std::uint32_t processPortSlot(std::uint32_t slots) {
+#ifdef _WIN32
+ const auto id = static_cast(_getpid()) / 4; // Windows process ids are multiples of 4
+#else
+ const auto id = static_cast(getpid());
+#endif
+ return id % slots;
+}
+
+} // namespace mm2::test
diff --git a/tests/TestData.h b/tests/TestData.h
index a0cf091..e553565 100644
--- a/tests/TestData.h
+++ b/tests/TestData.h
@@ -11,7 +11,8 @@
// 25000-28999 game/test_net_race_rules 29000-32999 game/test_damage_sync
// 33000-36999 game/test_netgame_lobby 37000-40999 game/test_netgame_sync
// 41000-44999 game/test_netgame 45000-48999 game/test_traffic_net
-// Other network tests bind port 0.
+// Other network tests bind port 0. Within a block, test::processPortSlot
+// (NetTestPorts.h) picks this process's ports.
namespace mm2::test {
diff --git a/tests/game/test_damage_sync.cpp b/tests/game/test_damage_sync.cpp
index 1b192ba..f693a9c 100644
--- a/tests/game/test_damage_sync.cpp
+++ b/tests/game/test_damage_sync.cpp
@@ -2,6 +2,7 @@
// owner's recorder, the receivers' records (timing, resets, loss, hostile
// input), the same dents on both machines, and its trip through two
// NetGames. Also the knocked traffic cars' wheels (game/net/TrafficSync).
+#include "NetTestPorts.h"
#include "TestData.h"
#include "asset/VehicleModel.h"
#include "core/StringUtil.h"
@@ -665,7 +666,7 @@ namespace {
// test_netgame_sync.cpp).
std::uint16_t damagePort() {
static const std::uint16_t port = static_cast(
- 29000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 2000 * 2);
+ 29000 + test::processPortSlot(2000) * 2);
return port;
}
diff --git a/tests/game/test_net_race_rules.cpp b/tests/game/test_net_race_rules.cpp
index fc7e141..469e460 100644
--- a/tests/game/test_net_race_rules.cpp
+++ b/tests/game/test_net_race_rules.cpp
@@ -4,6 +4,7 @@
// or corrected by the host's word (Session's net rules), the message that
// carries it (game::NetRules, net/RulesState.h), and Cops and Robbers' rules
// on the host with a client's predicted pickup.
+#include "NetTestPorts.h"
#include "TestData.h"
#include "city/CityData.h"
#include "game/Strings.h"
@@ -738,7 +739,7 @@ namespace {
// This file's block of ports (below 49152; see test_netgame.cpp).
std::uint16_t rulesPort() {
static const std::uint16_t port = static_cast(
- 25000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 1000 * 4);
+ 25000 + test::processPortSlot(1000) * 4);
return port;
}
diff --git a/tests/game/test_netgame.cpp b/tests/game/test_netgame.cpp
index 8d61ced..33edb23 100644
--- a/tests/game/test_netgame.cpp
+++ b/tests/game/test_netgame.cpp
@@ -1,3 +1,4 @@
+#include "NetTestPorts.h"
#include "game/net/NetGame.h"
#include
@@ -16,10 +17,12 @@ namespace {
// Each test file has its own block of ports, all below 49152: Windows hands
// out and reserves ports in its dynamic range (49152-65535), where a bind can
-// fail. Within a block, distinct ports per process for parallel ctest runs.
+// fail. Within a block, distinct ports per process for parallel ctest runs:
+// a process here uses its base port, +1 (LAN discovery), +2 and +4, so the
+// slots are 6 ports apart (neighbouring processes get neighbouring slots).
std::uint16_t basePort() {
static const std::uint16_t port = static_cast(
- 41000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 2000 * 2);
+ 41000 + test::processPortSlot(666) * 6);
return port;
}
@@ -223,6 +226,29 @@ TEST(NetGame, HostJoinChatReadyCountdownAndState) {
EXPECT_EQ(*notice, "The Host has quit");
}
+// The sessions page gives a renamed driver to the NetGame it already has
+// (no new one): the next host or join uses the new name.
+TEST(NetGame, ANameSetBeforeHostingOrJoiningIsUsed) {
+ NetGame host(options("Old host", 4));
+ NetGame client(options("Old client", 4));
+ host.setPlayerName("New host");
+ client.setPlayerName("New client");
+ std::string err;
+ ASSERT_TRUE(host.host(circuitConfig(), {"", "", 4, true}, {"vpbug", 1, 0}, &err)) << err;
+ ASSERT_TRUE(client.join(std::format("127.0.0.1:{}", basePort() + 4), "", {"vpcab", 0, 1}, &err)) << err;
+ ASSERT_TRUE(pump({&host, &client},
+ [&] { return client.phase() == NetGame::Phase::Lobby && host.players().size() == 2; }));
+ auto nameOf = [](const NetGame& g, std::uint8_t id) {
+ for (const auto& p : g.players())
+ if (p.id == id)
+ return p.name;
+ return std::string("?");
+ };
+ EXPECT_EQ(nameOf(host, host.localId()), "New host");
+ EXPECT_EQ(nameOf(host, client.localId()), "New client");
+ EXPECT_EQ(nameOf(client, host.localId()), "New host");
+}
+
TEST(NetGame, LanDiscoveryPasswordAndKick) {
NetGame host(options("Hosty", 2));
NetGame browser(options("Browser", 2));
diff --git a/tests/game/test_netgame_lobby.cpp b/tests/game/test_netgame_lobby.cpp
index 1831148..fe44b0d 100644
--- a/tests/game/test_netgame_lobby.cpp
+++ b/tests/game/test_netgame_lobby.cpp
@@ -1,5 +1,6 @@
// The lobby and race flow of game::NetGame between a host and a client in
// one process (docs/review/multiplayer-lobby.md).
+#include "NetTestPorts.h"
#include "game/net/NetGame.h"
#include
@@ -19,7 +20,7 @@ namespace {
// fail. Within a block, distinct ports per process for parallel ctest runs.
std::uint16_t basePort() {
static const std::uint16_t port = static_cast(
- 33000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 2000 * 2);
+ 33000 + test::processPortSlot(2000) * 2);
return port;
}
diff --git a/tests/game/test_netgame_start.cpp b/tests/game/test_netgame_start.cpp
index c94db2e..38e6492 100644
--- a/tests/game/test_netgame_start.cpp
+++ b/tests/game/test_netgame_start.cpp
@@ -1,6 +1,7 @@
// A network race's start on each machine (game::NetRaceStart, MM2's
// mmMultiRace / mmMultiCircuit / mmMultiBlitz::UpdateGame state 0) and the
// handshake through game::NetGame (docs/multiplayer.md, "Race start").
+#include "NetTestPorts.h"
#include "game/Strings.h"
#include "game/net/NetGame.h"
#include "game/net/RaceStart.h"
@@ -28,7 +29,7 @@ constexpr float kFrame = 1.0f / 60.0f;
// distinct ports per process for parallel ctest runs.
std::uint16_t basePort() {
static const std::uint16_t port = static_cast(
- 17000 + (Clock::now().time_since_epoch().count() / 1000) % 1500 * 2);
+ 17000 + test::processPortSlot(1500) * 2);
return port;
}
diff --git a/tests/game/test_netgame_sync.cpp b/tests/game/test_netgame_sync.cpp
index 2f00033..053a828 100644
--- a/tests/game/test_netgame_sync.cpp
+++ b/tests/game/test_netgame_sync.cpp
@@ -1,5 +1,6 @@
// NetGame's replication timing: a car's state carries the time the
// simulation was at, and remote cars are sampled with the simulation's lag.
+#include "NetTestPorts.h"
#include "game/net/NetGame.h"
#include
@@ -19,7 +20,7 @@ namespace {
// fail. Within a block, distinct ports per process for parallel ctest runs.
std::uint16_t syncPort() {
static const std::uint16_t port = static_cast(
- 37000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 2000 * 2);
+ 37000 + test::processPortSlot(2000) * 2);
return port;
}
diff --git a/tests/game/test_traffic_net.cpp b/tests/game/test_traffic_net.cpp
index ff5be6b..4f14eff 100644
--- a/tests/game/test_traffic_net.cpp
+++ b/tests/game/test_traffic_net.cpp
@@ -3,6 +3,7 @@
// (game::TrafficHost), sent on the Ambient channel and followed by the client
// (game::TrafficClient) at the interpolation delay; then the client's hit
// report back to the host.
+#include "NetTestPorts.h"
#include "game/net/NetGame.h"
#include "game/net/TrafficSync.h"
@@ -27,7 +28,7 @@ namespace {
// fail. Within a block, distinct ports per process for parallel ctest runs.
std::uint16_t trafficPort() {
static const std::uint16_t port = static_cast(
- 45000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 2000 * 2);
+ 45000 + test::processPortSlot(2000) * 2);
return port;
}
diff --git a/tests/net/test_player_cars.cpp b/tests/net/test_player_cars.cpp
index a512c2c..2b0e5d9 100644
--- a/tests/net/test_player_cars.cpp
+++ b/tests/net/test_player_cars.cpp
@@ -1,6 +1,7 @@
// The host-simulated players' cars on the wire (net/PlayerCarState.h): the
// clients' inputs and the host's states, their encodings, what a receiver
// refuses, mutated messages, and their delivery through real sessions.
+#include "NetTestPorts.h"
#include "net/PlayerCarState.h"
#include "net/Session.h"
@@ -24,7 +25,7 @@ using Bytes = std::vector;
// This file's block of ports, below 49152 (see tests/game/test_traffic_net.cpp).
std::uint16_t testPort(int k) {
static const auto base = static_cast(
- 21000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 1000 * 4);
+ 21000 + test::processPortSlot(1000) * 4);
return static_cast(base + k);
}