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 game_gui.cpp %Window for configuring the Game Script */
10 #include "../stdafx.h"
12 #include "../settings_gui.h"
13 #include "../querystring_gui.h"
14 #include "../window_func.h"
15 #include "../network/network.h"
16 #include "../network/network_content.h"
17 #include "../dropdown_type.h"
18 #include "../dropdown_func.h"
19 #include "../timer/timer.h"
20 #include "../timer/timer_window.h"
23 #include "game_gui.hpp"
24 #include "game_config.hpp"
25 #include "game_info.hpp"
26 #include "../script/script_gui.h"
27 #include "../script_config.hpp"
28 #include "../table/strings.h"
30 #include "../safeguards.h"
33 /** Widgets for the configure GS window. */
34 static constexpr NWidgetPart _nested_gs_config_widgets
[] = {
35 NWidget(NWID_HORIZONTAL
),
36 NWidget(WWT_CLOSEBOX
, COLOUR_MAUVE
),
37 NWidget(WWT_CAPTION
, COLOUR_MAUVE
), SetDataTip(STR_AI_CONFIG_CAPTION_GAMESCRIPT
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
38 NWidget(WWT_DEFSIZEBOX
, COLOUR_MAUVE
),
40 NWidget(WWT_PANEL
, COLOUR_MAUVE
, WID_GSC_BACKGROUND
),
41 NWidget(NWID_VERTICAL
), SetPIP(0, WidgetDimensions::unscaled
.vsep_wide
, 0), SetPadding(WidgetDimensions::unscaled
.sparse_resize
),
42 NWidget(WWT_FRAME
, COLOUR_MAUVE
), SetDataTip(STR_AI_CONFIG_GAMESCRIPT
, STR_NULL
), SetFill(1, 0), SetResize(1, 0),
43 NWidget(WWT_MATRIX
, COLOUR_MAUVE
, WID_GSC_GSLIST
), SetMinimalSize(288, 14), SetFill(1, 1), SetResize(1, 0), SetMatrixDataTip(1, 1, STR_AI_CONFIG_GAMELIST_TOOLTIP
),
45 NWidget(WWT_FRAME
, COLOUR_MAUVE
), SetDataTip(STR_AI_CONFIG_GAMESCRIPT_PARAM
, STR_NULL
), SetFill(1, 1), SetResize(1, 0), SetPIP(0, WidgetDimensions::unscaled
.vsep_sparse
, 0),
46 NWidget(NWID_HORIZONTAL
),
47 NWidget(WWT_MATRIX
, COLOUR_MAUVE
, WID_GSC_SETTINGS
), SetFill(1, 0), SetResize(1, 1), SetMinimalSize(188, 182), SetMatrixDataTip(1, 0, STR_NULL
), SetScrollbar(WID_GSC_SCROLLBAR
),
48 NWidget(NWID_VSCROLLBAR
, COLOUR_MAUVE
, WID_GSC_SCROLLBAR
),
50 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_GSC_RESET
), SetFill(1, 0), SetResize(1, 0), SetDataTip(STR_AI_SETTINGS_RESET
, STR_NULL
),
52 NWidget(NWID_HORIZONTAL
), SetPIP(0, WidgetDimensions::unscaled
.hsep_wide
, 0),
53 NWidget(NWID_VERTICAL
, NC_EQUALSIZE
),
54 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_GSC_CHANGE
), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_AI_CONFIG_CHANGE_GAMESCRIPT
, STR_AI_CONFIG_CHANGE_TOOLTIP
),
55 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_GSC_CONTENT_DOWNLOAD
), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_INTRO_ONLINE_CONTENT
, STR_INTRO_TOOLTIP_ONLINE_CONTENT
),
57 NWidget(NWID_VERTICAL
, NC_EQUALSIZE
),
58 NWidget(NWID_HORIZONTAL
, NC_EQUALSIZE
),
59 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_GSC_OPEN_URL
), SetResize(1, 0), SetFill(1, 0), SetDataTip(STR_CONTENT_OPEN_URL
, STR_CONTENT_OPEN_URL_TOOLTIP
),
60 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_GSC_TEXTFILE
+ TFT_README
), SetFill(1, 1), SetResize(1, 0), SetMinimalSize(93, 0), SetDataTip(STR_TEXTFILE_VIEW_README
, STR_TEXTFILE_VIEW_README_TOOLTIP
),
62 NWidget(NWID_HORIZONTAL
, NC_EQUALSIZE
),
63 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_GSC_TEXTFILE
+ TFT_CHANGELOG
), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_TEXTFILE_VIEW_CHANGELOG
, STR_TEXTFILE_VIEW_CHANGELOG_TOOLTIP
),
64 NWidget(WWT_PUSHTXTBTN
, COLOUR_YELLOW
, WID_GSC_TEXTFILE
+ TFT_LICENSE
), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_TEXTFILE_VIEW_LICENCE
, STR_TEXTFILE_VIEW_LICENCE_TOOLTIP
),
69 NWidget(NWID_HORIZONTAL
),
70 NWidget(NWID_SPACER
), SetFill(1, 0), SetResize(1, 0),
71 NWidget(WWT_RESIZEBOX
, COLOUR_MAUVE
), SetDataTip(RWV_HIDE_BEVEL
, STR_TOOLTIP_RESIZE
),
76 /** Window definition for the configure GS window. */
77 static WindowDesc
_gs_config_desc(
78 WDP_CENTER
, "settings_gs_config", 500, 350,
79 WC_GAME_OPTIONS
, WC_NONE
,
81 _nested_gs_config_widgets
85 * Window to configure which GSs will start.
87 struct GSConfigWindow
: public Window
{
88 ScriptConfig
*gs_config
; ///< The configuration we're modifying.
89 int line_height
; ///< Height of a single GS-name line.
90 int clicked_button
; ///< The button we clicked.
91 bool clicked_increase
; ///< Whether we clicked the increase or decrease button.
92 bool clicked_dropdown
; ///< Whether the dropdown is open.
93 bool closing_dropdown
; ///< True, if the dropdown list is currently closing.
94 int clicked_row
; ///< The clicked row of settings.
95 Scrollbar
*vscroll
; ///< Cache of the vertical scrollbar.
96 typedef std::vector
<const ScriptConfigItem
*> VisibleSettingsList
; ///< typdef for a vector of script settings
97 VisibleSettingsList visible_settings
; ///< List of visible GS settings
99 GSConfigWindow() : Window(_gs_config_desc
),
101 clicked_dropdown(false),
102 closing_dropdown(false)
104 this->gs_config
= GameConfig::GetConfig();
106 this->CreateNestedTree(); // Initializes 'this->line_height' as a side effect.
107 this->vscroll
= this->GetScrollbar(WID_GSC_SCROLLBAR
);
108 this->FinishInitNested(WN_GAME_OPTIONS_GS
);
109 this->OnInvalidateData(0);
111 this->RebuildVisibleSettings();
114 void Close([[maybe_unused
]] int data
= 0) override
116 CloseWindowByClass(WC_SCRIPT_LIST
);
117 this->Window::Close();
121 * Rebuilds the list of visible settings. GS settings with the flag
122 * GSCONFIG_GS_DEVELOPER set will only be visible if the game setting
123 * gui.ai_developer_tools is enabled.
125 void RebuildVisibleSettings()
127 visible_settings
.clear();
129 for (const auto &item
: *this->gs_config
->GetConfigList()) {
130 bool no_hide
= (item
.flags
& SCRIPTCONFIG_DEVELOPER
) == 0;
131 if (no_hide
|| _settings_client
.gui
.ai_developer_tools
) {
132 visible_settings
.push_back(&item
);
136 this->vscroll
->SetCount(this->visible_settings
.size());
139 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
142 case WID_GSC_SETTINGS
:
143 this->line_height
= std::max(SETTING_BUTTON_HEIGHT
, GetCharacterHeight(FS_NORMAL
)) + padding
.height
;
145 resize
.height
= this->line_height
;
146 size
.height
= 5 * this->line_height
;
150 this->line_height
= GetCharacterHeight(FS_NORMAL
) + padding
.height
;
151 size
.height
= 1 * this->line_height
;
157 * Can the GS config be edited?
158 * @return True if the given GS Config slot can be edited, otherwise false.
160 static bool IsEditable()
162 return _game_mode
!= GM_NORMAL
|| Game::GetInstance() != nullptr;
165 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
168 case WID_GSC_GSLIST
: {
169 StringID text
= STR_AI_CONFIG_NONE
;
171 if (GameConfig::GetConfig()->GetInfo() != nullptr) {
172 SetDParamStr(0, GameConfig::GetConfig()->GetInfo()->GetName());
173 text
= STR_JUST_RAW_STRING
;
176 /* There is only one slot, unlike with the GS GUI, so it should never be white */
177 DrawString(r
.Shrink(WidgetDimensions::scaled
.matrix
), text
, (IsEditable() ? TC_ORANGE
: TC_SILVER
));
180 case WID_GSC_SETTINGS
: {
181 Rect ir
= r
.Shrink(WidgetDimensions::scaled
.framerect
);
182 bool rtl
= _current_text_dir
== TD_RTL
;
183 Rect br
= ir
.WithWidth(SETTING_BUTTON_WIDTH
, rtl
);
184 Rect tr
= ir
.Indent(SETTING_BUTTON_WIDTH
+ WidgetDimensions::scaled
.hsep_wide
, rtl
);
187 int button_y_offset
= (this->line_height
- SETTING_BUTTON_HEIGHT
) / 2;
188 int text_y_offset
= (this->line_height
- GetCharacterHeight(FS_NORMAL
)) / 2;
190 const auto [first
, last
] = this->vscroll
->GetVisibleRangeIterators(this->visible_settings
);
191 for (auto it
= first
; it
!= last
; ++it
) {
192 const ScriptConfigItem
&config_item
= **it
;
193 int current_value
= this->gs_config
->GetSetting(config_item
.name
);
194 bool editable
= this->IsEditableItem(config_item
);
199 if (config_item
.description
.empty()) {
200 str
= STR_JUST_STRING1
;
203 str
= STR_AI_SETTINGS_SETTING
;
204 colour
= TC_LIGHT_BLUE
;
205 SetDParamStr(idx
++, config_item
.description
);
208 if ((config_item
.flags
& SCRIPTCONFIG_BOOLEAN
) != 0) {
209 DrawBoolButton(br
.left
, y
+ button_y_offset
, current_value
!= 0, editable
);
210 SetDParam(idx
++, current_value
== 0 ? STR_CONFIG_SETTING_OFF
: STR_CONFIG_SETTING_ON
);
212 int i
= static_cast<int>(std::distance(std::begin(this->visible_settings
), it
));
213 if (config_item
.complete_labels
) {
214 DrawDropDownButton(br
.left
, y
+ button_y_offset
, COLOUR_YELLOW
, this->clicked_row
== i
&& clicked_dropdown
, editable
);
216 DrawArrowButtons(br
.left
, y
+ button_y_offset
, COLOUR_YELLOW
, (this->clicked_button
== i
) ? 1 + (this->clicked_increase
!= rtl
) : 0, editable
&& current_value
> config_item
.min_value
, editable
&& current_value
< config_item
.max_value
);
219 auto config_iterator
= config_item
.labels
.find(current_value
);
220 if (config_iterator
!= config_item
.labels
.end()) {
221 SetDParam(idx
++, STR_JUST_RAW_STRING
);
222 SetDParamStr(idx
++, config_iterator
->second
);
224 SetDParam(idx
++, STR_JUST_INT
);
225 SetDParam(idx
++, current_value
);
229 DrawString(tr
.left
, tr
.right
, y
+ text_y_offset
, str
, colour
);
230 y
+= this->line_height
;
237 void OnPaint() override
239 if (this->closing_dropdown
) {
240 this->closing_dropdown
= false;
241 this->clicked_dropdown
= false;
246 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
248 if (widget
>= WID_GSC_TEXTFILE
&& widget
< WID_GSC_TEXTFILE
+ TFT_CONTENT_END
) {
249 if (GameConfig::GetConfig() == nullptr) return;
251 ShowScriptTextfileWindow((TextfileType
)(widget
- WID_GSC_TEXTFILE
), (CompanyID
)OWNER_DEITY
);
256 case WID_GSC_GSLIST
: {
257 this->InvalidateData();
258 if (click_count
> 1 && _game_mode
!= GM_NORMAL
) ShowScriptListWindow((CompanyID
)OWNER_DEITY
, _ctrl_pressed
);
262 case WID_GSC_CHANGE
: // choose other Game Script
263 ShowScriptListWindow((CompanyID
)OWNER_DEITY
, _ctrl_pressed
);
266 case WID_GSC_CONTENT_DOWNLOAD
:
267 if (!_network_available
) {
268 ShowErrorMessage(STR_NETWORK_ERROR_NOTAVAILABLE
, INVALID_STRING_ID
, WL_ERROR
);
270 ShowNetworkContentListWindow(nullptr, CONTENT_TYPE_GAME
);
274 case WID_GSC_SETTINGS
: {
275 auto it
= this->vscroll
->GetScrolledItemFromWidget(this->visible_settings
, pt
.y
, this, widget
);
276 if (it
== this->visible_settings
.end()) break;
278 const ScriptConfigItem
&config_item
= **it
;
279 if (!this->IsEditableItem(config_item
)) return;
281 int num
= it
- this->visible_settings
.begin();
282 if (this->clicked_row
!= num
) {
283 this->CloseChildWindows(WC_QUERY_STRING
);
284 this->CloseChildWindows(WC_DROPDOWN_MENU
);
285 this->clicked_row
= num
;
286 this->clicked_dropdown
= false;
289 bool bool_item
= (config_item
.flags
& SCRIPTCONFIG_BOOLEAN
) != 0;
291 Rect r
= this->GetWidget
<NWidgetBase
>(widget
)->GetCurrentRect().Shrink(WidgetDimensions::scaled
.matrix
, RectPadding::zero
);
292 int x
= pt
.x
- r
.left
;
293 if (_current_text_dir
== TD_RTL
) x
= r
.Width() - 1 - x
;
295 /* One of the arrows is clicked (or green/red rect in case of bool value) */
296 int old_val
= this->gs_config
->GetSetting(config_item
.name
);
297 if (!bool_item
&& IsInsideMM(x
, 0, SETTING_BUTTON_WIDTH
) && config_item
.complete_labels
) {
298 if (this->clicked_dropdown
) {
299 /* unclick the dropdown */
300 this->CloseChildWindows(WC_DROPDOWN_MENU
);
301 this->clicked_dropdown
= false;
302 this->closing_dropdown
= false;
304 int rel_y
= (pt
.y
- r
.top
) % this->line_height
;
307 wi_rect
.left
= pt
.x
- (_current_text_dir
== TD_RTL
? SETTING_BUTTON_WIDTH
- 1 - x
: x
);
308 wi_rect
.right
= wi_rect
.left
+ SETTING_BUTTON_WIDTH
- 1;
309 wi_rect
.top
= pt
.y
- rel_y
+ (this->line_height
- SETTING_BUTTON_HEIGHT
) / 2;
310 wi_rect
.bottom
= wi_rect
.top
+ SETTING_BUTTON_HEIGHT
- 1;
312 /* If the mouse is still held but dragged outside of the dropdown list, keep the dropdown open */
313 if (pt
.y
>= wi_rect
.top
&& pt
.y
<= wi_rect
.bottom
) {
314 this->clicked_dropdown
= true;
315 this->closing_dropdown
= false;
318 for (int i
= config_item
.min_value
; i
<= config_item
.max_value
; i
++) {
319 list
.push_back(MakeDropDownListStringItem(config_item
.labels
.find(i
)->second
, i
));
322 ShowDropDownListAt(this, std::move(list
), old_val
, WID_GSC_SETTING_DROPDOWN
, wi_rect
, COLOUR_ORANGE
);
325 } else if (IsInsideMM(x
, 0, SETTING_BUTTON_WIDTH
)) {
326 int new_val
= old_val
;
329 } else if (x
>= SETTING_BUTTON_WIDTH
/ 2) {
330 /* Increase button clicked */
331 new_val
+= config_item
.step_size
;
332 if (new_val
> config_item
.max_value
) new_val
= config_item
.max_value
;
333 this->clicked_increase
= true;
335 /* Decrease button clicked */
336 new_val
-= config_item
.step_size
;
337 if (new_val
< config_item
.min_value
) new_val
= config_item
.min_value
;
338 this->clicked_increase
= false;
341 if (new_val
!= old_val
) {
342 this->gs_config
->SetSetting(config_item
.name
, new_val
);
343 this->clicked_button
= num
;
344 this->unclick_timeout
.Reset();
346 } else if (!bool_item
&& !config_item
.complete_labels
) {
347 /* Display a query box so users can enter a custom value. */
348 SetDParam(0, old_val
);
349 ShowQueryString(STR_JUST_INT
, STR_CONFIG_SETTING_QUERY_CAPTION
, INT32_DIGITS_WITH_SIGN_AND_TERMINATION
, this, CS_NUMERAL_SIGNED
, QSF_NONE
);
355 case WID_GSC_OPEN_URL
: {
356 const GameConfig
*config
= GameConfig::GetConfig();
357 if (config
== nullptr || config
->GetInfo() == nullptr) return;
358 OpenBrowser(config
->GetInfo()->GetURL());
363 this->gs_config
->ResetEditableSettings(_game_mode
== GM_MENU
);
369 void OnQueryTextFinished(std::optional
<std::string
> str
) override
371 if (!str
.has_value() || str
->empty()) return;
372 int32_t value
= atoi(str
->c_str());
376 void OnDropdownSelect(WidgetID widget
, int index
) override
378 if (widget
!= WID_GSC_SETTING_DROPDOWN
) return;
379 assert(this->clicked_dropdown
);
383 void OnDropdownClose(Point
, WidgetID widget
, int, bool) override
385 if (widget
!= WID_GSC_SETTING_DROPDOWN
) return;
386 /* We cannot raise the dropdown button just yet. OnClick needs some hint, whether
387 * the same dropdown button was clicked again, and then not open the dropdown again.
388 * So, we only remember that it was closed, and process it on the next OnPaint, which is
390 assert(this->clicked_dropdown
);
391 this->closing_dropdown
= true;
395 void OnResize() override
397 this->vscroll
->SetCapacityFromWidget(this, WID_GSC_SETTINGS
);
400 /** When reset, unclick the button after a small timeout. */
401 TimeoutTimer
<TimerWindow
> unclick_timeout
= {std::chrono::milliseconds(150), [this]() {
402 this->clicked_button
= -1;
407 * Some data on this window has become invalid.
408 * @param data Information about the changed data.
409 * @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.
411 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
413 if (!gui_scope
) return;
415 this->SetWidgetDisabledState(WID_GSC_CHANGE
, (_game_mode
== GM_NORMAL
) || !IsEditable());
417 const GameConfig
*config
= GameConfig::GetConfig();
418 this->SetWidgetDisabledState(WID_GSC_OPEN_URL
, config
->GetInfo() == nullptr || config
->GetInfo()->GetURL().empty());
419 for (TextfileType tft
= TFT_CONTENT_BEGIN
; tft
< TFT_CONTENT_END
; tft
++) {
420 this->SetWidgetDisabledState(WID_GSC_TEXTFILE
+ tft
, !config
->GetTextfile(tft
, (CompanyID
)OWNER_DEITY
).has_value());
422 this->RebuildVisibleSettings();
423 this->CloseChildWindows(WC_DROPDOWN_MENU
);
424 this->CloseChildWindows(WC_QUERY_STRING
);
427 bool IsEditableItem(const ScriptConfigItem
&config_item
) const
429 return _game_mode
== GM_MENU
430 || _game_mode
== GM_EDITOR
431 || (config_item
.flags
& SCRIPTCONFIG_INGAME
) != 0
432 || _settings_client
.gui
.ai_developer_tools
;
435 void SetValue(int value
)
437 const ScriptConfigItem
&config_item
= *this->visible_settings
[this->clicked_row
];
438 if (_game_mode
== GM_NORMAL
&& (config_item
.flags
& SCRIPTCONFIG_INGAME
) == 0) return;
439 this->gs_config
->SetSetting(config_item
.name
, value
);
444 /** Open the GS config window. */
445 void ShowGSConfigWindow()
447 CloseWindowByClass(WC_GAME_OPTIONS
);
448 new GSConfigWindow();