From 8f2b1a772c1848bc303ebc3ed1cedb67c03e2458 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Wed, 4 Sep 2024 22:26:43 +0200 Subject: [PATCH] Allow using construction modifier keys for track design placement (#22669) * Allow using construction modifier keys for track design placement * Reduce nesting in OnToolDown * Allow ctrl-matching against more interaction types * Amend changelog --- distribution/changelog.txt | 1 + src/openrct2-ui/windows/TrackDesignPlace.cpp | 272 +++++++++++++------ 2 files changed, 190 insertions(+), 83 deletions(-) diff --git a/distribution/changelog.txt b/distribution/changelog.txt index d41db57f70..f90df107a1 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -1,5 +1,6 @@ 0.4.15 (in development) ------------------------------------------------------------------------ +- Feature: [#15642] Track design placement can now use contruction modifier keys (ctrl/shift). - Fix: [#22231] Invalid object version can cause a crash. - Fix: [#22653] Add several .parkpatch files for missing water tiles in RCT1 and RCT2 scenarios. diff --git a/src/openrct2-ui/windows/TrackDesignPlace.cpp b/src/openrct2-ui/windows/TrackDesignPlace.cpp index 289ac5bd11..71d1986eee 100644 --- a/src/openrct2-ui/windows/TrackDesignPlace.cpp +++ b/src/openrct2-ui/windows/TrackDesignPlace.cpp @@ -7,9 +7,10 @@ * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ -#include "../interface/ViewportInteraction.h" - +#include +#include #include +#include #include #include #include @@ -77,6 +78,26 @@ namespace OpenRCT2::Ui::Windows class TrackDesignPlaceWindow final : public Window { + private: + std::unique_ptr _trackDesign; + + CoordsXYZD _placementLoc; + RideId _placementGhostRideId; + bool _hasPlacementGhost; + money64 _placementCost; + CoordsXYZD _placementGhostLoc; + + std::vector _miniPreview; + + bool _trackPlaceCtrlState = false; + int32_t _trackPlaceCtrlZ; + + bool _trackPlaceShiftState = false; + ScreenCoordsXY _trackPlaceShiftStart; + int32_t _trackPlaceShiftZ; + + int32_t _trackPlaceZ; + public: void OnOpen() override { @@ -146,23 +167,38 @@ namespace OpenRCT2::Ui::Windows void OnToolUpdate(WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords) override { TrackDesignState tds{}; - int16_t mapZ; MapInvalidateMapSelectionTiles(); gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE; gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE_CONSTRUCT; gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE_ARROW; + // Take shift modifier into account + ScreenCoordsXY targetScreenCoords = screenCoords; + if (_trackPlaceShiftState) + targetScreenCoords = _trackPlaceShiftStart; + // Get the tool map position - CoordsXY mapCoords = ViewportInteractionGetTileStartAtCursor(screenCoords); + CoordsXY mapCoords = ViewportInteractionGetTileStartAtCursor(targetScreenCoords); if (mapCoords.IsNull()) { ClearProvisional(); return; } + // Get base Z position + // NB: always use the actual screenCoords here, not the shifted ones + auto maybeMapZ = GetBaseZ(mapCoords, screenCoords); + if (!maybeMapZ.has_value()) + { + ClearProvisional(); + return; + } + + CoordsXYZD trackLoc = { mapCoords, *maybeMapZ, _currentTrackPieceDirection }; + // Check if tool map position has changed since last update - if (mapCoords == _placementLoc) + if (trackLoc == _placementLoc) { TrackDesignPreviewDrawOutlines( tds, *_trackDesign, RideGetTemporaryForPreview(), { mapCoords, 0, _currentTrackPieceDirection }); @@ -170,11 +206,6 @@ namespace OpenRCT2::Ui::Windows } money64 cost = kMoney64Undefined; - - // Get base Z position - mapZ = GetBaseZ(mapCoords); - CoordsXYZD trackLoc = { mapCoords, mapZ, _currentTrackPieceDirection }; - if (GameIsNotPaused() || GetGameState().Cheats.BuildInPauseMode) { ClearProvisional(); @@ -216,59 +247,73 @@ namespace OpenRCT2::Ui::Windows gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE_CONSTRUCT; gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE_ARROW; - const CoordsXY mapCoords = ViewportInteractionGetTileStartAtCursor(screenCoords); + // Take shift modifier into account + ScreenCoordsXY targetScreenCoords = screenCoords; + if (_trackPlaceShiftState) + targetScreenCoords = _trackPlaceShiftStart; + + // Get the tool map position + CoordsXY mapCoords = ViewportInteractionGetTileStartAtCursor(targetScreenCoords); if (mapCoords.IsNull()) - return; - - // Try increasing Z until a feasible placement is found - int16_t mapZ = GetBaseZ(mapCoords); - CoordsXYZ trackLoc = { mapCoords, mapZ }; - - auto res = FindValidTrackDesignPlaceHeight(trackLoc, 0); - if (res.Error == GameActions::Status::Ok) { - auto tdAction = TrackDesignAction({ trackLoc, _currentTrackPieceDirection }, *_trackDesign); - tdAction.SetCallback([&](const GameAction*, const GameActions::Result* result) { - if (result->Error == GameActions::Status::Ok) - { - rideId = result->GetData(); - auto getRide = GetRide(rideId); - if (getRide != nullptr) - { - WindowCloseByClass(WindowClass::Error); - OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::PlaceItem, trackLoc); - - _currentRideIndex = rideId; - if (TrackDesignAreEntranceAndExitPlaced()) - { - auto intent = Intent(WindowClass::Ride); - intent.PutExtra(INTENT_EXTRA_RIDE_ID, rideId.ToUnderlying()); - ContextOpenIntent(&intent); - auto wnd = WindowFindByClass(WindowClass::TrackDesignPlace); - WindowClose(*wnd); - } - else - { - RideInitialiseConstructionWindow(*getRide); - auto wnd = WindowFindByClass(WindowClass::RideConstruction); - wnd->OnMouseUp(WC_RIDE_CONSTRUCTION__WIDX_ENTRANCE); - } - } - } - else - { - OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::Error, result->Position); - } - }); - GameActions::Execute(&tdAction); + ClearProvisional(); return; } - // Unable to build track - OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::Error, trackLoc); + // NB: always use the actual screenCoords here, not the shifted ones + auto maybeMapZ = GetBaseZ(mapCoords, screenCoords); + if (!maybeMapZ.has_value()) + { + ClearProvisional(); + return; + } - auto windowManager = GetContext()->GetUiContext()->GetWindowManager(); - windowManager->ShowError(res.GetErrorTitle(), res.GetErrorMessage()); + // Try increasing Z until a feasible placement is found + CoordsXYZ trackLoc = { mapCoords, maybeMapZ.value() }; + auto res = FindValidTrackDesignPlaceHeight(trackLoc, 0); + if (res.Error != GameActions::Status::Ok) + { + // Unable to build track + Audio::Play3D(Audio::SoundId::Error, trackLoc); + + auto windowManager = GetContext()->GetUiContext()->GetWindowManager(); + windowManager->ShowError(res.GetErrorTitle(), res.GetErrorMessage()); + return; + } + + auto tdAction = TrackDesignAction({ trackLoc, _currentTrackPieceDirection }, *_trackDesign); + tdAction.SetCallback([&](const GameAction*, const GameActions::Result* result) { + if (result->Error != GameActions::Status::Ok) + { + Audio::Play3D(Audio::SoundId::Error, result->Position); + return; + } + + rideId = result->GetData(); + auto getRide = GetRide(rideId); + if (getRide != nullptr) + { + WindowCloseByClass(WindowClass::Error); + Audio::Play3D(Audio::SoundId::PlaceItem, trackLoc); + + _currentRideIndex = rideId; + if (TrackDesignAreEntranceAndExitPlaced()) + { + auto intent = Intent(WindowClass::Ride); + intent.PutExtra(INTENT_EXTRA_RIDE_ID, rideId.ToUnderlying()); + ContextOpenIntent(&intent); + auto wnd = WindowFindByClass(WindowClass::TrackDesignPlace); + WindowClose(*wnd); + } + else + { + RideInitialiseConstructionWindow(*getRide); + auto wnd = WindowFindByClass(WindowClass::RideConstruction); + wnd->OnMouseUp(WC_RIDE_CONSTRUCTION__WIDX_ENTRANCE); + } + } + }); + GameActions::Execute(&tdAction); } void OnToolAbort(WidgetIndex widgetIndex) override @@ -385,16 +430,6 @@ namespace OpenRCT2::Ui::Windows } private: - std::unique_ptr _trackDesign; - - CoordsXY _placementLoc; - RideId _placementGhostRideId; - bool _hasPlacementGhost; - money64 _placementCost; - CoordsXYZD _placementGhostLoc; - - std::vector _miniPreview; - void ClearProvisional() { if (_hasPlacementGhost) @@ -408,31 +443,102 @@ namespace OpenRCT2::Ui::Windows } } - int32_t GetBaseZ(const CoordsXY& loc) + std::optional GetBaseZ([[maybe_unused]] const CoordsXY& loc, const ScreenCoordsXY& screenCoords) { - auto surfaceElement = MapGetSurfaceElementAt(loc); + CoordsXY mapCoords = ViewportInteractionGetTileStartAtCursor(screenCoords); + auto surfaceElement = MapGetSurfaceElementAt(mapCoords); if (surfaceElement == nullptr) - return 0; + return std::nullopt; - auto z = surfaceElement->GetBaseZ(); + auto& im = GetInputManager(); - // Increase Z above slope - if (surfaceElement->GetSlope() & kTileSlopeRaisedCornersMask) + if (!_trackPlaceCtrlState && im.IsModifierKeyPressed(ModifierKey::ctrl)) { - z += 16; + constexpr auto interactionFlags = EnumsToFlags( + ViewportInteractionItem::Terrain, ViewportInteractionItem::Ride, ViewportInteractionItem::Scenery, + ViewportInteractionItem::Footpath, ViewportInteractionItem::Wall, ViewportInteractionItem::LargeScenery); - // Increase Z above double slope - if (surfaceElement->GetSlope() & kTileSlopeDiagonalFlag) - z += 16; + auto info = GetMapCoordinatesFromPos(screenCoords, interactionFlags); + if (info.SpriteType == ViewportInteractionItem::Terrain) + { + _trackPlaceCtrlZ = Floor2(surfaceElement->GetBaseZ(), kCoordsZStep); + + // Increase Z above water + if (surfaceElement->GetWaterHeight() > 0) + _trackPlaceCtrlZ = std::max(_trackPlaceCtrlZ, surfaceElement->GetWaterHeight()); + } + else + { + _trackPlaceCtrlZ = Floor2(info.Element->GetBaseZ(), kCoordsZStep); + } + + _trackPlaceCtrlState = true; + } + else if (!im.IsModifierKeyPressed(ModifierKey::ctrl)) + { + _trackPlaceCtrlState = false; + _trackPlaceCtrlZ = 0; } - // Increase Z above water - if (surfaceElement->GetWaterHeight() > 0) - z = std::max(z, surfaceElement->GetWaterHeight()); + if (!_trackPlaceShiftState && im.IsModifierKeyPressed(ModifierKey::shift)) + { + _trackPlaceShiftState = true; + _trackPlaceShiftStart = screenCoords; + _trackPlaceShiftZ = 0; + } + else if (im.IsModifierKeyPressed(ModifierKey::shift)) + { + uint16_t maxHeight = ZoomLevel::max().ApplyTo( + std::numeric_limits::max() - 32); - return z + _trackPlaceShiftZ = _trackPlaceShiftStart.y - screenCoords.y + 4; + + // Scale delta by zoom to match mouse position. + auto* mainWnd = WindowGetMain(); + if (mainWnd != nullptr && mainWnd->viewport != nullptr) + _trackPlaceShiftZ = mainWnd->viewport->zoom.ApplyTo(_trackPlaceShiftZ); + + // Floor to closest kCoordsZStep + _trackPlaceShiftZ = Floor2(_trackPlaceShiftZ, kCoordsZStep); + + // Clamp to maximum possible value of BaseHeight can offer. + _trackPlaceShiftZ = std::min(_trackPlaceShiftZ, maxHeight); + } + else if (_trackPlaceShiftState) + { + _trackPlaceShiftState = false; + _trackPlaceShiftZ = 0; + } + + if (!_trackPlaceCtrlState) + { + _trackPlaceZ = Floor2(surfaceElement->GetBaseZ(), kCoordsZStep); + + // Increase Z above water + if (surfaceElement->GetWaterHeight() > 0) + _trackPlaceZ = std::max(_trackPlaceZ, surfaceElement->GetWaterHeight()); + + if (_trackPlaceShiftState) + { + _trackPlaceZ += _trackPlaceShiftZ; + _trackPlaceZ = std::max(16, _trackPlaceZ); + } + } + else + { + _trackPlaceZ = _trackPlaceCtrlZ; + if (_trackPlaceShiftState) + _trackPlaceZ += _trackPlaceShiftZ; + + _trackPlaceZ = std::max(16, _trackPlaceZ); + } + + if (mapCoords.x == kLocationNull) + return std::nullopt; + + return _trackPlaceZ + TrackDesignGetZPlacement( - *_trackDesign, RideGetTemporaryForPreview(), { loc, z, _currentTrackPieceDirection }); + *_trackDesign, RideGetTemporaryForPreview(), { mapCoords, _trackPlaceZ, _currentTrackPieceDirection }); } void DrawMiniPreviewEntrances( @@ -597,7 +703,7 @@ namespace OpenRCT2::Ui::Windows GameActions::Result FindValidTrackDesignPlaceHeight(CoordsXYZ& loc, uint32_t newFlags) { GameActions::Result res; - for (int32_t i = 0; i < 7; i++, loc.z += 8) + for (int32_t i = 0; i < 7; i++, loc.z += kCoordsZStep) { auto tdAction = TrackDesignAction( CoordsXYZD{ loc.x, loc.y, loc.z, _currentTrackPieceDirection }, *_trackDesign);