OpenLoco/src/OpenLoco/Ui/ViewportInteraction.cpp

639 lines
22 KiB
C++

#include "../CompanyManager.h"
#include "../Config.h"
#include "../Entities/EntityManager.h"
#include "../IndustryManager.h"
#include "../Input.h"
#include "../Interop/Interop.hpp"
#include "../Localisation/FormatArguments.hpp"
#include "../Localisation/StringIds.h"
#include "../Localisation/StringManager.h"
#include "../Map/TileManager.h"
#include "../Objects/CargoObject.h"
#include "../Objects/ObjectManager.h"
#include "../Paint/Paint.h"
#include "../StationManager.h"
#include "../TownManager.h"
#include "../Ui.h"
#include "../Ui/ScrollView.h"
#include "../Vehicles/Vehicle.h"
#include "../ViewportManager.h"
#include "../Window.h"
#include "WindowManager.h"
using namespace OpenLoco::Interop;
namespace OpenLoco::Ui::ViewportInteraction
{
InteractionArg::InteractionArg(const Paint::PaintStruct& ps)
: object(ps.entity)
, type(ps.type)
, unkBh(ps.var_29)
{
pos = Pos2{ ps.map_x, ps.map_y };
}
static bool getStationArguments(InteractionArg& interaction);
static bool getStationArguments(StationId_t id);
// 0x004CD95A
static bool _track(InteractionArg& interaction)
{
auto* tileElement = reinterpret_cast<Map::TileElement*>(interaction.object);
auto* track = tileElement->asTrack();
if (track == nullptr)
return false;
if (!track->hasStationElement())
return false;
tileElement++;
auto* station = tileElement->asStation();
if (station == nullptr)
return false;
if (station->isFlag5())
return false;
interaction.type = InteractionItem::trackStation;
interaction.object = station;
return getStationArguments(interaction);
}
// 0x004CD974
static bool _road(InteractionArg& interaction)
{
auto* tileElement = reinterpret_cast<Map::TileElement*>(interaction.object);
auto* road = tileElement->asRoad();
if (road == nullptr)
return false;
if (!road->hasStationElement())
return false;
Map::StationElement* station = nullptr;
Map::Tile tile{ interaction.pos, tileElement };
for (auto& t : tile)
{
station = t.asStation();
if (station != nullptr)
{
break;
}
}
if (station == nullptr)
{
return false;
}
interaction.object = station;
interaction.type = InteractionItem::dock;
if (station->isFlag5())
return false;
interaction.type = InteractionItem::roadStation;
return getStationArguments(interaction);
}
// 0x004CD99A
static bool getStationArguments(InteractionArg& interaction)
{
auto* tileElement = reinterpret_cast<Map::TileElement*>(interaction.object);
auto* station = tileElement->asStation();
if (station == nullptr)
return false;
if (station->isGhost())
return false;
interaction.value = station->stationId();
interaction.type = InteractionItem::stationLabel;
return getStationArguments(station->stationId());
}
static loco_global<uint16_t, 0x00F252A4> _hoveredStationId;
// 0x004CD9B0
static bool getStationArguments(const StationId_t id)
{
_hoveredStationId = id;
auto station = StationManager::get(id);
Input::setMapSelectionFlags(Input::MapSelectionFlags::unk_6);
ViewportManager::invalidate(station);
Windows::MapToolTip::setOwner(station->owner);
auto args = FormatArguments::mapToolTip(StringIds::stringid_stringid_wcolour3_stringid);
args.push(station->name);
args.push(station->town);
args.push(getTransportIconsFromStationFlags(station->flags));
char* buffer = const_cast<char*>(StringManager::getString(StringIds::buffer_338));
buffer = station->getStatusString(buffer);
buffer = StringManager::formatString(buffer, StringIds::station_accepts);
bool seperator = false; // First cargo item does not need a seperator
for (uint32_t cargoId = 0; cargoId < max_cargo_stats; cargoId++)
{
auto& stats = station->cargo_stats[cargoId];
if (!stats.isAccepted())
{
continue;
}
if (seperator)
{
buffer = StringManager::formatString(buffer, StringIds::unit_separator);
}
buffer = StringManager::formatString(buffer, ObjectManager::get<CargoObject>(cargoId)->name);
seperator = true;
}
args.push(StringIds::buffer_338);
return true;
}
// 0x004CD7FB
static bool getTownArguments(const TownId_t id)
{
auto town = TownManager::get(id);
auto args = FormatArguments::mapToolTip(StringIds::wcolour3_stringid_2, town->name); // args + 4 empty
args.skip(2);
args.push(StringIds::town_size_and_population);
args.push(town->getTownSizeString());
args.push(town->population);
return true;
}
// 0x004CD8D5
static bool getIndustryArguments(InteractionArg& interaction)
{
auto* tileElement = reinterpret_cast<Map::TileElement*>(interaction.object);
auto* industryTile = tileElement->asIndustry();
if (industryTile == nullptr)
return false;
if (industryTile->isGhost())
return false;
interaction.value = industryTile->industryId();
auto industry = industryTile->industry();
char* buffer = const_cast<char*>(StringManager::getString(StringIds::buffer_338));
*buffer = 0;
industry->getStatusString(buffer);
auto args = FormatArguments::mapToolTip();
if (std::strlen(buffer) != 0)
{
args.push(StringIds::wcolour3_stringid_2);
args.push(industry->name);
args.push(industry->town);
args.push(StringIds::buffer_338);
}
else
{
args.push(industry->name);
args.push(industry->town);
}
return true;
}
// 0x004CDA7C
static bool getVehicleArguments(const InteractionArg& interaction)
{
auto* entity = reinterpret_cast<EntityBase*>(interaction.object);
auto vehicle = entity->asVehicle();
if (vehicle == nullptr)
{
return false;
}
auto head = EntityManager::get<Vehicles::VehicleHead>(vehicle->getHead());
auto company = CompanyManager::get(head->owner);
Windows::MapToolTip::setOwner(head->owner);
auto status = head->getStatus();
auto args = FormatArguments::mapToolTip();
if (status.status2 == StringIds::null)
{
args.push(StringIds::wcolour3_stringid);
}
else
{
args.push(StringIds::wcolour3_stringid_stringid);
}
args.push(CompanyManager::getControllingId() == head->owner ? StringIds::company_vehicle : StringIds::competitor_vehicle);
args.push(company->name); // args + 6 is empty
args.skip(2);
args.push(head->name);
args.push(head->ordinalNumber);
args.push(status.status1);
args.push(status.status1Args); //32bit
args.push(status.status2);
args.push(status.status2Args); //32bit
return true;
}
// 0x004CD84A
static bool getHeadquarterArguments(const InteractionArg& interaction)
{
auto* tileElement = reinterpret_cast<Map::TileElement*>(interaction.object);
auto* buildingTile = tileElement->asBuilding();
if (buildingTile == nullptr)
{
return false;
}
if (buildingTile->isGhost())
{
return false;
}
const auto index = buildingTile->multiTileIndex();
const auto firstTile = interaction.pos - Map::offsets[index];
const Map::Pos3 pos = { firstTile.x, firstTile.y, buildingTile->baseZ() };
for (auto& company : CompanyManager::companies())
{
if (company.headquarters_x != pos.x || company.headquarters_y != pos.y || company.headquarters_z != pos.z)
{
continue;
}
Windows::MapToolTip::setOwner(company.id());
auto args = FormatArguments::mapToolTip(StringIds::wcolour3_stringid_2, company.name);
args.skip(2);
args.push(StringIds::headquarters);
return true;
}
return false;
}
static std::optional<uint32_t> vehicleDistanceFromLocation(const Vehicles::VehicleBase& component, const viewport_pos& targetPosition)
{
ViewportRect rect = {
component.sprite_left,
component.sprite_top,
component.sprite_bottom,
component.sprite_right
};
if (rect.contains(targetPosition))
{
uint32_t xDiff = std::abs(targetPosition.x - (component.sprite_right + component.sprite_left) / 2);
uint32_t yDiff = std::abs(targetPosition.y - (component.sprite_top + component.sprite_bottom) / 2);
return xDiff + yDiff;
}
return {};
}
static void checkAndSetNearestVehicle(uint32_t& nearestDistance, Vehicles::VehicleBase*& nearestVehicle, Vehicles::VehicleBase& checkVehicle, const viewport_pos& targetPosition)
{
if (checkVehicle.sprite_left != Location::null)
{
auto distanceRes = vehicleDistanceFromLocation(checkVehicle, targetPosition);
if (distanceRes)
{
if (*distanceRes < nearestDistance)
{
nearestDistance = *distanceRes;
nearestVehicle = &checkVehicle;
}
}
}
}
// 0x004CD658
InteractionArg getItemLeft(int16_t tempX, int16_t tempY)
{
if (OpenLoco::isTitleMode())
return InteractionArg{};
auto interactionsToInclude = ~(InteractionItemFlags::entity | InteractionItemFlags::townLabel | InteractionItemFlags::stationLabel);
auto res = getMapCoordinatesFromPos(tempX, tempY, interactionsToInclude);
auto interaction = res.first;
if (interaction.type != InteractionItem::entity)
{
// clang-format off
interactionsToInclude = ~(InteractionItemFlags::entity | InteractionItemFlags::track | InteractionItemFlags::roadAndTram
| InteractionItemFlags::headquarterBuilding | InteractionItemFlags::station | InteractionItemFlags::townLabel
| InteractionItemFlags::stationLabel | InteractionItemFlags::industry);
// clang-format on
res = getMapCoordinatesFromPos(tempX, tempY, interactionsToInclude);
interaction = res.first;
}
// TODO: Rework so that getting the interaction arguments and getting the map tooltip format arguments are seperated
bool success = false;
switch (interaction.type)
{
case InteractionItem::track:
success = _track(interaction);
break;
case InteractionItem::road:
success = _road(interaction);
break;
case InteractionItem::townLabel:
success = getTownArguments(static_cast<TownId_t>(interaction.value));
break;
case InteractionItem::stationLabel:
success = getStationArguments(static_cast<StationId_t>(interaction.value));
break;
case InteractionItem::trackStation:
case InteractionItem::roadStation:
case InteractionItem::airport:
case InteractionItem::dock:
success = getStationArguments(interaction);
break;
case InteractionItem::industry:
success = getIndustryArguments(interaction);
break;
case InteractionItem::headquarterBuilding:
success = getHeadquarterArguments(interaction);
break;
case InteractionItem::entity:
success = getVehicleArguments(interaction);
break;
default:
break;
}
if (success == true)
{
return interaction;
}
auto window = WindowManager::findAt(tempX, tempY);
if (window == nullptr)
return InteractionArg{};
auto viewport = window->viewports[0];
if (viewport == nullptr)
return InteractionArg{};
if (viewport->zoom > Config::get().vehicles_min_scale)
return InteractionArg{};
uint32_t nearestDistance = std::numeric_limits<uint32_t>().max();
Vehicles::VehicleBase* nearestVehicle = nullptr;
auto targetPosition = viewport->uiToMap({ tempX, tempY });
for (auto v : EntityManager::VehicleList())
{
auto train = Vehicles::Vehicle(v);
checkAndSetNearestVehicle(nearestDistance, nearestVehicle, *train.veh2, targetPosition);
for (auto car : train.cars)
{
for (auto carComponent : car)
{
checkAndSetNearestVehicle(nearestDistance, nearestVehicle, *carComponent.front, targetPosition);
checkAndSetNearestVehicle(nearestDistance, nearestVehicle, *carComponent.back, targetPosition);
checkAndSetNearestVehicle(nearestDistance, nearestVehicle, *carComponent.body, targetPosition);
}
}
}
if (nearestDistance <= 32 && nearestVehicle != nullptr)
{
interaction.type = InteractionItem::entity;
interaction.object = reinterpret_cast<void*>(nearestVehicle);
interaction.pos = nearestVehicle->position;
getVehicleArguments(interaction);
return interaction;
}
return InteractionArg{};
}
// 0x004CDB2B
InteractionArg rightOver(int16_t x, int16_t y)
{
if (OpenLoco::isTitleMode())
return InteractionArg{};
// Interaction types to exclude by default
auto interactionsToExclude = 0 | InteractionItemFlags::surface | InteractionItemFlags::water;
// TODO: Handle in the paint functions
// Get the viewport and add extra flags for hidden scenery
auto screenPos = Ui::Point(x, y);
auto w = WindowManager::findAt(screenPos);
if (w != nullptr)
{
for (auto vp : w->viewports)
{
if (vp != nullptr && vp->containsUi({ screenPos.x, screenPos.y }))
{
if (vp->flags & ViewportFlags::hide_foreground_scenery_buildings)
{
interactionsToExclude |= InteractionItemFlags::building | InteractionItemFlags::headquarterBuilding | InteractionItemFlags::industry | InteractionItemFlags::tree | InteractionItemFlags::wall;
}
}
}
}
registers regs;
regs.ax = x;
regs.bx = y;
regs.edx = interactionsToExclude;
call(0x004CDB3F, regs);
InteractionArg result;
result.value = regs.edx;
result.pos.x = regs.ax;
result.pos.y = regs.cx;
result.unkBh = regs.bh;
result.type = static_cast<InteractionItem>(regs.bl);
return result;
}
// 0x00459E54
std::pair<ViewportInteraction::InteractionArg, Viewport*> getMapCoordinatesFromPos(int32_t screenX, int32_t screenY, int32_t flags)
{
static loco_global<uint8_t, 0x0050BF68> _50BF68; // If in get map coords
static loco_global<Gfx::Context, 0x00E0C3E4> _context1;
static loco_global<Gfx::Context, 0x00E0C3F4> _context2;
_50BF68 = 1;
ViewportInteraction::InteractionArg interaction{};
Ui::Point screenPos = { static_cast<int16_t>(screenX), static_cast<int16_t>(screenY) };
auto w = WindowManager::findAt(screenPos);
if (w == nullptr)
{
_50BF68 = 0;
return std::make_pair(interaction, nullptr);
}
Viewport* chosenV = nullptr;
for (auto vp : w->viewports)
{
if (vp == nullptr)
continue;
if (!vp->containsUi({ screenPos.x, screenPos.y }))
continue;
chosenV = vp;
auto vpPos = vp->uiToMap({ screenPos.x, screenPos.y });
_context1->zoom_level = vp->zoom;
_context1->x = (0xFFFF << vp->zoom) & vpPos.x;
_context1->y = (0xFFFF << vp->zoom) & vpPos.y;
_context2->x = _context1->x;
_context2->y = _context1->y;
_context2->width = 1;
_context2->height = 1;
_context2->zoom_level = _context1->zoom_level;
auto* session = Paint::allocateSession(_context2, vp->flags);
session->generate();
session->arrangeStructs();
interaction = session->getNormalInteractionInfo(flags);
if (!(vp->flags & ViewportFlags::station_names_displayed))
{
if (_context2->zoom_level <= Config::get().station_names_min_scale)
{
auto stationInteraction = session->getStationNameInteractionInfo(flags);
if (stationInteraction.type != InteractionItem::noInteraction)
{
interaction = stationInteraction;
}
}
}
if (!(vp->flags & ViewportFlags::town_names_displayed))
{
auto townInteraction = session->getTownNameInteractionInfo(flags);
if (townInteraction.type != InteractionItem::noInteraction)
{
interaction = townInteraction;
}
}
break;
}
_50BF68 = 0;
return std::make_pair(interaction, chosenV);
}
// 0x00460781
// regs.ax = screenCoords.x;
// regs.bx = screenCoords.y;
// returns
// regs.edx = InteractionInfo.value (unsure if ever used)
// regs.ax = mapX, 0x8000 - in case of failure
// regs.bx = mapY
// regs.ecx = closestEdge (unsure if ever used)
std::optional<Pos2> getSurfaceOrWaterLocFromUi(const Point& screenCoords)
{
auto [info, viewport] = getMapCoordinatesFromPos(screenCoords.x, screenCoords.y, ~(InteractionItemFlags::surface | InteractionItemFlags::water));
if (info.type == InteractionItem::noInteraction)
{
return {};
}
int16_t waterHeight = 0; // E40130
if (info.type == InteractionItem::water)
{
auto* surface = static_cast<const SurfaceElement*>(info.object);
waterHeight = surface->water() * 16;
}
const auto minPosition = info.pos; // E40128/A
const auto maxPosition = info.pos + Pos2{ 31, 31 }; // E4012C/E
auto mapPos = info.pos + Pos2{ 16, 16 };
const auto initialVPPos = viewport->uiToMap(screenCoords);
for (int32_t i = 0; i < 5; i++)
{
int16_t z = waterHeight;
if (info.type != InteractionItem::water)
{
z = TileManager::getHeight(mapPos);
}
mapPos = viewportCoordToMapCoord(initialVPPos.x, initialVPPos.y, z, viewport->getRotation());
mapPos.x = std::clamp(mapPos.x, minPosition.x, maxPosition.x);
mapPos.y = std::clamp(mapPos.y, minPosition.y, maxPosition.y);
}
// Determine to which edge the cursor is closest
[[maybe_unused]] uint32_t closestEdge = getSideFromPos(mapPos); // ecx
return { Pos2(mapPos.x & 0xFFE0, mapPos.y & 0xFFE0) };
}
// 0x0045F1A7
std::optional<std::pair<Pos2, Viewport*>> getSurfaceLocFromUi(const Point& screenCoords)
{
auto [info, viewport] = getMapCoordinatesFromPos(screenCoords.x, screenCoords.y, ~InteractionItemFlags::surface);
if (info.type == InteractionItem::noInteraction)
{
return {};
}
const auto minPosition = info.pos; // E40128/A
const auto maxPosition = info.pos + Pos2{ 31, 31 }; // E4012C/E
auto mapPos = info.pos + Pos2{ 16, 16 };
const auto initialVPPos = viewport->uiToMap(screenCoords);
for (int32_t i = 0; i < 5; i++)
{
const auto z = TileManager::getHeight(mapPos);
mapPos = viewportCoordToMapCoord(initialVPPos.x, initialVPPos.y, z, viewport->getRotation());
mapPos.x = std::clamp(mapPos.x, minPosition.x, maxPosition.x);
mapPos.y = std::clamp(mapPos.y, minPosition.y, maxPosition.y);
}
return { std::make_pair(mapPos, viewport) };
}
// 0x0045FE05
// NOTE: Original call getSurfaceLocFromUi within this function
// instead OpenLoco has split it in two. Also note that result of original
// was a Pos2 start i.e. (& 0xFFE0) both components
uint8_t getQuadrantFromPos(const Map::Pos2& loc)
{
const auto xNibble = loc.x & 0x1F;
const auto yNibble = loc.y & 0x1F;
if (xNibble > 16)
{
return (yNibble >= 16) ? 0 : 1;
}
else
{
return (yNibble >= 16) ? 3 : 2;
}
}
// 0x0045FE4C
// NOTE: Original call getSurfaceLocFromUi within this function
// instead OpenLoco has split it in two. Also note that result of original
// was a Pos2 start i.e. (& 0xFFE0) both components
uint8_t getSideFromPos(const Map::Pos2& loc)
{
const auto xNibble = loc.x & 0x1F;
const auto yNibble = loc.y & 0x1F;
if (xNibble < yNibble)
{
return (xNibble + yNibble < 32) ? 0 : 1;
}
else
{
return (xNibble + yNibble < 32) ? 3 : 2;
}
}
// 0x0045FD8E
// NOTE: Original call getSurfaceLocFromUi within this function
// instead OpenLoco has split it in two. Also note that result of original
// was a Pos2 start i.e. (& 0xFFE0) both components
uint8_t getQuadrantOrCentreFromPos(const Map::Pos2& loc)
{
// Determine to which quadrants the cursor is closest 4 == all quadrants
const auto xNibbleCentre = std::abs((loc.x & 0xFFE0) + 16 - loc.x);
const auto yNibbleCentre = std::abs((loc.y & 0xFFE0) + 16 - loc.y);
if (std::max(xNibbleCentre, yNibbleCentre) <= 7)
{
// Is centre so all quadrants
return 4;
}
return getQuadrantFromPos(loc);
}
}