From 879b011abb9b07f8df114e9b554104cb39acacf3 Mon Sep 17 00:00:00 2001 From: lowlevelmetal Date: Sat, 10 Oct 2026 18:31:51 -0700 Subject: [PATCH 1/4] Use the net name in the field whether or not Enter was pressed The sessions page applied a typed net name only on Enter. A name typed and followed by HOST, JOIN or leaving the page showed in the field but was not used, and came back as the old name next time. MM2's field edits the menu's own name buffer (NetSelectMenu +0x8c), which hosting and joining read; NetSelectMenu::NetNameCB only logs it. The name is now applied on Enter, before HOST and JOIN, and on leaving the page. Applying it no longer replaces the NetGame: tearing down the old one's port forwarding stalled the menus and cleared the LAN list. NetGame:: setPlayerName gives the next host or join the new name, and the profile is saved only when the name changed. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/app/frontend/PagesMulti.cpp | 52 +++++++++++++++++++++------------ src/game/net/NetGame.h | 3 ++ tests/game/test_netgame.cpp | 23 +++++++++++++++ 3 files changed, 60 insertions(+), 18 deletions(-) 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/game/test_netgame.cpp b/tests/game/test_netgame.cpp index 8d61ced..fcb4669 100644 --- a/tests/game/test_netgame.cpp +++ b/tests/game/test_netgame.cpp @@ -223,6 +223,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)); From 2675ed9fbba90fd4350afc7e9cbf89542196dcb0 Mon Sep 17 00:00:00 2001 From: lowlevelmetal Date: Sat, 10 Oct 2026 18:44:22 -0700 Subject: [PATCH 2/4] Pick a network test's ports from its process id, not the clock ctest runs each test as a process of its own, several at a time, and the port slot came from the clock's microseconds: two processes could land on the same slot now and then, and the second could not bind its port (NetGameLobby.CopsVsRobbersCarsAreTheSameOnEveryMachine failed that way on the v0.4.1 tag's Clang job, after passing in the PR). Processes running at the same time have distinct ids, handed out in order on Linux, so test::processPortSlot (tests/NetTestPorts.h) gives them distinct slots. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/NetTestPorts.h | 29 +++++++++++++++++++++++++++++ tests/TestData.h | 3 ++- tests/game/test_damage_sync.cpp | 3 ++- tests/game/test_net_race_rules.cpp | 3 ++- tests/game/test_netgame.cpp | 3 ++- tests/game/test_netgame_lobby.cpp | 3 ++- tests/game/test_netgame_start.cpp | 3 ++- tests/game/test_netgame_sync.cpp | 3 ++- tests/game/test_traffic_net.cpp | 3 ++- tests/net/test_player_cars.cpp | 3 ++- 10 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 tests/NetTestPorts.h diff --git a/tests/NetTestPorts.h b/tests/NetTestPorts.h new file mode 100644 index 0000000..293db49 --- /dev/null +++ b/tests/NetTestPorts.h @@ -0,0 +1,29 @@ +#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. + +#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 fcb4669..14ecb63 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 @@ -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( - 41000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 2000 * 2); + 41000 + test::processPortSlot(2000) * 2); return port; } 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); } From 8f06cb38f2307b357d617cc74ab019242b2768cb Mon Sep 17 00:00:00 2001 From: lowlevelmetal Date: Sat, 10 Oct 2026 18:44:38 -0700 Subject: [PATCH 3/4] Call this version 0.4.2 Co-Authored-By: Claude Opus 5.5 (1M context) --- CMakeLists.txt | 2 +- packaging/linux/io.github.lowlevelmetal.OpenMM2.metainfo.xml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6395728..90ed33e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25...4.0) project(OpenMM2 - VERSION 0.4.1 + VERSION 0.4.2 DESCRIPTION "Open source reimplementation of Midtown Madness 2" HOMEPAGE_URL "https://github.com/lowlevelmetal/OpenMM2" LANGUAGES C CXX) diff --git a/packaging/linux/io.github.lowlevelmetal.OpenMM2.metainfo.xml b/packaging/linux/io.github.lowlevelmetal.OpenMM2.metainfo.xml index 4abba44..06a32f8 100644 --- a/packaging/linux/io.github.lowlevelmetal.OpenMM2.metainfo.xml +++ b/packaging/linux/io.github.lowlevelmetal.OpenMM2.metainfo.xml @@ -44,6 +44,11 @@ 768 + + +

The multiplayer net name is used as typed, without having to press Enter, and changing it no longer stalls the menus.

+
+

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.

From b6eca04af4e3f1801d9c775932969a0f760e69bd Mon Sep 17 00:00:00 2001 From: lowlevelmetal Date: Sat, 10 Oct 2026 19:16:23 -0700 Subject: [PATCH 4/4] Space test_netgame's port slots by the ports one process uses A process in test_netgame.cpp binds its base port, +1 (LAN discovery), +2 and +4, but its slots were 2 apart. Process ids in CI's container come in order, so neighbouring tests got neighbouring slots and one's +2 port was the next one's base: NetGame.LanDiscoveryPasswordAndKick could not listen on both Linux jobs of PR #7. The slots are now 6 ports apart (666 slots in the block), and NetTestPorts.h says a block's spacing must cover a process's ports; the other files use at most their spacing already. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/NetTestPorts.h | 4 +++- tests/game/test_netgame.cpp | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/NetTestPorts.h b/tests/NetTestPorts.h index 293db49..bdd99f0 100644 --- a/tests/NetTestPorts.h +++ b/tests/NetTestPorts.h @@ -5,7 +5,9 @@ // 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. +// 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 diff --git a/tests/game/test_netgame.cpp b/tests/game/test_netgame.cpp index 14ecb63..33edb23 100644 --- a/tests/game/test_netgame.cpp +++ b/tests/game/test_netgame.cpp @@ -17,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 + test::processPortSlot(2000) * 2); + 41000 + test::processPortSlot(666) * 6); return port; }