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"
13 #include "company_base.h"
14 #include "company_gui.h"
17 #include "league_base.h"
18 #include "sortlist_type.h"
19 #include "story_base.h"
20 #include "strings_func.h"
23 #include "viewport_func.h"
24 #include "window_gui.h"
25 #include "widgets/league_widget.h"
26 #include "table/strings.h"
27 #include "table/sprites.h"
29 #include "safeguards.h"
32 static const StringID _performance_titles
[] = {
33 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_ENGINEER
,
34 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_ENGINEER
,
35 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_TRAFFIC_MANAGER
,
36 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_TRAFFIC_MANAGER
,
37 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_TRANSPORT_COORDINATOR
,
38 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_TRANSPORT_COORDINATOR
,
39 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_ROUTE_SUPERVISOR
,
40 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_ROUTE_SUPERVISOR
,
41 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_DIRECTOR
,
42 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_DIRECTOR
,
43 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_CHIEF_EXECUTIVE
,
44 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_CHIEF_EXECUTIVE
,
45 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_CHAIRMAN
,
46 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_CHAIRMAN
,
47 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_PRESIDENT
,
48 STR_COMPANY_LEAGUE_PERFORMANCE_TITLE_TYCOON
,
51 static inline StringID
GetPerformanceTitleFromValue(uint value
)
53 return _performance_titles
[std::min(value
, 1000u) >> 6];
56 class PerformanceLeagueWindow
: public Window
{
58 GUIList
<const Company
*> companies
;
59 uint ordinal_width
; ///< The width of the ordinal number
60 uint text_width
; ///< The width of the actual text
61 int line_height
; ///< Height of the text lines
62 Dimension icon
; ///< Dimension of the company icon.
65 * (Re)Build the company league list
67 void BuildCompanyList()
69 if (!this->companies
.NeedRebuild()) return;
71 this->companies
.clear();
73 for (const Company
*c
: Company::Iterate()) {
74 this->companies
.push_back(c
);
77 this->companies
.shrink_to_fit();
78 this->companies
.RebuildDone();
81 /** Sort the company league by performance history */
82 static bool PerformanceSorter(const Company
* const &c1
, const Company
* const &c2
)
84 return c2
->old_economy
[0].performance_history
< c1
->old_economy
[0].performance_history
;
88 PerformanceLeagueWindow(WindowDesc
*desc
, WindowNumber window_number
) : Window(desc
)
90 this->InitNested(window_number
);
91 this->companies
.ForceRebuild();
92 this->companies
.NeedResort();
95 void OnPaint() override
97 this->BuildCompanyList();
98 this->companies
.Sort(&PerformanceSorter
);
103 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
105 if (widget
!= WID_PLT_BACKGROUND
) return;
107 Rect ir
= r
.Shrink(WidgetDimensions::scaled
.framerect
);
108 int icon_y_offset
= (this->line_height
- this->icon
.height
) / 2;
109 int text_y_offset
= (this->line_height
- GetCharacterHeight(FS_NORMAL
)) / 2;
111 bool rtl
= _current_text_dir
== TD_RTL
;
112 Rect ordinal
= ir
.WithWidth(this->ordinal_width
, rtl
);
113 uint icon_left
= ir
.Indent(rtl
? this->text_width
: this->ordinal_width
, rtl
).left
;
114 Rect text
= ir
.WithWidth(this->text_width
, !rtl
);
116 for (uint i
= 0; i
!= this->companies
.size(); i
++) {
117 const Company
*c
= this->companies
[i
];
119 DrawString(ordinal
.left
, ordinal
.right
, ir
.top
+ text_y_offset
, STR_COMPANY_LEAGUE_COMPANY_RANK
);
121 DrawCompanyIcon(c
->index
, icon_left
, ir
.top
+ icon_y_offset
);
123 SetDParam(0, c
->index
);
124 SetDParam(1, c
->index
);
125 SetDParam(2, GetPerformanceTitleFromValue(c
->old_economy
[0].performance_history
));
126 DrawString(text
.left
, text
.right
, ir
.top
+ text_y_offset
, STR_COMPANY_LEAGUE_COMPANY_NAME
);
127 ir
.top
+= this->line_height
;
131 void UpdateWidgetSize(WidgetID widget
, Dimension
*size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
*fill
, [[maybe_unused
]] Dimension
*resize
) override
133 if (widget
!= WID_PLT_BACKGROUND
) return;
135 this->ordinal_width
= 0;
136 for (uint i
= 0; i
< MAX_COMPANIES
; i
++) {
138 this->ordinal_width
= std::max(this->ordinal_width
, GetStringBoundingBox(STR_COMPANY_LEAGUE_COMPANY_RANK
).width
);
140 this->ordinal_width
+= WidgetDimensions::scaled
.hsep_wide
; // Keep some extra spacing
142 uint widest_width
= 0;
143 uint widest_title
= 0;
144 for (uint i
= 0; i
< lengthof(_performance_titles
); i
++) {
145 uint width
= GetStringBoundingBox(_performance_titles
[i
]).width
;
146 if (width
> widest_width
) {
148 widest_width
= width
;
152 this->icon
= GetSpriteSize(SPR_COMPANY_ICON
);
153 this->line_height
= std::max
<int>(this->icon
.height
+ WidgetDimensions::scaled
.vsep_normal
, GetCharacterHeight(FS_NORMAL
));
155 for (const Company
*c
: Company::Iterate()) {
156 SetDParam(0, c
->index
);
157 SetDParam(1, c
->index
);
158 SetDParam(2, _performance_titles
[widest_title
]);
159 widest_width
= std::max(widest_width
, GetStringBoundingBox(STR_COMPANY_LEAGUE_COMPANY_NAME
).width
);
162 this->text_width
= widest_width
+ WidgetDimensions::scaled
.hsep_indent
* 3; // Keep some extra spacing
164 size
->width
= WidgetDimensions::scaled
.framerect
.Horizontal() + this->ordinal_width
+ this->icon
.width
+ this->text_width
+ WidgetDimensions::scaled
.hsep_wide
;
165 size
->height
= this->line_height
* MAX_COMPANIES
+ WidgetDimensions::scaled
.framerect
.Vertical();
168 void OnGameTick() override
170 if (this->companies
.NeedResort()) {
176 * Some data on this window has become invalid.
177 * @param data Information about the changed data.
178 * @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.
180 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
183 /* This needs to be done in command-scope to enforce rebuilding before resorting invalid data */
184 this->companies
.ForceRebuild();
186 this->companies
.ForceResort();
191 static constexpr NWidgetPart _nested_performance_league_widgets
[] = {
192 NWidget(NWID_HORIZONTAL
),
193 NWidget(WWT_CLOSEBOX
, COLOUR_BROWN
),
194 NWidget(WWT_CAPTION
, COLOUR_BROWN
), SetDataTip(STR_COMPANY_LEAGUE_TABLE_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
195 NWidget(WWT_SHADEBOX
, COLOUR_BROWN
),
196 NWidget(WWT_STICKYBOX
, COLOUR_BROWN
),
198 NWidget(WWT_PANEL
, COLOUR_BROWN
, WID_PLT_BACKGROUND
), SetMinimalSize(400, 0), SetMinimalTextLines(15, WidgetDimensions::unscaled
.framerect
.Vertical()),
202 static WindowDesc
_performance_league_desc(__FILE__
, __LINE__
,
203 WDP_AUTO
, "performance_league", 0, 0,
204 WC_COMPANY_LEAGUE
, WC_NONE
,
206 std::begin(_nested_performance_league_widgets
), std::end(_nested_performance_league_widgets
)
209 void ShowPerformanceLeagueTable()
211 AllocateWindowDescFront
<PerformanceLeagueWindow
>(&_performance_league_desc
, 0);
214 static void HandleLinkClick(Link link
)
218 case LT_NONE
: return;
221 if (!IsValidTile(link
.target
)) return;
226 if (!Industry::IsValidID(link
.target
)) return;
227 xy
= Industry::Get(link
.target
)->location
.tile
;
231 if (!Town::IsValidID(link
.target
)) return;
232 xy
= Town::Get(link
.target
)->xy
;
236 ShowCompany((CompanyID
)link
.target
);
239 case LT_STORY_PAGE
: {
240 if (!StoryPage::IsValidID(link
.target
)) return;
241 CompanyID story_company
= StoryPage::Get(link
.target
)->company
;
242 ShowStoryBook(story_company
, link
.target
);
246 default: NOT_REACHED();
250 ShowExtraViewportWindow(xy
);
252 ScrollMainWindowToTile(xy
);
257 class ScriptLeagueWindow
: public Window
{
260 std::vector
<std::pair
<uint
, const LeagueTableElement
*>> rows
;
261 uint rank_width
; ///< The width of the rank ordinal
262 uint text_width
; ///< The width of the actual text
263 uint score_width
; ///< The width of the score text
264 uint header_height
; ///< Height of the table header
265 int line_height
; ///< Height of the text lines
266 Dimension icon_size
; ///< Dimenion of the company icon.
270 * Rebuild the company league list
275 this->title
= std::string
{};
276 auto lt
= LeagueTable::GetIfValid(this->table
);
277 if (lt
== nullptr) return;
279 /* We store title in the window class so we can safely reference the string later */
280 this->title
= lt
->title
;
282 std::vector
<const LeagueTableElement
*> elements
;
283 for (LeagueTableElement
*lte
: LeagueTableElement::Iterate()) {
284 if (lte
->table
== this->table
) {
285 elements
.push_back(lte
);
288 std::sort(elements
.begin(), elements
.end(), [](auto a
, auto b
) { return a
->rating
> b
->rating
; });
290 /* Calculate rank, companies with the same rating share the ranks */
292 for (uint i
= 0; i
!= elements
.size(); i
++) {
293 auto *lte
= elements
[i
];
294 if (i
> 0 && elements
[i
- 1]->rating
!= lte
->rating
) rank
= i
;
295 this->rows
.emplace_back(std::make_pair(rank
, lte
));
300 ScriptLeagueWindow(WindowDesc
*desc
, LeagueTableID table
) : Window(desc
)
304 this->InitNested(table
);
307 void SetStringParameters(WidgetID widget
) const override
309 if (widget
!= WID_SLT_CAPTION
) return;
310 SetDParamStr(0, this->title
);
313 void OnPaint() override
318 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
320 if (widget
!= WID_SLT_BACKGROUND
) return;
322 auto lt
= LeagueTable::GetIfValid(this->table
);
323 if (lt
== nullptr) return;
325 Rect ir
= r
.Shrink(WidgetDimensions::scaled
.framerect
);
327 if (!lt
->header
.empty()) {
328 SetDParamStr(0, lt
->header
);
329 ir
.top
= DrawStringMultiLine(ir
.left
, ir
.right
, ir
.top
, UINT16_MAX
, STR_JUST_RAW_STRING
, TC_BLACK
) + WidgetDimensions::scaled
.vsep_wide
;
332 int icon_y_offset
= (this->line_height
- this->icon_size
.height
) / 2;
333 int text_y_offset
= (this->line_height
- GetCharacterHeight(FS_NORMAL
)) / 2;
335 /* Calculate positions.of the columns */
336 bool rtl
= _current_text_dir
== TD_RTL
;
337 int spacer
= WidgetDimensions::scaled
.hsep_wide
;
338 Rect rank_rect
= ir
.WithWidth(this->rank_width
, rtl
);
339 Rect icon_rect
= ir
.Indent(this->rank_width
+ (rtl
? 0 : spacer
), rtl
).WithWidth(this->icon_size
.width
, rtl
);
340 Rect text_rect
= ir
.Indent(this->rank_width
+ spacer
+ this->icon_size
.width
, rtl
).WithWidth(this->text_width
, rtl
);
341 Rect score_rect
= ir
.Indent(this->rank_width
+ 2 * spacer
+ this->icon_size
.width
+ this->text_width
, rtl
).WithWidth(this->score_width
, rtl
);
343 for (auto [rank
, lte
] : this->rows
) {
344 SetDParam(0, rank
+ 1);
345 DrawString(rank_rect
.left
, rank_rect
.right
, ir
.top
+ text_y_offset
, STR_COMPANY_LEAGUE_COMPANY_RANK
);
346 if (this->icon_size
.width
> 0 && lte
->company
!= INVALID_COMPANY
) DrawCompanyIcon(lte
->company
, icon_rect
.left
, ir
.top
+ icon_y_offset
);
347 SetDParamStr(0, lte
->text
);
348 DrawString(text_rect
.left
, text_rect
.right
, ir
.top
+ text_y_offset
, STR_JUST_RAW_STRING
, TC_BLACK
);
349 SetDParamStr(0, lte
->score
);
350 DrawString(score_rect
.left
, score_rect
.right
, ir
.top
+ text_y_offset
, STR_JUST_RAW_STRING
, TC_BLACK
, SA_RIGHT
);
351 ir
.top
+= this->line_height
;
354 if (!lt
->footer
.empty()) {
355 ir
.top
+= WidgetDimensions::scaled
.vsep_wide
;
356 SetDParamStr(0, lt
->footer
);
357 ir
.top
= DrawStringMultiLine(ir
.left
, ir
.right
, ir
.top
, UINT16_MAX
, STR_JUST_RAW_STRING
, TC_BLACK
);
361 void UpdateWidgetSize(WidgetID widget
, Dimension
*size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
*fill
, [[maybe_unused
]] Dimension
*resize
) override
363 if (widget
!= WID_SLT_BACKGROUND
) return;
365 auto lt
= LeagueTable::GetIfValid(this->table
);
366 if (lt
== nullptr) return;
368 this->icon_size
= GetSpriteSize(SPR_COMPANY_ICON
);
369 this->line_height
= std::max
<int>(this->icon_size
.height
+ WidgetDimensions::scaled
.fullbevel
.Vertical(), GetCharacterHeight(FS_NORMAL
));
371 /* Calculate maximum width of every column */
372 this->rank_width
= this->text_width
= this->score_width
= 0;
373 bool show_icon_column
= false;
374 for (auto [rank
, lte
] : this->rows
) {
375 SetDParam(0, rank
+ 1);
376 this->rank_width
= std::max(this->rank_width
, GetStringBoundingBox(STR_COMPANY_LEAGUE_COMPANY_RANK
).width
);
377 SetDParamStr(0, lte
->text
);
378 this->text_width
= std::max(this->text_width
, GetStringBoundingBox(STR_JUST_RAW_STRING
).width
);
379 SetDParamStr(0, lte
->score
);
380 this->score_width
= std::max(this->score_width
, GetStringBoundingBox(STR_JUST_RAW_STRING
).width
);
381 if (lte
->company
!= INVALID_COMPANY
) show_icon_column
= true;
384 if (!show_icon_column
) this->icon_size
.width
= 0;
385 else this->icon_size
.width
+= WidgetDimensions::scaled
.hsep_wide
;
387 size
->width
= this->rank_width
+ this->icon_size
.width
+ this->text_width
+ this->score_width
+ WidgetDimensions::scaled
.framerect
.Horizontal() + WidgetDimensions::scaled
.hsep_wide
* 2;
388 size
->height
= this->line_height
* std::max
<uint
>(3u, (unsigned)this->rows
.size()) + WidgetDimensions::scaled
.framerect
.Vertical();
390 if (!lt
->header
.empty()) {
391 SetDParamStr(0, lt
->header
);
392 this->header_height
= GetStringHeight(STR_JUST_RAW_STRING
, size
->width
- WidgetDimensions::scaled
.framerect
.Horizontal()) + WidgetDimensions::scaled
.vsep_wide
;
393 size
->height
+= header_height
;
394 } else this->header_height
= 0;
396 if (!lt
->footer
.empty()) {
397 SetDParamStr(0, lt
->footer
);
398 size
->height
+= GetStringHeight(STR_JUST_RAW_STRING
, size
->width
- WidgetDimensions::scaled
.framerect
.Horizontal()) + WidgetDimensions::scaled
.vsep_wide
;
402 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
404 if (widget
!= WID_SLT_BACKGROUND
) return;
406 auto *wid
= this->GetWidget
<NWidgetResizeBase
>(WID_SLT_BACKGROUND
);
407 int index
= (pt
.y
- WidgetDimensions::scaled
.framerect
.top
- wid
->pos_y
- this->header_height
) / this->line_height
;
408 if (index
>= 0 && (uint
)index
< this->rows
.size()) {
409 auto lte
= this->rows
[index
].second
;
410 HandleLinkClick(lte
->link
);
415 * Some data on this window has become invalid.
416 * @param data Information about the changed data.
417 * @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.
419 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
426 static constexpr NWidgetPart _nested_script_league_widgets
[] = {
427 NWidget(NWID_HORIZONTAL
),
428 NWidget(WWT_CLOSEBOX
, COLOUR_BROWN
),
429 NWidget(WWT_CAPTION
, COLOUR_BROWN
, WID_SLT_CAPTION
), SetDataTip(STR_JUST_RAW_STRING
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
430 NWidget(WWT_SHADEBOX
, COLOUR_BROWN
),
431 NWidget(WWT_STICKYBOX
, COLOUR_BROWN
),
433 NWidget(WWT_PANEL
, COLOUR_BROWN
, WID_SLT_BACKGROUND
), SetMinimalSize(400, 0), SetMinimalTextLines(15, WidgetDimensions::unscaled
.framerect
.Vertical()),
437 static WindowDesc
_script_league_desc(__FILE__
, __LINE__
,
438 WDP_AUTO
, "script_league", 0, 0,
439 WC_COMPANY_LEAGUE
, WC_NONE
,
441 std::begin(_nested_script_league_widgets
), std::end(_nested_script_league_widgets
)
444 void ShowScriptLeagueTable(LeagueTableID table
)
446 if (!LeagueTable::IsValidID(table
)) return;
447 AllocateWindowDescFront
<ScriptLeagueWindow
>(&_script_league_desc
, table
);
450 void ShowFirstLeagueTable()
452 auto it
= LeagueTable::Iterate();
454 ShowScriptLeagueTable((*it
.begin())->index
);
456 ShowPerformanceLeagueTable();