/***************************************************************************** * Copyright (c) 2014-2024 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 "Map.h" #include "../Cheats.h" #include "../Context.h" #include "../Game.h" #include "../GameState.h" #include "../Input.h" #include "../OpenRCT2.h" #include "../actions/BannerRemoveAction.h" #include "../actions/LargeSceneryRemoveAction.h" #include "../actions/ParkEntranceRemoveAction.h" #include "../actions/WallRemoveAction.h" #include "../audio/audio.h" #include "../config/Config.h" #include "../core/Guard.hpp" #include "../interface/Cursors.h" #include "../interface/Window.h" #include "../localisation/Date.h" #include "../localisation/Localisation.h" #include "../management/Finance.h" #include "../network/network.h" #include "../object/LargeSceneryEntry.h" #include "../object/ObjectManager.h" #include "../object/SmallSceneryEntry.h" #include "../object/TerrainSurfaceObject.h" #include "../profiling/Profiling.h" #include "../ride/RideConstruction.h" #include "../ride/RideData.h" #include "../ride/Track.h" #include "../ride/TrackData.h" #include "../ride/TrackDesign.h" #include "../scenario/Scenario.h" #include "../util/Util.h" #include "../windows/Intent.h" #include "../world/TilePointerIndex.hpp" #include "Banner.h" #include "Climate.h" #include "Footpath.h" #include "MapAnimation.h" #include "Park.h" #include "Scenery.h" #include "Surface.h" #include "TileElementsView.h" #include "TileInspector.h" #include "Wall.h" #include #include #include using namespace OpenRCT2; /** * Replaces 0x00993CCC, 0x00993CCE */ // clang-format off const std::array CoordsDirectionDelta = { CoordsXY{ -COORDS_XY_STEP, 0 }, CoordsXY{ 0, +COORDS_XY_STEP }, CoordsXY{ +COORDS_XY_STEP, 0 }, CoordsXY{ 0, -COORDS_XY_STEP }, CoordsXY{ -COORDS_XY_STEP, +COORDS_XY_STEP }, CoordsXY{ +COORDS_XY_STEP, +COORDS_XY_STEP }, CoordsXY{ +COORDS_XY_STEP, -COORDS_XY_STEP }, CoordsXY{ -COORDS_XY_STEP, -COORDS_XY_STEP } }; // clang-format on const TileCoordsXY TileDirectionDelta[] = { { -1, 0 }, { 0, +1 }, { +1, 0 }, { 0, -1 }, { -1, +1 }, { +1, +1 }, { +1, -1 }, { -1, -1 }, }; constexpr size_t MIN_TILE_ELEMENTS = 1024; uint16_t gMapSelectFlags; uint16_t gMapSelectType; CoordsXY gMapSelectPositionA; CoordsXY gMapSelectPositionB; CoordsXYZ gMapSelectArrowPosition; uint8_t gMapSelectArrowDirection; std::vector gMapSelectionTiles; bool gLandMountainMode; bool gLandPaintMode; bool gClearSmallScenery; bool gClearLargeScenery; bool gClearFootpath; uint32_t gLandRemainingOwnershipSales; uint32_t gLandRemainingConstructionSales; bool gMapLandRightsUpdateSuccess; static TilePointerIndex _tileIndex; static TilePointerIndex _tileIndexStash; static std::vector _tileElementsStash; static size_t _tileElementsInUse; static size_t _tileElementsInUseStash; static TileCoordsXY _mapSizeStash; void StashMap() { auto& gameState = GetGameState(); _tileIndexStash = std::move(_tileIndex); _tileElementsStash = std::move(gameState.TileElements); _mapSizeStash = GetGameState().MapSize; _tileElementsInUseStash = _tileElementsInUse; } void UnstashMap() { auto& gameState = GetGameState(); _tileIndex = std::move(_tileIndexStash); gameState.TileElements = std::move(_tileElementsStash); GetGameState().MapSize = _mapSizeStash; _tileElementsInUse = _tileElementsInUseStash; } CoordsXY GetMapSizeUnits() { auto& gameState = OpenRCT2::GetGameState(); return { (gameState.MapSize.x - 1) * COORDS_XY_STEP, (gameState.MapSize.y - 1) * COORDS_XY_STEP }; } CoordsXY GetMapSizeMinus2() { auto& gameState = OpenRCT2::GetGameState(); return { (gameState.MapSize.x * COORDS_XY_STEP) + (8 * COORDS_XY_STEP - 2), (gameState.MapSize.y * COORDS_XY_STEP) + (8 * COORDS_XY_STEP - 2) }; } CoordsXY GetMapSizeMaxXY() { return GetMapSizeUnits() - CoordsXY{ 1, 1 }; } const std::vector& GetTileElements() { return GetGameState().TileElements; } void SetTileElements(std::vector&& tileElements) { auto& gameState = GetGameState(); gameState.TileElements = std::move(tileElements); _tileIndex = TilePointerIndex( kMaximumMapSizeTechnical, gameState.TileElements.data(), gameState.TileElements.size()); _tileElementsInUse = gameState.TileElements.size(); } static TileElement GetDefaultSurfaceElement() { TileElement el; el.ClearAs(TileElementType::Surface); el.SetLastForTile(true); el.BaseHeight = 14; el.ClearanceHeight = 14; el.AsSurface()->SetWaterHeight(0); el.AsSurface()->SetSlope(TILE_ELEMENT_SLOPE_FLAT); el.AsSurface()->SetGrassLength(GRASS_LENGTH_CLEAR_0); el.AsSurface()->SetOwnership(OWNERSHIP_UNOWNED); el.AsSurface()->SetParkFences(0); el.AsSurface()->SetSurfaceObjectIndex(0); el.AsSurface()->SetEdgeObjectIndex(0); return el; } std::vector GetReorganisedTileElementsWithoutGhosts() { std::vector newElements; newElements.reserve(std::max(MIN_TILE_ELEMENTS, GetGameState().TileElements.size())); for (int32_t y = 0; y < kMaximumMapSizeTechnical; y++) { for (int32_t x = 0; x < kMaximumMapSizeTechnical; x++) { auto oldSize = newElements.size(); // Add all non-ghost elements const auto* element = MapGetFirstElementAt(TileCoordsXY{ x, y }); if (element != nullptr) { do { if (!element->IsGhost()) { newElements.push_back(*element); } } while (!(element++)->IsLastForTile()); } // Insert default surface element if no elements were added auto newSize = newElements.size(); if (oldSize == newSize) { newElements.push_back(GetDefaultSurfaceElement()); } // Ensure last element of tile has last flag set auto& lastEl = newElements.back(); lastEl.SetLastForTile(true); } } return newElements; } static void ReorganiseTileElements(size_t capacity) { ContextSetCurrentCursor(CursorID::ZZZ); std::vector newElements; newElements.reserve(std::max(MIN_TILE_ELEMENTS, capacity)); for (int32_t y = 0; y < kMaximumMapSizeTechnical; y++) { for (int32_t x = 0; x < kMaximumMapSizeTechnical; x++) { const auto* element = MapGetFirstElementAt(TileCoordsXY{ x, y }); if (element == nullptr) { newElements.push_back(GetDefaultSurfaceElement()); } else { do { newElements.push_back(*element); } while (!(element++)->IsLastForTile()); } } } SetTileElements(std::move(newElements)); } void ReorganiseTileElements() { ReorganiseTileElements(GetGameState().TileElements.size()); } static bool MapCheckFreeElementsAndReorganise(size_t numElementsOnTile, size_t numNewElements) { // Check hard cap on num in use tiles (this would be the size of _tileElements immediately after a reorg) if (_tileElementsInUse + numNewElements > MAX_TILE_ELEMENTS) { return false; } auto& gameState = GetGameState(); auto totalElementsRequired = numElementsOnTile + numNewElements; auto freeElements = gameState.TileElements.capacity() - gameState.TileElements.size(); if (freeElements >= totalElementsRequired) { return true; } // if space issue is due to fragmentation then Reorg Tiles without increasing capacity if (gameState.TileElements.size() > totalElementsRequired + _tileElementsInUse) { ReorganiseTileElements(); // This check is not expected to fail freeElements = gameState.TileElements.capacity() - gameState.TileElements.size(); if (freeElements >= totalElementsRequired) { return true; } } // Capacity must increase to handle the space (Note capacity can go above MAX_TILE_ELEMENTS) auto newCapacity = gameState.TileElements.capacity() * 2; ReorganiseTileElements(newCapacity); return true; } static size_t CountElementsOnTile(const CoordsXY& loc); bool MapCheckCapacityAndReorganise(const CoordsXY& loc, size_t numElements) { auto numElementsOnTile = CountElementsOnTile(loc); return MapCheckFreeElementsAndReorganise(numElementsOnTile, numElements); } static void ClearElementsAt(const CoordsXY& loc); static ScreenCoordsXY Translate3DTo2D(int32_t rotation, const CoordsXY& pos); void TileElementIteratorBegin(TileElementIterator* it) { it->x = 1; it->y = 1; it->element = MapGetFirstElementAt(TileCoordsXY{ 1, 1 }); } int32_t TileElementIteratorNext(TileElementIterator* it) { if (it->element == nullptr) { it->element = MapGetFirstElementAt(TileCoordsXY{ it->x, it->y }); return 1; } if (!it->element->IsLastForTile()) { it->element++; return 1; } auto& gameState = GetGameState(); if (it->y < (gameState.MapSize.y - 2)) { it->y++; it->element = MapGetFirstElementAt(TileCoordsXY{ it->x, it->y }); return 1; } if (it->x < (gameState.MapSize.x - 2)) { it->y = 1; it->x++; it->element = MapGetFirstElementAt(TileCoordsXY{ it->x, it->y }); return 1; } return 0; } void TileElementIteratorRestartForTile(TileElementIterator* it) { it->element = nullptr; } static bool IsTileLocationValid(const TileCoordsXY& coords) { const bool is_x_valid = coords.x < kMaximumMapSizeTechnical && coords.x >= 0; const bool is_y_valid = coords.y < kMaximumMapSizeTechnical && coords.y >= 0; return is_x_valid && is_y_valid; } TileElement* MapGetFirstElementAt(const TileCoordsXY& tilePos) { if (!IsTileLocationValid(tilePos)) { LOG_VERBOSE("Trying to access element outside of range"); return nullptr; } return _tileIndex.GetFirstElementAt(tilePos); } TileElement* MapGetFirstElementAt(const CoordsXY& elementPos) { return MapGetFirstElementAt(TileCoordsXY{ elementPos }); } TileElement* MapGetNthElementAt(const CoordsXY& coords, int32_t n) { TileElement* tileElement = MapGetFirstElementAt(coords); if (tileElement == nullptr) { return nullptr; } // Iterate through elements on this tile. This has to be walked, rather than // jumped directly to, because n may exceed element count for given tile, // and the order of tiles (unlike elements) is not synced over multiplayer. while (n >= 0) { if (n == 0) { return tileElement; } if (tileElement->IsLastForTile()) { break; } tileElement++; n--; } // The element sought for is not within given tile. return nullptr; } TileElement* MapGetFirstTileElementWithBaseHeightBetween(const TileCoordsXYRangedZ& loc, TileElementType type) { TileElement* tileElement = MapGetFirstElementAt(loc); if (tileElement == nullptr) return nullptr; do { if (tileElement->GetType() != type) continue; if (tileElement->BaseHeight >= loc.baseZ && tileElement->BaseHeight <= loc.clearanceZ) return tileElement; } while (!(tileElement++)->IsLastForTile()); return nullptr; } void MapSetTileElement(const TileCoordsXY& tilePos, TileElement* elements) { if (!MapIsLocationValid(tilePos.ToCoordsXY())) { LOG_ERROR("Trying to access element outside of range"); return; } _tileIndex.SetTile(tilePos, elements); } SurfaceElement* MapGetSurfaceElementAt(const TileCoordsXY& coords) { auto view = TileElementsView(coords); return *view.begin(); } SurfaceElement* MapGetSurfaceElementAt(const CoordsXY& coords) { return MapGetSurfaceElementAt(TileCoordsXY{ coords }); } PathElement* MapGetPathElementAt(const TileCoordsXYZ& loc) { for (auto* element : TileElementsView(loc.ToCoordsXY())) { if (element->IsGhost()) continue; if (element->BaseHeight != loc.z) continue; return element; } return nullptr; } BannerElement* MapGetBannerElementAt(const CoordsXYZ& bannerPos, uint8_t position) { const auto bannerTilePos = TileCoordsXYZ{ bannerPos }; for (auto* element : TileElementsView(bannerPos)) { if (element->BaseHeight != bannerTilePos.z) continue; if (element->GetPosition() != position) continue; return element; } return nullptr; } /** * * rct2: 0x0068AB4C */ void MapInit(const TileCoordsXY& size) { auto numTiles = kMaximumMapSizeTechnical * kMaximumMapSizeTechnical; SetTileElements(std::vector(numTiles, GetDefaultSurfaceElement())); auto& gameState = GetGameState(); gameState.GrassSceneryTileLoopPosition = 0; gameState.WidePathTileLoopPosition = {}; gameState.MapSize = size; MapRemoveOutOfRangeElements(); MapAnimationAutoCreate(); auto intent = Intent(INTENT_ACTION_MAP); ContextBroadcastIntent(&intent); } /** * Counts the number of surface tiles that offer land ownership rights for sale, * but haven't been bought yet. It updates gLandRemainingOwnershipSales and * gLandRemainingConstructionSales. */ void MapCountRemainingLandRights() { gLandRemainingOwnershipSales = 0; gLandRemainingConstructionSales = 0; auto& gameState = GetGameState(); for (int32_t y = 0; y < gameState.MapSize.y; y++) { for (int32_t x = 0; x < gameState.MapSize.x; x++) { auto* surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); // Surface elements are sometimes hacked out to save some space for other map elements if (surfaceElement == nullptr) { continue; } uint8_t flags = surfaceElement->GetOwnership(); // Do not combine this condition with (flags & OWNERSHIP_AVAILABLE) // As some RCT1 parks have owned tiles with the 'construction rights available' flag also set if (!(flags & OWNERSHIP_OWNED)) { if (flags & OWNERSHIP_AVAILABLE) { gLandRemainingOwnershipSales++; } else if ( (flags & OWNERSHIP_CONSTRUCTION_RIGHTS_AVAILABLE) && (flags & OWNERSHIP_CONSTRUCTION_RIGHTS_OWNED) == 0) { gLandRemainingConstructionSales++; } } } } } /** * This is meant to strip TILE_ELEMENT_FLAG_GHOST flag from all elements when * importing a park. * * This can only exist in hacked parks, as we remove ghost elements while saving. * * This is less invasive than removing ghost elements themselves, as they can * contain valid data. */ void MapStripGhostFlagFromElements() { auto& gameState = GetGameState(); for (auto& element : gameState.TileElements) { element.SetGhost(false); } } /** * Return the absolute height of an element, given its (x,y) coordinates * * ax: x * cx: y * dx: return remember to & with 0xFFFF if you don't want water affecting results * rct2: 0x00662783 */ int16_t TileElementHeight(const CoordsXY& loc) { // Off the map if (!MapIsLocationValid(loc)) return kMinimumLandZ; // Get the surface element for the tile auto surfaceElement = MapGetSurfaceElementAt(loc); if (surfaceElement == nullptr) { return kMinimumLandZ; } auto height = surfaceElement->GetBaseZ(); auto slope = surfaceElement->GetSlope(); return TileElementHeight(CoordsXYZ{ loc, height }, slope); } int16_t TileElementHeight(const CoordsXYZ& loc, uint8_t slope) { // Off the map if (!MapIsLocationValid(loc)) return kMinimumLandZ; auto height = loc.z; uint8_t extra_height = (slope & TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT) >> 4; // 0x10 is the 5th bit - sets slope to double height // Remove the extra height bit slope &= TILE_ELEMENT_SLOPE_ALL_CORNERS_UP; int8_t quad = 0, quad_extra = 0; // which quadrant the element is in? // quad_extra is for extra height tiles uint8_t xl, yl; // coordinates across this tile uint8_t TILE_SIZE = 32; xl = loc.x & 0x1f; yl = loc.y & 0x1f; // Slope logic: // Each of the four bits in slope represents that corner being raised // slope == 15 (all four bits) is not used and slope == 0 is flat // If the extra_height bit is set, then the slope goes up two z-levels // We arbitrarily take the SW corner to be closest to the viewer // One corner up if (slope == TILE_ELEMENT_SLOPE_N_CORNER_UP || slope == TILE_ELEMENT_SLOPE_E_CORNER_UP || slope == TILE_ELEMENT_SLOPE_S_CORNER_UP || slope == TILE_ELEMENT_SLOPE_W_CORNER_UP) { switch (slope) { case TILE_ELEMENT_SLOPE_N_CORNER_UP: quad = xl + yl - TILE_SIZE; break; case TILE_ELEMENT_SLOPE_E_CORNER_UP: quad = xl - yl; break; case TILE_ELEMENT_SLOPE_S_CORNER_UP: quad = TILE_SIZE - yl - xl; break; case TILE_ELEMENT_SLOPE_W_CORNER_UP: quad = yl - xl; break; } // If the element is in the quadrant with the slope, raise its height if (quad > 0) { height += quad / 2; } } // One side up switch (slope) { case TILE_ELEMENT_SLOPE_NE_SIDE_UP: height += xl / 2; break; case TILE_ELEMENT_SLOPE_SE_SIDE_UP: height += (TILE_SIZE - yl) / 2; break; case TILE_ELEMENT_SLOPE_NW_SIDE_UP: height += yl / 2; break; case TILE_ELEMENT_SLOPE_SW_SIDE_UP: height += (TILE_SIZE - xl) / 2; break; } // One corner down if ((slope == TILE_ELEMENT_SLOPE_W_CORNER_DN) || (slope == TILE_ELEMENT_SLOPE_S_CORNER_DN) || (slope == TILE_ELEMENT_SLOPE_E_CORNER_DN) || (slope == TILE_ELEMENT_SLOPE_N_CORNER_DN)) { switch (slope) { case TILE_ELEMENT_SLOPE_W_CORNER_DN: quad_extra = xl + TILE_SIZE - yl; quad = xl - yl; break; case TILE_ELEMENT_SLOPE_S_CORNER_DN: quad_extra = xl + yl; quad = xl + yl - TILE_SIZE; break; case TILE_ELEMENT_SLOPE_E_CORNER_DN: quad_extra = TILE_SIZE - xl + yl; quad = yl - xl; break; case TILE_ELEMENT_SLOPE_N_CORNER_DN: quad_extra = (TILE_SIZE - xl) + (TILE_SIZE - yl); quad = TILE_SIZE - yl - xl; break; } if (extra_height) { height += quad_extra / 2; return height; } // This tile is essentially at the next height level height += LAND_HEIGHT_STEP; // so we move *down* the slope if (quad < 0) { height += quad / 2; } } // Valleys if ((slope == TILE_ELEMENT_SLOPE_W_E_VALLEY) || (slope == TILE_ELEMENT_SLOPE_N_S_VALLEY)) { switch (slope) { case TILE_ELEMENT_SLOPE_W_E_VALLEY: quad = std::abs(xl - yl); break; case TILE_ELEMENT_SLOPE_N_S_VALLEY: quad = std::abs(xl + yl - TILE_SIZE); break; } height += quad / 2; } return height; } int16_t TileElementWaterHeight(const CoordsXY& loc) { // Off the map if (!MapIsLocationValid(loc)) return 0; // Get the surface element for the tile auto surfaceElement = MapGetSurfaceElementAt(loc); if (surfaceElement == nullptr) { return 0; } return surfaceElement->GetWaterHeight(); } /** * Checks if the tile at coordinate at height counts as connected. * @return 1 if connected, 0 otherwise */ bool MapCoordIsConnected(const TileCoordsXYZ& loc, uint8_t faceDirection) { TileElement* tileElement = MapGetFirstElementAt(loc); if (tileElement == nullptr) return false; do { if (tileElement->GetType() != TileElementType::Path) continue; uint8_t slopeDirection = tileElement->AsPath()->GetSlopeDirection(); if (tileElement->AsPath()->IsSloped()) { if (slopeDirection == faceDirection) { if (loc.z == tileElement->BaseHeight + 2) return true; } else if (DirectionReverse(slopeDirection) == faceDirection && loc.z == tileElement->BaseHeight) { return true; } } else { if (loc.z == tileElement->BaseHeight && (tileElement->AsPath()->GetEdges() & (1 << faceDirection))) return true; } } while (!(tileElement++)->IsLastForTile()); return false; } /** * * rct2: 0x006A876D */ void MapUpdatePathWideFlags() { PROFILED_FUNCTION(); if (gScreenFlags & (SCREEN_FLAGS_TRACK_DESIGNER | SCREEN_FLAGS_TRACK_MANAGER)) { return; } // Presumably update_path_wide_flags is too computationally expensive to call for every // tile every update, so gWidePathTileLoopX and gWidePathTileLoopY store the x and y // progress. A maximum of 128 calls is done per update. CoordsXY& loopPosition = GetGameState().WidePathTileLoopPosition; for (int32_t i = 0; i < 128; i++) { FootpathUpdatePathWideFlags(loopPosition); // Next x, y tile loopPosition.x += COORDS_XY_STEP; if (loopPosition.x >= MAXIMUM_MAP_SIZE_BIG) { loopPosition.x = 0; loopPosition.y += COORDS_XY_STEP; if (loopPosition.y >= MAXIMUM_MAP_SIZE_BIG) { loopPosition.y = 0; } } } } /** * * rct2: 0x006A7B84 */ int32_t MapHeightFromSlope(const CoordsXY& coords, int32_t slopeDirection, bool isSloped) { if (!isSloped) return 0; switch (slopeDirection % NumOrthogonalDirections) { case TILE_ELEMENT_DIRECTION_WEST: return (31 - (coords.x & 31)) / 2; case TILE_ELEMENT_DIRECTION_NORTH: return (coords.y & 31) / 2; case TILE_ELEMENT_DIRECTION_EAST: return (coords.x & 31) / 2; case TILE_ELEMENT_DIRECTION_SOUTH: return (31 - (coords.y & 31)) / 2; } return 0; } bool MapIsLocationValid(const CoordsXY& coords) { const bool is_x_valid = coords.x < MAXIMUM_MAP_SIZE_BIG && coords.x >= 0; const bool is_y_valid = coords.y < MAXIMUM_MAP_SIZE_BIG && coords.y >= 0; return is_x_valid && is_y_valid; } bool MapIsEdge(const CoordsXY& coords) { auto mapSizeUnits = GetMapSizeUnits(); return (coords.x < 32 || coords.y < 32 || coords.x >= mapSizeUnits.x || coords.y >= mapSizeUnits.y); } bool MapCanBuildAt(const CoordsXYZ& loc) { if (gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) return true; if (GetGameState().Cheats.SandboxMode) return true; if (MapIsLocationOwned(loc)) return true; return false; } /** * * rct2: 0x00664F72 */ bool MapIsLocationOwned(const CoordsXYZ& loc) { // This check is to avoid throwing lots of messages in logs. if (MapIsLocationValid(loc)) { auto* surfaceElement = MapGetSurfaceElementAt(loc); if (surfaceElement != nullptr) { if (surfaceElement->GetOwnership() & OWNERSHIP_OWNED) return true; if (surfaceElement->GetOwnership() & OWNERSHIP_CONSTRUCTION_RIGHTS_OWNED) { if (loc.z < surfaceElement->GetBaseZ() || loc.z >= surfaceElement->GetBaseZ() + ConstructionRightsClearanceBig) return true; } } } return false; } /** * * rct2: 0x00664F2C */ bool MapIsLocationInPark(const CoordsXY& coords) { if (MapIsLocationValid(coords)) { auto surfaceElement = MapGetSurfaceElementAt(coords); if (surfaceElement == nullptr) return false; if (surfaceElement->GetOwnership() & OWNERSHIP_OWNED) return true; } return false; } bool MapIsLocationOwnedOrHasRights(const CoordsXY& loc) { if (MapIsLocationValid(loc)) { auto surfaceElement = MapGetSurfaceElementAt(loc); if (surfaceElement == nullptr) { return false; } if (surfaceElement->GetOwnership() & OWNERSHIP_OWNED) return true; if (surfaceElement->GetOwnership() & OWNERSHIP_CONSTRUCTION_RIGHTS_OWNED) return true; } return false; } int32_t MapGetCornerHeight(int32_t z, int32_t slope, int32_t direction) { switch (direction) { case 0: if (slope & TILE_ELEMENT_SLOPE_N_CORNER_UP) { z += 2; if (slope == (TILE_ELEMENT_SLOPE_S_CORNER_DN | TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT)) { z += 2; } } break; case 1: if (slope & TILE_ELEMENT_SLOPE_E_CORNER_UP) { z += 2; if (slope == (TILE_ELEMENT_SLOPE_W_CORNER_DN | TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT)) { z += 2; } } break; case 2: if (slope & TILE_ELEMENT_SLOPE_S_CORNER_UP) { z += 2; if (slope == (TILE_ELEMENT_SLOPE_N_CORNER_DN | TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT)) { z += 2; } } break; case 3: if (slope & TILE_ELEMENT_SLOPE_W_CORNER_UP) { z += 2; if (slope == (TILE_ELEMENT_SLOPE_E_CORNER_DN | TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT)) { z += 2; } } break; } return z; } int32_t TileElementGetCornerHeight(const SurfaceElement* surfaceElement, int32_t direction) { int32_t z = surfaceElement->BaseHeight; int32_t slope = surfaceElement->GetSlope(); return MapGetCornerHeight(z, slope, direction); } uint8_t MapGetLowestLandHeight(const MapRange& range) { auto mapSizeMax = GetMapSizeMaxXY(); MapRange validRange = { std::max(range.GetLeft(), 32), std::max(range.GetTop(), 32), std::min(range.GetRight(), mapSizeMax.x), std::min(range.GetBottom(), mapSizeMax.y) }; uint8_t min_height = 0xFF; for (int32_t yi = validRange.GetTop(); yi <= validRange.GetBottom(); yi += COORDS_XY_STEP) { for (int32_t xi = validRange.GetLeft(); xi <= validRange.GetRight(); xi += COORDS_XY_STEP) { auto* surfaceElement = MapGetSurfaceElementAt(CoordsXY{ xi, yi }); if (surfaceElement != nullptr && min_height > surfaceElement->BaseHeight) { if (!(gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) && !GetGameState().Cheats.SandboxMode) { if (!MapIsLocationInPark(CoordsXY{ xi, yi })) { continue; } } min_height = surfaceElement->BaseHeight; } } } return min_height; } uint8_t MapGetHighestLandHeight(const MapRange& range) { auto mapSizeMax = GetMapSizeMaxXY(); MapRange validRange = { std::max(range.GetLeft(), 32), std::max(range.GetTop(), 32), std::min(range.GetRight(), mapSizeMax.x), std::min(range.GetBottom(), mapSizeMax.y) }; uint8_t max_height = 0; for (int32_t yi = validRange.GetTop(); yi <= validRange.GetBottom(); yi += COORDS_XY_STEP) { for (int32_t xi = validRange.GetLeft(); xi <= validRange.GetRight(); xi += COORDS_XY_STEP) { auto* surfaceElement = MapGetSurfaceElementAt(CoordsXY{ xi, yi }); if (surfaceElement != nullptr) { if (!(gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) && !GetGameState().Cheats.SandboxMode) { if (!MapIsLocationInPark(CoordsXY{ xi, yi })) { continue; } } uint8_t BaseHeight = surfaceElement->BaseHeight; if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_ALL_CORNERS_UP) BaseHeight += 2; if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT) BaseHeight += 2; if (max_height < BaseHeight) max_height = BaseHeight; } } } return max_height; } bool MapIsLocationAtEdge(const CoordsXY& loc) { return loc.x < 32 || loc.y < 32 || loc.x >= (MAXIMUM_TILE_START_XY) || loc.y >= (MAXIMUM_TILE_START_XY); } /** * * rct2: 0x0068B280 */ void TileElementRemove(TileElement* tileElement) { // Replace Nth element by (N+1)th element. // This loop will make tileElement point to the old last element position, // after copy it to it's new position if (!tileElement->IsLastForTile()) { do { *tileElement = *(tileElement + 1); } while (!(++tileElement)->IsLastForTile()); } // Mark the latest element with the last element flag. (tileElement - 1)->SetLastForTile(true); tileElement->BaseHeight = MAX_ELEMENT_HEIGHT; _tileElementsInUse--; auto& gameState = GetGameState(); if (tileElement == &gameState.TileElements.back()) { gameState.TileElements.pop_back(); } } /** * * rct2: 0x00675A8E */ void MapRemoveAllRides() { TileElementIterator it; TileElementIteratorBegin(&it); do { switch (it.element->GetType()) { case TileElementType::Path: if (it.element->AsPath()->IsQueue()) { it.element->AsPath()->SetHasQueueBanner(false); it.element->AsPath()->SetRideIndex(RideId::GetNull()); } break; case TileElementType::Entrance: if (it.element->AsEntrance()->GetEntranceType() == ENTRANCE_TYPE_PARK_ENTRANCE) break; [[fallthrough]]; case TileElementType::Track: FootpathQueueChainReset(); FootpathRemoveEdgesAt(TileCoordsXY{ it.x, it.y }.ToCoordsXY(), it.element); TileElementRemove(it.element); TileElementIteratorRestartForTile(&it); break; default: break; } } while (TileElementIteratorNext(&it)); } /** * * rct2: 0x0068AB1B */ void MapInvalidateMapSelectionTiles() { if (!(gMapSelectFlags & MAP_SELECT_FLAG_ENABLE_CONSTRUCT)) return; for (const auto& position : gMapSelectionTiles) MapInvalidateTileFull(position); } static void MapGetBoundingBox(const MapRange& _range, int32_t* left, int32_t* top, int32_t* right, int32_t* bottom) { uint32_t rotation = GetCurrentRotation(); const std::array corners{ CoordsXY{ _range.GetLeft(), _range.GetTop() }, CoordsXY{ _range.GetRight(), _range.GetTop() }, CoordsXY{ _range.GetRight(), _range.GetBottom() }, CoordsXY{ _range.GetLeft(), _range.GetBottom() }, }; *left = std::numeric_limits::max(); *top = std::numeric_limits::max(); *right = std::numeric_limits::min(); *bottom = std::numeric_limits::min(); for (const auto& corner : corners) { auto screenCoord = Translate3DTo2D(rotation, corner); if (screenCoord.x < *left) *left = screenCoord.x; if (screenCoord.x > *right) *right = screenCoord.x; if (screenCoord.y > *bottom) *bottom = screenCoord.y; if (screenCoord.y < *top) *top = screenCoord.y; } } /** * * rct2: 0x0068AAE1 */ void MapInvalidateSelectionRect() { int32_t x0, y0, x1, y1, left, right, top, bottom; if (!(gMapSelectFlags & MAP_SELECT_FLAG_ENABLE)) return; x0 = gMapSelectPositionA.x + 16; y0 = gMapSelectPositionA.y + 16; x1 = gMapSelectPositionB.x + 16; y1 = gMapSelectPositionB.y + 16; MapGetBoundingBox({ x0, y0, x1, y1 }, &left, &top, &right, &bottom); left -= 32; right += 32; bottom += 32; top -= 32 + 2080; ViewportsInvalidate({ { left, top }, { right, bottom } }); } static size_t CountElementsOnTile(const CoordsXY& loc) { size_t count = 0; auto* element = _tileIndex.GetFirstElementAt(TileCoordsXY(loc)); do { count++; } while (!(element++)->IsLastForTile()); return count; } static TileElement* AllocateTileElements(size_t numElementsOnTile, size_t numNewElements) { if (!MapCheckFreeElementsAndReorganise(numElementsOnTile, numNewElements)) { LOG_ERROR("Cannot insert new element"); return nullptr; } auto& gameState = GetGameState(); auto oldSize = gameState.TileElements.size(); gameState.TileElements.resize(gameState.TileElements.size() + numElementsOnTile + numNewElements); _tileElementsInUse += numNewElements; return &gameState.TileElements[oldSize]; } /** * * rct2: 0x0068B1F6 */ TileElement* TileElementInsert(const CoordsXYZ& loc, int32_t occupiedQuadrants, TileElementType type) { const auto& tileLoc = TileCoordsXYZ(loc); auto numElementsOnTileOld = CountElementsOnTile(loc); auto* newTileElement = AllocateTileElements(numElementsOnTileOld, 1); auto* originalTileElement = _tileIndex.GetFirstElementAt(tileLoc); if (newTileElement == nullptr) { return nullptr; } // Set tile index pointer to point to new element block _tileIndex.SetTile(tileLoc, newTileElement); bool isLastForTile = false; if (originalTileElement == nullptr) { isLastForTile = true; } else { // Copy all elements that are below the insert height while (loc.z >= originalTileElement->GetBaseZ()) { // Copy over map element *newTileElement = *originalTileElement; originalTileElement->BaseHeight = MAX_ELEMENT_HEIGHT; originalTileElement++; newTileElement++; if ((newTileElement - 1)->IsLastForTile()) { // No more elements above the insert element (newTileElement - 1)->SetLastForTile(false); isLastForTile = true; break; } } } // Insert new map element auto* insertedElement = newTileElement; newTileElement->Type = 0; newTileElement->SetType(type); newTileElement->SetBaseZ(loc.z); newTileElement->Flags = 0; newTileElement->SetLastForTile(isLastForTile); newTileElement->SetOccupiedQuadrants(occupiedQuadrants); newTileElement->SetClearanceZ(loc.z); newTileElement->Owner = 0; std::memset(&newTileElement->Pad05, 0, sizeof(newTileElement->Pad05)); std::memset(&newTileElement->Pad08, 0, sizeof(newTileElement->Pad08)); newTileElement++; // Insert rest of map elements above insert height if (!isLastForTile) { do { // Copy over map element *newTileElement = *originalTileElement; originalTileElement->BaseHeight = MAX_ELEMENT_HEIGHT; originalTileElement++; newTileElement++; } while (!((newTileElement - 1)->IsLastForTile())); } return insertedElement; } /** * Updates grass length, scenery age and jumping fountains. * * rct2: 0x006646E1 */ void MapUpdateTiles() { PROFILED_FUNCTION(); int32_t ignoreScreenFlags = SCREEN_FLAGS_SCENARIO_EDITOR | SCREEN_FLAGS_TRACK_DESIGNER | SCREEN_FLAGS_TRACK_MANAGER; if (gScreenFlags & ignoreScreenFlags) return; auto& gameState = GetGameState(); // Update 43 more tiles (for each 256x256 block) for (int32_t j = 0; j < 43; j++) { int32_t x = 0; int32_t y = 0; uint16_t interleaved_xy = gameState.GrassSceneryTileLoopPosition; for (int32_t i = 0; i < 8; i++) { x = (x << 1) | (interleaved_xy & 1); interleaved_xy >>= 1; y = (y << 1) | (interleaved_xy & 1); interleaved_xy >>= 1; } // Repeat for each 256x256 block on the map for (int32_t blockY = 0; blockY < gameState.MapSize.y; blockY += 256) { for (int32_t blockX = 0; blockX < gameState.MapSize.x; blockX += 256) { auto mapPos = TileCoordsXY{ blockX + x, blockY + y }.ToCoordsXY(); if (MapIsEdge(mapPos)) continue; auto* surfaceElement = MapGetSurfaceElementAt(mapPos); if (surfaceElement != nullptr) { surfaceElement->UpdateGrassLength(mapPos); SceneryUpdateTile(mapPos); } } } gameState.GrassSceneryTileLoopPosition++; } } void MapRemoveProvisionalElements() { PROFILED_FUNCTION(); if (gProvisionalFootpath.Flags & PROVISIONAL_PATH_FLAG_1) { FootpathProvisionalRemove(); gProvisionalFootpath.Flags |= PROVISIONAL_PATH_FLAG_1; } if (WindowFindByClass(WindowClass::RideConstruction) != nullptr) { RideRemoveProvisionalTrackPiece(); RideEntranceExitRemoveGhost(); } // This is in non performant so only make network games suffer for it // non networked games do not need this as its to prevent desyncs. if ((NetworkGetMode() != NETWORK_MODE_NONE) && WindowFindByClass(WindowClass::TrackDesignPlace) != nullptr) { auto intent = Intent(INTENT_ACTION_TRACK_DESIGN_REMOVE_PROVISIONAL); ContextBroadcastIntent(&intent); } } void MapRestoreProvisionalElements() { PROFILED_FUNCTION(); if (gProvisionalFootpath.Flags & PROVISIONAL_PATH_FLAG_1) { gProvisionalFootpath.Flags &= ~PROVISIONAL_PATH_FLAG_1; FootpathProvisionalSet( gProvisionalFootpath.SurfaceIndex, gProvisionalFootpath.RailingsIndex, gProvisionalFootpath.Position, gProvisionalFootpath.Slope, gProvisionalFootpath.ConstructFlags); } if (WindowFindByClass(WindowClass::RideConstruction) != nullptr) { RideRestoreProvisionalTrackPiece(); RideEntranceExitPlaceProvisionalGhost(); } // This is in non performant so only make network games suffer for it // non networked games do not need this as its to prevent desyncs. if ((NetworkGetMode() != NETWORK_MODE_NONE) && WindowFindByClass(WindowClass::TrackDesignPlace) != nullptr) { auto intent = Intent(INTENT_ACTION_TRACK_DESIGN_RESTORE_PROVISIONAL); ContextBroadcastIntent(&intent); } } /** * Removes elements that are out of the map size range and crops the park perimeter. * rct2: 0x0068ADBC */ void MapRemoveOutOfRangeElements() { auto mapSizeMax = GetMapSizeMaxXY(); // Ensure that we can remove elements // // NOTE: This is only a workaround for non-networked games. // Map resize has to become its own Game Action to properly solve this issue. // bool buildState = GetGameState().Cheats.BuildInPauseMode; GetGameState().Cheats.BuildInPauseMode = true; for (int32_t y = MAXIMUM_MAP_SIZE_BIG - COORDS_XY_STEP; y >= 0; y -= COORDS_XY_STEP) { for (int32_t x = MAXIMUM_MAP_SIZE_BIG - COORDS_XY_STEP; x >= 0; x -= COORDS_XY_STEP) { if (x == 0 || y == 0 || x >= mapSizeMax.x || y >= mapSizeMax.y) { // Note this purposely does not use LandSetRightsAction as X Y coordinates are outside of normal range. auto surfaceElement = MapGetSurfaceElementAt(CoordsXY{ x, y }); if (surfaceElement != nullptr) { surfaceElement->SetOwnership(OWNERSHIP_UNOWNED); Park::UpdateFencesAroundTile({ x, y }); } ClearElementsAt({ x, y }); } } } // Reset cheat state GetGameState().Cheats.BuildInPauseMode = buildState; } static void MapExtendBoundarySurfaceExtendTile(const SurfaceElement& sourceTile, SurfaceElement& destTile) { destTile.SetSurfaceObjectIndex(sourceTile.GetSurfaceObjectIndex()); destTile.SetEdgeObjectIndex(sourceTile.GetEdgeObjectIndex()); destTile.SetGrassLength(sourceTile.GetGrassLength()); destTile.SetOwnership(OWNERSHIP_UNOWNED); destTile.SetWaterHeight(sourceTile.GetWaterHeight()); auto z = sourceTile.BaseHeight; auto slope = sourceTile.GetSlope() & TILE_ELEMENT_SLOPE_NW_SIDE_UP; if (slope == TILE_ELEMENT_SLOPE_NW_SIDE_UP) { z += 2; slope = TILE_ELEMENT_SLOPE_FLAT; if (sourceTile.GetSlope() & TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT) { slope = TILE_ELEMENT_SLOPE_N_CORNER_UP; if (sourceTile.GetSlope() & TILE_ELEMENT_SLOPE_S_CORNER_UP) { slope = TILE_ELEMENT_SLOPE_W_CORNER_UP; if (sourceTile.GetSlope() & TILE_ELEMENT_SLOPE_E_CORNER_UP) { slope = TILE_ELEMENT_SLOPE_FLAT; } } } } if (slope & TILE_ELEMENT_SLOPE_N_CORNER_UP) slope |= TILE_ELEMENT_SLOPE_E_CORNER_UP; if (slope & TILE_ELEMENT_SLOPE_W_CORNER_UP) slope |= TILE_ELEMENT_SLOPE_S_CORNER_UP; destTile.SetSlope(slope); destTile.BaseHeight = z; destTile.ClearanceHeight = z; } /** * Copies the terrain and slope from the Y edge of the map to the new tiles. Used when increasing the size of the map. */ void MapExtendBoundarySurfaceY() { auto y = GetGameState().MapSize.y - 2; for (auto x = 0; x < kMaximumMapSizeTechnical; x++) { auto existingTileElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y - 1 }); auto newTileElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); if (existingTileElement != nullptr && newTileElement != nullptr) { MapExtendBoundarySurfaceExtendTile(*existingTileElement, *newTileElement); } Park::UpdateFences({ x << 5, y << 5 }); } } /** * Copies the terrain and slope from the X edge of the map to the new tiles. Used when increasing the size of the map. */ void MapExtendBoundarySurfaceX() { auto x = GetGameState().MapSize.x - 2; for (auto y = 0; y < kMaximumMapSizeTechnical; y++) { auto existingTileElement = MapGetSurfaceElementAt(TileCoordsXY{ x - 1, y }); auto newTileElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); if (existingTileElement != nullptr && newTileElement != nullptr) { MapExtendBoundarySurfaceExtendTile(*existingTileElement, *newTileElement); } Park::UpdateFences({ x << 5, y << 5 }); } } /** * Clears the provided element properly from a certain tile, and updates * the pointer (when needed) passed to this function to point to the next element. */ static void ClearElementAt(const CoordsXY& loc, TileElement** elementPtr) { TileElement* element = *elementPtr; switch (element->GetType()) { case TileElementType::Surface: element->BaseHeight = kMinimumLandHeight; element->ClearanceHeight = kMinimumLandHeight; element->Owner = 0; element->AsSurface()->SetSlope(TILE_ELEMENT_SLOPE_FLAT); element->AsSurface()->SetSurfaceObjectIndex(0); element->AsSurface()->SetEdgeObjectIndex(0); element->AsSurface()->SetGrassLength(GRASS_LENGTH_CLEAR_0); element->AsSurface()->SetOwnership(OWNERSHIP_UNOWNED); element->AsSurface()->SetParkFences(0); element->AsSurface()->SetWaterHeight(0); // Because this element is not completely removed, the pointer must be updated manually // The rest of the elements are removed from the array, so the pointer doesn't need to be updated. (*elementPtr)++; break; case TileElementType::Entrance: { int32_t rotation = element->GetDirectionWithOffset(1); auto seqLoc = loc; switch (element->AsEntrance()->GetSequenceIndex()) { case 1: seqLoc += CoordsDirectionDelta[rotation]; break; case 2: seqLoc -= CoordsDirectionDelta[rotation]; break; } auto parkEntranceRemoveAction = ParkEntranceRemoveAction(CoordsXYZ{ seqLoc, element->GetBaseZ() }); auto result = GameActions::ExecuteNested(&parkEntranceRemoveAction); // If asking nicely did not work, forcibly remove this to avoid an infinite loop. if (result.Error != GameActions::Status::Ok) { TileElementRemove(element); } break; } case TileElementType::Wall: { CoordsXYZD wallLocation = { loc.x, loc.y, element->GetBaseZ(), element->GetDirection() }; auto wallRemoveAction = WallRemoveAction(wallLocation); auto result = GameActions::ExecuteNested(&wallRemoveAction); // If asking nicely did not work, forcibly remove this to avoid an infinite loop. if (result.Error != GameActions::Status::Ok) { TileElementRemove(element); } } break; case TileElementType::LargeScenery: { auto removeSceneryAction = LargeSceneryRemoveAction( { loc.x, loc.y, element->GetBaseZ(), element->GetDirection() }, element->AsLargeScenery()->GetSequenceIndex()); auto result = GameActions::ExecuteNested(&removeSceneryAction); // If asking nicely did not work, forcibly remove this to avoid an infinite loop. if (result.Error != GameActions::Status::Ok) { TileElementRemove(element); } } break; case TileElementType::Banner: { auto bannerRemoveAction = BannerRemoveAction( { loc.x, loc.y, element->GetBaseZ(), element->AsBanner()->GetPosition() }); auto result = GameActions::ExecuteNested(&bannerRemoveAction); // If asking nicely did not work, forcibly remove this to avoid an infinite loop. if (result.Error != GameActions::Status::Ok) { TileElementRemove(element); } break; } default: TileElementRemove(element); break; } } /** * Clears all elements properly from a certain tile. * rct2: 0x0068AE2A */ static void ClearElementsAt(const CoordsXY& loc) { auto& gameState = GetGameState(); // Remove the spawn point (if there is one in the current tile) gameState.PeepSpawns.erase( std::remove_if( gameState.PeepSpawns.begin(), gameState.PeepSpawns.end(), [loc](const CoordsXY& spawn) { return spawn.ToTileStart() == loc.ToTileStart(); }), gameState.PeepSpawns.end()); TileElement* tileElement = MapGetFirstElementAt(loc); if (tileElement == nullptr) return; // Remove all elements except the last one while (!tileElement->IsLastForTile()) ClearElementAt(loc, &tileElement); // Remove the last element ClearElementAt(loc, &tileElement); } int32_t MapGetHighestZ(const CoordsXY& loc) { auto surfaceElement = MapGetSurfaceElementAt(loc); if (surfaceElement == nullptr) return -1; auto z = surfaceElement->GetBaseZ(); // Raise z so that is above highest point of land and water on tile if ((surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_ALL_CORNERS_UP) != TILE_ELEMENT_SLOPE_FLAT) z += LAND_HEIGHT_STEP; if ((surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT) != 0) z += LAND_HEIGHT_STEP; z = std::max(z, surfaceElement->GetWaterHeight()); return z; } LargeSceneryElement* MapGetLargeScenerySegment(const CoordsXYZD& sceneryPos, int32_t sequence) { TileElement* tileElement = MapGetFirstElementAt(sceneryPos); if (tileElement == nullptr) { return nullptr; } auto sceneryTilePos = TileCoordsXYZ{ sceneryPos }; do { if (tileElement->GetType() != TileElementType::LargeScenery) continue; if (tileElement->BaseHeight != sceneryTilePos.z) continue; if (tileElement->AsLargeScenery()->GetSequenceIndex() != sequence) continue; if ((tileElement->GetDirection()) != sceneryPos.direction) continue; return tileElement->AsLargeScenery(); } while (!(tileElement++)->IsLastForTile()); return nullptr; } EntranceElement* MapGetParkEntranceElementAt(const CoordsXYZ& entranceCoords, bool ghost) { auto entranceTileCoords = TileCoordsXYZ(entranceCoords); TileElement* tileElement = MapGetFirstElementAt(entranceCoords); if (tileElement != nullptr) { do { if (tileElement->GetType() != TileElementType::Entrance) continue; if (tileElement->BaseHeight != entranceTileCoords.z) continue; if (tileElement->AsEntrance()->GetEntranceType() != ENTRANCE_TYPE_PARK_ENTRANCE) continue; if (!ghost && tileElement->IsGhost()) continue; return tileElement->AsEntrance(); } while (!(tileElement++)->IsLastForTile()); } return nullptr; } EntranceElement* MapGetRideEntranceElementAt(const CoordsXYZ& entranceCoords, bool ghost) { auto entranceTileCoords = TileCoordsXYZ{ entranceCoords }; TileElement* tileElement = MapGetFirstElementAt(entranceCoords); if (tileElement != nullptr) { do { if (tileElement->GetType() != TileElementType::Entrance) continue; if (tileElement->BaseHeight != entranceTileCoords.z) continue; if (tileElement->AsEntrance()->GetEntranceType() != ENTRANCE_TYPE_RIDE_ENTRANCE) continue; if (!ghost && tileElement->IsGhost()) continue; return tileElement->AsEntrance(); } while (!(tileElement++)->IsLastForTile()); } return nullptr; } EntranceElement* MapGetRideExitElementAt(const CoordsXYZ& exitCoords, bool ghost) { auto exitTileCoords = TileCoordsXYZ{ exitCoords }; TileElement* tileElement = MapGetFirstElementAt(exitCoords); if (tileElement != nullptr) { do { if (tileElement->GetType() != TileElementType::Entrance) continue; if (tileElement->BaseHeight != exitTileCoords.z) continue; if (tileElement->AsEntrance()->GetEntranceType() != ENTRANCE_TYPE_RIDE_EXIT) continue; if (!ghost && tileElement->IsGhost()) continue; return tileElement->AsEntrance(); } while (!(tileElement++)->IsLastForTile()); } return nullptr; } SmallSceneryElement* MapGetSmallSceneryElementAt(const CoordsXYZ& sceneryCoords, int32_t type, uint8_t quadrant) { auto sceneryTileCoords = TileCoordsXYZ{ sceneryCoords }; TileElement* tileElement = MapGetFirstElementAt(sceneryCoords); if (tileElement != nullptr) { do { if (tileElement->GetType() != TileElementType::SmallScenery) continue; if (tileElement->AsSmallScenery()->GetSceneryQuadrant() != quadrant) continue; if (tileElement->BaseHeight != sceneryTileCoords.z) continue; if (tileElement->AsSmallScenery()->GetEntryIndex() != type) continue; return tileElement->AsSmallScenery(); } while (!(tileElement++)->IsLastForTile()); } return nullptr; } std::optional MapLargeSceneryGetOrigin( const CoordsXYZD& sceneryPos, int32_t sequence, LargeSceneryElement** outElement) { LargeSceneryTile* tile; auto tileElement = MapGetLargeScenerySegment(sceneryPos, sequence); if (tileElement == nullptr) return std::nullopt; auto* sceneryEntry = tileElement->GetEntry(); tile = &sceneryEntry->tiles[sequence]; CoordsXY offsetPos{ tile->x_offset, tile->y_offset }; auto rotatedOffsetPos = offsetPos.Rotate(sceneryPos.direction); auto origin = CoordsXYZ{ sceneryPos.x - rotatedOffsetPos.x, sceneryPos.y - rotatedOffsetPos.y, sceneryPos.z - tile->z_offset }; if (outElement != nullptr) *outElement = tileElement; return origin; } /** * * rct2: 0x006B9B05 */ bool MapLargeScenerySignSetColour(const CoordsXYZD& signPos, int32_t sequence, uint8_t mainColour, uint8_t textColour) { LargeSceneryElement* tileElement; LargeSceneryTile *sceneryTiles, *tile; auto sceneryOrigin = MapLargeSceneryGetOrigin(signPos, sequence, &tileElement); if (!sceneryOrigin) { return false; } auto* sceneryEntry = tileElement->GetEntry(); sceneryTiles = sceneryEntry->tiles; // Iterate through each tile of the large scenery element sequence = 0; for (tile = sceneryTiles; tile->x_offset != -1; tile++, sequence++) { CoordsXY offsetPos{ tile->x_offset, tile->y_offset }; auto rotatedOffsetPos = offsetPos.Rotate(signPos.direction); auto tmpSignPos = CoordsXYZD{ sceneryOrigin->x + rotatedOffsetPos.x, sceneryOrigin->y + rotatedOffsetPos.y, sceneryOrigin->z + tile->z_offset, signPos.direction }; tileElement = MapGetLargeScenerySegment(tmpSignPos, sequence); if (tileElement != nullptr) { tileElement->SetPrimaryColour(mainColour); tileElement->SetSecondaryColour(textColour); MapInvalidateTile({ tmpSignPos, tileElement->GetBaseZ(), tileElement->GetClearanceZ() }); } } return true; } static ScreenCoordsXY Translate3DTo2D(int32_t rotation, const CoordsXY& pos) { return Translate3DTo2DWithZ(rotation, CoordsXYZ{ pos, 0 }); } 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 }; } static void MapInvalidateTileUnderZoom(int32_t x, int32_t y, int32_t z0, int32_t z1, ZoomLevel maxZoom) { if (gOpenRCT2Headless) return; ViewportsInvalidate(x, y, z0, z1, maxZoom); } /** * * rct2: 0x006EC847 */ void MapInvalidateTile(const CoordsXYRangedZ& tilePos) { MapInvalidateTileUnderZoom(tilePos.x, tilePos.y, tilePos.baseZ, tilePos.clearanceZ, ZoomLevel{ -1 }); } /** * * rct2: 0x006ECB60 */ void MapInvalidateTileZoom1(const CoordsXYRangedZ& tilePos) { MapInvalidateTileUnderZoom(tilePos.x, tilePos.y, tilePos.baseZ, tilePos.clearanceZ, ZoomLevel{ 1 }); } /** * * rct2: 0x006EC9CE */ void MapInvalidateTileZoom0(const CoordsXYRangedZ& tilePos) { MapInvalidateTileUnderZoom(tilePos.x, tilePos.y, tilePos.baseZ, tilePos.clearanceZ, ZoomLevel{ 0 }); } /** * * rct2: 0x006EC6D7 */ void MapInvalidateTileFull(const CoordsXY& tilePos) { MapInvalidateTile({ tilePos, 0, 2080 }); } void MapInvalidateElement(const CoordsXY& elementPos, TileElement* tileElement) { MapInvalidateTile({ elementPos, tileElement->GetBaseZ(), tileElement->GetClearanceZ() }); } void MapInvalidateRegion(const CoordsXY& mins, const CoordsXY& maxs) { int32_t x0, y0, x1, y1, left, right, top, bottom; x0 = mins.x + 16; y0 = mins.y + 16; x1 = maxs.x + 16; y1 = maxs.y + 16; MapGetBoundingBox({ x0, y0, x1, y1 }, &left, &top, &right, &bottom); left -= 32; right += 32; bottom += 32; top -= 32 + 2080; ViewportsInvalidate({ { left, top }, { right, bottom } }); } int32_t MapGetTileSide(const CoordsXY& mapPos) { int32_t subMapX = mapPos.x & (32 - 1); int32_t subMapY = mapPos.y & (32 - 1); return (subMapX < subMapY) ? ((subMapX + subMapY) < 32 ? 0 : 1) : ((subMapX + subMapY) < 32 ? 3 : 2); } int32_t MapGetTileQuadrant(const CoordsXY& mapPos) { int32_t subMapX = mapPos.x & (32 - 1); int32_t subMapY = mapPos.y & (32 - 1); return (subMapX > 16) ? (subMapY < 16 ? 1 : 0) : (subMapY < 16 ? 2 : 3); } /** * * rct2: 0x00693BFF */ bool MapSurfaceIsBlocked(const CoordsXY& mapCoords) { if (!MapIsLocationValid(mapCoords)) return true; auto surfaceElement = MapGetSurfaceElementAt(mapCoords); if (surfaceElement == nullptr) { return true; } if (surfaceElement->GetWaterHeight() > surfaceElement->GetBaseZ()) return true; int16_t base_z = surfaceElement->BaseHeight; int16_t clear_z = surfaceElement->BaseHeight + 2; if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT) clear_z += 2; auto tileElement = reinterpret_cast(surfaceElement); while (!(tileElement++)->IsLastForTile()) { if (clear_z >= tileElement->ClearanceHeight) continue; if (base_z < tileElement->BaseHeight) continue; if (tileElement->GetType() == TileElementType::Path || tileElement->GetType() == TileElementType::Wall) continue; if (tileElement->GetType() != TileElementType::SmallScenery) return true; auto* sceneryEntry = tileElement->AsSmallScenery()->GetEntry(); if (sceneryEntry == nullptr) { return false; } if (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_FULL_TILE)) return true; } return false; } /* Clears all map elements, to be used before generating a new map */ void MapClearAllElements() { for (int32_t y = 0; y < MAXIMUM_MAP_SIZE_BIG; y += COORDS_XY_STEP) { for (int32_t x = 0; x < MAXIMUM_MAP_SIZE_BIG; x += COORDS_XY_STEP) { ClearElementsAt({ x, y }); } } } /** * Gets the track element at x, y, z. * @param x x units, not tiles. * @param y y units, not tiles. * @param z Base height. */ TrackElement* MapGetTrackElementAt(const CoordsXYZ& trackPos) { TileElement* tileElement = MapGetFirstElementAt(trackPos); if (tileElement == nullptr) return nullptr; do { if (tileElement->GetType() != TileElementType::Track) continue; if (tileElement->GetBaseZ() != trackPos.z) continue; return tileElement->AsTrack(); } while (!(tileElement++)->IsLastForTile()); return nullptr; } /** * Gets the track element at x, y, z that is the given track type. * @param x x units, not tiles. * @param y y units, not tiles. * @param z Base height. */ TileElement* MapGetTrackElementAtOfType(const CoordsXYZ& trackPos, track_type_t trackType) { TileElement* tileElement = MapGetFirstElementAt(trackPos); if (tileElement == nullptr) return nullptr; auto trackTilePos = TileCoordsXYZ{ trackPos }; do { if (tileElement->GetType() != TileElementType::Track) continue; if (tileElement->BaseHeight != trackTilePos.z) continue; if (tileElement->AsTrack()->GetTrackType() != trackType) continue; return tileElement; } while (!(tileElement++)->IsLastForTile()); return nullptr; } /** * Gets the track element at x, y, z that is the given track type and sequence. * @param x x units, not tiles. * @param y y units, not tiles. * @param z Base height. */ TileElement* MapGetTrackElementAtOfTypeSeq(const CoordsXYZ& trackPos, track_type_t trackType, int32_t sequence) { TileElement* tileElement = MapGetFirstElementAt(trackPos); auto trackTilePos = TileCoordsXYZ{ trackPos }; do { if (tileElement == nullptr) break; if (tileElement->GetType() != TileElementType::Track) continue; if (tileElement->BaseHeight != trackTilePos.z) continue; if (tileElement->AsTrack()->GetTrackType() != trackType) continue; if (tileElement->AsTrack()->GetSequenceIndex() != sequence) continue; return tileElement; } while (!(tileElement++)->IsLastForTile()); return nullptr; } TrackElement* MapGetTrackElementAtOfType(const CoordsXYZD& location, track_type_t trackType) { auto tileElement = MapGetFirstElementAt(location); if (tileElement != nullptr) { do { auto trackElement = tileElement->AsTrack(); if (trackElement != nullptr) { if (trackElement->GetBaseZ() != location.z) continue; if (trackElement->GetDirection() != location.direction) continue; if (trackElement->GetTrackType() != trackType) continue; return trackElement; } } while (!(tileElement++)->IsLastForTile()); } return nullptr; } TrackElement* MapGetTrackElementAtOfTypeSeq(const CoordsXYZD& location, track_type_t trackType, int32_t sequence) { auto tileElement = MapGetFirstElementAt(location); if (tileElement != nullptr) { do { auto trackElement = tileElement->AsTrack(); if (trackElement != nullptr) { if (trackElement->GetBaseZ() != location.z) continue; if (trackElement->GetDirection() != location.direction) continue; if (trackElement->GetTrackType() != trackType) continue; if (trackElement->GetSequenceIndex() != sequence) continue; return trackElement; } } while (!(tileElement++)->IsLastForTile()); } return nullptr; } /** * Gets the track element at x, y, z that is the given track type and sequence. * @param x x units, not tiles. * @param y y units, not tiles. * @param z Base height. */ TileElement* MapGetTrackElementAtOfTypeFromRide(const CoordsXYZ& trackPos, track_type_t trackType, RideId rideIndex) { TileElement* tileElement = MapGetFirstElementAt(trackPos); if (tileElement == nullptr) return nullptr; auto trackTilePos = TileCoordsXYZ{ trackPos }; do { if (tileElement->GetType() != TileElementType::Track) continue; if (tileElement->BaseHeight != trackTilePos.z) continue; if (tileElement->AsTrack()->GetRideIndex() != rideIndex) continue; if (tileElement->AsTrack()->GetTrackType() != trackType) continue; return tileElement; } while (!(tileElement++)->IsLastForTile()); return nullptr; }; /** * Gets the track element at x, y, z that is the given track type and sequence. * @param x x units, not tiles. * @param y y units, not tiles. * @param z Base height. */ TileElement* MapGetTrackElementAtFromRide(const CoordsXYZ& trackPos, RideId rideIndex) { TileElement* tileElement = MapGetFirstElementAt(trackPos); if (tileElement == nullptr) return nullptr; auto trackTilePos = TileCoordsXYZ{ trackPos }; do { if (tileElement->GetType() != TileElementType::Track) continue; if (tileElement->BaseHeight != trackTilePos.z) continue; if (tileElement->AsTrack()->GetRideIndex() != rideIndex) continue; return tileElement; } while (!(tileElement++)->IsLastForTile()); return nullptr; }; /** * Gets the track element at x, y, z that is the given track type and sequence. * @param x x units, not tiles. * @param y y units, not tiles. * @param z Base height. * @param direction The direction (0 - 3). */ TileElement* MapGetTrackElementAtWithDirectionFromRide(const CoordsXYZD& trackPos, RideId rideIndex) { TileElement* tileElement = MapGetFirstElementAt(trackPos); if (tileElement == nullptr) return nullptr; auto trackTilePos = TileCoordsXYZ{ trackPos }; do { if (tileElement->GetType() != TileElementType::Track) continue; if (tileElement->BaseHeight != trackTilePos.z) continue; if (tileElement->AsTrack()->GetRideIndex() != rideIndex) continue; if (tileElement->GetDirection() != trackPos.direction) continue; return tileElement; } while (!(tileElement++)->IsLastForTile()); return nullptr; }; WallElement* MapGetWallElementAt(const CoordsXYRangedZ& coords) { auto tileElement = MapGetFirstElementAt(coords); if (tileElement != nullptr) { do { if (tileElement->GetType() == TileElementType::Wall && coords.baseZ < tileElement->GetClearanceZ() && coords.clearanceZ > tileElement->GetBaseZ()) { return tileElement->AsWall(); } } while (!(tileElement++)->IsLastForTile()); } return nullptr; } WallElement* MapGetWallElementAt(const CoordsXYZD& wallCoords) { auto tileWallCoords = TileCoordsXYZ(wallCoords); TileElement* tileElement = MapGetFirstElementAt(wallCoords); if (tileElement == nullptr) return nullptr; do { if (tileElement->GetType() != TileElementType::Wall) continue; if (tileElement->BaseHeight != tileWallCoords.z) continue; if (tileElement->GetDirection() != wallCoords.direction) continue; return tileElement->AsWall(); } while (!(tileElement++)->IsLastForTile()); return nullptr; } uint16_t CheckMaxAllowableLandRightsForTile(const CoordsXYZ& tileMapPos) { TileElement* tileElement = MapGetFirstElementAt(tileMapPos); uint16_t destOwnership = OWNERSHIP_OWNED; // Sometimes done deliberately. if (tileElement == nullptr) { return OWNERSHIP_OWNED; } auto tilePos = TileCoordsXYZ{ tileMapPos }; do { auto type = tileElement->GetType(); if (type == TileElementType::Path || (type == TileElementType::Entrance && tileElement->AsEntrance()->GetEntranceType() == ENTRANCE_TYPE_PARK_ENTRANCE)) { destOwnership = OWNERSHIP_CONSTRUCTION_RIGHTS_OWNED; // Do not own construction rights if too high/below surface if (tileElement->BaseHeight - ConstructionRightsClearanceSmall > tilePos.z || tileElement->BaseHeight < tilePos.z) { destOwnership = OWNERSHIP_UNOWNED; break; } } } while (!(tileElement++)->IsLastForTile()); return destOwnership; } void FixLandOwnershipTiles(std::initializer_list tiles) { FixLandOwnershipTilesWithOwnership(tiles, OWNERSHIP_AVAILABLE); } void FixLandOwnershipTilesWithOwnership(std::initializer_list tiles, uint8_t ownership, bool doNotDowngrade) { for (const TileCoordsXY* tile = tiles.begin(); tile != tiles.end(); ++tile) { auto surfaceElement = MapGetSurfaceElementAt(*tile); if (surfaceElement != nullptr) { if (doNotDowngrade && surfaceElement->GetOwnership() == OWNERSHIP_OWNED) continue; surfaceElement->SetOwnership(ownership); Park::UpdateFencesAroundTile({ (*tile).x * 32, (*tile).y * 32 }); } } } MapRange ClampRangeWithinMap(const MapRange& range) { auto mapSizeMax = GetMapSizeMaxXY(); auto aX = std::max(COORDS_XY_STEP, range.GetLeft()); auto bX = std::min(mapSizeMax.x, range.GetRight()); auto aY = std::max(COORDS_XY_STEP, range.GetTop()); auto bY = std::min(mapSizeMax.y, range.GetBottom()); MapRange validRange = MapRange{ aX, aY, bX, bY }; return validRange; }