Avoid fast-forwarding peep into the ride

The code being removed in the patch tries to fast forward a peep into the ride when it is the second peep for a vehicle that is used in pairs. Problem is that funds checking does not happen, so it happens that a peep may pay against its will.
Lets say a rich peep enters in line and a poor peep enters in line right after.
If the price of the ride is such that the rich peep can pay and the poor peep can't, it will be dragged into the ride because funds checking only happened for the first.
The second part of the patch just adjusts we consider the vehicle a full car if the second position is filled.

Add test to verify that a peep is not dragged into a ride it can't pay

This test puts two peeps in a Ferris Wheel. The first peep is rich and the second peep is poor. When they are both in line, the ride price is raised so that the poor peep can't pay.
Make sure the poor peep turns back and leaves the ride.

During development, a mistake in the logic would have broken all rides other than ferris wheels in a way that multiple guests could enter the same car.
Also add a test to make sure that is never broken.
This commit is contained in:
Breno Rodrigues Guimarães 2020-04-12 11:00:02 -03:00 committed by Breno Guimaraes
parent 38ffc4f577
commit ab53ddf59f
8 changed files with 211 additions and 6 deletions

View File

@ -10,6 +10,7 @@
- Fix: [#6119] Advertising campaign for ride window not updated properly (original bug).
- Fix: [#11072] Land and water tools working out of bounds (original bug).
- Fix: [#11259] Custom JSON object breaks saves.
- Fix: [#11290] Perform funds checking for all peeps entering a ride.
- Fix: [#11315] Ride that has never opened is shown as favorite ride of many guests.
- Fix: [#11405] Building a path through walls does not always remove the walls.
- Fix: RCT1 scenarios have more items in the object list than are present in the park or the research list.

View File

@ -31,7 +31,7 @@
// This string specifies which version of network stream current build uses.
// It is used for making sure only compatible builds get connected, even within
// single OpenRCT2 version.
#define NETWORK_STREAM_VERSION "7"
#define NETWORK_STREAM_VERSION "8"
#define NETWORK_STREAM_ID OPENRCT2_VERSION "-" NETWORK_STREAM_VERSION
static Peep* _pickup_peep = nullptr;

View File

@ -2598,14 +2598,13 @@ bool Guest::FindVehicleToEnter(Ride* ride, std::vector<uint8_t>& car_array)
uint8_t num_seats = vehicle->num_seats;
if (vehicle_is_used_in_pairs(vehicle))
{
num_seats &= VEHICLE_SEAT_NUM_MASK;
if (vehicle->next_free_seat & 1)
{
current_car = i;
peep_choose_seat_from_car(this, ride, vehicle);
GoToRideEntrance(ride);
return false;
car_array.clear();
car_array.push_back(i);
return true;
}
num_seats &= VEHICLE_SEAT_NUM_MASK;
}
if (num_seats == vehicle->next_free_seat)
continue;

View File

@ -223,6 +223,15 @@ target_link_libraries(test_replays ${GTEST_LIBRARIES} libopenrct2 ${LDL} z)
target_link_platform_libraries(test_replays)
add_test(NAME replay_tests COMMAND test_replays)
# Play tests
set(PLAY_TEST_SOURCES "${CMAKE_CURRENT_LIST_DIR}/PlayTests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/TestData.cpp")
add_executable(test_plays ${PLAY_TEST_SOURCES})
SET_CHECK_CXX_FLAGS(test_plays)
target_link_libraries(test_plays ${GTEST_LIBRARIES} libopenrct2 ${LDL} z)
target_link_platform_libraries(test_plays)
add_test(NAME play_tests COMMAND test_plays)
# Pathfinding test
set(PATHFINDING_TEST_SOURCES "${CMAKE_CURRENT_LIST_DIR}/Pathfinding.cpp"
"${CMAKE_CURRENT_LIST_DIR}/TestData.cpp")

195
test/tests/PlayTests.cpp Normal file
View File

@ -0,0 +1,195 @@
/*****************************************************************************
* Copyright (c) 2014-2020 OpenRCT2 developers
*
* For a complete list of all authors, please refer to contributors.md
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
*
* OpenRCT2 is licensed under the GNU General Public License version 3.
*****************************************************************************/
#include "TestData.h"
#include <gtest/gtest.h>
#include <openrct2/Context.h>
#include <openrct2/Game.h>
#include <openrct2/GameState.h>
#include <openrct2/OpenRCT2.h>
#include <openrct2/ParkImporter.h>
#include <openrct2/actions/ParkSetParameterAction.hpp>
#include <openrct2/actions/RideSetPriceAction.hpp>
#include <openrct2/object/ObjectManager.h>
#include <openrct2/peep/Peep.h>
#include <openrct2/platform/platform.h>
#include <openrct2/ride/Ride.h>
#include <openrct2/world/MapAnimation.h>
#include <openrct2/world/Park.h>
#include <openrct2/world/Scenery.h>
#include <openrct2/world/Sprite.h>
#include <string>
using namespace OpenRCT2;
class PlayTests : public testing::Test
{
};
static std::unique_ptr<IContext> localStartGame(const std::string& parkPath)
{
gOpenRCT2Headless = true;
gOpenRCT2NoGraphics = true;
core_init();
auto context = CreateContext();
if (!context->Initialise())
return {};
auto importer = ParkImporter::CreateS6(context->GetObjectRepository());
auto loadResult = importer->LoadSavedGame(parkPath.c_str(), false);
context->GetObjectManager().LoadObjects(loadResult.RequiredObjects.data(), loadResult.RequiredObjects.size());
importer->Import();
reset_sprite_spatial_index();
reset_all_sprite_quadrant_placements();
scenery_set_default_placement_configuration();
load_palette();
map_reorganise_elements();
sprite_position_tween_reset();
AutoCreateMapAnimations();
fix_invalid_vehicle_sprite_sizes();
gGameSpeed = 1;
return context;
}
template<class Fn> static bool updateUntil(GameState& gs, int maxSteps, Fn&& fn)
{
while (maxSteps-- && !fn())
{
gs.UpdateLogic();
}
return maxSteps > 0;
}
template<class GA, class... Args> static void execute(Args&&... args)
{
GA ga(std::forward<Args>(args)...);
GameActions::Execute(&ga);
}
TEST_F(PlayTests, SecondGuestInQueueShouldNotRideIfNoFunds)
{
/* This test verifies that a guest, when second in queue, won't be forced to enter
* the ride if it has not enough money to pay for it.
* To simulate this scenario, two guests (a rich and a poor) are encouraged to enter
* the ride queue, and then the price is raised such that the second guest in line
* (the poor one) cannot pay. The poor guest should not enter the ride.
*/
std::string initStateFile = TestData::GetParkPath("small_park_with_ferris_wheel.sv6");
auto context = localStartGame(initStateFile);
ASSERT_NE(context.get(), nullptr);
auto gs = context->GetGameState();
ASSERT_NE(gs, nullptr);
// Open park for free but charging for rides
execute<ParkSetParameterAction>(ParkParameter::Open);
park_set_entrance_fee(0);
gParkFlags |= PARK_FLAGS_UNLOCK_ALL_PRICES;
// Find ferris wheel
auto rideManager = GetRideManager();
auto it = std::find_if(
rideManager.begin(), rideManager.end(), [](auto& ride) { return ride.type == RIDE_TYPE_FERRIS_WHEEL; });
ASSERT_NE(it, rideManager.end());
Ride& ferrisWheel = *it;
// Open it for free
ride_set_status(&ferrisWheel, RIDE_STATUS_OPEN);
execute<RideSetPriceAction>(ferrisWheel.id, 0, true);
// Ignore intesity to stimulate peeps to queue into ferris wheel
gCheatsIgnoreRideIntensity = true;
// Insert a rich guest
auto richGuest = gs->GetPark().GenerateGuest();
richGuest->cash_in_pocket = 3000;
// Wait for rich guest to get in queue
bool matched = updateUntil(*gs, 1000, [&]() { return richGuest->state == PEEP_STATE_QUEUING; });
ASSERT_TRUE(matched);
// Insert poor guest
auto poorGuest = gs->GetPark().GenerateGuest();
poorGuest->cash_in_pocket = 5;
// Wait for poor guest to get in queue
matched = updateUntil(*gs, 1000, [&]() { return poorGuest->state == PEEP_STATE_QUEUING; });
ASSERT_TRUE(matched);
// Raise the price of the ride to a value poor guest can't pay
execute<RideSetPriceAction>(ferrisWheel.id, 10, true);
// Verify that the poor guest goes back to walking without riding
// since it doesn't have enough money to pay for it
bool enteredTheRide = false;
matched = updateUntil(*gs, 10000, [&]() {
enteredTheRide |= poorGuest->state == PEEP_STATE_ON_RIDE;
return poorGuest->state == PEEP_STATE_WALKING || enteredTheRide;
});
ASSERT_TRUE(matched);
ASSERT_FALSE(enteredTheRide);
}
TEST_F(PlayTests, CarRideWithOneCarOnlyAcceptsTwoGuests)
{
// This test verifies that a car ride with one car will accept at most two guests
std::string initStateFile = TestData::GetParkPath("small_park_car_ride_one_car.sv6");
auto context = localStartGame(initStateFile);
ASSERT_NE(context.get(), nullptr);
auto gs = context->GetGameState();
ASSERT_NE(gs, nullptr);
// Open park for free but charging for rides
execute<ParkSetParameterAction>(ParkParameter::Open);
park_set_entrance_fee(0);
gParkFlags |= PARK_FLAGS_UNLOCK_ALL_PRICES;
// Find car ride
auto rideManager = GetRideManager();
auto it = std::find_if(rideManager.begin(), rideManager.end(), [](auto& ride) { return ride.type == RIDE_TYPE_CAR_RIDE; });
ASSERT_NE(it, rideManager.end());
Ride& carRide = *it;
// Open it for free
ride_set_status(&carRide, RIDE_STATUS_OPEN);
execute<RideSetPriceAction>(carRide.id, 0, true);
// Ignore intesity to stimulate peeps to queue into the ride
gCheatsIgnoreRideIntensity = true;
// Create some guests
std::vector<Peep*> guests;
for (int i = 0; i < 25; i++)
{
guests.push_back(gs->GetPark().GenerateGuest());
}
// Wait until one of them is riding
auto guestIsOnRide = [](auto* g) { return g->state == PEEP_STATE_ON_RIDE; };
bool matched = updateUntil(*gs, 10000, [&]() { return std::any_of(guests.begin(), guests.end(), guestIsOnRide); });
ASSERT_TRUE(matched);
// For the next few ticks at most two guests can be on the ride
for (int i = 0; i < 100; i++)
{
int numRiding = std::count_if(guests.begin(), guests.end(), guestIsOnRide);
ASSERT_LE(numRiding, 2);
gs->UpdateLogic();
}
}

Binary file not shown.

Binary file not shown.

View File

@ -66,6 +66,7 @@
<ClCompile Include="Localisation.cpp" />
<ClCompile Include="MultiLaunch.cpp" />
<ClCompile Include="ReplayTests.cpp" />
<ClCompile Include="PlayTests.cpp" />
<ClCompile Include="Pathfinding.cpp" />
<ClCompile Include="RideRatings.cpp" />
<ClCompile Include="S6ImportExportTests.cpp" />