Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
5 changes: 5 additions & 0 deletions packaging/linux/io.github.lowlevelmetal.OpenMM2.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
<display_length compare="ge">768</display_length>
</requires>
<releases>
<release version="0.4.2" date="2026-10-10" type="development">
<description>
<p>The multiplayer net name is used as typed, without having to press Enter, and changing it no longer stalls the menus.</p>
</description>
</release>
<release version="0.4.1" date="2026-10-10" type="development">
<description>
<p>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.</p>
Expand Down
52 changes: 34 additions & 18 deletions src/app/frontend/PagesMulti.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,30 +190,24 @@ class SessionsPage final : public Page {
}
// NetSelectMenu::NetSelectMenu: the NET NAME field takes 12 characters.
auto& name = menu.add<ui::TextEntry>(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<ui::SpriteButton>(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<ui::SpriteButton>(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<net::DiscoveredSession>{};
if (m_selected >= 0 && m_selected < static_cast<int>(list.size()))
joinListed(fe, list[static_cast<std::size_t>(m_selected)]);
Expand All @@ -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<net::DiscoveredSession>{};
if (m_selected >= 0 && m_selected < static_cast<int>(list.size()))
joinListed(fe, list[static_cast<std::size_t>(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);
}
Expand Down Expand Up @@ -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<std::string> rows(Frontend& fe) const {
std::vector<std::string> out;
if (!fe.ctx.netGame)
Expand Down
3 changes: 3 additions & 0 deletions src/game/net/NetGame.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
31 changes: 31 additions & 0 deletions tests/NetTestPorts.h
Original file line number Diff line number Diff line change
@@ -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 <cstdint>

#ifdef _WIN32
#include <process.h>
#else
#include <unistd.h>
#endif

namespace mm2::test {

inline std::uint32_t processPortSlot(std::uint32_t slots) {
#ifdef _WIN32
const auto id = static_cast<std::uint32_t>(_getpid()) / 4; // Windows process ids are multiples of 4
#else
const auto id = static_cast<std::uint32_t>(getpid());
#endif
return id % slots;
}

} // namespace mm2::test
3 changes: 2 additions & 1 deletion tests/TestData.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
3 changes: 2 additions & 1 deletion tests/game/test_damage_sync.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -665,7 +666,7 @@ namespace {
// test_netgame_sync.cpp).
std::uint16_t damagePort() {
static const std::uint16_t port = static_cast<std::uint16_t>(
29000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 2000 * 2);
29000 + test::processPortSlot(2000) * 2);
return port;
}

Expand Down
3 changes: 2 additions & 1 deletion tests/game/test_net_race_rules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<std::uint16_t>(
25000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 1000 * 4);
25000 + test::processPortSlot(1000) * 4);
return port;
}

Expand Down
30 changes: 28 additions & 2 deletions tests/game/test_netgame.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "NetTestPorts.h"
#include "game/net/NetGame.h"

#include <gtest/gtest.h>
Expand All @@ -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<std::uint16_t>(
41000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 2000 * 2);
41000 + test::processPortSlot(666) * 6);
return port;
}

Expand Down Expand Up @@ -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));
Expand Down
3 changes: 2 additions & 1 deletion tests/game/test_netgame_lobby.cpp
Original file line number Diff line number Diff line change
@@ -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 <gtest/gtest.h>
Expand All @@ -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<std::uint16_t>(
33000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 2000 * 2);
33000 + test::processPortSlot(2000) * 2);
return port;
}

Expand Down
3 changes: 2 additions & 1 deletion tests/game/test_netgame_start.cpp
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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<std::uint16_t>(
17000 + (Clock::now().time_since_epoch().count() / 1000) % 1500 * 2);
17000 + test::processPortSlot(1500) * 2);
return port;
}

Expand Down
3 changes: 2 additions & 1 deletion tests/game/test_netgame_sync.cpp
Original file line number Diff line number Diff line change
@@ -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 <gtest/gtest.h>
Expand All @@ -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<std::uint16_t>(
37000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 2000 * 2);
37000 + test::processPortSlot(2000) * 2);
return port;
}

Expand Down
3 changes: 2 additions & 1 deletion tests/game/test_traffic_net.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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<std::uint16_t>(
45000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 2000 * 2);
45000 + test::processPortSlot(2000) * 2);
return port;
}

Expand Down
3 changes: 2 additions & 1 deletion tests/net/test_player_cars.cpp
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -24,7 +25,7 @@ using Bytes = std::vector<std::byte>;
// 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<std::uint16_t>(
21000 + (std::chrono::steady_clock::now().time_since_epoch().count() / 1000) % 1000 * 4);
21000 + test::processPortSlot(1000) * 4);
return static_cast<std::uint16_t>(base + k);
}

Expand Down
Loading