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"
37 #include "stringfilter_type.h"
38 #include "dropdown_func.h"
39 #include "town_kdtree.h"
41 #include "timer/timer.h"
42 #include "timer/timer_game_calendar.h"
43 #include "timer/timer_window.h"
44 #include "zoom_func.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
),
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
),
75 /** Town authority window. */
76 struct TownAuthorityWindow
: Window
{
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
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
)
99 for (uint i
: SetBitIterator(this->enabled_actions
)) {
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
);
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
));
159 if (!this->IsShaded())
166 /** Draw the contents of the ratings panel. May request a resize of the window if the contents does not fit. */
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
++;
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. */
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
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
);
263 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
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
);
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
;
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
;
295 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
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();
310 case WID_TA_COMMAND_LIST
: {
311 int y
= this->GetRowFromWidget(pt
.y
, WID_TA_COMMAND_LIST
, 1, GetCharacterHeight(FS_NORMAL
)) - 1;
319 /* When double-clicking, continue */
320 if (click_count
== 1 || y
< 0 || !HasBit(this->available_actions
, y
)) break;
325 Command
<CMD_DO_TOWN_ACTION
>::Post(STR_ERROR_CAN_T_DO_THIS
, this->town
->xy
, this->window_number
, this->sel_index
);
330 /** Redraw the whole window on a regular interval. */
331 IntervalTimer
<TimerWindow
> redraw_interval
= {std::chrono::seconds(3), [this](auto) {
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
{
362 Town
*town
; ///< Town displayed by the window.
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
);
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
);
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;
434 DrawString(tr
, STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH
);
435 tr
.top
+= GetCharacterHeight(FS_NORMAL
);
439 bool rtl
= _current_text_dir
== TD_RTL
;
441 const CargoSpec
*cargo
= FindFirstCargoWithTownAcceptanceEffect((TownAcceptanceEffect
)i
);
442 assert(cargo
!= nullptr);
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
);
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
);
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
499 case WID_TV_CENTER_VIEW
: // scroll to location
501 ShowExtraViewportWindow(this->town
->xy
);
503 ScrollMainWindowToTile(this->town
->xy
);
507 case WID_TV_SHOW_AUTHORITY
: // town authority
508 ShowTownAuthorityWindow(this->window_number
);
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
);
516 case WID_TV_CATCHMENT
:
517 SetViewportCatchmentTown(Town::Get(this->window_number
), !this->IsWidgetLowered(WID_TV_CATCHMENT
));
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);
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
);
531 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
535 size
.height
= GetDesiredInfoHeight(size
.width
) + padding
.height
;
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
);
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;
555 aimed_height
+= GetCharacterHeight(FS_NORMAL
);
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());
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
)) {
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 */
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
),
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),
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
),
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
),
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),
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
),
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
);
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
),
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
),
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
),
715 NWidget(NWID_VERTICAL
),
716 NWidget(NWID_VSCROLLBAR
, COLOUR_BROWN
, WID_TD_SCROLLBAR
),
717 NWidget(WWT_RESIZEBOX
, COLOUR_BROWN
),
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
{
730 /* Runtime saved values */
731 static Listing last_sorting
;
733 /* Constants for sorting towns */
734 static inline const StringID sorter_names
[] = {
736 STR_SORT_BY_POPULATION
,
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
};
748 void BuildSortTownList()
750 if (this->towns
.NeedRebuild()) {
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
);
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. */
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
;
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
);
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
831 SetDParam(0, this->vscroll
->GetCount());
832 SetDParam(1, Town::GetNumItems());
835 case WID_TD_WORLD_POPULATION
:
836 SetDParam(0, GetWorldPopulation());
839 case WID_TD_SORT_CRITERIA
:
840 SetDParam(0, TownDirectoryWindow::sorter_names
[this->towns
.SortType()]);
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
858 case WID_TD_SORT_ORDER
:
859 this->DrawSortButtonState(widget
, this->towns
.IsDescSortOrder() ? SBS_DOWN
: SBS_UP
);
863 Rect tr
= r
.Shrink(WidgetDimensions::scaled
.framerect
);
864 if (this->towns
.empty()) { // No towns available.
865 DrawString(tr
, STR_TOWN_DIRECTORY_NONE
);
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
) {
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);
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
;
901 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
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
);
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
);
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
;
934 d
.width
+= padding
.width
;
935 d
.height
+= padding
.height
;
936 size
= maxdim(size
, d
);
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
);
950 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
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.
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();
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);
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
976 assert(t
!= nullptr);
978 ShowExtraViewportWindow(t
->xy
);
980 ScrollMainWindowToTile(t
->xy
);
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();
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
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();
1036 case TDIWD_POPULATION_CHANGE
:
1037 if (this->towns
.SortType() == 1) this->towns
.ForceResort();
1041 this->towns
.ForceResort();
1045 EventState
OnHotkey(int hotkey
) override
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.
1053 return ES_NOT_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
= {
1068 &TownPopulationSorter
,
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
),
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),
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),
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),
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),
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),
1151 /** Found a town window class. */
1152 struct FoundTownWindow
: Window
{
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
1163 FoundTownWindow(WindowDesc
&desc
, WindowNumber window_number
) :
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();
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
);
1212 template <typename Tcallback
>
1213 void ExecuteFoundTownCommand(TileIndex tile
, bool random
, StringID errstr
, Tcallback cc
)
1217 if (!this->townnamevalid
) {
1218 name
= this->townname_editbox
.text
.buf
;
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
1235 case WID_TF_NEW_TOWN
:
1236 HandlePlacePushButton(this, WID_TF_NEW_TOWN
, SPR_CURSOR_TOWN
, HT_RECT
);
1239 case WID_TF_RANDOM_TOWN
:
1240 this->ExecuteFoundTownCommand(0, true, STR_ERROR_CAN_T_GENERATE_TOWN
, CcFoundRandomTown
);
1243 case WID_TF_TOWN_NAME_RANDOM
:
1244 this->RandomTownName();
1245 this->SetFocusedWidget(WID_TF_TOWN_NAME_EDITBOX
);
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();
1259 case WID_TF_LOAD_FROM_FILE
:
1260 ShowSaveLoadDialog(FT_TOWN_DATA
, SLO_LOAD
);
1263 case WID_TF_EXPAND_ALL_TOWNS
:
1264 for (Town
*t
: Town::Iterate()) {
1265 Command
<CMD_EXPAND_TOWN
>::Do(DC_EXEC
, t
->index
, 0);
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);
1276 this->SetWidgetLoweredState(WID_TF_CITY
, this->city
);
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);
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
,
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
);
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.
1425 draw(x
, y
, house_id
, view
);
1430 class HousePickerCallbacks
: public PickerCallbacks
{
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
);
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());
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
);
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) {
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});
1570 int class_index
= FindFirstBit(it
->building_availability
& HZ_ZONALL
);
1571 dst
.insert( {item
.grfid
, item
.local_id
, class_index
, it
->Index()});
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();
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;
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
),
1648 NWidget(NWID_HORIZONTAL
),
1649 NWidgetFunction(MakePickerClassWidgets
),
1650 NWidgetFunction(MakePickerTypeWidgets
),
1654 static WindowDesc
_build_house_desc(
1655 WDP_AUTO
, "build_house", 0, 0,
1656 WC_BUILD_HOUSE
, WC_BUILD_TOOLBAR
,
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
);