Fix: Don't allow right-click to close world generation progress window. (#13084)
[openttd-github.git] / src / town_gui.cpp
blob656b4efd13844332826777856096bb712029dc90
1 /*
2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6 */
8 /** @file town_gui.cpp GUI for towns. */
10 #include "stdafx.h"
11 #include "town.h"
12 #include "viewport_func.h"
13 #include "error.h"
14 #include "gui.h"
15 #include "house.h"
16 #include "newgrf_house.h"
17 #include "picker_gui.h"
18 #include "command_func.h"
19 #include "company_func.h"
20 #include "company_base.h"
21 #include "company_gui.h"
22 #include "network/network.h"
23 #include "string_func.h"
24 #include "strings_func.h"
25 #include "sound_func.h"
26 #include "tilehighlight_func.h"
27 #include "sortlist_type.h"
28 #include "road_cmd.h"
29 #include "landscape.h"
30 #include "querystring_gui.h"
31 #include "window_func.h"
32 #include "townname_func.h"
33 #include "core/backup_type.hpp"
34 #include "core/geometry_func.hpp"
35 #include "genworld.h"
36 #include "fios.h"
37 #include "stringfilter_type.h"
38 #include "dropdown_func.h"
39 #include "town_kdtree.h"
40 #include "town_cmd.h"
41 #include "timer/timer.h"
42 #include "timer/timer_game_calendar.h"
43 #include "timer/timer_window.h"
44 #include "zoom_func.h"
45 #include "hotkeys.h"
47 #include "widgets/town_widget.h"
49 #include "table/strings.h"
51 #include "safeguards.h"
53 TownKdtree _town_local_authority_kdtree(&Kdtree_TownXYFunc);
55 typedef GUIList<const Town*, const bool &> GUITownList;
57 static constexpr NWidgetPart _nested_town_authority_widgets[] = {
58 NWidget(NWID_HORIZONTAL),
59 NWidget(WWT_CLOSEBOX, COLOUR_BROWN),
60 NWidget(WWT_CAPTION, COLOUR_BROWN, WID_TA_CAPTION), SetDataTip(STR_LOCAL_AUTHORITY_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
61 NWidget(WWT_TEXTBTN, COLOUR_BROWN, WID_TA_ZONE_BUTTON), SetMinimalSize(50, 0), SetDataTip(STR_LOCAL_AUTHORITY_ZONE, STR_LOCAL_AUTHORITY_ZONE_TOOLTIP),
62 NWidget(WWT_SHADEBOX, COLOUR_BROWN),
63 NWidget(WWT_DEFSIZEBOX, COLOUR_BROWN),
64 NWidget(WWT_STICKYBOX, COLOUR_BROWN),
65 EndContainer(),
66 NWidget(WWT_PANEL, COLOUR_BROWN, WID_TA_RATING_INFO), SetMinimalSize(317, 92), SetResize(1, 1), EndContainer(),
67 NWidget(WWT_PANEL, COLOUR_BROWN, WID_TA_COMMAND_LIST), SetMinimalSize(317, 52), SetResize(1, 0), SetDataTip(0x0, STR_LOCAL_AUTHORITY_ACTIONS_TOOLTIP), EndContainer(),
68 NWidget(WWT_PANEL, COLOUR_BROWN, WID_TA_ACTION_INFO), SetMinimalSize(317, 52), SetResize(1, 0), EndContainer(),
69 NWidget(NWID_HORIZONTAL),
70 NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_TA_EXECUTE), SetMinimalSize(317, 12), SetResize(1, 0), SetFill(1, 0), SetDataTip(STR_LOCAL_AUTHORITY_DO_IT_BUTTON, STR_LOCAL_AUTHORITY_DO_IT_TOOLTIP),
71 NWidget(WWT_RESIZEBOX, COLOUR_BROWN),
72 EndContainer()
75 /** Town authority window. */
76 struct TownAuthorityWindow : Window {
77 private:
78 Town *town; ///< Town being displayed.
79 int sel_index; ///< Currently selected town action, \c 0 to \c TACT_COUNT-1, \c -1 means no action selected.
80 uint displayed_actions_on_previous_painting; ///< Actions that were available on the previous call to OnPaint()
81 TownActions enabled_actions; ///< Actions that are enabled in settings.
82 TownActions available_actions; ///< Actions that are available to execute for the current company.
83 StringID action_tooltips[TACT_COUNT];
85 Dimension icon_size; ///< Dimensions of company icon
86 Dimension exclusive_size; ///< Dimensions of exlusive icon
88 /**
89 * Get the position of the Nth set bit.
91 * If there is no Nth bit set return -1
93 * @param n The Nth set bit from which we want to know the position
94 * @return The position of the Nth set bit, or -1 if no Nth bit set.
96 int GetNthSetBit(int n)
98 if (n >= 0) {
99 for (uint i : SetBitIterator(this->enabled_actions)) {
100 n--;
101 if (n < 0) return i;
104 return -1;
108 * Gets all town authority actions enabled in settings.
110 * @return Bitmask of actions enabled in the settings.
112 static TownActions GetEnabledActions()
114 TownActions enabled = TACT_ALL;
116 if (!_settings_game.economy.fund_roads) CLRBITS(enabled, TACT_ROAD_REBUILD);
117 if (!_settings_game.economy.fund_buildings) CLRBITS(enabled, TACT_FUND_BUILDINGS);
118 if (!_settings_game.economy.exclusive_rights) CLRBITS(enabled, TACT_BUY_RIGHTS);
119 if (!_settings_game.economy.bribe) CLRBITS(enabled, TACT_BRIBE);
121 return enabled;
124 public:
125 TownAuthorityWindow(WindowDesc &desc, WindowNumber window_number) : Window(desc), sel_index(-1), displayed_actions_on_previous_painting(0), available_actions(TACT_NONE)
127 this->town = Town::Get(window_number);
128 this->enabled_actions = GetEnabledActions();
130 auto realtime = TimerGameEconomy::UsingWallclockUnits();
131 this->action_tooltips[0] = STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_SMALL_ADVERTISING;
132 this->action_tooltips[1] = STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_MEDIUM_ADVERTISING;
133 this->action_tooltips[2] = STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_LARGE_ADVERTISING;
134 this->action_tooltips[3] = realtime ? STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_ROAD_RECONSTRUCTION_MINUTES : STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_ROAD_RECONSTRUCTION_MONTHS;
135 this->action_tooltips[4] = STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_STATUE_OF_COMPANY;
136 this->action_tooltips[5] = STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_NEW_BUILDINGS;
137 this->action_tooltips[6] = realtime ? STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_EXCLUSIVE_TRANSPORT_MINUTES : STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_EXCLUSIVE_TRANSPORT_MONTHS;
138 this->action_tooltips[7] = STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_BRIBE;
140 this->InitNested(window_number);
143 void OnInit() override
145 this->icon_size = GetSpriteSize(SPR_COMPANY_ICON);
146 this->exclusive_size = GetSpriteSize(SPR_EXCLUSIVE_TRANSPORT);
149 void OnPaint() override
151 this->available_actions = GetMaskOfTownActions(_local_company, this->town);
152 if (this->available_actions != displayed_actions_on_previous_painting) this->SetDirty();
153 displayed_actions_on_previous_painting = this->available_actions;
155 this->SetWidgetLoweredState(WID_TA_ZONE_BUTTON, this->town->show_zone);
156 this->SetWidgetDisabledState(WID_TA_EXECUTE, (this->sel_index == -1) || !HasBit(this->available_actions, this->sel_index));
158 this->DrawWidgets();
159 if (!this->IsShaded())
161 this->DrawRatings();
162 this->DrawActions();
166 /** Draw the contents of the ratings panel. May request a resize of the window if the contents does not fit. */
167 void DrawRatings()
169 Rect r = this->GetWidget<NWidgetBase>(WID_TA_RATING_INFO)->GetCurrentRect().Shrink(WidgetDimensions::scaled.framerect);
171 int text_y_offset = (this->resize.step_height - GetCharacterHeight(FS_NORMAL)) / 2;
172 int icon_y_offset = (this->resize.step_height - this->icon_size.height) / 2;
173 int exclusive_y_offset = (this->resize.step_height - this->exclusive_size.height) / 2;
175 DrawString(r.left, r.right, r.top + text_y_offset, STR_LOCAL_AUTHORITY_COMPANY_RATINGS);
176 r.top += this->resize.step_height;
178 bool rtl = _current_text_dir == TD_RTL;
179 Rect icon = r.WithWidth(this->icon_size.width, rtl);
180 Rect exclusive = r.Indent(this->icon_size.width + WidgetDimensions::scaled.hsep_normal, rtl).WithWidth(this->exclusive_size.width, rtl);
181 Rect text = r.Indent(this->icon_size.width + WidgetDimensions::scaled.hsep_normal + this->exclusive_size.width + WidgetDimensions::scaled.hsep_normal, rtl);
183 /* Draw list of companies */
184 for (const Company *c : Company::Iterate()) {
185 if ((HasBit(this->town->have_ratings, c->index) || this->town->exclusivity == c->index)) {
186 DrawCompanyIcon(c->index, icon.left, text.top + icon_y_offset);
188 SetDParam(0, c->index);
189 SetDParam(1, c->index);
191 int rating = this->town->ratings[c->index];
192 StringID str = STR_CARGO_RATING_APPALLING;
193 if (rating > RATING_APPALLING) str++;
194 if (rating > RATING_VERYPOOR) str++;
195 if (rating > RATING_POOR) str++;
196 if (rating > RATING_MEDIOCRE) str++;
197 if (rating > RATING_GOOD) str++;
198 if (rating > RATING_VERYGOOD) str++;
199 if (rating > RATING_EXCELLENT) str++;
201 SetDParam(2, str);
202 if (this->town->exclusivity == c->index) {
203 DrawSprite(SPR_EXCLUSIVE_TRANSPORT, COMPANY_SPRITE_COLOUR(c->index), exclusive.left, text.top + exclusive_y_offset);
206 DrawString(text.left, text.right, text.top + text_y_offset, STR_LOCAL_AUTHORITY_COMPANY_RATING);
207 text.top += this->resize.step_height;
211 text.bottom = text.top - 1;
212 if (text.bottom > r.bottom) {
213 /* If the company list is too big to fit, mark ourself dirty and draw again. */
214 ResizeWindow(this, 0, text.bottom - r.bottom, false);
218 /** Draws the contents of the actions panel. May re-initialise window to resize panel, if the list does not fit. */
219 void DrawActions()
221 Rect r = this->GetWidget<NWidgetBase>(WID_TA_COMMAND_LIST)->GetCurrentRect().Shrink(WidgetDimensions::scaled.framerect);
223 DrawString(r, STR_LOCAL_AUTHORITY_ACTIONS_TITLE);
224 r.top += GetCharacterHeight(FS_NORMAL);
226 /* Draw list of actions */
227 for (int i = 0; i < TACT_COUNT; i++) {
228 /* Don't show actions if disabled in settings. */
229 if (!HasBit(this->enabled_actions, i)) continue;
231 /* Set colour of action based on ability to execute and if selected. */
232 TextColour action_colour = TC_GREY | TC_NO_SHADE;
233 if (HasBit(this->available_actions, i)) action_colour = TC_ORANGE;
234 if (this->sel_index == i) action_colour = TC_WHITE;
236 DrawString(r, STR_LOCAL_AUTHORITY_ACTION_SMALL_ADVERTISING_CAMPAIGN + i, action_colour);
237 r.top += GetCharacterHeight(FS_NORMAL);
241 void SetStringParameters(WidgetID widget) const override
243 if (widget == WID_TA_CAPTION) SetDParam(0, this->window_number);
246 void DrawWidget(const Rect &r, WidgetID widget) const override
248 switch (widget) {
249 case WID_TA_ACTION_INFO:
250 if (this->sel_index != -1) {
251 Money action_cost = _price[PR_TOWN_ACTION] * _town_action_costs[this->sel_index] >> 8;
252 bool affordable = Company::IsValidID(_local_company) && action_cost < GetAvailableMoney(_local_company);
254 SetDParam(0, action_cost);
255 DrawStringMultiLine(r.Shrink(WidgetDimensions::scaled.framerect),
256 this->action_tooltips[this->sel_index],
257 affordable ? TC_YELLOW : TC_RED);
259 break;
263 void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override
265 switch (widget) {
266 case WID_TA_ACTION_INFO: {
267 assert(size.width > padding.width && size.height > padding.height);
268 Dimension d = {0, 0};
269 for (int i = 0; i < TACT_COUNT; i++) {
270 SetDParam(0, _price[PR_TOWN_ACTION] * _town_action_costs[i] >> 8);
271 d = maxdim(d, GetStringMultiLineBoundingBox(this->action_tooltips[i], size));
273 d.width += padding.width;
274 d.height += padding.height;
275 size = maxdim(size, d);
276 break;
279 case WID_TA_COMMAND_LIST:
280 size.height = (TACT_COUNT + 1) * GetCharacterHeight(FS_NORMAL) + padding.height;
281 size.width = GetStringBoundingBox(STR_LOCAL_AUTHORITY_ACTIONS_TITLE).width;
282 for (uint i = 0; i < TACT_COUNT; i++ ) {
283 size.width = std::max(size.width, GetStringBoundingBox(STR_LOCAL_AUTHORITY_ACTION_SMALL_ADVERTISING_CAMPAIGN + i).width + padding.width);
285 size.width += padding.width;
286 break;
288 case WID_TA_RATING_INFO:
289 resize.height = std::max({this->icon_size.height + WidgetDimensions::scaled.vsep_normal, this->exclusive_size.height + WidgetDimensions::scaled.vsep_normal, (uint)GetCharacterHeight(FS_NORMAL)});
290 size.height = 9 * resize.height + padding.height;
291 break;
295 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
297 switch (widget) {
298 case WID_TA_ZONE_BUTTON: {
299 bool new_show_state = !this->town->show_zone;
300 TownID index = this->town->index;
302 new_show_state ? _town_local_authority_kdtree.Insert(index) : _town_local_authority_kdtree.Remove(index);
304 this->town->show_zone = new_show_state;
305 this->SetWidgetLoweredState(widget, new_show_state);
306 MarkWholeScreenDirty();
307 break;
310 case WID_TA_COMMAND_LIST: {
311 int y = this->GetRowFromWidget(pt.y, WID_TA_COMMAND_LIST, 1, GetCharacterHeight(FS_NORMAL)) - 1;
313 y = GetNthSetBit(y);
314 if (y >= 0) {
315 this->sel_index = y;
316 this->SetDirty();
319 /* When double-clicking, continue */
320 if (click_count == 1 || y < 0 || !HasBit(this->available_actions, y)) break;
321 [[fallthrough]];
324 case WID_TA_EXECUTE:
325 Command<CMD_DO_TOWN_ACTION>::Post(STR_ERROR_CAN_T_DO_THIS, this->town->xy, this->window_number, this->sel_index);
326 break;
330 /** Redraw the whole window on a regular interval. */
331 IntervalTimer<TimerWindow> redraw_interval = {std::chrono::seconds(3), [this](auto) {
332 this->SetDirty();
335 void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override
337 if (!gui_scope) return;
339 this->enabled_actions = this->GetEnabledActions();
340 if (!HasBit(this->enabled_actions, this->sel_index)) {
341 this->sel_index = -1;
346 static WindowDesc _town_authority_desc(
347 WDP_AUTO, "view_town_authority", 317, 222,
348 WC_TOWN_AUTHORITY, WC_NONE,
350 _nested_town_authority_widgets
353 static void ShowTownAuthorityWindow(uint town)
355 AllocateWindowDescFront<TownAuthorityWindow>(_town_authority_desc, town);
359 /* Town view window. */
360 struct TownViewWindow : Window {
361 private:
362 Town *town; ///< Town displayed by the window.
364 public:
365 static const int WID_TV_HEIGHT_NORMAL = 150;
367 TownViewWindow(WindowDesc &desc, WindowNumber window_number) : Window(desc)
369 this->CreateNestedTree();
371 this->town = Town::Get(window_number);
372 if (this->town->larger_town) this->GetWidget<NWidgetCore>(WID_TV_CAPTION)->widget_data = STR_TOWN_VIEW_CITY_CAPTION;
374 this->FinishInitNested(window_number);
376 this->flags |= WF_DISABLE_VP_SCROLL;
377 NWidgetViewport *nvp = this->GetWidget<NWidgetViewport>(WID_TV_VIEWPORT);
378 nvp->InitializeViewport(this, this->town->xy, ScaleZoomGUI(ZOOM_LVL_TOWN));
380 /* disable renaming town in network games if you are not the server */
381 this->SetWidgetDisabledState(WID_TV_CHANGE_NAME, _networking && !_network_server);
384 void Close([[maybe_unused]] int data = 0) override
386 SetViewportCatchmentTown(Town::Get(this->window_number), false);
387 this->Window::Close();
390 void SetStringParameters(WidgetID widget) const override
392 if (widget == WID_TV_CAPTION) SetDParam(0, this->town->index);
395 void OnPaint() override
397 extern const Town *_viewport_highlight_town;
398 this->SetWidgetLoweredState(WID_TV_CATCHMENT, _viewport_highlight_town == this->town);
400 this->DrawWidgets();
403 void DrawWidget(const Rect &r, WidgetID widget) const override
405 if (widget != WID_TV_INFO) return;
407 Rect tr = r.Shrink(WidgetDimensions::scaled.framerect);
409 SetDParam(0, this->town->cache.population);
410 SetDParam(1, this->town->cache.num_houses);
411 DrawString(tr, STR_TOWN_VIEW_POPULATION_HOUSES);
412 tr.top += GetCharacterHeight(FS_NORMAL);
414 StringID str_last_period = TimerGameEconomy::UsingWallclockUnits() ? STR_TOWN_VIEW_CARGO_LAST_MINUTE_MAX : STR_TOWN_VIEW_CARGO_LAST_MONTH_MAX;
416 for (auto tpe : {TPE_PASSENGERS, TPE_MAIL}) {
417 for (const CargoSpec *cs : CargoSpec::town_production_cargoes[tpe]) {
418 CargoID cid = cs->Index();
419 SetDParam(0, 1ULL << cid);
420 SetDParam(1, this->town->supplied[cid].old_act);
421 SetDParam(2, this->town->supplied[cid].old_max);
422 DrawString(tr, str_last_period);
423 tr.top += GetCharacterHeight(FS_NORMAL);
427 bool first = true;
428 for (int i = TAE_BEGIN; i < TAE_END; i++) {
429 if (this->town->goal[i] == 0) continue;
430 if (this->town->goal[i] == TOWN_GROWTH_WINTER && (TileHeight(this->town->xy) < LowestSnowLine() || this->town->cache.population <= 90)) continue;
431 if (this->town->goal[i] == TOWN_GROWTH_DESERT && (GetTropicZone(this->town->xy) != TROPICZONE_DESERT || this->town->cache.population <= 60)) continue;
433 if (first) {
434 DrawString(tr, STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH);
435 tr.top += GetCharacterHeight(FS_NORMAL);
436 first = false;
439 bool rtl = _current_text_dir == TD_RTL;
441 const CargoSpec *cargo = FindFirstCargoWithTownAcceptanceEffect((TownAcceptanceEffect)i);
442 assert(cargo != nullptr);
444 StringID string;
446 if (this->town->goal[i] == TOWN_GROWTH_DESERT || this->town->goal[i] == TOWN_GROWTH_WINTER) {
447 /* For 'original' gameplay, don't show the amount required (you need 1 or more ..) */
448 string = STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_DELIVERED_GENERAL;
449 if (this->town->received[i].old_act == 0) {
450 string = STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL;
452 if (this->town->goal[i] == TOWN_GROWTH_WINTER && TileHeight(this->town->xy) < GetSnowLine()) {
453 string = STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER;
457 SetDParam(0, cargo->name);
458 } else {
459 string = STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_DELIVERED;
460 if (this->town->received[i].old_act < this->town->goal[i]) {
461 string = STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED;
464 SetDParam(0, cargo->Index());
465 SetDParam(1, this->town->received[i].old_act);
466 SetDParam(2, cargo->Index());
467 SetDParam(3, this->town->goal[i]);
469 DrawString(tr.Indent(20, rtl), string);
470 tr.top += GetCharacterHeight(FS_NORMAL);
473 if (HasBit(this->town->flags, TOWN_IS_GROWING)) {
474 SetDParam(0, RoundDivSU(this->town->growth_rate + 1, Ticks::DAY_TICKS));
475 DrawString(tr, this->town->fund_buildings_months == 0 ? STR_TOWN_VIEW_TOWN_GROWS_EVERY : STR_TOWN_VIEW_TOWN_GROWS_EVERY_FUNDED);
476 tr.top += GetCharacterHeight(FS_NORMAL);
477 } else {
478 DrawString(tr, STR_TOWN_VIEW_TOWN_GROW_STOPPED);
479 tr.top += GetCharacterHeight(FS_NORMAL);
482 /* only show the town noise, if the noise option is activated. */
483 if (_settings_game.economy.station_noise_level) {
484 SetDParam(0, this->town->noise_reached);
485 SetDParam(1, this->town->MaxTownNoise());
486 DrawString(tr, STR_TOWN_VIEW_NOISE_IN_TOWN);
487 tr.top += GetCharacterHeight(FS_NORMAL);
490 if (!this->town->text.empty()) {
491 SetDParamStr(0, this->town->text);
492 tr.top = DrawStringMultiLine(tr, STR_JUST_RAW_STRING, TC_BLACK);
496 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
498 switch (widget) {
499 case WID_TV_CENTER_VIEW: // scroll to location
500 if (_ctrl_pressed) {
501 ShowExtraViewportWindow(this->town->xy);
502 } else {
503 ScrollMainWindowToTile(this->town->xy);
505 break;
507 case WID_TV_SHOW_AUTHORITY: // town authority
508 ShowTownAuthorityWindow(this->window_number);
509 break;
511 case WID_TV_CHANGE_NAME: // rename
512 SetDParam(0, this->window_number);
513 ShowQueryString(STR_TOWN_NAME, STR_TOWN_VIEW_RENAME_TOWN_BUTTON, MAX_LENGTH_TOWN_NAME_CHARS, this, CS_ALPHANUMERAL, QSF_ENABLE_DEFAULT | QSF_LEN_IN_CHARS);
514 break;
516 case WID_TV_CATCHMENT:
517 SetViewportCatchmentTown(Town::Get(this->window_number), !this->IsWidgetLowered(WID_TV_CATCHMENT));
518 break;
520 case WID_TV_EXPAND: { // expand town - only available on Scenario editor
521 Command<CMD_EXPAND_TOWN>::Post(STR_ERROR_CAN_T_EXPAND_TOWN, this->window_number, 0);
522 break;
525 case WID_TV_DELETE: // delete town - only available on Scenario editor
526 Command<CMD_DELETE_TOWN>::Post(STR_ERROR_TOWN_CAN_T_DELETE, this->window_number);
527 break;
531 void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override
533 switch (widget) {
534 case WID_TV_INFO:
535 size.height = GetDesiredInfoHeight(size.width) + padding.height;
536 break;
541 * Gets the desired height for the information panel.
542 * @return the desired height in pixels.
544 uint GetDesiredInfoHeight(int width) const
546 uint aimed_height = static_cast<uint>(1 + CargoSpec::town_production_cargoes[TPE_PASSENGERS].size() + CargoSpec::town_production_cargoes[TPE_MAIL].size()) * GetCharacterHeight(FS_NORMAL);
548 bool first = true;
549 for (int i = TAE_BEGIN; i < TAE_END; i++) {
550 if (this->town->goal[i] == 0) continue;
551 if (this->town->goal[i] == TOWN_GROWTH_WINTER && (TileHeight(this->town->xy) < LowestSnowLine() || this->town->cache.population <= 90)) continue;
552 if (this->town->goal[i] == TOWN_GROWTH_DESERT && (GetTropicZone(this->town->xy) != TROPICZONE_DESERT || this->town->cache.population <= 60)) continue;
554 if (first) {
555 aimed_height += GetCharacterHeight(FS_NORMAL);
556 first = false;
558 aimed_height += GetCharacterHeight(FS_NORMAL);
560 aimed_height += GetCharacterHeight(FS_NORMAL);
562 if (_settings_game.economy.station_noise_level) aimed_height += GetCharacterHeight(FS_NORMAL);
564 if (!this->town->text.empty()) {
565 SetDParamStr(0, this->town->text);
566 aimed_height += GetStringHeight(STR_JUST_RAW_STRING, width - WidgetDimensions::scaled.framerect.Horizontal());
569 return aimed_height;
572 void ResizeWindowAsNeeded()
574 const NWidgetBase *nwid_info = this->GetWidget<NWidgetBase>(WID_TV_INFO);
575 uint aimed_height = GetDesiredInfoHeight(nwid_info->current_x);
576 if (aimed_height > nwid_info->current_y || (aimed_height < nwid_info->current_y && nwid_info->current_y > nwid_info->smallest_y)) {
577 this->ReInit();
581 void OnResize() override
583 if (this->viewport != nullptr) {
584 NWidgetViewport *nvp = this->GetWidget<NWidgetViewport>(WID_TV_VIEWPORT);
585 nvp->UpdateViewportCoordinates(this);
587 ScrollWindowToTile(this->town->xy, this, true); // Re-center viewport.
591 void OnMouseWheel(int wheel) override
593 if (_settings_client.gui.scrollwheel_scrolling != SWS_OFF) {
594 DoZoomInOutWindow(wheel < 0 ? ZOOM_IN : ZOOM_OUT, this);
599 * Some data on this window has become invalid.
600 * @param data Information about the changed data.
601 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
603 void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override
605 if (!gui_scope) return;
606 /* Called when setting station noise or required cargoes have changed, in order to resize the window */
607 this->SetDirty(); // refresh display for current size. This will allow to avoid glitches when downgrading
608 this->ResizeWindowAsNeeded();
611 void OnQueryTextFinished(std::optional<std::string> str) override
613 if (!str.has_value()) return;
615 Command<CMD_RENAME_TOWN>::Post(STR_ERROR_CAN_T_RENAME_TOWN, this->window_number, *str);
618 IntervalTimer<TimerGameCalendar> daily_interval = {{TimerGameCalendar::DAY, TimerGameCalendar::Priority::NONE}, [this](auto) {
619 /* Refresh after possible snowline change */
620 this->SetDirty();
624 static constexpr NWidgetPart _nested_town_game_view_widgets[] = {
625 NWidget(NWID_HORIZONTAL),
626 NWidget(WWT_CLOSEBOX, COLOUR_BROWN),
627 NWidget(WWT_PUSHIMGBTN, COLOUR_BROWN, WID_TV_CHANGE_NAME), SetAspect(WidgetDimensions::ASPECT_RENAME), SetDataTip(SPR_RENAME, STR_TOWN_VIEW_RENAME_TOOLTIP),
628 NWidget(WWT_CAPTION, COLOUR_BROWN, WID_TV_CAPTION), SetDataTip(STR_TOWN_VIEW_TOWN_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
629 NWidget(WWT_PUSHIMGBTN, COLOUR_BROWN, WID_TV_CENTER_VIEW), SetAspect(WidgetDimensions::ASPECT_LOCATION), SetDataTip(SPR_GOTO_LOCATION, STR_TOWN_VIEW_CENTER_TOOLTIP),
630 NWidget(WWT_SHADEBOX, COLOUR_BROWN),
631 NWidget(WWT_DEFSIZEBOX, COLOUR_BROWN),
632 NWidget(WWT_STICKYBOX, COLOUR_BROWN),
633 EndContainer(),
634 NWidget(WWT_PANEL, COLOUR_BROWN),
635 NWidget(WWT_INSET, COLOUR_BROWN), SetPadding(2, 2, 2, 2),
636 NWidget(NWID_VIEWPORT, INVALID_COLOUR, WID_TV_VIEWPORT), SetMinimalSize(254, 86), SetFill(1, 0), SetResize(1, 1),
637 EndContainer(),
638 EndContainer(),
639 NWidget(WWT_PANEL, COLOUR_BROWN, WID_TV_INFO), SetMinimalSize(260, 32), SetResize(1, 0), SetFill(1, 0), EndContainer(),
640 NWidget(NWID_HORIZONTAL, NC_EQUALSIZE),
641 NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_TV_SHOW_AUTHORITY), SetMinimalSize(80, 12), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_TOWN_VIEW_LOCAL_AUTHORITY_BUTTON, STR_TOWN_VIEW_LOCAL_AUTHORITY_TOOLTIP),
642 NWidget(WWT_TEXTBTN, COLOUR_BROWN, WID_TV_CATCHMENT), SetMinimalSize(40, 12), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_BUTTON_CATCHMENT, STR_TOOLTIP_CATCHMENT),
643 NWidget(WWT_RESIZEBOX, COLOUR_BROWN),
644 EndContainer(),
647 static WindowDesc _town_game_view_desc(
648 WDP_AUTO, "view_town", 260, TownViewWindow::WID_TV_HEIGHT_NORMAL,
649 WC_TOWN_VIEW, WC_NONE,
651 _nested_town_game_view_widgets
654 static constexpr NWidgetPart _nested_town_editor_view_widgets[] = {
655 NWidget(NWID_HORIZONTAL),
656 NWidget(WWT_CLOSEBOX, COLOUR_BROWN),
657 NWidget(WWT_PUSHIMGBTN, COLOUR_BROWN, WID_TV_CHANGE_NAME), SetAspect(WidgetDimensions::ASPECT_RENAME), SetDataTip(SPR_RENAME, STR_TOWN_VIEW_RENAME_TOOLTIP),
658 NWidget(WWT_CAPTION, COLOUR_BROWN, WID_TV_CAPTION), SetDataTip(STR_TOWN_VIEW_TOWN_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
659 NWidget(WWT_PUSHIMGBTN, COLOUR_BROWN, WID_TV_CENTER_VIEW), SetAspect(WidgetDimensions::ASPECT_LOCATION), SetDataTip(SPR_GOTO_LOCATION, STR_TOWN_VIEW_CENTER_TOOLTIP),
660 NWidget(WWT_SHADEBOX, COLOUR_BROWN),
661 NWidget(WWT_DEFSIZEBOX, COLOUR_BROWN),
662 NWidget(WWT_STICKYBOX, COLOUR_BROWN),
663 EndContainer(),
664 NWidget(WWT_PANEL, COLOUR_BROWN),
665 NWidget(WWT_INSET, COLOUR_BROWN), SetPadding(2, 2, 2, 2),
666 NWidget(NWID_VIEWPORT, INVALID_COLOUR, WID_TV_VIEWPORT), SetMinimalSize(254, 86), SetFill(1, 1), SetResize(1, 1),
667 EndContainer(),
668 EndContainer(),
669 NWidget(WWT_PANEL, COLOUR_BROWN, WID_TV_INFO), SetMinimalSize(260, 32), SetResize(1, 0), SetFill(1, 0), EndContainer(),
670 NWidget(NWID_HORIZONTAL, NC_EQUALSIZE),
671 NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_TV_EXPAND), SetMinimalSize(80, 12), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_TOWN_VIEW_EXPAND_BUTTON, STR_TOWN_VIEW_EXPAND_TOOLTIP),
672 NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_TV_DELETE), SetMinimalSize(80, 12), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_TOWN_VIEW_DELETE_BUTTON, STR_TOWN_VIEW_DELETE_TOOLTIP),
673 NWidget(WWT_TEXTBTN, COLOUR_BROWN, WID_TV_CATCHMENT), SetMinimalSize(40, 12), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_BUTTON_CATCHMENT, STR_TOOLTIP_CATCHMENT),
674 NWidget(WWT_RESIZEBOX, COLOUR_BROWN),
675 EndContainer(),
678 static WindowDesc _town_editor_view_desc(
679 WDP_AUTO, "view_town_scen", 260, TownViewWindow::WID_TV_HEIGHT_NORMAL,
680 WC_TOWN_VIEW, WC_NONE,
682 _nested_town_editor_view_widgets
685 void ShowTownViewWindow(TownID town)
687 if (_game_mode == GM_EDITOR) {
688 AllocateWindowDescFront<TownViewWindow>(_town_editor_view_desc, town);
689 } else {
690 AllocateWindowDescFront<TownViewWindow>(_town_game_view_desc, town);
694 static constexpr NWidgetPart _nested_town_directory_widgets[] = {
695 NWidget(NWID_HORIZONTAL),
696 NWidget(WWT_CLOSEBOX, COLOUR_BROWN),
697 NWidget(WWT_CAPTION, COLOUR_BROWN, WID_TD_CAPTION), SetDataTip(STR_TOWN_DIRECTORY_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
698 NWidget(WWT_SHADEBOX, COLOUR_BROWN),
699 NWidget(WWT_DEFSIZEBOX, COLOUR_BROWN),
700 NWidget(WWT_STICKYBOX, COLOUR_BROWN),
701 EndContainer(),
702 NWidget(NWID_HORIZONTAL),
703 NWidget(NWID_VERTICAL),
704 NWidget(NWID_HORIZONTAL),
705 NWidget(WWT_TEXTBTN, COLOUR_BROWN, WID_TD_SORT_ORDER), SetDataTip(STR_BUTTON_SORT_BY, STR_TOOLTIP_SORT_ORDER),
706 NWidget(WWT_DROPDOWN, COLOUR_BROWN, WID_TD_SORT_CRITERIA), SetDataTip(STR_JUST_STRING, STR_TOOLTIP_SORT_CRITERIA),
707 NWidget(WWT_EDITBOX, COLOUR_BROWN, WID_TD_FILTER), SetFill(1, 0), SetResize(1, 0), SetDataTip(STR_LIST_FILTER_OSKTITLE, STR_LIST_FILTER_TOOLTIP),
708 EndContainer(),
709 NWidget(WWT_PANEL, COLOUR_BROWN, WID_TD_LIST), SetDataTip(0x0, STR_TOWN_DIRECTORY_LIST_TOOLTIP),
710 SetFill(1, 0), SetResize(1, 1), SetScrollbar(WID_TD_SCROLLBAR), EndContainer(),
711 NWidget(WWT_PANEL, COLOUR_BROWN),
712 NWidget(WWT_TEXT, COLOUR_BROWN, WID_TD_WORLD_POPULATION), SetPadding(2, 0, 2, 2), SetFill(1, 0), SetResize(1, 0), SetDataTip(STR_TOWN_POPULATION, STR_NULL),
713 EndContainer(),
714 EndContainer(),
715 NWidget(NWID_VERTICAL),
716 NWidget(NWID_VSCROLLBAR, COLOUR_BROWN, WID_TD_SCROLLBAR),
717 NWidget(WWT_RESIZEBOX, COLOUR_BROWN),
718 EndContainer(),
719 EndContainer(),
722 /** Enum referring to the Hotkeys in the town directory window */
723 enum TownDirectoryHotkeys {
724 TDHK_FOCUS_FILTER_BOX, ///< Focus the filter box
727 /** Town directory window class. */
728 struct TownDirectoryWindow : public Window {
729 private:
730 /* Runtime saved values */
731 static Listing last_sorting;
733 /* Constants for sorting towns */
734 static inline const StringID sorter_names[] = {
735 STR_SORT_BY_NAME,
736 STR_SORT_BY_POPULATION,
737 STR_SORT_BY_RATING,
739 static const std::initializer_list<GUITownList::SortFunction * const> sorter_funcs;
741 StringFilter string_filter; ///< Filter for towns
742 QueryString townname_editbox; ///< Filter editbox
744 GUITownList towns{TownDirectoryWindow::last_sorting.order};
746 Scrollbar *vscroll;
748 void BuildSortTownList()
750 if (this->towns.NeedRebuild()) {
751 this->towns.clear();
752 this->towns.reserve(Town::GetNumItems());
754 for (const Town *t : Town::Iterate()) {
755 if (this->string_filter.IsEmpty()) {
756 this->towns.push_back(t);
757 continue;
759 this->string_filter.ResetState();
760 this->string_filter.AddLine(t->GetCachedName());
761 if (this->string_filter.GetState()) this->towns.push_back(t);
764 this->towns.RebuildDone();
765 this->vscroll->SetCount(this->towns.size()); // Update scrollbar as well.
767 /* Always sort the towns. */
768 this->towns.Sort();
769 this->SetWidgetDirty(WID_TD_LIST); // Force repaint of the displayed towns.
772 /** Sort by town name */
773 static bool TownNameSorter(const Town * const &a, const Town * const &b, const bool &)
775 return StrNaturalCompare(a->GetCachedName(), b->GetCachedName()) < 0; // Sort by name (natural sorting).
778 /** Sort by population (default descending, as big towns are of the most interest). */
779 static bool TownPopulationSorter(const Town * const &a, const Town * const &b, const bool &order)
781 uint32_t a_population = a->cache.population;
782 uint32_t b_population = b->cache.population;
783 if (a_population == b_population) return TownDirectoryWindow::TownNameSorter(a, b, order);
784 return a_population < b_population;
787 /** Sort by town rating */
788 static bool TownRatingSorter(const Town * const &a, const Town * const &b, const bool &order)
790 bool before = !order; // Value to get 'a' before 'b'.
792 /* Towns without rating are always after towns with rating. */
793 if (HasBit(a->have_ratings, _local_company)) {
794 if (HasBit(b->have_ratings, _local_company)) {
795 int16_t a_rating = a->ratings[_local_company];
796 int16_t b_rating = b->ratings[_local_company];
797 if (a_rating == b_rating) return TownDirectoryWindow::TownNameSorter(a, b, order);
798 return a_rating < b_rating;
800 return before;
802 if (HasBit(b->have_ratings, _local_company)) return !before;
804 /* Sort unrated towns always on ascending town name. */
805 if (before) return TownDirectoryWindow::TownNameSorter(a, b, order);
806 return TownDirectoryWindow::TownNameSorter(b, a, order);
809 public:
810 TownDirectoryWindow(WindowDesc &desc) : Window(desc), townname_editbox(MAX_LENGTH_TOWN_NAME_CHARS * MAX_CHAR_LENGTH, MAX_LENGTH_TOWN_NAME_CHARS)
812 this->CreateNestedTree();
814 this->vscroll = this->GetScrollbar(WID_TD_SCROLLBAR);
816 this->towns.SetListing(this->last_sorting);
817 this->towns.SetSortFuncs(TownDirectoryWindow::sorter_funcs);
818 this->towns.ForceRebuild();
819 this->BuildSortTownList();
821 this->FinishInitNested(0);
823 this->querystrings[WID_TD_FILTER] = &this->townname_editbox;
824 this->townname_editbox.cancel_button = QueryString::ACTION_CLEAR;
827 void SetStringParameters(WidgetID widget) const override
829 switch (widget) {
830 case WID_TD_CAPTION:
831 SetDParam(0, this->vscroll->GetCount());
832 SetDParam(1, Town::GetNumItems());
833 break;
835 case WID_TD_WORLD_POPULATION:
836 SetDParam(0, GetWorldPopulation());
837 break;
839 case WID_TD_SORT_CRITERIA:
840 SetDParam(0, TownDirectoryWindow::sorter_names[this->towns.SortType()]);
841 break;
846 * Get the string to draw the town name.
847 * @param t Town to draw.
848 * @return The string to use.
850 static StringID GetTownString(const Town *t)
852 return t->larger_town ? STR_TOWN_DIRECTORY_CITY : STR_TOWN_DIRECTORY_TOWN;
855 void DrawWidget(const Rect &r, WidgetID widget) const override
857 switch (widget) {
858 case WID_TD_SORT_ORDER:
859 this->DrawSortButtonState(widget, this->towns.IsDescSortOrder() ? SBS_DOWN : SBS_UP);
860 break;
862 case WID_TD_LIST: {
863 Rect tr = r.Shrink(WidgetDimensions::scaled.framerect);
864 if (this->towns.empty()) { // No towns available.
865 DrawString(tr, STR_TOWN_DIRECTORY_NONE);
866 break;
869 /* At least one town available. */
870 bool rtl = _current_text_dir == TD_RTL;
871 Dimension icon_size = GetSpriteSize(SPR_TOWN_RATING_GOOD);
872 int icon_x = tr.WithWidth(icon_size.width, rtl).left;
873 tr = tr.Indent(icon_size.width + WidgetDimensions::scaled.hsep_normal, rtl);
875 auto [first, last] = this->vscroll->GetVisibleRangeIterators(this->towns);
876 for (auto it = first; it != last; ++it) {
877 const Town *t = *it;
878 assert(t->xy != INVALID_TILE);
880 /* Draw rating icon. */
881 if (_game_mode == GM_EDITOR || !HasBit(t->have_ratings, _local_company)) {
882 DrawSprite(SPR_TOWN_RATING_NA, PAL_NONE, icon_x, tr.top + (this->resize.step_height - icon_size.height) / 2);
883 } else {
884 SpriteID icon = SPR_TOWN_RATING_APALLING;
885 if (t->ratings[_local_company] > RATING_VERYPOOR) icon = SPR_TOWN_RATING_MEDIOCRE;
886 if (t->ratings[_local_company] > RATING_GOOD) icon = SPR_TOWN_RATING_GOOD;
887 DrawSprite(icon, PAL_NONE, icon_x, tr.top + (this->resize.step_height - icon_size.height) / 2);
890 SetDParam(0, t->index);
891 SetDParam(1, t->cache.population);
892 DrawString(tr.left, tr.right, tr.top + (this->resize.step_height - GetCharacterHeight(FS_NORMAL)) / 2, GetTownString(t));
894 tr.top += this->resize.step_height;
896 break;
901 void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override
903 switch (widget) {
904 case WID_TD_SORT_ORDER: {
905 Dimension d = GetStringBoundingBox(this->GetWidget<NWidgetCore>(widget)->widget_data);
906 d.width += padding.width + Window::SortButtonWidth() * 2; // Doubled since the string is centred and it also looks better.
907 d.height += padding.height;
908 size = maxdim(size, d);
909 break;
911 case WID_TD_SORT_CRITERIA: {
912 Dimension d = GetStringListBoundingBox(TownDirectoryWindow::sorter_names);
913 d.width += padding.width;
914 d.height += padding.height;
915 size = maxdim(size, d);
916 break;
918 case WID_TD_LIST: {
919 Dimension d = GetStringBoundingBox(STR_TOWN_DIRECTORY_NONE);
920 for (uint i = 0; i < this->towns.size(); i++) {
921 const Town *t = this->towns[i];
923 assert(t != nullptr);
925 SetDParam(0, t->index);
926 SetDParamMaxDigits(1, 8);
927 d = maxdim(d, GetStringBoundingBox(GetTownString(t)));
929 Dimension icon_size = GetSpriteSize(SPR_TOWN_RATING_GOOD);
930 d.width += icon_size.width + 2;
931 d.height = std::max(d.height, icon_size.height);
932 resize.height = d.height;
933 d.height *= 5;
934 d.width += padding.width;
935 d.height += padding.height;
936 size = maxdim(size, d);
937 break;
939 case WID_TD_WORLD_POPULATION: {
940 SetDParamMaxDigits(0, 10);
941 Dimension d = GetStringBoundingBox(STR_TOWN_POPULATION);
942 d.width += padding.width;
943 d.height += padding.height;
944 size = maxdim(size, d);
945 break;
950 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
952 switch (widget) {
953 case WID_TD_SORT_ORDER: // Click on sort order button
954 if (this->towns.SortType() != 2) { // A different sort than by rating.
955 this->towns.ToggleSortOrder();
956 this->last_sorting = this->towns.GetListing(); // Store new sorting order.
957 } else {
958 /* Some parts are always sorted ascending on name. */
959 this->last_sorting.order = !this->last_sorting.order;
960 this->towns.SetListing(this->last_sorting);
961 this->towns.ForceResort();
962 this->towns.Sort();
964 this->SetDirty();
965 break;
967 case WID_TD_SORT_CRITERIA: // Click on sort criteria dropdown
968 ShowDropDownMenu(this, TownDirectoryWindow::sorter_names, this->towns.SortType(), WID_TD_SORT_CRITERIA, 0, 0);
969 break;
971 case WID_TD_LIST: { // Click on Town Matrix
972 auto it = this->vscroll->GetScrolledItemFromWidget(this->towns, pt.y, this, WID_TD_LIST, WidgetDimensions::scaled.framerect.top);
973 if (it == this->towns.end()) return; // click out of town bounds
975 const Town *t = *it;
976 assert(t != nullptr);
977 if (_ctrl_pressed) {
978 ShowExtraViewportWindow(t->xy);
979 } else {
980 ScrollMainWindowToTile(t->xy);
982 break;
987 void OnDropdownSelect(WidgetID widget, int index) override
989 if (widget != WID_TD_SORT_CRITERIA) return;
991 if (this->towns.SortType() != index) {
992 this->towns.SetSortType(index);
993 this->last_sorting = this->towns.GetListing(); // Store new sorting order.
994 this->BuildSortTownList();
998 void OnPaint() override
1000 if (this->towns.NeedRebuild()) this->BuildSortTownList();
1001 this->DrawWidgets();
1004 /** Redraw the whole window on a regular interval. */
1005 IntervalTimer<TimerWindow> rebuild_interval = {std::chrono::seconds(3), [this](auto) {
1006 this->BuildSortTownList();
1007 this->SetDirty();
1010 void OnResize() override
1012 this->vscroll->SetCapacityFromWidget(this, WID_TD_LIST, WidgetDimensions::scaled.framerect.Vertical());
1015 void OnEditboxChanged(WidgetID wid) override
1017 if (wid == WID_TD_FILTER) {
1018 this->string_filter.SetFilterTerm(this->townname_editbox.text.buf);
1019 this->InvalidateData(TDIWD_FORCE_REBUILD);
1024 * Some data on this window has become invalid.
1025 * @param data Information about the changed data.
1026 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
1028 void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override
1030 switch (data) {
1031 case TDIWD_FORCE_REBUILD:
1032 /* This needs to be done in command-scope to enforce rebuilding before resorting invalid data */
1033 this->towns.ForceRebuild();
1034 break;
1036 case TDIWD_POPULATION_CHANGE:
1037 if (this->towns.SortType() == 1) this->towns.ForceResort();
1038 break;
1040 default:
1041 this->towns.ForceResort();
1045 EventState OnHotkey(int hotkey) override
1047 switch (hotkey) {
1048 case TDHK_FOCUS_FILTER_BOX:
1049 this->SetFocusedWidget(WID_TD_FILTER);
1050 SetFocusedWindow(this); // The user has asked to give focus to the text box, so make sure this window is focused.
1051 break;
1052 default:
1053 return ES_NOT_HANDLED;
1055 return ES_HANDLED;
1058 static inline HotkeyList hotkeys {"towndirectory", {
1059 Hotkey('F', "focus_filter_box", TDHK_FOCUS_FILTER_BOX),
1063 Listing TownDirectoryWindow::last_sorting = {false, 0};
1065 /** Available town directory sorting functions. */
1066 const std::initializer_list<GUITownList::SortFunction * const> TownDirectoryWindow::sorter_funcs = {
1067 &TownNameSorter,
1068 &TownPopulationSorter,
1069 &TownRatingSorter,
1072 static WindowDesc _town_directory_desc(
1073 WDP_AUTO, "list_towns", 208, 202,
1074 WC_TOWN_DIRECTORY, WC_NONE,
1076 _nested_town_directory_widgets,
1077 &TownDirectoryWindow::hotkeys
1080 void ShowTownDirectory()
1082 if (BringWindowToFrontById(WC_TOWN_DIRECTORY, 0)) return;
1083 new TownDirectoryWindow(_town_directory_desc);
1086 void CcFoundTown(Commands, const CommandCost &result, TileIndex tile)
1088 if (result.Failed()) return;
1090 if (_settings_client.sound.confirm) SndPlayTileFx(SND_1F_CONSTRUCTION_OTHER, tile);
1091 if (!_settings_client.gui.persistent_buildingtools) ResetObjectToPlace();
1094 void CcFoundRandomTown(Commands, const CommandCost &result, Money, TownID town_id)
1096 if (result.Succeeded()) ScrollMainWindowToTile(Town::Get(town_id)->xy);
1099 static constexpr NWidgetPart _nested_found_town_widgets[] = {
1100 NWidget(NWID_HORIZONTAL),
1101 NWidget(WWT_CLOSEBOX, COLOUR_DARK_GREEN),
1102 NWidget(WWT_CAPTION, COLOUR_DARK_GREEN), SetDataTip(STR_FOUND_TOWN_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
1103 NWidget(WWT_SHADEBOX, COLOUR_DARK_GREEN),
1104 NWidget(WWT_STICKYBOX, COLOUR_DARK_GREEN),
1105 EndContainer(),
1106 /* Construct new town(s) buttons. */
1107 NWidget(WWT_PANEL, COLOUR_DARK_GREEN),
1108 NWidget(NWID_VERTICAL), SetPIP(0, WidgetDimensions::unscaled.vsep_normal, 0), SetPadding(WidgetDimensions::unscaled.picker),
1109 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_TF_NEW_TOWN), SetDataTip(STR_FOUND_TOWN_NEW_TOWN_BUTTON, STR_FOUND_TOWN_NEW_TOWN_TOOLTIP), SetFill(1, 0),
1110 NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_TF_RANDOM_TOWN), SetDataTip(STR_FOUND_TOWN_RANDOM_TOWN_BUTTON, STR_FOUND_TOWN_RANDOM_TOWN_TOOLTIP), SetFill(1, 0),
1111 NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_TF_MANY_RANDOM_TOWNS), SetDataTip(STR_FOUND_TOWN_MANY_RANDOM_TOWNS, STR_FOUND_TOWN_RANDOM_TOWNS_TOOLTIP), SetFill(1, 0),
1112 NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_TF_LOAD_FROM_FILE), SetDataTip(STR_FOUND_TOWN_LOAD_FROM_FILE, STR_FOUND_TOWN_LOAD_FROM_FILE_TOOLTIP), SetFill(1, 0),
1113 NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_TF_EXPAND_ALL_TOWNS), SetDataTip(STR_FOUND_TOWN_EXPAND_ALL_TOWNS, STR_FOUND_TOWN_EXPAND_ALL_TOWNS_TOOLTIP), SetFill(1, 0),
1115 /* Town name selection. */
1116 NWidget(WWT_LABEL, COLOUR_DARK_GREEN), SetDataTip(STR_FOUND_TOWN_NAME_TITLE, STR_NULL),
1117 NWidget(WWT_EDITBOX, COLOUR_GREY, WID_TF_TOWN_NAME_EDITBOX), SetDataTip(STR_FOUND_TOWN_NAME_EDITOR_TITLE, STR_FOUND_TOWN_NAME_EDITOR_HELP), SetFill(1, 0),
1118 NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_TF_TOWN_NAME_RANDOM), SetDataTip(STR_FOUND_TOWN_NAME_RANDOM_BUTTON, STR_FOUND_TOWN_NAME_RANDOM_TOOLTIP), SetFill(1, 0),
1120 /* Town size selection. */
1121 NWidget(WWT_LABEL, COLOUR_DARK_GREEN), SetDataTip(STR_FOUND_TOWN_INITIAL_SIZE_TITLE, STR_NULL),
1122 NWidget(NWID_VERTICAL),
1123 NWidget(NWID_HORIZONTAL, NC_EQUALSIZE),
1124 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_TF_SIZE_SMALL), SetDataTip(STR_FOUND_TOWN_INITIAL_SIZE_SMALL_BUTTON, STR_FOUND_TOWN_INITIAL_SIZE_TOOLTIP), SetFill(1, 0),
1125 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_TF_SIZE_MEDIUM), SetDataTip(STR_FOUND_TOWN_INITIAL_SIZE_MEDIUM_BUTTON, STR_FOUND_TOWN_INITIAL_SIZE_TOOLTIP), SetFill(1, 0),
1126 EndContainer(),
1127 NWidget(NWID_HORIZONTAL, NC_EQUALSIZE),
1128 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_TF_SIZE_LARGE), SetDataTip(STR_FOUND_TOWN_INITIAL_SIZE_LARGE_BUTTON, STR_FOUND_TOWN_INITIAL_SIZE_TOOLTIP), SetFill(1, 0),
1129 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_TF_SIZE_RANDOM), SetDataTip(STR_FOUND_TOWN_SIZE_RANDOM, STR_FOUND_TOWN_INITIAL_SIZE_TOOLTIP), SetFill(1, 0),
1130 EndContainer(),
1131 EndContainer(),
1132 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_TF_CITY), SetDataTip(STR_FOUND_TOWN_CITY, STR_FOUND_TOWN_CITY_TOOLTIP), SetFill(1, 0),
1134 /* Town roads selection. */
1135 NWidget(WWT_LABEL, COLOUR_DARK_GREEN), SetDataTip(STR_FOUND_TOWN_ROAD_LAYOUT, STR_NULL),
1136 NWidget(NWID_VERTICAL),
1137 NWidget(NWID_HORIZONTAL, NC_EQUALSIZE),
1138 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_TF_LAYOUT_ORIGINAL), SetDataTip(STR_FOUND_TOWN_SELECT_LAYOUT_ORIGINAL, STR_FOUND_TOWN_SELECT_TOWN_ROAD_LAYOUT), SetFill(1, 0),
1139 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_TF_LAYOUT_BETTER), SetDataTip(STR_FOUND_TOWN_SELECT_LAYOUT_BETTER_ROADS, STR_FOUND_TOWN_SELECT_TOWN_ROAD_LAYOUT), SetFill(1, 0),
1140 EndContainer(),
1141 NWidget(NWID_HORIZONTAL, NC_EQUALSIZE),
1142 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_TF_LAYOUT_GRID2), SetDataTip(STR_FOUND_TOWN_SELECT_LAYOUT_2X2_GRID, STR_FOUND_TOWN_SELECT_TOWN_ROAD_LAYOUT), SetFill(1, 0),
1143 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_TF_LAYOUT_GRID3), SetDataTip(STR_FOUND_TOWN_SELECT_LAYOUT_3X3_GRID, STR_FOUND_TOWN_SELECT_TOWN_ROAD_LAYOUT), SetFill(1, 0),
1144 EndContainer(),
1145 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_TF_LAYOUT_RANDOM), SetDataTip(STR_FOUND_TOWN_SELECT_LAYOUT_RANDOM, STR_FOUND_TOWN_SELECT_TOWN_ROAD_LAYOUT), SetFill(1, 0),
1146 EndContainer(),
1147 EndContainer(),
1148 EndContainer(),
1151 /** Found a town window class. */
1152 struct FoundTownWindow : Window {
1153 private:
1154 TownSize town_size; ///< Selected town size
1155 TownLayout town_layout; ///< Selected town layout
1156 bool city; ///< Are we building a city?
1157 QueryString townname_editbox; ///< Townname editbox
1158 bool townnamevalid; ///< Is generated town name valid?
1159 uint32_t townnameparts; ///< Generated town name
1160 TownNameParams params; ///< Town name parameters
1162 public:
1163 FoundTownWindow(WindowDesc &desc, WindowNumber window_number) :
1164 Window(desc),
1165 town_size(TSZ_MEDIUM),
1166 town_layout(_settings_game.economy.town_layout),
1167 townname_editbox(MAX_LENGTH_TOWN_NAME_CHARS * MAX_CHAR_LENGTH, MAX_LENGTH_TOWN_NAME_CHARS),
1168 params(_settings_game.game_creation.town_name)
1170 this->InitNested(window_number);
1171 this->querystrings[WID_TF_TOWN_NAME_EDITBOX] = &this->townname_editbox;
1172 this->RandomTownName();
1173 this->UpdateButtons(true);
1176 void RandomTownName()
1178 this->townnamevalid = GenerateTownName(_interactive_random, &this->townnameparts);
1180 if (!this->townnamevalid) {
1181 this->townname_editbox.text.DeleteAll();
1182 } else {
1183 this->townname_editbox.text.Assign(GetTownName(&this->params, this->townnameparts));
1185 UpdateOSKOriginalText(this, WID_TF_TOWN_NAME_EDITBOX);
1187 this->SetWidgetDirty(WID_TF_TOWN_NAME_EDITBOX);
1190 void UpdateButtons(bool check_availability)
1192 if (check_availability && _game_mode != GM_EDITOR) {
1193 this->SetWidgetsDisabledState(true, WID_TF_RANDOM_TOWN, WID_TF_MANY_RANDOM_TOWNS, WID_TF_LOAD_FROM_FILE, WID_TF_EXPAND_ALL_TOWNS, WID_TF_SIZE_LARGE);
1194 this->SetWidgetsDisabledState(_settings_game.economy.found_town != TF_CUSTOM_LAYOUT,
1195 WID_TF_LAYOUT_ORIGINAL, WID_TF_LAYOUT_BETTER, WID_TF_LAYOUT_GRID2, WID_TF_LAYOUT_GRID3, WID_TF_LAYOUT_RANDOM);
1196 if (_settings_game.economy.found_town != TF_CUSTOM_LAYOUT) town_layout = _settings_game.economy.town_layout;
1199 for (WidgetID i = WID_TF_SIZE_SMALL; i <= WID_TF_SIZE_RANDOM; i++) {
1200 this->SetWidgetLoweredState(i, i == WID_TF_SIZE_SMALL + this->town_size);
1203 this->SetWidgetLoweredState(WID_TF_CITY, this->city);
1205 for (WidgetID i = WID_TF_LAYOUT_ORIGINAL; i <= WID_TF_LAYOUT_RANDOM; i++) {
1206 this->SetWidgetLoweredState(i, i == WID_TF_LAYOUT_ORIGINAL + this->town_layout);
1209 this->SetDirty();
1212 template <typename Tcallback>
1213 void ExecuteFoundTownCommand(TileIndex tile, bool random, StringID errstr, Tcallback cc)
1215 std::string name;
1217 if (!this->townnamevalid) {
1218 name = this->townname_editbox.text.buf;
1219 } else {
1220 /* If user changed the name, send it */
1221 std::string original_name = GetTownName(&this->params, this->townnameparts);
1222 if (original_name != this->townname_editbox.text.buf) name = this->townname_editbox.text.buf;
1225 bool success = Command<CMD_FOUND_TOWN>::Post(errstr, cc,
1226 tile, this->town_size, this->city, this->town_layout, random, townnameparts, name);
1228 /* Rerandomise name, if success and no cost-estimation. */
1229 if (success && !_shift_pressed) this->RandomTownName();
1232 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
1234 switch (widget) {
1235 case WID_TF_NEW_TOWN:
1236 HandlePlacePushButton(this, WID_TF_NEW_TOWN, SPR_CURSOR_TOWN, HT_RECT);
1237 break;
1239 case WID_TF_RANDOM_TOWN:
1240 this->ExecuteFoundTownCommand(0, true, STR_ERROR_CAN_T_GENERATE_TOWN, CcFoundRandomTown);
1241 break;
1243 case WID_TF_TOWN_NAME_RANDOM:
1244 this->RandomTownName();
1245 this->SetFocusedWidget(WID_TF_TOWN_NAME_EDITBOX);
1246 break;
1248 case WID_TF_MANY_RANDOM_TOWNS: {
1249 Backup<bool> old_generating_world(_generating_world, true);
1250 UpdateNearestTownForRoadTiles(true);
1251 if (!GenerateTowns(this->town_layout)) {
1252 ShowErrorMessage(STR_ERROR_CAN_T_GENERATE_TOWN, STR_ERROR_NO_SPACE_FOR_TOWN, WL_INFO);
1254 UpdateNearestTownForRoadTiles(false);
1255 old_generating_world.Restore();
1256 break;
1259 case WID_TF_LOAD_FROM_FILE:
1260 ShowSaveLoadDialog(FT_TOWN_DATA, SLO_LOAD);
1261 break;
1263 case WID_TF_EXPAND_ALL_TOWNS:
1264 for (Town *t : Town::Iterate()) {
1265 Command<CMD_EXPAND_TOWN>::Do(DC_EXEC, t->index, 0);
1267 break;
1269 case WID_TF_SIZE_SMALL: case WID_TF_SIZE_MEDIUM: case WID_TF_SIZE_LARGE: case WID_TF_SIZE_RANDOM:
1270 this->town_size = (TownSize)(widget - WID_TF_SIZE_SMALL);
1271 this->UpdateButtons(false);
1272 break;
1274 case WID_TF_CITY:
1275 this->city ^= true;
1276 this->SetWidgetLoweredState(WID_TF_CITY, this->city);
1277 this->SetDirty();
1278 break;
1280 case WID_TF_LAYOUT_ORIGINAL: case WID_TF_LAYOUT_BETTER: case WID_TF_LAYOUT_GRID2:
1281 case WID_TF_LAYOUT_GRID3: case WID_TF_LAYOUT_RANDOM:
1282 this->town_layout = (TownLayout)(widget - WID_TF_LAYOUT_ORIGINAL);
1284 /* If we are in the editor, sync the settings of the current game to the chosen layout,
1285 * so that importing towns from file uses the selected layout. */
1286 if (_game_mode == GM_EDITOR) _settings_game.economy.town_layout = this->town_layout;
1288 this->UpdateButtons(false);
1289 break;
1293 void OnPlaceObject([[maybe_unused]] Point pt, TileIndex tile) override
1295 this->ExecuteFoundTownCommand(tile, false, STR_ERROR_CAN_T_FOUND_TOWN_HERE, CcFoundTown);
1298 void OnPlaceObjectAbort() override
1300 this->RaiseButtons();
1301 this->UpdateButtons(false);
1305 * Some data on this window has become invalid.
1306 * @param data Information about the changed data.
1307 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
1309 void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override
1311 if (!gui_scope) return;
1312 this->UpdateButtons(true);
1316 static WindowDesc _found_town_desc(
1317 WDP_AUTO, "build_town", 160, 162,
1318 WC_FOUND_TOWN, WC_NONE,
1319 WDF_CONSTRUCTION,
1320 _nested_found_town_widgets
1323 void ShowFoundTownWindow()
1325 if (_game_mode != GM_EDITOR && !Company::IsValidID(_local_company)) return;
1326 AllocateWindowDescFront<FoundTownWindow>(_found_town_desc, 0);
1329 void InitializeTownGui()
1331 _town_local_authority_kdtree.Clear();
1335 * Draw representation of a house tile for GUI purposes.
1336 * @param x Position x of image.
1337 * @param y Position y of image.
1338 * @param spec House spec to draw.
1339 * @param house_id House ID to draw.
1340 * @param view The house's 'view'.
1342 void DrawNewHouseTileInGUI(int x, int y, const HouseSpec *spec, HouseID house_id, int view)
1344 HouseResolverObject object(house_id, INVALID_TILE, nullptr, CBID_NO_CALLBACK, 0, 0, true, view);
1345 const SpriteGroup *group = object.Resolve();
1346 if (group == nullptr || group->type != SGT_TILELAYOUT) return;
1348 uint8_t stage = TOWN_HOUSE_COMPLETED;
1349 const DrawTileSprites *dts = reinterpret_cast<const TileLayoutSpriteGroup *>(group)->ProcessRegisters(&stage);
1351 PaletteID palette = GENERAL_SPRITE_COLOUR(spec->random_colour[0]);
1352 if (HasBit(spec->callback_mask, CBM_HOUSE_COLOUR)) {
1353 uint16_t callback = GetHouseCallback(CBID_HOUSE_COLOUR, 0, 0, house_id, nullptr, INVALID_TILE, true, view);
1354 if (callback != CALLBACK_FAILED) {
1355 /* If bit 14 is set, we should use a 2cc colour map, else use the callback value. */
1356 palette = HasBit(callback, 14) ? GB(callback, 0, 8) + SPR_2CCMAP_BASE : callback;
1360 SpriteID image = dts->ground.sprite;
1361 PaletteID pal = dts->ground.pal;
1363 if (HasBit(image, SPRITE_MODIFIER_CUSTOM_SPRITE)) image += stage;
1364 if (HasBit(pal, SPRITE_MODIFIER_CUSTOM_SPRITE)) pal += stage;
1366 if (GB(image, 0, SPRITE_WIDTH) != 0) {
1367 DrawSprite(image, GroundSpritePaletteTransform(image, pal, palette), x, y);
1370 DrawNewGRFTileSeqInGUI(x, y, dts, stage, palette);
1374 * Draw a house that does not exist.
1375 * @param x Position x of image.
1376 * @param y Position y of image.
1377 * @param house_id House ID to draw.
1378 * @param view The house's 'view'.
1380 void DrawHouseInGUI(int x, int y, HouseID house_id, int view)
1382 auto draw = [](int x, int y, HouseID house_id, int view) {
1383 if (house_id >= NEW_HOUSE_OFFSET) {
1384 /* Houses don't necessarily need new graphics. If they don't have a
1385 * spritegroup associated with them, then the sprite for the substitute
1386 * house id is drawn instead. */
1387 const HouseSpec *spec = HouseSpec::Get(house_id);
1388 if (spec->grf_prop.spritegroup[0] != nullptr) {
1389 DrawNewHouseTileInGUI(x, y, spec, house_id, view);
1390 return;
1391 } else {
1392 house_id = HouseSpec::Get(house_id)->grf_prop.subst_id;
1396 /* Retrieve data from the draw town tile struct */
1397 const DrawBuildingsTileStruct &dcts = GetTownDrawTileData()[house_id << 4 | view << 2 | TOWN_HOUSE_COMPLETED];
1398 DrawSprite(dcts.ground.sprite, dcts.ground.pal, x, y);
1400 /* Add a house on top of the ground? */
1401 if (dcts.building.sprite != 0) {
1402 Point pt = RemapCoords(dcts.subtile_x, dcts.subtile_y, 0);
1403 DrawSprite(dcts.building.sprite, dcts.building.pal, x + UnScaleGUI(pt.x), y + UnScaleGUI(pt.y));
1407 /* Houses can have 1x1, 1x2, 2x1 and 2x2 layouts which are individual HouseIDs. For the GUI we need
1408 * draw all of the tiles with appropriate positions. */
1409 int x_delta = ScaleGUITrad(TILE_PIXELS);
1410 int y_delta = ScaleGUITrad(TILE_PIXELS / 2);
1412 const HouseSpec *hs = HouseSpec::Get(house_id);
1413 if (hs->building_flags & TILE_SIZE_2x2) {
1414 draw(x, y - y_delta - y_delta, house_id, view); // North corner.
1415 draw(x + x_delta, y - y_delta, house_id + 1, view); // West corner.
1416 draw(x - x_delta, y - y_delta, house_id + 2, view); // East corner.
1417 draw(x, y, house_id + 3, view); // South corner.
1418 } else if (hs->building_flags & TILE_SIZE_2x1) {
1419 draw(x + x_delta / 2, y - y_delta, house_id, view); // North east tile.
1420 draw(x - x_delta / 2, y, house_id + 1, view); // South west tile.
1421 } else if (hs->building_flags & TILE_SIZE_1x2) {
1422 draw(x - x_delta / 2, y - y_delta, house_id, view); // North west tile.
1423 draw(x + x_delta / 2, y, house_id + 1, view); // South east tile.
1424 } else {
1425 draw(x, y, house_id, view);
1430 class HousePickerCallbacks : public PickerCallbacks {
1431 public:
1432 HousePickerCallbacks() : PickerCallbacks("fav_houses") {}
1435 * Set climate mask for filtering buildings from current landscape.
1437 void SetClimateMask()
1439 switch (_settings_game.game_creation.landscape) {
1440 case LT_TEMPERATE: this->climate_mask = HZ_TEMP; break;
1441 case LT_ARCTIC: this->climate_mask = HZ_SUBARTC_ABOVE | HZ_SUBARTC_BELOW; break;
1442 case LT_TROPIC: this->climate_mask = HZ_SUBTROPIC; break;
1443 case LT_TOYLAND: this->climate_mask = HZ_TOYLND; break;
1444 default: NOT_REACHED();
1447 /* In some cases, not all 'classes' (house zones) have distinct houses, so we need to disable those.
1448 * As we need to check all types, and this cannot change with the picker window open, pre-calculate it.
1449 * This loop calls GetTypeName() instead of directly checking properties so that there is no discrepancy. */
1450 this->class_mask = 0;
1452 int num_classes = this->GetClassCount();
1453 for (int cls_id = 0; cls_id < num_classes; ++cls_id) {
1454 int num_types = this->GetTypeCount(cls_id);
1455 for (int id = 0; id < num_types; ++id) {
1456 if (this->GetTypeName(cls_id, id) != INVALID_STRING_ID) {
1457 SetBit(this->class_mask, cls_id);
1458 break;
1464 HouseZones climate_mask;
1465 uint8_t class_mask; ///< Mask of available 'classes'.
1467 static inline int sel_class; ///< Currently selected 'class'.
1468 static inline int sel_type; ///< Currently selected HouseID.
1469 static inline int sel_view; ///< Currently selected 'view'. This is not controllable as its based on random data.
1471 /* Houses do not have classes like NewGRFClass. We'll make up fake classes based on town zone
1472 * availability instead. */
1473 static inline const std::array<StringID, HZB_END> zone_names = {
1474 STR_HOUSE_PICKER_CLASS_ZONE1,
1475 STR_HOUSE_PICKER_CLASS_ZONE2,
1476 STR_HOUSE_PICKER_CLASS_ZONE3,
1477 STR_HOUSE_PICKER_CLASS_ZONE4,
1478 STR_HOUSE_PICKER_CLASS_ZONE5,
1481 StringID GetClassTooltip() const override { return STR_PICKER_HOUSE_CLASS_TOOLTIP; }
1482 StringID GetTypeTooltip() const override { return STR_PICKER_HOUSE_TYPE_TOOLTIP; }
1483 bool IsActive() const override { return true; }
1485 bool HasClassChoice() const override { return true; }
1486 int GetClassCount() const override { return static_cast<int>(zone_names.size()); }
1488 void Close([[maybe_unused]] int data) override { ResetObjectToPlace(); }
1490 int GetSelectedClass() const override { return HousePickerCallbacks::sel_class; }
1491 void SetSelectedClass(int cls_id) const override { HousePickerCallbacks::sel_class = cls_id; }
1493 StringID GetClassName(int id) const override
1495 if (id >= GetClassCount()) return INVALID_STRING_ID;
1496 if (!HasBit(this->class_mask, id)) return INVALID_STRING_ID;
1497 return zone_names[id];
1500 int GetTypeCount(int cls_id) const override
1502 if (cls_id < GetClassCount()) return static_cast<int>(HouseSpec::Specs().size());
1503 return 0;
1506 PickerItem GetPickerItem(int cls_id, int id) const override
1508 const auto *spec = HouseSpec::Get(id);
1509 if (spec->grf_prop.grffile == nullptr) return {0, spec->Index(), cls_id, id};
1510 return {spec->grf_prop.grffile->grfid, spec->grf_prop.local_id, cls_id, id};
1513 int GetSelectedType() const override { return sel_type; }
1514 void SetSelectedType(int id) const override { sel_type = id; }
1516 StringID GetTypeName(int cls_id, int id) const override
1518 const HouseSpec *spec = HouseSpec::Get(id);
1519 if (spec == nullptr) return INVALID_STRING_ID;
1520 if (!spec->enabled) return INVALID_STRING_ID;
1521 if ((spec->building_availability & climate_mask) == 0) return INVALID_STRING_ID;
1522 if (!HasBit(spec->building_availability, cls_id)) return INVALID_STRING_ID;
1523 for (int i = 0; i < cls_id; i++) {
1524 /* Don't include if it's already included in an earlier zone. */
1525 if (HasBit(spec->building_availability, i)) return INVALID_STRING_ID;
1528 return spec->building_name;
1531 bool IsTypeAvailable(int, int id) const override
1533 const HouseSpec *hs = HouseSpec::Get(id);
1534 return hs->enabled;
1537 void DrawType(int x, int y, int, int id) const override
1539 DrawHouseInGUI(x, y, id, HousePickerCallbacks::sel_view);
1542 void FillUsedItems(std::set<PickerItem> &items) override
1544 auto id_count = GetBuildingHouseIDCounts();
1545 for (auto it = id_count.begin(); it != id_count.end(); ++it) {
1546 if (*it == 0) continue;
1547 HouseID house = static_cast<HouseID>(std::distance(id_count.begin(), it));
1548 const HouseSpec *hs = HouseSpec::Get(house);
1549 int class_index = FindFirstBit(hs->building_availability & HZ_ZONALL);
1550 items.insert({0, house, class_index, house});
1554 std::set<PickerItem> UpdateSavedItems(const std::set<PickerItem> &src) override
1556 if (src.empty()) return src;
1558 const auto specs = HouseSpec::Specs();
1559 std::set<PickerItem> dst;
1560 for (const auto &item : src) {
1561 if (item.grfid == 0) {
1562 dst.insert(item);
1563 } else {
1564 /* Search for spec by grfid and local index. */
1565 auto it = std::find_if(specs.begin(), specs.end(), [&item](const HouseSpec &spec) { return spec.grf_prop.grffile != nullptr && spec.grf_prop.grffile->grfid == item.grfid && spec.grf_prop.local_id == item.local_id; });
1566 if (it == specs.end()) {
1567 /* Not preset, hide from UI. */
1568 dst.insert({item.grfid, item.local_id, -1, -1});
1569 } else {
1570 int class_index = FindFirstBit(it->building_availability & HZ_ZONALL);
1571 dst.insert( {item.grfid, item.local_id, class_index, it->Index()});
1576 return dst;
1579 static HousePickerCallbacks instance;
1581 /* static */ HousePickerCallbacks HousePickerCallbacks::instance;
1583 struct BuildHouseWindow : public PickerWindow {
1584 BuildHouseWindow(WindowDesc &desc, Window *parent) : PickerWindow(desc, parent, 0, HousePickerCallbacks::instance)
1586 HousePickerCallbacks::instance.SetClimateMask();
1587 this->ConstructWindow();
1588 this->InvalidateData();
1591 void UpdateSelectSize(const HouseSpec *spec)
1593 if (spec == nullptr) {
1594 SetTileSelectSize(1, 1);
1595 ResetObjectToPlace();
1596 } else {
1597 SetObjectToPlaceWnd(SPR_CURSOR_TOWN, PAL_NONE, HT_RECT | HT_DIAGONAL, this);
1598 if (spec->building_flags & TILE_SIZE_2x2) {
1599 SetTileSelectSize(2, 2);
1600 } else if (spec->building_flags & TILE_SIZE_2x1) {
1601 SetTileSelectSize(2, 1);
1602 } else if (spec->building_flags & TILE_SIZE_1x2) {
1603 SetTileSelectSize(1, 2);
1604 } else if (spec->building_flags & TILE_SIZE_1x1) {
1605 SetTileSelectSize(1, 1);
1610 void OnInvalidateData(int data = 0, bool gui_scope = true) override
1612 this->PickerWindow::OnInvalidateData(data, gui_scope);
1613 if (!gui_scope) return;
1615 if ((data & PickerWindow::PFI_POSITION) != 0) {
1616 const HouseSpec *spec = HouseSpec::Get(HousePickerCallbacks::sel_type);
1617 UpdateSelectSize(spec);
1621 void OnPlaceObject([[maybe_unused]] Point pt, TileIndex tile) override
1623 const HouseSpec *spec = HouseSpec::Get(HousePickerCallbacks::sel_type);
1624 Command<CMD_PLACE_HOUSE>::Post(STR_ERROR_CAN_T_BUILD_HOUSE, CcPlaySound_CONSTRUCTION_OTHER, tile, spec->Index());
1627 IntervalTimer<TimerWindow> view_refresh_interval = {std::chrono::milliseconds(2500), [this](auto) {
1628 /* There are four different 'views' that are random based on house tile position. As this is not
1629 * user-controllable, instead we automatically cycle through them. */
1630 HousePickerCallbacks::sel_view = (HousePickerCallbacks::sel_view + 1) % 4;
1631 this->SetDirty();
1634 static inline HotkeyList hotkeys{"buildhouse", {
1635 Hotkey('F', "focus_filter_box", PCWHK_FOCUS_FILTER_BOX),
1639 /** Nested widget definition for the build NewGRF rail waypoint window */
1640 static constexpr NWidgetPart _nested_build_house_widgets[] = {
1641 NWidget(NWID_HORIZONTAL),
1642 NWidget(WWT_CLOSEBOX, COLOUR_DARK_GREEN),
1643 NWidget(WWT_CAPTION, COLOUR_DARK_GREEN), SetDataTip(STR_HOUSE_PICKER_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
1644 NWidget(WWT_SHADEBOX, COLOUR_DARK_GREEN),
1645 NWidget(WWT_DEFSIZEBOX, COLOUR_DARK_GREEN),
1646 NWidget(WWT_STICKYBOX, COLOUR_DARK_GREEN),
1647 EndContainer(),
1648 NWidget(NWID_HORIZONTAL),
1649 NWidgetFunction(MakePickerClassWidgets),
1650 NWidgetFunction(MakePickerTypeWidgets),
1651 EndContainer(),
1654 static WindowDesc _build_house_desc(
1655 WDP_AUTO, "build_house", 0, 0,
1656 WC_BUILD_HOUSE, WC_BUILD_TOOLBAR,
1657 WDF_CONSTRUCTION,
1658 _nested_build_house_widgets,
1659 &BuildHouseWindow::hotkeys
1662 void ShowBuildHousePicker(Window *parent)
1664 if (BringWindowToFrontById(WC_BUILD_HOUSE, 0)) return;
1665 new BuildHouseWindow(_build_house_desc, parent);