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 ai_gui.cpp %Window for configuring the AIs */
10 #include "../stdafx.h"
12 #include "../company_base.h"
13 #include "../window_func.h"
14 #include "../network/network.h"
15 #include "../settings_func.h"
16 #include "../network/network_content.h"
17 #include "../core/geometry_func.hpp"
21 #include "ai_config.hpp"
22 #include "ai_info.hpp"
23 #include "../script/script_gui.h"
24 #include "table/strings.h"
26 #include "../safeguards.h"
29 /** Widgets for the configure AI window. */
30 static constexpr NWidgetPart _nested_ai_config_widgets
[] = {
31 NWidget(NWID_HORIZONTAL
),
32 NWidget(WWT_CLOSEBOX
, COLOUR_MAUVE
),
33 NWidget(WWT_CAPTION
, COLOUR_MAUVE
), SetDataTip(STR_AI_CONFIG_CAPTION_AI
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
35 NWidget(WWT_PANEL
, COLOUR_MAUVE
, WID_AIC_BACKGROUND
),
36 NWidget(NWID_VERTICAL
), SetPIP(0, WidgetDimensions::unscaled
.vsep_wide
, 0), SetPadding(WidgetDimensions::unscaled
.sparse
),
37 NWidget(NWID_VERTICAL
), SetPIP(0, WidgetDimensions::unscaled
.vsep_sparse
, 0),
38 NWidget(NWID_HORIZONTAL
), SetPIP(0, WidgetDimensions::unscaled
.hsep_wide
, 0),
39 NWidget(NWID_HORIZONTAL
, NC_EQUALSIZE
),
40 NWidget(WWT_PUSHARROWBTN
, COLOUR_YELLOW
, WID_AIC_DECREASE_NUMBER
), SetDataTip(AWV_DECREASE
, STR_NULL
),
41 NWidget(WWT_PUSHARROWBTN
, COLOUR_YELLOW
, WID_AIC_INCREASE_NUMBER
), SetDataTip(AWV_INCREASE
, STR_NULL
),
43 NWidget(WWT_TEXT
, COLOUR_MAUVE
, WID_AIC_NUMBER
), SetDataTip(STR_AI_CONFIG_MAX_COMPETITORS
, STR_NULL
), SetFill(1, 0),
45 NWidget(NWID_HORIZONTAL
), SetPIP(0, WidgetDimensions::unscaled
.hsep_wide
, 0),
46 NWidget(NWID_HORIZONTAL
, NC_EQUALSIZE
),
47 NWidget(WWT_PUSHARROWBTN
, COLOUR_YELLOW
, WID_AIC_DECREASE_INTERVAL
), SetDataTip(AWV_DECREASE
, STR_NULL
),
48 NWidget(WWT_PUSHARROWBTN
, COLOUR_YELLOW
, WID_AIC_INCREASE_INTERVAL
), SetDataTip(AWV_INCREASE
, STR_NULL
),
50 NWidget(WWT_TEXT
, COLOUR_MAUVE
, WID_AIC_INTERVAL
), SetDataTip(STR_AI_CONFIG_COMPETITORS_INTERVAL
, STR_NULL
), SetFill(1, 0),
52 NWidget(NWID_HORIZONTAL
, NC_EQUALSIZE
),
53 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_AIC_MOVE_UP
), SetResize(1, 0), SetFill(1, 0), SetDataTip(STR_AI_CONFIG_MOVE_UP
, STR_AI_CONFIG_MOVE_UP_TOOLTIP
),
54 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_AIC_MOVE_DOWN
), SetResize(1, 0), SetFill(1, 0), SetDataTip(STR_AI_CONFIG_MOVE_DOWN
, STR_AI_CONFIG_MOVE_DOWN_TOOLTIP
),
57 NWidget(WWT_FRAME
, COLOUR_MAUVE
), SetDataTip(STR_AI_CONFIG_AI
, STR_NULL
), SetPIP(0, WidgetDimensions::unscaled
.vsep_sparse
, 0),
58 NWidget(NWID_HORIZONTAL
),
59 NWidget(WWT_MATRIX
, COLOUR_MAUVE
, WID_AIC_LIST
), SetMinimalSize(288, 112), SetFill(1, 0), SetMatrixDataTip(1, 8, STR_AI_CONFIG_AILIST_TOOLTIP
), SetScrollbar(WID_AIC_SCROLLBAR
),
60 NWidget(NWID_VSCROLLBAR
, COLOUR_MAUVE
, WID_AIC_SCROLLBAR
),
62 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_AIC_CONFIGURE
), SetFill(1, 0), SetDataTip(STR_AI_CONFIG_CONFIGURE
, STR_AI_CONFIG_CONFIGURE_TOOLTIP
),
64 NWidget(NWID_HORIZONTAL
), SetPIP(0, WidgetDimensions::unscaled
.hsep_wide
, 0),
65 NWidget(NWID_VERTICAL
, NC_EQUALSIZE
),
66 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_AIC_CHANGE
), SetFill(1, 1), SetDataTip(STR_AI_CONFIG_CHANGE_AI
, STR_AI_CONFIG_CHANGE_TOOLTIP
),
67 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_AIC_CONTENT_DOWNLOAD
), SetFill(1, 1), SetDataTip(STR_INTRO_ONLINE_CONTENT
, STR_INTRO_TOOLTIP_ONLINE_CONTENT
),
69 NWidget(NWID_VERTICAL
, NC_EQUALSIZE
),
70 NWidget(NWID_HORIZONTAL
, NC_EQUALSIZE
),
71 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_AIC_OPEN_URL
), SetResize(1, 0), SetFill(1, 0), SetDataTip(STR_CONTENT_OPEN_URL
, STR_CONTENT_OPEN_URL_TOOLTIP
),
72 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_AIC_TEXTFILE
+ TFT_README
), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_TEXTFILE_VIEW_README
, STR_TEXTFILE_VIEW_README_TOOLTIP
),
74 NWidget(NWID_HORIZONTAL
, NC_EQUALSIZE
),
75 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_AIC_TEXTFILE
+ TFT_CHANGELOG
), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_TEXTFILE_VIEW_CHANGELOG
, STR_TEXTFILE_VIEW_CHANGELOG_TOOLTIP
),
76 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_AIC_TEXTFILE
+ TFT_LICENSE
), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_TEXTFILE_VIEW_LICENCE
, STR_TEXTFILE_VIEW_LICENCE_TOOLTIP
),
84 /** Window definition for the configure AI window. */
85 static WindowDesc
_ai_config_desc(
86 WDP_CENTER
, nullptr, 0, 0,
87 WC_GAME_OPTIONS
, WC_NONE
,
89 _nested_ai_config_widgets
93 * Window to configure which AIs will start.
95 struct AIConfigWindow
: public Window
{
96 CompanyID selected_slot
; ///< The currently selected AI slot or \c INVALID_COMPANY.
97 int line_height
; ///< Height of a single AI-name line.
98 Scrollbar
*vscroll
; ///< Cache of the vertical scrollbar.
100 AIConfigWindow() : Window(_ai_config_desc
)
102 this->InitNested(WN_GAME_OPTIONS_AI
); // Initializes 'this->line_height' as a side effect.
103 this->vscroll
= this->GetScrollbar(WID_AIC_SCROLLBAR
);
104 this->selected_slot
= INVALID_COMPANY
;
105 NWidgetCore
*nwi
= this->GetWidget
<NWidgetCore
>(WID_AIC_LIST
);
106 this->vscroll
->SetCapacity(nwi
->current_y
/ this->line_height
);
107 this->vscroll
->SetCount(MAX_COMPANIES
);
108 this->OnInvalidateData(0);
111 void Close([[maybe_unused
]] int data
= 0) override
113 CloseWindowByClass(WC_SCRIPT_LIST
);
114 CloseWindowByClass(WC_SCRIPT_SETTINGS
);
115 this->Window::Close();
118 void SetStringParameters(WidgetID widget
) const override
122 SetDParam(0, GetGameSettings().difficulty
.max_no_competitors
);
125 case WID_AIC_INTERVAL
:
126 SetDParam(0, GetGameSettings().difficulty
.competitors_interval
);
131 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
134 case WID_AIC_DECREASE_NUMBER
:
135 case WID_AIC_INCREASE_NUMBER
:
136 case WID_AIC_DECREASE_INTERVAL
:
137 case WID_AIC_INCREASE_INTERVAL
:
138 size
= maxdim(size
, NWidgetScrollbar::GetHorizontalDimension());
142 this->line_height
= GetCharacterHeight(FS_NORMAL
) + padding
.height
;
143 resize
.height
= this->line_height
;
144 size
.height
= 8 * this->line_height
;
150 * Can the AI config in the given company slot be edited?
151 * @param slot The slot to query.
152 * @return True if and only if the given AI Config slot can be edited.
154 static bool IsEditable(CompanyID slot
)
156 if (_game_mode
!= GM_NORMAL
) {
157 return slot
> 0 && slot
< MAX_COMPANIES
;
159 return slot
< MAX_COMPANIES
&& !Company::IsValidID(slot
);
162 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
166 Rect tr
= r
.Shrink(WidgetDimensions::scaled
.matrix
);
167 int max_slot
= GetGameSettings().difficulty
.max_no_competitors
;
168 if (_game_mode
== GM_NORMAL
) {
169 for (const Company
*c
: Company::Iterate()) {
170 if (c
->is_ai
) max_slot
--;
172 for (CompanyID cid
= COMPANY_FIRST
; cid
< (CompanyID
)max_slot
&& cid
< MAX_COMPANIES
; cid
++) {
173 if (Company::IsValidID(cid
)) max_slot
++;
176 max_slot
++; // Slot 0 is human
178 for (int i
= this->vscroll
->GetPosition(); this->vscroll
->IsVisible(i
) && i
< MAX_COMPANIES
; i
++) {
181 if ((_game_mode
!= GM_NORMAL
&& i
== 0) || (_game_mode
== GM_NORMAL
&& Company::IsValidHumanID(i
))) {
182 text
= STR_AI_CONFIG_HUMAN_PLAYER
;
183 } else if (AIConfig::GetConfig((CompanyID
)i
)->GetInfo() != nullptr) {
184 SetDParamStr(0, AIConfig::GetConfig((CompanyID
)i
)->GetInfo()->GetName());
185 text
= STR_JUST_RAW_STRING
;
187 text
= STR_AI_CONFIG_RANDOM_AI
;
190 TextColour tc
= TC_SILVER
;
191 if (this->selected_slot
== i
) {
193 } else if (IsEditable((CompanyID
)i
)) {
194 if (i
< max_slot
) tc
= TC_ORANGE
;
195 } else if (Company::IsValidAiID(i
)) {
198 DrawString(tr
, text
, tc
);
199 tr
.top
+= this->line_height
;
206 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
208 if (widget
>= WID_AIC_TEXTFILE
&& widget
< WID_AIC_TEXTFILE
+ TFT_CONTENT_END
) {
209 if (this->selected_slot
== INVALID_COMPANY
|| AIConfig::GetConfig(this->selected_slot
) == nullptr) return;
211 ShowScriptTextfileWindow((TextfileType
)(widget
- WID_AIC_TEXTFILE
), this->selected_slot
);
216 case WID_AIC_DECREASE_NUMBER
:
217 case WID_AIC_INCREASE_NUMBER
: {
219 if (widget
== WID_AIC_DECREASE_NUMBER
) {
220 new_value
= std::max(0, GetGameSettings().difficulty
.max_no_competitors
- 1);
222 new_value
= std::min(MAX_COMPANIES
- 1, GetGameSettings().difficulty
.max_no_competitors
+ 1);
224 IConsoleSetSetting("difficulty.max_no_competitors", new_value
);
225 this->InvalidateData();
229 case WID_AIC_DECREASE_INTERVAL
:
230 case WID_AIC_INCREASE_INTERVAL
: {
232 if (widget
== WID_AIC_DECREASE_INTERVAL
) {
233 new_value
= std::max(static_cast<int>(MIN_COMPETITORS_INTERVAL
), GetGameSettings().difficulty
.competitors_interval
- 1);
235 new_value
= std::min(static_cast<int>(MAX_COMPETITORS_INTERVAL
), GetGameSettings().difficulty
.competitors_interval
+ 1);
237 IConsoleSetSetting("difficulty.competitors_interval", new_value
);
238 this->InvalidateData();
242 case WID_AIC_LIST
: { // Select a slot
243 this->selected_slot
= (CompanyID
)this->vscroll
->GetScrolledRowFromWidget(pt
.y
, this, widget
);
244 this->InvalidateData();
245 if (click_count
> 1 && IsEditable(this->selected_slot
)) ShowScriptListWindow((CompanyID
)this->selected_slot
, _ctrl_pressed
);
249 case WID_AIC_MOVE_UP
:
250 if (IsEditable(this->selected_slot
) && IsEditable((CompanyID
)(this->selected_slot
- 1))) {
251 Swap(GetGameSettings().ai_config
[this->selected_slot
], GetGameSettings().ai_config
[this->selected_slot
- 1]);
252 this->selected_slot
--;
253 this->vscroll
->ScrollTowards(this->selected_slot
);
254 this->InvalidateData();
258 case WID_AIC_MOVE_DOWN
:
259 if (IsEditable(this->selected_slot
) && IsEditable((CompanyID
)(this->selected_slot
+ 1))) {
260 Swap(GetGameSettings().ai_config
[this->selected_slot
], GetGameSettings().ai_config
[this->selected_slot
+ 1]);
261 this->selected_slot
++;
262 this->vscroll
->ScrollTowards(this->selected_slot
);
263 this->InvalidateData();
267 case WID_AIC_OPEN_URL
: {
268 const AIConfig
*config
= AIConfig::GetConfig(this->selected_slot
);
269 if (this->selected_slot
== INVALID_COMPANY
|| config
== nullptr || config
->GetInfo() == nullptr) return;
270 OpenBrowser(config
->GetInfo()->GetURL());
274 case WID_AIC_CHANGE
: // choose other AI
275 if (IsEditable(this->selected_slot
)) ShowScriptListWindow((CompanyID
)this->selected_slot
, _ctrl_pressed
);
278 case WID_AIC_CONFIGURE
: // change the settings for an AI
279 ShowScriptSettingsWindow((CompanyID
)this->selected_slot
);
282 case WID_AIC_CONTENT_DOWNLOAD
:
283 if (!_network_available
) {
284 ShowErrorMessage(STR_NETWORK_ERROR_NOTAVAILABLE
, INVALID_STRING_ID
, WL_ERROR
);
286 ShowNetworkContentListWindow(nullptr, CONTENT_TYPE_AI
);
293 * Some data on this window has become invalid.
294 * @param data Information about the changed data.
295 * @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.
297 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
299 if (!IsEditable(this->selected_slot
) && !Company::IsValidAiID(this->selected_slot
)) {
300 this->selected_slot
= INVALID_COMPANY
;
303 if (!gui_scope
) return;
305 AIConfig
*config
= this->selected_slot
== INVALID_COMPANY
? nullptr : AIConfig::GetConfig(this->selected_slot
);
307 this->SetWidgetDisabledState(WID_AIC_DECREASE_NUMBER
, GetGameSettings().difficulty
.max_no_competitors
== 0);
308 this->SetWidgetDisabledState(WID_AIC_INCREASE_NUMBER
, GetGameSettings().difficulty
.max_no_competitors
== MAX_COMPANIES
- 1);
309 this->SetWidgetDisabledState(WID_AIC_DECREASE_INTERVAL
, GetGameSettings().difficulty
.competitors_interval
== MIN_COMPETITORS_INTERVAL
);
310 this->SetWidgetDisabledState(WID_AIC_INCREASE_INTERVAL
, GetGameSettings().difficulty
.competitors_interval
== MAX_COMPETITORS_INTERVAL
);
311 this->SetWidgetDisabledState(WID_AIC_CHANGE
, !IsEditable(this->selected_slot
));
312 this->SetWidgetDisabledState(WID_AIC_CONFIGURE
, this->selected_slot
== INVALID_COMPANY
|| config
->GetConfigList()->empty());
313 this->SetWidgetDisabledState(WID_AIC_MOVE_UP
, !IsEditable(this->selected_slot
) || !IsEditable((CompanyID
)(this->selected_slot
- 1)));
314 this->SetWidgetDisabledState(WID_AIC_MOVE_DOWN
, !IsEditable(this->selected_slot
) || !IsEditable((CompanyID
)(this->selected_slot
+ 1)));
316 this->SetWidgetDisabledState(WID_AIC_OPEN_URL
, this->selected_slot
== INVALID_COMPANY
|| config
->GetInfo() == nullptr || config
->GetInfo()->GetURL().empty());
317 for (TextfileType tft
= TFT_CONTENT_BEGIN
; tft
< TFT_CONTENT_END
; tft
++) {
318 this->SetWidgetDisabledState(WID_AIC_TEXTFILE
+ tft
, this->selected_slot
== INVALID_COMPANY
|| !config
->GetTextfile(tft
, this->selected_slot
).has_value());
323 /** Open the AI config window. */
324 void ShowAIConfigWindow()
326 CloseWindowByClass(WC_GAME_OPTIONS
);
327 new AIConfigWindow();