Files
openttd-cmclient/src/citymania/cm_station_gui.cpp
2025-11-28 17:36:05 +05:00

1561 lines
58 KiB
C++

#include "../stdafx.h"
#include "cm_station_gui.hpp"
#include "cm_command_type.hpp"
#include "cm_highlight.hpp"
#include "cm_highlight_type.hpp"
#include "cm_hotkeys.hpp"
#include "cm_commands.hpp"
#include "../core/math_func.hpp"
#include "../command_type.h"
#include "../command_func.h"
#include "../company_func.h"
#include "../industry_map.h"
#include "../industry.h"
#include "../landscape.h"
#include "../newgrf_airport.h" // AirportClassID, AirportClass
#include "../newgrf_station.h" // StationClassID
#include "../newgrf_house.h" // GetHouseCallback
#include "../newgrf_cargo.h" // GetCargoTranslation
#include "../object_type.h"
#include "../object_map.h"
#include "../station_base.h"
#include "../station_cmd.h"
#include "../strings_func.h" // GetString, SetDParam
#include "../tilehighlight_type.h"
#include "../town_map.h"
#include "../town.h"
#include "../viewport_func.h"
#include "../viewport_kdtree.h"
#include "../window_func.h" // SetWindowDirty
#include "../window_gui.h"
#include "../zoom_type.h"
#include "../zoom_func.h"
#include "cm_type.hpp"
#include "generated/cm_gen_commands.hpp"
#include <cassert>
#include <complex>
#include <cstdio>
#include <optional>
#include <sstream>
#include <unordered_set>
#include <variant>
bool _remove_button_clicked; // replace vanilla static vars
extern const Station *_viewport_highlight_station;
extern TileHighlightData _thd;
extern bool CheckClickOnViewportSign(const Viewport *vp, int x, int y, const ViewportSign *sign);
extern Rect ExpandRectWithViewportSignMargins(Rect r, ZoomLevel zoom);
extern RoadBits FindRailsToConnect(TileIndex tile);
extern ViewportSignKdtree _viewport_sign_kdtree;
extern AirportClassID _selected_airport_class;
extern int _selected_airport_index;
extern byte _selected_airport_layout;
extern RailType _cur_railtype; // rail_gui.cpp
extern RoadType _cur_roadtype; // road_gui.cpp
extern RailStationGUISettings _railstation; // rail_gui.cpp
extern RoadStopGUISettings _roadstop_gui_settings; // road_gui.cpp
extern AirportClassID _selected_airport_class; ///< the currently visible airport class
extern int _selected_airport_index; ///< the index of the selected airport in the current class or -1
extern byte _selected_airport_layout; ///< selected airport layout number.
namespace citymania {
bool UseImprovedStationJoin() {
return _settings_client.gui.cm_use_improved_station_join && _settings_game.station.distant_join_stations && _settings_game.station.adjacent_stations;
}
namespace StationAction {
struct Create {};
struct Join { StationID station; };
struct Picker {};
using Mode = std::variant<Create, Join, Picker>;
};
StationAction::Mode _station_action = StationAction::Create{};
static const int MAX_TILE_EXTENT_LEFT = ZOOM_LVL_BASE * TILE_PIXELS; ///< Maximum left extent of tile relative to north corner.
static const int MAX_TILE_EXTENT_RIGHT = ZOOM_LVL_BASE * TILE_PIXELS; ///< Maximum right extent of tile relative to north corner.
static const int MAX_TILE_EXTENT_TOP = ZOOM_LVL_BASE * MAX_BUILDING_PIXELS; ///< Maximum top extent of tile relative to north corner (not considering bridges).
static const int MAX_TILE_EXTENT_BOTTOM = ZOOM_LVL_BASE * (TILE_PIXELS + 2 * TILE_HEIGHT); ///< Maximum bottom extent of tile relative to north corner (worst case: #SLOPE_STEEP_N).
void MarkTileAreaDirty(const TileArea &ta) {
if (ta.tile == INVALID_TILE) return;
auto x = TileX(ta.tile);
auto y = TileY(ta.tile);
Point p1 = RemapCoords(x * TILE_SIZE, y * TILE_SIZE, TileHeight(ta.tile) * TILE_HEIGHT);
Point p2 = RemapCoords((x + ta.w) * TILE_SIZE, (y + ta.h) * TILE_SIZE, TileHeight(TileXY(x + ta.w - 1, y + ta.h - 1)) * TILE_HEIGHT);
Point p3 = RemapCoords((x + ta.w) * TILE_SIZE, y * TILE_SIZE, TileHeight(TileXY(x + ta.w - 1, y)) * TILE_HEIGHT);
Point p4 = RemapCoords(x * TILE_SIZE, (y + ta.h) * TILE_SIZE, TileHeight(TileXY(x, y + ta.h - 1)) * TILE_HEIGHT);
MarkAllViewportsDirty(
p3.x - MAX_TILE_EXTENT_LEFT,
p4.x - MAX_TILE_EXTENT_TOP,
p1.y + MAX_TILE_EXTENT_RIGHT,
p2.y + MAX_TILE_EXTENT_BOTTOM);
}
// static void MarkCoverageAreaDirty(const Station *station) {
// MarkTileAreaDirty(station->catchment_tiles);
// }
void MarkCoverageHighlightDirty() {
MarkCatchmentTilesDirty();
}
void OnStationTileSetChange(const Station *station, bool /* adding */, StationType /* type */) {
// TODO
// if (station == _highlight_station_to_join) {
// // if (_highlight_join_area.tile != INVALID_TILE)
// // UpdateHiglightJoinArea(_station_to_join);
// if (_settings_client.gui.station_show_coverage)
// MarkCoverageAreaDirty(_highlight_station_to_join);
// }
// if (station == _viewport_highlight_station) MarkCoverageAreaDirty(_viewport_highlight_station);
}
void OnStationDeleted(const Station *station) {
// TODO
// if (_highlight_station_to_join == station) {
// MarkCoverageAreaDirty(station);
// _highlight_station_to_join = nullptr;
// }
}
// const Station *_last_built_station;
void OnStationPartBuilt(const Station *station) {
// _last_built_station = station;
// CheckRedrawStationCoverage();
}
const Station *CheckClickOnDeadStationSign() {
int x = _cursor.pos.x;
int y = _cursor.pos.y;
Window *w = FindWindowFromPt(x, y);
if (w == nullptr) return nullptr;
Viewport *vp = IsPtInWindowViewport(w, x, y);
if (!HasBit(_display_opt, DO_SHOW_STATION_NAMES) && !IsInvisibilitySet(TO_SIGNS))
return nullptr;
x = ScaleByZoom(x - vp->left, vp->zoom) + vp->virtual_left;
y = ScaleByZoom(y - vp->top, vp->zoom) + vp->virtual_top;
Rect search_rect{ x - 1, y - 1, x + 1, y + 1 };
search_rect = ExpandRectWithViewportSignMargins(search_rect, vp->zoom);
const Station *last_st = nullptr;
_viewport_sign_kdtree.FindContained(search_rect.left, search_rect.top, search_rect.right, search_rect.bottom, [&](const ViewportSignKdtreeItem & item) {
if (item.type != ViewportSignKdtreeItem::VKI_STATION) return;
auto st = Station::Get(item.id.station);
if (st->IsInUse()) return;
if (_local_company != st->owner) return;
if (CheckClickOnViewportSign(vp, x, y, &st->sign)) last_st = st;
});
return last_st;
}
// bool CheckStationJoin(TileIndex start_tile, TileIndex /* end_tile */) {
// if (citymania::_fn_mod) {
// if (IsTileType (start_tile, MP_STATION)) {
// citymania::SelectStationToJoin(Station::GetByTile(start_tile));
// return true;
// }
// auto st = CheckClickOnDeadStationSign();
// if (st) {
// citymania::SelectStationToJoin(st);
// return true;
// }
// }
// return false;
// }
// template <typename Tcommand, typename Tcallback>
// void JoinAndBuild(Tcommand command, Tcallback *callback) {
// auto join_to = _highlight_station_to_join;
// command.adjacent = (citymania::_fn_mod || join_to);
// command.station_to_join = INVALID_STATION;
// if (citymania::_fn_mod) command.station_to_join = NEW_STATION;
// else if (join_to) command.station_to_join = join_to->index;
// command.with_callback([] (bool res)->bool {
// if (!res) return false;
// // _station_to_join = _last_built_station;
// return true;
// }).post(callback);
// }
static DiagDirection TileFractCoordsToDiagDir(Point pt) {
auto x = pt.x & TILE_UNIT_MASK;
auto y = pt.y & TILE_UNIT_MASK;
bool diag = (x + y) < 16;
if (x < y) {
return diag ? DIAGDIR_NE : DIAGDIR_SE;
}
return diag ? DIAGDIR_NW : DIAGDIR_SW;
}
static DiagDirection RoadBitsToDiagDir(RoadBits bits) {
if (bits < ROAD_SE) {
return bits == ROAD_NW ? DIAGDIR_NW : DIAGDIR_SW;
}
return bits == ROAD_SE ? DIAGDIR_SE : DIAGDIR_NE;
}
DiagDirection AutodetectRailObjectDirection(TileIndex tile, Point pt) {
RoadBits bits = FindRailsToConnect(tile);
// FIXME after this point repeats road autodetection
if (HasExactlyOneBit(bits)) return RoadBitsToDiagDir(bits);
if (bits == ROAD_NONE) bits = ROAD_ALL;
RoadBits frac_bits = DiagDirToRoadBits(TileFractCoordsToDiagDir(pt));
if (HasExactlyOneBit(frac_bits & bits)) {
return RoadBitsToDiagDir(frac_bits & bits);
}
frac_bits |= MirrorRoadBits(frac_bits);
if (HasExactlyOneBit(frac_bits & bits)) {
return RoadBitsToDiagDir(frac_bits & bits);
}
for (DiagDirection ddir = DIAGDIR_BEGIN; ddir < DIAGDIR_END; ddir++) {
if (DiagDirToRoadBits(ddir) & bits) {
return ddir;
}
}
NOT_REACHED();
}
static RoadBits FindRoadsToConnect(TileIndex tile, RoadType roadtype) {
RoadBits bits = ROAD_NONE;
DiagDirection ddir;
auto cur_rtt = GetRoadTramType(roadtype);
// Prioritize roadbits that head in this direction
for (ddir = DIAGDIR_BEGIN; ddir < DIAGDIR_END; ddir++) {
TileIndex cur_tile = TileAddByDiagDir(tile, ddir);
if (GetAnyRoadBits(cur_tile, cur_rtt, true) &
DiagDirToRoadBits(ReverseDiagDir(ddir)))
{
bits |= DiagDirToRoadBits(ddir);
}
}
if (bits != ROAD_NONE) {
return bits;
}
// Try to connect to any road passing by
for (ddir = DIAGDIR_BEGIN; ddir < DIAGDIR_END; ddir++) {
TileIndex cur_tile = TileAddByDiagDir(tile, ddir);
if (GetTileType(cur_tile) == MP_ROAD && HasTileRoadType(cur_tile, cur_rtt) &&
(GetRoadTileType(cur_tile) == ROAD_TILE_NORMAL)) {
bits |= DiagDirToRoadBits(ddir);
}
}
return bits;
}
bool CheckDriveThroughRoadStopDirection(TileArea area, RoadBits r) {
for (TileIndex tile : area) {
if (GetTileType(tile) != MP_ROAD) continue;
if (GetRoadTileType(tile) != ROAD_TILE_NORMAL) continue;
if (GetAllRoadBits(tile) & ~r) return false;
}
return true;
}
/*
* Selects orientation for road object (depot, terminal station)
*/
DiagDirection AutodetectRoadObjectDirection(TileIndex tile, Point pt, RoadType roadtype) {
RoadBits bits = FindRoadsToConnect(tile, roadtype);
if (HasExactlyOneBit(bits)) {
return RoadBitsToDiagDir(bits);
}
if (bits == ROAD_NONE) {
bits = ROAD_ALL;
}
RoadBits frac_bits = DiagDirToRoadBits(TileFractCoordsToDiagDir(pt));
if (HasExactlyOneBit(frac_bits & bits)) {
return RoadBitsToDiagDir(frac_bits & bits);
}
frac_bits |= MirrorRoadBits(frac_bits);
if (HasExactlyOneBit(frac_bits & bits)) {
return RoadBitsToDiagDir(frac_bits & bits);
}
for (DiagDirection ddir = DIAGDIR_BEGIN; ddir < DIAGDIR_END; ddir++) {
if (DiagDirToRoadBits(ddir) & bits) {
return ddir;
}
}
NOT_REACHED();
}
/*
* Automaticaly selects direction to use for road stop.
* @param area road stop area
* @return selected direction
*/
DiagDirection AutodetectDriveThroughRoadStopDirection(TileArea area, Point pt, RoadType roadtype) {
bool se_suits, ne_suits;
// Check which direction is available
// If both are not use SE, building will fail anyway
se_suits = CheckDriveThroughRoadStopDirection(area, ROAD_Y);
ne_suits = CheckDriveThroughRoadStopDirection(area, ROAD_X);
if (!ne_suits) return STATIONDIR_Y;
if (!se_suits) return STATIONDIR_X;
// Build station along the longer direction
if (area.w > area.h) return STATIONDIR_X;
if (area.w < area.h) return STATIONDIR_Y;
return DiagDirToAxis(AutodetectRoadObjectDirection(area.tile, pt, roadtype)) == AXIS_X ? STATIONDIR_X : STATIONDIR_Y;
}
uint GetMonthlyFrom256Tick(uint amount) {
return ((amount * Ticks::DAY_TICKS * EconomyTime::DAYS_IN_ECONOMY_MONTH) >> 8);
}
uint GetAverageHouseProduction(byte amount) {
static const uint AVG[2][256] = {
{0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 18, 20, 22, 24, 27, 30, 33, 36, 39, 42, 45, 48, 52, 56, 60, 64, 68, 72, 76, 80, 85, 90, 95, 100, 105, 110, 115, 120, 126, 132, 138, 144, 150, 156, 162, 168, 175, 182, 189, 196, 203, 210, 217, 224, 232, 240, 248, 256, 264, 272, 280, 288, 297, 306, 315, 324, 333, 342, 351, 360, 370, 380, 390, 400, 410, 420, 430, 440, 451, 462, 473, 484, 495, 506, 517, 528, 540, 552, 564, 576, 588, 600, 612, 624, 637, 650, 663, 676, 689, 702, 715, 728, 742, 756, 770, 784, 798, 812, 826, 840, 855, 870, 885, 900, 915, 930, 945, 960, 976, 992, 1008, 1024, 1040, 1056, 1072, 1088, 1105, 1122, 1139, 1156, 1173, 1190, 1207, 1224, 1242, 1260, 1278, 1296, 1314, 1332, 1350, 1368, 1387, 1406, 1425, 1444, 1463, 1482, 1501, 1520, 1540, 1560, 1580, 1600, 1620, 1640, 1660, 1680, 1701, 1722, 1743, 1764, 1785, 1806, 1827, 1848, 1870, 1892, 1914, 1936, 1958, 1980, 2002, 2024, 2047, 2070, 2093, 2116, 2139, 2162, 2185, 2208, 2232, 2256, 2280, 2304, 2328, 2352, 2376, 2400, 2425, 2450, 2475, 2500, 2525, 2550, 2575, 2600, 2626, 2652, 2678, 2704, 2730, 2756, 2782, 2808, 2835, 2862, 2889, 2916, 2943, 2970, 2997, 3024, 3052, 3080, 3108, 3136, 3164, 3192, 3220, 3248, 3277, 3306, 3335, 3364, 3393, 3422, 3451, 3480, 3510, 3540, 3570, 3600, 3630, 3660, 3690, 3720, 3751, 3782, 3813, 3844, 3875, 3906, 3937, 3968, 4000, 4032, 4064, 4096, 4128, 4160, 4192},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 148, 152, 156, 160, 165, 170, 175, 180, 185, 190, 195, 200, 205, 210, 215, 220, 225, 230, 235, 240, 246, 252, 258, 264, 270, 276, 282, 288, 294, 300, 306, 312, 318, 324, 330, 336, 343, 350, 357, 364, 371, 378, 385, 392, 399, 406, 413, 420, 427, 434, 441, 448, 456, 464, 472, 480, 488, 496, 504, 512, 520, 528, 536, 544, 552, 560, 568, 576, 585, 594, 603, 612, 621, 630, 639, 648, 657, 666, 675, 684, 693, 702, 711, 720, 730, 740, 750, 760, 770, 780, 790, 800, 810, 820, 830, 840, 850, 860, 870, 880, 891, 902, 913, 924, 935, 946, 957, 968, 979, 990, 1001, 1012, 1023, 1034, 1045, 1056, 1068, 1080, 1092, 1104, 1116, 1128, 1140, 1152, 1164, 1176, 1188, 1200, 1212, 1224, 1236, 1248, 1261, 1274, 1287, 1300, 1313, 1326, 1339, 1352, 1365, 1378, 1391, 1404, 1417, 1430, 1443, 1456, 1470, 1484, 1498, 1512, 1526, 1540, 1554, 1568, 1582, 1596, 1610, 1624, 1638, 1652, 1666, 1680, 1695, 1710, 1725, 1740, 1755, 1770, 1785, 1800, 1815, 1830, 1845, 1860, 1875, 1890, 1905, 1920, 1936, 1952, 1968, 1984, 2000, 2016, 2032, 2048, 2064, 2080, 2096, 2112, 2128, 2144, 2160}
};
if (amount == 0) return 0;
switch (_settings_game.economy.town_cargogen_mode) {
case TCGM_ORIGINAL:
return GetMonthlyFrom256Tick(AVG[EconomyIsInRecession() ? 1 : 0][amount]);
case TCGM_BITCOUNT: {
uint amt = (amount + 7) / 8;
if (EconomyIsInRecession()) amt += 2;
else amt *= 2;
return GetMonthlyFrom256Tick(amt * 16);
}
default:
NOT_REACHED();
}
return 0;
}
static void AddProducedCargo_Town(TileIndex tile, CargoArray &produced)
{
if (!IsHouseCompleted(tile)) return;
HouseID house_id = GetHouseType(tile);
const HouseSpec *hs = HouseSpec::Get(house_id);
Town *t = Town::GetByTile(tile);
if (HasBit(hs->callback_mask, CBM_HOUSE_PRODUCE_CARGO)) {
for (uint i = 0; i < 256; i++) {
uint16 callback = GetHouseCallback(CBID_HOUSE_PRODUCE_CARGO, i, 0, house_id, t, tile);
if (callback == CALLBACK_FAILED || callback == CALLBACK_HOUSEPRODCARGO_END) break;
CargoID cargo = GetCargoTranslation(GB(callback, 8, 7), hs->grf_prop.grffile);
if (cargo == CT_INVALID) continue;
produced[cargo] += GetMonthlyFrom256Tick((uint)GB(callback, 0, 8)) ;
}
} else {
if (hs->population > 0) {
auto avg = GetAverageHouseProduction(hs->population);
for (const CargoSpec *cs : CargoSpec::town_production_cargoes[TPE_PASSENGERS]) {
produced[cs->Index()] += avg;
}
}
if (hs->mail_generation > 0) {
auto avg = GetAverageHouseProduction(hs->mail_generation);
for (const CargoSpec *cs : CargoSpec::town_production_cargoes[TPE_MAIL]) {
produced[cs->Index()] += avg;
}
}
}
}
// Similar to ::GetProductionAroundTiles but counts production total
CargoArray GetProductionAroundTiles(TileIndex tile, int w, int h, int rad)
{
static const uint HQ_AVG_POP[2][5] = {
{48, 64, 84, 128, 384},
{48, 64, 84, 128, 256},
};
static const uint HQ_AVG_MAIL[2][5] = {
{36, 48, 64, 96, 264},
{36, 48, 64, 96, 196}
};
CargoArray produced{};
std::set<IndustryID> industries;
TileArea ta = TileArea(tile, w, h).Expand(rad);
/* Loop over all tiles to get the produced cargo of
* everything except industries */
for(auto tile : ta) {
switch (GetTileType(tile)) {
case MP_INDUSTRY:
industries.insert(GetIndustryIndex(tile));
break;
case MP_HOUSE:
AddProducedCargo_Town(tile, produced);
break;
case MP_OBJECT:
if (IsObjectType(tile, OBJECT_HQ)) {
auto pax_avg = GetMonthlyFrom256Tick(HQ_AVG_POP[EconomyIsInRecession() ? 1 : 0][GetAnimationFrame(tile)]);
auto mail_avg = GetMonthlyFrom256Tick(HQ_AVG_MAIL[EconomyIsInRecession() ? 1 : 0][GetAnimationFrame(tile)]);
for (const CargoSpec *cs : CargoSpec::town_production_cargoes[TPE_PASSENGERS])
produced[cs->Index()] += pax_avg;
for (const CargoSpec *cs : CargoSpec::town_production_cargoes[TPE_MAIL])
produced[cs->Index()] += mail_avg;
}
default: break;
}
}
/* Loop over the seen industries. They produce cargo for
* anything that is within 'rad' of any one of their tiles.
*/
for (IndustryID industry : industries) {
const Industry *i = Industry::Get(industry);
/* Skip industry with neutral station */
if (i->neutral_station != nullptr && !_settings_game.station.serve_neutral_industries) continue;
for (const auto &p : i->produced) {
if (IsValidCargoID(p.cargo)) produced[p.cargo] += ((uint)p.history[LAST_MONTH].production) << 8;
}
}
return produced;
}
// ---- NEw code
static TileArea GetStationJoinArea(StationID station_id) {
auto station = Station::GetIfValid(station_id);
if (station == nullptr) return {};
auto &r = station->rect;
auto d = (int)_settings_game.station.station_spread - 1;
TileArea ta(
TileXY(std::max<int>(r.right - d, 0),
std::max<int>(r.bottom - d, 0)),
TileXY(std::min<int>(r.left + d, Map::SizeX() - 1),
std::min<int>(r.top + d, Map::SizeY() - 1))
);
return ta;
}
extern DiagDirection AddAutodetectionRotation(DiagDirection ddir); // cm_highlight.cpp
// void VanillaStationPreview::Update(Point pt, TileIndex tile) {
// StationPreviewBase::Update(pt, tile);
// this->palette = CM_PALETTE_TINT_WHITE;
// if (this->remove_mode) return;
// if (this->selected_station_to_join != INVALID_STATION) {
// this->station_to_join = this->selected_station_to_join;
// return;
// }
// if (!IsValidTile(this->type->cur_tile)) return;
// this->station_to_join = INVALID_STATION;
// auto area = this->type->GetArea(false);
// area.Expand(1);
// area.ClampToMap();
// for (auto tile : area) {
// if (IsTileType(tile, MP_STATION) && GetTileOwner(tile) == _local_company) {
// Station *st = Station::GetByTile(tile);
// if (st == nullptr || st->index == this->station_to_join) continue;
// if (this->station_to_join != INVALID_STATION) {
// this->station_to_join = INVALID_STATION;
// this->palette = CM_PALETTE_TINT_YELLOW;
// break;
// }
// this->station_to_join = st->index;
// // TODO check for command to return multiple? but also check each to
// // see if they can be built
// // if (this->GetCommand(true, st->index)->test().Succeeded()) {
// // if (this->station_to_join != INVALID_STATION) {
// // this->station_to_join = INVALID_STATION;
// // this->palette = CM_PALETTE_TINT_YELLOW;
// // break;
// // } else this->station_to_join = st->index;
// // }
// }
// }
// if (this->station_to_join == INVALID_STATION && !this->GetCommand(true, NEW_STATION)->test().Succeeded())
// this->palette = CM_PALETTE_TINT_RED_DEEP;
// }
// void VanillaStationPreview::Execute() {
// if (this->remove_mode) {
// this->type->Execute(this->type->GetRemoveCommand(), true);
// return;
// }
// auto proc = [type=this->type](bool test, StationID to_join) -> bool {
// auto cmd = type->GetCommand(_fn_mod, to_join);
// if (test) return cmd->test().Succeeded();
// return type->Execute(std::move(cmd), false);
// };
// ShowSelectStationIfNeeded(this->type->GetArea(false), proc);
// }
// void VanillaStationPreview::OnStationRemoved(const Station *station) {
// if (this->station_to_join == station->index) this->station_to_join = INVALID_STATION;
// if (this->selected_station_to_join == station->index) this->station_to_join = INVALID_STATION;
// }
// StationPreview::StationPreview(sp<PreviewStationType> type)
// :StationPreviewBase{type}
// {
// auto seconds_since_selected = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - _station_to_join_selected).count();
// if (seconds_since_selected < 30) this->station_to_join = _station_to_join;
// else this->station_to_join = INVALID_STATION;
// }
// StationPreview::~StationPreview() {
// _station_to_join_selected = std::chrono::system_clock::now();
// }
// up<Command> StationPreview::GetCommand() {
// if (this->select_mode) return nullptr;
// auto res = StationPreviewBase::GetCommand(true, this->station_to_join);
// if (this->remove_mode) return res;
// res->with_callback([] (bool res) -> bool {
// if (!res) return false;
// if (_last_built_station == nullptr) return false;
// _station_to_join = _last_built_station->index;
// _station_to_join_selected = std::chrono::system_clock::now();
// // auto p = dynamic_cast<StationPreview*>(_ap.preview.get());
// // if (p == nullptr) return false;
// // p->station_to_join = _last_built_station->index;
// return true;
// });
// return res;
// }
// void StationPreview::OnStationRemoved(const Station *station) {
// if (this->station_to_join == station->index) this->station_to_join = INVALID_STATION;
// }
// Non-tool station highlight management (coverage area and picker selection)
enum class StationHighlightMode {
None,
Picker,
Coverage
};
std::optional<ObjectHighlight> _active_highlight_object = std::nullopt;
StationID _selected_station = INVALID_STATION;
StationHighlightMode _station_highlight_mode = StationHighlightMode::None;
void SetSelectedStationToJoin(StationID station_id) {
_selected_station = station_id;
UpdateActiveTool();
}
void ResetSelectedStationToJoin() {
_station_highlight_mode = StationHighlightMode::None;
UpdateActiveTool();
}
void SetHighlightCoverageStation(Station *station, bool sel) {
if (_station_highlight_mode == StationHighlightMode::Picker) return;
SetWindowDirty(WC_STATION_VIEW, _selected_station);
if (station == nullptr || !sel) {
_station_highlight_mode = StationHighlightMode::None;
} else {
_selected_station = station->index;
_station_highlight_mode = StationHighlightMode::Coverage;
}
SetWindowDirty(WC_STATION_VIEW, _selected_station);
UpdateActiveTool();
}
static void ResetHighlightCoverageStation() {
SetHighlightCoverageStation(nullptr, false);
}
bool IsHighlightCoverageStation(const Station *station) {
if (_station_highlight_mode != StationHighlightMode::Coverage) return false;
if (station == nullptr) return false;
return station->index == _selected_station;
}
void OnStationRemoved(const Station *station) {
// if (_last_built_station == station) _last_built_station = nullptr;
if (auto mode = std::get_if<StationAction::Join>(&_station_action); mode && mode->station == station->index) {
_station_action = StationAction::Create{};
UpdateActiveTool();
}
if (_selected_station == station->index) {
_selected_station = INVALID_STATION;
if (_station_highlight_mode == StationHighlightMode::Coverage) {
_station_highlight_mode = StationHighlightMode::None;
SetWindowDirty(WC_STATION_VIEW, _selected_station);
}
UpdateActiveTool();
}
// TODO?
// if (GetActiveTool() != nullptr) GetActiveTool()->OnStationRemoved(station);
}
static void SetActiveHighlightObject(std::optional<ObjectHighlight> &ohl) {
_active_highlight_object = ohl;
if (_active_highlight_object.has_value()) _active_highlight_object->UpdateTiles();
if (_station_highlight_mode == StationHighlightMode::Coverage)
SetWindowDirty(WC_STATION_VIEW, _selected_station);
_station_highlight_mode = StationHighlightMode::Picker;
}
void AbortStationPlacement() {
// TODO is it necessary?
// SetHighlightStationToJoin(station=nullptr, with_area=false);
}
bool HasSelectedStationHighlight() {
return _station_highlight_mode != StationHighlightMode::None;
}
static void UpdateStationAction(std::optional<TileArea> area, up<Command> cmdptr) {
if (UseImprovedStationJoin()) return;
_station_action = StationAction::Create{};
if (!area.has_value()) return;
if (_fn_mod) {
if (!_settings_game.station.distant_join_stations) return;
// TODO ctrl with overbuilding errors out in vanilla, shows empty picker here
_station_action = StationAction::Picker{};
return;
}
auto cmd = dynamic_cast<cmd::BuildRailStation *>(cmdptr.get());
if (cmd == nullptr) return;
cmd->station_to_join = NEW_STATION;
bool new_valid = cmd->test().Succeeded(); // TODO rarely needed
std::set<StationID> checked_joins;
std::vector<StationID> valid_joins; // TODO probably don't need the whole vector
area->Expand(1);
area->ClampToMap();
for (auto tile : *area) {
if (IsTileType(tile, MP_STATION) && GetTileOwner(tile) == _local_company) {
Station *st = Station::GetByTile(tile);
auto it = checked_joins.find(st->index);
if (st == nullptr || it != checked_joins.end()) continue;
checked_joins.insert(it, st->index);
cmd->station_to_join = st->index;
if (cmd->test().Succeeded()) valid_joins.push_back(st->index);
}
}
// TODO w/o distant join and no ctrl -> adjacent=false
// No nearby stations -> create new (default)
if (checked_joins.empty()) return;
if (valid_joins.empty()) {
if (_settings_game.station.distant_join_stations && new_valid) {
// We can build a new one but require a picker for that
_station_action = StationAction::Picker{};
}
// Error out, create action will do that
return;
}
if (valid_joins.size() > 1) {
if (_settings_game.station.distant_join_stations) {
_station_action = StationAction::Picker{};
}
// W/o distant join just error out, create action will do that
return;
}
// Only one valid join -> do it
_station_action = StationAction::Join{valid_joins[0]};
}
static HighlightMap PrepareHighilightMap(Station *st_join, std::optional<ObjectHighlight> ohl, SpriteID pal, bool show_join_area, bool show_coverage, uint rad) {
bool add_current = true; // FIXME
auto hlmap = ohl.has_value() ? ohl->GetHighlightMap(pal) : HighlightMap{};
TileArea join_area;
std::set<TileIndex> coverage_area;
if (show_join_area && st_join != nullptr) {
join_area = GetStationJoinArea(st_join->index);
hlmap.AddTileArea(join_area, CM_PALETTE_TINT_CYAN);
}
if (show_coverage && st_join != nullptr) {
// Add joining station coverage
for (auto t : st_join->catchment_tiles) {
auto pal = join_area.Contains(t) ? MixTints(CM_PALETTE_TINT_CYAN, CM_PALETTE_TINT_WHITE) : CM_PALETTE_TINT_WHITE;
hlmap.Add(t, ObjectTileHighlight::make_tint(pal));
coverage_area.insert(t);
}
}
auto area = ohl.has_value() ? ohl->GetArea() : std::nullopt;
if (!_settings_game.station.modified_catchment) rad = CA_UNMODIFIED;
std::optional<TileArea> rad_area = std::nullopt;
if (area.has_value()) {
auto xarea = *area;
xarea.Expand(rad);
xarea = ClampToVisibleMap(xarea);
rad_area = xarea;
}
if (show_coverage && add_current && rad_area.has_value()) {
// Add current station coverage
for (auto t : *rad_area) {
auto pal = join_area.Contains(t) ? MixTints(CM_PALETTE_TINT_CYAN, CM_PALETTE_TINT_WHITE) : CM_PALETTE_TINT_WHITE;
hlmap.Add(t, ObjectTileHighlight::make_tint(pal));
coverage_area.insert(t);
}
}
if (show_coverage) {
hlmap.AddTilesBorder(coverage_area, CM_PALETTE_TINT_WHITE);
}
if (st_join != nullptr) {
// Highlight joining station blue
TileArea ta(TileXY(st_join->rect.left, st_join->rect.top), TileXY(st_join->rect.right, st_join->rect.bottom));
for (TileIndex t : ta) {
if (!IsTileType(t, MP_STATION) || GetStationIndex(t) != st_join->index) continue;
hlmap.Add(t, ObjectTileHighlight::make_struct_tint(CM_PALETTE_TINT_BLUE));
}
}
return hlmap;
}
ToolGUIInfo GetSelectedStationGUIInfo() {
if (_station_highlight_mode == StationHighlightMode::None) return {};
auto st = Station::GetIfValid(_selected_station);
auto hlmap = PrepareHighilightMap(
st,
_station_highlight_mode == StationHighlightMode::Picker ? _active_highlight_object : std::nullopt,
CM_PALETTE_TINT_WHITE,
false,
_station_highlight_mode == StationHighlightMode::Coverage,
0
);
return {hlmap, {}, {}};
}
// --- Action base class ---
void Action::OnStationRemoved(const Station *) {}
// --- RemoveAction ---
void RemoveAction::Update(Point, TileIndex tile) {
this->cur_tile = tile;
}
bool RemoveAction::HandleMousePress() {
if (!IsValidTile(this->cur_tile)) return false;
this->start_tile = this->cur_tile;
return true;
}
void RemoveAction::HandleMouseRelease() {
auto area = this->GetArea();
if (!area.has_value()) return;
this->Execute(area.value());
this->start_tile = INVALID_TILE;
}
std::optional<TileArea> RemoveAction::GetArea() const {
if (!IsValidTile(this->cur_tile)) return std::nullopt;
if (!IsValidTile(this->start_tile)) return TileArea{this->cur_tile, this->cur_tile};
return TileArea{this->start_tile, this->cur_tile};
}
ToolGUIInfo RemoveAction::GetGUIInfo() {
HighlightMap hlmap;
BuildInfoOverlayData data;
auto area = this->GetArea();
CommandCost cost;
if (area.has_value()) {
hlmap.AddTileAreaWithBorder(area.value(), CM_PALETTE_TINT_RED_DEEP);
auto cmd = this->GetCommand(area.value());
if (cmd) cost = cmd->test();
}
return {hlmap, data, cost};
}
void RemoveAction::OnStationRemoved(const Station *) {}
// --- PlacementAction ---
ToolGUIInfo PlacementAction::PrepareGUIInfo(std::optional<ObjectHighlight> ohl, up<Command> cmd, StationCoverageType sct, uint rad) {
if (!cmd || !ohl.has_value()) return {};
ohl.value().UpdateTiles();
auto palette = CM_PALETTE_TINT_WHITE;
auto area = ohl.value().GetArea();
StationID to_join = INVALID_STATION;
std::visit(Overload{
[&](StationAction::Join &a) {
to_join = a.station;
},
[&](StationAction::Create &) {
to_join = NEW_STATION;
},
[&](StationAction::Picker &) {
palette = CM_PALETTE_TINT_YELLOW;
}
}, _station_action);
if (to_join != INVALID_STATION) {
auto station_cmd = dynamic_cast<StationBuildCommand *>(cmd.get());
if (station_cmd != nullptr) station_cmd->station_to_join = to_join;
}
CommandCost cost = cmd->test();
if (palette != CM_PALETTE_TINT_YELLOW) {
palette = cost.Succeeded() ? CM_PALETTE_TINT_WHITE : CM_PALETTE_TINT_RED_DEEP;
}
bool show_coverage = _settings_client.gui.station_show_coverage;
auto hlmap = PrepareHighilightMap(
Station::GetIfValid(to_join),
ohl.value(),
palette,
true,
show_coverage,
rad
);
// Prepare build overlay
BuildInfoOverlayData data;
if (auto mode = std::get_if<StationAction::Join>(&_station_action)) {
SetDParam(0, mode->station);
data.emplace_back(0, PAL_NONE, GetString(CM_STR_BULID_INFO_OVERLAY_JOIN_STATION));
} else {
data.emplace_back(0, PAL_NONE, GetString(CM_STR_BULID_INFO_OVERLAY_NEW_STATION));
}
if (area.has_value()) {
// Add supplied cargo information
// TODO can we use rad_area since we already have it?
auto production = citymania::GetProductionAroundTiles(area->tile, area->w, area->h, rad);
bool has_header = false;
for (CargoID i = 0; i < NUM_CARGO; i++) {
if (production[i] == 0) continue;
switch (sct) {
case SCT_PASSENGERS_ONLY: if (!IsCargoInClass(i, CC_PASSENGERS)) continue; break;
case SCT_NON_PASSENGERS_ONLY: if (IsCargoInClass(i, CC_PASSENGERS)) continue; break;
case SCT_ALL: break;
default: NOT_REACHED();
}
const CargoSpec *cs = CargoSpec::Get(i);
if (cs == nullptr) continue;
if (!has_header) {
data.emplace_back(0, PAL_NONE, GetString(CM_STR_BUILD_INFO_OVERLAY_STATION_SUPPLIES));
has_header = true;
}
SetDParam(0, i);
SetDParam(1, production[i] >> 8);
data.emplace_back(1, cs->GetCargoIcon(), GetString(CM_STR_BUILD_INFO_OVERLAY_STATION_CARGO));
}
// Add accepted cargo information
CargoTypes always_accepted;
auto cargoes = ::GetAcceptanceAroundTiles(area->tile, area->w, area->h, rad, &always_accepted);
/* Convert cargo counts to a set of cargo bits, and draw the result. */
std::vector<std::pair<uint, std::string>> cargostr;
for (CargoID i = 0; i < NUM_CARGO; i++) {
switch (sct) {
case SCT_PASSENGERS_ONLY: if (!IsCargoInClass(i, CC_PASSENGERS)) continue; break;
case SCT_NON_PASSENGERS_ONLY: if (IsCargoInClass(i, CC_PASSENGERS)) continue; break;
case SCT_ALL: break;
default: NOT_REACHED();
}
if (cargoes[i] > 0) {
SetDParam(0, 1 << i);
if(cargoes[i] < 8) {
SetDParam(1, cargoes[i]);
cargostr.emplace_back(2, GetString(CM_STR_BULID_INFO_OVERLAY_ACCEPTS_CARGO_PARTIAL));
} else if (HasBit(always_accepted, i)) {
SetDParam(1, cargoes[i]);
cargostr.emplace_back(1, GetString(CM_STR_BULID_INFO_OVERLAY_ACCEPTS_CARGO_FULL));
} else {
cargostr.emplace_back(0, GetString(CM_STR_BULID_INFO_OVERLAY_ACCEPTS_CARGO));
}
}
}
std::string cargoliststr;
if (cargostr.size() > 0) {
// Sort by first element (priority)
std::stable_sort(cargostr.begin(), cargostr.end(), [](const auto& a, const auto& b) {
return a.first < b.first;
});
std::vector<std::string> cargolistsorted;
cargolistsorted.reserve((cargostr.size()));
for (auto [_, s] : cargostr) cargolistsorted.push_back(s);
cargoliststr = string::join(cargolistsorted, ", ");
} else {
cargoliststr = GetString(STR_JUST_NOTHING);
}
SetDParamStr(0, cargoliststr);
data.emplace_back(0, PAL_NONE, GetString(CM_STR_BULID_INFO_OVERLAY_ACCEPTS));
Town *t = ClosestTownFromTile(area->tile, _settings_game.economy.dist_local_authority);
if (t) {
auto town_allowed = CheckIfAuthorityAllowsNewStation(area->tile, DC_NONE).Succeeded();
auto rating = t->ratings[_current_company];
auto dist = DistanceManhattan(t->xy, area->tile);
SetDParam(0, t->index);
SetDParam(1, rating);
if (dist <= 10) {
SetDParam(2, CM_STR_BULID_INFO_OVERLAY_TOWN_S_ADS);
} else if (dist <= 15) {
SetDParam(2, CM_STR_BULID_INFO_OVERLAY_TOWN_M_ADS);
} else if (dist <= 20) {
SetDParam(2, CM_STR_BULID_INFO_OVERLAY_TOWN_L_ADS);
} else {
SetDParam(2, CM_STR_BULID_INFO_OVERLAY_TOWN_NO_ADS);
}
data.emplace_back(0, PAL_NONE, GetString(town_allowed ? CM_STR_BULID_INFO_OVERLAY_TOWN_ALLOWS : CM_STR_BULID_INFO_OVERLAY_TOWN_DENIES));
} else {
data.emplace_back(0, PAL_NONE, GetString(CM_STR_BULID_INFO_OVERLAY_TOWN_NONE));
}
SetDParam(0, area->w);
SetDParam(1, area->h);
data.emplace_back(0, PAL_NONE, GetString(CM_STR_BULID_INFO_OVERLAY_STATION_SIZE));
}
return {hlmap, data, cost};
}
// --- SizedPlacementAction ---
void SizedPlacementAction::Update(Point, TileIndex tile) {
this->cur_tile = tile;
UpdateStationAction(this->GetArea(), std::move(this->GetCommand(tile, INVALID_STATION)));
}
bool SizedPlacementAction::HandleMousePress() {
return IsValidTile(this->cur_tile);
}
void SizedPlacementAction::HandleMouseRelease() {
if (!IsValidTile(this->cur_tile)) return;
this->Execute(this->cur_tile);
}
ToolGUIInfo SizedPlacementAction::GetGUIInfo() {
if (!IsValidTile(this->cur_tile)) return {};
auto [sct, rad] = this->GetCatchmentParams();
return this->PrepareGUIInfo(
this->GetObjectHighlight(this->cur_tile),
this->GetCommand(this->cur_tile, INVALID_STATION),
sct,
rad
);
}
void SizedPlacementAction::OnStationRemoved(const Station *) {}
// --- DragNDropPlacementAction ---
std::optional<TileArea> DragNDropPlacementAction::GetArea() const {
// TODO separate common fuctions with RemoveAction into base class
if (!IsValidTile(this->cur_tile)) return std::nullopt;
if (!IsValidTile(this->start_tile)) return TileArea{this->cur_tile, this->cur_tile};
return TileArea{this->start_tile, this->cur_tile};
}
void DragNDropPlacementAction::Update(Point, TileIndex tile) {
this->cur_tile = tile;
auto area = this->GetArea();
if (!area.has_value()) return;
UpdateStationAction(area, std::move(this->GetCommand(*area, INVALID_STATION)));
}
bool DragNDropPlacementAction::HandleMousePress() {
if (!IsValidTile(this->cur_tile)) return false;
this->start_tile = this->cur_tile;
return true;
}
void DragNDropPlacementAction::HandleMouseRelease() {
auto area = this->GetArea();
if (!area.has_value()) return;
this->Execute(area.value());
this->start_tile = INVALID_TILE;
}
ToolGUIInfo DragNDropPlacementAction::GetGUIInfo() {
auto area = this->GetArea();
if (!area.has_value()) return {};
auto ohl = this->GetObjectHighlight(area.value());
auto [sct, rad] = this->GetCatchmentParams();
return this->PrepareGUIInfo(
this->GetObjectHighlight(area.value()),
this->GetCommand(area.value(), INVALID_STATION),
sct,
rad
);
}
void DragNDropPlacementAction::OnStationRemoved(const Station *) {}
// --- StationSelectAction ---
void StationSelectAction::Update(Point, TileIndex tile) {
this->cur_tile = tile;
}
bool StationSelectAction::HandleMousePress() {
return true;
}
void StationSelectAction::HandleMouseRelease() {
// TODO station sign click
if (!IsValidTile(this->cur_tile)) return;
_station_action = StationAction::Create{};
if (IsTileType(this->cur_tile, MP_STATION)) {
auto st = Station::GetByTile(this->cur_tile);
if (st) _station_action = StationAction::Join{st->index};
}
}
ToolGUIInfo StationSelectAction::GetGUIInfo() {
if (!IsValidTile(this->cur_tile)) return {};
HighlightMap hlmap;
hlmap.Add(this->cur_tile, ObjectTileHighlight::make_border(CM_PALETTE_TINT_BLUE, ZoningBorder::FULL));
BuildInfoOverlayData data;
Station *st = IsTileType(this->cur_tile, MP_STATION) ? Station::GetByTile(this->cur_tile) : nullptr;
if (st) {
SetDParam(0, st->index);
data.emplace_back(0, PAL_NONE, GetString(CM_STR_BULID_INFO_OVERLAY_JOIN_STATION));
} else {
data.emplace_back(0, PAL_NONE, GetString(CM_STR_BULID_INFO_OVERLAY_NEW_STATION));
}
return {hlmap, data, {}};
}
void StationSelectAction::OnStationRemoved(const Station *station) {
// if (this->selected_station == station->index) this->selected_station = INVALID_STATION;
}
// --- Misc functions ---
TileArea GetCommandArea(const up<Command> &cmd) {
if (auto rail_cmd = dynamic_cast<cmd::BuildRailStation *>(cmd.get())) {
auto w = rail_cmd->numtracks;
auto h = rail_cmd->plat_len;
if (!rail_cmd->axis) Swap(w, h);
return {rail_cmd->tile_org, w, h};
} else if (auto road_cmd = dynamic_cast<cmd::BuildRoadStop *>(cmd.get())) {
return {road_cmd->tile, road_cmd->width, road_cmd->length};
} else if (auto dock_cmd = dynamic_cast<cmd::BuildDock *>(cmd.get())) {
DiagDirection dir = GetInclinedSlopeDirection(GetTileSlope(dock_cmd->tile));
TileIndex tile_to = (dir != INVALID_DIAGDIR ? TileAddByDiagDir(dock_cmd->tile, ReverseDiagDir(dir)) : dock_cmd->tile);
return {dock_cmd->tile, tile_to};
} else if (auto airport_cmd = dynamic_cast<cmd::BuildAirport *>(cmd.get())) {
const AirportSpec *as = AirportSpec::Get(airport_cmd->airport_type);
if (as == nullptr) return {};
return {airport_cmd->tile, as->size_x, as->size_y};
}
NOT_REACHED();
}
StationBuildTool::StationBuildTool() {
ResetHighlightCoverageStation();
}
extern void ShowSelectStationWindow(TileArea ta, StationPickerCmdProc&& proc);
template<typename Taction, typename Tcallback, typename Targ>
bool ExecuteBuildCommand(Taction *action, Tcallback callback, Targ arg) {
std::visit(Overload{
[&](StationAction::Join &a) {
auto cmd = action->GetCommand(arg, a.station);
return cmd ? cmd->post(callback) : false;
},
[&](StationAction::Create &) {
auto cmd = action->GetCommand(arg, NEW_STATION);
return cmd ? cmd->post(callback) : false;
},
[&](StationAction::Picker &) {
auto cmd = action->GetCommand(arg, INVALID_STATION);
auto proc = [cmd=sp<Command>{std::move(cmd)}, callback](bool test, StationID to_join) -> bool {
if (!cmd) return false;
auto station_cmd = dynamic_cast<StationBuildCommand *>(cmd.get());
if (station_cmd == nullptr) return false;
station_cmd->station_to_join = to_join;
if (test) {
return cmd->test().Succeeded();
} else {
ResetSelectedStationToJoin();
return cmd->post(callback);
}
};
auto ohl = action->GetObjectHighlight(arg);
if (!ohl.has_value()) return false;
auto area = ohl->GetArea();
if (!area.has_value()) return false;
// SetActiveHighlightObject(ohl);
ShowSelectStationWindow(*area, std::move(proc));
return true;
}
}, _station_action);
return true;
}
// --- RailStationBuildTool::RemoveAction ---
up<Command> RailStationBuildTool::RemoveAction::GetCommand(TileArea area) {
auto cmd = make_up<cmd::RemoveFromRailStation>(
area.tile,
area.CMGetEndTile(),
!citymania::_fn_mod
);
cmd->with_error(STR_ERROR_CAN_T_REMOVE_PART_OF_STATION);
return cmd;
}
bool RailStationBuildTool::RemoveAction::Execute(TileArea area) {
auto cmd = this->GetCommand(area);
return cmd->post(&CcPlaySound_CONSTRUCTION_RAIL);
}
// --- RailStationBuildTool::SizedPlacementAction ---
std::optional<TileArea> RailStationBuildTool::SizedPlacementAction::GetArea() const {
if (!IsValidTile(this->cur_tile)) return std::nullopt;
auto w = _settings_client.gui.station_numtracks;
auto h = _settings_client.gui.station_platlength;
if (_railstation.orientation == AXIS_X) std::swap(w, h);
return TileArea{this->cur_tile, w, h};
}
up<Command> RailStationBuildTool::SizedPlacementAction::GetCommand(TileIndex tile, StationID to_join) {
// TODO mostly same as DragNDropPlacement
auto cmd = make_up<cmd::BuildRailStation>(
tile,
_cur_railtype,
_railstation.orientation,
_settings_client.gui.station_numtracks,
_settings_client.gui.station_platlength,
_railstation.station_class,
_railstation.station_type,
to_join,
true
);
cmd->with_error(STR_ERROR_CAN_T_BUILD_RAILROAD_STATION);
return cmd;
}
bool RailStationBuildTool::SizedPlacementAction::Execute(TileIndex tile) {
return ExecuteBuildCommand(this, &CcStation, tile);
}
// --- RailStationBuildTool::DragNDropPlacementAction ---
up<Command> RailStationBuildTool::DragNDropPlacementAction::GetCommand(TileArea area, StationID to_join) {
uint numtracks = area.w;
uint platlength = area.h;
if (_railstation.orientation == AXIS_X) Swap(numtracks, platlength);
auto cmd = make_up<cmd::BuildRailStation>(
area.tile,
_cur_railtype,
_railstation.orientation,
numtracks,
platlength,
_railstation.station_class,
_railstation.station_type,
to_join,
true
);
cmd->with_error(STR_ERROR_CAN_T_BUILD_RAILROAD_STATION);
return cmd;
}
bool RailStationBuildTool::DragNDropPlacementAction::Execute(TileArea area) {
return ExecuteBuildCommand(this, &CcStation, area);
}
std::optional<ObjectHighlight> RailStationBuildTool::DragNDropPlacementAction::GetObjectHighlight(TileArea area) {
return ObjectHighlight::make_rail_station(area.tile, area.w, area.h, _railstation.orientation, _railstation.station_class, _railstation.station_type);
}
std::optional<ObjectHighlight> RailStationBuildTool::SizedPlacementAction::GetObjectHighlight(TileIndex tile) {
uint16_t w = _settings_client.gui.station_numtracks;
uint16_t h = _settings_client.gui.station_platlength;
if (_railstation.orientation == AXIS_X) std::swap(w, h);
return ObjectHighlight::make_rail_station(tile, w, h, _railstation.orientation, _railstation.station_class, _railstation.station_type);
}
// --- RailStationBuildTool implementation ---
RailStationBuildTool::RailStationBuildTool() : mode(Mode::SIZED) {
this->action = make_sp<RailStationBuildTool::SizedPlacementAction>();
}
void RailStationBuildTool::Update(Point pt, TileIndex tile) {
Mode new_mode;
if (_remove_button_clicked) {
new_mode = Mode::REMOVE;
} else if (citymania::UseImprovedStationJoin() && _fn_mod) {
new_mode = Mode::SELECT;
} else if (_settings_client.gui.station_dragdrop) {
new_mode = Mode::DRAGDROP;
} else {
new_mode = Mode::SIZED;
}
if (new_mode != this->mode) {
switch (new_mode) {
case Mode::REMOVE:
this->action = make_sp<RailStationBuildTool::RemoveAction>();
break;
case Mode::SELECT:
this->action = make_sp<StationSelectAction>();
break;
case Mode::DRAGDROP:
this->action = make_sp<RailStationBuildTool::DragNDropPlacementAction>();
break;
case Mode::SIZED:
this->action = make_sp<RailStationBuildTool::SizedPlacementAction>();
break;
default:
NOT_REACHED();
}
this->mode = new_mode;
}
this->action->Update(pt, tile);
}
CursorID RailStationBuildTool::GetCursor() { return SPR_CURSOR_RAIL_STATION; }
// --- RoadStopBuildTool::RemoveAction ---
up<Command> RoadStopBuildTool::RemoveAction::GetCommand(TileArea area) {
auto cmd = make_up<cmd::RemoveRoadStop>(
area.tile,
area.w,
area.h,
this->stop_type,
_fn_mod
);
auto rti = GetRoadTypeInfo(_cur_roadtype);
cmd->with_error(rti->strings.err_remove_station[this->stop_type]);
return cmd;
}
bool RoadStopBuildTool::RemoveAction::Execute(TileArea area) {
auto cmd = this->GetCommand(area);
return cmd->post(&CcPlaySound_CONSTRUCTION_OTHER);
}
up<Command> RoadStopBuildTool::DragNDropPlacementAction::GetCommand(TileArea area, StationID to_join) {
DiagDirection ddir = this->ddir;
bool drive_through = this->ddir >= DIAGDIR_END;
if (drive_through) ddir = static_cast<DiagDirection>(this->ddir - DIAGDIR_END); // Adjust picker result to actual direction.
auto res = make_up<cmd::BuildRoadStop>(
area.tile,
area.w,
area.h,
this->stop_type,
drive_through,
ddir,
this->road_type,
_roadstop_gui_settings.roadstop_class,
_roadstop_gui_settings.roadstop_type,
to_join,
true
);
return res;
}
bool RoadStopBuildTool::DragNDropPlacementAction::Execute(TileArea area) {
return ExecuteBuildCommand(this, &CcRoadStop, area);
}
std::optional<ObjectHighlight> RoadStopBuildTool::DragNDropPlacementAction::GetObjectHighlight(TileArea area) {
return ObjectHighlight::make_road_stop(
area.tile,
area.w,
area.h,
this->road_type,
this->ddir,
this->stop_type == ROADSTOP_TRUCK,
_roadstop_gui_settings.roadstop_class,
_roadstop_gui_settings.roadstop_type
);
}
// --- RoadStopBuildTool implementation ---
RoadStopBuildTool::RoadStopBuildTool(RoadStopType stop_type) : mode(Mode::DRAGDROP), stop_type(stop_type)
{
this->action = make_sp<RoadStopBuildTool::DragNDropPlacementAction>(_cur_roadtype, this->stop_type);
}
void RoadStopBuildTool::DragNDropPlacementAction::Update(Point pt, TileIndex tile) {
citymania::DragNDropPlacementAction::Update(pt, tile);
this->ddir = DIAGDIR_NE;
auto area = this->GetArea();
if (pt.x != -1 && area.has_value()) {
auto ddir = _roadstop_gui_settings.orientation;
if (ddir >= DIAGDIR_END && ddir < STATIONDIR_AUTO) {
// When placed on road autorotate anyway
if (ddir == STATIONDIR_X) {
if (!CheckDriveThroughRoadStopDirection(area.value(), ROAD_X))
ddir = STATIONDIR_Y;
} else {
if (!CheckDriveThroughRoadStopDirection(area.value(), ROAD_Y))
ddir = STATIONDIR_X;
}
} else if (ddir == STATIONDIR_AUTO) {
ddir = AddAutodetectionRotation(AutodetectRoadObjectDirection(tile, pt, this->road_type));
} else if (ddir == STATIONDIR_AUTO_XY) {
ddir = AddAutodetectionRotation(AutodetectDriveThroughRoadStopDirection(area.value(), pt, this->road_type));
}
this->ddir = ddir;
}
}
void RoadStopBuildTool::Update(Point pt, TileIndex tile) {
Mode new_mode;
if (_remove_button_clicked) {
new_mode = Mode::REMOVE;
} else if (UseImprovedStationJoin() && _fn_mod) {
new_mode = Mode::SELECT;
} else {
new_mode = Mode::DRAGDROP;
}
if (new_mode != this->mode) {
switch (new_mode) {
case Mode::REMOVE:
this->action = make_sp<RoadStopBuildTool::RemoveAction>(this->stop_type);
break;
case Mode::SELECT:
this->action = make_sp<StationSelectAction>();
break;
case Mode::DRAGDROP:
this->action = make_sp<RoadStopBuildTool::DragNDropPlacementAction>(_cur_roadtype, this->stop_type);
break;
}
this->mode = new_mode;
}
this->action->Update(pt, tile);
}
CursorID RoadStopBuildTool::GetCursor() {
return this->stop_type == ROADSTOP_TRUCK ? SPR_CURSOR_TRUCK_STATION : SPR_CURSOR_BUS_STATION;
}
// --- DockBuildTool::RemoveAction ---
up<Command> DockBuildTool::RemoveAction::GetCommand(TileArea area) {
// TODO: Implement dock removal command if available
return nullptr;
}
bool DockBuildTool::RemoveAction::Execute(TileArea area) {
// TODO: Implement dock removal execution if available
return false;
}
// --- DockBuildTool::SizedPlacementAction ---
std::optional<TileArea> DockBuildTool::SizedPlacementAction::GetArea() const {
auto ddir = this->GetDirection(this->cur_tile);
if (!ddir.has_value()) return std::nullopt;
return TileArea{this->cur_tile, TileAddByDiagDir(this->cur_tile, *ddir)};
}
up<Command> DockBuildTool::SizedPlacementAction::GetCommand(TileIndex tile, StationID to_join) {
return make_up<cmd::BuildDock>(
tile,
to_join,
true
);
}
bool DockBuildTool::SizedPlacementAction::Execute(TileIndex tile) {
return ExecuteBuildCommand(this, &CcBuildDocks, tile);
}
std::optional<ObjectHighlight> DockBuildTool::SizedPlacementAction::GetObjectHighlight(TileIndex tile) {
return ObjectHighlight::make_dock(tile, this->GetDirection(tile).value_or(DIAGDIR_SE));
}
std::optional<DiagDirection> DockBuildTool::SizedPlacementAction::GetDirection(TileIndex tile) const {
if (!IsValidTile(tile)) return std::nullopt;
auto slope_dir = GetInclinedSlopeDirection(GetTileSlope(tile));
if (slope_dir == INVALID_DIAGDIR) return std::nullopt;
return ReverseDiagDir(slope_dir);
};
// --- DockBuildTool Implementation ---
DockBuildTool::DockBuildTool() : mode(Mode::SIZED) {
this->action = make_sp<DockBuildTool::SizedPlacementAction>();
}
void DockBuildTool::Update(Point pt, TileIndex tile) {
Mode new_mode;
if (_remove_button_clicked) {
new_mode = Mode::REMOVE;
} else if (citymania::UseImprovedStationJoin() && _fn_mod) {
new_mode = Mode::SELECT;
} else {
new_mode = Mode::SIZED;
}
if (new_mode != this->mode) {
switch (new_mode) {
case Mode::REMOVE:
this->action = make_up<DockBuildTool::RemoveAction>();
break;
case Mode::SELECT:
this->action = make_up<StationSelectAction>();
break;
case Mode::SIZED:
this->action = make_up<DockBuildTool::SizedPlacementAction>();
break;
default:
NOT_REACHED();
}
this->mode = new_mode;
}
this->action->Update(pt, tile);
}
CursorID DockBuildTool::GetCursor() {
return SPR_CURSOR_DOCK;
}
// --- AirportBuildTool::RemoveAction ---
up<Command> AirportBuildTool::RemoveAction::GetCommand(TileArea area) {
// TODO: Implement aiport removal command if available
return nullptr;
}
bool AirportBuildTool::RemoveAction::Execute(TileArea area) {
// TODO: Implement airport removal execution if available
return false;
}
// --- AirportBuildTool::SizedPlacementAction ---
std::optional<TileArea> AirportBuildTool::SizedPlacementAction::GetArea() const {
if (!IsValidTile(this->cur_tile)) return std::nullopt;
auto as = AirportClass::Get(_selected_airport_class)->GetSpec(_selected_airport_index);
if (as == nullptr) return std::nullopt;
return TileArea{this->cur_tile, as->size_x, as->size_y};
}
up<Command> AirportBuildTool::SizedPlacementAction::GetCommand(TileIndex tile, StationID to_join) {
auto as = AirportClass::Get(_selected_airport_class)->GetSpec(_selected_airport_index);
if (as == nullptr) return nullptr;
byte airport_type = as->GetIndex();
byte layout = _selected_airport_layout;
auto cmd = make_up<cmd::BuildAirport>(
tile,
airport_type,
layout,
to_join,
true
);
cmd->with_error(STR_ERROR_CAN_T_BUILD_AIRPORT_HERE);
return cmd;
}
bool AirportBuildTool::SizedPlacementAction::Execute(TileIndex tile) {
return ExecuteBuildCommand(this, &CcBuildAirport, tile);
}
std::optional<ObjectHighlight> AirportBuildTool::SizedPlacementAction::GetObjectHighlight(TileIndex tile) {
byte airport_type = AirportClass::Get(_selected_airport_class)->GetSpec(_selected_airport_index)->GetIndex();
byte layout = _selected_airport_layout;
return ObjectHighlight::make_airport(tile, airport_type, layout);
}
std::pair<StationCoverageType, uint> AirportBuildTool::SizedPlacementAction::GetCatchmentParams() {
auto rad = AirportClass::Get(_selected_airport_class)->GetSpec(_selected_airport_index)->catchment;
return {SCT_ALL, rad};
}
// --- AirportBuildTool implementation ---
AirportBuildTool::AirportBuildTool() : mode(Mode::SIZED) {
this->action = make_sp<AirportBuildTool::SizedPlacementAction>();
}
void AirportBuildTool::Update(Point pt, TileIndex tile) {
Mode new_mode;
if (_remove_button_clicked) {
new_mode = Mode::REMOVE;
} else if (citymania::UseImprovedStationJoin() && _fn_mod) {
new_mode = Mode::SELECT;
} else {
new_mode = Mode::SIZED;
}
if (new_mode != this->mode) {
switch (new_mode) {
case Mode::REMOVE:
this->action = make_sp<AirportBuildTool::RemoveAction>();
break;
case Mode::SELECT:
this->action = make_sp<StationSelectAction>();
break;
case Mode::SIZED:
this->action = make_sp<AirportBuildTool::SizedPlacementAction>();
break;
default:
NOT_REACHED();
}
this->mode = new_mode;
}
this->action->Update(pt, tile);
}
CursorID AirportBuildTool::GetCursor() {
return SPR_CURSOR_AIRPORT;
}
} // namespace citymania