/***************************************************************************** * 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 "Viewport.h" #include "../Context.h" #include "../Diagnostic.h" #include "../Game.h" #include "../GameState.h" #include "../Input.h" #include "../OpenRCT2.h" #include "../config/Config.h" #include "../core/Guard.hpp" #include "../core/JobPool.h" #include "../core/Numerics.hpp" #include "../drawing/Drawing.h" #include "../drawing/IDrawingEngine.h" #include "../entity/EntityList.h" #include "../entity/Guest.h" #include "../entity/PatrolArea.h" #include "../entity/Staff.h" #include "../interface/Cursors.h" #include "../object/LargeSceneryEntry.h" #include "../object/SmallSceneryEntry.h" #include "../object/WallSceneryEntry.h" #include "../paint/Paint.h" #include "../profiling/Profiling.h" #include "../ride/Ride.h" #include "../ride/RideData.h" #include "../ride/TrackDesign.h" #include "../ride/Vehicle.h" #include "../ui/WindowManager.h" #include "../world/Climate.h" #include "../world/Map.h" #include "../world/tile_element/LargeSceneryElement.h" #include "../world/tile_element/SmallSceneryElement.h" #include "../world/tile_element/TileElement.h" #include "../world/tile_element/WallElement.h" #include "Colour.h" #include "Window.h" #include "WindowBase.h" #include #include #include namespace OpenRCT2 { using namespace OpenRCT2::Numerics; enum : uint8_t { IMAGE_TYPE_DEFAULT = 0, IMAGE_TYPE_REMAP = (1 << 1), IMAGE_TYPE_TRANSPARENT = (1 << 2), }; uint8_t gShowGridLinesRefCount; uint8_t gShowLandRightsRefCount; uint8_t gShowConstructionRightsRefCount; static std::list _viewports; Viewport* g_music_tracking_viewport; static std::unique_ptr _paintJobs; static std::vector _paintColumns; InteractionInfo::InteractionInfo(const PaintStruct* ps) : Loc(ps->MapPos) , Element(ps->Element) , Entity(ps->Entity) , interactionType(ps->InteractionItem) { } static void ViewportPaintWeatherGloom(DrawPixelInfo& dpi); static void ViewportPaint(const Viewport* viewport, DrawPixelInfo& dpi); static void ViewportUpdateFollowSprite(WindowBase* window); static void ViewportUpdateSmartFollowEntity(WindowBase* window); static void ViewportUpdateSmartFollowStaff(WindowBase* window, const Staff& peep); static void ViewportUpdateSmartFollowVehicle(WindowBase* window); static void ViewportInvalidate(const Viewport* viewport, const ScreenRect& screenRect); /** * This is not a viewport function. It is used to setup many variables for * multiple things. * rct2: 0x006E6EAC */ void ViewportInitAll() { if (!gOpenRCT2NoGraphics) { ColoursInitMaps(); } WindowInitAll(); // ? gInputFlags.clearAll(); InputSetState(InputState::Reset); gPressedWidget.window_classification = WindowClass::Null; gPickupPeepImage = ImageId(); ResetTooltipNotShown(); gMapSelectFlags = 0; ClearPatrolAreaToRender(); TextinputCancel(); } /** * Converts between 3d point of a sprite to 2d coordinates for centring on that * sprite * rct2: 0x006EB0C1 * x : ax * y : bx * z : cx * out_x : ax * out_y : bx */ std::optional centre_2d_coordinates(const CoordsXYZ& loc, Viewport* viewport) { // If the start location was invalid // propagate the invalid location to the output. // This fixes a bug that caused the game to enter an infinite loop. if (loc.IsNull()) { return std::nullopt; } auto screenCoord = Translate3DTo2DWithZ(viewport->rotation, loc); screenCoord.x -= viewport->ViewWidth() / 2; screenCoord.y -= viewport->ViewHeight() / 2; return { screenCoord }; } CoordsXYZ Focus::GetPos() const { return std::visit( [](auto&& arg) { using T = std::decay_t; if constexpr (std::is_same_v) return arg; else if constexpr (std::is_same_v) { auto* centreEntity = GetEntity(arg); if (centreEntity != nullptr) { return CoordsXYZ{ centreEntity->x, centreEntity->y, centreEntity->z }; } else { LOG_ERROR("Invalid entity for focus."); return CoordsXYZ{}; } } }, data); } /** * Viewport will look at sprite or at coordinates as specified in flags 0b_1X * for sprite 0b_0X for coordinates * * rct2: 0x006EB009 * x: ax * y: eax (top 16) * width: bx * height: ebx (top 16) * zoom: cl (8 bits) * centre_x: edx lower 16 bits * centre_y: edx upper 16 bits * centre_z: ecx upper 16 bits * sprite: edx lower 16 bits * flags: edx top most 2 bits 0b_X1 for zoom clear see below for 2nd bit. * w: esi */ void ViewportCreate(WindowBase* w, const ScreenCoordsXY& screenCoords, int32_t width, int32_t height, const Focus& focus) { Viewport* viewport = nullptr; if (_viewports.size() >= kMaxViewportCount) { LOG_ERROR("No more viewport slots left to allocate."); return; } auto itViewport = _viewports.insert(_viewports.end(), Viewport{}); viewport = &*itViewport; viewport->pos = screenCoords; viewport->width = width; viewport->height = height; const auto zoom = focus.zoom; viewport->zoom = zoom; viewport->flags = 0; viewport->rotation = GetCurrentRotation(); if (Config::Get().general.AlwaysShowGridlines) viewport->flags |= VIEWPORT_FLAG_GRIDLINES; w->viewport = viewport; CoordsXYZ centrePos = focus.GetPos(); w->viewport_target_sprite = std::visit( [](auto&& arg) { using T = std::decay_t; if constexpr (std::is_same_v) return EntityId::GetNull(); else if constexpr (std::is_same_v) return arg; }, focus.data); auto centreLoc = centre_2d_coordinates(centrePos, viewport); if (!centreLoc.has_value()) { LOG_ERROR("Invalid location for viewport."); return; } w->savedViewPos = *centreLoc; viewport->viewPos = *centreLoc; } void ViewportRemove(Viewport* viewport) { auto it = std::find_if(_viewports.begin(), _viewports.end(), [viewport](const auto& vp) { return &vp == viewport; }); if (it == _viewports.end()) { LOG_ERROR("Unable to remove viewport: %p", viewport); return; } _viewports.erase(it); } static Viewport* ViewportGetMain() { auto mainWindow = WindowGetMain(); if (mainWindow == nullptr) { return nullptr; } return mainWindow->viewport; } void ViewportsInvalidate(int32_t x, int32_t y, int32_t z0, int32_t z1, ZoomLevel maxZoom) { for (auto& vp : _viewports) { if (maxZoom == ZoomLevel{ -1 } || vp.zoom <= ZoomLevel{ maxZoom }) { int32_t x1, y1, x2, y2; x += 16; y += 16; auto screenCoord = Translate3DTo2DWithZ(vp.rotation, CoordsXYZ{ x, y, 0 }); x1 = screenCoord.x - 32; y1 = screenCoord.y - 32 - z1; x2 = screenCoord.x + 32; y2 = screenCoord.y + 32 - z0; ViewportInvalidate(&vp, ScreenRect{ { x1, y1 }, { x2, y2 } }); } } } void ViewportsInvalidate(const CoordsXYZ& pos, int32_t width, int32_t minHeight, int32_t maxHeight, ZoomLevel maxZoom) { for (auto& vp : _viewports) { if (maxZoom == ZoomLevel{ -1 } || vp.zoom <= ZoomLevel{ maxZoom }) { auto screenCoords = Translate3DTo2DWithZ(vp.rotation, pos); auto screenPos = ScreenRect( screenCoords - ScreenCoordsXY{ width, minHeight }, screenCoords + ScreenCoordsXY{ width, maxHeight }); ViewportInvalidate(&vp, screenPos); } } } void ViewportsInvalidate(const ScreenRect& screenRect, ZoomLevel maxZoom) { for (auto& vp : _viewports) { if (maxZoom == ZoomLevel{ -1 } || vp.zoom <= ZoomLevel{ maxZoom }) { ViewportInvalidate(&vp, screenRect); } } } /** * * rct2: 0x00689174 * edx is assumed to be (and always is) the current rotation, so it is not * needed as parameter. */ CoordsXYZ ViewportAdjustForMapHeight(const ScreenCoordsXY& startCoords, uint8_t rotation) { int32_t height = 0; CoordsXY pos{}; for (int32_t i = 0; i < 6; i++) { pos = ViewportPosToMapPos(startCoords, height, rotation); height = TileElementHeight(pos); // HACK: This is to prevent the x and y values being set to values outside // of the map. This can happen when the height is larger than the map size. auto mapSizeMinus2 = GetMapSizeMinus2(); if (pos.x > mapSizeMinus2.x && pos.y > mapSizeMinus2.y) { static constexpr CoordsXY kCorr[] = { { -1, -1 }, { 1, -1 }, { 1, 1 }, { -1, 1 }, }; pos.x += kCorr[rotation].x * height; pos.y += kCorr[rotation].y * height; } } return { pos, height }; } /* * rct2: 0x006E7FF3 */ static void ViewportRedrawAfterShift( DrawPixelInfo& dpi, WindowBase* window, const WindowBase* originalWindow, const ScreenCoordsXY shift, const ScreenRect& drawRect) { // sub-divide by intersecting windows if (window != nullptr) { // skip current window and non-intersecting windows if (window == originalWindow || drawRect.GetRight() <= window->windowPos.x || drawRect.GetLeft() >= window->windowPos.x + window->width || drawRect.GetBottom() <= window->windowPos.y || drawRect.GetTop() >= window->windowPos.y + window->height) { auto itWindowPos = WindowGetIterator(window); // Get next valid window after. auto itNextWindow = [&]() { auto itNext = std::next(itWindowPos); while (itNext != g_window_list.end() && (itNext->get()->flags & WF_DEAD)) { ++itNext; } return itNext; }(); ViewportRedrawAfterShift( dpi, itNextWindow == g_window_list.end() ? nullptr : itNextWindow->get(), originalWindow, shift, drawRect); return; } if (drawRect.GetLeft() < window->windowPos.x) { ScreenRect leftRect = { drawRect.Point1, { window->windowPos.x, drawRect.GetBottom() } }; ViewportRedrawAfterShift(dpi, window, originalWindow, shift, leftRect); ScreenRect rightRect = { { window->windowPos.x, drawRect.GetTop() }, drawRect.Point2 }; ViewportRedrawAfterShift(dpi, window, originalWindow, shift, rightRect); } else if (drawRect.GetRight() > window->windowPos.x + window->width) { ScreenRect leftRect = { drawRect.Point1, { window->windowPos.x + window->width, drawRect.GetBottom() } }; ViewportRedrawAfterShift(dpi, window, originalWindow, shift, leftRect); ScreenRect rightRect = { { window->windowPos.x + window->width, drawRect.GetTop() }, drawRect.Point2 }; ViewportRedrawAfterShift(dpi, window, originalWindow, shift, rightRect); } else if (drawRect.GetTop() < window->windowPos.y) { ScreenRect topRect = { drawRect.Point1, { drawRect.GetRight(), window->windowPos.y } }; ViewportRedrawAfterShift(dpi, window, originalWindow, shift, topRect); ScreenRect bottomRect = { { drawRect.GetLeft(), window->windowPos.y }, drawRect.Point2 }; ViewportRedrawAfterShift(dpi, window, originalWindow, shift, bottomRect); } else if (drawRect.GetBottom() > window->windowPos.y + window->height) { ScreenRect topRect = { drawRect.Point1, { drawRect.GetRight(), window->windowPos.y + window->height } }; ViewportRedrawAfterShift(dpi, window, originalWindow, shift, topRect); ScreenRect bottomRect = { { drawRect.GetLeft(), window->windowPos.y + window->height }, drawRect.Point2 }; ViewportRedrawAfterShift(dpi, window, originalWindow, shift, bottomRect); } } else { auto left = drawRect.GetLeft(); auto right = drawRect.GetRight(); auto top = drawRect.GetTop(); auto bottom = drawRect.GetBottom(); // if moved more than the draw rectangle size if (abs(shift.x) < drawRect.GetWidth() && abs(shift.y) < drawRect.GetHeight()) { // update whole block ? DrawingEngineCopyRect( drawRect.GetLeft(), drawRect.GetTop(), drawRect.GetWidth(), drawRect.GetHeight(), shift.x, shift.y); if (shift.x > 0) { // draw left auto _right = left + shift.x; WindowDrawAll(dpi, left, top, _right, bottom); left += shift.x; } else if (shift.x < 0) { // draw right auto _left = right + shift.x; WindowDrawAll(dpi, _left, top, right, bottom); right += shift.x; } if (shift.y > 0) { // draw top bottom = top + shift.y; WindowDrawAll(dpi, left, top, right, bottom); } else if (shift.y < 0) { // draw bottom top = bottom + shift.y; WindowDrawAll(dpi, left, top, right, bottom); } } else { // redraw whole draw rectangle WindowDrawAll(dpi, left, top, right, bottom); } } } static void ViewportShiftPixels(DrawPixelInfo& dpi, WindowBase* window, Viewport* viewport, int32_t x_diff, int32_t y_diff) { // This loop redraws all parts covered by transparent windows. auto it = WindowGetIterator(window); for (; it != g_window_list.end(); it++) { auto w = it->get(); if (!(w->flags & WF_TRANSPARENT) || (w->flags & WF_DEAD)) continue; if (w->viewport == viewport) continue; if (viewport->pos.x + viewport->width <= w->windowPos.x) continue; if (w->windowPos.x + w->width <= viewport->pos.x) continue; if (viewport->pos.y + viewport->height <= w->windowPos.y) continue; if (w->windowPos.y + w->height <= viewport->pos.y) continue; auto left = w->windowPos.x; auto right = w->windowPos.x + w->width; auto top = w->windowPos.y; auto bottom = w->windowPos.y + w->height; if (left < viewport->pos.x) left = viewport->pos.x; if (right > viewport->pos.x + viewport->width) right = viewport->pos.x + viewport->width; if (top < viewport->pos.y) top = viewport->pos.y; if (bottom > viewport->pos.y + viewport->height) bottom = viewport->pos.y + viewport->height; if (left >= right) continue; if (top >= bottom) continue; WindowDrawAll(dpi, left, top, right, bottom); } ViewportRedrawAfterShift( dpi, window, window, { x_diff, y_diff }, { viewport->pos, { viewport->pos.x + viewport->width, viewport->pos.y + viewport->height } }); } static void ViewportMove(const ScreenCoordsXY& coords, WindowBase* w, Viewport* viewport) { auto zoom = viewport->zoom; // Note: do not do the subtraction and then divide! // Note: Due to arithmetic shift != /zoom a shift will have to be used // hopefully when 0x006E7FF3 is finished this can be converted to /zoom. auto x_diff = viewport->zoom.ApplyInversedTo(viewport->viewPos.x) - viewport->zoom.ApplyInversedTo(coords.x); auto y_diff = viewport->zoom.ApplyInversedTo(viewport->viewPos.y) - viewport->zoom.ApplyInversedTo(coords.y); viewport->viewPos = coords; // If no change in viewing area if ((!x_diff) && (!y_diff)) return; if (w->flags & WF_7) { int32_t left = std::max(viewport->pos.x, 0); int32_t top = std::max(viewport->pos.y, 0); int32_t right = std::min(viewport->pos.x + viewport->width, ContextGetWidth()); int32_t bottom = std::min(viewport->pos.y + viewport->height, ContextGetHeight()); if (left >= right) return; if (top >= bottom) return; if (DrawingEngineHasDirtyOptimisations()) { DrawPixelInfo& dpi = DrawingEngineGetDpi(); WindowDrawAll(dpi, left, top, right, bottom); return; } } Viewport view_copy = *viewport; if (viewport->pos.x < 0) { viewport->width += viewport->pos.x; viewport->viewPos.x -= zoom.ApplyTo(viewport->pos.x); viewport->pos.x = 0; } int32_t eax = viewport->pos.x + viewport->width - ContextGetWidth(); if (eax > 0) { viewport->width -= eax; } if (viewport->width <= 0) { *viewport = view_copy; return; } if (viewport->pos.y < 0) { viewport->height += viewport->pos.y; viewport->viewPos.y -= zoom.ApplyTo(viewport->pos.y); viewport->pos.y = 0; } eax = viewport->pos.y + viewport->height - ContextGetHeight(); if (eax > 0) { viewport->height -= eax; } if (viewport->height <= 0) { *viewport = view_copy; return; } if (DrawingEngineHasDirtyOptimisations()) { DrawPixelInfo& dpi = DrawingEngineGetDpi(); ViewportShiftPixels(dpi, w, viewport, x_diff, y_diff); } *viewport = view_copy; } // rct2: 0x006E7A15 static void ViewportSetUndergroundFlag(int32_t underground, WindowBase* window, Viewport* viewport) { if (window->classification != WindowClass::MainWindow || (window->classification == WindowClass::MainWindow && !window->viewport_smart_follow_sprite.IsNull())) { if (!underground) { int32_t bit = viewport->flags & VIEWPORT_FLAG_UNDERGROUND_INSIDE; viewport->flags &= ~VIEWPORT_FLAG_UNDERGROUND_INSIDE; if (!bit) return; } else { int32_t bit = viewport->flags & VIEWPORT_FLAG_UNDERGROUND_INSIDE; viewport->flags |= VIEWPORT_FLAG_UNDERGROUND_INSIDE; if (bit) return; } window->Invalidate(); } } /** * * rct2: 0x006E7A3A */ void ViewportUpdatePosition(WindowBase* window) { window->OnResize(); Viewport* viewport = window->viewport; if (viewport == nullptr) return; if (!window->viewport_smart_follow_sprite.IsNull()) { ViewportUpdateSmartFollowEntity(window); } if (!window->viewport_target_sprite.IsNull()) { ViewportUpdateFollowSprite(window); return; } ViewportSetUndergroundFlag(0, window, viewport); auto viewportMidPoint = ScreenCoordsXY{ window->savedViewPos.x + viewport->ViewWidth() / 2, window->savedViewPos.y + viewport->ViewHeight() / 2 }; auto mapCoord = ViewportPosToMapPos(viewportMidPoint, 0, viewport->rotation); // Clamp to the map minimum value int32_t at_map_edge = 0; if (mapCoord.x < kMapMinimumXY) { mapCoord.x = kMapMinimumXY; at_map_edge = 1; } if (mapCoord.y < kMapMinimumXY) { mapCoord.y = kMapMinimumXY; at_map_edge = 1; } // Clamp to the map maximum value (scenario specific) auto mapSizeMinus2 = GetMapSizeMinus2(); if (mapCoord.x > mapSizeMinus2.x) { mapCoord.x = mapSizeMinus2.x; at_map_edge = 1; } if (mapCoord.y > mapSizeMinus2.y) { mapCoord.y = mapSizeMinus2.y; at_map_edge = 1; } if (at_map_edge) { auto centreLoc = centre_2d_coordinates({ mapCoord, 0 }, viewport); if (centreLoc.has_value()) { window->savedViewPos = centreLoc.value(); } } auto windowCoords = window->savedViewPos; if (window->flags & WF_SCROLLING_TO_LOCATION) { // Moves the viewport if focusing in on an item uint8_t flags = 0; windowCoords.x -= viewport->viewPos.x; if (windowCoords.x < 0) { windowCoords.x = -windowCoords.x; flags |= 1; } windowCoords.y -= viewport->viewPos.y; if (windowCoords.y < 0) { windowCoords.y = -windowCoords.y; flags |= 2; } windowCoords.x = (windowCoords.x + 7) / 8; windowCoords.y = (windowCoords.y + 7) / 8; // If we are at the final zoom position if (!windowCoords.x && !windowCoords.y) { window->flags &= ~WF_SCROLLING_TO_LOCATION; } if (flags & 1) { windowCoords.x = -windowCoords.x; } if (flags & 2) { windowCoords.y = -windowCoords.y; } windowCoords.x += viewport->viewPos.x; windowCoords.y += viewport->viewPos.y; } ViewportMove(windowCoords, window, viewport); } void ViewportUpdateFollowSprite(WindowBase* window) { if (!window->viewport_target_sprite.IsNull() && window->viewport != nullptr) { auto* sprite = GetEntity(window->viewport_target_sprite); if (sprite == nullptr) { return; } if (gLegacyScene != LegacyScene::titleSequence) { int32_t height = TileElementHeight({ sprite->x, sprite->y }) - 16; int32_t underground = sprite->z < height; ViewportSetUndergroundFlag(underground, window, window->viewport); } auto centreLoc = centre_2d_coordinates(sprite->GetLocation(), window->viewport); if (centreLoc.has_value()) { window->savedViewPos = *centreLoc; ViewportMove(*centreLoc, window, window->viewport); } } } void ViewportUpdateSmartFollowEntity(WindowBase* window) { auto entity = TryGetEntity(window->viewport_smart_follow_sprite); if (entity == nullptr || entity->Type == EntityType::Null) { window->viewport_smart_follow_sprite = EntityId::GetNull(); window->viewport_target_sprite = EntityId::GetNull(); return; } switch (entity->Type) { case EntityType::Vehicle: ViewportUpdateSmartFollowVehicle(window); break; case EntityType::Guest: { auto* guest = entity->As(); if (guest == nullptr) { return; } ViewportUpdateSmartFollowGuest(window, *guest); break; } case EntityType::Staff: { auto* staff = entity->As(); if (staff == nullptr) { return; } ViewportUpdateSmartFollowStaff(window, *staff); break; } default: // All other types don't need any "smart" following; steam particle, duck, money effect, etc. window->focus = Focus(window->viewport_smart_follow_sprite); window->viewport_target_sprite = window->viewport_smart_follow_sprite; break; } } void ViewportUpdateSmartFollowGuest(WindowBase* window, const Guest& peep) { Focus focus = Focus(peep.Id); window->viewport_target_sprite = peep.Id; if (peep.State == PeepState::Picked) { window->viewport_smart_follow_sprite = EntityId::GetNull(); window->viewport_target_sprite = EntityId::GetNull(); window->focus = std::nullopt; // No focus return; } bool overallFocus = true; if (peep.State == PeepState::OnRide || peep.State == PeepState::EnteringRide || (peep.State == PeepState::LeavingRide && peep.x == kLocationNull)) { auto ride = GetRide(peep.CurrentRide); if (ride != nullptr && (ride->lifecycleFlags & RIDE_LIFECYCLE_ON_TRACK)) { auto train = GetEntity(ride->vehicles[peep.CurrentTrain]); if (train != nullptr) { const auto car = train->GetCar(peep.CurrentCar); if (car != nullptr) { focus = Focus(car->Id); overallFocus = false; window->viewport_target_sprite = car->Id; } } } } if (peep.x == kLocationNull && overallFocus) { auto ride = GetRide(peep.CurrentRide); if (ride != nullptr) { auto xy = ride->overallView.ToTileCentre(); CoordsXYZ coordFocus; coordFocus.x = xy.x; coordFocus.y = xy.y; coordFocus.z = TileElementHeight(xy) + (4 * kCoordsZStep); focus = Focus(coordFocus); window->viewport_target_sprite = EntityId::GetNull(); } } window->focus = focus; } void ViewportUpdateSmartFollowStaff(WindowBase* window, const Staff& peep) { if (peep.State == PeepState::Picked) { window->viewport_smart_follow_sprite = EntityId::GetNull(); window->viewport_target_sprite = EntityId::GetNull(); window->focus = std::nullopt; return; } window->focus = Focus(window->viewport_smart_follow_sprite); window->viewport_target_sprite = window->viewport_smart_follow_sprite; } void ViewportUpdateSmartFollowVehicle(WindowBase* window) { window->focus = Focus(window->viewport_smart_follow_sprite); window->viewport_target_sprite = window->viewport_smart_follow_sprite; } static void ViewportRotateSingleInternal(WindowBase& w, int32_t direction) { auto* viewport = w.viewport; if (viewport == nullptr) return; auto windowPos = ScreenCoordsXY{ (viewport->width >> 1), (viewport->height >> 1) } + viewport->pos; // has something to do with checking if middle of the viewport is obstructed Viewport* other; auto mapXYCoords = ScreenGetMapXY(windowPos, &other); CoordsXYZ coords{}; // other != viewport probably triggers on viewports in ride or guest window? // mapXYCoords is nullopt if middle of viewport is obstructed by another window? if (!mapXYCoords.has_value() || other != viewport) { auto viewPos = ScreenCoordsXY{ (viewport->ViewWidth() >> 1), (viewport->ViewHeight() >> 1) } + viewport->viewPos; coords = ViewportAdjustForMapHeight(viewPos, viewport->rotation); } else { coords.x = mapXYCoords->x; coords.y = mapXYCoords->y; coords.z = TileElementHeight(coords); } viewport->rotation = (viewport->rotation + direction) & 3; auto centreLoc = centre_2d_coordinates(coords, viewport); if (centreLoc.has_value()) { w.savedViewPos = centreLoc.value(); viewport->viewPos = *centreLoc; } w.Invalidate(); w.OnViewportRotate(); } void ViewportRotateSingle(WindowBase* window, int32_t direction) { ViewportRotateSingleInternal(*window, direction); } void ViewportRotateAll(int32_t direction) { WindowVisitEach([direction](WindowBase* w) { auto* viewport = w->viewport; if (viewport == nullptr) return; if (viewport->flags & VIEWPORT_FLAG_INDEPEDENT_ROTATION) return; ViewportRotateSingleInternal(*w, direction); }); } /** * * rct2: 0x00685C02 * ax: left * bx: top * dx: right * esi: viewport * edi: dpi * ebp: bottom */ void ViewportRender(DrawPixelInfo& dpi, const Viewport* viewport) { if (viewport->flags & VIEWPORT_FLAG_RENDERING_INHIBITED) return; if (dpi.x + dpi.width <= viewport->pos.x) return; if (dpi.y + dpi.height <= viewport->pos.y) return; if (dpi.x >= viewport->pos.x + viewport->width) return; if (dpi.y >= viewport->pos.y + viewport->height) return; ViewportPaint(viewport, dpi); } static void ViewportFillColumn(PaintSession& session) { PROFILED_FUNCTION(); PaintSessionGenerate(session); PaintSessionArrange(session); } static void ViewportPaintColumn(PaintSession& session) { PROFILED_FUNCTION(); if (session.ViewFlags & (VIEWPORT_FLAG_HIDE_VERTICAL | VIEWPORT_FLAG_HIDE_BASE | VIEWPORT_FLAG_UNDERGROUND_INSIDE | VIEWPORT_FLAG_CLIP_VIEW) && (~session.ViewFlags & VIEWPORT_FLAG_TRANSPARENT_BACKGROUND)) { uint8_t colour = COLOUR_AQUAMARINE; if (session.ViewFlags & VIEWPORT_FLAG_HIDE_ENTITIES) { colour = COLOUR_BLACK; } GfxClear(session.DPI, colour); } PaintDrawStructs(session); if (Config::Get().general.RenderWeatherGloom && !gTrackDesignSaveMode && !(session.ViewFlags & VIEWPORT_FLAG_HIDE_ENTITIES) && !(session.ViewFlags & VIEWPORT_FLAG_HIGHLIGHT_PATH_ISSUES)) { ViewportPaintWeatherGloom(session.DPI); } if (session.PSStringHead != nullptr) { PaintDrawMoneyStructs(session.DPI, session.PSStringHead); } } /** * * rct2: 0x00685CBF * eax: left * ebx: top * edx: right * esi: viewport * edi: dpi * ebp: bottom */ static void ViewportPaint(const Viewport* viewport, DrawPixelInfo& dpi) { PROFILED_FUNCTION(); const int32_t offsetX = dpi.x - viewport->pos.x; const int32_t offsetY = dpi.y - viewport->pos.y; const int32_t worldX = viewport->zoom.ApplyInversedTo(viewport->viewPos.x) + std::max(0, offsetX); const int32_t worldY = viewport->zoom.ApplyInversedTo(viewport->viewPos.y) + std::max(0, offsetY); const int32_t width = std::min(viewport->pos.x + viewport->width, dpi.x + dpi.width) - std::max(viewport->pos.x, dpi.x); const int32_t height = std::min(viewport->pos.y + viewport->height, dpi.y + dpi.height) - std::max(viewport->pos.y, dpi.y); DrawPixelInfo worldDpi; worldDpi.DrawingEngine = dpi.DrawingEngine; worldDpi.bits = dpi.bits + std::max(0, -offsetX) + std::max(0, -offsetY) * dpi.LineStride(); worldDpi.x = worldX; worldDpi.y = worldY; worldDpi.width = width; worldDpi.height = height; worldDpi.pitch = dpi.LineStride() - worldDpi.width; worldDpi.zoom_level = viewport->zoom; _paintColumns.clear(); bool useMultithreading = Config::Get().general.MultiThreading; if (useMultithreading && _paintJobs == nullptr) { _paintJobs = std::make_unique(); } else if (useMultithreading == false && _paintJobs != nullptr) { _paintJobs.reset(); } bool useParallelDrawing = false; if (useMultithreading && dpi.DrawingEngine->GetFlags().has(DrawingEngineFlag::parallelDrawing)) { useParallelDrawing = true; } const int32_t columnWidth = worldDpi.zoom_level.ApplyInversedTo(kCoordsXYStep); const int32_t rightBorder = worldDpi.x + worldDpi.width; const int32_t alignedX = floor2(worldDpi.x, columnWidth); // Generate and sort columns. for (int32_t x = alignedX; x < rightBorder; x += columnWidth) { PaintSession* session = PaintSessionAlloc(worldDpi, viewport->flags, viewport->rotation); _paintColumns.push_back(session); DrawPixelInfo& columnDpi = session->DPI; if (x >= columnDpi.x) { const int32_t leftPitch = x - columnDpi.x; columnDpi.width = columnDpi.width - leftPitch; columnDpi.bits += leftPitch; columnDpi.pitch += leftPitch; columnDpi.x = x; } int32_t paintRight = columnDpi.x + columnDpi.width; if (paintRight >= x + columnWidth) { const int32_t rightPitch = paintRight - x - columnWidth; paintRight -= rightPitch; columnDpi.pitch += rightPitch; } columnDpi.width = paintRight - columnDpi.x; if (useMultithreading) { _paintJobs->AddTask([session]() -> void { ViewportFillColumn(*session); }); } else { ViewportFillColumn(*session); } } if (useMultithreading) { _paintJobs->Join(); } // Paint columns. for (auto* session : _paintColumns) { if (useParallelDrawing) { _paintJobs->AddTask([session]() -> void { ViewportPaintColumn(*session); }); } else { ViewportPaintColumn(*session); } } if (useParallelDrawing) { _paintJobs->Join(); } // Release resources. for (auto* session : _paintColumns) { PaintSessionFree(session); } } static void ViewportPaintWeatherGloom(DrawPixelInfo& dpi) { auto paletteId = ClimateGetWeatherGloomPaletteId(getGameState().weatherCurrent); if (paletteId != FilterPaletteID::PaletteNull) { auto x = dpi.x; auto y = dpi.y; auto w = dpi.width; auto h = dpi.height; GfxFilterRect(dpi, ScreenRect(x, y, x + w, y + h), paletteId); } } /** * * rct2: 0x0068958D */ std::optional ScreenPosToMapPos(const ScreenCoordsXY& screenCoords, int32_t* direction) { auto mapCoords = ScreenGetMapXY(screenCoords, nullptr); if (!mapCoords.has_value()) return std::nullopt; int32_t my_direction; int32_t dist_from_centre_x = abs(mapCoords->x % 32); int32_t dist_from_centre_y = abs(mapCoords->y % 32); if (dist_from_centre_x > 8 && dist_from_centre_x < 24 && dist_from_centre_y > 8 && dist_from_centre_y < 24) { my_direction = 4; } else { auto mod_x = mapCoords->x & 0x1F; auto mod_y = mapCoords->y & 0x1F; if (mod_x <= 16) { if (mod_y < 16) { my_direction = 2; } else { my_direction = 3; } } else { if (mod_y < 16) { my_direction = 1; } else { my_direction = 0; } } } if (direction != nullptr) *direction = my_direction; return { mapCoords->ToTileStart() }; } [[nodiscard]] ScreenCoordsXY Viewport::ScreenToViewportCoord(const ScreenCoordsXY& screenCoords) const { ScreenCoordsXY ret; ret.x = (zoom.ApplyTo(screenCoords.x - pos.x)) + viewPos.x; ret.y = (zoom.ApplyTo(screenCoords.y - pos.y)) + viewPos.y; return ret; } void Viewport::Invalidate() const { ViewportInvalidate(this, { viewPos, viewPos + ScreenCoordsXY{ ViewWidth(), ViewHeight() } }); } CoordsXY ViewportPosToMapPos(const ScreenCoordsXY& coords, int32_t z, uint8_t rotation) { // Reverse of Translate3DTo2DWithZ CoordsXY ret = { coords.y - coords.x / 2 + z, coords.y + coords.x / 2 + z }; auto inverseRotation = DirectionFlipXAxis(rotation); return ret.Rotate(inverseRotation); } /** * * rct2: 0x00664689 */ void ShowGridlines() { if (gShowGridLinesRefCount == 0) { WindowBase* mainWindow = WindowGetMain(); if (mainWindow != nullptr) { if (!(mainWindow->viewport->flags & VIEWPORT_FLAG_GRIDLINES)) { mainWindow->viewport->flags |= VIEWPORT_FLAG_GRIDLINES; mainWindow->Invalidate(); } } } gShowGridLinesRefCount++; } /** * * rct2: 0x006646B4 */ void HideGridlines() { if (gShowGridLinesRefCount > 0) gShowGridLinesRefCount--; if (gShowGridLinesRefCount == 0) { WindowBase* mainWindow = WindowGetMain(); if (mainWindow != nullptr) { if (!Config::Get().general.AlwaysShowGridlines) { mainWindow->viewport->flags &= ~VIEWPORT_FLAG_GRIDLINES; mainWindow->Invalidate(); } } } } /** * * rct2: 0x00664E8E */ void ShowLandRights() { if (gShowLandRightsRefCount == 0) { WindowBase* mainWindow = WindowGetMain(); if (mainWindow != nullptr) { if (!(mainWindow->viewport->flags & VIEWPORT_FLAG_LAND_OWNERSHIP)) { mainWindow->viewport->flags |= VIEWPORT_FLAG_LAND_OWNERSHIP; mainWindow->Invalidate(); } } } gShowLandRightsRefCount++; } /** * * rct2: 0x00664EB9 */ void HideLandRights() { if (gShowLandRightsRefCount > 0) gShowLandRightsRefCount--; if (gShowLandRightsRefCount == 0) { WindowBase* mainWindow = WindowGetMain(); if (mainWindow != nullptr) { if (mainWindow->viewport->flags & VIEWPORT_FLAG_LAND_OWNERSHIP) { mainWindow->viewport->flags &= ~VIEWPORT_FLAG_LAND_OWNERSHIP; mainWindow->Invalidate(); } } } } /** * * rct2: 0x00664EDD */ void ShowConstructionRights() { if (gShowConstructionRightsRefCount == 0) { WindowBase* mainWindow = WindowGetMain(); if (mainWindow != nullptr) { if (!(mainWindow->viewport->flags & VIEWPORT_FLAG_CONSTRUCTION_RIGHTS)) { mainWindow->viewport->flags |= VIEWPORT_FLAG_CONSTRUCTION_RIGHTS; mainWindow->Invalidate(); } } } gShowConstructionRightsRefCount++; } /** * * rct2: 0x00664F08 */ void HideConstructionRights() { if (gShowConstructionRightsRefCount > 0) gShowConstructionRightsRefCount--; if (gShowConstructionRightsRefCount == 0) { WindowBase* mainWindow = WindowGetMain(); if (mainWindow != nullptr) { if (mainWindow->viewport->flags & VIEWPORT_FLAG_CONSTRUCTION_RIGHTS) { mainWindow->viewport->flags &= ~VIEWPORT_FLAG_CONSTRUCTION_RIGHTS; mainWindow->Invalidate(); } } } } /** * * rct2: 0x006CB70A */ void ViewportSetVisibility(ViewportVisibility mode) { WindowBase* window = WindowGetMain(); if (window != nullptr) { Viewport* vp = window->viewport; uint32_t invalidate = 0; switch (mode) { case ViewportVisibility::Default: { // Set all these flags to 0, and invalidate if any were active uint32_t mask = VIEWPORT_FLAG_UNDERGROUND_INSIDE | VIEWPORT_FLAG_HIDE_RIDES | VIEWPORT_FLAG_HIDE_SCENERY | VIEWPORT_FLAG_HIDE_PATHS | VIEWPORT_FLAG_LAND_HEIGHTS | VIEWPORT_FLAG_TRACK_HEIGHTS | VIEWPORT_FLAG_PATH_HEIGHTS | VIEWPORT_FLAG_HIDE_GUESTS | VIEWPORT_FLAG_HIDE_STAFF | VIEWPORT_FLAG_HIDE_BASE | VIEWPORT_FLAG_HIDE_VERTICAL | VIEWPORT_FLAG_HIDE_VEHICLES | VIEWPORT_FLAG_HIDE_SUPPORTS | VIEWPORT_FLAG_HIDE_VEGETATION; invalidate += vp->flags & mask; vp->flags &= ~mask; break; } case ViewportVisibility::UndergroundViewOn: // 6CB79D case ViewportVisibility::UndergroundViewGhostOn: // 6CB7C4 // Set underground on, invalidate if it was off invalidate += !(vp->flags & VIEWPORT_FLAG_UNDERGROUND_INSIDE); vp->flags |= VIEWPORT_FLAG_UNDERGROUND_INSIDE; break; case ViewportVisibility::TrackHeights: // 6CB7EB // Set track heights on, invalidate if off invalidate += !(vp->flags & VIEWPORT_FLAG_TRACK_HEIGHTS); vp->flags |= VIEWPORT_FLAG_TRACK_HEIGHTS; break; case ViewportVisibility::UndergroundViewOff: // 6CB7B1 case ViewportVisibility::UndergroundViewGhostOff: // 6CB7D8 // Set underground off, invalidate if it was on invalidate += vp->flags & VIEWPORT_FLAG_UNDERGROUND_INSIDE; vp->flags &= ~(static_cast(VIEWPORT_FLAG_UNDERGROUND_INSIDE)); break; } if (invalidate != 0) window->Invalidate(); } } static bool IsCursorIdVegetation(CursorID cursor) { switch (cursor) { case CursorID::TreeDown: case CursorID::FlowerDown: return true; default: return false; } } static bool IsTileElementVegetation(const TileElement* tileElement) { switch (tileElement->GetType()) { case TileElementType::SmallScenery: { auto sceneryItem = tileElement->AsSmallScenery(); auto sceneryEntry = sceneryItem->GetEntry(); if (sceneryEntry != nullptr && (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_IS_TREE) || IsCursorIdVegetation(sceneryEntry->tool_id))) { return true; } break; } case TileElementType::LargeScenery: { auto sceneryItem = tileElement->AsLargeScenery(); auto sceneryEntry = sceneryItem->GetEntry(); if (sceneryEntry != nullptr && IsCursorIdVegetation(sceneryEntry->tool_id)) { return true; } break; } case TileElementType::Wall: { auto sceneryItem = tileElement->AsWall(); auto sceneryEntry = sceneryItem->GetEntry(); if (sceneryEntry != nullptr && IsCursorIdVegetation(sceneryEntry->tool_id)) { return true; } break; } default: break; } return false; } VisibilityKind GetPaintStructVisibility(const PaintStruct* ps, uint32_t viewFlags) { // the cut-away view is active and see-through is activated auto cutAwayViewWithTransparency = (viewFlags & VIEWPORT_FLAG_CLIP_VIEW) && (viewFlags & VIEWPORT_FLAG_CLIP_VIEW_SEE_THROUGH); // the element is above the cut-off height auto clipped = cutAwayViewWithTransparency && ps->Element == nullptr && ps->Entity != nullptr && ps->Entity->GetLocation().z > (gClipHeight * kCoordsZStep); // the entity is above the cut-off height clipped |= cutAwayViewWithTransparency && ps->Element != nullptr && (ps->Element->GetBaseZ() > gClipHeight * kCoordsZStep); switch (ps->InteractionItem) { case ViewportInteractionItem::Entity: if (ps->Entity != nullptr) { switch (ps->Entity->Type) { case EntityType::Vehicle: { if (viewFlags & VIEWPORT_FLAG_HIDE_VEHICLES || clipped) { return (viewFlags & VIEWPORT_FLAG_INVISIBLE_VEHICLES) ? VisibilityKind::Hidden : VisibilityKind::Partial; } // Rides without track can technically have a 'vehicle': // these should be hidden if 'hide rides' is enabled if (viewFlags & VIEWPORT_FLAG_HIDE_RIDES || clipped) { auto vehicle = ps->Entity->As(); if (vehicle == nullptr) break; auto ride = vehicle->GetRide(); if (ride != nullptr && !ride->getRideTypeDescriptor().HasFlag(RtdFlag::hasTrack)) { return (viewFlags & VIEWPORT_FLAG_INVISIBLE_RIDES) ? VisibilityKind::Hidden : VisibilityKind::Partial; } } break; } case EntityType::Guest: if (viewFlags & VIEWPORT_FLAG_HIDE_GUESTS) { return VisibilityKind::Hidden; } else if (clipped) { return VisibilityKind::Partial; } break; case EntityType::Staff: if (viewFlags & VIEWPORT_FLAG_HIDE_STAFF) { return VisibilityKind::Hidden; } else if (clipped) { return VisibilityKind::Partial; } break; default: if (clipped) { return VisibilityKind::Partial; } break; } } break; case ViewportInteractionItem::Ride: if (viewFlags & VIEWPORT_FLAG_HIDE_RIDES || clipped) { return (viewFlags & VIEWPORT_FLAG_INVISIBLE_RIDES) ? VisibilityKind::Hidden : VisibilityKind::Partial; } break; case ViewportInteractionItem::Footpath: case ViewportInteractionItem::PathAddition: case ViewportInteractionItem::Banner: if (viewFlags & VIEWPORT_FLAG_HIDE_PATHS || clipped) { return (viewFlags & VIEWPORT_FLAG_INVISIBLE_PATHS) ? VisibilityKind::Hidden : VisibilityKind::Partial; } break; case ViewportInteractionItem::Scenery: case ViewportInteractionItem::LargeScenery: case ViewportInteractionItem::Wall: if (ps->Element != nullptr) { if (IsTileElementVegetation(ps->Element)) { if (viewFlags & VIEWPORT_FLAG_HIDE_VEGETATION || clipped) { return (viewFlags & VIEWPORT_FLAG_INVISIBLE_VEGETATION) ? VisibilityKind::Hidden : VisibilityKind::Partial; } } else { if (viewFlags & VIEWPORT_FLAG_HIDE_SCENERY || clipped) { return (viewFlags & VIEWPORT_FLAG_INVISIBLE_SCENERY) ? VisibilityKind::Hidden : VisibilityKind::Partial; } } } if (ps->InteractionItem == ViewportInteractionItem::Wall && (viewFlags & VIEWPORT_FLAG_UNDERGROUND_INSIDE || clipped)) { return VisibilityKind::Partial; } break; default: if (clipped) { return VisibilityKind::Partial; } break; } return VisibilityKind::Visible; } /** * Checks if a PaintStruct sprite type is in the filter mask. */ static bool PSInteractionTypeIsInFilter(PaintStruct* ps, uint16_t filter) { if (ps->InteractionItem != ViewportInteractionItem::None && ps->InteractionItem != ViewportInteractionItem::Label && ps->InteractionItem <= ViewportInteractionItem::Banner) { auto mask = EnumToFlag(ps->InteractionItem); if (filter & mask) { return true; } } return false; } /** * rct2: 0x00679236, 0x00679662, 0x00679B0D, 0x00679FF1 */ static bool IsPixelPresentBMP( const uint32_t imageType, const G1Element* g1, const int32_t x, const int32_t y, const PaletteMap& paletteMap) { uint8_t* index = g1->offset + (y * g1->width) + x; // Needs investigation as it has no consideration for pure BMP maps. if (!(g1->flags & G1_FLAG_HAS_TRANSPARENCY)) { return false; } if (imageType & IMAGE_TYPE_REMAP) { return paletteMap[*index] != 0; } if (imageType & IMAGE_TYPE_TRANSPARENT) { return false; } return (*index != 0); } /** * rct2: 0x0067933B, 0x00679788, 0x00679C4A, 0x0067A117 */ static bool IsPixelPresentRLE(const uint8_t* imgData, const int32_t x, const int32_t y) { uint16_t lineOffset; std::memcpy(&lineOffset, &imgData[y * sizeof(uint16_t)], sizeof(uint16_t)); const uint8_t* data8 = imgData + lineOffset; bool lastDataLine = false; while (!lastDataLine) { int32_t numPixels = *data8++; uint8_t pixelRunStart = *data8++; lastDataLine = numPixels & 0x80; numPixels &= 0x7F; data8 += numPixels; if (pixelRunStart <= x && x < pixelRunStart + numPixels) return true; } return false; } /** * rct2: 0x00679074 */ static bool IsSpriteInteractedWithPaletteSet( DrawPixelInfo& dpi, ImageId imageId, const ScreenCoordsXY& coords, const PaletteMap& paletteMap, const uint8_t imageType) { PROFILED_FUNCTION(); const G1Element* g1 = GfxGetG1Element(imageId); if (g1 == nullptr) { return false; } ZoomLevel zoomLevel = dpi.zoom_level; ScreenCoordsXY interactionPoint{ dpi.WorldX(), dpi.WorldY() }; ScreenCoordsXY origin = coords; if (dpi.zoom_level > ZoomLevel{ 0 }) { if (g1->flags & G1_FLAG_NO_ZOOM_DRAW) { return false; } while (g1->flags & G1_FLAG_HAS_ZOOM_SPRITE && zoomLevel > ZoomLevel{ 0 }) { imageId = imageId.WithIndex(imageId.GetIndex() - g1->zoomed_offset); g1 = GfxGetG1Element(imageId); if (g1 == nullptr || g1->flags & G1_FLAG_NO_ZOOM_DRAW) { return false; } zoomLevel = zoomLevel - 1; interactionPoint.x >>= 1; interactionPoint.y >>= 1; origin.x >>= 1; origin.y >>= 1; } } origin.x += g1->x_offset; origin.y += g1->y_offset; interactionPoint -= origin; if (interactionPoint.x < 0 || interactionPoint.y < 0 || interactionPoint.x >= g1->width || interactionPoint.y >= g1->height) { return false; } if (g1->flags & G1_FLAG_RLE_COMPRESSION) { return IsPixelPresentRLE(g1->offset, interactionPoint.x, interactionPoint.y); } if (!(g1->flags & G1_FLAG_1)) { return IsPixelPresentBMP(imageType, g1, interactionPoint.x, interactionPoint.y, paletteMap); } Guard::Assert(false, "Invalid image type encountered."); return false; } /** * * rct2: 0x00679023 */ static bool IsSpriteInteractedWith(DrawPixelInfo& dpi, ImageId imageId, const ScreenCoordsXY& coords) { PROFILED_FUNCTION(); auto paletteMap = PaletteMap::GetDefault(); uint8_t imageType; if (imageId.HasPrimary() || imageId.IsRemap()) { imageType = IMAGE_TYPE_REMAP; uint8_t paletteIndex; if (imageId.HasSecondary()) { paletteIndex = imageId.GetPrimary(); } else { paletteIndex = imageId.GetRemap(); } if (auto pm = GetPaletteMapForColour(paletteIndex); pm.has_value()) { paletteMap = pm.value(); } } else { imageType = IMAGE_TYPE_DEFAULT; } return IsSpriteInteractedWithPaletteSet(dpi, imageId, coords, paletteMap, imageType); } /** * * rct2: 0x0068862C */ InteractionInfo SetInteractionInfoFromPaintSession(PaintSession* session, uint32_t viewFlags, uint16_t filter) { PROFILED_FUNCTION(); InteractionInfo info{}; PaintStruct* ps = session->PaintHead; while (ps != nullptr) { PaintStruct* old_ps = ps; PaintStruct* next_ps = ps; while (next_ps != nullptr) { ps = next_ps; if (IsSpriteInteractedWith(session->DPI, ps->image_id, ps->ScreenPos)) { if (PSInteractionTypeIsInFilter(ps, filter) && GetPaintStructVisibility(ps, viewFlags) == VisibilityKind::Visible) { info = { ps }; } } next_ps = ps->Children; } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wnull-dereference" for (AttachedPaintStruct* attached_ps = ps->Attached; attached_ps != nullptr; attached_ps = attached_ps->NextEntry) { if (IsSpriteInteractedWith(session->DPI, attached_ps->image_id, ps->ScreenPos + attached_ps->RelativePos)) { if (PSInteractionTypeIsInFilter(ps, filter) && GetPaintStructVisibility(ps, viewFlags) == VisibilityKind::Visible) { info = { ps }; } } } #pragma GCC diagnostic pop ps = old_ps->NextQuadrantEntry; } return info; } /** * * rct2: 0x00685ADC * screenX: eax * screenY: ebx * flags: edx * x: ax * y: cx * interactionType: bl * tileElement: edx * viewport: edi */ InteractionInfo GetMapCoordinatesFromPos(const ScreenCoordsXY& screenCoords, int32_t flags) { auto* windowMgr = Ui::GetWindowManager(); WindowBase* window = windowMgr->FindFromPoint(screenCoords); return GetMapCoordinatesFromPosWindow(window, screenCoords, flags); } InteractionInfo GetMapCoordinatesFromPosWindow(WindowBase* window, const ScreenCoordsXY& screenCoords, int32_t flags) { InteractionInfo info{}; if (window == nullptr || window->viewport == nullptr) { return info; } Viewport* viewport = window->viewport; auto viewLoc = screenCoords; viewLoc -= viewport->pos; if (viewLoc.x >= 0 && viewLoc.x < static_cast(viewport->width) && viewLoc.y >= 0 && viewLoc.y < static_cast(viewport->height)) { viewLoc.x = viewport->zoom.ApplyTo(viewLoc.x); viewLoc.y = viewport->zoom.ApplyTo(viewLoc.y); viewLoc += viewport->viewPos; if (viewport->zoom > ZoomLevel{ 0 }) { viewLoc.x &= viewport->zoom.ApplyTo(0xFFFFFFFF) & 0xFFFFFFFF; viewLoc.y &= viewport->zoom.ApplyTo(0xFFFFFFFF) & 0xFFFFFFFF; } DrawPixelInfo dpi; dpi.zoom_level = viewport->zoom; dpi.x = viewport->zoom.ApplyInversedTo(viewLoc.x); dpi.y = viewport->zoom.ApplyInversedTo(viewLoc.y); dpi.height = 1; dpi.width = 1; PaintSession* session = PaintSessionAlloc(dpi, viewport->flags, viewport->rotation); PaintSessionGenerate(*session); PaintSessionArrange(*session); info = SetInteractionInfoFromPaintSession(session, viewport->flags, flags & 0xFFFF); PaintSessionFree(session); } return info; } /** * screenRect represents 2D map coordinates at zoom 0. */ void ViewportInvalidate(const Viewport* viewport, const ScreenRect& screenRect) { PROFILED_FUNCTION(); // if unknown viewport visibility, use the containing window to discover the status if (viewport->visibility == VisibilityCache::unknown) { auto windowManager = Ui::GetWindowManager(); auto owner = windowManager->GetOwner(viewport); if (owner != nullptr && owner->classification != WindowClass::MainWindow) { // note, window_is_visible will update viewport->visibility, so this should have a low hit count if (!WindowIsVisible(*owner)) { return; } } } if (viewport->visibility == VisibilityCache::covered) return; auto zoom = viewport->zoom; auto viewPos = viewport->viewPos; auto viewportScreenPos = viewport->pos; ScreenRect invalidRect = { { zoom.ApplyInversedTo(screenRect.GetLeft() - viewPos.x), zoom.ApplyInversedTo(screenRect.GetTop() - viewPos.y) }, { zoom.ApplyInversedTo(screenRect.GetRight() - viewPos.x), zoom.ApplyInversedTo(screenRect.GetBottom() - viewPos.y) } }; if (invalidRect.GetTop() >= viewport->height || invalidRect.GetBottom() <= 0 || invalidRect.GetLeft() >= viewport->width || invalidRect.GetRight() <= 0) { return; } invalidRect.Point1 += viewportScreenPos; invalidRect.Point2 += viewportScreenPos; GfxSetDirtyBlocks(invalidRect); } static Viewport* ViewportFindFromPoint(const ScreenCoordsXY& screenCoords) { auto* windowMgr = Ui::GetWindowManager(); WindowBase* w = windowMgr->FindFromPoint(screenCoords); if (w == nullptr) return nullptr; Viewport* viewport = w->viewport; if (viewport == nullptr) return nullptr; if (viewport->ContainsScreen(screenCoords)) return viewport; return nullptr; } /** * * rct2: 0x00688972 * In: * screen_x: eax * screen_y: ebx * Out: * x: ax * y: bx * tile_element: edx ? * viewport: edi */ std::optional ScreenGetMapXY(const ScreenCoordsXY& screenCoords, Viewport** viewport) { auto* windowMgr = Ui::GetWindowManager(); // This will get the tile location but we will need the more accuracy WindowBase* window = windowMgr->FindFromPoint(screenCoords); if (window == nullptr || window->viewport == nullptr) { return std::nullopt; } auto myViewport = window->viewport; auto info = GetMapCoordinatesFromPosWindow(window, screenCoords, EnumsToFlags(ViewportInteractionItem::Terrain)); if (info.interactionType == ViewportInteractionItem::None) { return std::nullopt; } auto start_vp_pos = myViewport->ScreenToViewportCoord(screenCoords); CoordsXY cursorMapPos = info.Loc.ToTileCentre(); // Iterates the cursor location to work out exactly where on the tile it is for (int32_t i = 0; i < 5; i++) { int32_t z = TileElementHeight(cursorMapPos); cursorMapPos = ViewportPosToMapPos(start_vp_pos, z, myViewport->rotation); cursorMapPos.x = std::clamp(cursorMapPos.x, info.Loc.x, info.Loc.x + 31); cursorMapPos.y = std::clamp(cursorMapPos.y, info.Loc.y, info.Loc.y + 31); } if (viewport != nullptr) *viewport = myViewport; return cursorMapPos; } /** * * rct2: 0x006894D4 */ std::optional ScreenGetMapXYWithZ(const ScreenCoordsXY& screenCoords, int32_t z) { Viewport* viewport = ViewportFindFromPoint(screenCoords); if (viewport == nullptr) { return std::nullopt; } auto vpCoords = viewport->ScreenToViewportCoord(screenCoords); auto mapPosition = ViewportPosToMapPos(vpCoords, z, viewport->rotation); if (!MapIsLocationValid(mapPosition)) { return std::nullopt; } return mapPosition; } /** * * rct2: 0x00689604 */ std::optional ScreenGetMapXYQuadrant(const ScreenCoordsXY& screenCoords, uint8_t* quadrant) { auto mapCoords = ScreenGetMapXY(screenCoords, nullptr); if (!mapCoords.has_value()) return std::nullopt; *quadrant = MapGetTileQuadrant(*mapCoords); return mapCoords->ToTileStart(); } /** * * rct2: 0x0068964B */ std::optional ScreenGetMapXYQuadrantWithZ(const ScreenCoordsXY& screenCoords, int32_t z, uint8_t* quadrant) { auto mapCoords = ScreenGetMapXYWithZ(screenCoords, z); if (!mapCoords.has_value()) return std::nullopt; *quadrant = MapGetTileQuadrant(*mapCoords); return mapCoords->ToTileStart(); } /** * * rct2: 0x00689692 */ std::optional ScreenGetMapXYSide(const ScreenCoordsXY& screenCoords, uint8_t* side) { auto mapCoords = ScreenGetMapXY(screenCoords, nullptr); if (!mapCoords.has_value()) return std::nullopt; *side = MapGetTileSide(*mapCoords); return mapCoords->ToTileStart(); } /** * * rct2: 0x006896DC */ std::optional ScreenGetMapXYSideWithZ(const ScreenCoordsXY& screenCoords, int32_t z, uint8_t* side) { auto mapCoords = ScreenGetMapXYWithZ(screenCoords, z); if (!mapCoords.has_value()) return std::nullopt; *side = MapGetTileSide(*mapCoords); return mapCoords->ToTileStart(); } ScreenCoordsXY Translate3DTo2DWithZ(int32_t rotation, const CoordsXYZ& pos) { auto rotated = pos.Rotate(rotation); // Use right shift to avoid issues like #9301 return ScreenCoordsXY{ rotated.y - rotated.x, ((rotated.x + rotated.y) >> 1) - pos.z }; } /** * Get current viewport rotation. * * If an invalid rotation is detected and DEBUG_LEVEL_1 is enabled, an error * will be reported. * * @returns rotation in range 0-3 (inclusive) */ uint8_t GetCurrentRotation() { auto* viewport = ViewportGetMain(); if (viewport == nullptr) { LOG_VERBOSE("No viewport found! Will return 0."); return 0; } uint8_t rotation = viewport->rotation; uint8_t rotation_masked = rotation & 3; #if defined(DEBUG_LEVEL_1) && DEBUG_LEVEL_1 if (rotation != rotation_masked) { LOG_ERROR( "Found wrong rotation %d! Will return %d instead.", static_cast(rotation), static_cast(rotation_masked)); } #endif // DEBUG_LEVEL_1 return rotation_masked; } int32_t GetHeightMarkerOffset() { // Height labels in units if (Config::Get().general.ShowHeightAsUnits) return 0; // Height labels in feet if (Config::Get().general.MeasurementFormat == MeasurementFormat::Imperial) return 1 * 256; // Height labels in metres return 2 * 256; } void ViewportSetSavedView() { WindowBase* w = WindowGetMain(); if (w != nullptr) { Viewport* viewport = w->viewport; auto& gameState = getGameState(); gameState.savedView = ScreenCoordsXY{ viewport->ViewWidth() / 2, viewport->ViewHeight() / 2 } + viewport->viewPos; gameState.savedViewZoom = viewport->zoom; gameState.savedViewRotation = viewport->rotation; } } } // namespace OpenRCT2 ZoomLevel ZoomLevel::min() { return ZoomLevel{ -2 }; }