632 lines
17 KiB
C++
632 lines
17 KiB
C++
#include "TileManager.h"
|
|
#include "../Input.h"
|
|
#include "../Interop/Interop.hpp"
|
|
#include "../Map/Map.hpp"
|
|
#include "../Ui.h"
|
|
#include "../ViewportManager.h"
|
|
|
|
using namespace OpenLoco::Interop;
|
|
|
|
namespace OpenLoco::Map::TileManager
|
|
{
|
|
|
|
#pragma pack(push, 1)
|
|
struct TileAnimation
|
|
{
|
|
uint8_t baseZ;
|
|
uint8_t type;
|
|
Map::Pos2 pos;
|
|
};
|
|
static_assert(sizeof(TileAnimation) == 6);
|
|
#pragma pack(pop)
|
|
|
|
constexpr size_t maxAnimations = 0x2000;
|
|
|
|
static loco_global<TileElement*, 0x005230C8> _elements;
|
|
static loco_global<TileElement* [0x30004], 0x00E40134> _tiles;
|
|
static loco_global<TileElement*, 0x00F00134> _elementsEnd;
|
|
static loco_global<coord_t, 0x00F24486> _mapSelectionAX;
|
|
static loco_global<coord_t, 0x00F24488> _mapSelectionBX;
|
|
static loco_global<coord_t, 0x00F2448A> _mapSelectionAY;
|
|
static loco_global<coord_t, 0x00F2448C> _mapSelectionBY;
|
|
static loco_global<uint16_t, 0x00F2448E> _word_F2448E;
|
|
static loco_global<int16_t, 0x0050A000> _adjustToolSize;
|
|
static loco_global<uint16_t, 0x00525F6C> _numAnimations;
|
|
static loco_global<TileAnimation[maxAnimations], 0x0094C6DC> _animations;
|
|
|
|
constexpr uint16_t mapSelectedTilesSize = 300;
|
|
static loco_global<Pos2[mapSelectedTilesSize], 0x00F24490> _mapSelectedTiles;
|
|
|
|
static TileElement* InvalidTile = reinterpret_cast<TileElement*>(static_cast<intptr_t>(-1));
|
|
|
|
// 0x00461179
|
|
void initialise()
|
|
{
|
|
call(0x00461179);
|
|
}
|
|
|
|
stdx::span<TileElement> getElements()
|
|
{
|
|
return stdx::span<TileElement>(_elements, getElementsEnd());
|
|
}
|
|
|
|
void setMapSelectionArea(const Pos2& locA, const Pos2& locB)
|
|
{
|
|
_mapSelectionAX = locA.x;
|
|
_mapSelectionAY = locA.y;
|
|
_mapSelectionBX = locB.x;
|
|
_mapSelectionBY = locB.y;
|
|
}
|
|
|
|
std::pair<Pos2, Pos2> getMapSelectionArea()
|
|
{
|
|
return std::make_pair(Pos2{ _mapSelectionAX, _mapSelectionAY }, Pos2{ _mapSelectionBX, _mapSelectionBY });
|
|
}
|
|
|
|
void setMapSelectionCorner(const uint8_t corner)
|
|
{
|
|
_word_F2448E = corner;
|
|
}
|
|
|
|
uint8_t getMapSelectionCorner()
|
|
{
|
|
return _word_F2448E;
|
|
}
|
|
|
|
TileElement* getElementsEnd()
|
|
{
|
|
return _elementsEnd;
|
|
}
|
|
|
|
void setElements(stdx::span<TileElement> elements)
|
|
{
|
|
TileElement* dst = _elements;
|
|
std::memset(dst, 0, maxElements * sizeof(TileElement));
|
|
std::memcpy(dst, elements.data(), elements.size_bytes());
|
|
TileManager::updateTilePointers();
|
|
}
|
|
|
|
TileElement** getElementIndex()
|
|
{
|
|
return _tiles.get();
|
|
}
|
|
|
|
Tile get(TilePos2 pos)
|
|
{
|
|
size_t index = (pos.y << 9) | pos.x;
|
|
auto data = _tiles[index];
|
|
if (data == InvalidTile)
|
|
{
|
|
data = nullptr;
|
|
}
|
|
return Tile(pos, data);
|
|
}
|
|
|
|
Tile get(Pos2 pos)
|
|
{
|
|
return get(pos.x, pos.y);
|
|
}
|
|
|
|
Tile get(coord_t x, coord_t y)
|
|
{
|
|
return get(TilePos2(x / Map::tile_size, y / Map::tile_size));
|
|
}
|
|
|
|
/**
|
|
* Return the absolute height of an element, given its (x, y) coordinates
|
|
* remember to & with 0xFFFF if you don't want water affecting results
|
|
*
|
|
* @param x @<ax>
|
|
* @param y @<cx>
|
|
* @return height @<edx>
|
|
*
|
|
* 0x00467297 rct2: 0x00662783 (numbers different)
|
|
*/
|
|
TileHeight getHeight(const Pos2& pos)
|
|
{
|
|
TileHeight height{ 16, 0 };
|
|
// Off the map
|
|
if ((unsigned)pos.x >= (Map::map_width - 1) || (unsigned)pos.y >= (Map::map_height - 1))
|
|
return height;
|
|
|
|
auto tile = TileManager::get(pos);
|
|
// Get the surface element for the tile
|
|
auto surfaceEl = tile.surface();
|
|
|
|
if (surfaceEl == nullptr)
|
|
{
|
|
return height;
|
|
}
|
|
|
|
height.waterHeight = surfaceEl->water() * 16;
|
|
height.landHeight = surfaceEl->baseZ() * 4;
|
|
|
|
const auto slope = surfaceEl->slopeCorners();
|
|
if (slope == SurfaceSlope::flat)
|
|
{
|
|
// Flat surface requires no further calculations.
|
|
return height;
|
|
}
|
|
|
|
int8_t quad = 0;
|
|
int8_t quad_extra = 0; // which quadrant the element is in?
|
|
// quad_extra is for extra height tiles
|
|
|
|
constexpr uint8_t TILE_SIZE = 31;
|
|
|
|
// Subtile coords
|
|
const auto xl = pos.x & 0x1f;
|
|
const auto yl = pos.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
|
|
switch (slope)
|
|
{
|
|
case SurfaceSlope::n_corner_up:
|
|
quad = xl + yl - TILE_SIZE;
|
|
break;
|
|
case SurfaceSlope::e_corner_up:
|
|
quad = xl - yl;
|
|
break;
|
|
case SurfaceSlope::s_corner_up:
|
|
quad = TILE_SIZE - yl - xl;
|
|
break;
|
|
case SurfaceSlope::w_corner_up:
|
|
quad = yl - xl;
|
|
break;
|
|
}
|
|
// If the element is in the quadrant with the slope, raise its height
|
|
if (quad > 0)
|
|
{
|
|
height.landHeight += quad / 2;
|
|
}
|
|
|
|
// One side up
|
|
switch (slope)
|
|
{
|
|
case SurfaceSlope::ne_side_up:
|
|
height.landHeight += xl / 2 + 1;
|
|
break;
|
|
case SurfaceSlope::se_side_up:
|
|
height.landHeight += (TILE_SIZE - yl) / 2;
|
|
break;
|
|
case SurfaceSlope::nw_side_up:
|
|
height.landHeight += yl / 2;
|
|
height.landHeight++;
|
|
break;
|
|
case SurfaceSlope::sw_side_up:
|
|
height.landHeight += (TILE_SIZE - xl) / 2;
|
|
break;
|
|
}
|
|
|
|
// One corner down
|
|
switch (slope)
|
|
{
|
|
case SurfaceSlope::w_corner_dn:
|
|
quad_extra = xl + TILE_SIZE - yl;
|
|
quad = xl - yl;
|
|
break;
|
|
case SurfaceSlope::s_corner_dn:
|
|
quad_extra = xl + yl;
|
|
quad = xl + yl - TILE_SIZE - 1;
|
|
break;
|
|
case SurfaceSlope::e_corner_dn:
|
|
quad_extra = TILE_SIZE - xl + yl;
|
|
quad = yl - xl;
|
|
break;
|
|
case SurfaceSlope::n_corner_dn:
|
|
quad_extra = (TILE_SIZE - xl) + (TILE_SIZE - yl);
|
|
quad = TILE_SIZE - yl - xl - 1;
|
|
break;
|
|
}
|
|
|
|
if (surfaceEl->isSlopeDoubleHeight())
|
|
{
|
|
height.landHeight += quad_extra / 2;
|
|
height.landHeight++;
|
|
return height;
|
|
}
|
|
|
|
// This tile is essentially at the next height level
|
|
height.landHeight += 0x10;
|
|
// so we move *down* the slope
|
|
if (quad < 0)
|
|
{
|
|
height.landHeight += quad / 2;
|
|
}
|
|
|
|
// Valleys
|
|
switch (slope)
|
|
{
|
|
case SurfaceSlope::w_e_valley:
|
|
if (xl + yl <= TILE_SIZE + 1)
|
|
{
|
|
return height;
|
|
}
|
|
quad = TILE_SIZE - xl - yl;
|
|
break;
|
|
case SurfaceSlope::n_s_valley:
|
|
quad = xl - yl;
|
|
break;
|
|
}
|
|
|
|
if (quad > 0)
|
|
{
|
|
height.landHeight += quad / 2;
|
|
}
|
|
|
|
return height;
|
|
}
|
|
|
|
static void clearTilePointers()
|
|
{
|
|
std::fill(_tiles.begin(), _tiles.end(), InvalidTile);
|
|
}
|
|
|
|
static void set(TilePos2 pos, TileElement* elements)
|
|
{
|
|
_tiles[(pos.y * map_pitch) + pos.x] = elements;
|
|
}
|
|
|
|
// 0x00461348
|
|
void updateTilePointers()
|
|
{
|
|
clearTilePointers();
|
|
|
|
TileElement* el = _elements;
|
|
for (tile_coord_t y = 0; y < map_rows; y++)
|
|
{
|
|
for (tile_coord_t x = 0; x < map_columns; x++)
|
|
{
|
|
set(TilePos2(x, y), el);
|
|
|
|
// Skip remaining elements on this tile
|
|
do
|
|
{
|
|
el++;
|
|
} while (!(el - 1)->isLast());
|
|
}
|
|
}
|
|
|
|
_elementsEnd = el;
|
|
}
|
|
|
|
// 0x0046148F
|
|
void reorganise()
|
|
{
|
|
Ui::setCursor(Ui::CursorId::busy);
|
|
|
|
try
|
|
{
|
|
// Allocate a temporary buffer and tighly pack all the tile elements in the map
|
|
std::vector<TileElement> tempBuffer;
|
|
tempBuffer.resize(maxElements * sizeof(TileElement));
|
|
|
|
size_t numElements = 0;
|
|
for (tile_coord_t y = 0; y < map_rows; y++)
|
|
{
|
|
for (tile_coord_t x = 0; x < map_columns; x++)
|
|
{
|
|
auto tile = get(TilePos2(x, y));
|
|
for (const auto& element : tile)
|
|
{
|
|
tempBuffer[numElements] = element;
|
|
numElements++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Copy organised elements back to original element buffer
|
|
std::memcpy(_elements, tempBuffer.data(), numElements * sizeof(TileElement));
|
|
|
|
// Zero all unused elements
|
|
auto remainingElements = maxElements - numElements;
|
|
std::memset(_elements + numElements, 0, remainingElements * sizeof(TileElement));
|
|
|
|
updateTilePointers();
|
|
|
|
// Note: original implementation did not revert the cursor
|
|
Ui::setCursor(Ui::CursorId::pointer);
|
|
}
|
|
catch (const std::bad_alloc&)
|
|
{
|
|
exitWithError(4370, StringIds::null);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// TODO: Return std::optional
|
|
uint16_t setMapSelectionTiles(const Map::Pos2& loc, const uint8_t selectionType)
|
|
{
|
|
uint16_t xPos = loc.x;
|
|
uint16_t yPos = loc.y;
|
|
uint8_t count = 0;
|
|
|
|
if (!Input::hasMapSelectionFlag(Input::MapSelectionFlags::enable))
|
|
{
|
|
Input::setMapSelectionFlags(Input::MapSelectionFlags::enable);
|
|
count++;
|
|
}
|
|
|
|
if (_word_F2448E != selectionType)
|
|
{
|
|
_word_F2448E = selectionType;
|
|
count++;
|
|
}
|
|
|
|
uint16_t toolSizeA = _adjustToolSize;
|
|
|
|
if (!toolSizeA)
|
|
toolSizeA = 1;
|
|
|
|
toolSizeA = toolSizeA << 5;
|
|
uint16_t toolSizeB = toolSizeA;
|
|
toolSizeB -= 32;
|
|
toolSizeA = toolSizeA >> 1;
|
|
toolSizeA -= 16;
|
|
xPos -= toolSizeA;
|
|
yPos -= toolSizeA;
|
|
xPos &= 0xFFE0;
|
|
yPos &= 0xFFE0;
|
|
|
|
if (xPos != _mapSelectionAX)
|
|
{
|
|
_mapSelectionAX = xPos;
|
|
count++;
|
|
}
|
|
|
|
if (yPos != _mapSelectionAY)
|
|
{
|
|
_mapSelectionAY = yPos;
|
|
count++;
|
|
}
|
|
|
|
xPos += toolSizeB;
|
|
yPos += toolSizeB;
|
|
|
|
if (xPos != _mapSelectionBX)
|
|
{
|
|
_mapSelectionBX = xPos;
|
|
count++;
|
|
}
|
|
|
|
if (yPos != _mapSelectionBY)
|
|
{
|
|
_mapSelectionBY = yPos;
|
|
count++;
|
|
}
|
|
|
|
mapInvalidateSelectionRect();
|
|
|
|
return count;
|
|
}
|
|
|
|
uint16_t setMapSelectionSingleTile(const Map::Pos2& loc, bool setQuadrant)
|
|
{
|
|
uint16_t xPos = loc.x & 0xFFE0;
|
|
uint16_t yPos = loc.y & 0xFFE0;
|
|
uint16_t cursorQuadrant = Ui::ViewportInteraction::getQuadrantOrCentreFromPos(loc);
|
|
|
|
auto count = 0;
|
|
if (!Input::hasMapSelectionFlag(Input::MapSelectionFlags::enable))
|
|
{
|
|
Input::setMapSelectionFlags(Input::MapSelectionFlags::enable);
|
|
count++;
|
|
}
|
|
|
|
if (setQuadrant && _word_F2448E != cursorQuadrant)
|
|
{
|
|
_word_F2448E = cursorQuadrant;
|
|
count++;
|
|
}
|
|
else if (!setQuadrant && _word_F2448E != 4)
|
|
{
|
|
_word_F2448E = 4;
|
|
count++;
|
|
}
|
|
|
|
if (xPos != _mapSelectionAX)
|
|
{
|
|
_mapSelectionAX = xPos;
|
|
count++;
|
|
}
|
|
|
|
if (yPos != _mapSelectionAY)
|
|
{
|
|
_mapSelectionAY = yPos;
|
|
count++;
|
|
}
|
|
|
|
if (xPos != _mapSelectionBX)
|
|
{
|
|
_mapSelectionBX = xPos;
|
|
count++;
|
|
}
|
|
|
|
if (yPos != _mapSelectionBY)
|
|
{
|
|
_mapSelectionBY = yPos;
|
|
count++;
|
|
}
|
|
|
|
mapInvalidateSelectionRect();
|
|
|
|
return count;
|
|
}
|
|
|
|
// 0x004610F2
|
|
void mapInvalidateSelectionRect()
|
|
{
|
|
if (Input::hasMapSelectionFlag(Input::MapSelectionFlags::enable))
|
|
{
|
|
for (coord_t x = _mapSelectionAX; x <= _mapSelectionBX; x += 32)
|
|
{
|
|
for (coord_t y = _mapSelectionAY; y <= _mapSelectionBY; y += 32)
|
|
{
|
|
mapInvalidateTileFull({ x, y });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 0x004CBE5F
|
|
// regs.ax: pos.x
|
|
// regs.cx: pos.y
|
|
void mapInvalidateTileFull(Map::Pos2 pos)
|
|
{
|
|
Ui::ViewportManager::invalidate(pos, 0, 1120, ZoomLevel::eighth);
|
|
}
|
|
|
|
// 0x0046112C
|
|
void mapInvalidateMapSelectionTiles()
|
|
{
|
|
if (!Input::hasMapSelectionFlag(Input::MapSelectionFlags::enableConstruct))
|
|
return;
|
|
|
|
for (uint16_t index = 0; index < mapSelectedTilesSize; ++index)
|
|
{
|
|
auto& position = _mapSelectedTiles[index];
|
|
if (position.x == -1)
|
|
break;
|
|
mapInvalidateTileFull(position);
|
|
}
|
|
}
|
|
|
|
// 0x0046A747
|
|
void resetSurfaceClearance()
|
|
{
|
|
for (coord_t y = 0; y < map_height; y += tile_size)
|
|
{
|
|
for (coord_t x = 0; x < map_width; x += tile_size)
|
|
{
|
|
auto tile = get(x, y);
|
|
auto surface = tile.surface();
|
|
if (surface != nullptr && surface->slope() == 0)
|
|
{
|
|
surface->setClearZ(surface->baseZ());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 0x004612A6
|
|
void createAnimation(uint8_t type, const Pos2& pos, tile_coord_t baseZ)
|
|
{
|
|
if (_numAnimations >= maxAnimations)
|
|
return;
|
|
|
|
for (size_t i = 0; i < _numAnimations; i++)
|
|
{
|
|
auto& animation = _animations[i];
|
|
if (animation.type == type && animation.pos == pos && animation.baseZ == baseZ)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
auto& newAnimation = _animations[_numAnimations++];
|
|
newAnimation.baseZ = baseZ;
|
|
newAnimation.type = type;
|
|
newAnimation.pos = pos;
|
|
}
|
|
|
|
// 0x00461166
|
|
void resetAnimations()
|
|
{
|
|
_numAnimations = 0;
|
|
}
|
|
|
|
// 0x004C5596
|
|
uint16_t countSurroundingWaterTiles(const Pos2& pos)
|
|
{
|
|
// Search a 10x10 area centred at pos.
|
|
// Initial tile position is the top left of the area.
|
|
auto initialTilePos = Map::TilePos2(pos) - Map::TilePos2(5, 5);
|
|
|
|
uint16_t surroundingWaterTiles = 0;
|
|
for (uint8_t yOffset = 0; yOffset < 11; yOffset++)
|
|
{
|
|
for (uint8_t xOffset = 0; xOffset < 11; xOffset++)
|
|
{
|
|
auto tilePos = initialTilePos + Map::TilePos2(xOffset, yOffset);
|
|
if (!Map::validCoords(tilePos))
|
|
continue;
|
|
|
|
auto tile = get(tilePos);
|
|
auto* surface = tile.surface();
|
|
if (surface != nullptr && surface->water() > 0)
|
|
surroundingWaterTiles++;
|
|
}
|
|
}
|
|
|
|
return surroundingWaterTiles;
|
|
}
|
|
|
|
// 0x004BE048
|
|
uint16_t countSurroundingTrees(const Pos2& pos)
|
|
{
|
|
// Search a 10x10 area centred at pos.
|
|
// Initial tile position is the top left of the area.
|
|
auto initialTilePos = Map::TilePos2(pos) - Map::TilePos2(5, 5);
|
|
|
|
uint16_t surroundingTrees = 0;
|
|
for (uint8_t yOffset = 0; yOffset < 11; yOffset++)
|
|
{
|
|
for (uint8_t xOffset = 0; xOffset < 11; xOffset++)
|
|
{
|
|
auto tilePos = initialTilePos + Map::TilePos2(xOffset, yOffset);
|
|
if (!Map::validCoords(tilePos))
|
|
continue;
|
|
|
|
auto tile = get(tilePos);
|
|
for (auto& element : tile)
|
|
{
|
|
// NB: vanilla was checking for trees above the surface element.
|
|
// This has been omitted from our implementation.
|
|
auto* tree = element.asTree();
|
|
if (tree == nullptr)
|
|
continue;
|
|
|
|
if (tree->isGhost())
|
|
continue;
|
|
|
|
surroundingTrees++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return surroundingTrees;
|
|
}
|
|
|
|
void registerHooks()
|
|
{
|
|
registerHook(
|
|
0x004612A6,
|
|
[](registers& regs) FORCE_ALIGN_ARG_POINTER -> uint8_t {
|
|
createAnimation(regs.dh, { regs.ax, regs.cx }, regs.dl);
|
|
return 0;
|
|
});
|
|
|
|
// This hook can be removed once sub_4599B3 has been implemented
|
|
registerHook(
|
|
0x004BE048,
|
|
[](registers& regs) FORCE_ALIGN_ARG_POINTER -> uint8_t {
|
|
regs.dx = countSurroundingTrees({ regs.ax, regs.cx });
|
|
return 0;
|
|
});
|
|
|
|
registerHook(
|
|
0x004C5596,
|
|
[](registers& regs) FORCE_ALIGN_ARG_POINTER -> uint8_t {
|
|
regs.dx = countSurroundingWaterTiles({ regs.ax, regs.cx });
|
|
return 0;
|
|
});
|
|
}
|
|
}
|