diff --git a/distribution/changelog.txt b/distribution/changelog.txt index eb891f9458..502deeb5fb 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -6,6 +6,7 @@ - Feature: [#22206] Add option to randomise train or vehicle colours. - Feature: [#22392] [Plugin] Expose ride vehicle’s spin to the plugin API. - Feature: [#22414] Finance graphs can be resized. +- Feature: [#22569] Footpath placement now respects the construction modifier keys (ctrl/shift). - Change: [#21659] Increase the Hybrid Roller Coaster’s maximum lift speed to 17 km/h (11 mph). - Change: [#22466] The Clear Scenery tool now uses a bulldozer cursor instead of a generic crosshair. - Change: [#22490] The tool to change land and construction rights has been moved out of the Map window. diff --git a/src/openrct2-ui/windows/Footpath.cpp b/src/openrct2-ui/windows/Footpath.cpp index 5ac22ba7f4..8d38a84a98 100644 --- a/src/openrct2-ui/windows/Footpath.cpp +++ b/src/openrct2-ui/windows/Footpath.cpp @@ -7,11 +7,13 @@ * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ -#include "../interface/ViewportQuery.h" - +#include +#include #include #include #include +#include +#include #include #include #include @@ -162,6 +164,15 @@ static constexpr uint8_t ConstructionPreviewImages[][4] = { uint8_t _lastUpdatedCameraRotation = UINT8_MAX; bool _footpathErrorOccured = false; + bool _footpathPlaceCtrlState; + int32_t _footpathPlaceCtrlZ; + + bool _footpathPlaceShiftState; + ScreenCoordsXY _footpathPlaceShiftStart; + int32_t _footpathPlaceShiftZ; + + int32_t _footpathPlaceZ; + public: #pragma region Window Override Events @@ -180,6 +191,9 @@ static constexpr uint8_t ConstructionPreviewImages[][4] = { _footpathErrorOccured = false; WindowFootpathSetEnabledAndPressedWidgets(); + _footpathPlaceCtrlState = false; + _footpathPlaceShiftState = false; + hold_down_widgets = (1u << WIDX_CONSTRUCT) | (1u << WIDX_REMOVE); } @@ -756,6 +770,195 @@ static constexpr uint8_t ConstructionPreviewImages[][4] = { WindowFootpathSetEnabledAndPressedWidgets(); } + std::optional FootpathGetPlacePositionFromScreenPosition(ScreenCoordsXY screenCoords) + { + CoordsXY mapCoords; + auto& im = GetInputManager(); + + if (!_footpathPlaceCtrlState) + { + if (im.IsModifierKeyPressed(ModifierKey::ctrl)) + { + constexpr auto interactionFlags = EnumsToFlags( + ViewportInteractionItem::Terrain, ViewportInteractionItem::Ride, ViewportInteractionItem::Scenery, + ViewportInteractionItem::Footpath, ViewportInteractionItem::Wall, + ViewportInteractionItem::LargeScenery); + + auto info = GetMapCoordinatesFromPos(screenCoords, interactionFlags); + if (info.SpriteType != ViewportInteractionItem::None) + { + const bool allowInvalidHeights = GetGameState().Cheats.AllowTrackPlaceInvalidHeights; + const auto heightStep = kCoordsZStep * (!allowInvalidHeights ? 2 : 1); + + _footpathPlaceCtrlZ = Floor2(info.Element->GetBaseZ(), heightStep); + _footpathPlaceCtrlState = true; + } + } + } + else if (!im.IsModifierKeyPressed(ModifierKey::ctrl)) + { + _footpathPlaceCtrlState = false; + _footpathPlaceCtrlZ = 0; + } + + if (!_footpathPlaceShiftState && im.IsModifierKeyPressed(ModifierKey::shift)) + { + _footpathPlaceShiftState = true; + _footpathPlaceShiftStart = screenCoords; + _footpathPlaceShiftZ = 0; + } + else if (im.IsModifierKeyPressed(ModifierKey::shift)) + { + uint16_t maxHeight = ZoomLevel::max().ApplyTo( + std::numeric_limits::max() - 32); + + _footpathPlaceShiftZ = _footpathPlaceShiftStart.y - screenCoords.y + 4; + // Scale delta by zoom to match mouse position. + auto* mainWnd = WindowGetMain(); + if (mainWnd != nullptr && mainWnd->viewport != nullptr) + { + _footpathPlaceShiftZ = mainWnd->viewport->zoom.ApplyTo(_footpathPlaceShiftZ); + } + + const bool allowInvalidHeights = GetGameState().Cheats.AllowTrackPlaceInvalidHeights; + const auto heightStep = kCoordsZStep * (!allowInvalidHeights ? 2 : 1); + _footpathPlaceShiftZ = Floor2(_footpathPlaceShiftZ, heightStep); + + // Clamp to maximum possible value of BaseHeight can offer. + _footpathPlaceShiftZ = std::min(_footpathPlaceShiftZ, maxHeight); + + screenCoords = _footpathPlaceShiftStart; + } + else if (_footpathPlaceShiftState) + { + _footpathPlaceShiftState = false; + _footpathPlaceShiftZ = 0; + } + + if (!_footpathPlaceCtrlState) + { + auto info = GetMapCoordinatesFromPos( + screenCoords, EnumsToFlags(ViewportInteractionItem::Terrain, ViewportInteractionItem::Footpath)); + + if (info.SpriteType == ViewportInteractionItem::None) + return std::nullopt; + + mapCoords = info.Loc; + _footpathPlaceZ = 0; + + if (_footpathPlaceShiftState) + { + auto surfaceElement = MapGetSurfaceElementAt(mapCoords); + if (surfaceElement == nullptr) + return std::nullopt; + + auto mapZ = Floor2(surfaceElement->GetBaseZ(), 16); + mapZ += _footpathPlaceShiftZ; + mapZ = std::max(mapZ, 16); + _footpathPlaceZ = mapZ; + } + } + else + { + auto mapZ = _footpathPlaceCtrlZ; + auto mapXYCoords = ScreenGetMapXYWithZ(screenCoords, mapZ); + if (mapXYCoords.has_value()) + { + mapCoords = mapXYCoords.value(); + } + else + { + return std::nullopt; + } + + if (_footpathPlaceShiftState != 0) + { + mapZ += _footpathPlaceShiftZ; + } + _footpathPlaceZ = std::max(mapZ, 16); + } + + if (mapCoords.x == kLocationNull) + return std::nullopt; + + return mapCoords.ToTileStart(); + } + + int32_t FootpathGetSlopeFromInfo(const InteractionInfo& info) + { + if (info.SpriteType == ViewportInteractionItem::None || info.Element == nullptr) + { + gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE; + FootpathProvisionalUpdate(); + return kTileSlopeFlat; + } + + switch (info.SpriteType) + { + case ViewportInteractionItem::Terrain: + { + auto surfaceElement = info.Element->AsSurface(); + if (surfaceElement != nullptr) + { + return DefaultPathSlope[surfaceElement->GetSlope() & kTileSlopeRaisedCornersMask]; + } + break; + } + case ViewportInteractionItem::Footpath: + { + auto pathElement = info.Element->AsPath(); + if (pathElement != nullptr) + { + auto slope = pathElement->GetSlopeDirection(); + if (pathElement->IsSloped()) + { + slope |= FOOTPATH_PROPERTIES_FLAG_IS_SLOPED; + } + return slope; + } + break; + } + default: + break; + } + + return kTileSlopeFlat; + } + + int32_t FootpathGetBaseZFromInfo(const InteractionInfo& info) + { + if (info.SpriteType == ViewportInteractionItem::None || info.Element == nullptr) + { + return 0; + } + + switch (info.SpriteType) + { + case ViewportInteractionItem::Terrain: + { + auto surfaceElement = info.Element->AsSurface(); + if (surfaceElement != nullptr) + { + return surfaceElement->GetBaseZ(); + } + break; + } + case ViewportInteractionItem::Footpath: + { + auto pathElement = info.Element->AsPath(); + if (pathElement != nullptr) + { + return pathElement->GetBaseZ(); + } + break; + } + default: + break; + } + + return 0; + } + /** * * rct2: 0x006A81FB @@ -765,73 +968,56 @@ static constexpr uint8_t ConstructionPreviewImages[][4] = { MapInvalidateSelectionRect(); gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE_ARROW; - auto info = GetMapCoordinatesFromPos( - screenCoords, EnumsToFlags(ViewportInteractionItem::Terrain, ViewportInteractionItem::Footpath)); + // Get current map pos and handle key modifier state + auto mapPos = FootpathGetPlacePositionFromScreenPosition(screenCoords); + if (!mapPos) + return; - if (info.SpriteType == ViewportInteractionItem::None || info.Element == nullptr) + // Check for change + auto provisionalPos = CoordsXYZ(*mapPos, _footpathPlaceZ); + if ((gProvisionalFootpath.Flags & PROVISIONAL_PATH_FLAG_1) && gProvisionalFootpath.Position == provisionalPos) { - gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE; - FootpathProvisionalUpdate(); + return; } - else + + // Set map selection + gMapSelectFlags |= MAP_SELECT_FLAG_ENABLE; + gMapSelectType = MAP_SELECT_TYPE_FULL; + gMapSelectPositionA = *mapPos; + gMapSelectPositionB = *mapPos; + + FootpathProvisionalUpdate(); + + // Figure out what slope and height to use + int32_t slope = kTileSlopeFlat; + auto baseZ = _footpathPlaceZ; + if (baseZ == 0) { - // Check for change - if ((gProvisionalFootpath.Flags & PROVISIONAL_PATH_FLAG_1) - && gProvisionalFootpath.Position == CoordsXYZ{ info.Loc, info.Element->GetBaseZ() }) - { - return; - } + auto info = GetMapCoordinatesFromPos( + screenCoords, EnumsToFlags(ViewportInteractionItem::Terrain, ViewportInteractionItem::Footpath)); - // Set map selection - gMapSelectFlags |= MAP_SELECT_FLAG_ENABLE; - gMapSelectType = MAP_SELECT_TYPE_FULL; - gMapSelectPositionA = info.Loc; - gMapSelectPositionB = info.Loc; - - FootpathProvisionalUpdate(); - - // Set provisional path - int32_t slope = 0; - switch (info.SpriteType) - { - case ViewportInteractionItem::Terrain: - { - auto surfaceElement = info.Element->AsSurface(); - if (surfaceElement != nullptr) - { - slope = DefaultPathSlope[surfaceElement->GetSlope() & kTileSlopeRaisedCornersMask]; - } - break; - } - case ViewportInteractionItem::Footpath: - { - auto pathElement = info.Element->AsPath(); - if (pathElement != nullptr) - { - slope = pathElement->GetSlopeDirection(); - if (pathElement->IsSloped()) - { - slope |= FOOTPATH_PROPERTIES_FLAG_IS_SLOPED; - } - } - break; - } - default: - break; - } - auto z = info.Element->GetBaseZ(); + baseZ = FootpathGetBaseZFromInfo(info); + slope = FootpathGetSlopeFromInfo(info); if (slope & RAISE_FOOTPATH_FLAG) { slope &= ~RAISE_FOOTPATH_FLAG; - z += kPathHeightStep; + baseZ += kPathHeightStep; } - auto pathType = gFootpathSelection.GetSelectedSurface(); - auto constructFlags = FootpathCreateConstructFlags(pathType); - _windowFootpathCost = FootpathProvisionalSet( - pathType, gFootpathSelection.Railings, { info.Loc, z }, slope, constructFlags); - WindowInvalidateByClass(WindowClass::Footpath); + if (baseZ == 0) + { + gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE; + FootpathProvisionalUpdate(); + return; + } } + + // Set provisional path + auto pathType = gFootpathSelection.GetSelectedSurface(); + auto constructFlags = FootpathCreateConstructFlags(pathType); + _windowFootpathCost = FootpathProvisionalSet( + pathType, gFootpathSelection.Railings, { *mapPos, baseZ }, slope, constructFlags); + WindowInvalidateByClass(WindowClass::Footpath); } /** @@ -891,36 +1077,24 @@ static constexpr uint8_t ConstructionPreviewImages[][4] = { FootpathProvisionalUpdate(); - const auto info = GetMapCoordinatesFromPos( - screenCoords, EnumsToFlags(ViewportInteractionItem::Terrain, ViewportInteractionItem::Footpath)); - - if (info.SpriteType == ViewportInteractionItem::None) - { + auto mapPos = FootpathGetPlacePositionFromScreenPosition(screenCoords); + if (!mapPos) return; - } - // Set path - auto slope = 0; - switch (info.SpriteType) + auto slope = kTileSlopeFlat; + auto baseZ = _footpathPlaceZ; + if (baseZ == 0) { - case ViewportInteractionItem::Terrain: - slope = DefaultPathSlope[info.Element->AsSurface()->GetSlope() & kTileSlopeRaisedCornersMask]; - break; - case ViewportInteractionItem::Footpath: - slope = info.Element->AsPath()->GetSlopeDirection(); - if (info.Element->AsPath()->IsSloped()) - { - slope |= FOOTPATH_PROPERTIES_FLAG_IS_SLOPED; - } - break; - default: - break; - } - auto z = info.Element->GetBaseZ(); - if (slope & RAISE_FOOTPATH_FLAG) - { - slope &= ~RAISE_FOOTPATH_FLAG; - z += kPathHeightStep; + const auto info = GetMapCoordinatesFromPos( + screenCoords, EnumsToFlags(ViewportInteractionItem::Terrain, ViewportInteractionItem::Footpath)); + + slope = FootpathGetSlopeFromInfo(info); + baseZ = FootpathGetBaseZFromInfo(info); + if (slope & RAISE_FOOTPATH_FLAG) + { + slope &= ~RAISE_FOOTPATH_FLAG; + baseZ += kPathHeightStep; + } } // Try and place path @@ -928,7 +1102,7 @@ static constexpr uint8_t ConstructionPreviewImages[][4] = { PathConstructFlags constructFlags = FootpathCreateConstructFlags(selectedType); auto footpathPlaceAction = FootpathPlaceAction( - { info.Loc, z }, slope, selectedType, gFootpathSelection.Railings, INVALID_DIRECTION, constructFlags); + { *mapPos, baseZ }, slope, selectedType, gFootpathSelection.Railings, INVALID_DIRECTION, constructFlags); footpathPlaceAction.SetCallback([this](const GameAction* ga, const GameActions::Result* result) { if (result->Error == GameActions::Status::Ok) {