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/>.
8 /** @file town_gui.cpp GUI for towns. */
12 #include "viewport_func.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"
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"
36 #include "stringfilter_type.h"
37 #include "dropdown_func.h"
38 #include "town_kdtree.h"
40 #include "timer/timer.h"
41 #include "timer/timer_game_calendar.h"
42 #include "timer/timer_window.h"
43 #include "zoom_func.h"
46 #include "widgets/town_widget.h"
48 #include "table/strings.h"
50 #include "safeguards.h"
52 TownKdtree
_town_local_authority_kdtree(&Kdtree_TownXYFunc
);
54 typedef GUIList
<const Town
*, const bool &> GUITownList
;
56 static constexpr NWidgetPart _nested_town_authority_widgets
[] = {
57 NWidget(NWID_HORIZONTAL
),
58 NWidget(WWT_CLOSEBOX
, COLOUR_BROWN
),
59 NWidget(WWT_CAPTION
, COLOUR_BROWN
, WID_TA_CAPTION
), SetDataTip(STR_LOCAL_AUTHORITY_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
60 NWidget(WWT_TEXTBTN
, COLOUR_BROWN
, WID_TA_ZONE_BUTTON
), SetMinimalSize(50, 0), SetDataTip(STR_LOCAL_AUTHORITY_ZONE
, STR_LOCAL_AUTHORITY_ZONE_TOOLTIP
),
61 NWidget(WWT_SHADEBOX
, COLOUR_BROWN
),
62 NWidget(WWT_DEFSIZEBOX
, COLOUR_BROWN
),
63 NWidget(WWT_STICKYBOX
, COLOUR_BROWN
),
65 NWidget(WWT_PANEL
, COLOUR_BROWN
, WID_TA_RATING_INFO
), SetMinimalSize(317, 92), SetResize(1, 1), EndContainer(),
66 NWidget(WWT_PANEL
, COLOUR_BROWN
, WID_TA_COMMAND_LIST
), SetMinimalSize(317, 52), SetResize(1, 0), SetDataTip(0x0, STR_LOCAL_AUTHORITY_ACTIONS_TOOLTIP
), EndContainer(),
67 NWidget(WWT_PANEL
, COLOUR_BROWN
, WID_TA_ACTION_INFO
), SetMinimalSize(317, 52), SetResize(1, 0), EndContainer(),
68 NWidget(NWID_HORIZONTAL
),
69 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
),
70 NWidget(WWT_RESIZEBOX
, COLOUR_BROWN
),
74 /** Town authority window. */
75 struct TownAuthorityWindow
: Window
{
77 Town
*town
; ///< Town being displayed.
78 int sel_index
; ///< Currently selected town action, \c 0 to \c TACT_COUNT-1, \c -1 means no action selected.
79 uint displayed_actions_on_previous_painting
; ///< Actions that were available on the previous call to OnPaint()
80 TownActions enabled_actions
; ///< Actions that are enabled in settings.
81 TownActions available_actions
; ///< Actions that are available to execute for the current company.
82 StringID action_tooltips
[TACT_COUNT
];
84 Dimension icon_size
; ///< Dimensions of company icon
85 Dimension exclusive_size
; ///< Dimensions of exlusive icon
88 * Get the position of the Nth set bit.
90 * If there is no Nth bit set return -1
92 * @param n The Nth set bit from which we want to know the position
93 * @return The position of the Nth set bit, or -1 if no Nth bit set.
95 int GetNthSetBit(int n
)
98 for (uint i
: SetBitIterator(this->enabled_actions
)) {
107 * Gets all town authority actions enabled in settings.
109 * @return Bitmask of actions enabled in the settings.
111 static TownActions
GetEnabledActions()
113 TownActions enabled
= TACT_ALL
;
115 if (!_settings_game
.economy
.fund_roads
) CLRBITS(enabled
, TACT_ROAD_REBUILD
);
116 if (!_settings_game
.economy
.fund_buildings
) CLRBITS(enabled
, TACT_FUND_BUILDINGS
);
117 if (!_settings_game
.economy
.exclusive_rights
) CLRBITS(enabled
, TACT_BUY_RIGHTS
);
118 if (!_settings_game
.economy
.bribe
) CLRBITS(enabled
, TACT_BRIBE
);
124 TownAuthorityWindow(WindowDesc
&desc
, WindowNumber window_number
) : Window(desc
), sel_index(-1), displayed_actions_on_previous_painting(0), available_actions(TACT_NONE
)
126 this->town
= Town::Get(window_number
);
127 this->enabled_actions
= GetEnabledActions();
129 auto realtime
= TimerGameEconomy::UsingWallclockUnits();
130 this->action_tooltips
[0] = STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_SMALL_ADVERTISING
;
131 this->action_tooltips
[1] = STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_MEDIUM_ADVERTISING
;
132 this->action_tooltips
[2] = STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_LARGE_ADVERTISING
;
133 this->action_tooltips
[3] = realtime
? STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_ROAD_RECONSTRUCTION_MINUTES
: STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_ROAD_RECONSTRUCTION_MONTHS
;
134 this->action_tooltips
[4] = STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_STATUE_OF_COMPANY
;
135 this->action_tooltips
[5] = STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_NEW_BUILDINGS
;
136 this->action_tooltips
[6] = realtime
? STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_EXCLUSIVE_TRANSPORT_MINUTES
: STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_EXCLUSIVE_TRANSPORT_MONTHS
;
137 this->action_tooltips
[7] = STR_LOCAL_AUTHORITY_ACTION_TOOLTIP_BRIBE
;
139 this->InitNested(window_number
);
142 void OnInit() override
144 this->icon_size
= GetSpriteSize(SPR_COMPANY_ICON
);
145 this->exclusive_size
= GetSpriteSize(SPR_EXCLUSIVE_TRANSPORT
);
148 void OnPaint() override
150 this->available_actions
= GetMaskOfTownActions(_local_company
, this->town
);
151 if (this->available_actions
!= displayed_actions_on_previous_painting
) this->SetDirty();
152 displayed_actions_on_previous_painting
= this->available_actions
;
154 this->SetWidgetLoweredState(WID_TA_ZONE_BUTTON
, this->town
->show_zone
);
155 this->SetWidgetDisabledState(WID_TA_EXECUTE
, (this->sel_index
== -1) || !HasBit(this->available_actions
, this->sel_index
));
158 if (!this->IsShaded())
165 /** Draw the contents of the ratings panel. May request a resize of the window if the contents does not fit. */
168 Rect r
= this->GetWidget
<NWidgetBase
>(WID_TA_RATING_INFO
)->GetCurrentRect().Shrink(WidgetDimensions::scaled
.framerect
);
170 int text_y_offset
= (this->resize
.step_height
- GetCharacterHeight(FS_NORMAL
)) / 2;
171 int icon_y_offset
= (this->resize
.step_height
- this->icon_size
.height
) / 2;
172 int exclusive_y_offset
= (this->resize
.step_height
- this->exclusive_size
.height
) / 2;
174 DrawString(r
.left
, r
.right
, r
.top
+ text_y_offset
, STR_LOCAL_AUTHORITY_COMPANY_RATINGS
);
175 r
.top
+= this->resize
.step_height
;
177 bool rtl
= _current_text_dir
== TD_RTL
;
178 Rect icon
= r
.WithWidth(this->icon_size
.width
, rtl
);
179 Rect exclusive
= r
.Indent(this->icon_size
.width
+ WidgetDimensions::scaled
.hsep_normal
, rtl
).WithWidth(this->exclusive_size
.width
, rtl
);
180 Rect text
= r
.Indent(this->icon_size
.width
+ WidgetDimensions::scaled
.hsep_normal
+ this->exclusive_size
.width
+ WidgetDimensions::scaled
.hsep_normal
, rtl
);
182 /* Draw list of companies */
183 for (const Company
*c
: Company::Iterate()) {
184 if ((HasBit(this->town
->have_ratings
, c
->index
) || this->town
->exclusivity
== c
->index
)) {
185 DrawCompanyIcon(c
->index
, icon
.left
, text
.top
+ icon_y_offset
);
187 SetDParam(0, c
->index
);
188 SetDParam(1, c
->index
);
190 int rating
= this->town
->ratings
[c
->index
];
191 StringID str
= STR_CARGO_RATING_APPALLING
;
192 if (rating
> RATING_APPALLING
) str
++;
193 if (rating
> RATING_VERYPOOR
) str
++;
194 if (rating
> RATING_POOR
) str
++;
195 if (rating
> RATING_MEDIOCRE
) str
++;
196 if (rating
> RATING_GOOD
) str
++;
197 if (rating
> RATING_VERYGOOD
) str
++;
198 if (rating
> RATING_EXCELLENT
) str
++;
201 if (this->town
->exclusivity
== c
->index
) {
202 DrawSprite(SPR_EXCLUSIVE_TRANSPORT
, COMPANY_SPRITE_COLOUR(c
->index
), exclusive
.left
, text
.top
+ exclusive_y_offset
);
205 DrawString(text
.left
, text
.right
, text
.top
+ text_y_offset
, STR_LOCAL_AUTHORITY_COMPANY_RATING
);
206 text
.top
+= this->resize
.step_height
;
210 text
.bottom
= text
.top
- 1;
211 if (text
.bottom
> r
.bottom
) {
212 /* If the company list is too big to fit, mark ourself dirty and draw again. */
213 ResizeWindow(this, 0, text
.bottom
- r
.bottom
, false);
217 /** Draws the contents of the actions panel. May re-initialise window to resize panel, if the list does not fit. */
220 Rect r
= this->GetWidget
<NWidgetBase
>(WID_TA_COMMAND_LIST
)->GetCurrentRect().Shrink(WidgetDimensions::scaled
.framerect
);
222 DrawString(r
, STR_LOCAL_AUTHORITY_ACTIONS_TITLE
);
223 r
.top
+= GetCharacterHeight(FS_NORMAL
);
225 /* Draw list of actions */
226 for (int i
= 0; i
< TACT_COUNT
; i
++) {
227 /* Don't show actions if disabled in settings. */
228 if (!HasBit(this->enabled_actions
, i
)) continue;
230 /* Set colour of action based on ability to execute and if selected. */
231 TextColour action_colour
= TC_GREY
| TC_NO_SHADE
;
232 if (HasBit(this->available_actions
, i
)) action_colour
= TC_ORANGE
;
233 if (this->sel_index
== i
) action_colour
= TC_WHITE
;
235 DrawString(r
, STR_LOCAL_AUTHORITY_ACTION_SMALL_ADVERTISING_CAMPAIGN
+ i
, action_colour
);
236 r
.top
+= GetCharacterHeight(FS_NORMAL
);
240 void SetStringParameters(WidgetID widget
) const override
242 if (widget
== WID_TA_CAPTION
) SetDParam(0, this->window_number
);
245 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
248 case WID_TA_ACTION_INFO
:
249 if (this->sel_index
!= -1) {
250 Money action_cost
= _price
[PR_TOWN_ACTION
] * _town_action_costs
[this->sel_index
] >> 8;
251 bool affordable
= Company::IsValidID(_local_company
) && action_cost
< GetAvailableMoney(_local_company
);
253 SetDParam(0, action_cost
);
254 DrawStringMultiLine(r
.Shrink(WidgetDimensions::scaled
.framerect
),
255 this->action_tooltips
[this->sel_index
],
256 affordable
? TC_YELLOW
: TC_RED
);
262 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
265 case WID_TA_ACTION_INFO
: {
266 assert(size
.width
> padding
.width
&& size
.height
> padding
.height
);
267 Dimension d
= {0, 0};
268 for (int i
= 0; i
< TACT_COUNT
; i
++) {
269 SetDParam(0, _price
[PR_TOWN_ACTION
] * _town_action_costs
[i
] >> 8);
270 d
= maxdim(d
, GetStringMultiLineBoundingBox(this->action_tooltips
[i
], size
));
272 d
.width
+= padding
.width
;
273 d
.height
+= padding
.height
;
274 size
= maxdim(size
, d
);
278 case WID_TA_COMMAND_LIST
:
279 size
.height
= (TACT_COUNT
+ 1) * GetCharacterHeight(FS_NORMAL
) + padding
.height
;
280 size
.width
= GetStringBoundingBox(STR_LOCAL_AUTHORITY_ACTIONS_TITLE
).width
;
281 for (uint i
= 0; i
< TACT_COUNT
; i
++ ) {
282 size
.width
= std::max(size
.width
, GetStringBoundingBox(STR_LOCAL_AUTHORITY_ACTION_SMALL_ADVERTISING_CAMPAIGN
+ i
).width
+ padding
.width
);
284 size
.width
+= padding
.width
;
287 case WID_TA_RATING_INFO
:
288 resize
.height
= std::max({this->icon_size
.height
+ WidgetDimensions::scaled
.vsep_normal
, this->exclusive_size
.height
+ WidgetDimensions::scaled
.vsep_normal
, (uint
)GetCharacterHeight(FS_NORMAL
)});
289 size
.height
= 9 * resize
.height
+ padding
.height
;
294 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
297 case WID_TA_ZONE_BUTTON
: {
298 bool new_show_state
= !this->town
->show_zone
;
299 TownID index
= this->town
->index
;
301 new_show_state
? _town_local_authority_kdtree
.Insert(index
) : _town_local_authority_kdtree
.Remove(index
);
303 this->town
->show_zone
= new_show_state
;
304 this->SetWidgetLoweredState(widget
, new_show_state
);
305 MarkWholeScreenDirty();
309 case WID_TA_COMMAND_LIST
: {
310 int y
= this->GetRowFromWidget(pt
.y
, WID_TA_COMMAND_LIST
, 1, GetCharacterHeight(FS_NORMAL
)) - 1;
318 /* When double-clicking, continue */
319 if (click_count
== 1 || y
< 0 || !HasBit(this->available_actions
, y
)) break;
324 Command
<CMD_DO_TOWN_ACTION
>::Post(STR_ERROR_CAN_T_DO_THIS
, this->town
->xy
, this->window_number
, this->sel_index
);
329 /** Redraw the whole window on a regular interval. */
330 IntervalTimer
<TimerWindow
> redraw_interval
= {std::chrono::seconds(3), [this](auto) {
334 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
336 if (!gui_scope
) return;
338 this->enabled_actions
= this->GetEnabledActions();
339 if (!HasBit(this->enabled_actions
, this->sel_index
)) {
340 this->sel_index
= -1;
345 static WindowDesc
_town_authority_desc(
346 WDP_AUTO
, "view_town_authority", 317, 222,
347 WC_TOWN_AUTHORITY
, WC_NONE
,
349 _nested_town_authority_widgets
352 static void ShowTownAuthorityWindow(uint town
)
354 AllocateWindowDescFront
<TownAuthorityWindow
>(_town_authority_desc
, town
);
358 /* Town view window. */
359 struct TownViewWindow
: Window
{
361 Town
*town
; ///< Town displayed by the window.
364 static const int WID_TV_HEIGHT_NORMAL
= 150;
366 TownViewWindow(WindowDesc
&desc
, WindowNumber window_number
) : Window(desc
)
368 this->CreateNestedTree();
370 this->town
= Town::Get(window_number
);
371 if (this->town
->larger_town
) this->GetWidget
<NWidgetCore
>(WID_TV_CAPTION
)->widget_data
= STR_TOWN_VIEW_CITY_CAPTION
;
373 this->FinishInitNested(window_number
);
375 this->flags
|= WF_DISABLE_VP_SCROLL
;
376 NWidgetViewport
*nvp
= this->GetWidget
<NWidgetViewport
>(WID_TV_VIEWPORT
);
377 nvp
->InitializeViewport(this, this->town
->xy
, ScaleZoomGUI(ZOOM_LVL_TOWN
));
379 /* disable renaming town in network games if you are not the server */
380 this->SetWidgetDisabledState(WID_TV_CHANGE_NAME
, _networking
&& !_network_server
);
383 void Close([[maybe_unused
]] int data
= 0) override
385 SetViewportCatchmentTown(Town::Get(this->window_number
), false);
386 this->Window::Close();
389 void SetStringParameters(WidgetID widget
) const override
391 if (widget
== WID_TV_CAPTION
) SetDParam(0, this->town
->index
);
394 void OnPaint() override
396 extern const Town
*_viewport_highlight_town
;
397 this->SetWidgetLoweredState(WID_TV_CATCHMENT
, _viewport_highlight_town
== this->town
);
402 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
404 if (widget
!= WID_TV_INFO
) return;
406 Rect tr
= r
.Shrink(WidgetDimensions::scaled
.framerect
);
408 SetDParam(0, this->town
->cache
.population
);
409 SetDParam(1, this->town
->cache
.num_houses
);
410 DrawString(tr
, STR_TOWN_VIEW_POPULATION_HOUSES
);
411 tr
.top
+= GetCharacterHeight(FS_NORMAL
);
413 StringID str_last_period
= TimerGameEconomy::UsingWallclockUnits() ? STR_TOWN_VIEW_CARGO_LAST_MINUTE_MAX
: STR_TOWN_VIEW_CARGO_LAST_MONTH_MAX
;
415 for (auto tpe
: {TPE_PASSENGERS
, TPE_MAIL
}) {
416 for (const CargoSpec
*cs
: CargoSpec::town_production_cargoes
[tpe
]) {
417 CargoID cid
= cs
->Index();
418 SetDParam(0, 1ULL << cid
);
419 SetDParam(1, this->town
->supplied
[cid
].old_act
);
420 SetDParam(2, this->town
->supplied
[cid
].old_max
);
421 DrawString(tr
, str_last_period
);
422 tr
.top
+= GetCharacterHeight(FS_NORMAL
);
427 for (int i
= TAE_BEGIN
; i
< TAE_END
; i
++) {
428 if (this->town
->goal
[i
] == 0) continue;
429 if (this->town
->goal
[i
] == TOWN_GROWTH_WINTER
&& (TileHeight(this->town
->xy
) < LowestSnowLine() || this->town
->cache
.population
<= 90)) continue;
430 if (this->town
->goal
[i
] == TOWN_GROWTH_DESERT
&& (GetTropicZone(this->town
->xy
) != TROPICZONE_DESERT
|| this->town
->cache
.population
<= 60)) continue;
433 DrawString(tr
, STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH
);
434 tr
.top
+= GetCharacterHeight(FS_NORMAL
);
438 bool rtl
= _current_text_dir
== TD_RTL
;
440 const CargoSpec
*cargo
= FindFirstCargoWithTownAcceptanceEffect((TownAcceptanceEffect
)i
);
441 assert(cargo
!= nullptr);
445 if (this->town
->goal
[i
] == TOWN_GROWTH_DESERT
|| this->town
->goal
[i
] == TOWN_GROWTH_WINTER
) {
446 /* For 'original' gameplay, don't show the amount required (you need 1 or more ..) */
447 string
= STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_DELIVERED_GENERAL
;
448 if (this->town
->received
[i
].old_act
== 0) {
449 string
= STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL
;
451 if (this->town
->goal
[i
] == TOWN_GROWTH_WINTER
&& TileHeight(this->town
->xy
) < GetSnowLine()) {
452 string
= STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER
;
456 SetDParam(0, cargo
->name
);
458 string
= STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_DELIVERED
;
459 if (this->town
->received
[i
].old_act
< this->town
->goal
[i
]) {
460 string
= STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED
;
463 SetDParam(0, cargo
->Index());
464 SetDParam(1, this->town
->received
[i
].old_act
);
465 SetDParam(2, cargo
->Index());
466 SetDParam(3, this->town
->goal
[i
]);
468 DrawString(tr
.Indent(20, rtl
), string
);
469 tr
.top
+= GetCharacterHeight(FS_NORMAL
);
472 if (HasBit(this->town
->flags
, TOWN_IS_GROWING
)) {
473 SetDParam(0, RoundDivSU(this->town
->growth_rate
+ 1, Ticks::DAY_TICKS
));
474 DrawString(tr
, this->town
->fund_buildings_months
== 0 ? STR_TOWN_VIEW_TOWN_GROWS_EVERY
: STR_TOWN_VIEW_TOWN_GROWS_EVERY_FUNDED
);
475 tr
.top
+= GetCharacterHeight(FS_NORMAL
);
477 DrawString(tr
, STR_TOWN_VIEW_TOWN_GROW_STOPPED
);
478 tr
.top
+= GetCharacterHeight(FS_NORMAL
);
481 /* only show the town noise, if the noise option is activated. */
482 if (_settings_game
.economy
.station_noise_level
) {
483 SetDParam(0, this->town
->noise_reached
);
484 SetDParam(1, this->town
->MaxTownNoise());
485 DrawString(tr
, STR_TOWN_VIEW_NOISE_IN_TOWN
);
486 tr
.top
+= GetCharacterHeight(FS_NORMAL
);
489 if (!this->town
->text
.empty()) {
490 SetDParamStr(0, this->town
->text
);
491 tr
.top
= DrawStringMultiLine(tr
, STR_JUST_RAW_STRING
, TC_BLACK
);
495 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
498 case WID_TV_CENTER_VIEW
: // scroll to location
500 ShowExtraViewportWindow(this->town
->xy
);
502 ScrollMainWindowToTile(this->town
->xy
);
506 case WID_TV_SHOW_AUTHORITY
: // town authority
507 ShowTownAuthorityWindow(this->window_number
);
510 case WID_TV_CHANGE_NAME
: // rename
511 SetDParam(0, this->window_number
);
512 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
);
515 case WID_TV_CATCHMENT
:
516 SetViewportCatchmentTown(Town::Get(this->window_number
), !this->IsWidgetLowered(WID_TV_CATCHMENT
));
519 case WID_TV_EXPAND
: { // expand town - only available on Scenario editor
520 Command
<CMD_EXPAND_TOWN
>::Post(STR_ERROR_CAN_T_EXPAND_TOWN
, this->window_number
, 0);
524 case WID_TV_DELETE
: // delete town - only available on Scenario editor
525 Command
<CMD_DELETE_TOWN
>::Post(STR_ERROR_TOWN_CAN_T_DELETE
, this->window_number
);
530 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
534 size
.height
= GetDesiredInfoHeight(size
.width
) + padding
.height
;
540 * Gets the desired height for the information panel.
541 * @return the desired height in pixels.
543 uint
GetDesiredInfoHeight(int width
) const
545 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 for (int i
= TAE_BEGIN
; i
< TAE_END
; i
++) {
549 if (this->town
->goal
[i
] == 0) continue;
550 if (this->town
->goal
[i
] == TOWN_GROWTH_WINTER
&& (TileHeight(this->town
->xy
) < LowestSnowLine() || this->town
->cache
.population
<= 90)) continue;
551 if (this->town
->goal
[i
] == TOWN_GROWTH_DESERT
&& (GetTropicZone(this->town
->xy
) != TROPICZONE_DESERT
|| this->town
->cache
.population
<= 60)) continue;
554 aimed_height
+= GetCharacterHeight(FS_NORMAL
);
557 aimed_height
+= GetCharacterHeight(FS_NORMAL
);
559 aimed_height
+= GetCharacterHeight(FS_NORMAL
);
561 if (_settings_game
.economy
.station_noise_level
) aimed_height
+= GetCharacterHeight(FS_NORMAL
);
563 if (!this->town
->text
.empty()) {
564 SetDParamStr(0, this->town
->text
);
565 aimed_height
+= GetStringHeight(STR_JUST_RAW_STRING
, width
- WidgetDimensions::scaled
.framerect
.Horizontal());
571 void ResizeWindowAsNeeded()
573 const NWidgetBase
*nwid_info
= this->GetWidget
<NWidgetBase
>(WID_TV_INFO
);
574 uint aimed_height
= GetDesiredInfoHeight(nwid_info
->current_x
);
575 if (aimed_height
> nwid_info
->current_y
|| (aimed_height
< nwid_info
->current_y
&& nwid_info
->current_y
> nwid_info
->smallest_y
)) {
580 void OnResize() override
582 if (this->viewport
!= nullptr) {
583 NWidgetViewport
*nvp
= this->GetWidget
<NWidgetViewport
>(WID_TV_VIEWPORT
);
584 nvp
->UpdateViewportCoordinates(this);
586 ScrollWindowToTile(this->town
->xy
, this, true); // Re-center viewport.
590 void OnMouseWheel(int wheel
) override
592 if (_settings_client
.gui
.scrollwheel_scrolling
!= SWS_OFF
) {
593 DoZoomInOutWindow(wheel
< 0 ? ZOOM_IN
: ZOOM_OUT
, this);
598 * Some data on this window has become invalid.
599 * @param data Information about the changed data.
600 * @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.
602 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
604 if (!gui_scope
) return;
605 /* Called when setting station noise or required cargoes have changed, in order to resize the window */
606 this->SetDirty(); // refresh display for current size. This will allow to avoid glitches when downgrading
607 this->ResizeWindowAsNeeded();
610 void OnQueryTextFinished(std::optional
<std::string
> str
) override
612 if (!str
.has_value()) return;
614 Command
<CMD_RENAME_TOWN
>::Post(STR_ERROR_CAN_T_RENAME_TOWN
, this->window_number
, *str
);
617 IntervalTimer
<TimerGameCalendar
> daily_interval
= {{TimerGameCalendar::DAY
, TimerGameCalendar::Priority::NONE
}, [this](auto) {
618 /* Refresh after possible snowline change */
623 static constexpr NWidgetPart _nested_town_game_view_widgets
[] = {
624 NWidget(NWID_HORIZONTAL
),
625 NWidget(WWT_CLOSEBOX
, COLOUR_BROWN
),
626 NWidget(WWT_PUSHIMGBTN
, COLOUR_BROWN
, WID_TV_CHANGE_NAME
), SetAspect(WidgetDimensions::ASPECT_RENAME
), SetDataTip(SPR_RENAME
, STR_TOWN_VIEW_RENAME_TOOLTIP
),
627 NWidget(WWT_CAPTION
, COLOUR_BROWN
, WID_TV_CAPTION
), SetDataTip(STR_TOWN_VIEW_TOWN_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
628 NWidget(WWT_PUSHIMGBTN
, COLOUR_BROWN
, WID_TV_CENTER_VIEW
), SetAspect(WidgetDimensions::ASPECT_LOCATION
), SetDataTip(SPR_GOTO_LOCATION
, STR_TOWN_VIEW_CENTER_TOOLTIP
),
629 NWidget(WWT_SHADEBOX
, COLOUR_BROWN
),
630 NWidget(WWT_DEFSIZEBOX
, COLOUR_BROWN
),
631 NWidget(WWT_STICKYBOX
, COLOUR_BROWN
),
633 NWidget(WWT_PANEL
, COLOUR_BROWN
),
634 NWidget(WWT_INSET
, COLOUR_BROWN
), SetPadding(2, 2, 2, 2),
635 NWidget(NWID_VIEWPORT
, INVALID_COLOUR
, WID_TV_VIEWPORT
), SetMinimalSize(254, 86), SetFill(1, 0), SetResize(1, 1),
638 NWidget(WWT_PANEL
, COLOUR_BROWN
, WID_TV_INFO
), SetMinimalSize(260, 32), SetResize(1, 0), SetFill(1, 0), EndContainer(),
639 NWidget(NWID_HORIZONTAL
, NC_EQUALSIZE
),
640 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
),
641 NWidget(WWT_TEXTBTN
, COLOUR_BROWN
, WID_TV_CATCHMENT
), SetMinimalSize(40, 12), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_BUTTON_CATCHMENT
, STR_TOOLTIP_CATCHMENT
),
642 NWidget(WWT_RESIZEBOX
, COLOUR_BROWN
),
646 static WindowDesc
_town_game_view_desc(
647 WDP_AUTO
, "view_town", 260, TownViewWindow::WID_TV_HEIGHT_NORMAL
,
648 WC_TOWN_VIEW
, WC_NONE
,
650 _nested_town_game_view_widgets
653 static constexpr NWidgetPart _nested_town_editor_view_widgets
[] = {
654 NWidget(NWID_HORIZONTAL
),
655 NWidget(WWT_CLOSEBOX
, COLOUR_BROWN
),
656 NWidget(WWT_PUSHIMGBTN
, COLOUR_BROWN
, WID_TV_CHANGE_NAME
), SetAspect(WidgetDimensions::ASPECT_RENAME
), SetDataTip(SPR_RENAME
, STR_TOWN_VIEW_RENAME_TOOLTIP
),
657 NWidget(WWT_CAPTION
, COLOUR_BROWN
, WID_TV_CAPTION
), SetDataTip(STR_TOWN_VIEW_TOWN_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
658 NWidget(WWT_PUSHIMGBTN
, COLOUR_BROWN
, WID_TV_CENTER_VIEW
), SetAspect(WidgetDimensions::ASPECT_LOCATION
), SetDataTip(SPR_GOTO_LOCATION
, STR_TOWN_VIEW_CENTER_TOOLTIP
),
659 NWidget(WWT_SHADEBOX
, COLOUR_BROWN
),
660 NWidget(WWT_DEFSIZEBOX
, COLOUR_BROWN
),
661 NWidget(WWT_STICKYBOX
, COLOUR_BROWN
),
663 NWidget(WWT_PANEL
, COLOUR_BROWN
),
664 NWidget(WWT_INSET
, COLOUR_BROWN
), SetPadding(2, 2, 2, 2),
665 NWidget(NWID_VIEWPORT
, INVALID_COLOUR
, WID_TV_VIEWPORT
), SetMinimalSize(254, 86), SetFill(1, 1), SetResize(1, 1),
668 NWidget(WWT_PANEL
, COLOUR_BROWN
, WID_TV_INFO
), SetMinimalSize(260, 32), SetResize(1, 0), SetFill(1, 0), EndContainer(),
669 NWidget(NWID_HORIZONTAL
, NC_EQUALSIZE
),
670 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
),
671 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
),
672 NWidget(WWT_TEXTBTN
, COLOUR_BROWN
, WID_TV_CATCHMENT
), SetMinimalSize(40, 12), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_BUTTON_CATCHMENT
, STR_TOOLTIP_CATCHMENT
),
673 NWidget(WWT_RESIZEBOX
, COLOUR_BROWN
),
677 static WindowDesc
_town_editor_view_desc(
678 WDP_AUTO
, "view_town_scen", 260, TownViewWindow::WID_TV_HEIGHT_NORMAL
,
679 WC_TOWN_VIEW
, WC_NONE
,
681 _nested_town_editor_view_widgets
684 void ShowTownViewWindow(TownID town
)
686 if (_game_mode
== GM_EDITOR
) {
687 AllocateWindowDescFront
<TownViewWindow
>(_town_editor_view_desc
, town
);
689 AllocateWindowDescFront
<TownViewWindow
>(_town_game_view_desc
, town
);
693 static constexpr NWidgetPart _nested_town_directory_widgets
[] = {
694 NWidget(NWID_HORIZONTAL
),
695 NWidget(WWT_CLOSEBOX
, COLOUR_BROWN
),
696 NWidget(WWT_CAPTION
, COLOUR_BROWN
, WID_TD_CAPTION
), SetDataTip(STR_TOWN_DIRECTORY_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
697 NWidget(WWT_SHADEBOX
, COLOUR_BROWN
),
698 NWidget(WWT_DEFSIZEBOX
, COLOUR_BROWN
),
699 NWidget(WWT_STICKYBOX
, COLOUR_BROWN
),
701 NWidget(NWID_HORIZONTAL
),
702 NWidget(NWID_VERTICAL
),
703 NWidget(NWID_HORIZONTAL
),
704 NWidget(WWT_TEXTBTN
, COLOUR_BROWN
, WID_TD_SORT_ORDER
), SetDataTip(STR_BUTTON_SORT_BY
, STR_TOOLTIP_SORT_ORDER
),
705 NWidget(WWT_DROPDOWN
, COLOUR_BROWN
, WID_TD_SORT_CRITERIA
), SetDataTip(STR_JUST_STRING
, STR_TOOLTIP_SORT_CRITERIA
),
706 NWidget(WWT_EDITBOX
, COLOUR_BROWN
, WID_TD_FILTER
), SetFill(1, 0), SetResize(1, 0), SetDataTip(STR_LIST_FILTER_OSKTITLE
, STR_LIST_FILTER_TOOLTIP
),
708 NWidget(WWT_PANEL
, COLOUR_BROWN
, WID_TD_LIST
), SetDataTip(0x0, STR_TOWN_DIRECTORY_LIST_TOOLTIP
),
709 SetFill(1, 0), SetResize(1, 1), SetScrollbar(WID_TD_SCROLLBAR
), EndContainer(),
710 NWidget(WWT_PANEL
, COLOUR_BROWN
),
711 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
),
714 NWidget(NWID_VERTICAL
),
715 NWidget(NWID_VSCROLLBAR
, COLOUR_BROWN
, WID_TD_SCROLLBAR
),
716 NWidget(WWT_RESIZEBOX
, COLOUR_BROWN
),
721 /** Enum referring to the Hotkeys in the town directory window */
722 enum TownDirectoryHotkeys
{
723 TDHK_FOCUS_FILTER_BOX
, ///< Focus the filter box
726 /** Town directory window class. */
727 struct TownDirectoryWindow
: public Window
{
729 /* Runtime saved values */
730 static Listing last_sorting
;
732 /* Constants for sorting towns */
733 static inline const StringID sorter_names
[] = {
735 STR_SORT_BY_POPULATION
,
738 static const std::initializer_list
<GUITownList::SortFunction
* const> sorter_funcs
;
740 StringFilter string_filter
; ///< Filter for towns
741 QueryString townname_editbox
; ///< Filter editbox
743 GUITownList towns
{TownDirectoryWindow::last_sorting
.order
};
747 void BuildSortTownList()
749 if (this->towns
.NeedRebuild()) {
751 this->towns
.reserve(Town::GetNumItems());
753 for (const Town
*t
: Town::Iterate()) {
754 if (this->string_filter
.IsEmpty()) {
755 this->towns
.push_back(t
);
758 this->string_filter
.ResetState();
759 this->string_filter
.AddLine(t
->GetCachedName());
760 if (this->string_filter
.GetState()) this->towns
.push_back(t
);
763 this->towns
.RebuildDone();
764 this->vscroll
->SetCount(this->towns
.size()); // Update scrollbar as well.
766 /* Always sort the towns. */
768 this->SetWidgetDirty(WID_TD_LIST
); // Force repaint of the displayed towns.
771 /** Sort by town name */
772 static bool TownNameSorter(const Town
* const &a
, const Town
* const &b
, const bool &)
774 return StrNaturalCompare(a
->GetCachedName(), b
->GetCachedName()) < 0; // Sort by name (natural sorting).
777 /** Sort by population (default descending, as big towns are of the most interest). */
778 static bool TownPopulationSorter(const Town
* const &a
, const Town
* const &b
, const bool &order
)
780 uint32_t a_population
= a
->cache
.population
;
781 uint32_t b_population
= b
->cache
.population
;
782 if (a_population
== b_population
) return TownDirectoryWindow::TownNameSorter(a
, b
, order
);
783 return a_population
< b_population
;
786 /** Sort by town rating */
787 static bool TownRatingSorter(const Town
* const &a
, const Town
* const &b
, const bool &order
)
789 bool before
= !order
; // Value to get 'a' before 'b'.
791 /* Towns without rating are always after towns with rating. */
792 if (HasBit(a
->have_ratings
, _local_company
)) {
793 if (HasBit(b
->have_ratings
, _local_company
)) {
794 int16_t a_rating
= a
->ratings
[_local_company
];
795 int16_t b_rating
= b
->ratings
[_local_company
];
796 if (a_rating
== b_rating
) return TownDirectoryWindow::TownNameSorter(a
, b
, order
);
797 return a_rating
< b_rating
;
801 if (HasBit(b
->have_ratings
, _local_company
)) return !before
;
803 /* Sort unrated towns always on ascending town name. */
804 if (before
) return TownDirectoryWindow::TownNameSorter(a
, b
, order
);
805 return TownDirectoryWindow::TownNameSorter(b
, a
, order
);
809 TownDirectoryWindow(WindowDesc
&desc
) : Window(desc
), townname_editbox(MAX_LENGTH_TOWN_NAME_CHARS
* MAX_CHAR_LENGTH
, MAX_LENGTH_TOWN_NAME_CHARS
)
811 this->CreateNestedTree();
813 this->vscroll
= this->GetScrollbar(WID_TD_SCROLLBAR
);
815 this->towns
.SetListing(this->last_sorting
);
816 this->towns
.SetSortFuncs(TownDirectoryWindow::sorter_funcs
);
817 this->towns
.ForceRebuild();
818 this->BuildSortTownList();
820 this->FinishInitNested(0);
822 this->querystrings
[WID_TD_FILTER
] = &this->townname_editbox
;
823 this->townname_editbox
.cancel_button
= QueryString::ACTION_CLEAR
;
826 void SetStringParameters(WidgetID widget
) const override
830 SetDParam(0, this->vscroll
->GetCount());
831 SetDParam(1, Town::GetNumItems());
834 case WID_TD_WORLD_POPULATION
:
835 SetDParam(0, GetWorldPopulation());
838 case WID_TD_SORT_CRITERIA
:
839 SetDParam(0, TownDirectoryWindow::sorter_names
[this->towns
.SortType()]);
845 * Get the string to draw the town name.
846 * @param t Town to draw.
847 * @return The string to use.
849 static StringID
GetTownString(const Town
*t
)
851 return t
->larger_town
? STR_TOWN_DIRECTORY_CITY
: STR_TOWN_DIRECTORY_TOWN
;
854 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
857 case WID_TD_SORT_ORDER
:
858 this->DrawSortButtonState(widget
, this->towns
.IsDescSortOrder() ? SBS_DOWN
: SBS_UP
);
862 Rect tr
= r
.Shrink(WidgetDimensions::scaled
.framerect
);
863 if (this->towns
.empty()) { // No towns available.
864 DrawString(tr
, STR_TOWN_DIRECTORY_NONE
);
868 /* At least one town available. */
869 bool rtl
= _current_text_dir
== TD_RTL
;
870 Dimension icon_size
= GetSpriteSize(SPR_TOWN_RATING_GOOD
);
871 int icon_x
= tr
.WithWidth(icon_size
.width
, rtl
).left
;
872 tr
= tr
.Indent(icon_size
.width
+ WidgetDimensions::scaled
.hsep_normal
, rtl
);
874 auto [first
, last
] = this->vscroll
->GetVisibleRangeIterators(this->towns
);
875 for (auto it
= first
; it
!= last
; ++it
) {
877 assert(t
->xy
!= INVALID_TILE
);
879 /* Draw rating icon. */
880 if (_game_mode
== GM_EDITOR
|| !HasBit(t
->have_ratings
, _local_company
)) {
881 DrawSprite(SPR_TOWN_RATING_NA
, PAL_NONE
, icon_x
, tr
.top
+ (this->resize
.step_height
- icon_size
.height
) / 2);
883 SpriteID icon
= SPR_TOWN_RATING_APALLING
;
884 if (t
->ratings
[_local_company
] > RATING_VERYPOOR
) icon
= SPR_TOWN_RATING_MEDIOCRE
;
885 if (t
->ratings
[_local_company
] > RATING_GOOD
) icon
= SPR_TOWN_RATING_GOOD
;
886 DrawSprite(icon
, PAL_NONE
, icon_x
, tr
.top
+ (this->resize
.step_height
- icon_size
.height
) / 2);
889 SetDParam(0, t
->index
);
890 SetDParam(1, t
->cache
.population
);
891 DrawString(tr
.left
, tr
.right
, tr
.top
+ (this->resize
.step_height
- GetCharacterHeight(FS_NORMAL
)) / 2, GetTownString(t
));
893 tr
.top
+= this->resize
.step_height
;
900 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
903 case WID_TD_SORT_ORDER
: {
904 Dimension d
= GetStringBoundingBox(this->GetWidget
<NWidgetCore
>(widget
)->widget_data
);
905 d
.width
+= padding
.width
+ Window::SortButtonWidth() * 2; // Doubled since the string is centred and it also looks better.
906 d
.height
+= padding
.height
;
907 size
= maxdim(size
, d
);
910 case WID_TD_SORT_CRITERIA
: {
911 Dimension d
= GetStringListBoundingBox(TownDirectoryWindow::sorter_names
);
912 d
.width
+= padding
.width
;
913 d
.height
+= padding
.height
;
914 size
= maxdim(size
, d
);
918 Dimension d
= GetStringBoundingBox(STR_TOWN_DIRECTORY_NONE
);
919 for (uint i
= 0; i
< this->towns
.size(); i
++) {
920 const Town
*t
= this->towns
[i
];
922 assert(t
!= nullptr);
924 SetDParam(0, t
->index
);
925 SetDParamMaxDigits(1, 8);
926 d
= maxdim(d
, GetStringBoundingBox(GetTownString(t
)));
928 Dimension icon_size
= GetSpriteSize(SPR_TOWN_RATING_GOOD
);
929 d
.width
+= icon_size
.width
+ 2;
930 d
.height
= std::max(d
.height
, icon_size
.height
);
931 resize
.height
= d
.height
;
933 d
.width
+= padding
.width
;
934 d
.height
+= padding
.height
;
935 size
= maxdim(size
, d
);
938 case WID_TD_WORLD_POPULATION
: {
939 SetDParamMaxDigits(0, 10);
940 Dimension d
= GetStringBoundingBox(STR_TOWN_POPULATION
);
941 d
.width
+= padding
.width
;
942 d
.height
+= padding
.height
;
943 size
= maxdim(size
, d
);
949 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
952 case WID_TD_SORT_ORDER
: // Click on sort order button
953 if (this->towns
.SortType() != 2) { // A different sort than by rating.
954 this->towns
.ToggleSortOrder();
955 this->last_sorting
= this->towns
.GetListing(); // Store new sorting order.
957 /* Some parts are always sorted ascending on name. */
958 this->last_sorting
.order
= !this->last_sorting
.order
;
959 this->towns
.SetListing(this->last_sorting
);
960 this->towns
.ForceResort();
966 case WID_TD_SORT_CRITERIA
: // Click on sort criteria dropdown
967 ShowDropDownMenu(this, TownDirectoryWindow::sorter_names
, this->towns
.SortType(), WID_TD_SORT_CRITERIA
, 0, 0);
970 case WID_TD_LIST
: { // Click on Town Matrix
971 auto it
= this->vscroll
->GetScrolledItemFromWidget(this->towns
, pt
.y
, this, WID_TD_LIST
, WidgetDimensions::scaled
.framerect
.top
);
972 if (it
== this->towns
.end()) return; // click out of town bounds
975 assert(t
!= nullptr);
977 ShowExtraViewportWindow(t
->xy
);
979 ScrollMainWindowToTile(t
->xy
);
986 void OnDropdownSelect(WidgetID widget
, int index
) override
988 if (widget
!= WID_TD_SORT_CRITERIA
) return;
990 if (this->towns
.SortType() != index
) {
991 this->towns
.SetSortType(index
);
992 this->last_sorting
= this->towns
.GetListing(); // Store new sorting order.
993 this->BuildSortTownList();
997 void OnPaint() override
999 if (this->towns
.NeedRebuild()) this->BuildSortTownList();
1000 this->DrawWidgets();
1003 /** Redraw the whole window on a regular interval. */
1004 IntervalTimer
<TimerWindow
> rebuild_interval
= {std::chrono::seconds(3), [this](auto) {
1005 this->BuildSortTownList();
1009 void OnResize() override
1011 this->vscroll
->SetCapacityFromWidget(this, WID_TD_LIST
, WidgetDimensions::scaled
.framerect
.Vertical());
1014 void OnEditboxChanged(WidgetID wid
) override
1016 if (wid
== WID_TD_FILTER
) {
1017 this->string_filter
.SetFilterTerm(this->townname_editbox
.text
.buf
);
1018 this->InvalidateData(TDIWD_FORCE_REBUILD
);
1023 * Some data on this window has become invalid.
1024 * @param data Information about the changed data.
1025 * @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.
1027 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
1030 case TDIWD_FORCE_REBUILD
:
1031 /* This needs to be done in command-scope to enforce rebuilding before resorting invalid data */
1032 this->towns
.ForceRebuild();
1035 case TDIWD_POPULATION_CHANGE
:
1036 if (this->towns
.SortType() == 1) this->towns
.ForceResort();
1040 this->towns
.ForceResort();
1044 EventState
OnHotkey(int hotkey
) override
1047 case TDHK_FOCUS_FILTER_BOX
:
1048 this->SetFocusedWidget(WID_TD_FILTER
);
1049 SetFocusedWindow(this); // The user has asked to give focus to the text box, so make sure this window is focused.
1052 return ES_NOT_HANDLED
;
1057 static inline HotkeyList hotkeys
{"towndirectory", {
1058 Hotkey('F', "focus_filter_box", TDHK_FOCUS_FILTER_BOX
),
1062 Listing
TownDirectoryWindow::last_sorting
= {false, 0};
1064 /** Available town directory sorting functions. */
1065 const std::initializer_list
<GUITownList::SortFunction
* const> TownDirectoryWindow::sorter_funcs
= {
1067 &TownPopulationSorter
,
1071 static WindowDesc
_town_directory_desc(
1072 WDP_AUTO
, "list_towns", 208, 202,
1073 WC_TOWN_DIRECTORY
, WC_NONE
,
1075 _nested_town_directory_widgets
,
1076 &TownDirectoryWindow::hotkeys
1079 void ShowTownDirectory()
1081 if (BringWindowToFrontById(WC_TOWN_DIRECTORY
, 0)) return;
1082 new TownDirectoryWindow(_town_directory_desc
);
1085 void CcFoundTown(Commands
, const CommandCost
&result
, TileIndex tile
)
1087 if (result
.Failed()) return;
1089 if (_settings_client
.sound
.confirm
) SndPlayTileFx(SND_1F_CONSTRUCTION_OTHER
, tile
);
1090 if (!_settings_client
.gui
.persistent_buildingtools
) ResetObjectToPlace();
1093 void CcFoundRandomTown(Commands
, const CommandCost
&result
, Money
, TownID town_id
)
1095 if (result
.Succeeded()) ScrollMainWindowToTile(Town::Get(town_id
)->xy
);
1098 static constexpr NWidgetPart _nested_found_town_widgets
[] = {
1099 NWidget(NWID_HORIZONTAL
),
1100 NWidget(WWT_CLOSEBOX
, COLOUR_DARK_GREEN
),
1101 NWidget(WWT_CAPTION
, COLOUR_DARK_GREEN
), SetDataTip(STR_FOUND_TOWN_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
1102 NWidget(WWT_SHADEBOX
, COLOUR_DARK_GREEN
),
1103 NWidget(WWT_STICKYBOX
, COLOUR_DARK_GREEN
),
1105 /* Construct new town(s) buttons. */
1106 NWidget(WWT_PANEL
, COLOUR_DARK_GREEN
),
1107 NWidget(NWID_VERTICAL
), SetPIP(0, WidgetDimensions::unscaled
.vsep_normal
, 0), SetPadding(WidgetDimensions::unscaled
.picker
),
1108 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),
1109 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),
1110 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),
1111 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),
1113 /* Town name selection. */
1114 NWidget(WWT_LABEL
, COLOUR_DARK_GREEN
), SetDataTip(STR_FOUND_TOWN_NAME_TITLE
, STR_NULL
),
1115 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),
1116 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),
1118 /* Town size selection. */
1119 NWidget(WWT_LABEL
, COLOUR_DARK_GREEN
), SetDataTip(STR_FOUND_TOWN_INITIAL_SIZE_TITLE
, STR_NULL
),
1120 NWidget(NWID_VERTICAL
),
1121 NWidget(NWID_HORIZONTAL
, NC_EQUALSIZE
),
1122 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),
1123 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),
1125 NWidget(NWID_HORIZONTAL
, NC_EQUALSIZE
),
1126 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),
1127 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 NWidget(WWT_TEXTBTN
, COLOUR_GREY
, WID_TF_CITY
), SetDataTip(STR_FOUND_TOWN_CITY
, STR_FOUND_TOWN_CITY_TOOLTIP
), SetFill(1, 0),
1132 /* Town roads selection. */
1133 NWidget(WWT_LABEL
, COLOUR_DARK_GREEN
), SetDataTip(STR_FOUND_TOWN_ROAD_LAYOUT
, STR_NULL
),
1134 NWidget(NWID_VERTICAL
),
1135 NWidget(NWID_HORIZONTAL
, NC_EQUALSIZE
),
1136 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),
1137 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),
1139 NWidget(NWID_HORIZONTAL
, NC_EQUALSIZE
),
1140 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),
1141 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),
1143 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),
1149 /** Found a town window class. */
1150 struct FoundTownWindow
: Window
{
1152 TownSize town_size
; ///< Selected town size
1153 TownLayout town_layout
; ///< Selected town layout
1154 bool city
; ///< Are we building a city?
1155 QueryString townname_editbox
; ///< Townname editbox
1156 bool townnamevalid
; ///< Is generated town name valid?
1157 uint32_t townnameparts
; ///< Generated town name
1158 TownNameParams params
; ///< Town name parameters
1161 FoundTownWindow(WindowDesc
&desc
, WindowNumber window_number
) :
1163 town_size(TSZ_MEDIUM
),
1164 town_layout(_settings_game
.economy
.town_layout
),
1165 townname_editbox(MAX_LENGTH_TOWN_NAME_CHARS
* MAX_CHAR_LENGTH
, MAX_LENGTH_TOWN_NAME_CHARS
),
1166 params(_settings_game
.game_creation
.town_name
)
1168 this->InitNested(window_number
);
1169 this->querystrings
[WID_TF_TOWN_NAME_EDITBOX
] = &this->townname_editbox
;
1170 this->RandomTownName();
1171 this->UpdateButtons(true);
1174 void RandomTownName()
1176 this->townnamevalid
= GenerateTownName(_interactive_random
, &this->townnameparts
);
1178 if (!this->townnamevalid
) {
1179 this->townname_editbox
.text
.DeleteAll();
1181 this->townname_editbox
.text
.Assign(GetTownName(&this->params
, this->townnameparts
));
1183 UpdateOSKOriginalText(this, WID_TF_TOWN_NAME_EDITBOX
);
1185 this->SetWidgetDirty(WID_TF_TOWN_NAME_EDITBOX
);
1188 void UpdateButtons(bool check_availability
)
1190 if (check_availability
&& _game_mode
!= GM_EDITOR
) {
1191 this->SetWidgetsDisabledState(true, WID_TF_RANDOM_TOWN
, WID_TF_MANY_RANDOM_TOWNS
, WID_TF_EXPAND_ALL_TOWNS
, WID_TF_SIZE_LARGE
);
1192 this->SetWidgetsDisabledState(_settings_game
.economy
.found_town
!= TF_CUSTOM_LAYOUT
,
1193 WID_TF_LAYOUT_ORIGINAL
, WID_TF_LAYOUT_BETTER
, WID_TF_LAYOUT_GRID2
, WID_TF_LAYOUT_GRID3
, WID_TF_LAYOUT_RANDOM
);
1194 if (_settings_game
.economy
.found_town
!= TF_CUSTOM_LAYOUT
) town_layout
= _settings_game
.economy
.town_layout
;
1197 for (WidgetID i
= WID_TF_SIZE_SMALL
; i
<= WID_TF_SIZE_RANDOM
; i
++) {
1198 this->SetWidgetLoweredState(i
, i
== WID_TF_SIZE_SMALL
+ this->town_size
);
1201 this->SetWidgetLoweredState(WID_TF_CITY
, this->city
);
1203 for (WidgetID i
= WID_TF_LAYOUT_ORIGINAL
; i
<= WID_TF_LAYOUT_RANDOM
; i
++) {
1204 this->SetWidgetLoweredState(i
, i
== WID_TF_LAYOUT_ORIGINAL
+ this->town_layout
);
1210 template <typename Tcallback
>
1211 void ExecuteFoundTownCommand(TileIndex tile
, bool random
, StringID errstr
, Tcallback cc
)
1215 if (!this->townnamevalid
) {
1216 name
= this->townname_editbox
.text
.buf
;
1218 /* If user changed the name, send it */
1219 std::string original_name
= GetTownName(&this->params
, this->townnameparts
);
1220 if (original_name
!= this->townname_editbox
.text
.buf
) name
= this->townname_editbox
.text
.buf
;
1223 bool success
= Command
<CMD_FOUND_TOWN
>::Post(errstr
, cc
,
1224 tile
, this->town_size
, this->city
, this->town_layout
, random
, townnameparts
, name
);
1226 /* Rerandomise name, if success and no cost-estimation. */
1227 if (success
&& !_shift_pressed
) this->RandomTownName();
1230 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
1233 case WID_TF_NEW_TOWN
:
1234 HandlePlacePushButton(this, WID_TF_NEW_TOWN
, SPR_CURSOR_TOWN
, HT_RECT
);
1237 case WID_TF_RANDOM_TOWN
:
1238 this->ExecuteFoundTownCommand(0, true, STR_ERROR_CAN_T_GENERATE_TOWN
, CcFoundRandomTown
);
1241 case WID_TF_TOWN_NAME_RANDOM
:
1242 this->RandomTownName();
1243 this->SetFocusedWidget(WID_TF_TOWN_NAME_EDITBOX
);
1246 case WID_TF_MANY_RANDOM_TOWNS
: {
1247 Backup
<bool> old_generating_world(_generating_world
, true);
1248 UpdateNearestTownForRoadTiles(true);
1249 if (!GenerateTowns(this->town_layout
)) {
1250 ShowErrorMessage(STR_ERROR_CAN_T_GENERATE_TOWN
, STR_ERROR_NO_SPACE_FOR_TOWN
, WL_INFO
);
1252 UpdateNearestTownForRoadTiles(false);
1253 old_generating_world
.Restore();
1257 case WID_TF_EXPAND_ALL_TOWNS
:
1258 for (Town
*t
: Town::Iterate()) {
1259 Command
<CMD_EXPAND_TOWN
>::Do(DC_EXEC
, t
->index
, 0);
1263 case WID_TF_SIZE_SMALL
: case WID_TF_SIZE_MEDIUM
: case WID_TF_SIZE_LARGE
: case WID_TF_SIZE_RANDOM
:
1264 this->town_size
= (TownSize
)(widget
- WID_TF_SIZE_SMALL
);
1265 this->UpdateButtons(false);
1270 this->SetWidgetLoweredState(WID_TF_CITY
, this->city
);
1274 case WID_TF_LAYOUT_ORIGINAL
: case WID_TF_LAYOUT_BETTER
: case WID_TF_LAYOUT_GRID2
:
1275 case WID_TF_LAYOUT_GRID3
: case WID_TF_LAYOUT_RANDOM
:
1276 this->town_layout
= (TownLayout
)(widget
- WID_TF_LAYOUT_ORIGINAL
);
1277 this->UpdateButtons(false);
1282 void OnPlaceObject([[maybe_unused
]] Point pt
, TileIndex tile
) override
1284 this->ExecuteFoundTownCommand(tile
, false, STR_ERROR_CAN_T_FOUND_TOWN_HERE
, CcFoundTown
);
1287 void OnPlaceObjectAbort() override
1289 this->RaiseButtons();
1290 this->UpdateButtons(false);
1294 * Some data on this window has become invalid.
1295 * @param data Information about the changed data.
1296 * @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.
1298 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
1300 if (!gui_scope
) return;
1301 this->UpdateButtons(true);
1305 static WindowDesc
_found_town_desc(
1306 WDP_AUTO
, "build_town", 160, 162,
1307 WC_FOUND_TOWN
, WC_NONE
,
1309 _nested_found_town_widgets
1312 void ShowFoundTownWindow()
1314 if (_game_mode
!= GM_EDITOR
&& !Company::IsValidID(_local_company
)) return;
1315 AllocateWindowDescFront
<FoundTownWindow
>(_found_town_desc
, 0);
1318 void InitializeTownGui()
1320 _town_local_authority_kdtree
.Clear();
1324 * Draw representation of a house tile for GUI purposes.
1325 * @param x Position x of image.
1326 * @param y Position y of image.
1327 * @param spec House spec to draw.
1328 * @param house_id House ID to draw.
1329 * @param view The house's 'view'.
1331 void DrawNewHouseTileInGUI(int x
, int y
, const HouseSpec
*spec
, HouseID house_id
, int view
)
1333 HouseResolverObject
object(house_id
, INVALID_TILE
, nullptr, CBID_NO_CALLBACK
, 0, 0, true, view
);
1334 const SpriteGroup
*group
= object
.Resolve();
1335 if (group
== nullptr || group
->type
!= SGT_TILELAYOUT
) return;
1337 uint8_t stage
= TOWN_HOUSE_COMPLETED
;
1338 const DrawTileSprites
*dts
= reinterpret_cast<const TileLayoutSpriteGroup
*>(group
)->ProcessRegisters(&stage
);
1340 PaletteID palette
= GENERAL_SPRITE_COLOUR(spec
->random_colour
[0]);
1341 if (HasBit(spec
->callback_mask
, CBM_HOUSE_COLOUR
)) {
1342 uint16_t callback
= GetHouseCallback(CBID_HOUSE_COLOUR
, 0, 0, house_id
, nullptr, INVALID_TILE
, true, view
);
1343 if (callback
!= CALLBACK_FAILED
) {
1344 /* If bit 14 is set, we should use a 2cc colour map, else use the callback value. */
1345 palette
= HasBit(callback
, 14) ? GB(callback
, 0, 8) + SPR_2CCMAP_BASE
: callback
;
1349 SpriteID image
= dts
->ground
.sprite
;
1350 PaletteID pal
= dts
->ground
.pal
;
1352 if (HasBit(image
, SPRITE_MODIFIER_CUSTOM_SPRITE
)) image
+= stage
;
1353 if (HasBit(pal
, SPRITE_MODIFIER_CUSTOM_SPRITE
)) pal
+= stage
;
1355 if (GB(image
, 0, SPRITE_WIDTH
) != 0) {
1356 DrawSprite(image
, GroundSpritePaletteTransform(image
, pal
, palette
), x
, y
);
1359 DrawNewGRFTileSeqInGUI(x
, y
, dts
, stage
, palette
);
1363 * Draw a house that does not exist.
1364 * @param x Position x of image.
1365 * @param y Position y of image.
1366 * @param house_id House ID to draw.
1367 * @param view The house's 'view'.
1369 void DrawHouseInGUI(int x
, int y
, HouseID house_id
, int view
)
1371 auto draw
= [](int x
, int y
, HouseID house_id
, int view
) {
1372 if (house_id
>= NEW_HOUSE_OFFSET
) {
1373 /* Houses don't necessarily need new graphics. If they don't have a
1374 * spritegroup associated with them, then the sprite for the substitute
1375 * house id is drawn instead. */
1376 const HouseSpec
*spec
= HouseSpec::Get(house_id
);
1377 if (spec
->grf_prop
.spritegroup
[0] != nullptr) {
1378 DrawNewHouseTileInGUI(x
, y
, spec
, house_id
, view
);
1381 house_id
= HouseSpec::Get(house_id
)->grf_prop
.subst_id
;
1385 /* Retrieve data from the draw town tile struct */
1386 const DrawBuildingsTileStruct
&dcts
= GetTownDrawTileData()[house_id
<< 4 | view
<< 2 | TOWN_HOUSE_COMPLETED
];
1387 DrawSprite(dcts
.ground
.sprite
, dcts
.ground
.pal
, x
, y
);
1389 /* Add a house on top of the ground? */
1390 if (dcts
.building
.sprite
!= 0) {
1391 Point pt
= RemapCoords(dcts
.subtile_x
, dcts
.subtile_y
, 0);
1392 DrawSprite(dcts
.building
.sprite
, dcts
.building
.pal
, x
+ UnScaleGUI(pt
.x
), y
+ UnScaleGUI(pt
.y
));
1396 /* Houses can have 1x1, 1x2, 2x1 and 2x2 layouts which are individual HouseIDs. For the GUI we need
1397 * draw all of the tiles with appropriate positions. */
1398 int x_delta
= ScaleGUITrad(TILE_PIXELS
);
1399 int y_delta
= ScaleGUITrad(TILE_PIXELS
/ 2);
1401 const HouseSpec
*hs
= HouseSpec::Get(house_id
);
1402 if (hs
->building_flags
& TILE_SIZE_2x2
) {
1403 draw(x
, y
- y_delta
- y_delta
, house_id
, view
); // North corner.
1404 draw(x
+ x_delta
, y
- y_delta
, house_id
+ 1, view
); // West corner.
1405 draw(x
- x_delta
, y
- y_delta
, house_id
+ 2, view
); // East corner.
1406 draw(x
, y
, house_id
+ 3, view
); // South corner.
1407 } else if (hs
->building_flags
& TILE_SIZE_2x1
) {
1408 draw(x
+ x_delta
/ 2, y
- y_delta
, house_id
, view
); // North east tile.
1409 draw(x
- x_delta
/ 2, y
, house_id
+ 1, view
); // South west tile.
1410 } else if (hs
->building_flags
& TILE_SIZE_1x2
) {
1411 draw(x
- x_delta
/ 2, y
- y_delta
, house_id
, view
); // North west tile.
1412 draw(x
+ x_delta
/ 2, y
, house_id
+ 1, view
); // South east tile.
1414 draw(x
, y
, house_id
, view
);
1419 class HousePickerCallbacks
: public PickerCallbacks
{
1421 HousePickerCallbacks() : PickerCallbacks("fav_houses") {}
1424 * Set climate mask for filtering buildings from current landscape.
1426 void SetClimateMask()
1428 switch (_settings_game
.game_creation
.landscape
) {
1429 case LT_TEMPERATE
: this->climate_mask
= HZ_TEMP
; break;
1430 case LT_ARCTIC
: this->climate_mask
= HZ_SUBARTC_ABOVE
| HZ_SUBARTC_BELOW
; break;
1431 case LT_TROPIC
: this->climate_mask
= HZ_SUBTROPIC
; break;
1432 case LT_TOYLAND
: this->climate_mask
= HZ_TOYLND
; break;
1433 default: NOT_REACHED();
1436 /* In some cases, not all 'classes' (house zones) have distinct houses, so we need to disable those.
1437 * As we need to check all types, and this cannot change with the picker window open, pre-calculate it.
1438 * This loop calls GetTypeName() instead of directly checking properties so that there is no discrepancy. */
1439 this->class_mask
= 0;
1441 int num_classes
= this->GetClassCount();
1442 for (int cls_id
= 0; cls_id
< num_classes
; ++cls_id
) {
1443 int num_types
= this->GetTypeCount(cls_id
);
1444 for (int id
= 0; id
< num_types
; ++id
) {
1445 if (this->GetTypeName(cls_id
, id
) != INVALID_STRING_ID
) {
1446 SetBit(this->class_mask
, cls_id
);
1453 HouseZones climate_mask
;
1454 uint8_t class_mask
; ///< Mask of available 'classes'.
1456 static inline int sel_class
; ///< Currently selected 'class'.
1457 static inline int sel_type
; ///< Currently selected HouseID.
1458 static inline int sel_view
; ///< Currently selected 'view'. This is not controllable as its based on random data.
1460 /* Houses do not have classes like NewGRFClass. We'll make up fake classes based on town zone
1461 * availability instead. */
1462 static inline const std::array
<StringID
, HZB_END
> zone_names
= {
1463 STR_HOUSE_PICKER_CLASS_ZONE1
,
1464 STR_HOUSE_PICKER_CLASS_ZONE2
,
1465 STR_HOUSE_PICKER_CLASS_ZONE3
,
1466 STR_HOUSE_PICKER_CLASS_ZONE4
,
1467 STR_HOUSE_PICKER_CLASS_ZONE5
,
1470 StringID
GetClassTooltip() const override
{ return STR_PICKER_HOUSE_CLASS_TOOLTIP
; }
1471 StringID
GetTypeTooltip() const override
{ return STR_PICKER_HOUSE_TYPE_TOOLTIP
; }
1472 bool IsActive() const override
{ return true; }
1474 bool HasClassChoice() const override
{ return true; }
1475 int GetClassCount() const override
{ return static_cast<int>(zone_names
.size()); }
1477 void Close([[maybe_unused
]] int data
) override
{ ResetObjectToPlace(); }
1479 int GetSelectedClass() const override
{ return HousePickerCallbacks::sel_class
; }
1480 void SetSelectedClass(int cls_id
) const override
{ HousePickerCallbacks::sel_class
= cls_id
; }
1482 StringID
GetClassName(int id
) const override
1484 if (id
>= GetClassCount()) return INVALID_STRING_ID
;
1485 if (!HasBit(this->class_mask
, id
)) return INVALID_STRING_ID
;
1486 return zone_names
[id
];
1489 int GetTypeCount(int cls_id
) const override
1491 if (cls_id
< GetClassCount()) return static_cast<int>(HouseSpec::Specs().size());
1495 PickerItem
GetPickerItem(int cls_id
, int id
) const override
1497 const auto *spec
= HouseSpec::Get(id
);
1498 if (spec
->grf_prop
.grffile
== nullptr) return {0, spec
->Index(), cls_id
, id
};
1499 return {spec
->grf_prop
.grffile
->grfid
, spec
->grf_prop
.local_id
, cls_id
, id
};
1502 int GetSelectedType() const override
{ return sel_type
; }
1503 void SetSelectedType(int id
) const override
{ sel_type
= id
; }
1505 StringID
GetTypeName(int cls_id
, int id
) const override
1507 const HouseSpec
*spec
= HouseSpec::Get(id
);
1508 if (spec
== nullptr) return INVALID_STRING_ID
;
1509 if (!spec
->enabled
) return INVALID_STRING_ID
;
1510 if ((spec
->building_availability
& climate_mask
) == 0) return INVALID_STRING_ID
;
1511 if (!HasBit(spec
->building_availability
, cls_id
)) return INVALID_STRING_ID
;
1512 for (int i
= 0; i
< cls_id
; i
++) {
1513 /* Don't include if it's already included in an earlier zone. */
1514 if (HasBit(spec
->building_availability
, i
)) return INVALID_STRING_ID
;
1517 return spec
->building_name
;
1520 bool IsTypeAvailable(int, int id
) const override
1522 const HouseSpec
*hs
= HouseSpec::Get(id
);
1526 void DrawType(int x
, int y
, int, int id
) const override
1528 DrawHouseInGUI(x
, y
, id
, HousePickerCallbacks::sel_view
);
1531 void FillUsedItems(std::set
<PickerItem
> &items
) override
1533 auto id_count
= GetBuildingHouseIDCounts();
1534 for (auto it
= id_count
.begin(); it
!= id_count
.end(); ++it
) {
1535 if (*it
== 0) continue;
1536 HouseID house
= static_cast<HouseID
>(std::distance(id_count
.begin(), it
));
1537 const HouseSpec
*hs
= HouseSpec::Get(house
);
1538 int class_index
= FindFirstBit(hs
->building_availability
& HZ_ZONALL
);
1539 items
.insert({0, house
, class_index
, house
});
1543 std::set
<PickerItem
> UpdateSavedItems(const std::set
<PickerItem
> &src
) override
1545 if (src
.empty()) return src
;
1547 const auto specs
= HouseSpec::Specs();
1548 std::set
<PickerItem
> dst
;
1549 for (const auto &item
: src
) {
1550 if (item
.grfid
== 0) {
1553 /* Search for spec by grfid and local index. */
1554 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
; });
1555 if (it
== specs
.end()) {
1556 /* Not preset, hide from UI. */
1557 dst
.insert({item
.grfid
, item
.local_id
, -1, -1});
1559 int class_index
= FindFirstBit(it
->building_availability
& HZ_ZONALL
);
1560 dst
.insert( {item
.grfid
, item
.local_id
, class_index
, it
->Index()});
1568 static HousePickerCallbacks instance
;
1570 /* static */ HousePickerCallbacks
HousePickerCallbacks::instance
;
1572 struct BuildHouseWindow
: public PickerWindow
{
1573 BuildHouseWindow(WindowDesc
&desc
, Window
*parent
) : PickerWindow(desc
, parent
, 0, HousePickerCallbacks::instance
)
1575 HousePickerCallbacks::instance
.SetClimateMask();
1576 this->ConstructWindow();
1577 this->InvalidateData();
1580 void UpdateSelectSize(const HouseSpec
*spec
)
1582 if (spec
== nullptr) {
1583 SetTileSelectSize(1, 1);
1584 ResetObjectToPlace();
1586 SetObjectToPlaceWnd(SPR_CURSOR_TOWN
, PAL_NONE
, HT_RECT
| HT_DIAGONAL
, this);
1587 if (spec
->building_flags
& TILE_SIZE_2x2
) {
1588 SetTileSelectSize(2, 2);
1589 } else if (spec
->building_flags
& TILE_SIZE_2x1
) {
1590 SetTileSelectSize(2, 1);
1591 } else if (spec
->building_flags
& TILE_SIZE_1x2
) {
1592 SetTileSelectSize(1, 2);
1593 } else if (spec
->building_flags
& TILE_SIZE_1x1
) {
1594 SetTileSelectSize(1, 1);
1599 void OnInvalidateData(int data
= 0, bool gui_scope
= true) override
1601 this->PickerWindow::OnInvalidateData(data
, gui_scope
);
1602 if (!gui_scope
) return;
1604 if ((data
& PickerWindow::PFI_POSITION
) != 0) {
1605 const HouseSpec
*spec
= HouseSpec::Get(HousePickerCallbacks::sel_type
);
1606 UpdateSelectSize(spec
);
1610 void OnPlaceObject([[maybe_unused
]] Point pt
, TileIndex tile
) override
1612 const HouseSpec
*spec
= HouseSpec::Get(HousePickerCallbacks::sel_type
);
1613 Command
<CMD_PLACE_HOUSE
>::Post(STR_ERROR_CAN_T_BUILD_HOUSE
, CcPlaySound_CONSTRUCTION_OTHER
, tile
, spec
->Index());
1616 IntervalTimer
<TimerWindow
> view_refresh_interval
= {std::chrono::milliseconds(2500), [this](auto) {
1617 /* There are four different 'views' that are random based on house tile position. As this is not
1618 * user-controllable, instead we automatically cycle through them. */
1619 HousePickerCallbacks::sel_view
= (HousePickerCallbacks::sel_view
+ 1) % 4;
1623 static inline HotkeyList hotkeys
{"buildhouse", {
1624 Hotkey('F', "focus_filter_box", PCWHK_FOCUS_FILTER_BOX
),
1628 /** Nested widget definition for the build NewGRF rail waypoint window */
1629 static constexpr NWidgetPart _nested_build_house_widgets
[] = {
1630 NWidget(NWID_HORIZONTAL
),
1631 NWidget(WWT_CLOSEBOX
, COLOUR_DARK_GREEN
),
1632 NWidget(WWT_CAPTION
, COLOUR_DARK_GREEN
), SetDataTip(STR_HOUSE_PICKER_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
1633 NWidget(WWT_SHADEBOX
, COLOUR_DARK_GREEN
),
1634 NWidget(WWT_DEFSIZEBOX
, COLOUR_DARK_GREEN
),
1635 NWidget(WWT_STICKYBOX
, COLOUR_DARK_GREEN
),
1637 NWidget(NWID_HORIZONTAL
),
1638 NWidgetFunction(MakePickerClassWidgets
),
1639 NWidgetFunction(MakePickerTypeWidgets
),
1643 static WindowDesc
_build_house_desc(
1644 WDP_AUTO
, "build_house", 0, 0,
1645 WC_BUILD_HOUSE
, WC_BUILD_TOOLBAR
,
1647 _nested_build_house_widgets
,
1648 &BuildHouseWindow::hotkeys
1651 void ShowBuildHousePicker(Window
*parent
)
1653 if (BringWindowToFrontById(WC_BUILD_HOUSE
, 0)) return;
1654 new BuildHouseWindow(_build_house_desc
, parent
);