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 cheat_gui.cpp GUI related to cheating. */
11 #include "command_func.h"
12 #include "cheat_type.h"
13 #include "company_base.h"
14 #include "company_func.h"
15 #include "saveload/saveload.h"
16 #include "vehicle_base.h"
17 #include "textbuf_gui.h"
18 #include "window_gui.h"
19 #include "string_func.h"
20 #include "strings_func.h"
21 #include "window_func.h"
23 #include "settings_gui.h"
24 #include "company_gui.h"
25 #include "linkgraph/linkgraphschedule.h"
31 #include "core/geometry_func.hpp"
32 #include "timer/timer.h"
33 #include "timer/timer_game_calendar.h"
34 #include "timer/timer_game_economy.h"
36 #include "widgets/cheat_widget.h"
38 #include "table/sprites.h"
40 #include "safeguards.h"
44 * The 'amount' to cheat with.
45 * This variable is semantically a constant value, but because the cheat
46 * code requires to be able to write to the variable it is not constified.
48 static int32_t _money_cheat_amount
= 10000000;
51 * Handle cheating of money.
52 * Note that the amount of money of a company must be changed through a command
53 * rather than by setting a variable. Since the cheat data structure expects a
54 * variable, the amount of given/taken money is used for this purpose.
55 * @param change_direction is -1 or +1 (down/up)
56 * @return Amount of money cheat.
58 static int32_t ClickMoneyCheat(int32_t, int32_t change_direction
)
60 Command
<CMD_MONEY_CHEAT
>::Post(Money(_money_cheat_amount
) * change_direction
);
61 return _money_cheat_amount
;
65 * Handle changing of company.
66 * @param new_value company to set to
67 * @param change_direction is -1 or +1 (down/up)
68 * @return The new company.
70 static int32_t ClickChangeCompanyCheat(int32_t new_value
, int32_t change_direction
)
72 while ((uint
)new_value
< Company::GetPoolSize()) {
73 if (Company::IsValidID((CompanyID
)new_value
)) {
74 SetLocalCompany((CompanyID
)new_value
);
75 return _local_company
;
77 new_value
+= change_direction
;
80 return _local_company
;
84 * Allow (or disallow) changing production of all industries.
85 * @param new_value new value
86 * @return New value allowing change of industry production.
88 static int32_t ClickSetProdCheat(int32_t new_value
, int32_t)
90 _cheats
.setup_prod
.value
= (new_value
!= 0);
91 InvalidateWindowClassesData(WC_INDUSTRY_VIEW
);
92 return _cheats
.setup_prod
.value
;
95 extern void CalendarEnginesMonthlyLoop();
98 * Handle changing of the current year.
99 * @param new_value The chosen year to change to.
102 static int32_t ClickChangeDateCheat(int32_t new_value
, int32_t)
104 /* Don't allow changing to an invalid year, or the current year. */
105 auto new_year
= Clamp(TimerGameCalendar::Year(new_value
), CalendarTime::MIN_YEAR
, CalendarTime::MAX_YEAR
);
106 if (new_year
== TimerGameCalendar::year
) return TimerGameCalendar::year
.base();
108 TimerGameCalendar::YearMonthDay ymd
= TimerGameCalendar::ConvertDateToYMD(TimerGameCalendar::date
);
109 TimerGameCalendar::Date new_calendar_date
= TimerGameCalendar::ConvertYMDToDate(new_year
, ymd
.month
, ymd
.day
);
111 TimerGameCalendar::SetDate(new_calendar_date
, TimerGameCalendar::date_fract
);
113 /* If not using wallclock units, we keep economy date in sync with calendar date and must change it also. */
114 if (!TimerGameEconomy::UsingWallclockUnits()) {
115 /* Keep economy and calendar dates synced. */
116 TimerGameEconomy::Date new_economy_date
= new_calendar_date
.base();
118 /* Shift cached dates before we change the date. */
119 for (auto v
: Vehicle::Iterate()) v
->ShiftDates(new_economy_date
- TimerGameEconomy::date
);
120 LinkGraphSchedule::instance
.ShiftDates(new_economy_date
- TimerGameEconomy::date
);
122 /* Now it's safe to actually change the date. */
123 TimerGameEconomy::SetDate(new_economy_date
, TimerGameEconomy::date_fract
);
126 CalendarEnginesMonthlyLoop();
127 SetWindowDirty(WC_STATUS_BAR
, 0);
128 InvalidateWindowClassesData(WC_BUILD_STATION
, 0);
129 InvalidateWindowClassesData(WC_BUS_STATION
, 0);
130 InvalidateWindowClassesData(WC_TRUCK_STATION
, 0);
131 InvalidateWindowClassesData(WC_BUILD_OBJECT
, 0);
132 ResetSignalVariant();
133 return TimerGameCalendar::year
.base();
137 * Allow (or disallow) a change of the maximum allowed heightlevel.
138 * @param new_value new value
139 * @return New value (or unchanged old value) of the maximum
140 * allowed heightlevel value.
142 static int32_t ClickChangeMaxHlCheat(int32_t new_value
, int32_t)
144 new_value
= Clamp(new_value
, MIN_MAP_HEIGHT_LIMIT
, MAX_MAP_HEIGHT_LIMIT
);
146 /* Check if at least one mountain on the map is higher than the new value.
147 * If yes, disallow the change. */
148 for (TileIndex t
= 0; t
< Map::Size(); t
++) {
149 if ((int32_t)TileHeight(t
) > new_value
) {
150 ShowErrorMessage(STR_CONFIG_SETTING_TOO_HIGH_MOUNTAIN
, INVALID_STRING_ID
, WL_ERROR
);
151 /* Return old, unchanged value */
152 return _settings_game
.construction
.map_height_limit
;
156 /* Execute the change and reload GRF Data */
157 _settings_game
.construction
.map_height_limit
= new_value
;
160 /* The smallmap uses an index from heightlevels to colours. Trigger rebuilding it. */
161 InvalidateWindowClassesData(WC_SMALLMAP
, 2);
163 return _settings_game
.construction
.map_height_limit
;
166 /** Available cheats. */
168 CHT_MONEY
, ///< Change amount of money.
169 CHT_CHANGE_COMPANY
, ///< Switch company.
170 CHT_EXTRA_DYNAMITE
, ///< Dynamite anything.
171 CHT_CROSSINGTUNNELS
, ///< Allow tunnels to cross each other.
172 CHT_NO_JETCRASH
, ///< Disable jet-airplane crashes.
173 CHT_SETUP_PROD
, ///< Allow manually editing of industry production.
174 CHT_STATION_RATING
, ///< Fix station ratings at 100%.
175 CHT_EDIT_MAX_HL
, ///< Edit maximum allowed heightlevel
176 CHT_CHANGE_DATE
, ///< Do time traveling.
178 CHT_NUM_CHEATS
, ///< Number of cheats.
182 * Signature of handler function when user clicks at a cheat.
183 * @param new_value The new value.
184 * @param change_direction Change direction (+1, +1), \c 0 for boolean settings.
186 typedef int32_t CheckButtonClick(int32_t new_value
, int32_t change_direction
);
188 /** Information of a cheat. */
190 VarType type
; ///< type of selector
191 StringID str
; ///< string with descriptive text
192 void *variable
; ///< pointer to the variable
193 bool *been_used
; ///< has this cheat been used before?
194 CheckButtonClick
*proc
;///< procedure
198 * The available cheats.
199 * Order matches with the values of #CheatNumbers
201 static const CheatEntry _cheats_ui
[] = {
202 {SLE_INT32
, STR_CHEAT_MONEY
, &_money_cheat_amount
, &_cheats
.money
.been_used
, &ClickMoneyCheat
},
203 {SLE_UINT8
, STR_CHEAT_CHANGE_COMPANY
, &_local_company
, &_cheats
.switch_company
.been_used
, &ClickChangeCompanyCheat
},
204 {SLE_BOOL
, STR_CHEAT_EXTRA_DYNAMITE
, &_cheats
.magic_bulldozer
.value
, &_cheats
.magic_bulldozer
.been_used
, nullptr },
205 {SLE_BOOL
, STR_CHEAT_CROSSINGTUNNELS
, &_cheats
.crossing_tunnels
.value
, &_cheats
.crossing_tunnels
.been_used
, nullptr },
206 {SLE_BOOL
, STR_CHEAT_NO_JETCRASH
, &_cheats
.no_jetcrash
.value
, &_cheats
.no_jetcrash
.been_used
, nullptr },
207 {SLE_BOOL
, STR_CHEAT_SETUP_PROD
, &_cheats
.setup_prod
.value
, &_cheats
.setup_prod
.been_used
, &ClickSetProdCheat
},
208 {SLE_BOOL
, STR_CHEAT_STATION_RATING
, &_cheats
.station_rating
.value
, &_cheats
.station_rating
.been_used
, nullptr },
209 {SLE_UINT8
, STR_CHEAT_EDIT_MAX_HL
, &_settings_game
.construction
.map_height_limit
, &_cheats
.edit_max_hl
.been_used
, &ClickChangeMaxHlCheat
},
210 {SLE_INT32
, STR_CHEAT_CHANGE_DATE
, &TimerGameCalendar::year
, &_cheats
.change_date
.been_used
, &ClickChangeDateCheat
},
213 static_assert(CHT_NUM_CHEATS
== lengthof(_cheats_ui
));
215 /** Widget definitions of the cheat GUI. */
216 static constexpr NWidgetPart _nested_cheat_widgets
[] = {
217 NWidget(NWID_HORIZONTAL
),
218 NWidget(WWT_CLOSEBOX
, COLOUR_GREY
),
219 NWidget(WWT_CAPTION
, COLOUR_GREY
), SetDataTip(STR_CHEATS
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
220 NWidget(WWT_SHADEBOX
, COLOUR_GREY
),
221 NWidget(WWT_STICKYBOX
, COLOUR_GREY
),
223 NWidget(WWT_PANEL
, COLOUR_GREY
, WID_C_PANEL
), EndContainer(),
226 /** GUI for the cheats. */
227 struct CheatWindow
: Window
{
231 Dimension icon
; ///< Dimension of company icon sprite
233 CheatWindow(WindowDesc
*desc
) : Window(desc
)
238 void OnInit() override
240 this->icon
= GetSpriteSize(SPR_COMPANY_ICON
);
243 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
245 if (widget
!= WID_C_PANEL
) return;
247 const Rect ir
= r
.Shrink(WidgetDimensions::scaled
.framerect
);
250 bool rtl
= _current_text_dir
== TD_RTL
;
251 uint button_left
= rtl
? ir
.right
- SETTING_BUTTON_WIDTH
: ir
.left
;
252 uint text_left
= ir
.left
+ (rtl
? 0 : WidgetDimensions::scaled
.hsep_wide
+ SETTING_BUTTON_WIDTH
);
253 uint text_right
= ir
.right
- (rtl
? WidgetDimensions::scaled
.hsep_wide
+ SETTING_BUTTON_WIDTH
: 0);
255 int text_y_offset
= (this->line_height
- GetCharacterHeight(FS_NORMAL
)) / 2;
256 int button_y_offset
= (this->line_height
- SETTING_BUTTON_HEIGHT
) / 2;
257 int icon_y_offset
= (this->line_height
- this->icon
.height
) / 2;
259 for (int i
= 0; i
!= lengthof(_cheats_ui
); i
++) {
260 const CheatEntry
*ce
= &_cheats_ui
[i
];
264 bool on
= (*(bool*)ce
->variable
);
266 DrawBoolButton(button_left
, y
+ button_y_offset
, on
, true);
267 SetDParam(0, on
? STR_CONFIG_SETTING_ON
: STR_CONFIG_SETTING_OFF
);
272 int32_t val
= (int32_t)ReadValue(ce
->variable
, ce
->type
);
274 /* Draw [<][>] boxes for settings of an integer-type */
275 DrawArrowButtons(button_left
, y
+ button_y_offset
, COLOUR_YELLOW
, clicked
- (i
* 2), true, true);
278 /* Display date for change date cheat */
279 case STR_CHEAT_CHANGE_DATE
: SetDParam(0, TimerGameCalendar::date
); break;
281 /* Draw coloured flag for change company cheat */
282 case STR_CHEAT_CHANGE_COMPANY
: {
283 SetDParam(0, val
+ 1);
284 uint offset
= WidgetDimensions::scaled
.hsep_indent
+ GetStringBoundingBox(ce
->str
).width
;
285 DrawCompanyIcon(_local_company
, rtl
? text_right
- offset
- WidgetDimensions::scaled
.hsep_indent
: text_left
+ offset
, y
+ icon_y_offset
);
289 default: SetDParam(0, val
);
295 DrawString(text_left
, text_right
, y
+ text_y_offset
, ce
->str
);
297 y
+= this->line_height
;
301 void UpdateWidgetSize(WidgetID widget
, Dimension
*size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
*fill
, [[maybe_unused
]] Dimension
*resize
) override
303 if (widget
!= WID_C_PANEL
) return;
306 for (int i
= 0; i
!= lengthof(_cheats_ui
); i
++) {
307 const CheatEntry
*ce
= &_cheats_ui
[i
];
310 SetDParam(0, STR_CONFIG_SETTING_ON
);
311 width
= std::max(width
, GetStringBoundingBox(ce
->str
).width
);
312 SetDParam(0, STR_CONFIG_SETTING_OFF
);
313 width
= std::max(width
, GetStringBoundingBox(ce
->str
).width
);
318 /* Display date for change date cheat */
319 case STR_CHEAT_CHANGE_DATE
:
320 SetDParam(0, TimerGameCalendar::ConvertYMDToDate(CalendarTime::MAX_YEAR
, 11, 31));
321 width
= std::max(width
, GetStringBoundingBox(ce
->str
).width
);
324 /* Draw coloured flag for change company cheat */
325 case STR_CHEAT_CHANGE_COMPANY
:
326 SetDParamMaxValue(0, MAX_COMPANIES
);
327 width
= std::max(width
, GetStringBoundingBox(ce
->str
).width
+ WidgetDimensions::scaled
.hsep_wide
);
331 SetDParam(0, INT64_MAX
);
332 width
= std::max(width
, GetStringBoundingBox(ce
->str
).width
);
339 this->line_height
= std::max
<uint
>(this->icon
.height
, SETTING_BUTTON_HEIGHT
);
340 this->line_height
= std::max
<uint
>(this->line_height
, GetCharacterHeight(FS_NORMAL
)) + WidgetDimensions::scaled
.framerect
.Vertical();
342 size
->width
= width
+ WidgetDimensions::scaled
.hsep_wide
* 2 + SETTING_BUTTON_WIDTH
;
343 size
->height
= WidgetDimensions::scaled
.framerect
.Vertical() + this->line_height
* lengthof(_cheats_ui
);
346 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
348 if (widget
!= WID_C_PANEL
) return;
350 Rect r
= this->GetWidget
<NWidgetBase
>(WID_C_PANEL
)->GetCurrentRect().Shrink(WidgetDimensions::scaled
.framerect
);
351 uint btn
= (pt
.y
- r
.top
) / this->line_height
;
352 int x
= pt
.x
- r
.left
;
353 bool rtl
= _current_text_dir
== TD_RTL
;
354 if (rtl
) x
= r
.Width() - 1 - x
;
356 if (btn
>= lengthof(_cheats_ui
)) return;
358 const CheatEntry
*ce
= &_cheats_ui
[btn
];
359 int value
= (int32_t)ReadValue(ce
->variable
, ce
->type
);
360 int oldvalue
= value
;
362 if (btn
== CHT_CHANGE_DATE
&& x
>= SETTING_BUTTON_WIDTH
) {
363 /* Click at the date text directly. */
364 clicked_widget
= CHT_CHANGE_DATE
;
366 ShowQueryString(STR_JUST_INT
, STR_CHEAT_CHANGE_DATE_QUERY_CAPT
, 8, this, CS_NUMERAL
, QSF_ACCEPT_UNCHANGED
);
368 } else if (btn
== CHT_EDIT_MAX_HL
&& x
>= SETTING_BUTTON_WIDTH
) {
369 clicked_widget
= CHT_EDIT_MAX_HL
;
371 ShowQueryString(STR_JUST_INT
, STR_CHEAT_EDIT_MAX_HL_QUERY_CAPT
, 8, this, CS_NUMERAL
, QSF_ACCEPT_UNCHANGED
);
375 /* Not clicking a button? */
376 if (!IsInsideMM(x
, 0, SETTING_BUTTON_WIDTH
)) return;
378 *ce
->been_used
= true;
383 if (ce
->proc
!= nullptr) ce
->proc(value
, 0);
387 /* Take whatever the function returns */
388 value
= ce
->proc(value
+ ((x
>= SETTING_BUTTON_WIDTH
/ 2) ? 1 : -1), (x
>= SETTING_BUTTON_WIDTH
/ 2) ? 1 : -1);
390 /* The first cheat (money), doesn't return a different value. */
391 if (value
!= oldvalue
|| btn
== CHT_MONEY
) this->clicked
= btn
* 2 + 1 + ((x
>= SETTING_BUTTON_WIDTH
/ 2) != rtl
? 1 : 0);
395 if (value
!= oldvalue
) WriteValue(ce
->variable
, ce
->type
, (int64_t)value
);
402 void OnTimeout() override
408 void OnQueryTextFinished(char *str
) override
410 /* Was 'cancel' pressed or nothing entered? */
411 if (str
== nullptr || StrEmpty(str
)) return;
413 const CheatEntry
*ce
= &_cheats_ui
[clicked_widget
];
414 int oldvalue
= (int32_t)ReadValue(ce
->variable
, ce
->type
);
415 int value
= atoi(str
);
416 *ce
->been_used
= true;
417 value
= ce
->proc(value
, value
- oldvalue
);
419 if (value
!= oldvalue
) WriteValue(ce
->variable
, ce
->type
, (int64_t)value
);
423 IntervalTimer
<TimerGameCalendar
> daily_interval
= {{TimerGameCalendar::MONTH
, TimerGameCalendar::Priority::NONE
}, [this](auto) {
428 /** Window description of the cheats GUI. */
429 static WindowDesc
_cheats_desc(__FILE__
, __LINE__
,
430 WDP_AUTO
, "cheats", 0, 0,
433 std::begin(_nested_cheat_widgets
), std::end(_nested_cheat_widgets
)
436 /** Open cheat window. */
437 void ShowCheatWindow()
439 CloseWindowById(WC_CHEATS
, 0);
440 new CheatWindow(&_cheats_desc
);