/***************************************************************************** * Copyright (c) 2014-2024 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 "Vehicle.h" #include "../Context.h" #include "../Editor.h" #include "../Game.h" #include "../GameState.h" #include "../OpenRCT2.h" #include "../actions/RideSetStatusAction.h" #include "../audio/AudioChannel.h" #include "../audio/AudioMixer.h" #include "../audio/audio.h" #include "../config/Config.h" #include "../core/Memory.hpp" #include "../entity/EntityRegistry.h" #include "../entity/Particle.h" #include "../entity/Yaw.hpp" #include "../interface/Viewport.h" #include "../localisation/Formatter.h" #include "../localisation/Localisation.h" #include "../management/NewsItem.h" #include "../math/Trigonometry.hpp" #include "../object/SmallSceneryEntry.h" #include "../platform/Platform.h" #include "../profiling/Profiling.h" #include "../rct12/RCT12.h" #include "../scenario/Scenario.h" #include "../scripting/HookEngine.h" #include "../scripting/ScriptEngine.h" #include "../ui/UiContext.h" #include "../ui/WindowManager.h" #include "../util/Util.h" #include "../windows/Intent.h" #include "../world/Map.h" #include "../world/MapAnimation.h" #include "../world/Park.h" #include "../world/Scenery.h" #include "../world/Surface.h" #include "../world/Wall.h" #include "CableLift.h" #include "Ride.h" #include "RideData.h" #include "Station.h" #include "Track.h" #include "TrackData.h" #include "TrainManager.h" #include "VehicleData.h" #include "VehicleSubpositionData.h" #include #include using namespace OpenRCT2; using namespace OpenRCT2::Audio; using namespace OpenRCT2::TrackMetaData; using namespace OpenRCT2::Math::Trigonometry; static bool vehicle_boat_is_location_accessible(const CoordsXYZ& location); constexpr int16_t VEHICLE_MAX_SPIN_SPEED = 1536; constexpr int16_t VEHICLE_MIN_SPIN_SPEED = -VEHICLE_MAX_SPIN_SPEED; constexpr int16_t VEHICLE_MAX_SPIN_SPEED_FOR_STOPPING = 700; constexpr int16_t VEHICLE_MAX_SPIN_SPEED_WATER_RIDE = 512; constexpr int16_t VEHICLE_MIN_SPIN_SPEED_WATER_RIDE = -VEHICLE_MAX_SPIN_SPEED_WATER_RIDE; constexpr int16_t VEHICLE_STOPPING_SPIN_SPEED = 600; Vehicle* gCurrentVehicle; static uint8_t _vehicleBreakdown; StationIndex _vehicleStationIndex; uint32_t _vehicleMotionTrackFlags; int32_t _vehicleVelocityF64E08; int32_t _vehicleVelocityF64E0C; int32_t _vehicleUnkF64E10; uint8_t _vehicleF64E2C; Vehicle* _vehicleFrontVehicle; CoordsXYZ _vehicleCurPosition; static constexpr OpenRCT2::Audio::SoundId _screamSet0[] = { OpenRCT2::Audio::SoundId::Scream8, OpenRCT2::Audio::SoundId::Scream1, }; static constexpr OpenRCT2::Audio::SoundId _screamSet1Wooden[] = { OpenRCT2::Audio::SoundId::Scream3, OpenRCT2::Audio::SoundId::Scream1, OpenRCT2::Audio::SoundId::Scream5, OpenRCT2::Audio::SoundId::Scream6, OpenRCT2::Audio::SoundId::Scream7, OpenRCT2::Audio::SoundId::Scream2, OpenRCT2::Audio::SoundId::Scream4, }; static constexpr OpenRCT2::Audio::SoundId _screamSet2[] = { OpenRCT2::Audio::SoundId::Scream1, OpenRCT2::Audio::SoundId::Scream6, }; // clang-format off static constexpr uint8_t SpaceRingsTimeToSpriteMap[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 23, 23, 23, 22, 22, 22, 21, 21, 21, 20, 20, 20, 19, 19, 19, 18, 18, 18, 17, 17, 17, 16, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 13, 12, 12, 12, 11, 11, 11, 10, 10, 10, 9, 9, 9, 8, 8, 8, 7, 7, 7, 6, 6, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, 23, 23, 23, 22, 22, 22, 21, 21, 21, 20, 20, 20, 19, 19, 19, 18, 18, 18, 17, 17, 17, 16, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 13, 12, 12, 12, 11, 11, 11, 10, 10, 10, 9, 9, 9, 8, 8, 8, 7, 7, 7, 6, 6, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, 23, 23, 23, 22, 22, 22, 21, 21, 21, 20, 20, 20, 19, 19, 19, 18, 18, 18, 17, 17, 17, 16, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 13, 12, 12, 12, 11, 11, 11, 10, 10, 10, 9, 9, 9, 8, 8, 8, 7, 7, 7, 7, 6, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, 31, 31, 31, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, 37, 37, 38, 38, 38, 39, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, 43, 44, 44, 44, 45, 45, 45, 46, 46, 46, 47, 47, 47, 48, 48, 48, 49, 49, 49, 50, 50, 50, 51, 51, 51, 52, 52, 52, 53, 53, 53, 54, 54, 54, 55, 55, 55, 56, 56, 56, 57, 57, 57, 58, 58, 58, 59, 59, 59, 60, 60, 60, 61, 61, 61, 62, 62, 62, 63, 63, 63, 64, 64, 64, 65, 65, 65, 66, 66, 66, 67, 67, 67, 68, 68, 68, 69, 69, 69, 70, 70, 70, 71, 71, 71, 72, 72, 72, 73, 73, 73, 74, 74, 74, 75, 75, 75, 76, 76, 76, 77, 77, 77, 78, 78, 78, 79, 79, 79, 80, 80, 80, 80, 81, 81, 81, 81, 82, 82, 82, 82, 82, 83, 83, 83, 83, 83, 84, 84, 84, 84, 84, 84, 85, 85, 85, 85, 85, 85, 86, 86, 86, 86, 86, 86, 86, 86, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 86, 86, 86, 86, 86, 86, 86, 85, 85, 85, 85, 85, 85, 84, 84, 84, 84, 84, 84, 83, 83, 83, 83, 83, 82, 82, 82, 82, 82, 81, 81, 81, 81, 80, 80, 80, 80, 79, 79, 79, 78, 78, 78, 77, 77, 77, 76, 76, 76, 75, 75, 75, 74, 74, 74, 73, 73, 73, 72, 72, 72, 71, 71, 71, 70, 70, 70, 69, 69, 69, 68, 68, 68, 67, 67, 67, 66, 66, 66, 65, 65, 65, 64, 64, 64, 63, 63, 63, 62, 62, 62, 61, 61, 61, 60, 60, 60, 59, 59, 59, 58, 58, 58, 57, 57, 57, 56, 56, 56, 55, 55, 55, 54, 54, 54, 53, 53, 53, 52, 52, 52, 51, 51, 51, 50, 50, 50, 49, 49, 49, 48, 48, 48, 47, 47, 47, 46, 46, 46, 45, 45, 45, 44, 44, 44, 43, 43, 43, 42, 42, 42, 41, 41, 41, 40, 40, 40, 39, 39, 39, 38, 38, 38, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 32, 32, 32, 31, 31, 31, 30, 30, 30, 30, 29, 29, 29, 29, 28, 28, 28, 28, 28, 27, 27, 27, 27, 27, 26, 26, 26, 26, 26, 26, 25, 25, 25, 25, 25, 25, 24, 24, 24, 24, 24, 24, 24, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 23, 23, 23, 22, 22, 22, 21, 21, 21, 20, 20, 20, 19, 19, 19, 18, 18, 18, 17, 17, 17, 16, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 13, 12, 12, 12, 11, 11, 11, 10, 10, 10, 9, 9, 9, 8, 8, 8, 7, 7, 7, 6, 6, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, 23, 23, 23, 22, 22, 22, 21, 21, 21, 20, 20, 20, 19, 19, 19, 18, 18, 18, 17, 17, 17, 16, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 13, 12, 12, 12, 11, 11, 11, 10, 10, 10, 9, 9, 9, 8, 8, 8, 7, 7, 7, 6, 6, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, 23, 23, 23, 22, 22, 22, 21, 21, 21, 20, 20, 20, 19, 19, 19, 18, 18, 18, 17, 17, 17, 16, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 13, 12, 12, 12, 11, 11, 11, 10, 10, 10, 9, 9, 9, 8, 8, 8, 7, 7, 7, 7, 6, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, 31, 31, 31, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, 37, 37, 38, 38, 38, 39, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, 43, 44, 44, 44, 45, 45, 45, 46, 46, 46, 47, 47, 47, 48, 48, 48, 49, 49, 49, 50, 50, 50, 51, 51, 51, 52, 52, 52, 53, 53, 53, 54, 54, 54, 55, 55, 55, 56, 56, 56, 57, 57, 57, 58, 58, 58, 59, 59, 59, 60, 60, 60, 61, 61, 61, 62, 62, 62, 63, 63, 63, 64, 64, 64, 65, 65, 65, 66, 66, 66, 67, 67, 67, 68, 68, 68, 69, 69, 69, 70, 70, 70, 71, 71, 71, 72, 72, 72, 73, 73, 73, 74, 74, 74, 75, 75, 75, 76, 76, 76, 77, 77, 77, 78, 78, 78, 79, 79, 79, 80, 80, 80, 80, 81, 81, 81, 81, 82, 82, 82, 82, 82, 83, 83, 83, 83, 83, 84, 84, 84, 84, 84, 84, 85, 85, 85, 85, 85, 85, 86, 86, 86, 86, 86, 86, 86, 86, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 86, 86, 86, 86, 86, 86, 86, 85, 85, 85, 85, 85, 85, 84, 84, 84, 84, 84, 84, 83, 83, 83, 83, 83, 82, 82, 82, 82, 82, 81, 81, 81, 81, 80, 80, 80, 80, 79, 79, 79, 78, 78, 78, 77, 77, 77, 76, 76, 76, 75, 75, 75, 74, 74, 74, 73, 73, 73, 72, 72, 72, 71, 71, 71, 70, 70, 70, 69, 69, 69, 68, 68, 68, 67, 67, 67, 66, 66, 66, 65, 65, 65, 64, 64, 64, 63, 63, 63, 62, 62, 62, 61, 61, 61, 60, 60, 60, 59, 59, 59, 58, 58, 58, 57, 57, 57, 56, 56, 56, 55, 55, 55, 54, 54, 54, 53, 53, 53, 52, 52, 52, 51, 51, 51, 50, 50, 50, 49, 49, 49, 48, 48, 48, 47, 47, 47, 46, 46, 46, 45, 45, 45, 44, 44, 44, 43, 43, 43, 42, 42, 42, 41, 41, 41, 40, 40, 40, 39, 39, 39, 38, 38, 38, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 32, 32, 32, 31, 31, 31, 30, 30, 30, 30, 29, 29, 29, 29, 28, 28, 28, 28, 28, 27, 27, 27, 27, 27, 26, 26, 26, 26, 26, 26, 25, 25, 25, 25, 25, 25, 24, 24, 24, 24, 24, 24, 24, 24, 0, 255, }; // clang-format on static constexpr int8_t SwingingTimeToSpriteMap_0[] = { 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, 0, 0, -128, }; static constexpr int8_t SwingingTimeToSpriteMap_1[] = { 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, 0, -128, }; static constexpr int8_t SwingingTimeToSpriteMap_2[] = { 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, -1, -1, -1, -2, -2, -2, -3, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -6, -6, -6, -6, -6, -6, -6, -6, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -3, -3, -3, -3, -2, -2, -2, -1, -1, -1, 0, -128, }; static constexpr int8_t SwingingTimeToSpriteMap_3[] = { 0, 1, 1, 2, 2, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 5, 5, 5, 5, 4, 4, 4, 3, 3, 2, 2, 1, 1, 0, 0, -1, -1, -2, -2, -3, -3, -4, -4, -4, -5, -5, -5, -5, -6, -6, -6, -6, -6, -7, -7, -7, -7, -7, -7, -8, -8, -8, -8, -8, -8, -8, -8, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -8, -8, -8, -8, -8, -8, -8, -8, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -5, -5, -5, -5, -4, -4, -4, -3, -3, -2, -2, -1, -1, 0, -128, }; static constexpr int8_t SwingingTimeToSpriteMap_4[] = { 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -5, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, -128, }; static constexpr int8_t SwingingTimeToSpriteMap_5[] = { 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 12, 12, 12, 12, 11, 11, 11, 11, 10, 10, 10, 10, 9, 9, 9, 9, 8, 8, 8, 8, 7, 7, 7, 7, 6, 6, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 3, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, -1, -1, -1, -1, -2, -2, -2, -2, -3, -3, -3, -3, -4, -4, -4, -4, -5, -5, -5, -5, -6, -6, -6, -6, -7, -7, -7, -7, -8, -8, -8, -8, -9, -9, -9, -9, -10, -10, -10, -10, -11, -11, -11, -11, -12, -12, -12, -12, -13, -13, -13, -13, -13, -13, -14, -14, -14, -14, -14, -14, -14, -14, -14, -14, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -14, -14, -14, -14, -14, -14, -14, -14, -14, -14, -13, -13, -13, -13, -13, -13, -12, -12, -12, -12, -11, -11, -11, -11, -10, -10, -10, -10, -9, -9, -9, -9, -8, -8, -8, -8, -7, -7, -7, -7, -6, -6, -6, -6, -5, -5, -5, -5, -4, -4, -4, -4, -3, -3, -3, -3, -2, -2, -2, -2, -1, -1, -1, -1, 0, 0, -128, }; static constexpr int8_t SwingingTimeToSpriteMap_6[] = { 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 24, 24, 24, 24, 24, 24, 24, 24, 24, 23, 23, 23, 23, 23, 22, 22, 22, 21, 21, 21, 20, 20, 20, 19, 19, 19, 18, 18, 18, 17, 17, 17, 16, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 13, 12, 12, 12, 11, 11, 11, 10, 10, 10, 9, 9, 9, 8, 8, 8, 7, 7, 7, 6, 6, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, -1, -1, -1, -2, -2, -2, -3, -3, -3, -4, -4, -4, -5, -5, -5, -6, -6, -6, -7, -7, -7, -8, -8, -8, -9, -9, -9, -10, -10, -10, -11, -11, -11, -12, -12, -12, -13, -13, -13, -14, -14, -14, -15, -15, -15, -16, -16, -16, -17, -17, -17, -18, -18, -18, -19, -19, -19, -20, -20, -20, -21, -21, -21, -22, -22, -22, -23, -23, -23, -23, -23, -24, -24, -24, -24, -24, -24, -24, -24, -24, -25, -25, -25, -25, -25, -25, -25, -25, -25, -25, -25, -25, -25, -25, -25, -25, -25, -25, -25, -24, -24, -24, -24, -24, -24, -24, -24, -24, -23, -23, -23, -23, -23, -22, -22, -22, -21, -21, -21, -20, -20, -20, -19, -19, -19, -18, -18, -18, -17, -17, -17, -16, -16, -16, -15, -15, -15, -14, -14, -14, -13, -13, -13, -12, -12, -12, -11, -11, -11, -10, -10, -10, -9, -9, -9, -8, -8, -8, -7, -7, -7, -6, -6, -6, -5, -5, -5, -4, -4, -4, -3, -3, -3, -2, -2, -2, -1, -1, -1, 0, 0, -128, }; static constexpr int8_t SwingingTimeToSpriteMap_7[] = { 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 26, 27, 27, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, -35, -35, -35, -34, -34, -34, -33, -33, -33, -32, -32, -32, -31, -31, -31, -30, -30, -30, -29, -29, -29, -28, -28, -28, -27, -27, -27, -26, -26, -26, -25, -25, -25, -24, -24, -24, -23, -23, -23, -22, -22, -22, -21, -21, -21, -20, -20, -20, -19, -19, -19, -18, -18, -18, -17, -17, -17, -16, -16, -16, -15, -15, -15, -14, -14, -14, -13, -13, -13, -12, -12, -12, -11, -11, -11, -10, -10, -10, -9, -9, -9, -8, -8, -8, -7, -7, -7, -6, -6, -6, -5, -5, -5, -4, -4, -4, -3, -3, -3, -2, -2, -2, -1, -1, -1, 0, 0, -128, }; static constexpr int8_t SwingingTimeToSpriteMap_8[] = { 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29, 29, 29, 29, 29, 28, 28, 28, 28, 28, 27, 27, 27, 27, 27, 27, 27, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 0, 0, -128, }; static constexpr int8_t SwingingTimeToSpriteMap_9[] = { 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29, 29, 29, 29, 29, 28, 28, 28, 28, 28, 27, 27, 27, 27, 27, 27, 27, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 0, 0, -128, }; static constexpr int8_t SwingingTimeToSpriteMap_10[] = { 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 14, 14, 14, 14, 14, 13, 13, 13, 13, 12, 12, 12, 12, 11, 11, 11, 11, 10, 10, 10, 10, 9, 9, 9, 9, 8, 8, 8, 8, 7, 7, 7, 7, 6, 6, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 3, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 31, 31, 31, 31, 30, 30, 30, 30, 29, 29, 29, 29, 28, 28, 28, 28, 27, 27, 27, 27, 26, 26, 26, 26, 25, 25, 25, 25, 24, 24, 24, 24, 23, 23, 23, 23, 22, 22, 22, 22, 21, 21, 21, 21, 20, 20, 20, 20, 19, 19, 19, 19, 18, 18, 18, 18, 18, 18, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, 31, 31, 31, 31, 0, 0, -128, }; static constexpr int8_t SwingingTimeToSpriteMap_11[] = { 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 26, 27, 27, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31, 0, -128, }; /** rct2: 0x0099F9D0 */ static constexpr const int8_t* SwingingTimeToSpriteMaps[] = { SwingingTimeToSpriteMap_0, SwingingTimeToSpriteMap_1, SwingingTimeToSpriteMap_2, SwingingTimeToSpriteMap_3, SwingingTimeToSpriteMap_4, SwingingTimeToSpriteMap_5, SwingingTimeToSpriteMap_6, SwingingTimeToSpriteMap_7, SwingingTimeToSpriteMap_8, SwingingTimeToSpriteMap_9, SwingingTimeToSpriteMap_10, SwingingTimeToSpriteMap_11, }; struct Unk9A36C4Struct { int16_t x; int16_t y; uint32_t distance; }; /** rct2: 0x009A36C4 */ static constexpr Unk9A36C4Struct Unk9A36C4[] = { { -1, 0, 8716 }, { -1, 0, 8716 }, { -1, 0, 8716 }, { -1, 1, 12327 }, { -1, 1, 12327 }, { -1, 1, 12327 }, { 0, 1, 8716 }, { -1, 1, 12327 }, { 0, 1, 8716 }, { 0, 1, 8716 }, { 0, 1, 8716 }, { 1, 1, 12327 }, { 1, 1, 12327 }, { 1, 1, 12327 }, { 1, 0, 8716 }, { 1, 1, 12327 }, { 1, 0, 8716 }, { 1, 0, 8716 }, { 1, 0, 8716 }, { 1, -1, 12327 }, { 1, -1, 12327 }, { 1, -1, 12327 }, { 0, -1, 8716 }, { 1, -1, 12327 }, { 0, -1, 8716 }, { 0, -1, 8716 }, { 0, -1, 8716 }, { -1, -1, 12327 }, { -1, -1, 12327 }, { -1, -1, 12327 }, { -1, 0, 8716 }, { -1, -1, 12327 }, }; /** rct2: 0x009A37C4 */ static constexpr CoordsXY SurroundingTiles[] = { { 0, 0 }, { 0, +COORDS_XY_STEP }, { +COORDS_XY_STEP, 0 }, { 0, -COORDS_XY_STEP }, { 0, -COORDS_XY_STEP }, { -COORDS_XY_STEP, 0 }, { -COORDS_XY_STEP, 0 }, { 0, +COORDS_XY_STEP }, { 0, +COORDS_XY_STEP }, }; /** rct2: 0x009A37E4 */ static constexpr int32_t Unk9A37E4[] = { 2147483647, 2106585154, 1985590284, 1636362342, 1127484953, 2106585154, 1985590284, 1636362342, 1127484953, 58579923, 0, -555809667, -1073741824, -1518500249, -1859775391, -2074309916, -2147483647, 58579923, 0, -555809667, -1073741824, -1518500249, -1859775391, -2074309916, 1859775393, 1073741824, 0, -1073741824, -1859775393, 1859775393, 1073741824, 0, -1073741824, -1859775393, 1859775393, 1073741824, 0, -1073741824, -1859775393, 1859775393, 1073741824, 0, -1073741824, -1859775393, 2144540595, 2139311823, 2144540595, 2139311823, 2135719507, 2135719507, 2125953864, 2061796213, 1411702590, 2125953864, 2061796213, 1411702590, 1985590284, 1636362342, 1127484953, 2115506168, }; /** rct2: 0x009A38D4 */ static constexpr int32_t Unk9A38D4[] = { 0, 417115092, 817995863, 1390684831, 1827693544, -417115092, -817995863, -1390684831, -1827693544, 2066040965, 2147483647, 2074309916, 1859775393, 1518500249, 1073741824, 555809666, 0, -2066040965, -2147483647, -2074309916, -1859775393, -1518500249, -1073741824, -555809666, 1073741824, 1859775393, 2147483647, 1859775393, 1073741824, -1073741824, -1859775393, -2147483647, -1859775393, -1073741824, 1073741824, 1859775393, 2147483647, 1859775393, 1073741824, -1073741824, -1859775393, -2147483647, -1859775393, -1073741824, 112390610, 187165532, -112390610, -187165532, 224473165, -224473165, 303325208, 600568389, 1618265062, -303325208, -600568389, -1618265062, -817995863, -1390684831, -1827693544, 369214930, }; /** rct2: 0x009A39C4 */ static constexpr int32_t Unk9A39C4[] = { 2147483647, 2096579710, 1946281152, 2096579710, 1946281152, 1380375879, 555809667, -372906620, -1231746017, -1859775391, 1380375879, 555809667, -372906620, -1231746017, -1859775391, 0, 2096579710, 1946281152, 2096579710, 1946281152, }; static constexpr CoordsXY AvoidCollisionMoveOffset[] = { { -1, 0 }, { 0, 1 }, { 1, 0 }, { 0, -1 }, }; static constexpr OpenRCT2::Audio::SoundId DoorOpenSoundIds[] = { OpenRCT2::Audio::SoundId::DoorOpen, OpenRCT2::Audio::SoundId::Portcullis, }; static constexpr OpenRCT2::Audio::SoundId DoorCloseSoundIds[] = { OpenRCT2::Audio::SoundId::DoorClose, OpenRCT2::Audio::SoundId::Portcullis, }; template<> bool EntityBase::Is() const { return Type == EntityType::Vehicle; } #ifdef ENABLE_SCRIPTING /** * Fires the "vehicle.crash" api hook * @param vehicleId Entity id of the vehicle that just crashed * @param crashId What the vehicle crashed into. Should be either "another_vehicle", "land", or "water" */ static void InvokeVehicleCrashHook(const EntityId vehicleId, const std::string_view crashId) { auto& hookEngine = OpenRCT2::GetContext()->GetScriptEngine().GetHookEngine(); if (hookEngine.HasSubscriptions(OpenRCT2::Scripting::HOOK_TYPE::VEHICLE_CRASH)) { auto ctx = OpenRCT2::GetContext()->GetScriptEngine().GetContext(); // Create event args object auto obj = OpenRCT2::Scripting::DukObject(ctx); obj.Set("id", vehicleId.ToUnderlying()); obj.Set("crashIntoType", crashId); // Call the subscriptions auto e = obj.Take(); hookEngine.Call(OpenRCT2::Scripting::HOOK_TYPE::VEHICLE_CRASH, e, true); } } #endif static bool vehicle_move_info_valid( VehicleTrackSubposition trackSubposition, track_type_t type, uint8_t direction, int32_t offset) { uint16_t typeAndDirection = (type << 2) | (direction & 3); if (trackSubposition >= VehicleTrackSubposition{ std::size(gTrackVehicleInfo) }) { return false; } int32_t size = 0; switch (trackSubposition) { case VehicleTrackSubposition::Default: size = VehicleTrackSubpositionSizeDefault; break; case VehicleTrackSubposition::ChairliftGoingOut: size = 692; break; case VehicleTrackSubposition::ChairliftGoingBack: case VehicleTrackSubposition::ChairliftEndBullwheel: case VehicleTrackSubposition::ChairliftStartBullwheel: size = 404; break; case VehicleTrackSubposition::GoKartsLeftLane: case VehicleTrackSubposition::GoKartsRightLane: case VehicleTrackSubposition::GoKartsMovingToRightLane: case VehicleTrackSubposition::GoKartsMovingToLeftLane: size = 208; break; case VehicleTrackSubposition::MiniGolfPathA9: // VehicleTrackSubposition::MiniGolfStart9 case VehicleTrackSubposition::MiniGolfBallPathA10: case VehicleTrackSubposition::MiniGolfPathB11: case VehicleTrackSubposition::MiniGolfBallPathB12: case VehicleTrackSubposition::MiniGolfPathC13: case VehicleTrackSubposition::MiniGolfBallPathC14: size = 824; break; case VehicleTrackSubposition::ReverserRCFrontBogie: case VehicleTrackSubposition::ReverserRCRearBogie: size = 868; break; default: break; } if (typeAndDirection >= size) { return false; } if (offset >= gTrackVehicleInfo[EnumValue(trackSubposition)][typeAndDirection]->size) { return false; } return true; } static const VehicleInfo* vehicle_get_move_info( VehicleTrackSubposition trackSubposition, track_type_t type, uint8_t direction, int32_t offset) { uint16_t typeAndDirection = (type << 2) | (direction & 3); if (!vehicle_move_info_valid(trackSubposition, type, direction, offset)) { static constexpr VehicleInfo zero = {}; return &zero; } return &gTrackVehicleInfo[EnumValue(trackSubposition)][typeAndDirection]->info[offset]; } const VehicleInfo* Vehicle::GetMoveInfo() const { return vehicle_get_move_info(TrackSubposition, GetTrackType(), GetTrackDirection(), track_progress); } uint16_t VehicleGetMoveInfoSize(VehicleTrackSubposition trackSubposition, track_type_t type, uint8_t direction) { uint16_t typeAndDirection = (type << 2) | (direction & 3); if (!vehicle_move_info_valid(trackSubposition, type, direction, 0)) { return 0; } return gTrackVehicleInfo[EnumValue(trackSubposition)][typeAndDirection]->size; } uint16_t Vehicle::GetTrackProgress() const { return VehicleGetMoveInfoSize(TrackSubposition, GetTrackType(), GetTrackDirection()); } void Vehicle::ApplyMass(int16_t appliedMass) { mass = std::clamp(mass + appliedMass, 1, std::numeric_limits::max()); } void Vehicle::MoveRelativeDistance(int32_t distance) { remaining_distance += distance; SetFlag(VehicleFlags::MoveSingleCar | VehicleFlags::CollisionDisabled); UpdateTrackMotion(nullptr); ClearFlag(VehicleFlags::MoveSingleCar | VehicleFlags::CollisionDisabled); } Vehicle* TryGetVehicle(EntityId spriteIndex) { return TryGetEntity(spriteIndex); } void VehicleSoundsUpdate() { auto windowManager = OpenRCT2::GetContext()->GetUiContext()->GetWindowManager(); windowManager->BroadcastIntent(Intent(INTENT_ACTION_UPDATE_VEHICLE_SOUNDS)); } /** * * rct2: 0x006D4204 */ void VehicleUpdateAll() { PROFILED_FUNCTION(); if (gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) return; if ((gScreenFlags & SCREEN_FLAGS_TRACK_DESIGNER) && GetGameState().EditorStep != EditorStep::RollercoasterDesigner) return; for (auto vehicle : TrainManager::View()) { vehicle->Update(); } } /** * * rct2: 0x006D6956 * @returns true when all closed */ bool Vehicle::CloseRestraints() { auto curRide = GetRide(); if (curRide == nullptr) return true; bool restraintsClosed = true; for (Vehicle* vehicle = GetEntity(Id); vehicle != nullptr; vehicle = GetEntity(vehicle->next_vehicle_on_train)) { if (vehicle->HasFlag(VehicleFlags::CarIsBroken) && vehicle->restraints_position != 0 && (curRide->breakdown_reason_pending == BREAKDOWN_RESTRAINTS_STUCK_OPEN || curRide->breakdown_reason_pending == BREAKDOWN_DOORS_STUCK_OPEN)) { if (!(curRide->lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN)) { curRide->lifecycle_flags |= RIDE_LIFECYCLE_BROKEN_DOWN; RideBreakdownAddNewsItem(*curRide); curRide->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAIN | RIDE_INVALIDATE_RIDE_LIST | RIDE_INVALIDATE_RIDE_MAINTENANCE; curRide->mechanic_status = RIDE_MECHANIC_STATUS_CALLING; Vehicle* broken_vehicle = GetEntity(curRide->vehicles[curRide->broken_vehicle]); if (broken_vehicle != nullptr) { curRide->inspection_station = broken_vehicle->current_station; } curRide->breakdown_reason = curRide->breakdown_reason_pending; } } else { vehicle->restraints_position = std::max(vehicle->restraints_position - 20, 0); if (vehicle->restraints_position == 0) { continue; } } vehicle->Invalidate(); restraintsClosed = false; } return restraintsClosed; } /** * * rct2: 0x006D6A2C * @returns true when all open */ bool Vehicle::OpenRestraints() { int32_t restraintsOpen = true; for (Vehicle* vehicle = GetEntity(Id); vehicle != nullptr; vehicle = GetEntity(vehicle->next_vehicle_on_train)) { vehicle->SwingPosition = 0; vehicle->SwingSpeed = 0; vehicle->SwingSprite = 0; auto curRide = vehicle->GetRide(); if (curRide == nullptr) continue; auto rideEntry = vehicle->GetRideEntry(); if (rideEntry == nullptr) { continue; } const auto& carEntry = rideEntry->Cars[vehicle->vehicle_type]; if (carEntry.flags & CAR_ENTRY_FLAG_SPINNING) { // If the vehicle is a spinner it must be spinning slow // For vehicles without additional frames there are 4 rotations it can unload from // For vehicles with additional frames it must be facing forward if (abs(vehicle->spin_speed) <= VEHICLE_MAX_SPIN_SPEED_FOR_STOPPING && !(vehicle->spin_sprite & 0x30) && (!(carEntry.flags & CAR_ENTRY_FLAG_SPINNING_ADDITIONAL_FRAMES) || !(vehicle->spin_sprite & 0xF8))) { vehicle->spin_speed = 0; } else { restraintsOpen = false; if (abs(vehicle->spin_speed) < VEHICLE_STOPPING_SPIN_SPEED) { // Note will look odd if spinning right. vehicle->spin_speed = VEHICLE_STOPPING_SPIN_SPEED; } int16_t value = vehicle->spin_speed / 256; vehicle->spin_sprite += value; vehicle->spin_speed -= value; vehicle->Invalidate(); continue; } } if (carEntry.animation == CarEntryAnimation::ObservationTower && vehicle->animation_frame != 0) { if (vehicle->animationState <= 0xCCCC) { vehicle->animationState += carEntry.AnimationSpeed; } else { vehicle->animationState = 0; vehicle->animation_frame++; vehicle->animation_frame %= carEntry.AnimationFrames; vehicle->Invalidate(); } restraintsOpen = false; continue; } if (carEntry.animation == CarEntryAnimation::AnimalFlying && (vehicle->animation_frame != 0 || vehicle->animationState > 0)) { vehicle->UpdateAnimationAnimalFlying(); restraintsOpen = false; continue; } if (vehicle->HasFlag(VehicleFlags::CarIsBroken) && vehicle->restraints_position != 0xFF && (curRide->breakdown_reason_pending == BREAKDOWN_RESTRAINTS_STUCK_CLOSED || curRide->breakdown_reason_pending == BREAKDOWN_DOORS_STUCK_CLOSED)) { if (!(curRide->lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN)) { curRide->lifecycle_flags |= RIDE_LIFECYCLE_BROKEN_DOWN; RideBreakdownAddNewsItem(*curRide); curRide->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAIN | RIDE_INVALIDATE_RIDE_LIST | RIDE_INVALIDATE_RIDE_MAINTENANCE; curRide->mechanic_status = RIDE_MECHANIC_STATUS_CALLING; Vehicle* broken_vehicle = GetEntity(curRide->vehicles[curRide->broken_vehicle]); if (broken_vehicle != nullptr) { curRide->inspection_station = broken_vehicle->current_station; } curRide->breakdown_reason = curRide->breakdown_reason_pending; } } else { if (vehicle->restraints_position + 20 > 0xFF) { vehicle->restraints_position = 255; continue; } vehicle->restraints_position += 20; } vehicle->Invalidate(); restraintsOpen = false; } return restraintsOpen; } void RideUpdateMeasurementsSpecialElements_Default(Ride& ride, const track_type_t trackType) { const auto& ted = GetTrackElementDescriptor(trackType); uint16_t trackFlags = ted.Flags; if (trackFlags & TRACK_ELEM_FLAG_NORMAL_TO_INVERSION) { if (ride.inversions < OpenRCT2::Limits::MaxInversions) ride.inversions++; } } void RideUpdateMeasurementsSpecialElements_MiniGolf(Ride& ride, const track_type_t trackType) { const auto& ted = GetTrackElementDescriptor(trackType); uint16_t trackFlags = ted.Flags; if (trackFlags & TRACK_ELEM_FLAG_IS_GOLF_HOLE) { if (ride.holes < OpenRCT2::Limits::MaxGolfHoles) ride.holes++; } } void RideUpdateMeasurementsSpecialElements_WaterCoaster(Ride& ride, const track_type_t trackType) { if (trackType >= TrackElemType::FlatCovered && trackType <= TrackElemType::RightQuarterTurn3TilesCovered) { ride.special_track_elements |= RIDE_ELEMENT_TUNNEL_SPLASH_OR_RAPIDS; } } /** * * rct2: 0x006D6D1F */ void Vehicle::UpdateMeasurements() { auto curRide = GetRide(); if (curRide == nullptr) return; if (status == Vehicle::Status::TravellingBoat) { curRide->lifecycle_flags |= RIDE_LIFECYCLE_TESTED; curRide->lifecycle_flags |= RIDE_LIFECYCLE_NO_RAW_STATS; curRide->lifecycle_flags &= ~RIDE_LIFECYCLE_TEST_IN_PROGRESS; ClearFlag(VehicleFlags::Testing); WindowInvalidateByNumber(WindowClass::Ride, ride.ToUnderlying()); return; } if (curRide->current_test_station.IsNull()) return; const auto& currentStation = curRide->GetStation(curRide->current_test_station); if (!currentStation.Entrance.IsNull()) { uint8_t test_segment = curRide->current_test_segment; StationIndex stationIndex = StationIndex::FromUnderlying(test_segment); auto& stationForTestSegment = curRide->GetStation(stationIndex); curRide->average_speed_test_timeout++; if (curRide->average_speed_test_timeout >= 32) curRide->average_speed_test_timeout = 0; int32_t absVelocity = abs(velocity); if (absVelocity > curRide->max_speed) { curRide->max_speed = absVelocity; } if (curRide->average_speed_test_timeout == 0 && absVelocity > 0x8000) { curRide->average_speed = AddClamp_int32_t(curRide->average_speed, absVelocity); stationForTestSegment.SegmentTime++; } int32_t distance = abs(((velocity + acceleration) >> 10) * 42); if (NumLaps == 0) { stationForTestSegment.SegmentLength = AddClamp_int32_t(stationForTestSegment.SegmentLength, distance); } if (curRide->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_HAS_G_FORCES)) { auto gForces = GetGForces(); gForces.VerticalG += curRide->previous_vertical_g; gForces.LateralG += curRide->previous_lateral_g; gForces.VerticalG /= 2; gForces.LateralG /= 2; curRide->previous_vertical_g = gForces.VerticalG; curRide->previous_lateral_g = gForces.LateralG; if (gForces.VerticalG <= 0) { curRide->total_air_time++; } if (gForces.VerticalG > curRide->max_positive_vertical_g) curRide->max_positive_vertical_g = gForces.VerticalG; if (gForces.VerticalG < curRide->max_negative_vertical_g) curRide->max_negative_vertical_g = gForces.VerticalG; gForces.LateralG = std::abs(gForces.LateralG); curRide->max_lateral_g = std::max(curRide->max_lateral_g, static_cast(gForces.LateralG)); } } // If we have already evaluated this track piece skip to next section TileCoordsXYZ curTrackLoc{ TrackLocation }; if (curTrackLoc != curRide->CurTestTrackLocation) { curRide->CurTestTrackLocation = curTrackLoc; if (currentStation.Entrance.IsNull()) return; auto trackElemType = GetTrackType(); if (trackElemType == TrackElemType::PoweredLift || HasFlag(VehicleFlags::OnLiftHill)) { if (!(curRide->testing_flags & RIDE_TESTING_POWERED_LIFT)) { curRide->testing_flags |= RIDE_TESTING_POWERED_LIFT; if (curRide->drops + 64 < 0xFF) { curRide->drops += 64; } } } else { curRide->testing_flags &= ~RIDE_TESTING_POWERED_LIFT; } const auto& rtd = curRide->GetRideTypeDescriptor(); rtd.UpdateMeasurementsSpecialElements(*curRide, trackElemType); switch (trackElemType) { case TrackElemType::Rapids: case TrackElemType::SpinningTunnel: curRide->special_track_elements |= RIDE_ELEMENT_TUNNEL_SPLASH_OR_RAPIDS; break; case TrackElemType::Waterfall: case TrackElemType::LogFlumeReverser: curRide->special_track_elements |= RIDE_ELEMENT_REVERSER_OR_WATERFALL; break; case TrackElemType::Whirlpool: curRide->special_track_elements |= RIDE_ELEMENT_WHIRLPOOL; break; case TrackElemType::Watersplash: if (velocity >= 11.0_mph) { curRide->special_track_elements |= RIDE_ELEMENT_TUNNEL_SPLASH_OR_RAPIDS; } } const auto& ted = GetTrackElementDescriptor(trackElemType); uint16_t trackFlags = ted.Flags; uint32_t testingFlags = curRide->testing_flags; if (testingFlags & RIDE_TESTING_TURN_LEFT && trackFlags & TRACK_ELEM_FLAG_TURN_LEFT) { // 0x800 as this is masked to kCurrentTurnCountMask curRide->turn_count_default += 0x800; } else if (testingFlags & RIDE_TESTING_TURN_RIGHT && trackFlags & TRACK_ELEM_FLAG_TURN_RIGHT) { // 0x800 as this is masked to kCurrentTurnCountMask curRide->turn_count_default += 0x800; } else if (testingFlags & RIDE_TESTING_TURN_RIGHT || testingFlags & RIDE_TESTING_TURN_LEFT) { curRide->testing_flags &= ~( RIDE_TESTING_TURN_LEFT | RIDE_TESTING_TURN_RIGHT | RIDE_TESTING_TURN_BANKED | RIDE_TESTING_TURN_SLOPED); uint8_t turnType = 1; if (!(testingFlags & RIDE_TESTING_TURN_BANKED)) { turnType = 2; if (!(testingFlags & RIDE_TESTING_TURN_SLOPED)) { turnType = 0; } } switch (curRide->turn_count_default >> 11) { case 0: IncrementTurnCount1Element(*curRide, turnType); break; case 1: IncrementTurnCount2Elements(*curRide, turnType); break; case 2: IncrementTurnCount3Elements(*curRide, turnType); break; default: IncrementTurnCount4PlusElements(*curRide, turnType); break; } } else { if (trackFlags & TRACK_ELEM_FLAG_TURN_LEFT) { curRide->testing_flags |= RIDE_TESTING_TURN_LEFT; curRide->turn_count_default &= ~kCurrentTurnCountMask; if (trackFlags & TRACK_ELEM_FLAG_TURN_BANKED) { curRide->testing_flags |= RIDE_TESTING_TURN_BANKED; } if (trackFlags & TRACK_ELEM_FLAG_TURN_SLOPED) { curRide->testing_flags |= RIDE_TESTING_TURN_SLOPED; } } if (trackFlags & TRACK_ELEM_FLAG_TURN_RIGHT) { curRide->testing_flags |= RIDE_TESTING_TURN_RIGHT; curRide->turn_count_default &= ~kCurrentTurnCountMask; if (trackFlags & TRACK_ELEM_FLAG_TURN_BANKED) { curRide->testing_flags |= RIDE_TESTING_TURN_BANKED; } if (trackFlags & TRACK_ELEM_FLAG_TURN_SLOPED) { curRide->testing_flags |= RIDE_TESTING_TURN_SLOPED; } } } if (testingFlags & RIDE_TESTING_DROP_DOWN) { if (velocity < 0 || !(trackFlags & TRACK_ELEM_FLAG_DOWN)) { curRide->testing_flags &= ~RIDE_TESTING_DROP_DOWN; int16_t curZ = z / COORDS_Z_STEP - curRide->start_drop_height; if (curZ < 0) { curZ = abs(curZ); if (curZ > curRide->highest_drop_height) { curRide->highest_drop_height = static_cast(curZ); } } } } else if (trackFlags & TRACK_ELEM_FLAG_DOWN && velocity >= 0) { curRide->testing_flags &= ~RIDE_TESTING_DROP_UP; curRide->testing_flags |= RIDE_TESTING_DROP_DOWN; uint8_t drops = curRide->drops & 0x3F; if (drops != 0x3F) drops++; curRide->drops &= ~0x3F; curRide->drops |= drops; curRide->start_drop_height = z / COORDS_Z_STEP; testingFlags &= ~RIDE_TESTING_DROP_UP; } if (testingFlags & RIDE_TESTING_DROP_UP) { if (velocity > 0 || !(trackFlags & TRACK_ELEM_FLAG_UP)) { curRide->testing_flags &= ~RIDE_TESTING_DROP_UP; int16_t curZ = z / COORDS_Z_STEP - curRide->start_drop_height; if (curZ < 0) { curZ = abs(curZ); if (curZ > curRide->highest_drop_height) { curRide->highest_drop_height = static_cast(curZ); } } } } else if (trackFlags & TRACK_ELEM_FLAG_UP && velocity <= 0) { curRide->testing_flags &= ~RIDE_TESTING_DROP_DOWN; curRide->testing_flags |= RIDE_TESTING_DROP_UP; uint8_t drops = curRide->drops & 0x3F; if (drops != 0x3F) drops++; curRide->drops &= ~0x3F; curRide->drops |= drops; curRide->start_drop_height = z / COORDS_Z_STEP; } if (trackFlags & TRACK_ELEM_FLAG_HELIX) { uint8_t helixes = RideGetHelixSections(*curRide); if (helixes != OpenRCT2::Limits::MaxHelices) helixes++; curRide->special_track_elements &= ~0x1F; curRide->special_track_elements |= helixes; } } if (currentStation.Entrance.IsNull()) return; if (x == LOCATION_NULL) { curRide->testing_flags &= ~RIDE_TESTING_SHELTERED; return; } auto surfaceElement = MapGetSurfaceElementAt(CoordsXY{ x, y }); // If vehicle above ground. if (surfaceElement != nullptr && surfaceElement->GetBaseZ() <= z) { // Set tile_element to first element. Since elements aren't always ordered by base height, // we must start at the first element and iterate through each tile element. auto tileElement = MapGetFirstElementAt(CoordsXY{ x, y }); if (tileElement == nullptr) return; bool coverFound = false; do { // If the tile_element is lower than the vehicle, continue (don't set flag) if (tileElement->GetBaseZ() <= z) continue; if (tileElement->GetType() == TileElementType::LargeScenery) { coverFound = true; break; } if (tileElement->GetType() == TileElementType::Path) { coverFound = true; break; } if (tileElement->GetType() != TileElementType::SmallScenery) continue; auto* sceneryEntry = tileElement->AsSmallScenery()->GetEntry(); if (sceneryEntry == nullptr) continue; if (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_FULL_TILE)) { coverFound = true; break; } // Iterate through each tile_element. } while (!(tileElement++)->IsLastForTile()); if (!coverFound) { curRide->testing_flags &= ~RIDE_TESTING_SHELTERED; return; } } if (!(curRide->testing_flags & RIDE_TESTING_SHELTERED)) { curRide->testing_flags |= RIDE_TESTING_SHELTERED; curRide->IncreaseNumShelteredSections(); if (Pitch != 0) { curRide->num_sheltered_sections |= ShelteredSectionsBits::RotatingWhileSheltered; } if (bank_rotation != 0) { curRide->num_sheltered_sections |= ShelteredSectionsBits::BankingWhileSheltered; } } int32_t distance = ((velocity + acceleration) >> 10) * 42; if (distance < 0) return; curRide->sheltered_length = AddClamp_int32_t(curRide->sheltered_length, distance); } struct SoundIdVolume { OpenRCT2::Audio::SoundId id; uint8_t volume; }; static SoundIdVolume Sub6D7AC0( OpenRCT2::Audio::SoundId currentSoundId, uint8_t currentVolume, OpenRCT2::Audio::SoundId targetSoundId, uint8_t targetVolume) { if (currentSoundId != OpenRCT2::Audio::SoundId::Null) { if (currentSoundId == targetSoundId) { currentVolume = std::min(currentVolume + 15, targetVolume); return { currentSoundId, currentVolume }; } currentVolume -= 9; if (currentVolume >= 80) return { currentSoundId, currentVolume }; } // Begin sound at quarter volume currentSoundId = targetSoundId; currentVolume = targetVolume == 255 ? 255 : targetVolume / 4; return { currentSoundId, currentVolume }; } void Vehicle::GetLiftHillSound(const Ride& curRide, SoundIdVolume& curSound) { scream_sound_id = OpenRCT2::Audio::SoundId::Null; if (curRide.type < std::size(RideTypeDescriptors)) { // Get lift hill sound curSound.id = GetRideTypeDescriptor(curRide.type).LiftData.sound_id; curSound.volume = 243; if (!(sound2_flags & VEHICLE_SOUND2_FLAGS_LIFT_HILL)) curSound.id = OpenRCT2::Audio::SoundId::Null; } } /** * * rct2: 0x006D77F2 */ void Vehicle::Update() { if (IsCableLift()) { CableLiftUpdate(); return; } auto rideEntry = GetRideEntry(); if (rideEntry == nullptr) return; auto curRide = GetRide(); if (curRide == nullptr) return; if (curRide->type >= RIDE_TYPE_COUNT) return; if (HasFlag(VehicleFlags::Testing)) UpdateMeasurements(); _vehicleBreakdown = 255; if (curRide->lifecycle_flags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN)) { _vehicleBreakdown = curRide->breakdown_reason_pending; auto carEntry = &rideEntry->Cars[vehicle_type]; if ((carEntry->flags & CAR_ENTRY_FLAG_POWERED) && curRide->breakdown_reason_pending == BREAKDOWN_SAFETY_CUT_OUT) { if (!(carEntry->flags & CAR_ENTRY_FLAG_WATER_RIDE) || (Pitch == 2 && velocity <= 2.0_mph)) { SetFlag(VehicleFlags::StoppedOnLift); } } } switch (status) { case Vehicle::Status::MovingToEndOfStation: UpdateMovingToEndOfStation(); break; case Vehicle::Status::WaitingForPassengers: UpdateWaitingForPassengers(); break; case Vehicle::Status::WaitingToDepart: UpdateWaitingToDepart(); break; case Vehicle::Status::Crashing: case Vehicle::Status::Crashed: UpdateCrash(); break; case Vehicle::Status::TravellingDodgems: UpdateDodgemsMode(); break; case Vehicle::Status::Swinging: UpdateSwinging(); break; case Vehicle::Status::SimulatorOperating: UpdateSimulatorOperating(); break; case Vehicle::Status::TopSpinOperating: UpdateTopSpinOperating(); break; case Vehicle::Status::FerrisWheelRotating: UpdateFerrisWheelRotating(); break; case Vehicle::Status::SpaceRingsOperating: UpdateSpaceRingsOperating(); break; case Vehicle::Status::HauntedHouseOperating: UpdateHauntedHouseOperating(); break; case Vehicle::Status::CrookedHouseOperating: UpdateCrookedHouseOperating(); break; case Vehicle::Status::Rotating: UpdateRotating(); break; case Vehicle::Status::Departing: UpdateDeparting(); break; case Vehicle::Status::Travelling: UpdateTravelling(); break; case Vehicle::Status::TravellingCableLift: UpdateTravellingCableLift(); break; case Vehicle::Status::TravellingBoat: UpdateTravellingBoat(); break; case Vehicle::Status::Arriving: UpdateArriving(); break; case Vehicle::Status::UnloadingPassengers: UpdateUnloadingPassengers(); break; case Vehicle::Status::WaitingForCableLift: UpdateWaitingForCableLift(); break; case Vehicle::Status::ShowingFilm: UpdateShowingFilm(); break; case Vehicle::Status::DoingCircusShow: UpdateDoingCircusShow(); default: break; } UpdateSound(); } /** * * rct2: 0x006D7BCC */ void Vehicle::UpdateMovingToEndOfStation() { auto curRide = GetRide(); if (curRide == nullptr) return; int32_t curFlags = 0; int32_t station = 0; switch (curRide->mode) { case RideMode::UpwardLaunch: case RideMode::RotatingLift: case RideMode::DownwardLaunch: case RideMode::FreefallDrop: if (velocity >= -131940) { acceleration = -3298; } if (velocity < -131940) { velocity -= velocity / 16; acceleration = 0; } curFlags = UpdateTrackMotion(&station); if (!(curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_5)) break; [[fallthrough]]; case RideMode::Dodgems: case RideMode::Swing: case RideMode::Rotation: case RideMode::ForwardRotation: case RideMode::BackwardRotation: case RideMode::FilmAvengingAviators: case RideMode::FilmThrillRiders: case RideMode::Beginners: case RideMode::Intense: case RideMode::Berserk: case RideMode::MouseTails3DFilm: case RideMode::StormChasers3DFilm: case RideMode::SpaceRaiders3DFilm: case RideMode::SpaceRings: case RideMode::HauntedHouse: case RideMode::CrookedHouse: case RideMode::Circus: current_station = StationIndex::FromUnderlying(0); velocity = 0; acceleration = 0; SetState(Vehicle::Status::WaitingForPassengers); break; default: { const auto* rideEntry = GetRideEntry(); if (rideEntry == nullptr) { return; } const auto& carEntry = rideEntry->Cars[vehicle_type]; if (!(carEntry.flags & CAR_ENTRY_FLAG_POWERED)) { if (velocity <= 131940) { acceleration = 3298; } } if (velocity > 131940) { velocity -= velocity / 16; acceleration = 0; } curFlags = UpdateTrackMotion(&station); if (curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_1) { velocity = 0; acceleration = 0; sub_state++; if (curRide->mode == RideMode::Race && sub_state >= 40) { SetState(Vehicle::Status::WaitingForPassengers); break; } } else { if (velocity > 98955) { sub_state = 0; } } if (!(curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_AT_STATION)) break; current_station = StationIndex::FromUnderlying(station); velocity = 0; acceleration = 0; SetState(Vehicle::Status::WaitingForPassengers); break; } } } /** * * rct2: 0x006D7FB4 */ void Vehicle::TrainReadyToDepart(uint8_t num_peeps_on_train, uint8_t num_used_seats) { if (num_peeps_on_train != num_used_seats) return; auto curRide = GetRide(); if (curRide == nullptr) return; if (curRide->status == RideStatus::Open && !(curRide->lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN) && !HasFlag(VehicleFlags::ReadyToDepart)) { return; } if (!(curRide->lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN)) { // Original code did not check if the ride was a boat hire, causing empty boats to leave the platform when closing a // Boat Hire with passengers on it. if (curRide->status != RideStatus::Closed || (curRide->num_riders != 0 && curRide->type != RIDE_TYPE_BOAT_HIRE)) { curRide->GetStation(current_station).TrainAtStation = RideStation::kNoTrain; sub_state = 2; return; } } if (curRide->mode == RideMode::ForwardRotation || curRide->mode == RideMode::BackwardRotation) { uint8_t seat = ((-Pitch) / 8) & 0xF; if (!peep[seat].IsNull()) { curRide->GetStation(current_station).TrainAtStation = RideStation::kNoTrain; SetState(Vehicle::Status::UnloadingPassengers); return; } if (num_peeps == 0) return; curRide->GetStation(current_station).TrainAtStation = RideStation::kNoTrain; sub_state = 2; return; } if (num_peeps_on_train == 0) return; curRide->GetStation(current_station).TrainAtStation = RideStation::kNoTrain; SetState(Vehicle::Status::WaitingForPassengers); } static std::optional ride_get_train_index_from_vehicle(const Ride& ride, EntityId spriteIndex) { uint32_t trainIndex = 0; while (ride.vehicles[trainIndex] != spriteIndex) { trainIndex++; if (trainIndex >= ride.NumTrains) { // This should really return nullopt, but doing so // would break some hacked parks that hide track by setting tracked rides' // track type to, e.g., Crooked House break; } if (trainIndex >= std::size(ride.vehicles)) { return std::nullopt; } } return { trainIndex }; } /** * * rct2: 0x006D7DA1 */ void Vehicle::UpdateWaitingForPassengers() { velocity = 0; auto curRide = GetRide(); if (curRide == nullptr) return; if (sub_state == 0) { if (!OpenRestraints()) return; auto& station = curRide->GetStation(current_station); if (station.Entrance.IsNull()) { station.TrainAtStation = RideStation::kNoTrain; sub_state = 2; return; } auto trainIndex = ride_get_train_index_from_vehicle(*curRide, Id); if (!trainIndex.has_value()) { return; } if (station.TrainAtStation != RideStation::kNoTrain) return; station.TrainAtStation = trainIndex.value(); sub_state = 1; time_waiting = 0; Invalidate(); return; } if (sub_state == 1) { if (time_waiting != 0xFFFF) time_waiting++; ClearFlag(VehicleFlags::ReadyToDepart); // 0xF64E31, 0xF64E32, 0xF64E33 uint8_t num_peeps_on_train = 0, num_used_seats_on_train = 0, num_seats_on_train = 0; for (const Vehicle* trainCar = GetEntity(Id); trainCar != nullptr; trainCar = GetEntity(trainCar->next_vehicle_on_train)) { num_peeps_on_train += trainCar->num_peeps; num_used_seats_on_train += trainCar->next_free_seat; num_seats_on_train += trainCar->num_seats; } num_seats_on_train &= 0x7F; if (curRide->SupportsStatus(RideStatus::Testing)) { if (time_waiting < 20) { TrainReadyToDepart(num_peeps_on_train, num_used_seats_on_train); return; } } else { if (num_peeps_on_train == 0) { TrainReadyToDepart(num_peeps_on_train, num_used_seats_on_train); return; } } if (curRide->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_HAS_LOAD_OPTIONS)) { if (curRide->depart_flags & RIDE_DEPART_WAIT_FOR_MINIMUM_LENGTH) { if (curRide->min_waiting_time * 32 > time_waiting) { TrainReadyToDepart(num_peeps_on_train, num_used_seats_on_train); return; } } if (curRide->depart_flags & RIDE_DEPART_WAIT_FOR_MAXIMUM_LENGTH) { if (curRide->max_waiting_time * 32 < time_waiting) { SetFlag(VehicleFlags::ReadyToDepart); TrainReadyToDepart(num_peeps_on_train, num_used_seats_on_train); return; } } } if (curRide->depart_flags & RIDE_DEPART_LEAVE_WHEN_ANOTHER_ARRIVES) { for (auto train_id : curRide->vehicles) { if (train_id == Id) continue; Vehicle* train = GetEntity(train_id); if (train == nullptr) continue; if (train->status == Vehicle::Status::UnloadingPassengers || train->status == Vehicle::Status::MovingToEndOfStation) { if (train->current_station == current_station) { SetFlag(VehicleFlags::ReadyToDepart); TrainReadyToDepart(num_peeps_on_train, num_used_seats_on_train); return; } } } } if (curRide->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_HAS_LOAD_OPTIONS) && curRide->depart_flags & RIDE_DEPART_WAIT_FOR_LOAD) { if (num_peeps_on_train == num_seats_on_train) { SetFlag(VehicleFlags::ReadyToDepart); TrainReadyToDepart(num_peeps_on_train, num_used_seats_on_train); return; } // any load: load=4 , full: load=3 , 3/4s: load=2 , half: load=1 , quarter: load=0 uint8_t load = curRide->depart_flags & RIDE_DEPART_WAIT_FOR_LOAD_MASK; // We want to wait for ceiling((load+1)/4 * num_seats_on_train) peeps, the +3 below is used instead of // ceil() to prevent issues on different cpus/platforms with floats. Note that vanilla RCT1/2 rounded // down here; our change reflects the expected behaviour for waiting for a minimum load target (see #9987) uint8_t peepTarget = ((load + 1) * num_seats_on_train + 3) / 4; if (load == 4) // take care of "any load" special case peepTarget = 1; if (num_peeps_on_train >= peepTarget) SetFlag(VehicleFlags::ReadyToDepart); TrainReadyToDepart(num_peeps_on_train, num_used_seats_on_train); return; } SetFlag(VehicleFlags::ReadyToDepart); TrainReadyToDepart(num_peeps_on_train, num_used_seats_on_train); return; } if (!CloseRestraints()) return; velocity = 0; ClearFlag(VehicleFlags::WaitingOnAdjacentStation); if (curRide->depart_flags & RIDE_DEPART_SYNCHRONISE_WITH_ADJACENT_STATIONS) { SetFlag(VehicleFlags::WaitingOnAdjacentStation); } SetState(Vehicle::Status::WaitingToDepart); } /** * * rct2: 0x006D91BF */ void Vehicle::UpdateDodgemsMode() { auto curRide = GetRide(); if (curRide == nullptr) return; const auto* rideEntry = GetRideEntry(); if (rideEntry == nullptr) { return; } const auto& carEntry = rideEntry->Cars[vehicle_type]; // Mark the dodgem as in use. if (carEntry.flags & CAR_ENTRY_FLAG_DODGEM_INUSE_LIGHTS && animation_frame != 1) { animation_frame = 1; Invalidate(); } UpdateMotionDodgems(); // Update the length of time vehicle has been in dodgems mode if (sub_state++ == 0xFF) { TimeActive++; } if (curRide->lifecycle_flags & RIDE_LIFECYCLE_PASS_STATION_NO_STOPPING) return; // Mark the dodgem as not in use. animation_frame = 0; Invalidate(); velocity = 0; acceleration = 0; SetState(Vehicle::Status::UnloadingPassengers); } /** * * rct2: 0x006D80BE */ void Vehicle::UpdateWaitingToDepart() { auto* curRide = GetRide(); if (curRide == nullptr) return; const auto& currentStation = curRide->GetStation(current_station); bool shouldBreak = false; if (curRide->lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN) { switch (curRide->breakdown_reason_pending) { case BREAKDOWN_RESTRAINTS_STUCK_CLOSED: case BREAKDOWN_RESTRAINTS_STUCK_OPEN: case BREAKDOWN_DOORS_STUCK_CLOSED: case BREAKDOWN_DOORS_STUCK_OPEN: break; default: shouldBreak = true; break; } } bool skipCheck = false; if (shouldBreak || curRide->status != RideStatus::Open) { if (curRide->mode == RideMode::ForwardRotation || curRide->mode == RideMode::BackwardRotation) { uint8_t seat = ((-Pitch) >> 3) & 0xF; if (peep[seat * 2].IsNull()) { if (num_peeps == 0) { skipCheck = true; } } else { if (!currentStation.Exit.IsNull()) { SetState(Vehicle::Status::UnloadingPassengers); return; } } } else { for (const Vehicle* trainCar = GetEntity(Id); trainCar != nullptr; trainCar = GetEntity(trainCar->next_vehicle_on_train)) { if (trainCar->num_peeps != 0) { if (!currentStation.Exit.IsNull()) { SetState(Vehicle::Status::UnloadingPassengers); return; } break; } } } } if (!skipCheck) { if (!(currentStation.Depart & kStationDepartFlag)) return; } if (curRide->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_CAN_SYNCHRONISE_ADJACENT_STATIONS)) { if (curRide->depart_flags & RIDE_DEPART_SYNCHRONISE_WITH_ADJACENT_STATIONS) { if (HasFlag(VehicleFlags::WaitingOnAdjacentStation)) { if (!CanDepartSynchronised()) { return; } } } } SetState(Vehicle::Status::Departing); if (curRide->lifecycle_flags & RIDE_LIFECYCLE_CABLE_LIFT) { CoordsXYE track; int32_t zUnused; int32_t direction; uint8_t trackDirection = GetTrackDirection(); if (TrackBlockGetNextFromZero(TrackLocation, *curRide, trackDirection, &track, &zUnused, &direction, false)) { if (track.element->AsTrack()->HasCableLift()) { SetState(Vehicle::Status::WaitingForCableLift, sub_state); } } } switch (curRide->mode) { case RideMode::Dodgems: // Dodgems mode uses sub_state and TimeActive to tell how long // the vehicle has been ridden. SetState(Vehicle::Status::TravellingDodgems); TimeActive = 0; UpdateDodgemsMode(); break; case RideMode::Swing: SetState(Vehicle::Status::Swinging); NumSwings = 0; current_time = -1; UpdateSwinging(); break; case RideMode::Rotation: SetState(Vehicle::Status::Rotating); NumRotations = 0; current_time = -1; UpdateRotating(); break; case RideMode::FilmAvengingAviators: SetState(Vehicle::Status::SimulatorOperating); current_time = -1; UpdateSimulatorOperating(); break; case RideMode::FilmThrillRiders: SetState(Vehicle::Status::SimulatorOperating, 1); current_time = -1; UpdateSimulatorOperating(); break; case RideMode::Beginners: case RideMode::Intense: case RideMode::Berserk: SetState(Vehicle::Status::TopSpinOperating, sub_state); switch (curRide->mode) { case RideMode::Beginners: sub_state = 0; break; case RideMode::Intense: sub_state = 1; break; case RideMode::Berserk: sub_state = 2; break; default: { // This is workaround for multiple compilation errors of type "enumeration value ‘RIDE_MODE_*' not handled // in switch [-Werror=switch]" } } current_time = -1; Pitch = 0; bank_rotation = 0; UpdateTopSpinOperating(); break; case RideMode::ForwardRotation: case RideMode::BackwardRotation: SetState(Vehicle::Status::FerrisWheelRotating, Pitch); NumRotations = 0; ferris_wheel_var_0 = 8; ferris_wheel_var_1 = 8; UpdateFerrisWheelRotating(); break; case RideMode::MouseTails3DFilm: case RideMode::StormChasers3DFilm: case RideMode::SpaceRaiders3DFilm: SetState(Vehicle::Status::ShowingFilm, sub_state); switch (curRide->mode) { case RideMode::MouseTails3DFilm: sub_state = 0; break; case RideMode::StormChasers3DFilm: sub_state = 1; break; case RideMode::SpaceRaiders3DFilm: sub_state = 2; break; default: { // This is workaround for multiple compilation errors of type "enumeration value ‘RIDE_MODE_*' not handled // in switch [-Werror=switch]" } } current_time = -1; UpdateShowingFilm(); break; case RideMode::Circus: SetState(Vehicle::Status::DoingCircusShow); current_time = -1; UpdateDoingCircusShow(); break; case RideMode::SpaceRings: SetState(Vehicle::Status::SpaceRingsOperating); Pitch = 0; current_time = -1; UpdateSpaceRingsOperating(); break; case RideMode::HauntedHouse: SetState(Vehicle::Status::HauntedHouseOperating); Pitch = 0; current_time = -1; UpdateHauntedHouseOperating(); break; case RideMode::CrookedHouse: SetState(Vehicle::Status::CrookedHouseOperating); Pitch = 0; current_time = -1; UpdateCrookedHouseOperating(); break; default: SetState(status); NumLaps = 0; break; } } struct SynchronisedVehicle { RideId ride_id; StationIndex stationIndex; EntityId vehicle_id; }; constexpr int32_t SYNCHRONISED_VEHICLE_COUNT = 16; // Synchronised vehicle info static SynchronisedVehicle _synchronisedVehicles[SYNCHRONISED_VEHICLE_COUNT] = {}; static SynchronisedVehicle* _lastSynchronisedVehicle = nullptr; /** * Checks if a map position contains a synchronised ride station and adds the vehicle * to synchronise to the vehicle synchronisation list. * rct2: 0x006DE1A4 */ static bool try_add_synchronised_station(const CoordsXYZ& coords) { // make sure we are in map bounds if (!MapIsLocationValid(coords)) { return false; } TileElement* tileElement = GetStationPlatform({ coords, coords.z + 2 * COORDS_Z_STEP }); if (tileElement == nullptr) { /* No station platform element found, * so no station to synchronise */ return false; } auto rideIndex = tileElement->AsTrack()->GetRideIndex(); auto ride = GetRide(rideIndex); if (ride == nullptr || !(ride->depart_flags & RIDE_DEPART_SYNCHRONISE_WITH_ADJACENT_STATIONS)) { /* Ride is not set to synchronise with adjacent stations. */ return false; } /* From this point on, the ride of the map element is one that is set * to sync with adjacent stations, so it will return true. * Still to determine if a vehicle to sync can be identified. */ auto stationIndex = tileElement->AsTrack()->GetStationIndex(); SynchronisedVehicle* sv = _lastSynchronisedVehicle; sv->ride_id = rideIndex; sv->stationIndex = stationIndex; sv->vehicle_id = EntityId::GetNull(); _lastSynchronisedVehicle++; /* Ride vehicles are not on the track (e.g. ride is/was under * construction), so just return; vehicle_id for this station * is SPRITE_INDEX_NULL. */ if (!(ride->lifecycle_flags & RIDE_LIFECYCLE_ON_TRACK)) { return true; } /* Station is not ready to depart, so just return; * vehicle_id for this station is SPRITE_INDEX_NULL. */ if (!(ride->GetStation(stationIndex).Depart & kStationDepartFlag)) { return true; } // Look for a vehicle on this station waiting to depart. for (int32_t i = 0; i < ride->NumTrains; i++) { auto* vehicle = GetEntity(ride->vehicles[i]); if (vehicle == nullptr) { continue; } if (vehicle->status != Vehicle::Status::WaitingToDepart) { continue; } if (vehicle->sub_state != 0) { continue; } if (!vehicle->HasFlag(VehicleFlags::WaitingOnAdjacentStation)) { continue; } if (vehicle->current_station != stationIndex) { continue; } sv->vehicle_id = vehicle->Id; return true; } /* No vehicle found waiting to depart (with sync adjacent) at the * station, so just return; vehicle_id for this station is * SPRITE_INDEX_NULL. */ return true; } /** * Checks whether a vehicle can depart a station when set to synchronise with adjacent stations. * rct2: 0x006DE287 * @param vehicle The vehicle waiting to depart. * @returns true if the vehicle can depart (all adjacent trains are ready or broken down), otherwise false. * * Permits vehicles to depart in two ways: * Returns true, permitting the vehicle in the param to depart immediately; * The vehicle flag VehicleFlags::WaitingOnAdjacentStation is cleared for those * vehicles that depart in sync with the vehicle in the param. */ static bool ride_station_can_depart_synchronised(const Ride& ride, StationIndex stationIndex) { const auto& station = ride.GetStation(stationIndex); auto location = station.GetStart(); auto tileElement = MapGetTrackElementAt(location); if (tileElement == nullptr) { return false; } // Reset the list of synchronised vehicles to empty. _lastSynchronisedVehicle = _synchronisedVehicles; /* Search for stations to sync in both directions from the current tile. * We allow for some space between stations, and every time a station * is found we allow for space between that and the next. */ int32_t direction = tileElement->GetDirectionWithOffset(1); constexpr uint8_t maxCheckDistance = kRideAdjacencyCheckDistance; uint8_t spaceBetween = maxCheckDistance; while (_lastSynchronisedVehicle < &_synchronisedVehicles[SYNCHRONISED_VEHICLE_COUNT - 1]) { location += CoordsXYZ{ CoordsDirectionDelta[direction], 0 }; if (try_add_synchronised_station(location)) { spaceBetween = maxCheckDistance; continue; } if (spaceBetween-- == 0) { break; } } // Other search direction. location = station.GetStart(); direction = DirectionReverse(direction) & 3; spaceBetween = maxCheckDistance; while (_lastSynchronisedVehicle < &_synchronisedVehicles[SYNCHRONISED_VEHICLE_COUNT - 1]) { location += CoordsXYZ{ CoordsDirectionDelta[direction], 0 }; if (try_add_synchronised_station(location)) { spaceBetween = maxCheckDistance; continue; } if (spaceBetween-- == 0) { break; } } if (_lastSynchronisedVehicle == _synchronisedVehicles) { // No adjacent stations, allow depart return true; } for (SynchronisedVehicle* sv = _synchronisedVehicles; sv < _lastSynchronisedVehicle; sv++) { Ride* sv_ride = GetRide(sv->ride_id); if (!(sv_ride->lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN)) { if (sv_ride->status != RideStatus::Closed) { if (sv_ride->IsBlockSectioned()) { if (!(sv_ride->GetStation(sv->stationIndex).Depart & kStationDepartFlag)) { sv = _synchronisedVehicles; RideId rideId = RideId::GetNull(); for (; sv < _lastSynchronisedVehicle; sv++) { if (rideId.IsNull()) { rideId = sv->ride_id; } if (rideId != sv->ride_id) { // Here the sync-ed stations are not all from the same ride. return false; } } /* Here all the of sync-ed stations are from the same ride */ auto curRide = GetRide(rideId); if (curRide != nullptr) { for (int32_t i = 0; i < curRide->NumTrains; i++) { Vehicle* v = GetEntity(curRide->vehicles[i]); if (v == nullptr) { continue; } if (v->status != Vehicle::Status::WaitingToDepart && v->velocity != 0) { // Here at least one vehicle on the ride is moving. return false; } } } // UNCERTAIN: is the return desired here, or rather continue on with the general checks? return true; } } // There is no vehicle waiting at this station to sync with. if (sv->vehicle_id.IsNull()) { // Check conditions for departing without all stations being in sync. if (_lastSynchronisedVehicle > &_synchronisedVehicles[1]) { // Sync condition: there are at least 3 stations to sync return false; } RideId someRideIndex = _synchronisedVehicles[0].ride_id; if (someRideIndex != ride.id) { // Sync condition: the first station to sync is a different ride return false; } int32_t numTrainsAtStation = 0; int32_t numTravelingTrains = 0; auto currentStation = sv->stationIndex; for (int32_t i = 0; i < sv_ride->NumTrains; i++) { auto* otherVehicle = GetEntity(sv_ride->vehicles[i]); if (otherVehicle == nullptr) { continue; } if (otherVehicle->status != Vehicle::Status::Travelling) { if (currentStation == otherVehicle->current_station) { if (otherVehicle->status == Vehicle::Status::WaitingToDepart || otherVehicle->status == Vehicle::Status::MovingToEndOfStation) { numTrainsAtStation++; } } } else { numTravelingTrains++; } } int32_t totalTrains = numTrainsAtStation + numTravelingTrains; if (totalTrains != sv_ride->NumTrains || numTravelingTrains >= sv_ride->NumTrains / 2) { // if (numArrivingTrains > 0 || numTravelingTrains >= sv_ride->NumTrains / 2) { /* Sync condition: If there are trains arriving at the * station or half or more of the ride trains are * travelling, this station must be sync-ed before the * trains can depart! */ return false; } /* Sync exception - train is not arriving at the station * and there are less than half the trains for the ride * travelling. */ continue; } } } } // At this point all vehicles in _snychronisedVehicles can depart. for (SynchronisedVehicle* sv = _synchronisedVehicles; sv < _lastSynchronisedVehicle; sv++) { auto v = GetEntity(sv->vehicle_id); if (v != nullptr) { v->ClearFlag(VehicleFlags::WaitingOnAdjacentStation); } } return true; } bool Vehicle::CanDepartSynchronised() const { auto curRide = GetRide(); if (curRide == nullptr) return false; return ride_station_can_depart_synchronised(*curRide, current_station); } /** * * rct2: 0x006D9EB0 */ void Vehicle::PeepEasterEggHereWeAre() const { for (Vehicle* vehicle = GetEntity(Id); vehicle != nullptr; vehicle = GetEntity(vehicle->next_vehicle_on_train)) { for (int32_t i = 0; i < vehicle->num_peeps; ++i) { auto* curPeep = GetEntity(vehicle->peep[i]); if (curPeep != nullptr && curPeep->PeepFlags & PEEP_FLAGS_HERE_WE_ARE) { curPeep->InsertNewThought(PeepThoughtType::HereWeAre, curPeep->CurrentRide); } } } } /** * Performed when vehicle has completed a full circuit * rct2: 0x006D7338 */ static void test_finish(Ride& ride) { ride.lifecycle_flags &= ~RIDE_LIFECYCLE_TEST_IN_PROGRESS; ride.lifecycle_flags |= RIDE_LIFECYCLE_TESTED; auto& rideStations = ride.GetStations(); for (int32_t i = ride.num_stations - 1; i >= 1; i--) { if (rideStations[i - 1].SegmentTime != 0) continue; uint16_t oldTime = rideStations[i - 1].SegmentTime; rideStations[i - 1].SegmentTime = rideStations[i].SegmentTime; rideStations[i].SegmentTime = oldTime; int32_t oldLength = rideStations[i - 1].SegmentLength; rideStations[i - 1].SegmentLength = rideStations[i].SegmentLength; rideStations[i].SegmentLength = oldLength; } uint32_t totalTime = 0; for (uint8_t i = 0; i < ride.num_stations; ++i) { totalTime += rideStations[i].SegmentTime; } totalTime = std::max(totalTime, 1u); ride.average_speed = ride.average_speed / totalTime; WindowInvalidateByNumber(WindowClass::Ride, ride.id.ToUnderlying()); } void Vehicle::UpdateTestFinish() { auto curRide = GetRide(); if (curRide == nullptr) return; test_finish(*curRide); ClearFlag(VehicleFlags::Testing); } /** * * rct2: 0x006D6BE7 */ static void test_reset(Ride& ride, StationIndex curStation) { ride.lifecycle_flags |= RIDE_LIFECYCLE_TEST_IN_PROGRESS; ride.lifecycle_flags &= ~RIDE_LIFECYCLE_NO_RAW_STATS; ride.max_speed = 0; ride.average_speed = 0; ride.current_test_segment = 0; ride.average_speed_test_timeout = 0; ride.max_positive_vertical_g = FIXED_2DP(1, 0); ride.max_negative_vertical_g = FIXED_2DP(1, 0); ride.max_lateral_g = 0; ride.previous_vertical_g = 0; ride.previous_lateral_g = 0; ride.testing_flags = 0; ride.CurTestTrackLocation.SetNull(); ride.turn_count_default = 0; ride.turn_count_banked = 0; ride.turn_count_sloped = 0; ride.inversions = 0; ride.holes = 0; ride.sheltered_eighths = 0; ride.drops = 0; ride.sheltered_length = 0; ride.var_11C = 0; ride.num_sheltered_sections = 0; ride.highest_drop_height = 0; ride.special_track_elements = 0; for (auto& station : ride.GetStations()) { station.SegmentLength = 0; station.SegmentTime = 0; } ride.total_air_time = 0; ride.current_test_station = curStation; WindowInvalidateByNumber(WindowClass::Ride, ride.id.ToUnderlying()); } void Vehicle::TestReset() { SetFlag(VehicleFlags::Testing); auto curRide = GetRide(); if (curRide == nullptr) return; test_reset(*curRide, current_station); } // The result of this function is used to decide whether a vehicle on a tower ride should go further up or not. // Therefore, it will return true if anything is amiss. bool Vehicle::CurrentTowerElementIsTop() { TileElement* tileElement = MapGetTrackElementAtOfType(TrackLocation, GetTrackType()); if (tileElement == nullptr) return true; while (!tileElement->IsLastForTile()) { tileElement++; if (tileElement->IsGhost()) continue; if (tileElement->GetType() != TileElementType::Track) continue; const auto* trackElement = tileElement->AsTrack(); if (trackElement->GetRideIndex() != ride) continue; if (trackElement->GetTrackType() != TrackElemType::TowerSection) continue; return false; } return true; } /** * * rct2: 0x006D986C */ void Vehicle::UpdateTravellingBoatHireSetup() { var_34 = Orientation; TrackLocation.x = x; TrackLocation.y = y; TrackLocation = TrackLocation.ToTileStart(); CoordsXY location = CoordsXY(TrackLocation) + CoordsDirectionDelta[Orientation >> 3]; BoatLocation = location; var_35 = 0; // No longer on a track so reset to 0 for import/export SetTrackDirection(0); SetTrackType(0); SetState(Vehicle::Status::TravellingBoat); remaining_distance += 27924; UpdateTravellingBoat(); } /** * * rct2: 0x006D982F */ void Vehicle::UpdateDepartingBoatHire() { lost_time_out = 0; auto curRide = GetRide(); if (curRide == nullptr) return; auto& station = curRide->GetStation(current_station); station.Depart &= kStationDepartFlag; uint8_t waitingTime = std::max(curRide->min_waiting_time, static_cast(3)); waitingTime = std::min(waitingTime, static_cast(127)); station.Depart |= waitingTime; UpdateTravellingBoatHireSetup(); } /** * * rct2: 0x006D845B */ void Vehicle::UpdateDeparting() { auto curRide = GetRide(); if (curRide == nullptr) return; const auto* rideEntry = GetRideEntry(); if (rideEntry == nullptr) return; if (sub_state == 0) { if (HasFlag(VehicleFlags::TrainIsBroken)) { if (curRide->lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN) return; curRide->lifecycle_flags |= RIDE_LIFECYCLE_BROKEN_DOWN; RideBreakdownAddNewsItem(*curRide); curRide->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAIN | RIDE_INVALIDATE_RIDE_LIST | RIDE_INVALIDATE_RIDE_MAINTENANCE; curRide->mechanic_status = RIDE_MECHANIC_STATUS_CALLING; curRide->inspection_station = current_station; curRide->breakdown_reason = curRide->breakdown_reason_pending; velocity = 0; return; } sub_state = 1; PeepEasterEggHereWeAre(); if (rideEntry->flags & RIDE_ENTRY_FLAG_PLAY_DEPART_SOUND) { auto soundId = (rideEntry->Cars[0].sound_range == 4) ? OpenRCT2::Audio::SoundId::Tram : OpenRCT2::Audio::SoundId::TrainDeparting; OpenRCT2::Audio::Play3D(soundId, GetLocation()); } if (curRide->mode == RideMode::UpwardLaunch || (curRide->mode == RideMode::DownwardLaunch && NumLaunches > 1)) { OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::RideLaunch2, GetLocation()); } if (!(curRide->lifecycle_flags & RIDE_LIFECYCLE_TESTED)) { if (HasFlag(VehicleFlags::Testing)) { if (curRide->current_test_segment + 1 < curRide->num_stations) { curRide->current_test_segment++; curRide->current_test_station = current_station; } else { UpdateTestFinish(); } } else if (!(curRide->lifecycle_flags & RIDE_LIFECYCLE_TEST_IN_PROGRESS) && !IsGhost()) { TestReset(); } } } const auto& carEntry = rideEntry->Cars[vehicle_type]; const auto& rtd = curRide->GetRideTypeDescriptor(); switch (curRide->mode) { case RideMode::ReverseInclineLaunchedShuttle: if (velocity >= -131940) acceleration = -3298; break; case RideMode::PoweredLaunchPasstrough: case RideMode::PoweredLaunch: case RideMode::PoweredLaunchBlockSectioned: case RideMode::LimPoweredLaunch: case RideMode::UpwardLaunch: if ((curRide->launch_speed << 16) > velocity) { acceleration = curRide->launch_speed << rtd.OperatingSettings.AccelerationFactor; } break; case RideMode::DownwardLaunch: if (NumLaunches >= 1) { if ((14 << 16) > velocity) acceleration = 14 << 12; break; } [[fallthrough]]; case RideMode::ContinuousCircuit: case RideMode::ContinuousCircuitBlockSectioned: case RideMode::RotatingLift: case RideMode::FreefallDrop: case RideMode::BoatHire: if (carEntry.flags & CAR_ENTRY_FLAG_POWERED) break; if (velocity <= 131940) acceleration = 3298; break; default: { // This is workaround for multiple compilation errors of type "enumeration value ‘RIDE_MODE_*' not handled // in switch [-Werror=switch]" } } uint32_t curFlags = UpdateTrackMotion(nullptr); if (curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_8) { if (curRide->mode == RideMode::ReverseInclineLaunchedShuttle) { velocity = -velocity; FinishDeparting(); return; } } if (curFlags & (VEHICLE_UPDATE_MOTION_TRACK_FLAG_5 | VEHICLE_UPDATE_MOTION_TRACK_FLAG_12)) { if (curRide->mode == RideMode::BoatHire) { UpdateDepartingBoatHire(); return; } if (curRide->mode == RideMode::ReverseInclineLaunchedShuttle) { velocity = -velocity; FinishDeparting(); return; } if (curRide->mode == RideMode::Shuttle) { Flags ^= VehicleFlags::PoweredCarInReverse; velocity = 0; // We have turned, so treat it like entering a new tile UpdateCrossings(); } } if (curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_ON_LIFT_HILL) { sound2_flags |= VEHICLE_SOUND2_FLAGS_LIFT_HILL; if (curRide->mode != RideMode::ReverseInclineLaunchedShuttle) { int32_t curSpeed = curRide->lift_hill_speed * 31079; if (velocity <= curSpeed) { acceleration = 15539; if (velocity != 0) { if (_vehicleBreakdown == BREAKDOWN_SAFETY_CUT_OUT) { SetFlag(VehicleFlags::StoppedOnLift); ClearFlag(VehicleFlags::CollisionDisabled); } } else ClearFlag(VehicleFlags::CollisionDisabled); } } else { int32_t curSpeed = curRide->lift_hill_speed * -31079; if (velocity >= curSpeed) { acceleration = -15539; if (velocity != 0) { if (_vehicleBreakdown == BREAKDOWN_SAFETY_CUT_OUT) { SetFlag(VehicleFlags::StoppedOnLift); ClearFlag(VehicleFlags::CollisionDisabled); } } else ClearFlag(VehicleFlags::CollisionDisabled); } } } if (curRide->mode == RideMode::FreefallDrop) { animation_frame++; } else { bool shouldLaunch = true; if (curRide->mode == RideMode::DownwardLaunch) { if (NumLaunches < 1) shouldLaunch = false; } if (shouldLaunch) { if (!(curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_3) || _vehicleStationIndex != current_station) { FinishDeparting(); return; } if (!(curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_5)) return; if (curRide->mode == RideMode::BoatHire || curRide->mode == RideMode::RotatingLift || curRide->mode == RideMode::Shuttle) return; UpdateCrashSetup(); return; } } if (!CurrentTowerElementIsTop()) { if (curRide->mode == RideMode::FreefallDrop) Invalidate(); return; } FinishDeparting(); } /** * Part of UpdateDeparting * Called after finishing departing sequence to enter * traveling state. * Vertical rides class the lift to the top of the tower * as the departing sequence. After this point station * boosters do not affect the ride. * rct2: 0x006D8858 */ void Vehicle::FinishDeparting() { auto curRide = GetRide(); if (curRide == nullptr) return; if (curRide->mode == RideMode::DownwardLaunch) { if (NumLaunches >= 1 && (14 << 16) > velocity) return; OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::RideLaunch1, GetLocation()); } if (curRide->mode == RideMode::UpwardLaunch) { if ((curRide->launch_speed << 16) > velocity) return; OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::RideLaunch1, GetLocation()); } if (curRide->mode != RideMode::Race && !curRide->IsBlockSectioned()) { auto& currentStation = curRide->GetStation(current_station); currentStation.Depart &= kStationDepartFlag; uint8_t waitingTime = 3; if (curRide->depart_flags & RIDE_DEPART_WAIT_FOR_MINIMUM_LENGTH) { waitingTime = std::max(curRide->min_waiting_time, static_cast(3)); waitingTime = std::min(waitingTime, static_cast(127)); } currentStation.Depart |= waitingTime; } lost_time_out = 0; SetState(Vehicle::Status::Travelling, 1); if (velocity < 0) sub_state = 0; } /** * * rct2: 0x006DE5CB */ void Vehicle::CheckIfMissing() { auto curRide = GetRide(); if (curRide == nullptr) return; if (curRide->lifecycle_flags & (RIDE_LIFECYCLE_BROKEN_DOWN | RIDE_LIFECYCLE_CRASHED)) return; if (curRide->IsBlockSectioned()) return; if (!curRide->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_CHECK_FOR_STALLING)) return; lost_time_out++; if (curRide->lifecycle_flags & RIDE_LIFECYCLE_HAS_STALLED_VEHICLE) return; uint16_t limit = curRide->type == RIDE_TYPE_BOAT_HIRE ? 15360 : 9600; if (lost_time_out <= limit) return; curRide->lifecycle_flags |= RIDE_LIFECYCLE_HAS_STALLED_VEHICLE; if (gConfigNotifications.RideStalledVehicles) { Formatter ft; ft.Add(GetRideComponentName(GetRideTypeDescriptor(curRide->type).NameConvention.vehicle).number); uint8_t vehicleIndex = 0; for (; vehicleIndex < curRide->NumTrains; ++vehicleIndex) if (curRide->vehicles[vehicleIndex] == Id) break; vehicleIndex++; ft.Add(vehicleIndex); curRide->FormatNameTo(ft); ft.Add(GetRideComponentName(GetRideTypeDescriptor(curRide->type).NameConvention.station).singular); News::AddItemToQueue(News::ItemType::Ride, STR_NEWS_VEHICLE_HAS_STALLED, ride.ToUnderlying(), ft); } } void Vehicle::SimulateCrash() const { auto curRide = GetRide(); if (curRide != nullptr) { curRide->lifecycle_flags |= RIDE_LIFECYCLE_CRASHED; } } /** * Setup function for a vehicle colliding with * another vehicle. * * rct2: 0x006DA059 */ void Vehicle::UpdateCollisionSetup() { auto curRide = GetRide(); if (curRide == nullptr) return; if (curRide->status == RideStatus::Simulating) { SimulateCrash(); return; } SetState(Vehicle::Status::Crashed, sub_state); if (!(curRide->lifecycle_flags & RIDE_LIFECYCLE_CRASHED)) { auto frontVehicle = GetHead(); auto trainIndex = ride_get_train_index_from_vehicle(*curRide, frontVehicle->Id); if (!trainIndex.has_value()) { return; } curRide->Crash(trainIndex.value()); if (curRide->status != RideStatus::Closed) { // We require this to execute right away during the simulation, always ignore network and queue. auto gameAction = RideSetStatusAction(curRide->id, RideStatus::Closed); GameActions::ExecuteNested(&gameAction); } } curRide->lifecycle_flags |= RIDE_LIFECYCLE_CRASHED; curRide->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAIN | RIDE_INVALIDATE_RIDE_LIST; KillAllPassengersInTrain(); Vehicle* lastVehicle = this; for (Vehicle* train = GetEntity(Id); train != nullptr; train = GetEntity(train->next_vehicle_on_train)) { lastVehicle = train; train->sub_state = 2; #ifdef ENABLE_SCRIPTING InvokeVehicleCrashHook(train->Id, "another_vehicle"); #endif const auto trainLoc = train->GetLocation(); OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::Crash, trainLoc); ExplosionCloud::Create(trainLoc); for (int32_t i = 0; i < 10; i++) { VehicleCrashParticle::Create(train->colours, trainLoc); } train->SetFlag(VehicleFlags::Crashed); train->animationState = ScenarioRand() & 0xFFFF; train->animation_frame = ScenarioRand() & 0x7; train->SpriteData.Width = 13; train->SpriteData.HeightMin = 45; train->SpriteData.HeightMax = 5; train->MoveTo(trainLoc); train->SwingSpeed = 0; } // Remove the current train from the ride linked list of trains auto prevTrain = GetEntity(prev_vehicle_on_ride); auto nextTrain = GetEntity(lastVehicle->next_vehicle_on_ride); if (prevTrain == nullptr || nextTrain == nullptr) { LOG_ERROR("Corrupted vehicle list for ride!"); } else { prevTrain->next_vehicle_on_ride = lastVehicle->next_vehicle_on_ride; nextTrain->prev_vehicle_on_ride = prev_vehicle_on_ride; } velocity = 0; } /** rct2: 0x009A3AC4, 0x009A3AC6 */ static constexpr CoordsXY stru_9A3AC4[] = { { -256, 0 }, { -236, 98 }, { -181, 181 }, { -98, 236 }, { 0, 256 }, { 98, 236 }, { 181, 181 }, { 236, 98 }, { 256, 0 }, { 236, -98 }, { 181, -181 }, { 98, -236 }, { 0, -256 }, { -98, -236 }, { -181, -181 }, { -236, -98 }, }; /** * * rct2: 0x006D9EFE */ void Vehicle::UpdateCrashSetup() { auto curRide = GetRide(); if (curRide != nullptr && curRide->status == RideStatus::Simulating) { SimulateCrash(); return; } SetState(Vehicle::Status::Crashing, sub_state); if (NumPeepsUntilTrainTail() != 0) { OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::HauntedHouseScream2, GetLocation()); } int32_t edx = velocity >> 10; Vehicle* lastVehicle = this; auto spriteId = Id; for (Vehicle* trainVehicle; !spriteId.IsNull(); spriteId = trainVehicle->next_vehicle_on_train) { trainVehicle = GetEntity(spriteId); if (trainVehicle == nullptr) { break; } lastVehicle = trainVehicle; trainVehicle->sub_state = 0; int32_t trainX = stru_9A3AC4[trainVehicle->Orientation / 2].x; int32_t trainY = stru_9A3AC4[trainVehicle->Orientation / 2].y; auto trainZ = Unk9A38D4[trainVehicle->Pitch] >> 23; int32_t ecx = Unk9A37E4[trainVehicle->Pitch] >> 15; trainX *= ecx; trainY *= ecx; trainX >>= 16; trainY >>= 16; trainX *= edx; trainY *= edx; trainZ *= edx; trainX >>= 8; trainY >>= 8; trainZ >>= 8; trainVehicle->crash_x = trainX; trainVehicle->crash_y = trainY; trainVehicle->crash_z = trainZ; trainVehicle->crash_x += (ScenarioRand() & 0xF) - 8; trainVehicle->crash_y += (ScenarioRand() & 0xF) - 8; trainVehicle->crash_z += (ScenarioRand() & 0xF) - 8; trainVehicle->TrackLocation = { 0, 0, 0 }; } // Remove the current train from the ride linked list of trains auto prevTrain = GetEntity(prev_vehicle_on_ride); auto nextTrain = GetEntity(lastVehicle->next_vehicle_on_ride); if (prevTrain == nullptr || nextTrain == nullptr) { LOG_ERROR("Corrupted vehicle list for ride!"); } else { prevTrain->next_vehicle_on_ride = lastVehicle->next_vehicle_on_ride; nextTrain->prev_vehicle_on_ride = prev_vehicle_on_ride; } velocity = 0; } /** * * rct2: 0x006D8937 */ void Vehicle::UpdateTravelling() { CheckIfMissing(); auto curRide = GetRide(); if (curRide == nullptr || (_vehicleBreakdown == 0 && curRide->mode == RideMode::RotatingLift)) return; if (sub_state == 2) { velocity = 0; acceleration = 0; var_C0--; if (var_C0 == 0) sub_state = 0; } if (curRide->mode == RideMode::FreefallDrop && animation_frame != 0) { animation_frame++; velocity = 0; acceleration = 0; Invalidate(); return; } uint32_t curFlags = UpdateTrackMotion(nullptr); bool skipCheck = false; if (curFlags & (VEHICLE_UPDATE_MOTION_TRACK_FLAG_8 | VEHICLE_UPDATE_MOTION_TRACK_FLAG_9) && curRide->mode == RideMode::ReverseInclineLaunchedShuttle && sub_state == 0) { sub_state = 1; velocity = 0; skipCheck = true; } if (!skipCheck) { if (curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_DERAILED) { UpdateCrashSetup(); return; } if (curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_COLLISION) { UpdateCollisionSetup(); return; } if (curFlags & (VEHICLE_UPDATE_MOTION_TRACK_FLAG_5 | VEHICLE_UPDATE_MOTION_TRACK_FLAG_12)) { if (curRide->mode == RideMode::RotatingLift) { if (sub_state <= 1) { SetState(Vehicle::Status::Arriving, 1); var_C0 = 0; return; } } else if (curRide->mode == RideMode::BoatHire) { UpdateTravellingBoatHireSetup(); return; } if (curRide->mode == RideMode::Shuttle) { Flags ^= VehicleFlags::PoweredCarInReverse; velocity = 0; } else { if (sub_state != 0) { UpdateCrashSetup(); return; } sub_state = 1; velocity = 0; } } } if (curRide->mode == RideMode::RotatingLift && sub_state <= 1) { if (sub_state == 0) { if (velocity >= -131940) acceleration = -3298; velocity = std::max(velocity, -131940); } else { if (CurrentTowerElementIsTop()) { velocity = 0; sub_state = 2; var_C0 = 150; } else { if (velocity <= 131940) acceleration = 3298; } } } if (curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_ON_LIFT_HILL) { if (curRide->mode == RideMode::ReverseInclineLaunchedShuttle) { if (sub_state == 0) { if (velocity != 0) sound2_flags |= VEHICLE_SOUND2_FLAGS_LIFT_HILL; if (!HasFlag(VehicleFlags::ReverseInclineCompletedLap)) { if (velocity >= curRide->lift_hill_speed * -31079) { acceleration = -15539; if (_vehicleBreakdown == 0) { sound2_flags &= ~VEHICLE_SOUND2_FLAGS_LIFT_HILL; SetFlag(VehicleFlags::StoppedOnLift); } } } } } else { sound2_flags |= VEHICLE_SOUND2_FLAGS_LIFT_HILL; if (velocity <= curRide->lift_hill_speed * 31079) { acceleration = 15539; if (velocity != 0) { if (_vehicleBreakdown == 0) { SetFlag(VehicleFlags::StoppedOnLift); sound2_flags &= ~VEHICLE_SOUND2_FLAGS_LIFT_HILL; } } else { sound2_flags &= ~VEHICLE_SOUND2_FLAGS_LIFT_HILL; } } } } if (!(curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_3)) return; if (curRide->mode == RideMode::ReverseInclineLaunchedShuttle && velocity >= 0 && !HasFlag(VehicleFlags::ReverseInclineCompletedLap)) { return; } if (curRide->mode == RideMode::PoweredLaunchPasstrough && velocity < 0) return; SetState(Vehicle::Status::Arriving); current_station = _vehicleStationIndex; var_C0 = 0; if (velocity < 0) sub_state = 1; } void Vehicle::UpdateArrivingPassThroughStation(const Ride& curRide, const CarEntry& carEntry, bool stationBrakesWork) { if (sub_state == 0) { if (curRide.mode == RideMode::Race && curRide.lifecycle_flags & RIDE_LIFECYCLE_PASS_STATION_NO_STOPPING) { return; } if (velocity <= 131940) { acceleration = 3298; return; } int32_t velocity_diff = velocity; if (velocity_diff >= 24.0_mph) velocity_diff /= 8; else velocity_diff /= 16; if (!stationBrakesWork) { return; } if (curRide.num_circuits != 1) { if (NumLaps + 1 < curRide.num_circuits) { return; } } velocity -= velocity_diff; acceleration = 0; } else { if (!(carEntry.flags & CAR_ENTRY_FLAG_POWERED) && velocity >= -131940) { acceleration = -3298; } if (velocity >= -131940) { return; } int32_t velocity_diff = velocity; if (velocity_diff < -24.0_mph) velocity_diff /= 8; else velocity_diff /= 16; if (!stationBrakesWork) { return; } if (NumLaps + 1 < curRide.num_circuits) { return; } if (NumLaps + 1 != curRide.num_circuits) { velocity -= velocity_diff; acceleration = 0; return; } if (GetRideTypeDescriptor(curRide.type).HasFlag(RIDE_TYPE_FLAG_ALLOW_MULTIPLE_CIRCUITS) && curRide.mode != RideMode::Shuttle && curRide.mode != RideMode::PoweredLaunch) { SetFlag(VehicleFlags::ReverseInclineCompletedLap); } else { velocity -= velocity_diff; acceleration = 0; } } } /** * * rct2: 0x006D8C36 */ void Vehicle::UpdateArriving() { auto curRide = GetRide(); if (curRide == nullptr) return; bool stationBrakesWork = true; uint32_t curFlags = 0; switch (curRide->mode) { case RideMode::Swing: case RideMode::Rotation: case RideMode::ForwardRotation: case RideMode::BackwardRotation: case RideMode::FilmAvengingAviators: case RideMode::FilmThrillRiders: case RideMode::Beginners: case RideMode::Intense: case RideMode::Berserk: case RideMode::MouseTails3DFilm: case RideMode::StormChasers3DFilm: case RideMode::SpaceRaiders3DFilm: case RideMode::Circus: case RideMode::SpaceRings: case RideMode::HauntedHouse: case RideMode::CrookedHouse: ClearFlag(VehicleFlags::ReverseInclineCompletedLap); velocity = 0; acceleration = 0; SetState(Vehicle::Status::UnloadingPassengers); return; default: { // This is workaround for multiple compilation errors of type "enumeration value ‘RIDE_MODE_*' not handled // in switch [-Werror=switch]" } } bool hasBrakesFailure = curRide->lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN && curRide->breakdown_reason_pending == BREAKDOWN_BRAKES_FAILURE; if (hasBrakesFailure && curRide->inspection_station == current_station && curRide->mechanic_status != RIDE_MECHANIC_STATUS_HAS_FIXED_STATION_BRAKES) { stationBrakesWork = false; } const auto* rideEntry = GetRideEntry(); const auto& carEntry = rideEntry->Cars[vehicle_type]; UpdateArrivingPassThroughStation(*curRide, carEntry, stationBrakesWork); curFlags = UpdateTrackMotion(nullptr); if (curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_COLLISION && !stationBrakesWork) { UpdateCollisionSetup(); return; } if (curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_AT_STATION && !stationBrakesWork) { SetState(Vehicle::Status::Departing, 1); return; } if (!(curFlags & (VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_AT_STATION | VEHICLE_UPDATE_MOTION_TRACK_FLAG_1 | VEHICLE_UPDATE_MOTION_TRACK_FLAG_5))) { if (velocity > 98955) var_C0 = 0; return; } var_C0++; if ((curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_1) && (carEntry.flags & CAR_ENTRY_FLAG_GO_KART) && (var_C0 < 40)) { return; } auto trackElement = MapGetTrackElementAt(TrackLocation); if (trackElement == nullptr) { return; } current_station = trackElement->GetStationIndex(); NumLaps++; if (sub_state != 0) { if (NumLaps < curRide->num_circuits) { SetState(Vehicle::Status::Departing, 1); return; } if (NumLaps == curRide->num_circuits && HasFlag(VehicleFlags::ReverseInclineCompletedLap)) { SetState(Vehicle::Status::Departing, 1); return; } } if (curRide->num_circuits != 1 && NumLaps < curRide->num_circuits) { SetState(Vehicle::Status::Departing, 1); return; } if ((curRide->mode == RideMode::UpwardLaunch || curRide->mode == RideMode::DownwardLaunch) && NumLaunches < 2) { OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::RideLaunch2, GetLocation()); velocity = 0; acceleration = 0; SetState(Vehicle::Status::Departing, 1); return; } if (curRide->mode == RideMode::Race && curRide->lifecycle_flags & RIDE_LIFECYCLE_PASS_STATION_NO_STOPPING) { SetState(Vehicle::Status::Departing, 1); return; } ClearFlag(VehicleFlags::ReverseInclineCompletedLap); velocity = 0; acceleration = 0; SetState(Vehicle::Status::UnloadingPassengers); } /** * * rct2: 0x006D9002 */ void Vehicle::UpdateUnloadingPassengers() { if (sub_state == 0) { if (OpenRestraints()) { sub_state = 1; } } const auto* curRide = GetRide(); if (curRide == nullptr) return; const auto& currentStation = curRide->GetStation(current_station); if (curRide->mode == RideMode::ForwardRotation || curRide->mode == RideMode::BackwardRotation) { uint8_t seat = ((-Pitch) >> 3) & 0xF; if (restraints_position == 255 && !peep[seat * 2].IsNull()) { next_free_seat -= 2; auto firstGuest = GetEntity(peep[seat * 2]); peep[seat * 2] = EntityId::GetNull(); if (firstGuest != nullptr) { firstGuest->SetState(PeepState::LeavingRide); firstGuest->RideSubState = PeepRideSubState::LeaveVehicle; } auto secondGuest = GetEntity(peep[seat * 2 + 1]); peep[seat * 2 + 1] = EntityId::GetNull(); if (secondGuest != nullptr) { secondGuest->SetState(PeepState::LeavingRide); secondGuest->RideSubState = PeepRideSubState::LeaveVehicle; } } } else { if (currentStation.Exit.IsNull()) { if (sub_state != 1) return; if (!(curRide->lifecycle_flags & RIDE_LIFECYCLE_TESTED) && HasFlag(VehicleFlags::Testing) && curRide->current_test_segment + 1 >= curRide->num_stations) { UpdateTestFinish(); } SetState(Vehicle::Status::MovingToEndOfStation); return; } for (Vehicle* train = GetEntity(Id); train != nullptr; train = GetEntity(train->next_vehicle_on_train)) { if (train->restraints_position != 255) continue; if (train->next_free_seat == 0) continue; train->next_free_seat = 0; for (uint8_t peepIndex = 0; peepIndex < train->num_peeps; peepIndex++) { Peep* curPeep = GetEntity(train->peep[peepIndex]); if (curPeep != nullptr) { curPeep->SetState(PeepState::LeavingRide); curPeep->RideSubState = PeepRideSubState::LeaveVehicle; } } } } if (sub_state != 1) return; for (Vehicle* train = GetEntity(Id); train != nullptr; train = GetEntity(train->next_vehicle_on_train)) { if (train->num_peeps != train->next_free_seat) return; } if (!(curRide->lifecycle_flags & RIDE_LIFECYCLE_TESTED) && HasFlag(VehicleFlags::Testing) && curRide->current_test_segment + 1 >= curRide->num_stations) { UpdateTestFinish(); } SetState(Vehicle::Status::MovingToEndOfStation); } /** * * rct2: 0x006D9CE9 */ void Vehicle::UpdateWaitingForCableLift() { auto curRide = GetRide(); if (curRide == nullptr) return; Vehicle* cableLift = GetEntity(curRide->cable_lift); if (cableLift == nullptr) return; if (cableLift->status != Vehicle::Status::WaitingForPassengers) return; cableLift->SetState(Vehicle::Status::WaitingToDepart, sub_state); cableLift->cable_lift_target = Id; } /** * * rct2: 0x006D9D21 */ void Vehicle::UpdateTravellingCableLift() { auto curRide = GetRide(); if (curRide == nullptr) return; if (sub_state == 0) { if (HasFlag(VehicleFlags::TrainIsBroken)) { if (curRide->lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN) return; curRide->lifecycle_flags |= RIDE_LIFECYCLE_BROKEN_DOWN; RideBreakdownAddNewsItem(*curRide); curRide->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAIN | RIDE_INVALIDATE_RIDE_LIST | RIDE_INVALIDATE_RIDE_MAINTENANCE; curRide->mechanic_status = RIDE_MECHANIC_STATUS_CALLING; curRide->inspection_station = current_station; curRide->breakdown_reason = curRide->breakdown_reason_pending; velocity = 0; return; } sub_state = 1; PeepEasterEggHereWeAre(); if (!(curRide->lifecycle_flags & RIDE_LIFECYCLE_TESTED)) { if (HasFlag(VehicleFlags::Testing)) { if (curRide->current_test_segment + 1 < curRide->num_stations) { curRide->current_test_segment++; curRide->current_test_station = current_station; } else { UpdateTestFinish(); } } else if (!(curRide->lifecycle_flags & RIDE_LIFECYCLE_TEST_IN_PROGRESS) && !IsGhost()) { TestReset(); } } } if (velocity <= 439800) { acceleration = 4398; } int32_t curFlags = UpdateTrackMotion(nullptr); if (curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_11) { SetState(Vehicle::Status::Travelling, 1); lost_time_out = 0; return; } if (sub_state == 2) return; if (curFlags & VEHICLE_UPDATE_MOTION_TRACK_FLAG_3 && current_station == _vehicleStationIndex) return; sub_state = 2; if (curRide->IsBlockSectioned()) return; // This is slightly different to the vanilla function auto& currentStation = curRide->GetStation(current_station); currentStation.Depart &= kStationDepartFlag; uint8_t waitingTime = 3; if (curRide->depart_flags & RIDE_DEPART_WAIT_FOR_MINIMUM_LENGTH) { waitingTime = std::max(curRide->min_waiting_time, static_cast(3)); waitingTime = std::min(waitingTime, static_cast(127)); } currentStation.Depart |= waitingTime; } /** * * rct2: 0x006D9820 */ void Vehicle::UpdateTravellingBoat() { CheckIfMissing(); UpdateMotionBoatHire(); } void Vehicle::TryReconnectBoatToTrack(const CoordsXY& currentBoatLocation, const CoordsXY& trackCoords) { remaining_distance = 0; if (!UpdateMotionCollisionDetection({ currentBoatLocation, z }, nullptr)) { TrackLocation.x = trackCoords.x; TrackLocation.y = trackCoords.y; auto curRide = GetRide(); if (curRide != nullptr) { auto trackElement = MapGetTrackElementAt(TrackLocation); if (trackElement != nullptr) SetTrackType(trackElement->GetTrackType()); SetTrackDirection(curRide->boat_hire_return_direction); BoatLocation.SetNull(); } track_progress = 0; SetState(Vehicle::Status::Travelling, sub_state); _vehicleCurPosition.x = currentBoatLocation.x; _vehicleCurPosition.y = currentBoatLocation.y; } } /** * * rct2: 0x006DA717 */ void Vehicle::UpdateMotionBoatHire() { _vehicleMotionTrackFlags = 0; velocity += acceleration; _vehicleVelocityF64E08 = velocity; _vehicleVelocityF64E0C = (velocity >> 10) * 42; auto carEntry = Entry(); if (carEntry == nullptr) { return; } if (carEntry->flags & (CAR_ENTRY_FLAG_VEHICLE_ANIMATION | CAR_ENTRY_FLAG_RIDER_ANIMATION)) { UpdateAdditionalAnimation(); } _vehicleUnkF64E10 = 1; acceleration = 0; remaining_distance += _vehicleVelocityF64E0C; if (remaining_distance >= 0x368A) { sound2_flags &= ~VEHICLE_SOUND2_FLAGS_LIFT_HILL; _vehicleCurPosition = GetLocation(); Invalidate(); for (;;) { // Loc6DA7A5 var_35++; auto loc = BoatLocation.ToTileCentre(); CoordsXY loc2 = loc; uint8_t bl; loc2.x -= x; if (loc2.x >= 0) { loc2.y -= y; if (loc2.y < 0) { // Loc6DA81A: loc2.y = -loc2.y; bl = 24; if (loc2.y <= loc2.x * 4) { bl = 16; if (loc2.x <= loc2.y * 4) { bl = 20; } } } else { bl = 8; if (loc2.y <= loc2.x * 4) { bl = 16; if (loc2.x <= loc2.y * 4) { bl = 12; } } } } else { loc2.y -= y; if (loc2.y < 0) { // Loc6DA83D: loc2.x = -loc2.x; loc2.y = -loc2.y; bl = 24; if (loc2.y <= loc2.x * 4) { bl = 0; if (loc2.x <= loc2.y * 4) { bl = 28; } } } else { loc2.x = -loc2.x; bl = 8; if (loc2.y <= loc2.x * 4) { bl = 0; if (loc2.x <= loc2.y * 4) { bl = 4; } } } } // Loc6DA861: var_34 = bl; loc2.x += loc2.y; if (loc2.x <= 12) { UpdateBoatLocation(); } if (!(var_35 & (1 << 0))) { uint8_t spriteDirection = Orientation; if (spriteDirection != var_34) { uint8_t dl = (var_34 + 16 - spriteDirection) & 0x1E; if (dl >= 16) { spriteDirection += 2; if (dl > 24) { var_35--; } } else { spriteDirection -= 2; if (dl < 8) { var_35--; } } Orientation = spriteDirection & 0x1E; } } int32_t edi = (Orientation | (var_35 & 1)) & 0x1F; loc2 = { x + Unk9A36C4[edi].x, y + Unk9A36C4[edi].y }; if (UpdateMotionCollisionDetection({ loc2, z }, nullptr)) { remaining_distance = 0; if (Orientation == var_34) { Orientation ^= (1 << 4); UpdateBoatLocation(); Orientation ^= (1 << 4); } break; } auto flooredLocation = loc2.ToTileStart(); if (flooredLocation != TrackLocation) { if (!vehicle_boat_is_location_accessible({ loc2, TrackLocation.z })) { // Loc6DA939: auto curRide = GetRide(); if (curRide == nullptr) return; bool do_Loc6DAA97 = false; if (sub_state != 1) { do_Loc6DAA97 = true; } else { auto flooredTileLoc = TileCoordsXY(flooredLocation); if (curRide->boat_hire_return_position != flooredTileLoc) { do_Loc6DAA97 = true; } } // Loc6DAA97: if (do_Loc6DAA97) { remaining_distance = 0; if (Orientation == var_34) { UpdateBoatLocation(); } break; } if (!(curRide->boat_hire_return_direction & 1)) { uint16_t tilePart = loc2.y % COORDS_XY_STEP; if (tilePart == COORDS_XY_HALF_TILE) { TryReconnectBoatToTrack(loc2, flooredLocation); break; } loc2 = _vehicleCurPosition; if (tilePart <= COORDS_XY_HALF_TILE) { loc2.y += 1; } else { loc2.y -= 1; } } else { // Loc6DA9A2: uint16_t tilePart = loc2.x % COORDS_XY_STEP; if (tilePart == COORDS_XY_HALF_TILE) { TryReconnectBoatToTrack(loc2, flooredLocation); break; } loc2 = _vehicleCurPosition; if (tilePart <= COORDS_XY_HALF_TILE) { loc2.x += 1; } else { loc2.x -= 1; } } // Loc6DA9D1: remaining_distance = 0; if (!UpdateMotionCollisionDetection({ loc2, z }, nullptr)) { _vehicleCurPosition.x = loc2.x; _vehicleCurPosition.y = loc2.y; } break; } TrackLocation = { flooredLocation, TrackLocation.z }; } remaining_distance -= Unk9A36C4[edi].distance; _vehicleCurPosition.x = loc2.x; _vehicleCurPosition.y = loc2.y; if (remaining_distance < 0x368A) { break; } _vehicleUnkF64E10++; } MoveTo(_vehicleCurPosition); } // Loc6DAAC9: { int32_t edx = velocity >> 8; edx = (edx * edx); if (velocity < 0) { edx = -edx; } edx >>= 5; // Hack to fix people messing with boat hire int32_t curMass = mass == 0 ? 1 : mass; int32_t eax = ((velocity >> 1) + edx) / curMass; int32_t ecx = -eax; if (carEntry->flags & CAR_ENTRY_FLAG_POWERED) { eax = speed << 14; int32_t ebx = (speed * curMass) >> 2; if (HasFlag(VehicleFlags::PoweredCarInReverse)) { eax = -eax; } eax -= velocity; edx = powered_acceleration * 2; ecx += (eax * edx) / ebx; } acceleration = ecx; } // eax = _vehicleMotionTrackFlags; // ebx = _vehicleStationIndex; } /** * * rct2: 0x006DA280 */ void Vehicle::UpdateBoatLocation() { auto curRide = GetRide(); if (curRide == nullptr) return; TileCoordsXY returnPosition = curRide->boat_hire_return_position; uint8_t returnDirection = curRide->boat_hire_return_direction & 3; CoordsXY location = CoordsXY{ x, y } + CoordsDirectionDelta[returnDirection]; if (location.ToTileStart() == returnPosition.ToCoordsXY()) { sub_state = 1; BoatLocation = location.ToTileStart(); return; } sub_state = 0; uint8_t curDirection = ((Orientation + 19) >> 3) & 3; uint8_t randDirection = ScenarioRand() & 3; if (lost_time_out > 1920) { if (ScenarioRand() & 1) { CoordsXY destLocation = (returnPosition.ToCoordsXY() - CoordsDirectionDelta[returnDirection]).ToTileCentre(); destLocation.x -= x; destLocation.y -= y; if (abs(destLocation.x) <= abs(destLocation.y)) { randDirection = destLocation.y < 0 ? 3 : 1; } else { randDirection = destLocation.x < 0 ? 0 : 2; } } } static constexpr int8_t rotations[] = { 0, 1, -1, 2 }; for (auto rotation : rotations) { if (randDirection + rotation == curDirection) { continue; } auto trackLocation = TrackLocation; trackLocation += CoordsDirectionDelta[(randDirection + rotation) & 3]; if (!vehicle_boat_is_location_accessible(trackLocation)) { continue; } BoatLocation = trackLocation.ToTileStart(); return; } CoordsXY trackLocation = TrackLocation; trackLocation += CoordsDirectionDelta[curDirection & 3]; BoatLocation = trackLocation.ToTileStart(); } /** * * rct2: 0x006DA22A */ static bool vehicle_boat_is_location_accessible(const CoordsXYZ& location) { TileElement* tileElement = MapGetFirstElementAt(location); if (tileElement == nullptr) return false; do { if (tileElement->IsGhost()) continue; if (tileElement->GetType() == TileElementType::Surface) { int32_t waterZ = tileElement->AsSurface()->GetWaterHeight(); if (location.z != waterZ) { return false; } } else { if (location.z > (tileElement->GetBaseZ() - (2 * COORDS_Z_STEP)) && location.z < tileElement->GetClearanceZ() + (2 * COORDS_Z_STEP)) { return false; } } } while (!(tileElement++)->IsLastForTile()); return true; } /** * * rct2: 0x006D9249 */ void Vehicle::UpdateSwinging() { auto curRide = GetRide(); if (curRide == nullptr) return; auto rideEntry = GetRideEntry(); if (rideEntry == nullptr) return; // SubState for this ride means swinging state // 0 == first swing // 3 == full swing uint8_t swingState = sub_state; if (rideEntry->flags & RIDE_ENTRY_FLAG_ALTERNATIVE_SWING_MODE_1) { swingState += 4; if (rideEntry->flags & RIDE_ENTRY_FLAG_ALTERNATIVE_SWING_MODE_2) swingState += 4; } const int8_t* spriteMap = SwingingTimeToSpriteMaps[swingState]; int8_t spriteType = spriteMap[current_time + 1]; // 0x80 indicates that a complete swing has been // completed and the next swing can start if (spriteType != -128) { current_time++; if (static_cast(spriteType) != Pitch) { // Used to know which sprite to draw Pitch = static_cast(spriteType); Invalidate(); } return; } current_time = -1; NumSwings++; if (curRide->status != RideStatus::Closed) { // It takes 3 swings to get into full swing // ride->rotations already takes this into account if (NumSwings + 3 < curRide->rotations) { // Go to the next swing state until we // are at full swing. if (sub_state != 3) { sub_state++; } UpdateSwinging(); return; } } // To get to this part of the code the // swing has to be in slowing down phase if (sub_state == 0) { SetState(Vehicle::Status::Arriving); var_C0 = 0; return; } // Go towards first swing state sub_state--; UpdateSwinging(); } /** * * rct2: 0x006D9413 */ void Vehicle::UpdateFerrisWheelRotating() { if (_vehicleBreakdown == 0) return; auto curRide = GetRide(); if (curRide == nullptr) return; if ((ferris_wheel_var_1 -= 1) != 0) return; int8_t curFerrisWheelVar0 = ferris_wheel_var_0; if (curFerrisWheelVar0 == 3) { ferris_wheel_var_0 = curFerrisWheelVar0; ferris_wheel_var_1 = curFerrisWheelVar0; } else if (curFerrisWheelVar0 < 3) { if (curFerrisWheelVar0 != -8) curFerrisWheelVar0--; ferris_wheel_var_0 = curFerrisWheelVar0; ferris_wheel_var_1 = -curFerrisWheelVar0; } else { curFerrisWheelVar0--; ferris_wheel_var_0 = curFerrisWheelVar0; ferris_wheel_var_1 = curFerrisWheelVar0; } uint8_t rotation = Pitch; if (curRide->mode == RideMode::ForwardRotation) rotation++; else rotation--; rotation &= 0x7F; Pitch = rotation; if (rotation == sub_state) NumRotations++; Invalidate(); uint8_t subState = sub_state; if (curRide->mode == RideMode::ForwardRotation) subState++; else subState--; subState &= 0x7F; if (subState == Pitch) { bool shouldStop = true; if (curRide->status != RideStatus::Closed) { if (NumRotations < curRide->rotations) shouldStop = false; } if (shouldStop) { curFerrisWheelVar0 = ferris_wheel_var_0; ferris_wheel_var_0 = -abs(curFerrisWheelVar0); ferris_wheel_var_1 = abs(curFerrisWheelVar0); } } if (ferris_wheel_var_0 != -8) return; subState = sub_state; if (curRide->mode == RideMode::ForwardRotation) subState += 8; else subState -= 8; subState &= 0x7F; if (subState != Pitch) return; SetState(Vehicle::Status::Arriving); var_C0 = 0; } /** * * rct2: 0x006D94F2 */ void Vehicle::UpdateSimulatorOperating() { if (_vehicleBreakdown == 0) return; assert(current_time >= -1); assert(current_time < MotionSimulatorTimeToSpriteMapCount); uint8_t al = MotionSimulatorTimeToSpriteMap[current_time + 1]; if (al != 0xFF) { current_time++; if (al == Pitch) return; Pitch = al; Invalidate(); return; } SetState(Vehicle::Status::Arriving); var_C0 = 0; } void UpdateRotatingDefault(Vehicle& vehicle) { vehicle.sub_state = 1; vehicle.UpdateRotating(); } void UpdateRotatingEnterprise(Vehicle& vehicle) { if (vehicle.sub_state == 2) { vehicle.SetState(Vehicle::Status::Arriving); vehicle.var_C0 = 0; return; } UpdateRotatingDefault(vehicle); } /** * * rct2: 0x006D92FF */ void Vehicle::UpdateRotating() { if (_vehicleBreakdown == 0) return; auto curRide = GetRide(); if (curRide == nullptr) return; auto rideEntry = GetRideEntry(); if (rideEntry == nullptr) { return; } const uint8_t* timeToSpriteMap; if (rideEntry->flags & RIDE_ENTRY_FLAG_ALTERNATIVE_ROTATION_MODE_1) { timeToSpriteMap = Rotation1TimeToSpriteMaps[sub_state]; } else if (rideEntry->flags & RIDE_ENTRY_FLAG_ALTERNATIVE_ROTATION_MODE_2) { timeToSpriteMap = Rotation2TimeToSpriteMaps[sub_state]; } else { timeToSpriteMap = Rotation3TimeToSpriteMaps[sub_state]; } uint16_t time = current_time; if (_vehicleBreakdown == BREAKDOWN_CONTROL_FAILURE) { time += (curRide->breakdown_sound_modifier >> 6) + 1; } time++; uint8_t sprite = timeToSpriteMap[time]; if (sprite != 0xFF) { current_time = time; if (sprite == Pitch) return; Pitch = sprite; Invalidate(); return; } current_time = -1; NumRotations++; if (_vehicleBreakdown != BREAKDOWN_CONTROL_FAILURE) { bool shouldStop = true; if (curRide->status != RideStatus::Closed) { sprite = NumRotations + 1; if (curRide->type == RIDE_TYPE_ENTERPRISE) sprite += 9; if (sprite < curRide->rotations) shouldStop = false; } if (shouldStop) { if (sub_state == 2) { SetState(Vehicle::Status::Arriving); var_C0 = 0; return; } sub_state++; UpdateRotating(); return; } } const auto& rtd = GetRideTypeDescriptor(curRide->type); rtd.UpdateRotating(*this); } /** * * rct2: 0x006D97CB */ void Vehicle::UpdateSpaceRingsOperating() { if (_vehicleBreakdown == 0) return; uint8_t spriteType = SpaceRingsTimeToSpriteMap[current_time + 1]; if (spriteType != 255) { current_time++; if (spriteType != Pitch) { Pitch = spriteType; Invalidate(); } } else { SetState(Vehicle::Status::Arriving); var_C0 = 0; } } /** * * rct2: 0x006D9641 */ void Vehicle::UpdateHauntedHouseOperating() { if (_vehicleBreakdown == 0) return; if (Pitch != 0) { if (GetGameState().CurrentTicks & 1) { Pitch++; Invalidate(); if (Pitch == 19) Pitch = 0; } } if (current_time + 1 > 1500) { SetState(Vehicle::Status::Arriving); var_C0 = 0; return; } current_time++; switch (current_time) { case 45: OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::HauntedHouseScare, GetLocation()); break; case 75: Pitch = 1; Invalidate(); break; case 400: OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::HauntedHouseScream1, GetLocation()); break; case 745: OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::HauntedHouseScare, GetLocation()); break; case 775: Pitch = 1; Invalidate(); break; case 1100: OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::HauntedHouseScream2, GetLocation()); break; } } /** * * rct2: 0x006d9781 */ void Vehicle::UpdateCrookedHouseOperating() { if (_vehicleBreakdown == 0) return; // Originally used an array of size 1 at 0x009A0AC4 and passed the sub state into it. if (static_cast(current_time + 1) > 600) { SetState(Vehicle::Status::Arriving); var_C0 = 0; return; } current_time++; } /** * * rct2: 0x006D9547 */ void Vehicle::UpdateTopSpinOperating() { if (_vehicleBreakdown == 0) return; const TopSpinTimeToSpriteMap* sprite_map = TopSpinTimeToSpriteMaps[sub_state]; uint8_t rotation = sprite_map[current_time + 1].arm_rotation; if (rotation != 0xFF) { current_time = current_time + 1; if (rotation != Pitch) { Pitch = rotation; Invalidate(); } rotation = sprite_map[current_time].bank_rotation; if (rotation != bank_rotation) { bank_rotation = rotation; Invalidate(); } return; } SetState(Vehicle::Status::Arriving); var_C0 = 0; } /** * * rct2: 0x006D95AD */ void Vehicle::UpdateShowingFilm() { int32_t currentTime, totalTime; if (_vehicleBreakdown == 0) return; totalTime = RideFilmLength[sub_state]; currentTime = current_time + 1; if (currentTime <= totalTime) { current_time = currentTime; } else { SetState(Vehicle::Status::Arriving); var_C0 = 0; } } /** * * rct2: 0x006D95F7 */ void Vehicle::UpdateDoingCircusShow() { if (_vehicleBreakdown == 0) return; int32_t currentTime = current_time + 1; if (currentTime <= 5000) { current_time = currentTime; } else { SetState(Vehicle::Status::Arriving); var_C0 = 0; } } /** * * rct2: 0x0068B8BD * @returns the map element that the vehicle will collide with or NULL if no collisions. */ static TileElement* vehicle_check_collision(const CoordsXYZ& vehiclePosition) { TileElement* tileElement = MapGetFirstElementAt(vehiclePosition); if (tileElement == nullptr) { return nullptr; } uint8_t quadrant; if ((vehiclePosition.x & 0x1F) >= 16) { quadrant = 1; if ((vehiclePosition.y & 0x1F) < 16) quadrant = 2; } else { quadrant = 4; if ((vehiclePosition.y & 0x1F) >= 16) quadrant = 8; } do { if (vehiclePosition.z < tileElement->GetBaseZ()) continue; if (vehiclePosition.z >= tileElement->GetClearanceZ()) continue; if (tileElement->GetOccupiedQuadrants() & quadrant) return tileElement; } while (!(tileElement++)->IsLastForTile()); return nullptr; } static void ride_train_crash(Ride& ride, uint16_t numFatalities) { Formatter ft; ft.Add(numFatalities); uint8_t crashType = numFatalities == 0 ? RIDE_CRASH_TYPE_NO_FATALITIES : RIDE_CRASH_TYPE_FATALITIES; if (crashType >= ride.last_crash_type) ride.last_crash_type = crashType; if (numFatalities != 0) { if (gConfigNotifications.RideCasualties) { ride.FormatNameTo(ft); News::AddItemToQueue( News::ItemType::Ride, numFatalities == 1 ? STR_X_PERSON_DIED_ON_X : STR_X_PEOPLE_DIED_ON_X, ride.id.ToUnderlying(), ft); } auto& gameState = GetGameState(); if (gameState.Park.RatingCasualtyPenalty < 500) { gameState.Park.RatingCasualtyPenalty += 200; } } } /** * * rct2: 0x006DE6C6 */ void Vehicle::KillAllPassengersInTrain() { auto curRide = GetRide(); if (curRide == nullptr) return; ride_train_crash(*curRide, NumPeepsUntilTrainTail()); for (Vehicle* trainCar = GetEntity(Id); trainCar != nullptr; trainCar = GetEntity(trainCar->next_vehicle_on_train)) { trainCar->KillPassengers(*curRide); } } void Vehicle::KillPassengers(const Ride& curRide) { if (num_peeps != next_free_seat) return; if (num_peeps == 0) return; for (auto i = 0; i < num_peeps; i++) { auto* curPeep = GetEntity(peep[i]); if (curPeep == nullptr) continue; if (!curPeep->OutsideOfPark) { DecrementGuestsInPark(); auto intent = Intent(INTENT_ACTION_UPDATE_GUEST_COUNT); ContextBroadcastIntent(&intent); } PeepEntityRemove(curPeep); } num_peeps = 0; next_free_seat = 0; } void Vehicle::CrashOnLand() { auto curRide = GetRide(); if (curRide == nullptr) return; if (curRide->status == RideStatus::Simulating) { SimulateCrash(); return; } SetState(Vehicle::Status::Crashed, sub_state); #ifdef ENABLE_SCRIPTING InvokeVehicleCrashHook(Id, "land"); #endif if (!(curRide->lifecycle_flags & RIDE_LIFECYCLE_CRASHED)) { auto frontVehicle = GetHead(); auto trainIndex = ride_get_train_index_from_vehicle(*curRide, frontVehicle->Id); if (!trainIndex.has_value()) { return; } curRide->Crash(trainIndex.value()); if (curRide->status != RideStatus::Closed) { // We require this to execute right away during the simulation, always ignore network and queue. auto gameAction = RideSetStatusAction(curRide->id, RideStatus::Closed); GameActions::ExecuteNested(&gameAction); } } curRide->lifecycle_flags |= RIDE_LIFECYCLE_CRASHED; curRide->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAIN | RIDE_INVALIDATE_RIDE_LIST; if (IsHead()) { KillAllPassengersInTrain(); } sub_state = 2; const auto curLoc = GetLocation(); OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::Crash, curLoc); ExplosionCloud::Create(curLoc); ExplosionFlare::Create(curLoc); uint8_t numParticles = std::min(SpriteData.Width, static_cast(7)); while (numParticles-- != 0) VehicleCrashParticle::Create(colours, curLoc); SetFlag(VehicleFlags::Crashed); animation_frame = 0; animationState = 0; SpriteData.Width = 13; SpriteData.HeightMin = 45; SpriteData.HeightMax = 5; MoveTo(curLoc); crash_z = 0; } void Vehicle::CrashOnWater() { auto curRide = GetRide(); if (curRide == nullptr) return; if (curRide->status == RideStatus::Simulating) { SimulateCrash(); return; } SetState(Vehicle::Status::Crashed, sub_state); #ifdef ENABLE_SCRIPTING InvokeVehicleCrashHook(Id, "water"); #endif if (!(curRide->lifecycle_flags & RIDE_LIFECYCLE_CRASHED)) { auto frontVehicle = GetHead(); auto trainIndex = ride_get_train_index_from_vehicle(*curRide, frontVehicle->Id); if (!trainIndex.has_value()) { return; } curRide->Crash(trainIndex.value()); if (curRide->status != RideStatus::Closed) { // We require this to execute right away during the simulation, always ignore network and queue. auto gameAction = RideSetStatusAction(curRide->id, RideStatus::Closed); GameActions::ExecuteNested(&gameAction); } } curRide->lifecycle_flags |= RIDE_LIFECYCLE_CRASHED; curRide->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAIN | RIDE_INVALIDATE_RIDE_LIST; if (IsHead()) { KillAllPassengersInTrain(); } sub_state = 2; const auto curLoc = GetLocation(); OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::Water1, curLoc); CrashSplashParticle::Create(curLoc); CrashSplashParticle::Create(curLoc + CoordsXYZ{ -8, -9, 0 }); CrashSplashParticle::Create(curLoc + CoordsXYZ{ 11, -9, 0 }); CrashSplashParticle::Create(curLoc + CoordsXYZ{ 11, 8, 0 }); CrashSplashParticle::Create(curLoc + CoordsXYZ{ -4, 8, 0 }); for (int32_t i = 0; i < 10; ++i) VehicleCrashParticle::Create(colours, curLoc + CoordsXYZ{ -4, 8, 0 }); SetFlag(VehicleFlags::Crashed); animation_frame = 0; animationState = 0; SpriteData.Width = 13; SpriteData.HeightMin = 45; SpriteData.HeightMax = 5; MoveTo(curLoc); crash_z = -1; } /** * * rct2: 0x006D98CA */ void Vehicle::UpdateCrash() { for (Vehicle* curVehicle = GetEntity(Id); curVehicle != nullptr; curVehicle = GetEntity(curVehicle->next_vehicle_on_train)) { CoordsXYZ curPos = curVehicle->GetLocation(); if (curVehicle->sub_state > 1) { if (curVehicle->crash_z <= 96) { curVehicle->crash_z++; if ((ScenarioRand() & 0xFFFF) <= 0x1555) { int32_t xOffset = (ScenarioRand() & 2) - 1; int32_t yOffset = (ScenarioRand() & 2) - 1; ExplosionCloud::Create(curPos + CoordsXYZ{ xOffset, yOffset, 0 }); } } if (curVehicle->animationState <= 0xe388) { curVehicle->animationState += 0x1c71; } else { curVehicle->animationState = 0; curVehicle->animation_frame++; if (curVehicle->animation_frame >= 8) curVehicle->animation_frame = 0; curVehicle->Invalidate(); } continue; } TileElement* collideElement = vehicle_check_collision(curPos); if (collideElement == nullptr) { curVehicle->sub_state = 1; } else if (curVehicle->sub_state == 1) { curVehicle->CrashOnLand(); continue; } int16_t height = TileElementHeight(curPos); int16_t waterHeight = TileElementWaterHeight(curPos); int16_t zDiff; if (waterHeight != 0) { zDiff = curPos.z - waterHeight; if (zDiff <= 0 && zDiff >= -20) { curVehicle->CrashOnWater(); continue; } } zDiff = curPos.z - height; if ((zDiff <= 0 && zDiff >= -20) || curPos.z < 16) { curVehicle->CrashOnLand(); continue; } curVehicle->Invalidate(); curPos.x += static_cast(curVehicle->crash_x >> 8); curPos.y += static_cast(curVehicle->crash_y >> 8); curPos.z += static_cast(curVehicle->crash_z >> 8); curVehicle->TrackLocation = { (curVehicle->crash_x << 8), (curVehicle->crash_y << 8), (curVehicle->crash_z << 8) }; if (!MapIsLocationValid(curPos)) { curVehicle->CrashOnLand(); continue; } curVehicle->MoveTo(curPos); if (curVehicle->sub_state == 1) { curVehicle->crash_z -= 20; } } } /** * * rct2: 0x006D7888 */ void Vehicle::UpdateSound() { // frictionVolume (bl) should be set before hand SoundIdVolume frictionSound = { OpenRCT2::Audio::SoundId::Null, 255 }; // bh screamVolume should be set before hand SoundIdVolume screamSound = { OpenRCT2::Audio::SoundId::Null, 255 }; auto curRide = GetRide(); if (curRide == nullptr) return; auto rideEntry = GetRideEntry(); if (rideEntry == nullptr) return; // Always use the head car's sound data (Some of the other vehicle subtypes have improperly set data) auto soundCarIndex = (rideEntry->FrontCar == 0xff) ? rideEntry->DefaultCar : rideEntry->FrontCar; const auto& carEntry = rideEntry->Cars[soundCarIndex]; int32_t ecx = abs(velocity) - 1.0_mph; if (ecx >= 0) { frictionSound.id = carEntry.friction_sound_id; ecx >>= 15; frictionSound.volume = std::min(208 + (ecx & 0xFF), 255); } const auto currentTicks = GetGameState().CurrentTicks; switch (carEntry.sound_range) { case SOUND_RANGE_WHISTLE: screamSound.id = scream_sound_id; if (!(currentTicks & 0x7F)) { if (velocity < 4.0_mph || scream_sound_id != OpenRCT2::Audio::SoundId::Null) { GetLiftHillSound(*curRide, screamSound); break; } if ((ScenarioRand() & 0xFFFF) <= 0x5555) { scream_sound_id = OpenRCT2::Audio::SoundId::TrainWhistle; screamSound.volume = 255; break; } } if (screamSound.id == OpenRCT2::Audio::SoundId::NoScream) screamSound.id = OpenRCT2::Audio::SoundId::Null; screamSound.volume = 255; break; case SOUND_RANGE_BELL: screamSound.id = scream_sound_id; if (!(currentTicks & 0x7F)) { if (velocity < 4.0_mph || scream_sound_id != OpenRCT2::Audio::SoundId::Null) { GetLiftHillSound(*curRide, screamSound); break; } if ((ScenarioRand() & 0xFFFF) <= 0x5555) { scream_sound_id = OpenRCT2::Audio::SoundId::Tram; screamSound.volume = 255; break; } } if (screamSound.id == OpenRCT2::Audio::SoundId::NoScream) screamSound.id = OpenRCT2::Audio::SoundId::Null; screamSound.volume = 255; break; default: if ((carEntry.flags & CAR_ENTRY_FLAG_RIDERS_SCREAM)) { screamSound.id = UpdateScreamSound(); if (screamSound.id == OpenRCT2::Audio::SoundId::NoScream) { screamSound.id = OpenRCT2::Audio::SoundId::Null; break; } if (screamSound.id != OpenRCT2::Audio::SoundId::Null) { break; } } GetLiftHillSound(*curRide, screamSound); } // Friction sound auto soundIdVolume = Sub6D7AC0(sound1_id, sound1_volume, frictionSound.id, frictionSound.volume); sound1_id = soundIdVolume.id; sound1_volume = soundIdVolume.volume; // Scream sound soundIdVolume = Sub6D7AC0(sound2_id, sound2_volume, screamSound.id, screamSound.volume); sound2_id = soundIdVolume.id; sound2_volume = soundIdVolume.volume; // Calculate Sound Vector (used for sound frequency calcs) int32_t soundDirection = SpriteDirectionToSoundDirection[Orientation]; int32_t soundVector = ((velocity >> 14) * soundDirection) >> 14; soundVector = std::clamp(soundVector, -127, 127); sound_vector_factor = soundVector & 0xFF; } /** * * rct2: 0x006D796B */ OpenRCT2::Audio::SoundId Vehicle::UpdateScreamSound() { int32_t totalNumPeeps = NumPeepsUntilTrainTail(); if (totalNumPeeps == 0) return OpenRCT2::Audio::SoundId::Null; if (velocity < 0) { if (velocity > -2.75_mph) return OpenRCT2::Audio::SoundId::Null; for (Vehicle* vehicle2 = GetEntity(Id); vehicle2 != nullptr; vehicle2 = GetEntity(vehicle2->next_vehicle_on_train)) { if (vehicle2->Pitch < 1) continue; if (vehicle2->Pitch <= 4) return ProduceScreamSound(totalNumPeeps); if (vehicle2->Pitch < 9) continue; if (vehicle2->Pitch <= 15) return ProduceScreamSound(totalNumPeeps); // Pitch 52 occurs on steep diagonal backward drops. // (50 and 51 occur on gentle ones.) if (vehicle2->Pitch == 52) return ProduceScreamSound(totalNumPeeps); } return OpenRCT2::Audio::SoundId::Null; } if (velocity < 2.75_mph) return OpenRCT2::Audio::SoundId::Null; for (Vehicle* vehicle2 = GetEntity(Id); vehicle2 != nullptr; vehicle2 = GetEntity(vehicle2->next_vehicle_on_train)) { if (vehicle2->Pitch < 5) continue; if (vehicle2->Pitch <= 8) return ProduceScreamSound(totalNumPeeps); if (vehicle2->Pitch < 17) continue; if (vehicle2->Pitch <= 23) return ProduceScreamSound(totalNumPeeps); // Pitch 55 occurs on steep diagonal drops. // (53 and 54 occur on gentle ones.) if (vehicle2->Pitch == 55) return ProduceScreamSound(totalNumPeeps); } return OpenRCT2::Audio::SoundId::Null; } OpenRCT2::Audio::SoundId Vehicle::ProduceScreamSound(const int32_t totalNumPeeps) { const auto* rideEntry = GetRideEntry(); const auto& carEntry = rideEntry->Cars[vehicle_type]; if (scream_sound_id == OpenRCT2::Audio::SoundId::Null) { auto r = ScenarioRand(); if (totalNumPeeps >= static_cast(r % 16)) { switch (carEntry.sound_range) { case SOUND_RANGE_SCREAMS_0: scream_sound_id = _screamSet0[r % std::size(_screamSet0)]; break; case SOUND_RANGE_SCREAMS_1_WOODEN_COASTERS: scream_sound_id = _screamSet1Wooden[r % std::size(_screamSet1Wooden)]; break; case SOUND_RANGE_SCREAMS_2: scream_sound_id = _screamSet2[r % std::size(_screamSet2)]; break; default: scream_sound_id = OpenRCT2::Audio::SoundId::NoScream; break; } } else { scream_sound_id = OpenRCT2::Audio::SoundId::NoScream; } } return scream_sound_id; } /** * * rct2: 0x006D73D0 * ax: verticalG * dx: lateralG * esi: vehicle */ GForces Vehicle::GetGForces() const { int32_t gForceVert = ((static_cast(0x280000)) * Unk9A37E4[Pitch]) >> 32; gForceVert = ((static_cast(gForceVert)) * Unk9A39C4[bank_rotation]) >> 32; const auto& ted = GetTrackElementDescriptor(GetTrackType()); const int32_t vertFactor = ted.VerticalFactor(track_progress); const int32_t lateralFactor = ted.LateralFactor(track_progress); int32_t gForceLateral = 0; if (vertFactor != 0) { gForceVert += abs(velocity) * 98 / vertFactor; } if (lateralFactor != 0) { gForceLateral += abs(velocity) * 98 / lateralFactor; } gForceVert *= 10; gForceLateral *= 10; gForceVert >>= 16; gForceLateral >>= 16; return { static_cast(gForceVert & 0xFFFF), static_cast(gForceLateral & 0xFFFF) }; } void Vehicle::SetMapToolbar() const { auto curRide = GetRide(); if (curRide != nullptr && curRide->type < RIDE_TYPE_COUNT) { const Vehicle* vehicle = GetHead(); if (vehicle == nullptr) return; size_t vehicleIndex; for (vehicleIndex = 0; vehicleIndex < std::size(curRide->vehicles); vehicleIndex++) if (curRide->vehicles[vehicleIndex] == vehicle->Id) break; auto ft = Formatter(); ft.Add(STR_RIDE_MAP_TIP); ft.Add(STR_MAP_TOOLTIP_STRINGID_STRINGID); curRide->FormatNameTo(ft); ft.Add(GetRideComponentName(GetRideTypeDescriptor(curRide->type).NameConvention.vehicle).capitalised); ft.Add(vehicleIndex + 1); curRide->FormatStatusTo(ft); auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP); intent.PutExtra(INTENT_EXTRA_FORMATTER, &ft); ContextBroadcastIntent(&intent); } } Vehicle* Vehicle::TrainHead() const { const Vehicle* vehicle = this; Vehicle* prevVehicle; for (;;) { prevVehicle = GetEntity(vehicle->prev_vehicle_on_ride); if (prevVehicle == nullptr) return nullptr; if (prevVehicle->next_vehicle_on_train.IsNull()) break; vehicle = prevVehicle; } return const_cast(vehicle); } Vehicle* Vehicle::TrainTail() const { const Vehicle* vehicle = this; EntityId spriteIndex = vehicle->next_vehicle_on_train; while (!spriteIndex.IsNull()) { vehicle = GetEntity(spriteIndex); if (vehicle == nullptr) { return const_cast(this); } spriteIndex = vehicle->next_vehicle_on_train; } return const_cast(vehicle); } int32_t Vehicle::IsUsedInPairs() const { return num_seats & kVehicleSeatPairFlag; } /** * * rct2: 0x006DA44E */ int32_t Vehicle::UpdateMotionDodgems() { _vehicleMotionTrackFlags = 0; auto curRide = GetRide(); if (curRide == nullptr) return _vehicleMotionTrackFlags; int32_t nextVelocity = velocity + acceleration; if (curRide->lifecycle_flags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN) && curRide->breakdown_reason_pending == BREAKDOWN_SAFETY_CUT_OUT) { nextVelocity = 0; } velocity = nextVelocity; _vehicleVelocityF64E08 = nextVelocity; _vehicleVelocityF64E0C = (nextVelocity / 1024) * 42; _vehicleUnkF64E10 = 1; acceleration = 0; if (!(curRide->lifecycle_flags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN)) || curRide->breakdown_reason_pending != BREAKDOWN_SAFETY_CUT_OUT) { if ((GetGameState().CurrentTicks & 1) && var_34 != 0) { if (var_34 > 0) { var_34--; Orientation += 2; } else { var_34++; Orientation -= 2; } Orientation &= 0x1E; Invalidate(); } else if ((ScenarioRand() & 0xFFFF) <= 2849) { if (var_35 & (1 << 6)) Orientation -= 2; else Orientation += 2; Orientation &= 0x1E; Invalidate(); } } std::optional collideSprite; if (DodgemsCollisionDirection != 0) { uint8_t oldCollisionDirection = DodgemsCollisionDirection & 0x1E; DodgemsCollisionDirection = 0; CoordsXYZ location = { x, y, z }; location.x += Unk9A36C4[oldCollisionDirection].x; location.y += Unk9A36C4[oldCollisionDirection].y; location.x += Unk9A36C4[oldCollisionDirection + 1].x; location.y += Unk9A36C4[oldCollisionDirection + 1].y; if (collideSprite = DodgemsCarWouldCollideAt(location); !collideSprite.has_value()) { MoveTo(location); } } remaining_distance += _vehicleVelocityF64E0C; if (remaining_distance >= 13962) { sound2_flags &= ~VEHICLE_SOUND2_FLAGS_LIFT_HILL; _vehicleCurPosition.x = x; _vehicleCurPosition.y = y; _vehicleCurPosition.z = z; while (true) { var_35++; uint8_t direction = Orientation; direction |= var_35 & 1; CoordsXY location = _vehicleCurPosition; location.x += Unk9A36C4[direction].x; location.y += Unk9A36C4[direction].y; if (collideSprite = DodgemsCarWouldCollideAt(location); collideSprite.has_value()) { break; } remaining_distance -= Unk9A36C4[direction].distance; _vehicleCurPosition.x = location.x; _vehicleCurPosition.y = location.y; if (remaining_distance < 13962) { break; } _vehicleUnkF64E10++; } if (remaining_distance >= 13962) { int32_t oldVelocity = velocity; remaining_distance = 0; velocity = 0; uint8_t direction = Orientation | 1; Vehicle* collideVehicle = GetEntity(collideSprite.value()); if (collideVehicle != nullptr) { var_34 = (ScenarioRand() & 1) ? 1 : -1; if (oldVelocity >= 2.0_mph) { collideVehicle->DodgemsCollisionDirection = direction; DodgemsCollisionDirection = direction ^ (1 << 4); } } else { var_34 = (ScenarioRand() & 1) ? 6 : -6; if (oldVelocity >= 2.0_mph) { DodgemsCollisionDirection = direction ^ (1 << 4); } } } MoveTo(_vehicleCurPosition); } int32_t eax = velocity / 2; int32_t edx = velocity >> 8; edx *= edx; if (velocity < 0) edx = -edx; edx >>= 5; eax += edx; if (mass != 0) { eax /= mass; } const auto* rideEntry = GetRideEntry(); const auto& carEntry = rideEntry->Cars[vehicle_type]; if (!(carEntry.flags & CAR_ENTRY_FLAG_POWERED)) { acceleration = -eax; return _vehicleMotionTrackFlags; } int32_t momentum = (speed * mass) >> 2; int32_t _eax = speed << 14; if (HasFlag(VehicleFlags::PoweredCarInReverse)) { _eax = -_eax; } _eax -= velocity; _eax *= powered_acceleration * 2; if (momentum != 0) _eax /= momentum; acceleration = _eax - eax; return _vehicleMotionTrackFlags; } /** * * rct2: 0x006DD365 */ static bool wouldCollideWithDodgemsTrackEdge( const CoordsXY& coords, const CoordsXY& trackLocation, uint32_t trackType, uint16_t dodgemsCarRadius) { int16_t rideLeft = trackLocation.x + GetDodgemsTrackSize(trackType).left; int16_t rideRight = trackLocation.x + GetDodgemsTrackSize(trackType).right; int16_t rideTop = trackLocation.y + GetDodgemsTrackSize(trackType).top; int16_t rideBottom = trackLocation.y + GetDodgemsTrackSize(trackType).bottom; return coords.x - dodgemsCarRadius < rideLeft || coords.y - dodgemsCarRadius < rideTop || coords.x + dodgemsCarRadius > rideRight || coords.y + dodgemsCarRadius > rideBottom; } std::optional Vehicle::DodgemsCarWouldCollideAt(const CoordsXY& coords) const { auto trackType = GetTrackType(); if (wouldCollideWithDodgemsTrackEdge(coords, TrackLocation, trackType, (var_44 * 30) >> 9)) { return EntityId::GetNull(); } auto location = coords; RideId rideIndex = ride; for (auto xy_offset : SurroundingTiles) { location += xy_offset; for (auto vehicle2 : EntityTileList(location)) { if (vehicle2 == this) continue; if (vehicle2->ride != rideIndex) continue; int32_t distX = abs(coords.x - vehicle2->x); if (distX > 32768) continue; int32_t distY = abs(coords.y - vehicle2->y); if (distY > 32768) continue; int32_t ecx = (var_44 + vehicle2->var_44) / 2; ecx *= 30; ecx >>= 8; if (std::max(distX, distY) < ecx) { return vehicle2->Id; } } } return std::nullopt; } /** * * rct2: 0x006DAB90 */ void Vehicle::UpdateTrackMotionUpStopCheck() const { const auto* carEntry = Entry(); if (carEntry == nullptr) { return; } // No up stops (coaster types) if (carEntry->flags & CAR_ENTRY_FLAG_NO_UPSTOP_WHEELS) { auto trackType = GetTrackType(); if (!TrackElementIsCovered(trackType)) { auto gForces = GetGForces(); gForces.LateralG = std::abs(gForces.LateralG); if (gForces.LateralG <= 150) { if (AccelerationFromPitch[Pitch] < 0) { if (gForces.VerticalG > -40) { return; } } else if (gForces.VerticalG > -80) { return; } } if (Pitch != 8) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_DERAILED; } } } else if (carEntry->flags & CAR_ENTRY_FLAG_NO_UPSTOP_BOBSLEIGH) { // No up stops bobsleigh type auto trackType = GetTrackType(); if (!TrackElementIsCovered(trackType)) { auto gForces = GetGForces(); if (AccelerationFromPitch[Pitch] < 0) { if (gForces.VerticalG > -45) { return; } } else { if (gForces.VerticalG > -80) { return; } } if (Pitch != 8 && Pitch != 55) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_DERAILED; } } } } /** * Modifies the train's velocity to match the block-brake fixed velocity. * This function must be called when the car is running through a non-stopping * state block-brake (precondition), which means that the block brake is acting * merely as a velocity regulator, in a closed state. When the brake is open, it * boosts the train to the speed limit */ void Vehicle::ApplyNonStopBlockBrake() { if (velocity >= 0) { // If the vehicle is below the speed limit if (velocity <= kBlockBrakeBaseSpeed) { // Boost it to the fixed block brake speed velocity = kBlockBrakeBaseSpeed; acceleration = 0; } else if (velocity > (brake_speed << 16) + kBlockBrakeSpeedOffset) { velocity -= velocity >> 4; acceleration = 0; } } } /** * * Modifies the train's velocity influenced by a block brake */ void Vehicle::ApplyStopBlockBrake() { // Slow it down till completely stop the car _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_AT_BLOCK_BRAKE; acceleration = 0; // If the this is slow enough, stop it. If not, slow it down if (velocity <= 2.0_mph) { velocity = 0; } else { velocity -= velocity >> 3; } } /** * * rct2: 0x006DAC43 */ void Vehicle::CheckAndApplyBlockSectionStopSite() { auto curRide = GetRide(); if (curRide == nullptr) return; auto carEntry = Entry(); if (carEntry == nullptr) return; // Is chair lift type if (carEntry->flags & CAR_ENTRY_FLAG_CHAIRLIFT) { velocity = _vehicleBreakdown == 0 ? 0 : curRide->speed << 16; acceleration = 0; } auto trackType = GetTrackType(); TileElement* trackElement = MapGetTrackElementAtOfType(TrackLocation, trackType); if (trackElement == nullptr) { return; } switch (trackType) { case TrackElemType::BlockBrakes: case TrackElemType::DiagBlockBrakes: if (curRide->IsBlockSectioned() && trackElement->AsTrack()->IsBrakeClosed()) ApplyStopBlockBrake(); else ApplyNonStopBlockBrake(); break; case TrackElemType::EndStation: if (trackElement->AsTrack()->IsBrakeClosed()) _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_AT_BLOCK_BRAKE; break; case TrackElemType::Up25ToFlat: case TrackElemType::Up60ToFlat: case TrackElemType::CableLiftHill: case TrackElemType::DiagUp25ToFlat: case TrackElemType::DiagUp60ToFlat: if (curRide->IsBlockSectioned()) { if (trackType == TrackElemType::CableLiftHill || trackElement->AsTrack()->HasChain()) { if (trackElement->AsTrack()->IsBrakeClosed()) { ApplyStopBlockBrake(); } } } break; } } /** * * rct2: 0x006DADAE */ void Vehicle::UpdateVelocity() { int32_t nextVelocity = acceleration + velocity; if (HasFlag(VehicleFlags::StoppedOnLift)) { nextVelocity = 0; } if (HasFlag(VehicleFlags::StoppedOnHoldingBrake)) { vertical_drop_countdown--; if (vertical_drop_countdown == -70) { ClearFlag(VehicleFlags::StoppedOnHoldingBrake); } if (vertical_drop_countdown >= 0) { nextVelocity = 0; acceleration = 0; } } velocity = nextVelocity; _vehicleVelocityF64E08 = nextVelocity; _vehicleVelocityF64E0C = (nextVelocity >> 10) * 42; } static void block_brakes_open_previous_section( const Ride& ride, const CoordsXYZ& vehicleTrackLocation, TileElement* tileElement) { auto location = vehicleTrackLocation; TrackBeginEnd trackBeginEnd, slowTrackBeginEnd; TileElement slowTileElement = *tileElement; bool counter = true; CoordsXY slowLocation = location; do { if (!TrackBlockGetPrevious({ location, tileElement }, &trackBeginEnd)) { return; } if (trackBeginEnd.begin_x == vehicleTrackLocation.x && trackBeginEnd.begin_y == vehicleTrackLocation.y && tileElement == trackBeginEnd.begin_element) { return; } location.x = trackBeginEnd.end_x; location.y = trackBeginEnd.end_y; location.z = trackBeginEnd.begin_z; tileElement = trackBeginEnd.begin_element; // #2081: prevent infinite loop counter = !counter; if (counter) { TrackBlockGetPrevious({ slowLocation, &slowTileElement }, &slowTrackBeginEnd); slowLocation.x = slowTrackBeginEnd.end_x; slowLocation.y = slowTrackBeginEnd.end_y; slowTileElement = *(slowTrackBeginEnd.begin_element); if (slowLocation == location && slowTileElement.GetBaseZ() == tileElement->GetBaseZ() && slowTileElement.GetType() == tileElement->GetType() && slowTileElement.GetDirection() == tileElement->GetDirection()) { return; } } } while (!(trackBeginEnd.begin_element->AsTrack()->IsBlockStart())); // Get the start of the track block instead of the end location = { trackBeginEnd.begin_x, trackBeginEnd.begin_y, trackBeginEnd.begin_z }; auto trackOrigin = MapGetTrackElementAtOfTypeSeq(location, trackBeginEnd.begin_element->AsTrack()->GetTrackType(), 0); if (trackOrigin == nullptr) { return; } auto trackElement = trackOrigin->AsTrack(); SetBrakeClosedMultiTile(*trackElement, location, false); MapInvalidateElement(location, reinterpret_cast(trackElement)); auto trackType = trackElement->GetTrackType(); if (trackType == TrackElemType::EndStation) { OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::BlockBrakeClose, location); } else if (TrackTypeIsBlockBrakes(trackType)) { OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::BlockBrakeClose, location); BlockBrakeSetLinkedBrakesClosed(location, *trackElement, false); } } int32_t Vehicle::GetSwingAmount() const { auto trackType = GetTrackType(); switch (trackType) { case TrackElemType::LeftQuarterTurn5Tiles: case TrackElemType::BankedLeftQuarterTurn5Tiles: case TrackElemType::LeftQuarterTurn5TilesUp25: case TrackElemType::LeftQuarterTurn5TilesDown25: case TrackElemType::LeftQuarterTurn5TilesCovered: case TrackElemType::LeftHalfBankedHelixUpLarge: case TrackElemType::LeftHalfBankedHelixDownLarge: case TrackElemType::LeftQuarterBankedHelixLargeUp: case TrackElemType::LeftQuarterBankedHelixLargeDown: case TrackElemType::LeftQuarterHelixLargeUp: case TrackElemType::LeftQuarterHelixLargeDown: case TrackElemType::LeftBankedQuarterTurn5TileUp25: case TrackElemType::LeftBankedQuarterTurn5TileDown25: // Loc6D67E1 return 14; case TrackElemType::RightQuarterTurn5Tiles: case TrackElemType::BankedRightQuarterTurn5Tiles: case TrackElemType::RightQuarterTurn5TilesUp25: case TrackElemType::RightQuarterTurn5TilesDown25: case TrackElemType::RightQuarterTurn5TilesCovered: case TrackElemType::RightHalfBankedHelixUpLarge: case TrackElemType::RightHalfBankedHelixDownLarge: case TrackElemType::RightQuarterBankedHelixLargeUp: case TrackElemType::RightQuarterBankedHelixLargeDown: case TrackElemType::RightQuarterHelixLargeUp: case TrackElemType::RightQuarterHelixLargeDown: case TrackElemType::RightBankedQuarterTurn5TileUp25: case TrackElemType::RightBankedQuarterTurn5TileDown25: // Loc6D6804 return -14; case TrackElemType::SBendLeft: case TrackElemType::SBendLeftCovered: // Loc6D67EF if (track_progress < 48) { return 14; } return -15; case TrackElemType::SBendRight: case TrackElemType::SBendRightCovered: // Loc6D67CC if (track_progress < 48) { return -14; } return 15; case TrackElemType::LeftQuarterTurn3Tiles: case TrackElemType::LeftBankedQuarterTurn3Tiles: case TrackElemType::LeftQuarterTurn3TilesUp25: case TrackElemType::LeftQuarterTurn3TilesDown25: case TrackElemType::LeftQuarterTurn3TilesCovered: case TrackElemType::LeftHalfBankedHelixUpSmall: case TrackElemType::LeftHalfBankedHelixDownSmall: case TrackElemType::LeftBankToLeftQuarterTurn3TilesUp25: case TrackElemType::LeftQuarterTurn3TilesDown25ToLeftBank: case TrackElemType::LeftCurvedLiftHill: case TrackElemType::LeftBankedQuarterTurn3TileUp25: case TrackElemType::LeftBankedQuarterTurn3TileDown25: // Loc6D67BE return 13; case TrackElemType::RightQuarterTurn3Tiles: case TrackElemType::RightBankedQuarterTurn3Tiles: case TrackElemType::RightQuarterTurn3TilesUp25: case TrackElemType::RightQuarterTurn3TilesDown25: case TrackElemType::RightQuarterTurn3TilesCovered: case TrackElemType::RightHalfBankedHelixUpSmall: case TrackElemType::RightHalfBankedHelixDownSmall: case TrackElemType::RightBankToRightQuarterTurn3TilesUp25: case TrackElemType::RightQuarterTurn3TilesDown25ToRightBank: case TrackElemType::RightCurvedLiftHill: case TrackElemType::RightBankedQuarterTurn3TileUp25: case TrackElemType::RightBankedQuarterTurn3TileDown25: // Loc6D67B0 return -13; case TrackElemType::LeftQuarterTurn1Tile: case TrackElemType::LeftQuarterTurn1TileUp60: case TrackElemType::LeftQuarterTurn1TileDown60: // Loc6D67A2 return 12; case TrackElemType::RightQuarterTurn1Tile: case TrackElemType::RightQuarterTurn1TileUp60: case TrackElemType::RightQuarterTurn1TileDown60: // Loc6D6794 return -12; case TrackElemType::LeftEighthToDiag: case TrackElemType::LeftEighthToOrthogonal: case TrackElemType::LeftEighthBankToDiag: case TrackElemType::LeftEighthBankToOrthogonal: // Loc6D67D3 return 15; case TrackElemType::RightEighthToDiag: case TrackElemType::RightEighthToOrthogonal: case TrackElemType::RightEighthBankToDiag: case TrackElemType::RightEighthBankToOrthogonal: // Loc6D67F6 return -15; } return 0; } static uint8_t GetSwingSprite(int16_t swingPosition) { if (swingPosition < -10012) return 11; if (swingPosition > 10012) return 12; if (swingPosition < -8191) return 9; if (swingPosition > 8191) return 10; if (swingPosition < -6371) return 7; if (swingPosition > 6371) return 8; if (swingPosition < -4550) return 5; if (swingPosition > 4550) return 6; if (swingPosition < -2730) return 3; if (swingPosition > 2730) return 4; if (swingPosition < -910) return 1; if (swingPosition > 910) return 2; return 0; } /** * * rct2: 0x006D6776 */ void Vehicle::UpdateSwingingCar() { int32_t dword_F64E08 = abs(_vehicleVelocityF64E08); if (HasFlag(VehicleFlags::CarIsReversed)) { dword_F64E08 *= -1; } SwingSpeed += (-SwingPosition) >> 6; int32_t swingAmount = GetSwingAmount(); if (swingAmount < 0) { SwingSpeed -= dword_F64E08 >> (-swingAmount); } else if (swingAmount > 0) { SwingSpeed += dword_F64E08 >> swingAmount; } auto carEntry = Entry(); if (carEntry == nullptr) { return; } int16_t dx = 3185; if (carEntry->flags & CAR_ENTRY_FLAG_SUSPENDED_SWING) { dx = 5006; } if (carEntry->flags & CAR_ENTRY_FLAG_WOODEN_WILD_MOUSE_SWING) { dx = 1820; } int16_t cx = -dx; if (carEntry->flags & CAR_ENTRY_FLAG_SLIDE_SWING) { dx = 5370; cx = -5370; auto trackType = GetTrackType(); switch (trackType) { case TrackElemType::BankedLeftQuarterTurn5Tiles: case TrackElemType::LeftBank: case TrackElemType::LeftBankedQuarterTurn3Tiles: dx = 10831; cx = -819; break; case TrackElemType::BankedRightQuarterTurn5Tiles: case TrackElemType::RightBank: case TrackElemType::RightBankedQuarterTurn3Tiles: dx = 819; cx = -10831; break; } if (TrackTypeIsStation(trackType) || TrackTypeIsBrakes(trackType) || TrackTypeIsBlockBrakes(trackType)) { dx = 0; cx = 0; } if (HasFlag(VehicleFlags::OnLiftHill)) { dx = 0; cx = 0; } } SwingPosition += SwingSpeed; SwingSpeed -= SwingSpeed >> 5; if (SwingPosition > dx) { SwingPosition = dx; SwingSpeed = 0; } if (SwingPosition < cx) { SwingPosition = cx; SwingSpeed = 0; } uint8_t swingSprite = GetSwingSprite(SwingPosition); if (swingSprite != SwingSprite) { SwingSprite = swingSprite; Invalidate(); } } /** * * rct2: 0x006D661F */ void Vehicle::UpdateSpinningCar() { if (HasFlag(VehicleFlags::SpinningIsLocked)) { spin_speed = 0; return; } auto carEntry = Entry(); if (carEntry == nullptr) { return; } int32_t spinningInertia = carEntry->spinning_inertia; auto trackType = GetTrackType(); int32_t dword_F64E08 = _vehicleVelocityF64E08; int32_t spinSpeed{}; // An L spin adds to the spin speed, R does the opposite // The number indicates how much right shift of the velocity will become spin // The bigger the number the less change in spin. const auto& ted = GetTrackElementDescriptor(trackType); switch (ted.SpinFunction) { case RC_SPIN: // On a rotation control track element spinningInertia += 6; spinSpeed = dword_F64E08 >> spinningInertia; // Alternate the spin direction (roughly). Perhaps in future save a value to the track if (Id.ToUnderlying() & 1) { spin_speed -= spinSpeed; } else { spin_speed += spinSpeed; } break; case R5_SPIN: // It looks like in the original there was going to be special code for whirlpool // this has been removed and just uses R5_SPIN spinningInertia += 5; spin_speed -= dword_F64E08 >> spinningInertia; break; case L5_SPIN: spinningInertia += 5; spin_speed += dword_F64E08 >> spinningInertia; break; case R7_SPIN: spinningInertia += 7; spin_speed -= dword_F64E08 >> spinningInertia; break; case L7_SPIN: spinningInertia += 7; spin_speed += dword_F64E08 >> spinningInertia; break; case RL_SPIN: // Right Left Curve Track Piece if (track_progress < 48) { // R8_SPIN spinningInertia += 8; spin_speed -= dword_F64E08 >> spinningInertia; break; } [[fallthrough]]; case L9_SPIN: spinningInertia += 9; spin_speed += dword_F64E08 >> spinningInertia; break; case L8_SPIN: spinningInertia += 8; spin_speed += dword_F64E08 >> spinningInertia; break; case SP_SPIN: // On rapids spin after fully on them if (track_progress > 22) { // L5_SPIN spinningInertia += 5; spin_speed += dword_F64E08 >> spinningInertia; } break; case LR_SPIN: // Left Right Curve Track Piece if (track_progress < 48) { // L8_SPIN spinningInertia += 8; spin_speed += dword_F64E08 >> spinningInertia; break; } [[fallthrough]]; case R9_SPIN: spinningInertia += 9; spin_speed -= dword_F64E08 >> spinningInertia; break; case R8_SPIN: spinningInertia += 8; spin_speed -= dword_F64E08 >> spinningInertia; break; } spinSpeed = std::clamp(spin_speed, VEHICLE_MIN_SPIN_SPEED, VEHICLE_MAX_SPIN_SPEED); spin_speed = spinSpeed; spin_sprite += spinSpeed >> 8; // Note this actually increases the spin speed if going right! spin_speed -= spinSpeed >> carEntry->spinning_friction; Invalidate(); } void Vehicle::UpdateAnimationAnimalFlying() { if (animationState > 0) { animationState--; return; } if (animation_frame == 0) { auto trackType = GetTrackType(); TileElement* trackElement = MapGetTrackElementAtOfTypeSeq(TrackLocation, trackType, 0); if (trackElement != nullptr && trackElement->AsTrack()->HasChain()) { // start flapping, bird animation_frame = 1; animationState = 5; Invalidate(); } } else { // continue flapping until reaching frame 0 animation_frame = (animation_frame + 1) % 4; Invalidate(); } // number of frames to skip before updating again constexpr std::array frameWaitTimes = { 5, 3, 5, 3 }; animationState = frameWaitTimes[animation_frame]; } /** * Get the frame of animation for the current animationState based on animation speed and animation frames */ static uint8_t GetTargetFrame(const CarEntry& carEntry, uint32_t animationState) { if (carEntry.AnimationSpeed == 0) return 0; auto targetFrame = animationState / (carEntry.AnimationSpeed << 2); // mask of 0xFF targetFrame &= std::numeric_limits::max(); // multiply by number of frames. After the bitshift 8, the range will be 0 to AnimationFrames - 1 targetFrame *= carEntry.AnimationFrames; return targetFrame >> std::numeric_limits::digits; } /** * Compute the position that steam should be spawned */ static constexpr CoordsXYZ ComputeSteamOffset(int32_t height, int32_t length, uint8_t pitch, uint8_t yaw) { uint8_t trueYaw = OpenRCT2::Entity::Yaw::YawTo64(yaw); auto offsets = PitchToDirectionVectorFromGeometry[pitch]; int32_t projectedRun = (offsets.x * length - offsets.y * height) / 256; int32_t projectedHeight = (offsets.x * height + offsets.y * length) / 256; return { ComputeXYVector(projectedRun, trueYaw), projectedHeight }; } /** * Decide based on current frame and number of frames if a steam particle should be generated on this frame */ static bool ShouldMakeSteam(uint8_t targetFrame, uint8_t animationFrames) { if (animationFrames < 1) return false; // steam is produced twice per wheel revolution return targetFrame == 0 || targetFrame == animationFrames / 2; } /** * Dummy function */ static void AnimateNone(Vehicle& vehicle, const CarEntry& carEntry) { return; } /** * Animate the vehicle based on its speed */ static void AnimateSimpleVehicle(Vehicle& vehicle, const CarEntry& carEntry) { vehicle.animationState += _vehicleVelocityF64E08; uint8_t targetFrame = GetTargetFrame(carEntry, vehicle.animationState); if (vehicle.animation_frame != targetFrame) { vehicle.animation_frame = targetFrame; vehicle.Invalidate(); } } /** * Animate the vehicle based on its speed plus add steam particles */ static void AnimateSteamLocomotive(Vehicle& vehicle, const CarEntry& carEntry) { vehicle.animationState += _vehicleVelocityF64E08; uint8_t targetFrame = GetTargetFrame(carEntry, vehicle.animationState); if (vehicle.animation_frame != targetFrame) { vehicle.animation_frame = targetFrame; if (ShouldMakeSteam(targetFrame, carEntry.AnimationFrames)) { auto curRide = vehicle.GetRide(); if (curRide != nullptr) { if (!RideHasStationShelter(*curRide) || (vehicle.status != Vehicle::Status::MovingToEndOfStation && vehicle.status != Vehicle::Status::Arriving)) { CoordsXYZ steamOffset = ComputeSteamOffset( carEntry.SteamEffect.Vertical, carEntry.SteamEffect.Longitudinal, vehicle.Pitch, vehicle.Orientation); SteamParticle::Create(CoordsXYZ(vehicle.x, vehicle.y, vehicle.z) + steamOffset); } } } vehicle.Invalidate(); } } /** * Animate the vehicle based on its speed. Specialized animation with exactly 2 frames due to how peep animation works. */ static void AnimateSwanBoat(Vehicle& vehicle, const CarEntry& carEntry) { // The animation of swan boats places frames at 0 and 2 instead of 0 and 1 like Water Tricycles due to the second // pair of peeps. The animation technically uses 4 frames, but ignores frames 1 and 3. vehicle.animationState += _vehicleVelocityF64E08; uint8_t targetFrame = GetTargetFrame(carEntry, vehicle.animationState) * 2; if (vehicle.animation_frame != targetFrame) { vehicle.animation_frame = targetFrame; vehicle.Invalidate(); } } /** * Monorail Cycle animation only animates when a peep is present */ static void AnimateMonorailCycle(Vehicle& vehicle, const CarEntry& carEntry) { if (vehicle.num_peeps != 0) { AnimateSimpleVehicle(vehicle, carEntry); } } /** * Observation tower animates at a constant speed continuously */ static void AnimateObservationTower(Vehicle& vehicle, const CarEntry& carEntry) { if (vehicle.animationState <= 0xCCCC) { vehicle.animationState += carEntry.AnimationSpeed; } else { vehicle.animationState = 0; vehicle.animation_frame += 1; vehicle.animation_frame %= carEntry.AnimationFrames; vehicle.Invalidate(); } } /** * seatRotation value of 4 translates to animationFrame value of 0. This function makes that true for any number of animation * frames */ static int16_t MultiDimensionTargetAngle(int16_t seatRotation, int16_t animationFrames) { return ((seatRotation - 4) % animationFrames + animationFrames) % animationFrames; } /** * Multidimension targets a specific animation frame based on track */ static void AnimateMultiDimension(Vehicle& vehicle, const CarEntry& carEntry) { if (vehicle.seat_rotation != vehicle.target_seat_rotation) { if (vehicle.animationState <= 0xCCCC) { vehicle.animationState += carEntry.AnimationSpeed; } else { vehicle.animationState = 0; if (vehicle.seat_rotation >= vehicle.target_seat_rotation) vehicle.seat_rotation--; else vehicle.seat_rotation++; int16_t targetSeatRotation = MultiDimensionTargetAngle(vehicle.seat_rotation, carEntry.AnimationFrames); if (targetSeatRotation != vehicle.animation_frame) { vehicle.animation_frame = targetSeatRotation; vehicle.Invalidate(); } } } } /** * Animal Flying animates only on chainlift and in an unusual way. Made by Spacek531 */ static void AnimateAnimalFlying(Vehicle& vehicle, const CarEntry& carEntry) { vehicle.UpdateAnimationAnimalFlying(); // makes animation play faster with vehicle speed uint8_t targetFrame = abs(_vehicleVelocityF64E08) >> carEntry.AnimationSpeed; vehicle.animationState = std::max(vehicle.animationState - targetFrame, 0u); } using AnimateFunction = void (*)(Vehicle& vehicle, const CarEntry& carEntry); constexpr static const AnimateFunction AnimationFunctions[]{ AnimateNone, AnimateSimpleVehicle, AnimateSteamLocomotive, AnimateSwanBoat, AnimateMonorailCycle, AnimateMultiDimension, AnimateObservationTower, AnimateAnimalFlying, }; static_assert(std::size(AnimationFunctions) == EnumValue(CarEntryAnimation::Count)); /** * * rct2: 0x006D63D4 */ void Vehicle::UpdateAdditionalAnimation() { auto carEntry = Entry(); if (carEntry == nullptr) { return; } if (carEntry->AnimationFrames == 0 || carEntry->animation >= CarEntryAnimation::Count) return; AnimationFunctions[EnumValue(carEntry->animation)](*this, *carEntry); } /** * * rct2: 0x006DEDB1 */ static void play_scenery_door_open_sound(const CoordsXYZ& loc, WallElement* tileElement) { auto* wallEntry = tileElement->GetEntry(); int32_t doorSoundType = WallEntryGetDoorSound(wallEntry); if (doorSoundType != 0) { auto soundId = DoorOpenSoundIds[doorSoundType - 1]; if (soundId != OpenRCT2::Audio::SoundId::Null) { OpenRCT2::Audio::Play3D(soundId, loc); } } } /** * * rct2: 0x006DED7A */ static void play_scenery_door_close_sound(const CoordsXYZ& loc, WallElement* tileElement) { auto* wallEntry = tileElement->GetEntry(); int32_t doorSoundType = WallEntryGetDoorSound(wallEntry); if (doorSoundType != 0) { auto soundId = DoorCloseSoundIds[doorSoundType - 1]; if (soundId != OpenRCT2::Audio::SoundId::Null) { Play3D(soundId, loc); } } } template static void AnimateSceneryDoor(const CoordsXYZD& doorLocation, const CoordsXYZ& trackLocation, bool isLastVehicle) { auto door = MapGetWallElementAt(doorLocation); if (door == nullptr) { return; } if (!isLastVehicle && (door->GetAnimationFrame() == 0)) { door->SetAnimationIsBackwards(isBackwards); door->SetAnimationFrame(1); MapAnimationCreate(MAP_ANIMATION_TYPE_WALL_DOOR, doorLocation); play_scenery_door_open_sound(trackLocation, door); } if (isLastVehicle) { door->SetAnimationIsBackwards(isBackwards); door->SetAnimationFrame(6); play_scenery_door_close_sound(trackLocation, door); } } /** * * rct2: 0x006DEE93 */ void Vehicle::UpdateSceneryDoor() const { auto trackType = GetTrackType(); const auto& ted = GetTrackElementDescriptor(trackType); const PreviewTrack* trackBlock = ted.Block; while ((trackBlock + 1)->index != 255) { trackBlock++; } const TrackCoordinates* trackCoordinates = &ted.Coordinates; auto wallCoords = CoordsXYZ{ x, y, TrackLocation.z - trackBlock->z + trackCoordinates->z_end }.ToTileStart(); int32_t direction = (GetTrackDirection() + trackCoordinates->rotation_end) & 3; AnimateSceneryDoor({ wallCoords, static_cast(direction) }, TrackLocation, next_vehicle_on_train.IsNull()); } template static void AnimateLandscapeDoor(TrackElement* trackElement, bool isLastVehicle) { auto doorState = isBackwards ? trackElement->GetDoorAState() : trackElement->GetDoorBState(); if (!isLastVehicle && doorState == LANDSCAPE_DOOR_CLOSED) { if (isBackwards) trackElement->SetDoorAState(LANDSCAPE_DOOR_OPEN); else trackElement->SetDoorBState(LANDSCAPE_DOOR_OPEN); // TODO: play door open sound } if (isLastVehicle) { if (isBackwards) trackElement->SetDoorAState(LANDSCAPE_DOOR_CLOSED); else trackElement->SetDoorBState(LANDSCAPE_DOOR_CLOSED); // TODO: play door close sound } } void Vehicle::UpdateLandscapeDoor() const { const auto* currentRide = GetRide(); if (currentRide == nullptr || !currentRide->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_HAS_LANDSCAPE_DOORS)) { return; } auto coords = CoordsXYZ{ x, y, TrackLocation.z }.ToTileStart(); auto* tileElement = MapGetTrackElementAtFromRide(coords, ride); if (tileElement != nullptr && tileElement->GetType() == TileElementType::Track) { AnimateLandscapeDoor(tileElement->AsTrack(), next_vehicle_on_train.IsNull()); } } /** * * rct2: 0x006DB38B */ static PitchAndRoll PitchAndRollStart(bool useInvertedSprites, TileElement* tileElement) { auto trackType = tileElement->AsTrack()->GetTrackType(); const auto& ted = GetTrackElementDescriptor(trackType); return PitchAndRoll{ ted.Definition.PitchStart, TrackGetActualBank3(useInvertedSprites, tileElement) }; } void Vehicle::UpdateGoKartAttemptSwitchLanes() { uint16_t probability = 0x8000; if (HasFlag(VehicleFlags::CurrentlyColliding)) { ClearFlag(VehicleFlags::CurrentlyColliding); } else { probability = 0x0A3D; } if ((ScenarioRand() & 0xFFFF) <= probability) { // This changes "riding left" to "moving to right lane" and "riding right" to "moving to left lane". TrackSubposition = VehicleTrackSubposition{ static_cast(static_cast(TrackSubposition) + 2u) }; } } /** * * rct2: 0x006DB545 */ static void trigger_on_ride_photo(const CoordsXYZ& loc, TileElement* tileElement) { tileElement->AsTrack()->SetPhotoTimeout(); MapAnimationCreate(MAP_ANIMATION_TYPE_TRACK_ONRIDEPHOTO, { loc, tileElement->GetBaseZ() }); } /** * * rct2: 0x006DEDE8 */ void Vehicle::UpdateSceneryDoorBackwards() const { auto trackType = GetTrackType(); const auto& ted = GetTrackElementDescriptor(trackType); const PreviewTrack* trackBlock = ted.Block; const TrackCoordinates* trackCoordinates = &ted.Coordinates; auto wallCoords = CoordsXYZ{ TrackLocation, TrackLocation.z - trackBlock->z + trackCoordinates->z_begin }; int32_t direction = (GetTrackDirection() + trackCoordinates->rotation_begin) & 3; direction = DirectionReverse(direction); AnimateSceneryDoor({ wallCoords, static_cast(direction) }, TrackLocation, next_vehicle_on_train.IsNull()); } void Vehicle::UpdateLandscapeDoorBackwards() const { const auto* currentRide = GetRide(); if (currentRide == nullptr || !currentRide->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_HAS_LANDSCAPE_DOORS)) { return; } auto coords = CoordsXYZ{ TrackLocation, TrackLocation.z }; auto* tileElement = MapGetTrackElementAtFromRide(coords, ride); if (tileElement != nullptr && tileElement->GetType() == TileElementType::Track) { AnimateLandscapeDoor(tileElement->AsTrack(), next_vehicle_on_train.IsNull()); } } static void vehicle_update_play_water_splash_sound() { if (_vehicleVelocityF64E08 <= kBlockBrakeBaseSpeed) { return; } OpenRCT2::Audio::Play3D( OpenRCT2::Audio::SoundId::WaterSplash, { _vehicleCurPosition.x, _vehicleCurPosition.y, _vehicleCurPosition.z }); } /** * * rct2: 0x006DB59E */ void Vehicle::UpdateHandleWaterSplash() const { const auto* rideEntry = GetRideEntry(); auto trackType = GetTrackType(); if (!(rideEntry->flags & RIDE_ENTRY_FLAG_PLAY_SPLASH_SOUND)) { if (rideEntry->flags & RIDE_ENTRY_FLAG_PLAY_SPLASH_SOUND_SLIDE) { if (IsHead()) { if (TrackElementIsCovered(trackType)) { Vehicle* nextVehicle = GetEntity(next_vehicle_on_ride); if (nextVehicle == nullptr) return; Vehicle* nextNextVehicle = GetEntity(nextVehicle->next_vehicle_on_ride); if (nextNextVehicle == nullptr) return; if (!TrackElementIsCovered(nextNextVehicle->GetTrackType())) { if (track_progress == 4) { vehicle_update_play_water_splash_sound(); } } } } } } else { if (trackType == TrackElemType::Down25ToFlat) { if (track_progress == 12) { vehicle_update_play_water_splash_sound(); } } } if (IsHead()) { if (trackType == TrackElemType::Watersplash) { if (track_progress == 48) { vehicle_update_play_water_splash_sound(); } } } } /** * * rct2: 0x006DB807 */ void Vehicle::UpdateReverserCarBogies() { const auto moveInfo = GetMoveInfo(); MoveTo({ TrackLocation.x + moveInfo->x, TrackLocation.y + moveInfo->y, z }); } bool Vehicle::IsCableLift() const { return ride_subtype == OBJECT_ENTRY_INDEX_NULL; } /** * Collision Detection * rct2: 0x006DD078 * @param vehicle (esi) * @param x (ax) * @param y (cx) * @param z (dx) * @param otherVehicleIndex (bp) */ bool Vehicle::UpdateMotionCollisionDetection(const CoordsXYZ& loc, EntityId* otherVehicleIndex) { if (HasFlag(VehicleFlags::CollisionDisabled)) return false; auto carEntry = Entry(); if (carEntry == nullptr) { return false; } if (!(carEntry->flags & CAR_ENTRY_FLAG_BOAT_HIRE_COLLISION_DETECTION)) { CollisionDetectionTimer = 0; // If hacking boat hire rides you can end up here if (otherVehicleIndex == nullptr) return false; Vehicle* collideVehicle = GetEntity(*otherVehicleIndex); if (collideVehicle == nullptr) return false; if (this == collideVehicle) return false; int32_t x_diff = abs(loc.x - collideVehicle->x); if (x_diff > 0x7FFF) return false; int32_t y_diff = abs(loc.y - collideVehicle->y); if (y_diff > 0x7FFF) return false; int32_t z_diff = abs(loc.z - collideVehicle->z); if (x_diff + y_diff + z_diff > 0xFFFF) return false; uint16_t ecx = std::min(var_44 + collideVehicle->var_44, 560); ecx = ((ecx >> 1) * 30) >> 8; if (x_diff + y_diff + z_diff >= ecx) return false; uint8_t direction = (Orientation - collideVehicle->Orientation + 7) & 0x1F; return direction < 0xF; } CoordsXY location = loc; bool mayCollide = false; Vehicle* collideVehicle = nullptr; for (auto xy_offset : SurroundingTiles) { location += xy_offset; for (auto vehicle2 : EntityTileList(location)) { if (vehicle2 == this) continue; int32_t z_diff = abs(vehicle2->z - loc.z); if (z_diff > 16) continue; if (vehicle2->IsCableLift()) continue; auto collideCarEntry = vehicle2->Entry(); if (collideCarEntry == nullptr) continue; if (!(collideCarEntry->flags & CAR_ENTRY_FLAG_BOAT_HIRE_COLLISION_DETECTION)) continue; uint32_t x_diff = abs(vehicle2->x - loc.x); if (x_diff > 0x7FFF) continue; uint32_t y_diff = abs(vehicle2->y - loc.y); if (y_diff > 0x7FFF) continue; VehicleTrackSubposition cl = std::min(TrackSubposition, vehicle2->TrackSubposition); VehicleTrackSubposition ch = std::max(TrackSubposition, vehicle2->TrackSubposition); if (cl != ch) { if (cl == VehicleTrackSubposition::GoKartsLeftLane && ch == VehicleTrackSubposition::GoKartsRightLane) continue; } uint32_t ecx = var_44 + vehicle2->var_44; ecx = ((ecx >> 1) * 30) >> 8; if (x_diff + y_diff >= ecx) continue; if (!(collideCarEntry->flags & CAR_ENTRY_FLAG_GO_KART)) { collideVehicle = vehicle2; mayCollide = true; break; } uint8_t direction = (Orientation - vehicle2->Orientation - 6) & 0x1F; if (direction < 0x14) continue; uint32_t offsetSpriteDirection = (Orientation + 4) & 31; uint32_t offsetDirection = offsetSpriteDirection >> 3; uint32_t next_x_diff = abs(loc.x + AvoidCollisionMoveOffset[offsetDirection].x - vehicle2->x); uint32_t next_y_diff = abs(loc.y + AvoidCollisionMoveOffset[offsetDirection].y - vehicle2->y); if (next_x_diff + next_y_diff < x_diff + y_diff) { collideVehicle = vehicle2; mayCollide = true; break; } } if (mayCollide) { break; } } if (!mayCollide) { CollisionDetectionTimer = 0; return false; } CollisionDetectionTimer++; if (CollisionDetectionTimer < 200) { SetFlag(VehicleFlags::CurrentlyColliding); if (otherVehicleIndex != nullptr) *otherVehicleIndex = collideVehicle->Id; return true; } // TODO Is it possible for collideVehicle to be NULL? if (status == Vehicle::Status::MovingToEndOfStation) { if (Orientation == 0) { if (x <= collideVehicle->x) { return false; } } else if (Orientation == 8) { if (y >= collideVehicle->y) { return false; } } else if (Orientation == 16) { if (x >= collideVehicle->x) { return false; } } else if (Orientation == 24) { if (y <= collideVehicle->y) { return false; } } } if (collideVehicle->status == Vehicle::Status::TravellingBoat && status != Vehicle::Status::Arriving && status != Vehicle::Status::Travelling) { return false; } SetFlag(VehicleFlags::CurrentlyColliding); if (otherVehicleIndex != nullptr) *otherVehicleIndex = collideVehicle->Id; return true; } /** * * rct2: 0x006DB7D6 */ void Vehicle::ReverseReverserCar() { Vehicle* previousVehicle = GetEntity(prev_vehicle_on_ride); Vehicle* nextVehicle = GetEntity(next_vehicle_on_ride); if (previousVehicle == nullptr || nextVehicle == nullptr) { return; } track_progress = 168; vehicle_type ^= 1; previousVehicle->track_progress = 86; nextVehicle->track_progress = 158; nextVehicle->UpdateReverserCarBogies(); previousVehicle->UpdateReverserCarBogies(); } /** * * rct2: 0x006DBF3E */ void Vehicle::Sub6DBF3E() { const auto* carEntry = Entry(); acceleration /= _vehicleUnkF64E10; if (TrackSubposition == VehicleTrackSubposition::ChairliftGoingBack) { return; } auto trackType = GetTrackType(); const auto& ted = GetTrackElementDescriptor(trackType); if (!(std::get<0>(ted.SequenceProperties) & TRACK_SEQUENCE_FLAG_ORIGIN)) { return; } _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_3; TileElement* tileElement = nullptr; if (MapIsLocationValid(TrackLocation)) { tileElement = MapGetTrackElementAtOfTypeSeq(TrackLocation, trackType, 0); } if (tileElement == nullptr) { return; } if (_vehicleStationIndex.IsNull()) { _vehicleStationIndex = tileElement->AsTrack()->GetStationIndex(); } if (trackType == TrackElemType::TowerBase && this == gCurrentVehicle) { if (track_progress > 3 && !HasFlag(VehicleFlags::PoweredCarInReverse)) { CoordsXYE output; int32_t outputZ, outputDirection; CoordsXYE input = { TrackLocation, tileElement }; if (!TrackBlockGetNext(&input, &output, &outputZ, &outputDirection)) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_12; } } if (track_progress <= 3) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_AT_STATION; } } if (trackType != TrackElemType::EndStation || this != gCurrentVehicle) { return; } uint16_t ax = track_progress; if (_vehicleVelocityF64E08 < 0) { if (ax <= 22) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_AT_STATION; } } else { uint16_t cx = 17; if (carEntry->flags & CAR_ENTRY_FLAG_CHAIRLIFT) { cx = 6; } if (carEntry->flags & CAR_ENTRY_FLAG_GO_KART) { // Determine the stop positions for the karts. If in left lane it's further along the track than the right lane. // Since it's not possible to overtake when the race has ended, this does not check for overtake states (7 and // 8). cx = TrackSubposition == VehicleTrackSubposition::GoKartsRightLane ? 18 : 20; } if (ax > cx) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_AT_STATION; } } } /** * Determine whether to use block brake speed or brake speed. If block brake is closed or no block brake present, use the * brake's speed; if block brake is open, use maximum of brake speed or block brake speed. */ uint8_t Vehicle::ChooseBrakeSpeed() const { if (!TrackTypeIsBrakes(GetTrackType())) return brake_speed; auto trackElement = MapGetTrackElementAtOfTypeSeq(TrackLocation, GetTrackType(), 0); if (trackElement != nullptr) { if (trackElement->AsTrack()->IsBrakeClosed()) return brake_speed; else return std::max(brake_speed, BlockBrakeSpeed); } return brake_speed; } /** * Populate the vehicle's brake_speed and BlockBrakeSpeed values. */ void Vehicle::PopulateBrakeSpeed(const CoordsXYZ& vehicleTrackLocation, TrackElement& brake) { auto trackSpeed = brake.GetBrakeBoosterSpeed(); brake_speed = trackSpeed; if (!TrackTypeIsBrakes(brake.GetTrackType())) { BlockBrakeSpeed = trackSpeed; return; } // As soon as feasible, encode block brake speed into track element so the lookforward can be skipped here. CoordsXYE output = CoordsXYE(vehicleTrackLocation.x, vehicleTrackLocation.y, reinterpret_cast(&brake)); int32_t outputZ = vehicleTrackLocation.z; uint16_t timeoutCount = 256; do { if (TrackTypeIsBlockBrakes(output.element->AsTrack()->GetTrackType())) { BlockBrakeSpeed = output.element->AsTrack()->GetBrakeBoosterSpeed(); return; } if (!TrackTypeIsBrakes(output.element->AsTrack()->GetTrackType())) { break; } timeoutCount--; } while (TrackBlockGetNext(&output, &output, &outputZ, nullptr) && timeoutCount); // If block brake is not found, use the track's speed BlockBrakeSpeed = trackSpeed; } /** * * rct2: 0x006DB08C */ bool Vehicle::UpdateTrackMotionForwardsGetNewTrack(uint16_t trackType, const Ride& curRide, const RideObjectEntry& rideEntry) { CoordsXYZD location = {}; auto pitchAndRollEnd = TrackPitchAndRollEnd(trackType); TileElement* tileElement = MapGetTrackElementAtOfTypeSeq(TrackLocation, trackType, 0); if (tileElement == nullptr) { return false; } if (trackType == TrackElemType::CableLiftHill && this == gCurrentVehicle) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_11; } if (tileElement->AsTrack()->IsBlockStart()) { if (next_vehicle_on_train.IsNull()) { SetBrakeClosedMultiTile(*tileElement->AsTrack(), TrackLocation, true); if (TrackTypeIsBlockBrakes(trackType) || trackType == TrackElemType::EndStation) { if (!(rideEntry.Cars[0].flags & CAR_ENTRY_FLAG_POWERED)) { OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::BlockBrakeRelease, TrackLocation); } } MapInvalidateElement(TrackLocation, tileElement); block_brakes_open_previous_section(curRide, TrackLocation, tileElement); if (TrackTypeIsBlockBrakes(trackType)) { BlockBrakeSetLinkedBrakesClosed(TrackLocation, *tileElement->AsTrack(), true); } } } // Change from original: this used to check if the vehicle allowed doors. UpdateSceneryDoor(); UpdateLandscapeDoor(); bool isGoingBack = false; switch (TrackSubposition) { case VehicleTrackSubposition::ChairliftGoingBack: case VehicleTrackSubposition::ChairliftEndBullwheel: TrackSubposition = VehicleTrackSubposition::ChairliftGoingBack; isGoingBack = true; break; case VehicleTrackSubposition::ChairliftStartBullwheel: TrackSubposition = VehicleTrackSubposition::ChairliftGoingOut; break; case VehicleTrackSubposition::GoKartsMovingToRightLane: TrackSubposition = VehicleTrackSubposition::GoKartsRightLane; break; case VehicleTrackSubposition::GoKartsMovingToLeftLane: TrackSubposition = VehicleTrackSubposition::GoKartsLeftLane; break; default: break; } if (isGoingBack) { TrackBeginEnd trackBeginEnd; if (!TrackBlockGetPrevious({ TrackLocation, tileElement }, &trackBeginEnd)) { return false; } location.x = trackBeginEnd.begin_x; location.y = trackBeginEnd.begin_y; location.z = trackBeginEnd.begin_z; location.direction = trackBeginEnd.begin_direction; tileElement = trackBeginEnd.begin_element; } else { { int32_t curZ, direction; CoordsXYE xyElement = { TrackLocation, tileElement }; if (!TrackBlockGetNext(&xyElement, &xyElement, &curZ, &direction)) { return false; } tileElement = xyElement.element; location = { xyElement, curZ, static_cast(direction) }; } if (tileElement->AsTrack()->GetTrackType() == TrackElemType::LeftReverser || tileElement->AsTrack()->GetTrackType() == TrackElemType::RightReverser) { if (IsHead() && velocity <= 3.0_mph) { velocity = 0; } } if (PitchAndRollStart(HasFlag(VehicleFlags::CarIsInverted), tileElement) != pitchAndRollEnd) { return false; } // Update VehicleFlags::CarIsInverted flag ClearFlag(VehicleFlags::CarIsInverted); { int32_t rideType = ::GetRide(tileElement->AsTrack()->GetRideIndex())->type; if (GetRideTypeDescriptor(rideType).HasFlag(RIDE_TYPE_FLAG_HAS_ALTERNATIVE_TRACK_TYPE)) { if (tileElement->AsTrack()->IsInverted()) { SetFlag(VehicleFlags::CarIsInverted); } } } } TrackLocation = location; // TODO check if getting the vehicle entry again is necessary const auto* carEntry = Entry(); if (carEntry == nullptr) { return false; } if ((carEntry->flags & CAR_ENTRY_FLAG_GO_KART) && TrackSubposition < VehicleTrackSubposition::GoKartsMovingToRightLane) { trackType = tileElement->AsTrack()->GetTrackType(); if (trackType == TrackElemType::Flat || ((curRide.lifecycle_flags & RIDE_LIFECYCLE_PASS_STATION_NO_STOPPING) && tileElement->AsTrack()->IsStation())) { UpdateGoKartAttemptSwitchLanes(); } } if (TrackSubposition >= VehicleTrackSubposition::ChairliftGoingOut && TrackSubposition <= VehicleTrackSubposition::ChairliftStartBullwheel) { TileCoordsXYZ curLocation{ TrackLocation }; if (curLocation == curRide.ChairliftBullwheelLocation[1]) { TrackSubposition = VehicleTrackSubposition::ChairliftEndBullwheel; } else if (curLocation == curRide.ChairliftBullwheelLocation[0]) { TrackSubposition = VehicleTrackSubposition::ChairliftStartBullwheel; } } // Loc6DB500 // Update VehicleFlags::OnLiftHill ClearFlag(VehicleFlags::OnLiftHill); if (tileElement->AsTrack()->HasChain()) { SetFlag(VehicleFlags::OnLiftHill); } trackType = tileElement->AsTrack()->GetTrackType(); if (trackType != TrackElemType::Brakes) { target_seat_rotation = tileElement->AsTrack()->GetSeatRotation(); } SetTrackDirection(location.direction); SetTrackType(trackType); PopulateBrakeSpeed(TrackLocation, *tileElement->AsTrack()); if (trackType == TrackElemType::OnRidePhoto) { trigger_on_ride_photo(TrackLocation, tileElement); } if (trackType == TrackElemType::RotationControlToggle) { Flags ^= VehicleFlags::SpinningIsLocked; } // Change from original: this used to check if the vehicle allowed doors. UpdateSceneryDoorBackwards(); UpdateLandscapeDoorBackwards(); return true; } /** * * rct2: 0x006DAEB9 */ bool Vehicle::UpdateTrackMotionForwards(const CarEntry* carEntry, const Ride& curRide, const RideObjectEntry& rideEntry) { EntityId otherVehicleIndex = EntityId::GetNull(); Loc6DAEB9: auto trackType = GetTrackType(); if (trackType == TrackElemType::HeartLineTransferUp || trackType == TrackElemType::HeartLineTransferDown) { if (track_progress == 80) { vehicle_type ^= 1; carEntry = Entry(); } if (_vehicleVelocityF64E08 >= 0x40000) { acceleration = -_vehicleVelocityF64E08 * 8; } else if (_vehicleVelocityF64E08 < 0x20000) { acceleration = 0x50000; } } else if (TrackTypeIsBrakes(trackType)) { bool hasBrakesFailure = curRide.lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN && curRide.breakdown_reason_pending == BREAKDOWN_BRAKES_FAILURE; if (!hasBrakesFailure || curRide.mechanic_status == RIDE_MECHANIC_STATUS_HAS_FIXED_STATION_BRAKES) { auto brakeSpeed = ChooseBrakeSpeed(); if ((brakeSpeed << 16) < _vehicleVelocityF64E08) { acceleration = -_vehicleVelocityF64E08 * 16; } else if (!(GetGameState().CurrentTicks & 0x0F)) { if (_vehicleF64E2C == 0) { _vehicleF64E2C++; OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::BrakeRelease, { x, y, z }); } } } } else if (TrackTypeIsBooster(trackType)) { auto boosterSpeed = GetBoosterSpeed(curRide.type, (brake_speed << 16)); if (boosterSpeed > _vehicleVelocityF64E08) { acceleration = GetRideTypeDescriptor(curRide.type).OperatingSettings.BoosterAcceleration << 16; //_vehicleVelocityF64E08 * 1.2; } } else if (rideEntry.flags & RIDE_ENTRY_FLAG_RIDER_CONTROLS_SPEED && num_peeps > 0) { acceleration += CalculateRiderBraking(); } if ((trackType == TrackElemType::Flat && curRide.type == RIDE_TYPE_REVERSE_FREEFALL_COASTER) || (trackType == TrackElemType::PoweredLift)) { acceleration = GetRideTypeDescriptor(curRide.type).OperatingSettings.PoweredLiftAcceleration << 16; } if (trackType == TrackElemType::BrakeForDrop) { if (IsHead()) { if (!HasFlag(VehicleFlags::StoppedOnHoldingBrake)) { if (track_progress >= 8) { acceleration = -_vehicleVelocityF64E08 * 16; if (track_progress >= 24) { SetFlag(VehicleFlags::StoppedOnHoldingBrake); vertical_drop_countdown = 90; } } } } } if (trackType == TrackElemType::LogFlumeReverser) { if (track_progress != 16 || velocity < 4.0_mph) { if (track_progress == 32) { vehicle_type = carEntry->ReversedCarIndex; carEntry = Entry(); } } else { track_progress += 17; } } uint16_t newTrackProgress = track_progress + 1; // Track Total Progress is in the two bytes before the move info list uint16_t trackTotalProgress = GetTrackProgress(); if (newTrackProgress >= trackTotalProgress) { UpdateCrossings(); if (!UpdateTrackMotionForwardsGetNewTrack(trackType, curRide, rideEntry)) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_5; _vehicleVelocityF64E0C -= remaining_distance + 1; remaining_distance = -1; return false; } newTrackProgress = 0; } track_progress = newTrackProgress; UpdateHandleWaterSplash(); // Loc6DB706 const auto moveInfo = GetMoveInfo(); trackType = GetTrackType(); uint8_t moveInfovehicleSpriteType; { auto nextVehiclePosition = TrackLocation + CoordsXYZ{ moveInfo->x, moveInfo->y, moveInfo->z + GetRideTypeDescriptor(curRide.type).Heights.VehicleZOffset }; uint8_t remainingDistanceFlags = 0; if (nextVehiclePosition.x != _vehicleCurPosition.x) { remainingDistanceFlags |= 1; } if (nextVehiclePosition.y != _vehicleCurPosition.y) { remainingDistanceFlags |= 2; } if (nextVehiclePosition.z != _vehicleCurPosition.z) { remainingDistanceFlags |= 4; } if (TrackSubposition == VehicleTrackSubposition::ReverserRCFrontBogie && (trackType == TrackElemType::LeftReverser || trackType == TrackElemType::RightReverser) && track_progress >= 30 && track_progress <= 66) { remainingDistanceFlags |= 8; } if (TrackSubposition == VehicleTrackSubposition::ReverserRCRearBogie && (trackType == TrackElemType::LeftReverser || trackType == TrackElemType::RightReverser) && track_progress == 96) { ReverseReverserCar(); const VehicleInfo* moveInfo2 = GetMoveInfo(); nextVehiclePosition.x = x + moveInfo2->x; nextVehiclePosition.y = y + moveInfo2->y; } // Loc6DB8A5 remaining_distance -= SubpositionTranslationDistances[remainingDistanceFlags]; _vehicleCurPosition = nextVehiclePosition; Orientation = moveInfo->direction; bank_rotation = moveInfo->bank_rotation; Pitch = moveInfo->Pitch; moveInfovehicleSpriteType = moveInfo->Pitch; if ((carEntry->flags & CAR_ENTRY_FLAG_WOODEN_WILD_MOUSE_SWING) && moveInfo->Pitch != 0) { SwingSprite = 0; SwingPosition = 0; SwingSpeed = 0; } // this == frontVehicle if (this == _vehicleFrontVehicle) { if (_vehicleVelocityF64E08 >= 0) { otherVehicleIndex = prev_vehicle_on_ride; if (UpdateMotionCollisionDetection(nextVehiclePosition, &otherVehicleIndex)) { _vehicleVelocityF64E0C -= remaining_distance + 1; remaining_distance = -1; // Might need to be bp rather than this, but hopefully not auto otherVeh = GetEntity(otherVehicleIndex); if (otherVeh == nullptr) { // This can never happen as prev_vehicle_on_ride will always be set to a vehicle LOG_ERROR("Failed to get next vehicle during update!"); return true; } auto head = otherVeh->TrainHead(); auto velocityDelta = abs(velocity - head->velocity); if (!(rideEntry.flags & RIDE_ENTRY_FLAG_DISABLE_COLLISION_CRASHES)) { if (velocityDelta > 14.0_mph) { if (!(carEntry->flags & CAR_ENTRY_FLAG_BOAT_HIRE_COLLISION_DETECTION)) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_COLLISION; } } } if (carEntry->flags & CAR_ENTRY_FLAG_GO_KART) { velocity -= velocity >> 2; } else { int32_t newHeadVelocity = velocity >> 1; velocity = head->velocity >> 1; head->velocity = newHeadVelocity; } _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_1; return false; } } } } // Loc6DB928 if (remaining_distance < 0x368A) { return true; } acceleration += AccelerationFromPitch[moveInfovehicleSpriteType]; _vehicleUnkF64E10++; goto Loc6DAEB9; } static PitchAndRoll PitchAndRollEnd(const Ride& curRide, bool useInvertedSprites, uint16_t trackType, TileElement* tileElement) { bool isInverted = useInvertedSprites ^ tileElement->AsTrack()->IsInverted(); const auto& ted = GetTrackElementDescriptor(trackType); return { ted.Definition.PitchEnd, TrackGetActualBank2(curRide.type, isInverted, ted.Definition.RollEnd) }; } /** * * rct2: 0x006DBAA6 */ bool Vehicle::UpdateTrackMotionBackwardsGetNewTrack(uint16_t trackType, const Ride& curRide, uint16_t* progress) { auto pitchAndRollStart = TrackPitchAndRollStart(trackType); TileElement* tileElement = MapGetTrackElementAtOfTypeSeq(TrackLocation, trackType, 0); if (tileElement == nullptr) return false; bool nextTileBackwards = true; int32_t direction = 0; // Loc6DBB08:; auto trackPos = CoordsXYZ{ TrackLocation.x, TrackLocation.y, 0 }; switch (TrackSubposition) { case VehicleTrackSubposition::ChairliftEndBullwheel: TrackSubposition = VehicleTrackSubposition::ChairliftGoingOut; break; case VehicleTrackSubposition::GoKartsMovingToRightLane: TrackSubposition = VehicleTrackSubposition::GoKartsLeftLane; break; case VehicleTrackSubposition::GoKartsMovingToLeftLane: TrackSubposition = VehicleTrackSubposition::GoKartsRightLane; break; case VehicleTrackSubposition::ChairliftGoingBack: case VehicleTrackSubposition::ChairliftStartBullwheel: TrackSubposition = VehicleTrackSubposition::ChairliftGoingBack; nextTileBackwards = false; break; default: break; } if (nextTileBackwards) { // Loc6DBB7E:; TrackBeginEnd trackBeginEnd; if (!TrackBlockGetPrevious({ trackPos, tileElement }, &trackBeginEnd)) { return false; } tileElement = trackBeginEnd.begin_element; trackType = tileElement->AsTrack()->GetTrackType(); if (trackType == TrackElemType::LeftReverser || trackType == TrackElemType::RightReverser) { return false; } if (PitchAndRollEnd(curRide, HasFlag(VehicleFlags::CarIsInverted), trackType, tileElement) != pitchAndRollStart) { return false; } // Update VehicleFlags::CarIsInverted ClearFlag(VehicleFlags::CarIsInverted); if (GetRideTypeDescriptor(curRide.type).HasFlag(RIDE_TYPE_FLAG_HAS_ALTERNATIVE_TRACK_TYPE)) { if (tileElement->AsTrack()->IsInverted()) { SetFlag(VehicleFlags::CarIsInverted); } } trackPos = { trackBeginEnd.begin_x, trackBeginEnd.begin_y, trackBeginEnd.begin_z }; direction = trackBeginEnd.begin_direction; } else { // Loc6DBB4F:; CoordsXYE input; CoordsXYE output; int32_t outputZ{}; input.x = trackPos.x; input.y = trackPos.y; input.element = tileElement; if (!TrackBlockGetNext(&input, &output, &outputZ, &direction)) { return false; } tileElement = output.element; trackPos = { output, outputZ }; } // Loc6DBC3B: TrackLocation = trackPos; if (TrackSubposition >= VehicleTrackSubposition::ChairliftGoingOut && TrackSubposition <= VehicleTrackSubposition::ChairliftStartBullwheel) { TileCoordsXYZ curLocation{ TrackLocation }; if (curLocation == curRide.ChairliftBullwheelLocation[1]) { TrackSubposition = VehicleTrackSubposition::ChairliftEndBullwheel; } else if (curLocation == curRide.ChairliftBullwheelLocation[0]) { TrackSubposition = VehicleTrackSubposition::ChairliftStartBullwheel; } } if (tileElement->AsTrack()->HasChain()) { if (_vehicleVelocityF64E08 < 0) { if (next_vehicle_on_train.IsNull()) { trackType = tileElement->AsTrack()->GetTrackType(); const auto& ted = GetTrackElementDescriptor(trackType); if (!(ted.Flags & TRACK_ELEM_FLAG_DOWN)) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_9; } } SetFlag(VehicleFlags::OnLiftHill); } } else { if (HasFlag(VehicleFlags::OnLiftHill)) { ClearFlag(VehicleFlags::OnLiftHill); if (next_vehicle_on_train.IsNull()) { if (_vehicleVelocityF64E08 < 0) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_8; } } } } trackType = tileElement->AsTrack()->GetTrackType(); if (trackType != TrackElemType::Brakes) { target_seat_rotation = tileElement->AsTrack()->GetSeatRotation(); } direction &= 3; SetTrackType(trackType); SetTrackDirection(direction); PopulateBrakeSpeed(TrackLocation, *tileElement->AsTrack()); // There are two bytes before the move info list uint16_t trackTotalProgress = GetTrackProgress(); *progress = trackTotalProgress - 1; return true; } /** * * rct2: 0x006DBA33 */ bool Vehicle::UpdateTrackMotionBackwards(const CarEntry* carEntry, const Ride& curRide, const RideObjectEntry& rideEntry) { EntityId otherVehicleIndex = EntityId::GetNull(); while (true) { auto trackType = GetTrackType(); if (trackType == TrackElemType::Flat && curRide.type == RIDE_TYPE_REVERSE_FREEFALL_COASTER) { int32_t unkVelocity = _vehicleVelocityF64E08; if (unkVelocity < -524288) { unkVelocity = abs(unkVelocity); acceleration = unkVelocity * 2; } } if (TrackTypeIsBrakes(trackType)) { auto brakeSpeed = ChooseBrakeSpeed(); if (-(brakeSpeed << 16) > _vehicleVelocityF64E08) { acceleration = _vehicleVelocityF64E08 * -16; } } if (trackType == TrackElemType::Booster) { auto boosterSpeed = GetBoosterSpeed(curRide.type, (brake_speed << 16)); if (boosterSpeed < _vehicleVelocityF64E08) { acceleration = GetRideTypeDescriptor(curRide.type).OperatingSettings.BoosterAcceleration << 16; } } uint16_t newTrackProgress = track_progress - 1; if (newTrackProgress == 0xFFFF) { UpdateCrossings(); if (!UpdateTrackMotionBackwardsGetNewTrack(trackType, curRide, &newTrackProgress)) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_5; _vehicleVelocityF64E0C -= remaining_distance - 0x368A; remaining_distance = 0x368A; return false; } } // Loc6DBD42 track_progress = newTrackProgress; uint8_t moveInfoVehicleSpriteType; { const VehicleInfo* moveInfo = GetMoveInfo(); auto nextVehiclePosition = TrackLocation + CoordsXYZ{ moveInfo->x, moveInfo->y, moveInfo->z + GetRideTypeDescriptor(curRide.type).Heights.VehicleZOffset }; uint8_t remainingDistanceFlags = 0; if (nextVehiclePosition.x != _vehicleCurPosition.x) { remainingDistanceFlags |= 1; } if (nextVehiclePosition.y != _vehicleCurPosition.y) { remainingDistanceFlags |= 2; } if (nextVehiclePosition.z != _vehicleCurPosition.z) { remainingDistanceFlags |= 4; } remaining_distance += SubpositionTranslationDistances[remainingDistanceFlags]; _vehicleCurPosition = nextVehiclePosition; Orientation = moveInfo->direction; bank_rotation = moveInfo->bank_rotation; Pitch = moveInfo->Pitch; moveInfoVehicleSpriteType = moveInfo->Pitch; if ((carEntry->flags & CAR_ENTRY_FLAG_WOODEN_WILD_MOUSE_SWING) && Pitch != 0) { SwingSprite = 0; SwingPosition = 0; SwingSpeed = 0; } if (this == _vehicleFrontVehicle) { if (_vehicleVelocityF64E08 < 0) { otherVehicleIndex = next_vehicle_on_ride; if (UpdateMotionCollisionDetection(nextVehiclePosition, &otherVehicleIndex)) { _vehicleVelocityF64E0C -= remaining_distance - 0x368A; remaining_distance = 0x368A; Vehicle* v3 = GetEntity(otherVehicleIndex); Vehicle* v4 = gCurrentVehicle; if (v3 == nullptr) { return false; } if (!(rideEntry.flags & RIDE_ENTRY_FLAG_DISABLE_COLLISION_CRASHES)) { if (abs(v4->velocity - v3->velocity) > 14.0_mph) { if (!(carEntry->flags & CAR_ENTRY_FLAG_BOAT_HIRE_COLLISION_DETECTION)) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_COLLISION; } } } if (carEntry->flags & CAR_ENTRY_FLAG_GO_KART) { velocity -= velocity >> 2; _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_2; } else { int32_t v3Velocity = v3->velocity; v3->velocity = v4->velocity >> 1; v4->velocity = v3Velocity >> 1; _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_2; } return false; } } } } // Loc6DBE3F if (remaining_distance >= 0) { return true; } acceleration += AccelerationFromPitch[moveInfoVehicleSpriteType]; _vehicleUnkF64E10++; } } /** * rct2: 0x006DC3A7 * * */ void Vehicle::UpdateTrackMotionMiniGolfVehicle(const Ride& curRide, const RideObjectEntry& rideEntry, const CarEntry* carEntry) { EntityId otherVehicleIndex = EntityId::GetNull(); TileElement* tileElement = nullptr; CoordsXYZ trackPos; int32_t direction{}; _vehicleUnkF64E10 = 1; acceleration = AccelerationFromPitch[Pitch]; if (!HasFlag(VehicleFlags::MoveSingleCar)) { remaining_distance = _vehicleVelocityF64E0C + remaining_distance; } if (remaining_distance >= 0 && remaining_distance < 0x368A) { Loc6DCE02(curRide); return; } sound2_flags &= ~VEHICLE_SOUND2_FLAGS_LIFT_HILL; _vehicleCurPosition.x = x; _vehicleCurPosition.y = y; _vehicleCurPosition.z = z; Invalidate(); if (remaining_distance < 0) goto Loc6DCA9A; Loc6DC462: if (var_D3 != 0) { var_D3--; remaining_distance -= 0x368A; if (remaining_distance < 0) { remaining_distance = 0; } if (remaining_distance < 0x368A) { Loc6DCDE4(curRide); return; } acceleration = AccelerationFromPitch[Pitch]; _vehicleUnkF64E10++; goto Loc6DC462; } if (mini_golf_flags & MiniGolfFlag::Flag2) { uint8_t nextFrame = animation_frame + 1; if (nextFrame < MiniGolfPeepAnimationLengths[EnumValue(mini_golf_current_animation)]) { animation_frame = nextFrame; remaining_distance -= 0x368A; if (remaining_distance < 0) { remaining_distance = 0; } if (remaining_distance < 0x368A) { Loc6DCDE4(curRide); return; } acceleration = AccelerationFromPitch[Pitch]; _vehicleUnkF64E10++; goto Loc6DC462; } mini_golf_flags &= ~MiniGolfFlag::Flag2; } if (mini_golf_flags & MiniGolfFlag::Flag0) { auto vehicleIdx = IsHead() ? next_vehicle_on_ride : prev_vehicle_on_ride; Vehicle* vEDI = GetEntity(vehicleIdx); if (vEDI == nullptr) { return; } if (!(vEDI->mini_golf_flags & MiniGolfFlag::Flag0) || (vEDI->mini_golf_flags & MiniGolfFlag::Flag2)) { remaining_distance -= 0x368A; if (remaining_distance < 0) { remaining_distance = 0; } if (remaining_distance < 0x368A) { Loc6DCDE4(curRide); return; } acceleration = AccelerationFromPitch[Pitch]; _vehicleUnkF64E10++; goto Loc6DC462; } if (vEDI->var_D3 != 0) { remaining_distance -= 0x368A; if (remaining_distance < 0) { remaining_distance = 0; } if (remaining_distance < 0x368A) { Loc6DCDE4(curRide); return; } acceleration = AccelerationFromPitch[Pitch]; _vehicleUnkF64E10++; goto Loc6DC462; } vEDI->mini_golf_flags &= ~MiniGolfFlag::Flag0; mini_golf_flags &= ~MiniGolfFlag::Flag0; } if (mini_golf_flags & MiniGolfFlag::Flag1) { auto vehicleIdx = IsHead() ? next_vehicle_on_ride : prev_vehicle_on_ride; Vehicle* vEDI = GetEntity(vehicleIdx); if (vEDI == nullptr) { return; } if (!(vEDI->mini_golf_flags & MiniGolfFlag::Flag1) || (vEDI->mini_golf_flags & MiniGolfFlag::Flag2)) { remaining_distance -= 0x368A; if (remaining_distance < 0) { remaining_distance = 0; } if (remaining_distance < 0x368A) { Loc6DCDE4(curRide); return; } acceleration = AccelerationFromPitch[Pitch]; _vehicleUnkF64E10++; goto Loc6DC462; } if (vEDI->var_D3 != 0) { remaining_distance -= 0x368A; if (remaining_distance < 0) { remaining_distance = 0; } if (remaining_distance < 0x368A) { Loc6DCDE4(curRide); return; } acceleration = AccelerationFromPitch[Pitch]; _vehicleUnkF64E10++; goto Loc6DC462; } vEDI->mini_golf_flags &= ~MiniGolfFlag::Flag1; mini_golf_flags &= ~MiniGolfFlag::Flag1; } if (mini_golf_flags & MiniGolfFlag::Flag3) { Vehicle* vEDI = this; for (;;) { vEDI = GetEntity(vEDI->prev_vehicle_on_ride); if (vEDI == this || vEDI == nullptr) { break; } if (vEDI->IsHead()) continue; if (!(vEDI->mini_golf_flags & MiniGolfFlag::Flag4)) continue; if (vEDI->TrackLocation != TrackLocation) continue; remaining_distance -= 0x368A; if (remaining_distance < 0) { remaining_distance = 0; } if (remaining_distance < 0x368A) { Loc6DCDE4(curRide); return; } acceleration = AccelerationFromPitch[Pitch]; _vehicleUnkF64E10++; goto Loc6DC462; } mini_golf_flags |= MiniGolfFlag::Flag4; mini_golf_flags &= ~MiniGolfFlag::Flag3; } { uint16_t trackTotalProgress = GetTrackProgress(); if (track_progress + 1 >= trackTotalProgress) { tileElement = MapGetTrackElementAtOfTypeSeq(TrackLocation, GetTrackType(), 0); { CoordsXYE output; int32_t outZ{}; int32_t outDirection{}; CoordsXYE input = { TrackLocation, tileElement }; if (!TrackBlockGetNext(&input, &output, &outZ, &outDirection)) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_5; _vehicleVelocityF64E0C -= remaining_distance + 1; remaining_distance = -1; if (remaining_distance >= 0) { Loc6DCDE4(curRide); } acceleration += AccelerationFromPitch[Pitch]; _vehicleUnkF64E10++; goto Loc6DCA9A; } tileElement = output.element; trackPos = { output.x, output.y, outZ }; direction = outDirection; } if (PitchAndRollStart(HasFlag(VehicleFlags::CarIsInverted), tileElement) != TrackPitchAndRollEnd(GetTrackType())) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_5; _vehicleVelocityF64E0C -= remaining_distance + 1; remaining_distance = -1; if (remaining_distance >= 0) { Loc6DCDE4(curRide); } acceleration += AccelerationFromPitch[Pitch]; _vehicleUnkF64E10++; goto Loc6DCA9A; } { int32_t rideType = ::GetRide(tileElement->AsTrack()->GetRideIndex())->type; ClearFlag(VehicleFlags::CarIsInverted); if (GetRideTypeDescriptor(rideType).HasFlag(RIDE_TYPE_FLAG_HAS_ALTERNATIVE_TRACK_TYPE)) { if (tileElement->AsTrack()->IsInverted()) { SetFlag(VehicleFlags::CarIsInverted); } } } TrackLocation = trackPos; if (!IsHead()) { Vehicle* prevVehicle = GetEntity(prev_vehicle_on_ride); if (prevVehicle != nullptr) { TrackSubposition = prevVehicle->TrackSubposition; } if (TrackSubposition != VehicleTrackSubposition::MiniGolfStart9) { TrackSubposition = VehicleTrackSubposition{ static_cast( static_cast(TrackSubposition) - 1u) }; } } ClearFlag(VehicleFlags::OnLiftHill); SetTrackType(tileElement->AsTrack()->GetTrackType()); SetTrackDirection(direction); brake_speed = tileElement->AsTrack()->GetBrakeBoosterSpeed(); track_progress = 0; } else { track_progress += 1; } } if (!IsHead()) { animation_frame++; if (animation_frame >= 6) { animation_frame = 0; } } const VehicleInfo* moveInfo; for (;;) { moveInfo = GetMoveInfo(); if (moveInfo->x != LOCATION_NULL) { break; } switch (MiniGolfState(moveInfo->y)) { case MiniGolfState::Unk0: // Loc6DC7B4 if (!IsHead()) { mini_golf_flags |= MiniGolfFlag::Flag3; } else { uint16_t rand16 = ScenarioRand() & 0xFFFF; VehicleTrackSubposition nextTrackSubposition = VehicleTrackSubposition::MiniGolfBallPathC14; if (rand16 <= 0xA000) { nextTrackSubposition = VehicleTrackSubposition::MiniGolfBallPathB12; if (rand16 <= 0x900) { nextTrackSubposition = VehicleTrackSubposition::MiniGolfBallPathA10; } } TrackSubposition = nextTrackSubposition; } track_progress++; break; case MiniGolfState::Unk1: // Loc6DC7ED LOG_ERROR("Unused move info..."); assert(false); var_D3 = static_cast(moveInfo->z); track_progress++; break; case MiniGolfState::Unk2: // Loc6DC800 mini_golf_flags |= MiniGolfFlag::Flag0; track_progress++; break; case MiniGolfState::Unk3: // Loc6DC810 mini_golf_flags |= MiniGolfFlag::Flag1; track_progress++; break; case MiniGolfState::Unk4: // Loc6DC820 { auto animation = MiniGolfAnimation(moveInfo->z); // When the ride is closed occasionally the peep is removed // but the vehicle is still on the track. This will prevent // it from crashing in that situation. auto* curPeep = TryGetEntity(peep[0]); if (curPeep != nullptr) { if (animation == MiniGolfAnimation::SwingLeft) { if (curPeep->PeepId & 7) { animation = MiniGolfAnimation::Swing; } } if (animation == MiniGolfAnimation::PuttLeft) { if (curPeep->PeepId & 7) { animation = MiniGolfAnimation::Putt; } } } mini_golf_current_animation = animation; animation_frame = 0; track_progress++; break; } case MiniGolfState::Unk5: // Loc6DC87A mini_golf_flags |= MiniGolfFlag::Flag2; track_progress++; break; case MiniGolfState::Unk6: // Loc6DC88A mini_golf_flags &= ~MiniGolfFlag::Flag4; mini_golf_flags |= MiniGolfFlag::Flag5; track_progress++; break; default: LOG_ERROR("Invalid move info..."); assert(false); break; } } // Loc6DC8A1 trackPos = { TrackLocation.x + moveInfo->x, TrackLocation.y + moveInfo->y, TrackLocation.z + moveInfo->z + GetRideTypeDescriptor(curRide.type).Heights.VehicleZOffset }; remaining_distance -= 0x368A; if (remaining_distance < 0) { remaining_distance = 0; } _vehicleCurPosition = trackPos; Orientation = moveInfo->direction; bank_rotation = moveInfo->bank_rotation; Pitch = moveInfo->Pitch; if (rideEntry.Cars[0].flags & CAR_ENTRY_FLAG_WOODEN_WILD_MOUSE_SWING) { if (Pitch != 0) { SwingSprite = 0; SwingPosition = 0; SwingSpeed = 0; } } if (this == _vehicleFrontVehicle) { if (_vehicleVelocityF64E08 >= 0) { otherVehicleIndex = prev_vehicle_on_ride; UpdateMotionCollisionDetection(trackPos, &otherVehicleIndex); } } if (remaining_distance < 0x368A) { Loc6DCDE4(curRide); return; } acceleration = AccelerationFromPitch[Pitch]; _vehicleUnkF64E10++; goto Loc6DC462; Loc6DCA9A: if (track_progress == 0) { tileElement = MapGetTrackElementAtOfTypeSeq(TrackLocation, GetTrackType(), 0); { TrackBeginEnd trackBeginEnd; if (!TrackBlockGetPrevious({ TrackLocation, tileElement }, &trackBeginEnd)) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_5; _vehicleVelocityF64E0C -= remaining_distance + 1; remaining_distance = -1; if (remaining_distance >= 0) { Loc6DCDE4(curRide); } acceleration += AccelerationFromPitch[Pitch]; _vehicleUnkF64E10++; goto Loc6DCA9A; } trackPos = { trackBeginEnd.begin_x, trackBeginEnd.begin_y, trackBeginEnd.begin_z }; direction = trackBeginEnd.begin_direction; tileElement = trackBeginEnd.begin_element; } if (PitchAndRollStart(HasFlag(VehicleFlags::CarIsInverted), tileElement) != TrackPitchAndRollEnd(GetTrackType())) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_5; _vehicleVelocityF64E0C -= remaining_distance - 0x368A; remaining_distance = 0x368A; if (remaining_distance < 0x368A) { Loc6DCDE4(curRide); return; } acceleration = AccelerationFromPitch[Pitch]; _vehicleUnkF64E10++; goto Loc6DC462; } { int32_t rideType = ::GetRide(tileElement->AsTrack()->GetRideIndex())->type; ClearFlag(VehicleFlags::CarIsInverted); if (GetRideTypeDescriptor(rideType).HasFlag(RIDE_TYPE_FLAG_HAS_ALTERNATIVE_TRACK_TYPE)) { if (tileElement->AsTrack()->IsInverted()) { SetFlag(VehicleFlags::CarIsInverted); } } } TrackLocation = trackPos; if (HasFlag(VehicleFlags::OnLiftHill)) { ClearFlag(VehicleFlags::OnLiftHill); if (next_vehicle_on_train.IsNull()) { if (_vehicleVelocityF64E08 < 0) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_8; } } } SetTrackType(tileElement->AsTrack()->GetTrackType()); SetTrackDirection(direction); brake_speed = tileElement->AsTrack()->GetBrakeBoosterSpeed(); // There are two bytes before the move info list track_progress = GetTrackProgress(); } else { track_progress -= 1; } moveInfo = GetMoveInfo(); trackPos = { TrackLocation.x + moveInfo->x, TrackLocation.y + moveInfo->y, TrackLocation.z + moveInfo->z + GetRideTypeDescriptor(curRide.type).Heights.VehicleZOffset }; remaining_distance -= 0x368A; if (remaining_distance < 0) { remaining_distance = 0; } _vehicleCurPosition = trackPos; Orientation = moveInfo->direction; bank_rotation = moveInfo->bank_rotation; Pitch = moveInfo->Pitch; if (rideEntry.Cars[0].flags & CAR_ENTRY_FLAG_WOODEN_WILD_MOUSE_SWING) { if (Pitch != 0) { SwingSprite = 0; SwingPosition = 0; SwingSpeed = 0; } } if (this == _vehicleFrontVehicle) { if (_vehicleVelocityF64E08 >= 0) { otherVehicleIndex = EntityId::FromUnderlying(var_44); // Possibly wrong?. if (UpdateMotionCollisionDetection(trackPos, &otherVehicleIndex)) { _vehicleVelocityF64E0C -= remaining_distance - 0x368A; remaining_distance = 0x368A; { Vehicle* vEBP = GetEntity(otherVehicleIndex); if (vEBP == nullptr) { return; } Vehicle* vEDI = gCurrentVehicle; if (abs(vEDI->velocity - vEBP->velocity) > 14.0_mph) { if (!(carEntry->flags & CAR_ENTRY_FLAG_BOAT_HIRE_COLLISION_DETECTION)) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_COLLISION; } } vEDI->velocity = vEBP->velocity >> 1; vEBP->velocity = vEDI->velocity >> 1; } _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_2; if (remaining_distance < 0x368A) { Loc6DCDE4(curRide); return; } acceleration = AccelerationFromPitch[Pitch]; _vehicleUnkF64E10++; goto Loc6DC462; } } } if (remaining_distance >= 0) { Loc6DCDE4(curRide); return; } acceleration += AccelerationFromPitch[Pitch]; _vehicleUnkF64E10++; goto Loc6DCA9A; } void Vehicle::Loc6DCDE4(const Ride& curRide) { MoveTo(_vehicleCurPosition); Loc6DCE02(curRide); } void Vehicle::Loc6DCE02(const Ride& curRide) { acceleration /= _vehicleUnkF64E10; if (TrackSubposition == VehicleTrackSubposition::ChairliftGoingBack) { return; } auto trackType = GetTrackType(); const auto& ted = GetTrackElementDescriptor(trackType); if (!(std::get<0>(ted.SequenceProperties) & TRACK_SEQUENCE_FLAG_ORIGIN)) { return; } _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_3; if (trackType != TrackElemType::EndStation) { return; } if (this != gCurrentVehicle) { return; } if (_vehicleVelocityF64E08 < 0) { if (track_progress > 11) { return; } } if (track_progress <= 8) { return; } _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_AT_STATION; for (const auto& station : curRide.GetStations()) { if (TrackLocation != station.Start) { continue; } if (TrackLocation.z != station.GetBaseZ()) { continue; } _vehicleStationIndex = curRide.GetStationIndex(&station); } } static constexpr int32_t GetAccelerationDecrease2(const int32_t velocity, const int32_t totalMass) { int32_t accelerationDecrease2 = velocity >> 8; accelerationDecrease2 *= accelerationDecrease2; if (velocity < 0) { accelerationDecrease2 = -accelerationDecrease2; } accelerationDecrease2 >>= 4; // OpenRCT2: vehicles from different track types can have 0 mass. if (totalMass != 0) { return accelerationDecrease2 / totalMass; } return accelerationDecrease2; } int32_t Vehicle::UpdateTrackMotionMiniGolfCalculateAcceleration(const CarEntry& carEntry) { int32_t sumAcceleration = 0; int32_t numVehicles = 0; uint16_t totalMass = 0; for (Vehicle* vehicle = this; vehicle != nullptr; vehicle = GetEntity(vehicle->next_vehicle_on_train)) { numVehicles++; totalMass += vehicle->mass; sumAcceleration += vehicle->acceleration; } int32_t newAcceleration = ((sumAcceleration / numVehicles) * 21) >> 9; newAcceleration -= velocity >> 12; newAcceleration -= GetAccelerationDecrease2(velocity, totalMass); if (!(carEntry.flags & CAR_ENTRY_FLAG_POWERED)) { return newAcceleration; } if (carEntry.flags & CAR_ENTRY_FLAG_POWERED_RIDE_UNRESTRICTED_GRAVITY) { if (speed * 0x4000 < velocity) { return newAcceleration; } } { int32_t poweredAcceleration = speed << 14; int32_t quarterForce = (speed * totalMass) >> 2; if (HasFlag(VehicleFlags::PoweredCarInReverse)) { poweredAcceleration = -poweredAcceleration; } poweredAcceleration -= velocity; poweredAcceleration *= powered_acceleration << 1; if (quarterForce != 0) poweredAcceleration /= quarterForce; if (carEntry.flags & CAR_ENTRY_FLAG_WATER_RIDE) { if (poweredAcceleration < 0) { poweredAcceleration >>= 4; } if (carEntry.flags & CAR_ENTRY_FLAG_SPINNING) { spin_speed = std::clamp(spin_speed, VEHICLE_MIN_SPIN_SPEED_WATER_RIDE, VEHICLE_MAX_SPIN_SPEED_WATER_RIDE); } if (Pitch != 0) { poweredAcceleration = std::max(0, poweredAcceleration); if (carEntry.flags & CAR_ENTRY_FLAG_SPINNING) { if (Pitch == 2) { spin_speed = 0; } } newAcceleration += poweredAcceleration; return newAcceleration; } } if (abs(velocity) > 1.0_mph) { newAcceleration = 0; } newAcceleration += poweredAcceleration; } return newAcceleration; } int32_t Vehicle::UpdateTrackMotionMiniGolf(int32_t* outStation) { auto curRide = GetRide(); if (curRide == nullptr) return 0; const auto* rideEntry = GetRideEntry(); if (rideEntry == nullptr) return 0; const auto* carEntry = Entry(); gCurrentVehicle = this; _vehicleMotionTrackFlags = 0; velocity += acceleration; _vehicleVelocityF64E08 = velocity; _vehicleVelocityF64E0C = (velocity >> 10) * 42; _vehicleFrontVehicle = _vehicleVelocityF64E08 < 0 ? TrainTail() : this; for (Vehicle* vehicle = _vehicleFrontVehicle; vehicle != nullptr;) { vehicle->UpdateTrackMotionMiniGolfVehicle(*curRide, *rideEntry, carEntry); if (vehicle->HasFlag(VehicleFlags::OnLiftHill)) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_ON_LIFT_HILL; } if (vehicle->HasFlag(VehicleFlags::MoveSingleCar)) { if (outStation != nullptr) *outStation = _vehicleStationIndex.ToUnderlying(); return _vehicleMotionTrackFlags; } if (_vehicleVelocityF64E08 >= 0) { vehicle = GetEntity(vehicle->next_vehicle_on_train); } else { if (vehicle == gCurrentVehicle) { break; } vehicle = GetEntity(vehicle->prev_vehicle_on_ride); } } acceleration = UpdateTrackMotionMiniGolfCalculateAcceleration(*carEntry); if (outStation != nullptr) *outStation = _vehicleStationIndex.ToUnderlying(); return _vehicleMotionTrackFlags; } /** * * rct2: 0x006DC1E4 */ static uint8_t modified_speed(uint16_t trackType, VehicleTrackSubposition trackSubposition, uint8_t speed) { enum { FULL_SPEED, THREE_QUARTER_SPEED, HALF_SPEED }; uint8_t speedModifier = FULL_SPEED; if (trackType == TrackElemType::LeftQuarterTurn1Tile) { speedModifier = (trackSubposition == VehicleTrackSubposition::GoKartsLeftLane) ? HALF_SPEED : THREE_QUARTER_SPEED; } else if (trackType == TrackElemType::RightQuarterTurn1Tile) { speedModifier = (trackSubposition == VehicleTrackSubposition::GoKartsRightLane) ? HALF_SPEED : THREE_QUARTER_SPEED; } switch (speedModifier) { case HALF_SPEED: return speed >> 1; case THREE_QUARTER_SPEED: return speed - (speed >> 2); } return speed; } int32_t Vehicle::UpdateTrackMotionPoweredRideAcceleration( const CarEntry* carEntry, uint32_t totalMass, const int32_t curAcceleration) { if (carEntry->flags & CAR_ENTRY_FLAG_POWERED_RIDE_UNRESTRICTED_GRAVITY) { if (velocity > (speed * 0x4000)) { // Same code as none powered rides if (curAcceleration <= 0 && curAcceleration >= -500 && velocity <= 0.5_mph) { return curAcceleration + 400; } return curAcceleration; } } uint8_t modifiedSpeed = modified_speed(GetTrackType(), TrackSubposition, speed); int32_t poweredAcceleration = modifiedSpeed << 14; int32_t quarterForce = (modifiedSpeed * totalMass) >> 2; if (HasFlag(VehicleFlags::PoweredCarInReverse)) { poweredAcceleration = -poweredAcceleration; } poweredAcceleration -= velocity; poweredAcceleration *= powered_acceleration << 1; if (quarterForce != 0) { poweredAcceleration /= quarterForce; } if (carEntry->flags & CAR_ENTRY_FLAG_LIFT) { poweredAcceleration *= 4; } if (carEntry->flags & CAR_ENTRY_FLAG_WATER_RIDE) { if (poweredAcceleration < 0) { poweredAcceleration >>= 4; } if (carEntry->flags & CAR_ENTRY_FLAG_SPINNING) { spin_speed = std::clamp(spin_speed, VEHICLE_MIN_SPIN_SPEED_WATER_RIDE, VEHICLE_MAX_SPIN_SPEED_WATER_RIDE); } if (Pitch != 0) { if (poweredAcceleration < 0) { poweredAcceleration = 0; } if (carEntry->flags & CAR_ENTRY_FLAG_SPINNING) { // If the vehicle is on the up slope kill the spin speedModifier if (Pitch == 2) { spin_speed = 0; } } return curAcceleration + poweredAcceleration; } } if (std::abs(velocity) <= 1.0_mph) { return poweredAcceleration; } return curAcceleration + poweredAcceleration; } /** * * rct2: 0x006DAB4C */ int32_t Vehicle::UpdateTrackMotion(int32_t* outStation) { auto curRide = GetRide(); if (curRide == nullptr) return 0; const auto* rideEntry = GetRideEntry(); const auto* carEntry = Entry(); if (carEntry == nullptr) { return 0; } if (carEntry->flags & CAR_ENTRY_FLAG_MINI_GOLF) { return UpdateTrackMotionMiniGolf(outStation); } _vehicleF64E2C = 0; gCurrentVehicle = this; _vehicleMotionTrackFlags = 0; _vehicleStationIndex = StationIndex::GetNull(); UpdateTrackMotionUpStopCheck(); CheckAndApplyBlockSectionStopSite(); UpdateVelocity(); Vehicle* vehicle = this; if (_vehicleVelocityF64E08 < 0 && !vehicle->HasFlag(VehicleFlags::MoveSingleCar)) { vehicle = vehicle->TrainTail(); } // This will be the front vehicle even when traveling // backwards. _vehicleFrontVehicle = vehicle; auto spriteId = vehicle->Id; while (!spriteId.IsNull()) { Vehicle* car = GetEntity(spriteId); if (car == nullptr) { break; } carEntry = car->Entry(); if (carEntry == nullptr) { goto Loc6DBF3E; } // Swinging cars if (carEntry->flags & CAR_ENTRY_FLAG_SWINGING) { car->UpdateSwingingCar(); } // Spinning cars if (carEntry->flags & CAR_ENTRY_FLAG_SPINNING) { car->UpdateSpinningCar(); } // Rider sprites?? animation?? if ((carEntry->flags & CAR_ENTRY_FLAG_VEHICLE_ANIMATION) || (carEntry->flags & CAR_ENTRY_FLAG_RIDER_ANIMATION)) { car->UpdateAdditionalAnimation(); } car->acceleration = AccelerationFromPitch[car->Pitch]; _vehicleUnkF64E10 = 1; if (!car->HasFlag(VehicleFlags::MoveSingleCar)) { car->remaining_distance += _vehicleVelocityF64E0C; } car->sound2_flags &= ~VEHICLE_SOUND2_FLAGS_LIFT_HILL; _vehicleCurPosition.x = car->x; _vehicleCurPosition.y = car->y; _vehicleCurPosition.z = car->z; car->Invalidate(); while (true) { if (car->remaining_distance < 0) { // Backward loop if (car->UpdateTrackMotionBackwards(carEntry, *curRide, *rideEntry)) { break; } if (car->remaining_distance < 0x368A) { break; } car->acceleration += AccelerationFromPitch[car->Pitch]; _vehicleUnkF64E10++; continue; } if (car->remaining_distance < 0x368A) { // Location found goto Loc6DBF3E; } if (car->UpdateTrackMotionForwards(carEntry, *curRide, *rideEntry)) { break; } if (car->remaining_distance >= 0) { break; } car->acceleration = AccelerationFromPitch[car->Pitch]; _vehicleUnkF64E10++; continue; } // Loc6DBF20 car->MoveTo(_vehicleCurPosition); Loc6DBF3E: car->Sub6DBF3E(); // Loc6DC0F7 if (car->HasFlag(VehicleFlags::OnLiftHill)) { _vehicleMotionTrackFlags |= VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_ON_LIFT_HILL; } if (car->HasFlag(VehicleFlags::MoveSingleCar)) { if (outStation != nullptr) *outStation = _vehicleStationIndex.ToUnderlying(); return _vehicleMotionTrackFlags; } if (_vehicleVelocityF64E08 >= 0) { spriteId = car->next_vehicle_on_train; } else { if (car == gCurrentVehicle) { break; } spriteId = car->prev_vehicle_on_ride; } } // Loc6DC144 vehicle = gCurrentVehicle; carEntry = vehicle->Entry(); // eax int32_t totalAcceleration = 0; // ebp int32_t totalMass = 0; // ebx int32_t numVehicles = 0; for (; vehicle != nullptr; vehicle = GetEntity(vehicle->next_vehicle_on_train)) { numVehicles++; totalMass += vehicle->mass; totalAcceleration += vehicle->acceleration; } vehicle = gCurrentVehicle; int32_t newAcceleration = (totalAcceleration / numVehicles) * 21; if (newAcceleration < 0) { newAcceleration += 511; } newAcceleration >>= 9; int32_t curAcceleration = newAcceleration; curAcceleration -= vehicle->velocity / 4096; curAcceleration -= GetAccelerationDecrease2(vehicle->velocity, totalMass); if (carEntry->flags & CAR_ENTRY_FLAG_POWERED) { curAcceleration = vehicle->UpdateTrackMotionPoweredRideAcceleration(carEntry, totalMass, curAcceleration); } else if (curAcceleration <= 0 && curAcceleration >= -500) { // Probably moving slowly on a flat track piece, low rolling resistance and drag. if (vehicle->velocity <= 0.5_mph && vehicle->velocity >= 0) { // Vehicle is creeping forwards very slowly (less than ~2km/h), boost speed a bit. curAcceleration += 400; } } if (vehicle->GetTrackType() == TrackElemType::Watersplash) { if (vehicle->track_progress >= 48 && vehicle->track_progress <= 128) { curAcceleration -= vehicle->velocity >> 6; } } if (rideEntry->flags & RIDE_ENTRY_FLAG_PLAY_SPLASH_SOUND_SLIDE) { if (vehicle->IsHead()) { if (TrackElementIsCovered(vehicle->GetTrackType())) { if (vehicle->velocity > 2.0_mph) { curAcceleration -= vehicle->velocity >> 6; } } } } vehicle->acceleration = curAcceleration; // hook_setreturnregisters(®s); if (outStation != nullptr) *outStation = _vehicleStationIndex.ToUnderlying(); return _vehicleMotionTrackFlags; } const RideObjectEntry* Vehicle::GetRideEntry() const { return GetRideEntryByIndex(ride_subtype); } const CarEntry* Vehicle::Entry() const { const auto* rideEntry = GetRideEntry(); if (rideEntry == nullptr) { return nullptr; } return &rideEntry->Cars[vehicle_type]; } Ride* Vehicle::GetRide() const { return ::GetRide(ride); } int32_t Vehicle::NumPeepsUntilTrainTail() const { int32_t numPeeps = 0; for (const Vehicle* vehicle = GetEntity(Id); vehicle != nullptr; vehicle = GetEntity(vehicle->next_vehicle_on_train)) { numPeeps += vehicle->num_peeps; } return numPeeps; } /** * * rct2: 0x006DA1EC */ void Vehicle::InvalidateWindow() { auto intent = Intent(INTENT_ACTION_INVALIDATE_VEHICLE_WINDOW); intent.PutExtra(INTENT_EXTRA_VEHICLE, this); ContextBroadcastIntent(&intent); } void Vehicle::UpdateCrossings() const { auto curRide = GetRide(); if (curRide == nullptr) { return; } // Parks may have rides hacked into the path. // Limit path blocking to rides actually supporting level crossings to prevent peeps getting stuck everywhere. if (!GetRideTypeDescriptor(curRide->type).HasFlag(RIDE_TYPE_FLAG_SUPPORTS_LEVEL_CROSSINGS)) { return; } // In shuttle mode, only the train head is considered to be travelling backwards // To prevent path getting blocked incorrectly, only update crossings when this is the train head if (curRide->mode == RideMode::Shuttle && TrainHead() != this) { return; } const Vehicle* frontVehicle{}; const Vehicle* backVehicle{}; bool travellingForwards = !HasFlag(VehicleFlags::PoweredCarInReverse); if (travellingForwards) { frontVehicle = this; backVehicle = TrainTail(); } else { frontVehicle = TrainTail(); backVehicle = this; } TrackBeginEnd output{}; int32_t direction{}; CoordsXYE xyElement = { frontVehicle->TrackLocation, MapGetTrackElementAtOfTypeSeq(frontVehicle->TrackLocation, frontVehicle->GetTrackType(), 0) }; int32_t curZ = frontVehicle->TrackLocation.z; if (xyElement.element != nullptr && status != Vehicle::Status::Arriving) { int16_t autoReserveAhead = 4 + abs(velocity) / 150000; int16_t crossingBonus = 0; bool playedClaxon = false; // vehicle positions mean we have to take larger // margins for travelling backwards if (!travellingForwards) { autoReserveAhead += 1; } while (true) { auto* pathElement = MapGetPathElementAt(TileCoordsXYZ(CoordsXYZ{ xyElement, xyElement.element->GetBaseZ() })); if (pathElement != nullptr) { if (!playedClaxon && !pathElement->IsBlockedByVehicle()) { Claxon(); } crossingBonus = 4; pathElement->SetIsBlockedByVehicle(true); } else { crossingBonus = 0; } if (--autoReserveAhead + crossingBonus <= 0) { break; } curZ = xyElement.element->BaseHeight; if (travellingForwards) { if (!TrackBlockGetNext(&xyElement, &xyElement, &curZ, &direction)) { break; } } else { if (!TrackBlockGetPrevious(xyElement, &output)) { break; } xyElement.x = output.begin_x; xyElement.y = output.begin_y; xyElement.element = output.begin_element; } // Ensure trains near a station don't block possible crossings after the stop, // except when they are departing if (xyElement.element->AsTrack()->IsStation() && status != Vehicle::Status::Departing) { break; } } } xyElement = { backVehicle->TrackLocation, MapGetTrackElementAtOfTypeSeq(backVehicle->TrackLocation, backVehicle->GetTrackType(), 0) }; if (xyElement.element == nullptr) { return; } // Ensure departing trains don't clear blocked crossings behind them that might already be blocked by another incoming train uint8_t freeCount = travellingForwards && status != Vehicle::Status::Departing ? 3 : 1; while (freeCount-- > 0) { if (travellingForwards) { if (TrackBlockGetPrevious(xyElement, &output)) { xyElement.x = output.begin_x; xyElement.y = output.begin_y; xyElement.element = output.begin_element; } } auto* pathElement = MapGetPathElementAt(TileCoordsXYZ(CoordsXYZ{ xyElement, xyElement.element->GetBaseZ() })); if (pathElement != nullptr) { pathElement->SetIsBlockedByVehicle(false); } } } void Vehicle::Claxon() const { const auto* rideEntry = GetRideEntry(); switch (rideEntry->Cars[vehicle_type].sound_range) { case SOUND_RANGE_WHISTLE: OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::TrainWhistle, { x, y, z }); break; case SOUND_RANGE_BELL: OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::Tram, { x, y, z }); break; } } Vehicle* Vehicle::GetHead() { auto v = this; while (v != nullptr && !v->IsHead()) { v = GetEntity(v->prev_vehicle_on_ride); } return v; } const Vehicle* Vehicle::GetHead() const { return (const_cast(this)->GetHead()); } Vehicle* Vehicle::GetCar(size_t carIndex) const { auto car = const_cast(this); for (; carIndex != 0; carIndex--) { car = GetEntity(car->next_vehicle_on_train); if (car == nullptr) { LOG_ERROR("Tried to get non-existent car from index!"); return nullptr; } } return car; } void Vehicle::SetState(Vehicle::Status vehicleStatus, uint8_t subState) { status = vehicleStatus; sub_state = subState; InvalidateWindow(); } bool Vehicle::IsGhost() const { auto r = GetRide(); return r != nullptr && r->status == RideStatus::Simulating; } void Vehicle::EnableCollisionsForTrain() { assert(this->IsHead()); for (auto vehicle = this; vehicle != nullptr; vehicle = GetEntity(vehicle->next_vehicle_on_train)) { vehicle->ClearFlag(VehicleFlags::CollisionDisabled); } } void Vehicle::Serialise(DataSerialiser& stream) { EntityBase::Serialise(stream); stream << SubType; stream << Pitch; stream << bank_rotation; stream << remaining_distance; stream << velocity; stream << acceleration; stream << ride; stream << vehicle_type; stream << colours; stream << track_progress; stream << TrackTypeAndDirection; stream << TrackLocation; stream << next_vehicle_on_train; stream << prev_vehicle_on_ride; stream << next_vehicle_on_ride; stream << var_44; stream << mass; stream << Flags; stream << SwingSprite; stream << current_station; stream << SwingPosition; stream << SwingSpeed; stream << status; stream << sub_state; stream << peep; stream << peep_tshirt_colours; stream << num_seats; stream << num_peeps; stream << next_free_seat; stream << restraints_position; stream << spin_speed; stream << sound2_flags; stream << spin_sprite; stream << sound1_id; stream << sound1_volume; stream << sound2_id; stream << sound2_volume; stream << sound_vector_factor; stream << var_C0; stream << speed; stream << powered_acceleration; stream << DodgemsCollisionDirection; stream << animation_frame; stream << animationState; stream << scream_sound_id; stream << TrackSubposition; stream << NumLaps; stream << brake_speed; stream << lost_time_out; stream << vertical_drop_countdown; stream << var_D3; stream << mini_golf_current_animation; stream << mini_golf_flags; stream << ride_subtype; stream << seat_rotation; stream << target_seat_rotation; stream << BoatLocation; stream << BlockBrakeSpeed; }