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 league_gui.cpp GUI for league tables. */
11 #include "league_gui.h"
12 #include "company_base.h"
13 #include "company_gui.h"
16 #include "league_base.h"
17 #include "sortlist_type.h"
18 #include "story_base.h"
19 #include "strings_func.h"
22 #include "viewport_func.h"
23 #include "window_gui.h"
25 #include "widgets/league_widget.h"
27 #include "table/strings.h"
28 #include "table/sprites.h"
30 #include "safeguards.h"
33 static const StringID _performance_titles
[] = {
34 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_ENGINEER
,
35 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_ENGINEER
,
36 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_TRAFFIC_MANAGER
,
37 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_TRAFFIC_MANAGER
,
38 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_TRANSPORT_COORDINATOR
,
39 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_TRANSPORT_COORDINATOR
,
40 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_ROUTE_SUPERVISOR
,
41 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_ROUTE_SUPERVISOR
,
42 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_DIRECTOR
,
43 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_DIRECTOR
,
44 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_CHIEF_EXECUTIVE
,
45 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_CHIEF_EXECUTIVE
,
46 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_CHAIRMAN
,
47 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_CHAIRMAN
,
48 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_PRESIDENT
,
49 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_TYCOON
,
52 static inline StringID
GetPerformanceTitleFromValue(uint value
)
54 return _performance_titles
[std::min(value
, 1000u) >> 6];
57 class PerformanceLeagueWindow
: public Window
{
59 GUIList
<const Company
*> companies
;
60 uint ordinal_width
; ///< The width of the ordinal number
61 uint text_width
; ///< The width of the actual text
62 int line_height
; ///< Height of the text lines
63 Dimension icon
; ///< Dimension of the company icon.
66 * (Re)Build the company league list
68 void BuildCompanyList()
70 if (!this->companies
.NeedRebuild()) return;
72 this->companies
.clear();
73 this->companies
.reserve(Company::GetNumItems());
75 for (const Company
*c
: Company::Iterate()) {
76 this->companies
.push_back(c
);
79 this->companies
.RebuildDone();
82 /** Sort the company league by performance history */
83 static bool PerformanceSorter(const Company
* const &c1
, const Company
* const &c2
)
85 return c2
->old_economy
[0].performance_history
< c1
->old_economy
[0].performance_history
;
89 PerformanceLeagueWindow(WindowDesc
&desc
, WindowNumber window_number
) : Window(desc
)
91 this->InitNested(window_number
);
92 this->companies
.ForceRebuild();
93 this->companies
.NeedResort();
96 void OnPaint() override
98 this->BuildCompanyList();
99 this->companies
.Sort(&PerformanceSorter
);
104 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
106 if (widget
!= WID_PLT_BACKGROUND
) return;
108 Rect ir
= r
.Shrink(WidgetDimensions::scaled
.framerect
);
109 int icon_y_offset
= (this->line_height
- this->icon
.height
) / 2;
110 int text_y_offset
= (this->line_height
- GetCharacterHeight(FS_NORMAL
)) / 2;
112 bool rtl
= _current_text_dir
== TD_RTL
;
113 Rect ordinal
= ir
.WithWidth(this->ordinal_width
, rtl
);
114 uint icon_left
= ir
.Indent(rtl
? this->text_width
: this->ordinal_width
, rtl
).left
;
115 Rect text
= ir
.WithWidth(this->text_width
, !rtl
);
117 for (uint i
= 0; i
!= this->companies
.size(); i
++) {
118 const Company
*c
= this->companies
[i
];
120 DrawString(ordinal
.left
, ordinal
.right
, ir
.top
+ text_y_offset
, STR_COMPANY_LEAGUE_COMPANY_RANK
);
122 DrawCompanyIcon(c
->index
, icon_left
, ir
.top
+ icon_y_offset
);
124 SetDParam(0, c
->index
);
125 SetDParam(1, c
->index
);
126 SetDParam(2, GetPerformanceTitleFromValue(c
->old_economy
[0].performance_history
));
127 DrawString(text
.left
, text
.right
, ir
.top
+ text_y_offset
, STR_COMPANY_LEAGUE_COMPANY_NAME
);
128 ir
.top
+= this->line_height
;
132 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
134 if (widget
!= WID_PLT_BACKGROUND
) return;
136 this->ordinal_width
= 0;
137 for (uint i
= 0; i
< MAX_COMPANIES
; i
++) {
139 this->ordinal_width
= std::max(this->ordinal_width
, GetStringBoundingBox(STR_COMPANY_LEAGUE_COMPANY_RANK
).width
);
141 this->ordinal_width
+= WidgetDimensions::scaled
.hsep_wide
; // Keep some extra spacing
143 uint widest_width
= 0;
144 StringID widest_title
= STR_NULL
;
145 for (auto title
: _performance_titles
) {
146 uint width
= GetStringBoundingBox(title
).width
;
147 if (width
> widest_width
) {
148 widest_title
= title
;
149 widest_width
= width
;
153 this->icon
= GetSpriteSize(SPR_COMPANY_ICON
);
154 this->line_height
= std::max
<int>(this->icon
.height
+ WidgetDimensions::scaled
.vsep_normal
, GetCharacterHeight(FS_NORMAL
));
156 for (const Company
*c
: Company::Iterate()) {
157 SetDParam(0, c
->index
);
158 SetDParam(1, c
->index
);
159 SetDParam(2, widest_title
);
160 widest_width
= std::max(widest_width
, GetStringBoundingBox(STR_COMPANY_LEAGUE_COMPANY_NAME
).width
);
163 this->text_width
= widest_width
+ WidgetDimensions::scaled
.hsep_indent
* 3; // Keep some extra spacing
165 size
.width
= WidgetDimensions::scaled
.framerect
.Horizontal() + this->ordinal_width
+ this->icon
.width
+ this->text_width
+ WidgetDimensions::scaled
.hsep_wide
;
166 size
.height
= this->line_height
* MAX_COMPANIES
+ WidgetDimensions::scaled
.framerect
.Vertical();
169 void OnGameTick() override
171 if (this->companies
.NeedResort()) {
177 * Some data on this window has become invalid.
178 * @param data Information about the changed data.
179 * @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.
181 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
184 /* This needs to be done in command-scope to enforce rebuilding before resorting invalid data */
185 this->companies
.ForceRebuild();
187 this->companies
.ForceResort();
192 static constexpr NWidgetPart _nested_performance_league_widgets
[] = {
193 NWidget(NWID_HORIZONTAL
),
194 NWidget(WWT_CLOSEBOX
, COLOUR_BROWN
),
195 NWidget(WWT_CAPTION
, COLOUR_BROWN
), SetDataTip(STR_COMPANY_LEAGUE_TABLE_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
196 NWidget(WWT_SHADEBOX
, COLOUR_BROWN
),
197 NWidget(WWT_STICKYBOX
, COLOUR_BROWN
),
199 NWidget(WWT_PANEL
, COLOUR_BROWN
, WID_PLT_BACKGROUND
), SetMinimalSize(400, 0), SetMinimalTextLines(15, WidgetDimensions::unscaled
.framerect
.Vertical()),
203 static WindowDesc
_performance_league_desc(
204 WDP_AUTO
, "performance_league", 0, 0,
205 WC_COMPANY_LEAGUE
, WC_NONE
,
207 _nested_performance_league_widgets
210 void ShowPerformanceLeagueTable()
212 AllocateWindowDescFront
<PerformanceLeagueWindow
>(_performance_league_desc
, 0);
215 static void HandleLinkClick(Link link
)
219 case LT_NONE
: return;
222 if (!IsValidTile(link
.target
)) return;
227 if (!Industry::IsValidID(link
.target
)) return;
228 xy
= Industry::Get(link
.target
)->location
.tile
;
232 if (!Town::IsValidID(link
.target
)) return;
233 xy
= Town::Get(link
.target
)->xy
;
237 ShowCompany((CompanyID
)link
.target
);
240 case LT_STORY_PAGE
: {
241 if (!StoryPage::IsValidID(link
.target
)) return;
242 CompanyID story_company
= StoryPage::Get(link
.target
)->company
;
243 ShowStoryBook(story_company
, link
.target
);
247 default: NOT_REACHED();
251 ShowExtraViewportWindow(xy
);
253 ScrollMainWindowToTile(xy
);
258 class ScriptLeagueWindow
: public Window
{
261 std::vector
<std::pair
<uint
, const LeagueTableElement
*>> rows
;
262 uint rank_width
; ///< The width of the rank ordinal
263 uint text_width
; ///< The width of the actual text
264 uint score_width
; ///< The width of the score text
265 uint header_height
; ///< Height of the table header
266 int line_height
; ///< Height of the text lines
267 Dimension icon_size
; ///< Dimenion of the company icon.
271 * Rebuild the company league list
276 this->title
= std::string
{};
277 auto lt
= LeagueTable::GetIfValid(this->table
);
278 if (lt
== nullptr) return;
280 /* We store title in the window class so we can safely reference the string later */
281 this->title
= lt
->title
;
283 std::vector
<const LeagueTableElement
*> elements
;
284 for (LeagueTableElement
*lte
: LeagueTableElement::Iterate()) {
285 if (lte
->table
== this->table
) {
286 elements
.push_back(lte
);
289 std::sort(elements
.begin(), elements
.end(), [](auto a
, auto b
) { return a
->rating
> b
->rating
; });
291 /* Calculate rank, companies with the same rating share the ranks */
293 for (uint i
= 0; i
!= elements
.size(); i
++) {
294 auto *lte
= elements
[i
];
295 if (i
> 0 && elements
[i
- 1]->rating
!= lte
->rating
) rank
= i
;
296 this->rows
.emplace_back(rank
, lte
);
301 ScriptLeagueWindow(WindowDesc
&desc
, LeagueTableID table
) : Window(desc
)
305 this->InitNested(table
);
308 void SetStringParameters(WidgetID widget
) const override
310 if (widget
!= WID_SLT_CAPTION
) return;
311 SetDParamStr(0, this->title
);
314 void OnPaint() override
319 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
321 if (widget
!= WID_SLT_BACKGROUND
) return;
323 auto lt
= LeagueTable::GetIfValid(this->table
);
324 if (lt
== nullptr) return;
326 Rect ir
= r
.Shrink(WidgetDimensions::scaled
.framerect
);
328 if (!lt
->header
.empty()) {
329 SetDParamStr(0, lt
->header
);
330 ir
.top
= DrawStringMultiLine(ir
.left
, ir
.right
, ir
.top
, UINT16_MAX
, STR_JUST_RAW_STRING
, TC_BLACK
) + WidgetDimensions::scaled
.vsep_wide
;
333 int icon_y_offset
= (this->line_height
- this->icon_size
.height
) / 2;
334 int text_y_offset
= (this->line_height
- GetCharacterHeight(FS_NORMAL
)) / 2;
336 /* Calculate positions.of the columns */
337 bool rtl
= _current_text_dir
== TD_RTL
;
338 int spacer
= WidgetDimensions::scaled
.hsep_wide
;
339 Rect rank_rect
= ir
.WithWidth(this->rank_width
, rtl
);
340 Rect icon_rect
= ir
.Indent(this->rank_width
+ (rtl
? 0 : spacer
), rtl
).WithWidth(this->icon_size
.width
, rtl
);
341 Rect text_rect
= ir
.Indent(this->rank_width
+ spacer
+ this->icon_size
.width
, rtl
).WithWidth(this->text_width
, rtl
);
342 Rect score_rect
= ir
.Indent(this->rank_width
+ 2 * spacer
+ this->icon_size
.width
+ this->text_width
, rtl
).WithWidth(this->score_width
, rtl
);
344 for (auto [rank
, lte
] : this->rows
) {
345 SetDParam(0, rank
+ 1);
346 DrawString(rank_rect
.left
, rank_rect
.right
, ir
.top
+ text_y_offset
, STR_COMPANY_LEAGUE_COMPANY_RANK
);
347 if (this->icon_size
.width
> 0 && lte
->company
!= INVALID_COMPANY
) DrawCompanyIcon(lte
->company
, icon_rect
.left
, ir
.top
+ icon_y_offset
);
348 SetDParamStr(0, lte
->text
);
349 DrawString(text_rect
.left
, text_rect
.right
, ir
.top
+ text_y_offset
, STR_JUST_RAW_STRING
, TC_BLACK
);
350 SetDParamStr(0, lte
->score
);
351 DrawString(score_rect
.left
, score_rect
.right
, ir
.top
+ text_y_offset
, STR_JUST_RAW_STRING
, TC_BLACK
, SA_RIGHT
);
352 ir
.top
+= this->line_height
;
355 if (!lt
->footer
.empty()) {
356 ir
.top
+= WidgetDimensions::scaled
.vsep_wide
;
357 SetDParamStr(0, lt
->footer
);
358 ir
.top
= DrawStringMultiLine(ir
.left
, ir
.right
, ir
.top
, UINT16_MAX
, STR_JUST_RAW_STRING
, TC_BLACK
);
362 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
364 if (widget
!= WID_SLT_BACKGROUND
) return;
366 auto lt
= LeagueTable::GetIfValid(this->table
);
367 if (lt
== nullptr) return;
369 this->icon_size
= GetSpriteSize(SPR_COMPANY_ICON
);
370 this->line_height
= std::max
<int>(this->icon_size
.height
+ WidgetDimensions::scaled
.fullbevel
.Vertical(), GetCharacterHeight(FS_NORMAL
));
372 /* Calculate maximum width of every column */
373 this->rank_width
= this->text_width
= this->score_width
= 0;
374 bool show_icon_column
= false;
375 for (auto [rank
, lte
] : this->rows
) {
376 SetDParam(0, rank
+ 1);
377 this->rank_width
= std::max(this->rank_width
, GetStringBoundingBox(STR_COMPANY_LEAGUE_COMPANY_RANK
).width
);
378 SetDParamStr(0, lte
->text
);
379 this->text_width
= std::max(this->text_width
, GetStringBoundingBox(STR_JUST_RAW_STRING
).width
);
380 SetDParamStr(0, lte
->score
);
381 this->score_width
= std::max(this->score_width
, GetStringBoundingBox(STR_JUST_RAW_STRING
).width
);
382 if (lte
->company
!= INVALID_COMPANY
) show_icon_column
= true;
385 if (!show_icon_column
) this->icon_size
.width
= 0;
386 else this->icon_size
.width
+= WidgetDimensions::scaled
.hsep_wide
;
388 size
.width
= this->rank_width
+ this->icon_size
.width
+ this->text_width
+ this->score_width
+ WidgetDimensions::scaled
.framerect
.Horizontal() + WidgetDimensions::scaled
.hsep_wide
* 2;
389 size
.height
= this->line_height
* std::max
<uint
>(3u, (unsigned)this->rows
.size()) + WidgetDimensions::scaled
.framerect
.Vertical();
391 if (!lt
->header
.empty()) {
392 SetDParamStr(0, lt
->header
);
393 this->header_height
= GetStringHeight(STR_JUST_RAW_STRING
, size
.width
- WidgetDimensions::scaled
.framerect
.Horizontal()) + WidgetDimensions::scaled
.vsep_wide
;
394 size
.height
+= header_height
;
395 } else this->header_height
= 0;
397 if (!lt
->footer
.empty()) {
398 SetDParamStr(0, lt
->footer
);
399 size
.height
+= GetStringHeight(STR_JUST_RAW_STRING
, size
.width
- WidgetDimensions::scaled
.framerect
.Horizontal()) + WidgetDimensions::scaled
.vsep_wide
;
403 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
405 if (widget
!= WID_SLT_BACKGROUND
) return;
407 auto *wid
= this->GetWidget
<NWidgetResizeBase
>(WID_SLT_BACKGROUND
);
408 int index
= (pt
.y
- WidgetDimensions::scaled
.framerect
.top
- wid
->pos_y
- this->header_height
) / this->line_height
;
409 if (index
>= 0 && (uint
)index
< this->rows
.size()) {
410 auto lte
= this->rows
[index
].second
;
411 HandleLinkClick(lte
->link
);
416 * Some data on this window has become invalid.
417 * @param data Information about the changed data.
418 * @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.
420 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
427 static constexpr NWidgetPart _nested_script_league_widgets
[] = {
428 NWidget(NWID_HORIZONTAL
),
429 NWidget(WWT_CLOSEBOX
, COLOUR_BROWN
),
430 NWidget(WWT_CAPTION
, COLOUR_BROWN
, WID_SLT_CAPTION
), SetDataTip(STR_JUST_RAW_STRING
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
431 NWidget(WWT_SHADEBOX
, COLOUR_BROWN
),
432 NWidget(WWT_STICKYBOX
, COLOUR_BROWN
),
434 NWidget(WWT_PANEL
, COLOUR_BROWN
, WID_SLT_BACKGROUND
), SetMinimalSize(400, 0), SetMinimalTextLines(15, WidgetDimensions::unscaled
.framerect
.Vertical()),
438 static WindowDesc
_script_league_desc(
439 WDP_AUTO
, "script_league", 0, 0,
440 WC_COMPANY_LEAGUE
, WC_NONE
,
442 _nested_script_league_widgets
445 void ShowScriptLeagueTable(LeagueTableID table
)
447 if (!LeagueTable::IsValidID(table
)) return;
448 AllocateWindowDescFront
<ScriptLeagueWindow
>(_script_league_desc
, table
);
451 void ShowFirstLeagueTable()
453 auto it
= LeagueTable::Iterate();
455 ShowScriptLeagueTable((*it
.begin())->index
);
457 ShowPerformanceLeagueTable();