/***************************************************************************** * Copyright (c) 2014-2025 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace OpenRCT2::Numerics; namespace OpenRCT2::Ui::Windows { struct ProvisionalTile { CoordsXYZ position; FootpathSlope slope; constexpr bool operator==(const ProvisionalTile& rhs) const { return position == rhs.position && slope == rhs.slope; } }; static money64 FootpathProvisionalSet( ObjectEntryIndex type, ObjectEntryIndex railingsType, const CoordsXY& footpathLocA, const CoordsXY& footpathLocB, int32_t startZ, const std::span tiles, PathConstructFlags constructFlags); enum class PathConstructionMode : uint8_t { onLand, dragArea, /** * When picking a location to start the bridge or tunnel */ bridgeOrTunnelPick, /** * When actually building a bridge or tunnel (enables the appropriate buttons) */ bridgeOrTunnel, }; enum class ProvisionalPathFlag : uint8_t { showArrow = 0, /** * Set when any provisional path is present. */ placed = 1, forceRecheck = 2, }; enum class SlopePitch : uint8_t { flat, upwards, downwards, }; using ProvisionalPathFlags = FlagHolder; struct ProvisionalFootpath { ObjectEntryIndex type{}; CoordsXY positionA{}; CoordsXY positionB{}; /** * Z of the first tile. Used for checking if the provisional path needs updating when in dragArea mode. */ int32_t startZ{}; ProvisionalPathFlags flags{}; ObjectEntryIndex surfaceIndex{}; ObjectEntryIndex railingsIndex{}; PathConstructFlags constructFlags{}; std::vector tiles{}; }; static ProvisionalFootpath _provisionalFootpath; static CoordsXYZ _footpathConstructFromPosition; static SlopePitch _footpathConstructSlope; #pragma region Measurements static constexpr StringId kWindowTitle = STR_FOOTPATHS; static constexpr ScreenSize kWindowSize = { 106, 461 }; #pragma endregion #pragma region Widgets enum WindowFootpathWidgetIdx : WidgetIndex { WIDX_BACKGROUND, WIDX_TITLE, WIDX_CLOSE, WIDX_TYPE_GROUP, WIDX_FOOTPATH_TYPE, WIDX_QUEUELINE_TYPE, WIDX_RAILINGS_TYPE, WIDX_DIRECTION_GROUP, WIDX_DIRECTION_NW, WIDX_DIRECTION_NE, WIDX_DIRECTION_SW, WIDX_DIRECTION_SE, WIDX_SLOPE_GROUP, WIDX_SLOPEDOWN, WIDX_LEVEL, WIDX_SLOPEUP, WIDX_CONSTRUCT, WIDX_REMOVE, WIDX_MODE_GROUP, WIDX_CONSTRUCT_ON_LAND, WIDX_CONSTRUCT_BRIDGE_OR_TUNNEL, WIDX_CONSTRUCT_DRAG_AREA, }; // clang-format off static constexpr auto kWindowFootpathWidgets = makeWidgets( makeWindowShim(kWindowTitle, kWindowSize), // Type group makeWidget({ 3, 17}, {100, 95}, WidgetType::groupbox, WindowColour::primary , STR_TYPE ), makeWidget({ 6, 30}, { 47, 36}, WidgetType::flatBtn, WindowColour::secondary, 0xFFFFFFFF, STR_FOOTPATH_TIP ), makeWidget({53, 30}, { 47, 36}, WidgetType::flatBtn, WindowColour::secondary, 0xFFFFFFFF, STR_QUEUE_LINE_PATH_TIP ), makeWidget({29, 69}, { 47, 36}, WidgetType::flatBtn, WindowColour::secondary, 0xFFFFFFFF, STR_OBJECT_SELECTION_FOOTPATH_RAILINGS ), // Direction group makeWidget({ 3, 115}, {100, 77}, WidgetType::groupbox, WindowColour::primary , STR_DIRECTION ), makeWidget({53, 127}, { 45, 29}, WidgetType::flatBtn, WindowColour::secondary, ImageId(SPR_CONSTRUCTION_DIRECTION_NE), STR_DIRECTION_TIP ), makeWidget({53, 156}, { 45, 29}, WidgetType::flatBtn, WindowColour::secondary, ImageId(SPR_CONSTRUCTION_DIRECTION_SE), STR_DIRECTION_TIP ), makeWidget({ 8, 156}, { 45, 29}, WidgetType::flatBtn, WindowColour::secondary, ImageId(SPR_CONSTRUCTION_DIRECTION_SW), STR_DIRECTION_TIP ), makeWidget({ 8, 127}, { 45, 29}, WidgetType::flatBtn, WindowColour::secondary, ImageId(SPR_CONSTRUCTION_DIRECTION_NW), STR_DIRECTION_TIP ), // Slope group makeWidget({ 3, 195}, {100, 41}, WidgetType::groupbox, WindowColour::primary , STR_SLOPE ), makeWidget({17, 207}, { 24, 24}, WidgetType::flatBtn, WindowColour::secondary, ImageId(SPR_RIDE_CONSTRUCTION_SLOPE_DOWN), STR_SLOPE_DOWN_TIP ), makeWidget({41, 207}, { 24, 24}, WidgetType::flatBtn, WindowColour::secondary, ImageId(SPR_RIDE_CONSTRUCTION_SLOPE_LEVEL), STR_LEVEL_TIP ), makeWidget({65, 207}, { 24, 24}, WidgetType::flatBtn, WindowColour::secondary, ImageId(SPR_RIDE_CONSTRUCTION_SLOPE_UP), STR_SLOPE_UP_TIP ), makeWidget({ 8, 242}, { 90, 90}, WidgetType::flatBtn, WindowColour::secondary, 0xFFFFFFFF, STR_CONSTRUCT_THE_SELECTED_FOOTPATH_SECTION_TIP), makeWidget({30, 335}, { 46, 24}, WidgetType::flatBtn, WindowColour::secondary, ImageId(SPR_DEMOLISH_CURRENT_SECTION), STR_REMOVE_PREVIOUS_FOOTPATH_SECTION_TIP ), // Mode group makeWidget({ 3, 361}, {100, 99}, WidgetType::groupbox, WindowColour::primary ), makeWidget({13, 372}, { 36, 36}, WidgetType::flatBtn, WindowColour::secondary, ImageId(SPR_CONSTRUCTION_FOOTPATH_LAND), STR_CONSTRUCT_FOOTPATH_ON_LAND_TIP ), makeWidget({57, 372}, { 36, 36}, WidgetType::flatBtn, WindowColour::secondary, ImageId(SPR_CONSTRUCTION_FOOTPATH_BRIDGE), STR_CONSTRUCT_BRIDGE_OR_TUNNEL_FOOTPATH_TIP ), makeWidget({35, 416}, { 36, 36}, WidgetType::flatBtn, WindowColour::secondary, ImageId(SPR_G2_ICON_PATH_DRAG_TOOL), STR_DRAG_AREA_OF_FOOTPATH_TIP ) ); #pragma endregion /** rct2: 0x0098D7E0 */ static constexpr uint8_t kConstructionPreviewImages[][4] = { { 5, 10, 5, 10 }, // SlopePitch::flat { 16, 17, 18, 19 }, // SlopePitch::upwards { 18, 19, 16, 17 }, // SlopePitch::downwards }; // clang-format on class FootpathWindow final : public Window { private: const uint16_t ARROW_PULSE_DURATION = 200; uint8_t _footpathConstructDirection; uint8_t _footpathConstructValidDirections; PathConstructionMode _footpathConstructionMode; std::vector> _dropdownEntries; money64 _windowFootpathCost; uint32_t _footpathConstructionNextArrowPulse = 0; uint8_t _lastUpdatedCameraRotation = UINT8_MAX; bool _footpathErrorOccured = false; bool _footpathPlaceCtrlState; int32_t _footpathPlaceCtrlZ; bool _footpathPlaceShiftState; ScreenCoordsXY _footpathPlaceShiftStart; int32_t _footpathPlaceShiftZ; int32_t _footpathPlaceZ; CoordsXY _dragStartPos; public: #pragma region Window Override Events void onOpen() override { setWidgets(kWindowFootpathWidgets); WindowInitScrollWidgets(*this); WindowPushOthersRight(*this); ShowGridlines(); ToolCancel(); _footpathConstructionMode = PathConstructionMode::onLand; ToolSet(*this, WIDX_CONSTRUCT_ON_LAND, Tool::pathDown); gInputFlags.set(InputFlag::unk6); _footpathErrorOccured = false; WindowFootpathSetEnabledAndPressedWidgets(); _footpathPlaceCtrlState = false; _footpathPlaceShiftState = false; holdDownWidgets = (1u << WIDX_CONSTRUCT) | (1u << WIDX_REMOVE); } void onClose() override { FootpathUpdateProvisional(); ViewportSetVisibility(ViewportVisibility::standard); MapInvalidateMapSelectionTiles(); gMapSelectFlags.unset(MapSelectFlag::enableConstruct); auto* windowMgr = Ui::GetWindowManager(); windowMgr->InvalidateByClass(WindowClass::topToolbar); HideGridlines(); } void onUpdate() override { WindowFootpathUpdateProvisionalPathForBridgeMode(); // #2502: The camera might have changed rotation, so we need to update which directional buttons are pressed uint8_t currentRotation = GetCurrentRotation(); if (_lastUpdatedCameraRotation != currentRotation) { _lastUpdatedCameraRotation = currentRotation; WindowFootpathSetEnabledAndPressedWidgets(); } // Check tool switch (_footpathConstructionMode) { case PathConstructionMode::onLand: if (!isToolActive(WindowClass::footpath, WIDX_CONSTRUCT_ON_LAND)) close(); break; case PathConstructionMode::dragArea: break; case PathConstructionMode::bridgeOrTunnelPick: if (!isToolActive(WindowClass::footpath, WIDX_CONSTRUCT_BRIDGE_OR_TUNNEL)) close(); break; case PathConstructionMode::bridgeOrTunnel: break; } // If another window has enabled a tool, close ours. if (gInputFlags.has(InputFlag::toolActive) && gCurrentToolWidget.windowClassification != WindowClass::footpath) close(); } void onMouseDown(WidgetIndex widgetIndex) override { switch (widgetIndex) { case WIDX_FOOTPATH_TYPE: WindowFootpathShowFootpathTypesDialog(&widgets[widgetIndex], false); break; case WIDX_QUEUELINE_TYPE: WindowFootpathShowFootpathTypesDialog(&widgets[widgetIndex], true); break; case WIDX_RAILINGS_TYPE: WindowFootpathShowRailingsTypesDialog(&widgets[widgetIndex]); break; case WIDX_DIRECTION_NW: WindowFootpathMousedownDirection(0); break; case WIDX_DIRECTION_NE: WindowFootpathMousedownDirection(1); break; case WIDX_DIRECTION_SW: WindowFootpathMousedownDirection(2); break; case WIDX_DIRECTION_SE: WindowFootpathMousedownDirection(3); break; case WIDX_SLOPEDOWN: WindowFootpathMousedownSlope(SlopePitch::downwards); break; case WIDX_LEVEL: WindowFootpathMousedownSlope(SlopePitch::flat); break; case WIDX_SLOPEUP: WindowFootpathMousedownSlope(SlopePitch::upwards); break; case WIDX_CONSTRUCT: WindowFootpathConstruct(); break; case WIDX_REMOVE: WindowFootpathRemove(); break; } } void enableDragAreaMode() { _windowFootpathCost = kMoney64Undefined; ToolCancel(); FootpathUpdateProvisional(); MapInvalidateMapSelectionTiles(); gMapSelectFlags.unset(MapSelectFlag::enableConstruct); _footpathConstructionMode = PathConstructionMode::dragArea; ToolSet(*this, WIDX_CONSTRUCT_DRAG_AREA, Tool::pathDown); gInputFlags.set(InputFlag::unk6); _footpathErrorOccured = false; WindowFootpathSetEnabledAndPressedWidgets(); } void onMouseUp(WidgetIndex widgetIndex) override { switch (widgetIndex) { case WIDX_CLOSE: close(); break; case WIDX_CONSTRUCT_ON_LAND: if (_footpathConstructionMode == PathConstructionMode::onLand) { break; } _windowFootpathCost = kMoney64Undefined; ToolCancel(); FootpathUpdateProvisional(); MapInvalidateMapSelectionTiles(); gMapSelectFlags.unset(MapSelectFlag::enableConstruct); _footpathConstructionMode = PathConstructionMode::onLand; ToolSet(*this, WIDX_CONSTRUCT_ON_LAND, Tool::pathDown); gInputFlags.set(InputFlag::unk6); _footpathErrorOccured = false; WindowFootpathSetEnabledAndPressedWidgets(); break; case WIDX_CONSTRUCT_DRAG_AREA: if (_footpathConstructionMode == PathConstructionMode::dragArea) { break; } enableDragAreaMode(); break; case WIDX_CONSTRUCT_BRIDGE_OR_TUNNEL: if (_footpathConstructionMode == PathConstructionMode::bridgeOrTunnelPick) { break; } _windowFootpathCost = kMoney64Undefined; ToolCancel(); FootpathUpdateProvisional(); MapInvalidateMapSelectionTiles(); gMapSelectFlags.unset(MapSelectFlag::enableConstruct); _footpathConstructionMode = PathConstructionMode::bridgeOrTunnelPick; ToolSet(*this, WIDX_CONSTRUCT_BRIDGE_OR_TUNNEL, Tool::crosshair); gInputFlags.set(InputFlag::unk6); _footpathErrorOccured = false; WindowFootpathSetEnabledAndPressedWidgets(); break; } } void onDropdown(WidgetIndex widgetIndex, int32_t selectedIndex) override { if (selectedIndex < 0 || static_cast(selectedIndex) >= _dropdownEntries.size()) return; auto entryIndex = _dropdownEntries[selectedIndex]; if (widgetIndex == WIDX_FOOTPATH_TYPE) { gFootpathSelection.IsQueueSelected = false; if (entryIndex.first == ObjectType::paths) { gFootpathSelection.LegacyPath = entryIndex.second; } else { gFootpathSelection.LegacyPath = kObjectEntryIndexNull; gFootpathSelection.NormalSurface = entryIndex.second; } } else if (widgetIndex == WIDX_QUEUELINE_TYPE) { gFootpathSelection.IsQueueSelected = true; if (entryIndex.first == ObjectType::paths) { gFootpathSelection.LegacyPath = entryIndex.second; } else { gFootpathSelection.LegacyPath = kObjectEntryIndexNull; gFootpathSelection.QueueSurface = entryIndex.second; } } else if (widgetIndex == WIDX_RAILINGS_TYPE) { gFootpathSelection.Railings = entryIndex.second; } else { return; } FootpathUpdateProvisional(); _windowFootpathCost = kMoney64Undefined; invalidate(); } void onToolUpdate(WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords) override { if (widgetIndex == WIDX_CONSTRUCT_ON_LAND) { WindowFootpathSetProvisionalPathAtPoint(screenCoords); } else if (widgetIndex == WIDX_CONSTRUCT_DRAG_AREA) { WindowFootpathPlaceDragAreaHover(screenCoords); } else if (widgetIndex == WIDX_CONSTRUCT_BRIDGE_OR_TUNNEL) { WindowFootpathSetSelectionStartBridgeAtPoint(screenCoords); } } void onToolUp(WidgetIndex widgetIndex, const ScreenCoordsXY&) override { if (widgetIndex == WIDX_CONSTRUCT_ON_LAND) { _footpathErrorOccured = false; } else if (widgetIndex == WIDX_CONSTRUCT_DRAG_AREA) { WindowFootpathPlacePath(); _footpathErrorOccured = false; } } void onToolDown(WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords) override { if (widgetIndex == WIDX_CONSTRUCT_ON_LAND) { WindowFootpathPlacePathAtPoint(screenCoords); } else if (widgetIndex == WIDX_CONSTRUCT_DRAG_AREA) { WindowFootpathPlaceDragAreaSetStart(screenCoords); } else if (widgetIndex == WIDX_CONSTRUCT_BRIDGE_OR_TUNNEL) { WindowFootpathStartBridgeAtPoint(screenCoords); } } void onToolDrag(WidgetIndex widgetIndex, const ScreenCoordsXY& screenCoords) override { if (widgetIndex == WIDX_CONSTRUCT_ON_LAND) { WindowFootpathPlacePathAtPoint(screenCoords); } else if (widgetIndex == WIDX_CONSTRUCT_DRAG_AREA) { WindowFootpathPlaceDragAreaSetEnd(screenCoords); } } void onToolAbort(WidgetIndex widgetIndex) override { // Needs both checks as this will otherwise be triggered when switching from the dragArea tool // to another. if (widgetIndex == WIDX_CONSTRUCT_DRAG_AREA && _footpathConstructionMode == PathConstructionMode::dragArea) { FootpathUpdateProvisional(); enableDragAreaMode(); // When this tool is aborted, the mouse button is still down. // Set this to prevent a stray piece of path from being placed under the mouse pointer. _footpathErrorOccured = true; } } void onPrepareDraw() override { // Press / unpress footpath and queue type buttons pressedWidgets &= ~(1uLL << WIDX_FOOTPATH_TYPE); pressedWidgets &= ~(1uLL << WIDX_QUEUELINE_TYPE); pressedWidgets |= gFootpathSelection.IsQueueSelected ? (1uLL << WIDX_QUEUELINE_TYPE) : (1uLL << WIDX_FOOTPATH_TYPE); // Enable / disable construct button widgets[WIDX_CONSTRUCT].type = _footpathConstructionMode == PathConstructionMode::bridgeOrTunnel ? WidgetType::imgBtn : WidgetType::empty; if (gFootpathSelection.LegacyPath == kObjectEntryIndexNull) { widgets[WIDX_RAILINGS_TYPE].type = WidgetType::flatBtn; } else { widgets[WIDX_RAILINGS_TYPE].type = WidgetType::empty; } } void onDraw(RenderTarget& rt) override { ScreenCoordsXY screenCoords; WindowDrawWidgets(*this, rt); WindowFootpathDrawDropdownButtons(rt); if (!isWidgetDisabled(WIDX_CONSTRUCT)) { // Get construction image uint8_t direction = (_footpathConstructDirection + GetCurrentRotation()) % kNumOrthogonalDirections; auto slopeOffset = EnumValue(_footpathConstructSlope); std::optional baseImage; if (gFootpathSelection.LegacyPath == kObjectEntryIndexNull) { auto selectedPath = gFootpathSelection.GetSelectedSurface(); const auto* pathType = GetPathSurfaceEntry(selectedPath); if (pathType != nullptr) { baseImage = pathType->BaseImageId; } } else { auto& objManager = OpenRCT2::GetContext()->GetObjectManager(); const auto* pathObj = objManager.GetLoadedObject(gFootpathSelection.LegacyPath); if (pathObj != nullptr) { auto pathEntry = reinterpret_cast(pathObj->GetLegacyData()); if (gFootpathSelection.IsQueueSelected) baseImage = pathEntry->GetQueueImage(); else baseImage = pathEntry->image; } } if (baseImage) { auto image = *baseImage + kConstructionPreviewImages[slopeOffset][direction]; // Draw construction image screenCoords = this->windowPos + ScreenCoordsXY{ widgets[WIDX_CONSTRUCT].midX(), widgets[WIDX_CONSTRUCT].bottom - 60 }; GfxDrawSprite(rt, ImageId(image), screenCoords); } // Draw build this... label screenCoords = this->windowPos + ScreenCoordsXY{ widgets[WIDX_CONSTRUCT].midX(), widgets[WIDX_CONSTRUCT].bottom - 23 }; DrawTextBasic(rt, screenCoords, STR_BUILD_THIS, {}, { TextAlignment::CENTRE }); } // Draw cost screenCoords = this->windowPos + ScreenCoordsXY{ widgets[WIDX_CONSTRUCT].midX(), widgets[WIDX_CONSTRUCT].bottom - 12 }; if (_windowFootpathCost != kMoney64Undefined) { if (!(getGameState().park.flags & PARK_FLAGS_NO_MONEY)) { auto ft = Formatter(); ft.Add(_windowFootpathCost); DrawTextBasic(rt, screenCoords, STR_COST_LABEL, ft, { TextAlignment::CENTRE }); } } } #pragma endregion private: /** * * rct2: 0x006A7760 */ void WindowFootpathUpdateProvisionalPathForBridgeMode() { if (_footpathConstructionMode != PathConstructionMode::bridgeOrTunnel) { return; } // Recheck area for construction. Set by ride_construction window if (_provisionalFootpath.flags.has(ProvisionalPathFlag::forceRecheck)) { FootpathRemoveProvisional(); _provisionalFootpath.flags.unset(ProvisionalPathFlag::forceRecheck); } // Update provisional bridge mode path if (!(_provisionalFootpath.flags.has(ProvisionalPathFlag::placed))) { ObjectEntryIndex type; ObjectEntryIndex railings = gFootpathSelection.Railings; CoordsXYZ footpathLoc; FootpathSlope slope; FootpathGetNextPathInfo(&type, footpathLoc, &slope); auto pathConstructFlags = FootpathCreateConstructFlags(type); auto tiles = std::array({ footpathLoc, slope }); _windowFootpathCost = FootpathProvisionalSet( type, railings, footpathLoc, footpathLoc, footpathLoc.z, tiles, pathConstructFlags); invalidateWidget(WIDX_CONSTRUCT); } auto curTime = Platform::GetTicks(); // Update little directional arrow on provisional bridge mode path if (_footpathConstructionNextArrowPulse < curTime) { _footpathConstructionNextArrowPulse = curTime + ARROW_PULSE_DURATION; _provisionalFootpath.flags.flip(ProvisionalPathFlag::showArrow); CoordsXYZ footpathLoc; FootpathSlope slope; FootpathGetNextPathInfo(nullptr, footpathLoc, &slope); gMapSelectArrowPosition = footpathLoc; gMapSelectArrowDirection = _footpathConstructDirection; if (_provisionalFootpath.flags.has(ProvisionalPathFlag::showArrow)) { gMapSelectFlags.set(MapSelectFlag::enableArrow); } else { gMapSelectFlags.unset(MapSelectFlag::enableArrow); } MapInvalidateTileFull(footpathLoc); } } void WindowFootpathDrawDropdownButtons(RenderTarget& rt) { if (gFootpathSelection.LegacyPath == kObjectEntryIndexNull) { // Set footpath and queue type button images auto pathImage = kSpriteIdNull; auto queueImage = kSpriteIdNull; auto pathEntry = GetPathSurfaceEntry(gFootpathSelection.NormalSurface); if (pathEntry != nullptr) { pathImage = pathEntry->PreviewImageId; } pathEntry = GetPathSurfaceEntry(gFootpathSelection.QueueSurface); if (pathEntry != nullptr) { queueImage = pathEntry->PreviewImageId; } WindowFootpathDrawDropdownButton(rt, WIDX_FOOTPATH_TYPE, pathImage); WindowFootpathDrawDropdownButton(rt, WIDX_QUEUELINE_TYPE, queueImage); // Set railing auto railingsImage = kSpriteIdNull; auto railingsEntry = GetPathRailingsEntry(gFootpathSelection.Railings); if (railingsEntry != nullptr) { railingsImage = railingsEntry->PreviewImageId; } WindowFootpathDrawDropdownButton(rt, WIDX_RAILINGS_TYPE, railingsImage); } else { auto& objManager = OpenRCT2::GetContext()->GetObjectManager(); // Set footpath and queue type button images auto pathImage = kSpriteIdNull; auto queueImage = kSpriteIdNull; const auto* pathObj = objManager.GetLoadedObject(gFootpathSelection.LegacyPath); if (pathObj != nullptr) { auto pathEntry = reinterpret_cast(pathObj->GetLegacyData()); pathImage = pathEntry->GetPreviewImage(); queueImage = pathEntry->GetQueuePreviewImage(); } WindowFootpathDrawDropdownButton(rt, WIDX_FOOTPATH_TYPE, pathImage); WindowFootpathDrawDropdownButton(rt, WIDX_QUEUELINE_TYPE, queueImage); } } void WindowFootpathDrawDropdownButton(RenderTarget& rt, WidgetIndex widgetIndex, ImageIndex image) { const auto& widget = widgets[widgetIndex]; GfxDrawSprite(rt, ImageId(image), { windowPos.x + widget.left, windowPos.y + widget.top }); } /** * * rct2: 0x006A7F88 */ void WindowFootpathShowFootpathTypesDialog(Widget* widget, bool showQueues) { auto& objManager = OpenRCT2::GetContext()->GetObjectManager(); uint32_t numPathTypes = 0; // If the game is in sandbox mode, also show paths that are normally restricted to the scenario editor bool showEditorPaths = (gLegacyScene == LegacyScene::scenarioEditor || getGameState().cheats.sandboxMode); _dropdownEntries.clear(); std::optional defaultIndex; for (ObjectEntryIndex i = 0; i < kMaxFootpathSurfaceObjects; i++) { const auto* pathType = objManager.GetLoadedObject(i); if (pathType == nullptr) { continue; } if ((pathType->Flags & FOOTPATH_ENTRY_FLAG_SHOW_ONLY_IN_SCENARIO_EDITOR) && !showEditorPaths) { continue; } // If regular paths can be used as queue, only hide the path if we’re _not_ looking at a queue, // but the path surface is one. if (getGameState().cheats.allowRegularPathAsQueue && !showQueues && ((pathType->Flags & FOOTPATH_ENTRY_FLAG_IS_QUEUE) != 0)) { continue; } // If the cheat is disabled, hide queues from the regular path view and vice versa. else if ( !getGameState().cheats.allowRegularPathAsQueue && showQueues != ((pathType->Flags & FOOTPATH_ENTRY_FLAG_IS_QUEUE) != 0)) { continue; } if (gFootpathSelection.LegacyPath == kObjectEntryIndexNull && i == (showQueues ? gFootpathSelection.QueueSurface : gFootpathSelection.NormalSurface)) { defaultIndex = numPathTypes; } gDropdown.items[numPathTypes] = Dropdown::ImageItem(ImageId(pathType->PreviewImageId), pathType->NameStringId); _dropdownEntries.push_back({ ObjectType::footpathSurface, i }); numPathTypes++; } for (ObjectEntryIndex i = 0; i < kMaxPathObjects; i++) { auto* pathObj = objManager.GetLoadedObject(i); if (pathObj == nullptr) { continue; } auto pathEntry = reinterpret_cast(pathObj->GetLegacyData()); if ((pathEntry->flags & FOOTPATH_ENTRY_FLAG_SHOW_ONLY_IN_SCENARIO_EDITOR) && !showEditorPaths) { continue; } if (gFootpathSelection.LegacyPath != kObjectEntryIndexNull && gFootpathSelection.LegacyPath == i) { defaultIndex = numPathTypes; } auto image = ImageId(showQueues ? pathEntry->GetQueuePreviewImage() : pathEntry->GetPreviewImage()); gDropdown.items[numPathTypes] = Dropdown::ImageItem(image, pathEntry->string_idx); _dropdownEntries.push_back({ ObjectType::paths, i }); numPathTypes++; } auto itemsPerRow = DropdownGetAppropriateImageDropdownItemsPerRow(numPathTypes); WindowDropdownShowImage( windowPos.x + widget->left, windowPos.y + widget->top, widget->height() + 1, colours[1], 0, numPathTypes, 47, 36, itemsPerRow); gDropdown.hasTooltips = true; if (defaultIndex) gDropdown.defaultIndex = static_cast(*defaultIndex); } void WindowFootpathShowRailingsTypesDialog(Widget* widget) { uint32_t numRailingsTypes = 0; // If the game is in sandbox mode, also show paths that are normally restricted to the scenario editor _dropdownEntries.clear(); std::optional defaultIndex; for (int32_t i = 0; i < kMaxFootpathRailingsObjects; i++) { const auto* railingsEntry = GetPathRailingsEntry(i); if (railingsEntry == nullptr) { continue; } if (i == gFootpathSelection.Railings) { defaultIndex = numRailingsTypes; } gDropdown.items[numRailingsTypes] = Dropdown::ImageItem( ImageId(railingsEntry->PreviewImageId), railingsEntry->NameStringId); _dropdownEntries.push_back({ ObjectType::footpathRailings, i }); numRailingsTypes++; } auto itemsPerRow = DropdownGetAppropriateImageDropdownItemsPerRow(numRailingsTypes); WindowDropdownShowImage( windowPos.x + widget->left, windowPos.y + widget->top, widget->height() + 1, colours[1], 0, numRailingsTypes, 47, 36, itemsPerRow); gDropdown.hasTooltips = true; if (defaultIndex) gDropdown.defaultIndex = static_cast(*defaultIndex); } /** * * rct2: 0x006A8111 0x006A8135 0x006A815C 0x006A8183 */ void WindowFootpathMousedownDirection(int32_t direction) { FootpathUpdateProvisional(); _footpathConstructDirection = (direction - GetCurrentRotation()) & 3; _windowFootpathCost = kMoney64Undefined; WindowFootpathSetEnabledAndPressedWidgets(); } /** * * rct2: 0x006A81AA 0x006A81C5 0x006A81E0 */ void WindowFootpathMousedownSlope(SlopePitch pitch) { FootpathUpdateProvisional(); _footpathConstructSlope = pitch; _windowFootpathCost = kMoney64Undefined; WindowFootpathSetEnabledAndPressedWidgets(); } void WindowFootpathUpdateModifierKeyState(ScreenCoordsXY& screenCoords) { auto& im = GetInputManager(); // First, store the initial copy/ctrl state if (!_footpathPlaceCtrlState && 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.interactionType != 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; } // In addition, vertical shifting on top of the base (copy) placement? if (!_footpathPlaceShiftState && im.isModifierKeyPressed(ModifierKey::shift)) { _footpathPlaceShiftState = true; _footpathPlaceShiftStart = screenCoords; _footpathPlaceShiftZ = 0; } else if (im.isModifierKeyPressed(ModifierKey::shift)) { uint16_t maxPathHeight = 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, maxPathHeight); screenCoords = _footpathPlaceShiftStart; } else if (_footpathPlaceShiftState) { _footpathPlaceShiftState = false; _footpathPlaceShiftZ = 0; } } std::optional FootpathGetPlacePositionFromScreenPosition(ScreenCoordsXY screenCoords) { WindowFootpathUpdateModifierKeyState(screenCoords); CoordsXY mapCoords; if (!_footpathPlaceCtrlState) { auto info = GetMapCoordinatesFromPos( screenCoords, EnumsToFlags(ViewportInteractionItem::terrain, ViewportInteractionItem::footpath)); if (info.interactionType == ViewportInteractionItem::none) return std::nullopt; mapCoords = info.Loc; _footpathPlaceZ = 0; if (_footpathPlaceShiftState) { auto res = FootpathGetPlacementFromInfo(info); auto mapZ = res.baseZ + _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(); } FootpathPlacementResult FootpathGetPlacementFromInfo(const InteractionInfo& info) { if (info.interactionType == ViewportInteractionItem::none || info.Element == nullptr) { gMapSelectFlags.unset(MapSelectFlag::enable); FootpathUpdateProvisional(); return {}; } switch (info.interactionType) { case ViewportInteractionItem::terrain: { auto surfaceElement = info.Element->AsSurface(); if (surfaceElement != nullptr) { return FootpathGetOnTerrainPlacement(*surfaceElement); } break; } case ViewportInteractionItem::footpath: { auto pathElement = info.Element->AsPath(); if (pathElement != nullptr) { auto slopeDirection = pathElement->GetSlopeDirection(); FootpathSlope slope = { FootpathSlopeType::flat, slopeDirection }; if (pathElement->IsSloped()) { slope.type = FootpathSlopeType::sloped; } return { pathElement->GetBaseZ(), slope }; } break; } default: break; } return {}; } /** * * rct2: 0x006A81FB */ void WindowFootpathSetProvisionalPathAtPoint(const ScreenCoordsXY& screenCoords) { MapInvalidateSelectionRect(); gMapSelectFlags.unset(MapSelectFlag::enableArrow); // Get current map pos and handle key modifier state auto mapPos = FootpathGetPlacePositionFromScreenPosition(screenCoords); if (!mapPos) return; // Check for change auto provisionalPos = CoordsXYZ(*mapPos, _footpathPlaceZ); if ((_provisionalFootpath.flags.has(ProvisionalPathFlag::placed)) && _provisionalFootpath.positionA == provisionalPos) { return; } // Set map selection gMapSelectFlags.set(MapSelectFlag::enable); gMapSelectType = MapSelectType::full; setMapSelectRange(*mapPos); FootpathUpdateProvisional(); // Figure out what slope and height to use auto placement = WindowFootpathGetPlacementFromScreenCoords(screenCoords); if (!placement.isValid()) { gMapSelectFlags.unset(MapSelectFlag::enable); FootpathUpdateProvisional(); return; } // Set provisional path auto pathType = gFootpathSelection.GetSelectedSurface(); auto constructFlags = FootpathCreateConstructFlags(pathType); auto footpathLoc = CoordsXYZ(*mapPos, placement.baseZ); auto tiles = std::array({ footpathLoc, placement.slope }); const auto footpathCost = FootpathProvisionalSet( pathType, gFootpathSelection.Railings, *mapPos, *mapPos, placement.baseZ, tiles, constructFlags); if (_windowFootpathCost != footpathCost) { _windowFootpathCost = footpathCost; invalidateWidget(WIDX_CONSTRUCT); } } static std::vector buildTileVector(MapRange range, int32_t baseZ) { std::vector tiles{}; for (auto y = range.GetY1(); y <= range.GetY2(); y += kCoordsXYStep) { for (auto x = range.GetX1(); x <= range.GetX2(); x += kCoordsXYStep) { FootpathPlacementResult placement = { baseZ, {} }; if (baseZ == 0) placement = FootpathGetOnTerrainPlacement(TileCoordsXY(CoordsXY(x, y))); auto calculatedLocation = CoordsXYZ(x, y, placement.baseZ); tiles.push_back({ calculatedLocation, placement.slope }); } } return tiles; } void WindowFootpathSetProvisionalPathDragArea(MapRange range, int32_t baseZ) { MapInvalidateSelectionRect(); gMapSelectFlags.unset(MapSelectFlag::enableArrow); // Check for change if ((_provisionalFootpath.flags.has(ProvisionalPathFlag::placed)) && range.Point1 == _provisionalFootpath.positionA && range.Point2 == _provisionalFootpath.positionB && baseZ == _provisionalFootpath.startZ) { return; } // Set map selection gMapSelectFlags.set(MapSelectFlag::enable); gMapSelectType = MapSelectType::full; FootpathUpdateProvisional(); // Set provisional path auto tiles = buildTileVector(range, baseZ); auto pathType = gFootpathSelection.GetSelectedSurface(); auto constructFlags = FootpathCreateConstructFlags(pathType); const auto footpathCost = FootpathProvisionalSet( pathType, gFootpathSelection.Railings, range.Point1, range.Point2, baseZ, tiles, constructFlags); if (_windowFootpathCost != footpathCost) { _windowFootpathCost = footpathCost; invalidateWidget(WIDX_CONSTRUCT); } } /** * * rct2: 0x006A8388 */ void WindowFootpathSetSelectionStartBridgeAtPoint(const ScreenCoordsXY& screenCoords) { int32_t direction; TileElement* tileElement; MapInvalidateSelectionRect(); gMapSelectFlags.unset(MapSelectFlag::enable, MapSelectFlag::enableArrow); auto mapCoords = FootpathBridgeGetInfoFromPos(screenCoords, &direction, &tileElement); if (mapCoords.IsNull()) { return; } gMapSelectFlags.set(MapSelectFlag::enable, MapSelectFlag::enableArrow); gMapSelectType = MapSelectType::full; setMapSelectRange(mapCoords); int32_t z = tileElement->GetBaseZ(); if (tileElement->GetType() == TileElementType::Surface) { uint8_t slope = tileElement->AsSurface()->GetSlope(); if (slope & kTileSlopeRaisedCornersMask) { z += kPathHeightStep; } // Add 2 for a slope if (slope & kTileSlopeDiagonalFlag) z += kPathHeightStep; // Add another 2 for a steep slope } gMapSelectArrowPosition = CoordsXYZ{ mapCoords, z }; gMapSelectArrowDirection = direction; MapInvalidateSelectionRect(); } FootpathPlacementResult WindowFootpathGetPlacementFromScreenCoords(const ScreenCoordsXY& screenCoords) { if (_footpathPlaceZ != 0) return { _footpathPlaceZ, { FootpathSlopeType::flat } }; const auto info = GetMapCoordinatesFromPos( screenCoords, EnumsToFlags(ViewportInteractionItem::terrain, ViewportInteractionItem::footpath)); return FootpathGetPlacementFromInfo(info); } void WindowFootpathPlaceDragAreaSetStart(const ScreenCoordsXY& screenCoords) { if (_footpathErrorOccured) { return; } auto mapPos = FootpathGetPlacePositionFromScreenPosition(screenCoords); if (!mapPos) return; auto placement = WindowFootpathGetPlacementFromScreenCoords(screenCoords); if (placement.baseZ > 0 && placement.slope.type == FootpathSlopeType::flat) _footpathPlaceZ = placement.baseZ; gMapSelectFlags.set(MapSelectFlag::enable); gMapSelectType = MapSelectType::full; auto correctedPosition = mapPos->ToTileStart(); setMapSelectRange(correctedPosition); _dragStartPos = correctedPosition; WindowFootpathSetProvisionalPathDragArea(getMapSelectRange(), placement.baseZ); } void WindowFootpathPlaceDragAreaSetEnd(const ScreenCoordsXY& screenCoords) { if (_footpathErrorOccured) { return; } std::optional mapPos; if (_footpathPlaceShiftState) mapPos = ScreenGetMapXYWithZ(screenCoords, _footpathPlaceZ); else mapPos = FootpathGetPlacePositionFromScreenPosition(screenCoords); if (!mapPos) { return; } auto correctedPos = mapPos->ToTileStart(); // For queues, only allow selecting a single line. if (gFootpathSelection.IsQueueSelected) { auto xDiff = correctedPos.x - _dragStartPos.x; auto yDiff = correctedPos.y - _dragStartPos.y; if (std::abs(xDiff) > std::abs(yDiff)) { correctedPos.y = _dragStartPos.y; } else { correctedPos.x = _dragStartPos.x; } } setMapSelectRange({ _dragStartPos, correctedPos }); WindowFootpathSetProvisionalPathDragArea(getMapSelectRange(), _footpathPlaceZ); } void WindowFootpathPlaceDragAreaHover(const ScreenCoordsXY& screenCoords) { bool isLeftMousePressed = gInputFlags.has(InputFlag::leftMousePressed); if (isLeftMousePressed) return; MapInvalidateSelectionRect(); gMapSelectFlags.unset(MapSelectFlag::enableArrow); auto mapPos = FootpathGetPlacePositionFromScreenPosition(screenCoords); if (!mapPos) { gMapSelectFlags.unset(MapSelectFlag::enable); return; } auto placement = WindowFootpathGetPlacementFromScreenCoords(screenCoords); // Set map selection gMapSelectFlags.set(MapSelectFlag::enable); gMapSelectType = MapSelectType::full; setMapSelectRange(*mapPos); WindowFootpathSetProvisionalPathDragArea(getMapSelectRange(), placement.baseZ); } void WindowFootpathPlacePath() { if (_footpathErrorOccured) { return; } FootpathUpdateProvisional(); if (_provisionalFootpath.tiles.size() == 0) return; // Try and place path auto selectedType = gFootpathSelection.GetSelectedSurface(); PathConstructFlags constructFlags = FootpathCreateConstructFlags(selectedType); bool anySuccess = false; money64 cost = 0; CoordsXYZ lastLocation; for (const auto& tile : _provisionalFootpath.tiles) { auto footpathPlaceAction = GameActions::FootpathPlaceAction( tile.position, tile.slope, selectedType, gFootpathSelection.Railings, kInvalidDirection, constructFlags); auto result = GameActions::Execute(&footpathPlaceAction, getGameState()); if (result.Error == GameActions::Status::Ok) { anySuccess = true; cost += result.Cost; } lastLocation = tile.position; } if (anySuccess) { // Don't play sound if it is no cost to prevent multiple sounds. TODO: make this work in no // money scenarios if (cost != 0) { Audio::Play3D(Audio::SoundId::placeItem, lastLocation); } } else { _footpathErrorOccured = true; } } /** * * rct2: 0x006A82C5 */ void WindowFootpathPlacePathAtPoint(const ScreenCoordsXY& screenCoords) { if (_footpathErrorOccured) { return; } FootpathUpdateProvisional(); auto mapPos = FootpathGetPlacePositionFromScreenPosition(screenCoords); if (!mapPos) return; auto placement = WindowFootpathGetPlacementFromScreenCoords(screenCoords); // Try and place path auto selectedType = gFootpathSelection.GetSelectedSurface(); PathConstructFlags constructFlags = FootpathCreateConstructFlags(selectedType); auto footpathPlaceAction = GameActions::FootpathPlaceAction( { *mapPos, placement.baseZ }, placement.slope, selectedType, gFootpathSelection.Railings, kInvalidDirection, constructFlags); footpathPlaceAction.SetCallback([this](const GameActions::GameAction* ga, const GameActions::Result* result) { if (result->Error == GameActions::Status::Ok) { // Don't play sound if it is no cost to prevent multiple sounds. TODO: make this work in no money scenarios if (result->Cost != 0) { OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::placeItem, result->Position); } } else { _footpathErrorOccured = true; } }); GameActions::Execute(&footpathPlaceAction, getGameState()); } /** * * rct2: 0x006A840F */ void WindowFootpathStartBridgeAtPoint(const ScreenCoordsXY& screenCoords) { int32_t z, direction; TileElement* tileElement; auto mapCoords = FootpathBridgeGetInfoFromPos(screenCoords, &direction, &tileElement); if (mapCoords.IsNull()) { return; } if (tileElement->GetType() == TileElementType::Surface) { // If we start the path on a slope, the arrow is slightly raised, so we // expect the path to be slightly raised as well. uint8_t slope = tileElement->AsSurface()->GetSlope(); z = tileElement->GetBaseZ(); if (slope & kTileSlopeDiagonalFlag) { // Steep diagonal slope z += 2 * kPathHeightStep; } else if (slope & kTileSlopeRaisedCornersMask) { // Normal slope z += kPathHeightStep; } } else { z = tileElement->GetBaseZ(); if (tileElement->GetType() == TileElementType::Path) { if (tileElement->AsPath()->IsSloped()) { if (direction == (tileElement->AsPath()->GetSlopeDirection())) { z += kPathHeightStep; } } } } ToolCancel(); _footpathConstructFromPosition = { mapCoords, z }; _footpathConstructDirection = direction; _provisionalFootpath.flags.clearAll(); _footpathConstructSlope = SlopePitch::flat; _footpathConstructionMode = PathConstructionMode::bridgeOrTunnel; _footpathConstructValidDirections = kInvalidDirection; WindowFootpathSetEnabledAndPressedWidgets(); } /** * Construct a piece of footpath while in bridge building mode. * rct2: 0x006A79B7 */ void WindowFootpathConstruct() { _windowFootpathCost = kMoney64Undefined; FootpathUpdateProvisional(); ObjectEntryIndex type; FootpathSlope slope; CoordsXYZ footpathLoc; FootpathGetNextPathInfo(&type, footpathLoc, &slope); PathConstructFlags constructFlags = FootpathCreateConstructFlags(type); auto footpathPlaceAction = GameActions::FootpathPlaceAction( footpathLoc, slope, type, gFootpathSelection.Railings, _footpathConstructDirection, constructFlags); footpathPlaceAction.SetCallback( [footpathLoc](const GameActions::GameAction* ga, const GameActions::Result* result) { if (result->Error == GameActions::Status::Ok) { Audio::Play3D(OpenRCT2::Audio::SoundId::placeItem, result->Position); } auto* windowMgr = GetWindowManager(); auto* self = static_cast(windowMgr->FindByClass(WindowClass::footpath)); if (self == nullptr) { return; } if (result->Error == GameActions::Status::Ok) { if (_footpathConstructSlope == SlopePitch::flat) { self->_footpathConstructValidDirections = kInvalidDirection; } else { self->_footpathConstructValidDirections = self->_footpathConstructDirection; } if (gFootpathGroundFlags & ELEMENT_IS_UNDERGROUND) { ViewportSetVisibility(ViewportVisibility::undergroundViewOn); } _footpathConstructFromPosition = footpathLoc; // If we have just built an upwards slope, the next path to construct is // a bit higher. Note that the z returned by footpath_get_next_path_info // already is lowered if we are building a downwards slope. if (_footpathConstructSlope == SlopePitch::upwards) { _footpathConstructFromPosition.z += kPathHeightStep; } } self->WindowFootpathSetEnabledAndPressedWidgets(); }); GameActions::Execute(&footpathPlaceAction, getGameState()); } /** * * rct2: 0x006A78EF */ void FootpathRemoveTileElement(TileElement* tileElement) { auto z = tileElement->GetBaseZ(); if (tileElement->AsPath()->IsSloped()) { uint8_t slopeDirection = tileElement->AsPath()->GetSlopeDirection(); slopeDirection = DirectionReverse(slopeDirection); if (slopeDirection == _footpathConstructDirection) { z += kPathHeightStep; } } // Find a connected edge int32_t edge = DirectionReverse(_footpathConstructDirection); if (!(tileElement->AsPath()->GetEdges() & (1 << edge))) { edge = (edge + 1) & 3; if (!(tileElement->AsPath()->GetEdges() & (1 << edge))) { edge = (edge + 2) & 3; if (!(tileElement->AsPath()->GetEdges() & (1 << edge))) { edge = (edge - 1) & 3; if (!(tileElement->AsPath()->GetEdges() & (1 << edge))) { edge = DirectionReverse(edge); } } } } _footpathConstructFromPosition.z = tileElement->GetBaseZ(); auto action = GameActions::FootpathRemoveAction(_footpathConstructFromPosition); GameActions::Execute(&action, getGameState()); // Move selection edge = DirectionReverse(edge); _footpathConstructFromPosition.x -= CoordsDirectionDelta[edge].x; _footpathConstructFromPosition.y -= CoordsDirectionDelta[edge].y; _footpathConstructFromPosition.z = z; _footpathConstructDirection = edge; _footpathConstructValidDirections = kInvalidDirection; } /** * * rct2: 0x006A7873 */ TileElement* FootpathGetTileElementToRemove() { TileElement* tileElement; int32_t z, zLow; if (!MapIsLocationValid(_footpathConstructFromPosition)) { return nullptr; } z = std::min(255 * kCoordsZStep, _footpathConstructFromPosition.z); zLow = z - kPathHeightStep; tileElement = MapGetFirstElementAt(_footpathConstructFromPosition); do { if (tileElement == nullptr) break; if (tileElement->GetType() == TileElementType::Path) { if (tileElement->GetBaseZ() == z) { if (tileElement->AsPath()->IsSloped()) { if (DirectionReverse(tileElement->AsPath()->GetSlopeDirection()) != _footpathConstructDirection) { continue; } } return tileElement; } if (tileElement->GetBaseZ() == zLow) { if (!tileElement->AsPath()->IsSloped()) { if ((tileElement->AsPath()->GetSlopeDirection()) == _footpathConstructDirection) { continue; } } return tileElement; } } } while (!(tileElement++)->IsLastForTile()); return nullptr; } /** * * rct2: 0x006A7863 */ void WindowFootpathRemove() { TileElement* tileElement; _windowFootpathCost = kMoney64Undefined; FootpathUpdateProvisional(); tileElement = FootpathGetTileElementToRemove(); if (tileElement != nullptr) { FootpathRemoveTileElement(tileElement); } WindowFootpathSetEnabledAndPressedWidgets(); } /** * * rct2: 0x006A855C */ void WindowFootpathSetEnabledAndPressedWidgets() { if (_footpathConstructionMode == PathConstructionMode::bridgeOrTunnel) { MapInvalidateMapSelectionTiles(); gMapSelectFlags.set(MapSelectFlag::enableConstruct, MapSelectFlag::green); int32_t direction = _footpathConstructDirection; gMapSelectionTiles.clear(); gMapSelectionTiles.push_back( { _footpathConstructFromPosition.x + CoordsDirectionDelta[direction].x, _footpathConstructFromPosition.y + CoordsDirectionDelta[direction].y }); MapInvalidateMapSelectionTiles(); } uint64_t newPressedWidgets = pressedWidgets & ~((1LL << WIDX_DIRECTION_NW) | (1LL << WIDX_DIRECTION_NE) | (1LL << WIDX_DIRECTION_SW) | (1LL << WIDX_DIRECTION_SE) | (1LL << WIDX_SLOPEDOWN) | (1LL << WIDX_LEVEL) | (1LL << WIDX_SLOPEUP)); uint64_t newDisabledWidgets = 0; int32_t currentRotation = GetCurrentRotation(); if (_footpathConstructionMode == PathConstructionMode::bridgeOrTunnel) { // Set pressed directional widget int32_t direction = (_footpathConstructDirection + currentRotation) & 3; newPressedWidgets |= (1LL << (WIDX_DIRECTION_NW + direction)); // Set pressed slope widget switch (_footpathConstructSlope) { case SlopePitch::flat: newPressedWidgets |= (1uLL << WIDX_LEVEL); break; case SlopePitch::upwards: newPressedWidgets |= (1uLL << WIDX_SLOPEUP); break; case SlopePitch::downwards: newPressedWidgets |= (1uLL << WIDX_SLOPEDOWN); break; } // Enable / disable directional widgets direction = _footpathConstructValidDirections; if (direction != kInvalidDirection) { newDisabledWidgets |= (1uLL << WIDX_DIRECTION_NW) | (1uLL << WIDX_DIRECTION_NE) | (1uLL << WIDX_DIRECTION_SW) | (1uLL << WIDX_DIRECTION_SE); direction = (direction + currentRotation) & 3; newDisabledWidgets &= ~(1 << (WIDX_DIRECTION_NW + direction)); } } else { // Disable all bridge mode widgets newDisabledWidgets |= (1uLL << WIDX_DIRECTION_GROUP) | (1uLL << WIDX_DIRECTION_NW) | (1uLL << WIDX_DIRECTION_NE) | (1uLL << WIDX_DIRECTION_SW) | (1uLL << WIDX_DIRECTION_SE) | (1uLL << WIDX_SLOPE_GROUP) | (1uLL << WIDX_SLOPEDOWN) | (1uLL << WIDX_LEVEL) | (1uLL << WIDX_SLOPEUP) | (1uLL << WIDX_CONSTRUCT) | (1uLL << WIDX_REMOVE); } pressedWidgets = newPressedWidgets; disabledWidgets = newDisabledWidgets; invalidate(); } /** * * rct2: 0x006A7B20 */ void FootpathGetNextPathInfo(ObjectEntryIndex* type, CoordsXYZ& footpathLoc, FootpathSlope* slope) { auto direction = _footpathConstructDirection; footpathLoc.x = _footpathConstructFromPosition.x + CoordsDirectionDelta[direction].x; footpathLoc.y = _footpathConstructFromPosition.y + CoordsDirectionDelta[direction].y; footpathLoc.z = _footpathConstructFromPosition.z; if (type != nullptr) { *type = gFootpathSelection.GetSelectedSurface(); } *slope = {}; if (_footpathConstructSlope != SlopePitch::flat) { *slope = { FootpathSlopeType::sloped, _footpathConstructDirection }; if (_footpathConstructSlope == SlopePitch::downwards) { footpathLoc.z -= kPathHeightStep; slope->direction = DirectionReverse(slope->direction); } } } PathConstructFlags FootpathCreateConstructFlags(ObjectEntryIndex& type) { PathConstructFlags pathConstructFlags = 0; if (gFootpathSelection.IsQueueSelected) pathConstructFlags |= PathConstructFlag::IsQueue; if (gFootpathSelection.LegacyPath != kObjectEntryIndexNull) { pathConstructFlags |= PathConstructFlag::IsLegacyPathObject; type = gFootpathSelection.LegacyPath; } return pathConstructFlags; } #pragma region Keyboard Shortcuts Events public: void KeyboardShortcutTurnLeft() { if (isWidgetDisabled(WIDX_DIRECTION_NW) || isWidgetDisabled(WIDX_DIRECTION_NE) || isWidgetDisabled(WIDX_DIRECTION_SW) || isWidgetDisabled(WIDX_DIRECTION_SE) || _footpathConstructionMode != PathConstructionMode::bridgeOrTunnel) { return; } int32_t currentRotation = GetCurrentRotation(); int32_t turnedRotation = _footpathConstructDirection - currentRotation + (currentRotation % 2 == 1 ? 1 : -1); WindowFootpathMousedownDirection(turnedRotation); } void KeyboardShortcutTurnRight() { if (isWidgetDisabled(WIDX_DIRECTION_NW) || isWidgetDisabled(WIDX_DIRECTION_NE) || isWidgetDisabled(WIDX_DIRECTION_SW) || isWidgetDisabled(WIDX_DIRECTION_SE) || _footpathConstructionMode != PathConstructionMode::bridgeOrTunnel) { return; } int32_t currentRotation = GetCurrentRotation(); int32_t turnedRotation = _footpathConstructDirection - currentRotation + (currentRotation % 2 == 1 ? -1 : 1); WindowFootpathMousedownDirection(turnedRotation); } void KeyboardShortcutShortcutSlopeDown() { if (isWidgetDisabled(WIDX_SLOPEDOWN) || isWidgetDisabled(WIDX_LEVEL) || isWidgetDisabled(WIDX_SLOPEUP) || widgets[WIDX_LEVEL].type == WidgetType::empty) { return; } switch (_footpathConstructSlope) { case SlopePitch::flat: onMouseDown(WIDX_SLOPEDOWN); break; case SlopePitch::upwards: onMouseDown(WIDX_LEVEL); break; default: case SlopePitch::downwards: return; } } void KeyboardShortcutSlopeUp() { if (isWidgetDisabled(WIDX_SLOPEDOWN) || isWidgetDisabled(WIDX_LEVEL) || isWidgetDisabled(WIDX_SLOPEUP) || widgets[WIDX_LEVEL].type == WidgetType::empty) { return; } switch (_footpathConstructSlope) { case SlopePitch::downwards: onMouseDown(WIDX_LEVEL); break; case SlopePitch::flat: onMouseDown(WIDX_SLOPEUP); break; default: case SlopePitch::upwards: return; } } void KeyboardShortcutSlopeLevel() { if (isWidgetDisabled(WIDX_SLOPEDOWN) || isWidgetDisabled(WIDX_LEVEL) || isWidgetDisabled(WIDX_SLOPEUP) || widgets[WIDX_LEVEL].type == WidgetType::empty || _footpathConstructSlope == SlopePitch::flat) { return; } onMouseDown(WIDX_LEVEL); } void KeyboardShortcutDemolishCurrent() { if (isWidgetDisabled(WIDX_REMOVE) || widgets[WIDX_REMOVE].type == WidgetType::empty || (!getGameState().cheats.buildInPauseMode && GameIsPaused())) { return; } WindowFootpathRemove(); } void KeyboardShortcutBuildCurrent() { if (isWidgetDisabled(WIDX_CONSTRUCT) || widgets[WIDX_CONSTRUCT].type == WidgetType::empty) { return; } onMouseDown(WIDX_CONSTRUCT); } #pragma endregion }; /** * * rct2: 0x006A7C43 */ WindowBase* FootpathOpen() { if (!WindowFootpathSelectDefault()) { // No path objects to select from, don't open window return nullptr; } auto* windowMgr = GetWindowManager(); return windowMgr->FocusOrCreate(WindowClass::footpath, kWindowSize, {}); } void WindowFootpathResetSelectedPath() { gFootpathSelection = {}; } void WindowFootpathKeyboardShortcutTurnLeft() { auto* windowMgr = GetWindowManager(); WindowBase* w = windowMgr->FindByClass(WindowClass::footpath); if (w != nullptr) { auto* footpathWindow = static_cast(w); if (footpathWindow != nullptr) { footpathWindow->KeyboardShortcutTurnLeft(); } } } void WindowFootpathKeyboardShortcutTurnRight() { auto* windowMgr = GetWindowManager(); WindowBase* w = windowMgr->FindByClass(WindowClass::footpath); if (w != nullptr) { auto* footpathWindow = static_cast(w); if (footpathWindow != nullptr) { footpathWindow->KeyboardShortcutTurnRight(); } } } void WindowFootpathKeyboardShortcutSlopeDown() { auto* windowMgr = GetWindowManager(); WindowBase* w = windowMgr->FindByClass(WindowClass::footpath); if (w != nullptr) { auto* footpathWindow = static_cast(w); if (footpathWindow != nullptr) { footpathWindow->KeyboardShortcutShortcutSlopeDown(); } } } void WindowFootpathKeyboardShortcutSlopeUp() { auto* windowMgr = GetWindowManager(); WindowBase* w = windowMgr->FindByClass(WindowClass::footpath); if (w != nullptr) { auto* footpathWindow = static_cast(w); if (footpathWindow != nullptr) { footpathWindow->KeyboardShortcutSlopeUp(); } } } void WindowFootpathKeyboardShortcutDemolishCurrent() { auto* windowMgr = GetWindowManager(); WindowBase* w = windowMgr->FindByClass(WindowClass::footpath); if (w != nullptr) { auto* footpathWindow = static_cast(w); if (footpathWindow != nullptr) { footpathWindow->KeyboardShortcutDemolishCurrent(); } } } void WindowFootpathKeyboardShortcutBuildCurrent() { auto* windowMgr = GetWindowManager(); WindowBase* w = windowMgr->FindByClass(WindowClass::footpath); if (w != nullptr) { auto* footpathWindow = static_cast(w); if (footpathWindow != nullptr) { footpathWindow->KeyboardShortcutBuildCurrent(); } } } /** * * rct2: 0x0066CCE7 */ void ToggleFootpathWindow() { auto* windowMgr = GetWindowManager(); if (windowMgr->FindByClass(WindowClass::footpath) == nullptr) { ContextOpenWindow(WindowClass::footpath); } else { ToolCancel(); windowMgr->CloseByClass(WindowClass::footpath); } } /** * * rct2: 0x006A76FF */ static money64 FootpathProvisionalSet( ObjectEntryIndex type, ObjectEntryIndex railingsType, const CoordsXY& footpathLocA, const CoordsXY& footpathLocB, int32_t startZ, const std::span tiles, PathConstructFlags constructFlags) { FootpathRemoveProvisional(); GameActions::Result res; std::vector succesfulTiles{}; money64 cost = 0; for (const auto& tile : tiles) { auto footpathPlaceAction = GameActions::FootpathPlaceAction( tile.position, tile.slope, type, railingsType, kInvalidDirection, constructFlags); footpathPlaceAction.SetFlags(GAME_COMMAND_FLAG_GHOST | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED); res = GameActions::Execute(&footpathPlaceAction, getGameState()); // The latter status will be returned if there is already path at the tile in question, to prevent a ghost // from glitching through the existing path. if (res.Error == GameActions::Status::Ok || res.Error == GameActions::Status::ItemAlreadyPlaced) { succesfulTiles.emplace_back(tile); cost += res.Cost; } } bool anySuccesful = succesfulTiles.size() > 0; if (anySuccesful) { _provisionalFootpath.surfaceIndex = type; _provisionalFootpath.railingsIndex = railingsType; _provisionalFootpath.positionA = footpathLocA; _provisionalFootpath.positionB = footpathLocB; _provisionalFootpath.startZ = startZ; _provisionalFootpath.tiles = succesfulTiles; _provisionalFootpath.constructFlags = constructFlags; _provisionalFootpath.flags.set(ProvisionalPathFlag::placed); if (gFootpathGroundFlags & ELEMENT_IS_UNDERGROUND) { ViewportSetVisibility(ViewportVisibility::undergroundViewOn); } else { ViewportSetVisibility(ViewportVisibility::undergroundViewOff); } } if (!isToolActive(WindowClass::scenery)) { if (succesfulTiles.size() == 0) { // If we can't build this, don't show a virtual floor. VirtualFloorSetHeight(0); } else if ( succesfulTiles[0].slope.type == FootpathSlopeType::flat || succesfulTiles[0].position.z < _footpathConstructFromPosition.z) { // Going either straight on, or down. VirtualFloorSetHeight(succesfulTiles[0].position.z); } else { // Going up in the world! VirtualFloorSetHeight(succesfulTiles[0].position.z + kLandHeightStep); } } return anySuccesful ? cost : kMoney64Undefined; } /** * * rct2: 0x006A77FF */ void FootpathRemoveProvisional() { if (!_provisionalFootpath.flags.has(ProvisionalPathFlag::placed)) return; _provisionalFootpath.flags.unset(ProvisionalPathFlag::placed); for (const auto& tile : _provisionalFootpath.tiles) { auto action = GameActions::FootpathRemoveAction(tile.position); action.SetFlags(GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND | GAME_COMMAND_FLAG_GHOST); GameActions::Execute(&action, getGameState()); } } /** * * rct2: 0x006A7831 */ void FootpathUpdateProvisional() { if (_provisionalFootpath.flags.has(ProvisionalPathFlag::showArrow)) { _provisionalFootpath.flags.unset(ProvisionalPathFlag::showArrow); gMapSelectFlags.unset(MapSelectFlag::enableArrow); MapInvalidateTileFull(_footpathConstructFromPosition); } FootpathRemoveProvisional(); } void FootpathRemoveProvisionalTemporarily() { if (_provisionalFootpath.flags.has(ProvisionalPathFlag::placed)) { FootpathRemoveProvisional(); _provisionalFootpath.flags.set(ProvisionalPathFlag::placed); } } void FootpathRestoreProvisional() { if (_provisionalFootpath.flags.has(ProvisionalPathFlag::placed)) { _provisionalFootpath.flags.unset(ProvisionalPathFlag::placed); FootpathProvisionalSet( _provisionalFootpath.surfaceIndex, _provisionalFootpath.railingsIndex, _provisionalFootpath.positionA, _provisionalFootpath.positionB, _provisionalFootpath.startZ, _provisionalFootpath.tiles, _provisionalFootpath.constructFlags); } } void FootpathRecheckProvisional() { _provisionalFootpath.flags.set(ProvisionalPathFlag::forceRecheck); } static ObjectEntryIndex FootpathGetDefaultSurface(bool queue) { bool showEditorPaths = (gLegacyScene == LegacyScene::scenarioEditor || getGameState().cheats.sandboxMode); for (ObjectEntryIndex i = 0; i < kMaxFootpathSurfaceObjects; i++) { auto pathEntry = GetPathSurfaceEntry(i); if (pathEntry != nullptr) { if (!showEditorPaths && (pathEntry->Flags & FOOTPATH_ENTRY_FLAG_SHOW_ONLY_IN_SCENARIO_EDITOR)) { continue; } if (queue == ((pathEntry->Flags & FOOTPATH_ENTRY_FLAG_IS_QUEUE) != 0)) { return i; } } } return kObjectEntryIndexNull; } static bool FootpathIsSurfaceEntryOkay(ObjectEntryIndex index, bool queue) { auto pathEntry = GetPathSurfaceEntry(index); if (pathEntry != nullptr) { bool showEditorPaths = (gLegacyScene == LegacyScene::scenarioEditor || getGameState().cheats.sandboxMode); if (!showEditorPaths && (pathEntry->Flags & FOOTPATH_ENTRY_FLAG_SHOW_ONLY_IN_SCENARIO_EDITOR)) { return false; } if (queue == ((pathEntry->Flags & FOOTPATH_ENTRY_FLAG_IS_QUEUE) != 0)) { return true; } } return false; } static ObjectEntryIndex FootpathGetDefaultRailings() { for (ObjectEntryIndex i = 0; i < kMaxFootpathRailingsObjects; i++) { const auto* railingEntry = GetPathRailingsEntry(i); if (railingEntry != nullptr) { return i; } } return kObjectEntryIndexNull; } static bool FootpathIsLegacyPathEntryOkay(ObjectEntryIndex index) { bool showEditorPaths = (gLegacyScene == LegacyScene::scenarioEditor || getGameState().cheats.sandboxMode); auto& objManager = OpenRCT2::GetContext()->GetObjectManager(); auto footpathObj = objManager.GetLoadedObject(index); if (footpathObj != nullptr) { auto pathEntry = reinterpret_cast(footpathObj->GetLegacyData()); return showEditorPaths || !(pathEntry->flags & FOOTPATH_ENTRY_FLAG_SHOW_ONLY_IN_SCENARIO_EDITOR); } return false; } static ObjectEntryIndex FootpathGetDefaultLegacyPath() { for (ObjectEntryIndex i = 0; i < kMaxPathObjects; i++) { if (FootpathIsLegacyPathEntryOkay(i)) { return i; } } return kObjectEntryIndexNull; } bool WindowFootpathSelectDefault() { // Select default footpath auto surfaceIndex = FootpathGetDefaultSurface(false); if (FootpathIsSurfaceEntryOkay(gFootpathSelection.NormalSurface, false)) { surfaceIndex = gFootpathSelection.NormalSurface; } // Select default queue auto queueIndex = FootpathGetDefaultSurface(true); if (FootpathIsSurfaceEntryOkay(gFootpathSelection.QueueSurface, true)) { queueIndex = gFootpathSelection.QueueSurface; } // Select default railing auto railingIndex = FootpathGetDefaultRailings(); const auto* railingEntry = GetPathRailingsEntry(gFootpathSelection.Railings); if (railingEntry != nullptr) { railingIndex = gFootpathSelection.Railings; } // Select default legacy path auto legacyPathIndex = FootpathGetDefaultLegacyPath(); if (gFootpathSelection.LegacyPath != kObjectEntryIndexNull) { if (FootpathIsLegacyPathEntryOkay(gFootpathSelection.LegacyPath)) { // Keep legacy path selected legacyPathIndex = gFootpathSelection.LegacyPath; } else { // Reset legacy path, we default to a surface (if there are any) gFootpathSelection.LegacyPath = kObjectEntryIndexNull; } } if (surfaceIndex == kObjectEntryIndexNull) { if (legacyPathIndex == kObjectEntryIndexNull) { // No surfaces or legacy paths available return false; } // No surfaces available, so default to legacy path gFootpathSelection.LegacyPath = legacyPathIndex; } gFootpathSelection.NormalSurface = surfaceIndex; gFootpathSelection.QueueSurface = queueIndex; gFootpathSelection.Railings = railingIndex; return true; } } // namespace OpenRCT2::Ui::Windows