diff --git a/dGame/dComponents/RacingControlComponent.cpp b/dGame/dComponents/RacingControlComponent.cpp index 314ec2f0f..add4741c0 100644 --- a/dGame/dComponents/RacingControlComponent.cpp +++ b/dGame/dComponents/RacingControlComponent.cpp @@ -371,6 +371,13 @@ void RacingControlComponent::OnRequestDie(Entity* player, const std::u16string& resetInfo.respawnPos = racingPlayer.respawnPosition; resetInfo.upcomingPlane = racingPlayer.respawnIndex + 1; resetInfo.Send(UNASSIGNED_SYSTEM_ADDRESS); + for (auto& current : m_RacingPlayers) { + if (current.playerID != racingPlayer.playerID) continue; + current.lastPlane = resetInfo.furthestResetPlane; + current.upcomingPlane = resetInfo.upcomingPlane; + current.wrongWayCount = 0; + current.wrongWayTime = -1.0f; + } DestroyableComponent::Resurrect(*vehicle); auto* destroyableComponent = vehicle->GetComponent(); @@ -392,6 +399,10 @@ void RacingControlComponent::OnRequestDie(Entity* player, const std::u16string& resetInfo.respawnPos = racingPlayer.respawnPosition; resetInfo.upcomingPlane = racingPlayer.respawnIndex + 1; resetInfo.Send(UNASSIGNED_SYSTEM_ADDRESS); + racingPlayer.lastPlane = resetInfo.furthestResetPlane; + racingPlayer.upcomingPlane = resetInfo.upcomingPlane; + racingPlayer.wrongWayCount = 0; + racingPlayer.wrongWayTime = -1.0f; GameMessages::RacingResetPlayerToLastReset resetPlayer; resetPlayer.target = m_Parent->GetObjectID(); @@ -804,6 +815,8 @@ void RacingControlComponent::Update(float deltaTime) { if (m_Finished != 0) Game::entityManager->SerializeEntity(m_Parent); + if (path && player.lap < m_NumberOfLaps) UpdateWrongWay(player, *vehicle, *path, deltaTime); + // Loop through all the waypoints and see if the player has reached a // new checkpoint uint32_t respawnIndex = 0; @@ -944,3 +957,70 @@ bool RacingControlComponent::MsgConfigureRacingControl(const GameMessages::Confi } return true; } + +void RacingControlComponent::StepResetPlanes(RacingPlayerInfo& player, const std::vector& waypoints, const NiPoint3& position) { + // How long the client counts down (its "UpdateRaceWrongWay" timer, 6 in 1.10.64) before a wrong way racer should + // be put back + constexpr float WRONG_WAY_SECONDS = 6.0f; + constexpr float EPSILON = 1e-5f; + + const auto count = static_cast(waypoints.size()); + if (count == 0) return; + + // Which side of a reset plane the car is on: the plane goes through the waypoint, facing along its rotation + // (ClassifyPointAgainstFacingPlane @ 010e2b80 in 1.10.64). 1 = in front, 2 = behind, 3 = on it. + const auto side = [&](const uint32_t index) { + const auto& waypoint = waypoints[index]; + const float distance = QuatUtils::Forward(waypoint.rotation).DotProduct(position - waypoint.position); + return distance > EPSILON ? 1 : distance < -EPSILON ? 2 : 3; + }; + + // Behind the last plane: driven back through it (LWORacingControlComponent::CheckLastResetPlane @ 00c7f1a0, CrossResetPlaneBackward @ 00cba380). The second plane in a row makes the + // client show its wrong way countdown (UpdateWrongWayCount @ 00be5c10). + bool wentBack = false; + for (uint32_t guard = 0; guard < count && player.lastPlane < count && side(player.lastPlane) == 2; guard++) { + wentBack = true; + player.wrongWayCount++; + if (player.wrongWayCount == 2) player.wrongWayTime = WRONG_WAY_SECONDS; + player.upcomingPlane = player.lastPlane; + player.lastPlane = player.lastPlane == 0 ? count - 1 : player.lastPlane - 1; + } + if (wentBack) return; + + // In front of the upcoming plane: driven forward through it, which ends going the wrong way (CheckUpcomingResetPlane @ 00c7edf0) + for (uint32_t guard = 0; guard < count && player.upcomingPlane < count && side(player.upcomingPlane) == 1; guard++) { + player.wrongWayCount = 0; + player.wrongWayTime = -1.0f; + player.lastPlane = player.upcomingPlane; + player.upcomingPlane = (player.upcomingPlane + 1) % count; + } +} + +void RacingControlComponent::UpdateWrongWay(RacingPlayerInfo& player, const Entity& vehicle, const Path& path, const float deltaTime) { + StepResetPlanes(player, path.pathWaypoints, vehicle.GetPosition()); + + if (player.wrongWayTime < 0.0f) return; + player.wrongWayTime -= deltaTime; + if (player.wrongWayTime > 0.0f) return; + + // Out of time: back to the last reset point, the way a reset without a smash does it (the client moves the car, + // clears its wrong way state and hides the countdown when it gets the reset info) + GameMessages::RacingSetPlayerResetInfo resetInfo; + resetInfo.target = m_Parent->GetObjectID(); + resetInfo.currentLap = static_cast(player.lap); + resetInfo.furthestResetPlane = player.respawnIndex; + resetInfo.playerID = player.playerID; + resetInfo.respawnPos = player.respawnPosition; + resetInfo.upcomingPlane = player.respawnIndex + 1; + resetInfo.Send(UNASSIGNED_SYSTEM_ADDRESS); + + GameMessages::RacingResetPlayerToLastReset resetPlayer; + resetPlayer.target = m_Parent->GetObjectID(); + resetPlayer.playerID = player.playerID; + resetPlayer.Send(UNASSIGNED_SYSTEM_ADDRESS); + + player.lastPlane = resetInfo.furthestResetPlane; + player.upcomingPlane = resetInfo.upcomingPlane; + player.wrongWayCount = 0; + player.wrongWayTime = -1.0f; +} diff --git a/dGame/dComponents/RacingControlComponent.h b/dGame/dComponents/RacingControlComponent.h index bedb636b6..23deedd38 100644 --- a/dGame/dComponents/RacingControlComponent.h +++ b/dGame/dComponents/RacingControlComponent.h @@ -99,11 +99,28 @@ struct RacingPlayerInfo { * Unused */ std::chrono::milliseconds raceTime; + + /** + * The reset planes (path waypoints) the player is between, and how many planes they have driven back through + * since they last went forward through one, tracked the way the client tracks them + * (LWORacingControlComponent::HandlePlayerAddedToRace @ 00cba0c0 starts them at 0 and 1). + */ + uint32_t lastPlane = 0; + uint32_t upcomingPlane = 1; + uint32_t wrongWayCount = 0; + + /** + * Seconds left until a player going the wrong way is put back on the track, negative when not counting down + */ + float wrongWayTime = -1.0f; }; /** * Component that's attached to a manager entity in each race zone that loads player vehicles, keep scores, etc. */ +struct Path; +struct PathWaypoint; + class RacingControlComponent final : public ActivityComponent { public: static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::RACING_CONTROL; @@ -119,6 +136,18 @@ public: */ void OnPlayerLoaded(Entity* player); + /** + * Moves the player's reset planes as they drive through them and puts them back on the track when they have been + * going the wrong way for as long as the client counts down. + */ + void UpdateWrongWay(RacingPlayerInfo& player, const Entity& vehicle, const Path& path, float deltaTime); + + /** + * Moves the player's reset planes for the car being at position, like the client does each frame, and starts the + * wrong way countdown when they have driven back through a second plane. + */ + static void StepResetPlanes(RacingPlayerInfo& player, const std::vector& waypoints, const NiPoint3& position); + /** * Initalize the player's vehicle. * diff --git a/tests/dGameTests/dComponentsTests/CMakeLists.txt b/tests/dGameTests/dComponentsTests/CMakeLists.txt index e641ac8be..13415b920 100644 --- a/tests/dGameTests/dComponentsTests/CMakeLists.txt +++ b/tests/dGameTests/dComponentsTests/CMakeLists.txt @@ -1,6 +1,7 @@ set(DCOMPONENTS_TESTS "DeletionRestrictionTests.cpp" "VendorBuybackTests.cpp" + "RacingWrongWayTests.cpp" "DestroyableComponentTests.cpp" "PetComponentTests.cpp" "SimplePhysicsComponentTests.cpp" diff --git a/tests/dGameTests/dComponentsTests/RacingWrongWayTests.cpp b/tests/dGameTests/dComponentsTests/RacingWrongWayTests.cpp new file mode 100644 index 000000000..8a8f0565b --- /dev/null +++ b/tests/dGameTests/dComponentsTests/RacingWrongWayTests.cpp @@ -0,0 +1,48 @@ +#include "GameMessages.h" +#include "RacingControlComponent.h" +#include "ZoneFile.h" + +#include + +namespace { + // A straight track along +Z with planes every 10 units, all facing along the track + std::vector StraightTrack() { + std::vector waypoints(5); + for (size_t i = 0; i < waypoints.size(); i++) waypoints[i].position = NiPoint3(0.0f, 0.0f, 10.0f * static_cast(i)); + return waypoints; + } +} + +TEST(RacingWrongWayTests, DrivingForwardMovesThePlanes) { + const auto track = StraightTrack(); + RacingPlayerInfo player{}; + RacingControlComponent::StepResetPlanes(player, track, NiPoint3(0.0f, 0.0f, 5.0f)); + EXPECT_EQ(player.lastPlane, 0u); + EXPECT_EQ(player.upcomingPlane, 1u); + RacingControlComponent::StepResetPlanes(player, track, NiPoint3(0.0f, 0.0f, 25.0f)); + EXPECT_EQ(player.lastPlane, 2u); + EXPECT_EQ(player.upcomingPlane, 3u); + EXPECT_EQ(player.wrongWayCount, 0u); + EXPECT_LT(player.wrongWayTime, 0.0f); +} + +TEST(RacingWrongWayTests, TheSecondPlaneBackStartsTheCountdown) { + const auto track = StraightTrack(); + RacingPlayerInfo player{}; + player.lastPlane = 3; + player.upcomingPlane = 4; + RacingControlComponent::StepResetPlanes(player, track, NiPoint3(0.0f, 0.0f, 25.0f)); + EXPECT_EQ(player.wrongWayCount, 1u); + EXPECT_LT(player.wrongWayTime, 0.0f); + RacingControlComponent::StepResetPlanes(player, track, NiPoint3(0.0f, 0.0f, 15.0f)); + EXPECT_EQ(player.wrongWayCount, 2u); + EXPECT_EQ(player.lastPlane, 1u); + EXPECT_EQ(player.upcomingPlane, 2u); + EXPECT_FLOAT_EQ(player.wrongWayTime, 6.0f); + + // Turning around and going through a plane again ends it + RacingControlComponent::StepResetPlanes(player, track, NiPoint3(0.0f, 0.0f, 21.0f)); + EXPECT_EQ(player.wrongWayCount, 0u); + EXPECT_LT(player.wrongWayTime, 0.0f); + EXPECT_EQ(player.lastPlane, 2u); +}