diff --git a/CMakeLists.txt b/CMakeLists.txt index cac790f..b098e0e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25...4.0) project(OpenMM2 - VERSION 0.3.0 + VERSION 0.3.1 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 2b936ab..56be0ec 100644 --- a/packaging/linux/io.github.lowlevelmetal.OpenMM2.metainfo.xml +++ b/packaging/linux/io.github.lowlevelmetal.OpenMM2.metainfo.xml @@ -44,6 +44,11 @@ 768 + + +

Fixes a crash when an opponent or police car drove onto a road narrower than its lane offset; such cars now steer as in the original game.

+
+

Smooth motion at any frame rate (drawn between the simulation steps), network races that start together once every player has loaded, and other players' and the police's damage shown in multiplayer.

diff --git a/src/ai/DrivingTargets.cpp b/src/ai/DrivingTargets.cpp index 90c7044..64ef8f2 100644 --- a/src/ai/DrivingTargets.cpp +++ b/src/ai/DrivingTargets.cpp @@ -693,7 +693,7 @@ void PhysicsDriver::calcRoadTarget(int i, Vec3& from) { -ra.x(setupIdx).x * (ra.c(ra.n - 1 - v).x - from.x); if (i == 1) { if (v >= 1) - m_roadOffset = std::clamp(off, -h, h); + m_roadOffset = clampAcross(off, h); off = m_roadOffset; } else { off = off < -h ? -h : (h < off ? h : off); @@ -780,13 +780,13 @@ void PhysicsDriver::calcRoadTarget(int i, Vec3& from) { if (!dirOf(r)) { const int last = ra.n - 1; float o = dotXZ(ra.c(last) - node.pos, -ra.x(last)); - o = std::clamp(o, -hn, hn) / hn; + o = clampAcross(o, hn) / hn; const Vec3 edge = ra.b80(0) - ra.x(0) * (m_rightSide + 1.0f); const Vec3& c = ra.c(0); node.pos = {(edge.x - c.x) * o + c.x, (edge.y - c.y) * o + c.y, (edge.z - c.z) * o + c.z}; } else { float o = dotXZ(ra.c(0) - node.pos, ra.x(0)); - o = std::clamp(o, -hn, hn) / hn; + o = clampAcross(o, hn) / hn; const int last = ra.n - 1; const Vec3 edge = ra.be4(last) + ra.x(last) * (m_rightSide + 1.0f); const Vec3& c = ra.c(last); @@ -1080,7 +1080,7 @@ void PhysicsDriver::calcDestinationTarget(int i, Vec3& viewOrigin) { const RoadArrays ls = caseRoad(leftSlot); if (rs.p && !dirOf(rightSlot) && rightV < rs.n - 1) { const float w = (rs.halfWidth - R) - 1.0f; - const float s = std::clamp(dotXZ(P - rs.c(rs.n - 1), rs.x(rs.n - 1)), -w, w); + const float s = clampAcross(dotXZ(P - rs.c(rs.n - 1), rs.x(rs.n - 1)), w); const int kk = rs.n - rightV - 1; const float off = w - w * (s / w); node.pos = rs.b80(kk) - rs.x(kk) * (R + 1.0f) - rs.x(kk) * off; @@ -1088,7 +1088,7 @@ void PhysicsDriver::calcDestinationTarget(int i, Vec3& viewOrigin) { finalV = rightV; } else if (ls.p && !dirOf(leftSlot) && leftV < ls.n - 1) { const float w = (ls.halfWidth - R) - 1.0f; - const float s = std::clamp(dotXZ(P - ls.c(ls.n - 1), ls.x(ls.n - 1)), -w, w); + const float s = clampAcross(dotXZ(P - ls.c(ls.n - 1), ls.x(ls.n - 1)), w); const int kk = ls.n - leftV - 1; const float f = (s / w + 1.0f) * ((ls.halfWidth - L) - 1.0f); node.pos = ls.be4(kk) + ls.x(kk) * (R + 1.0f) + ls.x(kk) * f; @@ -1096,14 +1096,14 @@ void PhysicsDriver::calcDestinationTarget(int i, Vec3& viewOrigin) { finalV = leftV; } else if (rs.p && dirOf(rightSlot) && rightV < rs.n - 1) { const float w = (rs.halfWidth - R) - 1.0f; - const float s = std::clamp(dotXZ(rs.c(0) - P, rs.x(0)), -w, w); + const float s = clampAcross(dotXZ(rs.c(0) - P, rs.x(0)), w); const float off = w - w * (s / w); node.pos = rs.be4(rightV) + rs.x(rightV) * (R + 1.0f) + rs.x(rightV) * off; finalSlot = rightSlot; finalV = rightV; } else if (ls.p && dirOf(leftSlot) && leftV < ls.n - 1) { const float w = (ls.halfWidth - L) - 1.0f; - const float s = std::clamp(dotXZ(ls.c(0) - P, ls.x(0)), -w, w); + const float s = clampAcross(dotXZ(ls.c(0) - P, ls.x(0)), w); if (ls.divided) { node.pos = ls.c(leftV) - ls.x(leftV) * (L + 1.0f); } else { diff --git a/src/ai/PathGeometry.h b/src/ai/PathGeometry.h index 9a6cae8..4008fe8 100644 --- a/src/ai/PathGeometry.h +++ b/src/ai/PathGeometry.h @@ -34,6 +34,17 @@ struct SharpTurn { Vec3 endDir; // +0x38 and to its end }; +// aiVehiclePhysics' clamp of an offset across a road to the half-width `h` +// the car may use (the road's half-width less the car's side and 1 m), as +// CalcRoadTarget and CalcDestinationTarget compare: the lower bound first. +// On a road narrower than that (h < 0) an offset below -h gives -h and any +// other gives h; std::clamp has no answer there, its bounds must be ordered. +inline float clampAcross(float v, float h) { + if (!(-h <= v)) + return -h; + return h < v ? h : v; +} + // The side's curb (`which` 0) or outer sidewalk edge (1): aiPath +0x80 / // +0xe4, the pair after the lane, sidewalk, tram and train vertices. const std::vector& pathBoundary(const city::AiRoadSide& side, int which); diff --git a/tests/ai/test_parity_ai_vehicles.cpp b/tests/ai/test_parity_ai_vehicles.cpp index 5b86431..834ec41 100644 --- a/tests/ai/test_parity_ai_vehicles.cpp +++ b/tests/ai/test_parity_ai_vehicles.cpp @@ -7,6 +7,7 @@ #include "ai/Driving.h" #include "ai/MapView.h" #include "ai/Opponent.h" +#include "ai/PathGeometry.h" #include "ai/Police.h" #include "ai/Traffic.h" #include "ai/World.h" @@ -16,6 +17,8 @@ #include +#include + #include #include #include @@ -77,6 +80,26 @@ city::AiMap squareBlock() { // aiPoliceForce::RegisterPerp does not check whether the cop is on the // suspect already: a second registration takes a second place. +// aiVehiclePhysics::CalcRoadTarget / CalcDestinationTarget clamp an offset +// across a road to +-h with the lower bound first. A road narrower than the +// car's side offset plus 1 m gives h < 0: MM2 then returns -h for an offset +// below -h and h for any other. (std::clamp with those bounds aborted a +// checked build in a race.) +TEST(ParityAiRoute, ClampAcrossKeepsMm2sOrderOnANarrowRoad) { + EXPECT_FLOAT_EQ(ai::clampAcross(0.5f, 2.0f), 0.5f); + EXPECT_FLOAT_EQ(ai::clampAcross(3.0f, 2.0f), 2.0f); + EXPECT_FLOAT_EQ(ai::clampAcross(-3.0f, 2.0f), -2.0f); + EXPECT_FLOAT_EQ(ai::clampAcross(-2.0f, 2.0f), -2.0f); + EXPECT_FLOAT_EQ(ai::clampAcross(2.0f, 2.0f), 2.0f); + // h = -0.5: the bounds cross. + EXPECT_FLOAT_EQ(ai::clampAcross(-1.0f, -0.5f), 0.5f); // below -h = 0.5 + EXPECT_FLOAT_EQ(ai::clampAcross(0.0f, -0.5f), 0.5f); + EXPECT_FLOAT_EQ(ai::clampAcross(0.7f, -0.5f), -0.5f); // not below -h: h + EXPECT_FLOAT_EQ(ai::clampAcross(0.5f, -0.5f), -0.5f); + // A NaN offset fails the first comparison, as in MM2. + EXPECT_FLOAT_EQ(ai::clampAcross(std::nanf(""), 2.0f), -2.0f); +} + TEST(ParityAiPolice, RegisterPerpCountsARepeatedCopTwice) { ai::PoliceForce force; EXPECT_TRUE(force.registerPerp(1, 100));