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.

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