From 6a5dd316a175934b7379a1bf0b66f4bc2e9fa8bc Mon Sep 17 00:00:00 2001 From: Michael Steenbeek <1478678+Gymnasiast@users.noreply.github.com> Date: Sun, 16 Nov 2025 17:14:20 +0100 Subject: [PATCH] Allow dragging lines of walls --- distribution/changelog.txt | 1 + src/openrct2-ui/windows/Scenery.cpp | 232 +++++++++++++++++++++-- src/openrct2/actions/WallPlaceAction.cpp | 2 +- src/openrct2/network/NetworkBase.cpp | 2 +- 4 files changed, 221 insertions(+), 16 deletions(-) diff --git a/distribution/changelog.txt b/distribution/changelog.txt index f0f73438d2..ee9881f99c 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -1,5 +1,6 @@ 0.4.29 (in development) ------------------------------------------------------------------------ +- Feature: [#25459] Wall line dragging tool. - Improved: [#25028] Stalls now support colour presets, just like regular rides. - Improved: [#25426] Building the track designs index is now quicker. - Improved: [#25490] The ‘New Ride’ window can now be resized. diff --git a/src/openrct2-ui/windows/Scenery.cpp b/src/openrct2-ui/windows/Scenery.cpp index 5e928b5f46..c3d3892c06 100644 --- a/src/openrct2-ui/windows/Scenery.cpp +++ b/src/openrct2-ui/windows/Scenery.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -206,6 +207,12 @@ namespace OpenRCT2::Ui::Windows } }; + struct ProvisionalWallTile + { + CoordsXYZD location; + int32_t calculatedZ; + }; + std::vector _tabEntries; int32_t _requiredWidth; int32_t _actualMinHeight; @@ -216,6 +223,18 @@ namespace OpenRCT2::Ui::Windows uint8_t _unkF64F0E{ 0 }; int16_t _unkF64F0A{ 0 }; + CoordsXY _dragStartPos{}; + /** + * When placing fences in the air using Shift, the user may shift their mouse to the left or right. + * We save this position to avoid detecting a wall drag where there isn’t one. + */ + CoordsXY _dragStartHoverPos{}; + CoordsXY _dragEndPos{}; + uint8_t _startEdge{}; + GameActions::Result _lastProvisionalError{}; + bool _inDragMode = false; + std::vector _provisionalTiles{}; + public: void onOpen() override { @@ -255,6 +274,7 @@ namespace OpenRCT2::Ui::Windows void onClose() override { SceneryRemoveGhostToolPlacement(); + removeProvisionalTilesFromMap(); HideGridlines(); ViewportSetVisibility(ViewportVisibility::standard); @@ -1814,6 +1834,13 @@ namespace OpenRCT2::Ui::Windows void onToolUpdateWall(WidgetIndex widgetIndex, const ScreenCoordsXY& screenPos, ScenerySelection selection) { + bool isLeftMousePressed = gInputFlags.has(InputFlag::leftMousePressed); + if (isLeftMousePressed) + { + gMapSelectFlags.set(MapSelectFlag::enable); + return; + } + CoordsXY mapTile = {}; uint8_t edge; @@ -1826,10 +1853,7 @@ namespace OpenRCT2::Ui::Windows } gMapSelectFlags.set(MapSelectFlag::enable); - gMapSelectPositionA.x = mapTile.x; - gMapSelectPositionA.y = mapTile.y; - gMapSelectPositionB.x = mapTile.x; - gMapSelectPositionB.y = mapTile.y; + setMapSelectRange(mapTile); gMapSelectType = getMapSelectEdge(edge); MapInvalidateSelectionRect(); @@ -2717,6 +2741,8 @@ namespace OpenRCT2::Ui::Windows if (gridPos.IsNull()) return; + gridPos = gridPos.ToTileStart(); + if (Config::Get().general.virtualFloorStyle != VirtualFloorStyles::Off) { VirtualFloorSetHeight(gSceneryPlaceZ); @@ -3027,6 +3053,31 @@ namespace OpenRCT2::Ui::Windows auto res = GameActions::Execute(&footpathAdditionPlaceAction, gameState); } + std::optional getMapPosFromScreenPos(const ScreenCoordsXY& screenCoords) + { + CoordsXY coords; + if (gSceneryPlaceZ > 0) + { + auto candidate = ScreenGetMapXYWithZ(screenCoords, gSceneryPlaceZ); + if (!candidate.has_value()) + return std::nullopt; + + coords = *candidate; + } + else + { + auto info = GetMapCoordinatesFromPos(screenCoords, EnumsToFlags(ViewportInteractionItem::terrain)); + + if (info.interactionType == ViewportInteractionItem::none) + return std::nullopt; + + coords = info.Loc; + } + + coords = coords.ToTileStart(); + return coords; + } + void onToolDownWall(WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords, uint16_t selectedScenery) { CoordsXY gridPos; @@ -3068,19 +3119,165 @@ namespace OpenRCT2::Ui::Windows } } - auto wallPlaceAction = GameActions::WallPlaceAction( - selectedScenery, { gridPos, gSceneryPlaceZ }, edges, _sceneryPrimaryColour, _scenerySecondaryColour, - _sceneryTertiaryColour); + _inDragMode = true; + _dragStartPos = gridPos; + auto hoverPos = getMapPosFromScreenPos(screenCoords); + _dragStartHoverPos = hoverPos.has_value() ? *hoverPos : gridPos; + _dragEndPos = {}; + _startEdge = edges; + gMapSelectFlags.set(MapSelectFlag::enable); + gMapSelectType = getMapSelectEdge(_startEdge); + setMapSelectRange(gridPos); + } - wallPlaceAction.SetCallback([](const GameActions::GameAction* ga, const GameActions::Result* result) { - if (result->Error == GameActions::Status::Ok) - { - Audio::Play3D(Audio::SoundId::placeItem, result->Position); - } - }); + void dragWallSetEndPos(const ScreenCoordsXY& screenCoords) + { + auto endCoords = getMapPosFromScreenPos(screenCoords); + if (!endCoords.has_value()) + return; + if (endCoords == _dragStartHoverPos) + { + endCoords = _dragStartPos; + } + + const bool onXAxis = (_startEdge == 1 || _startEdge == 3); + if (onXAxis) + { + endCoords->y = _dragStartPos.y; + } + else + { + endCoords->x = _dragStartPos.x; + } + + if (*endCoords != _dragEndPos) + { + setMapSelectRange({ _dragStartPos, *endCoords }); + updateProvisionalTiles(); + } + _dragEndPos = *endCoords; + } + + void removeProvisionalTilesFromMap() + { auto& gameState = getGameState(); - auto res = GameActions::Execute(&wallPlaceAction, gameState); + for (const auto& tile : _provisionalTiles) + { + auto location = tile.location; + location.z = tile.calculatedZ; + auto wallRemoveAction = GameActions::WallRemoveAction(location); + wallRemoveAction.SetFlags( + GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND | GAME_COMMAND_FLAG_GHOST); + wallRemoveAction.Execute(gameState); + } + } + + void updateProvisionalTiles() + { + auto& gameState = getGameState(); + auto tabSelection = WindowSceneryGetTabSelection(); + removeProvisionalTilesFromMap(); + + _provisionalTiles.clear(); + _lastProvisionalError = {}; + + auto mapRange = getMapSelectRange(); + for (auto y = mapRange.GetY1(); y <= mapRange.GetY2(); y += kCoordsXYStep) + { + for (auto x = mapRange.GetX1(); x <= mapRange.GetX2(); x += kCoordsXYStep) + { + auto wallPlaceAction = GameActions::WallPlaceAction( + tabSelection.EntryIndex, { x, y, gSceneryPlaceZ }, _startEdge, _sceneryPrimaryColour, + _scenerySecondaryColour, _sceneryTertiaryColour); + wallPlaceAction.SetFlags(GAME_COMMAND_FLAG_GHOST | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED); + + auto result = GameActions::Execute(&wallPlaceAction, gameState); + if (result.Error == GameActions::Status::Ok) + { + const auto placementData = result.GetData(); + _provisionalTiles.push_back({ CoordsXYZD(x, y, gSceneryPlaceZ, _startEdge), placementData.BaseHeight }); + } + else + { + _lastProvisionalError = result; + } + } + } + } + + void onToolDragWall(const ScreenCoordsXY& screenCoords) + { + if (!_inDragMode) + return; + + dragWallSetEndPos(screenCoords); + } + + void onToolUp(WidgetIndex, const ScreenCoordsXY&) override + { + if (_sceneryPaintEnabled || gWindowSceneryEyedropperEnabled) + return; + + auto tabSelection = WindowSceneryGetTabSelection(); + auto sceneryType = tabSelection.SceneryType; + if (sceneryType == SCENERY_TYPE_WALL) + { + onToolUpWall(tabSelection.EntryIndex); + } + } + + void onToolUpWall(uint16_t selectedScenery) + { + removeProvisionalTilesFromMap(); + + if (!_inDragMode) + { + _provisionalTiles.clear(); + return; + } + + if (_provisionalTiles.empty()) + { + auto z = gSceneryPlaceZ > 0 ? gSceneryPlaceZ : TileElementHeight(_dragStartPos); + Audio::Play3D(Audio::SoundId::error, { _dragStartPos, z }); + + if (_lastProvisionalError.Error != GameActions::Status::Ok) + { + auto windowManager = Ui::GetWindowManager(); + windowManager->ShowError( + _lastProvisionalError.GetErrorTitle(), _lastProvisionalError.GetErrorMessage(), true); + } + + _inDragMode = false; + return; + } + + auto mapRange = getMapSelectRange(); + bool anySuccessful = false; + CoordsXYZ lastLocation = { mapRange.Point2, gSceneryPlaceZ }; + for (const auto& tile : _provisionalTiles) + { + auto wallPlaceAction = GameActions::WallPlaceAction( + selectedScenery, tile.location, tile.location.direction, _sceneryPrimaryColour, _scenerySecondaryColour, + _sceneryTertiaryColour); + + auto& gameState = getGameState(); + auto result = GameActions::Execute(&wallPlaceAction, gameState); + if (result.Error == GameActions::Status::Ok) + { + anySuccessful = true; + lastLocation = result.Position; + } + } + + _provisionalTiles.clear(); + _inDragMode = false; + + if (anySuccessful) + { + Audio::Play3D(Audio::SoundId::placeItem, lastLocation); + } } void onToolDownLargeScenery(WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords, uint16_t selectedScenery) @@ -3223,6 +3420,13 @@ namespace OpenRCT2::Ui::Windows { if (_sceneryPaintEnabled || gWindowSceneryEyedropperEnabled) onToolDown(widgetIndex, screenCoords); + + auto tabSelection = WindowSceneryGetTabSelection(); + auto sceneryType = tabSelection.SceneryType; + if (sceneryType == SCENERY_TYPE_WALL) + { + onToolDragWall(screenCoords); + } } }; diff --git a/src/openrct2/actions/WallPlaceAction.cpp b/src/openrct2/actions/WallPlaceAction.cpp index 68ebf70c47..7945788ac4 100644 --- a/src/openrct2/actions/WallPlaceAction.cpp +++ b/src/openrct2/actions/WallPlaceAction.cpp @@ -268,7 +268,7 @@ namespace OpenRCT2::GameActions res.Cost = wallEntry->price; - res.SetData(WallPlaceActionResult{}); + res.SetData(WallPlaceActionResult{ targetHeight }); return res; } diff --git a/src/openrct2/network/NetworkBase.cpp b/src/openrct2/network/NetworkBase.cpp index ac7e74469a..69b9f42790 100644 --- a/src/openrct2/network/NetworkBase.cpp +++ b/src/openrct2/network/NetworkBase.cpp @@ -47,7 +47,7 @@ // It is used for making sure only compatible builds get connected, even within // single OpenRCT2 version. -constexpr uint8_t kStreamVersion = 3; +constexpr uint8_t kStreamVersion = 4; const std::string kStreamID = std::string(kOpenRCT2Version) + "-" + std::to_string(kStreamVersion);