1 /***************************************************************************
2  *   Free Heroes of Might and Magic II: https://github.com/ihhub/fheroes2  *
3  *   Copyright (C) 2021                                                    *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) any later version.                                   *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
19  ***************************************************************************/
20 
21 #include "dialog_system_options.h"
22 #include "agg_image.h"
23 #include "audio.h"
24 #include "cursor.h"
25 #include "dialog.h"
26 #include "game.h"
27 #include "game_delays.h"
28 #include "game_interface.h"
29 #include "icn.h"
30 #include "localevent.h"
31 #include "screen.h"
32 #include "settings.h"
33 #include "text.h"
34 #include "translations.h"
35 #include "ui_button.h"
36 
37 #include <cassert>
38 
39 namespace
40 {
41     enum class DialogAction : int
42     {
43         Open,
44         ChangeInterfaceTheme,
45         UpdateInterface,
46         SaveConfiguration,
47         Close
48     };
49 
50     const int textOffset = 2;
51 
drawOption(const fheroes2::Rect & optionRoi,const fheroes2::Sprite & icon,const std::string & title,const std::string & value)52     void drawOption( const fheroes2::Rect & optionRoi, const fheroes2::Sprite & icon, const std::string & title, const std::string & value )
53     {
54         fheroes2::Display & display = fheroes2::Display::instance();
55 
56         fheroes2::Blit( icon, display, optionRoi.x, optionRoi.y );
57         Text text( title, Font::SMALL );
58         text.Blit( optionRoi.x + ( optionRoi.width - text.w() ) / 2, optionRoi.y - text.h() - textOffset );
59 
60         text.Set( value );
61         text.Blit( optionRoi.x + ( optionRoi.width - text.w() ) / 2, optionRoi.y + optionRoi.height + textOffset );
62     }
63 
drawDialog(const std::vector<fheroes2::Rect> & rects)64     void drawDialog( const std::vector<fheroes2::Rect> & rects )
65     {
66         assert( rects.size() == 9 );
67 
68         const Settings & conf = Settings::Get();
69 
70         // Music volume.
71         const fheroes2::Sprite & musicVolumeIcon = fheroes2::AGG::GetICN( ICN::SPANEL, Audio::isValid() ? 1 : 0 );
72         std::string value;
73         if ( Audio::isValid() && conf.MusicVolume() ) {
74             value = std::to_string( conf.MusicVolume() );
75         }
76         else {
77             value = _( "off" );
78         }
79 
80         drawOption( rects[0], musicVolumeIcon, _( "Music" ), value );
81 
82         // Sound volume.
83         const fheroes2::Sprite & soundVolumeOption = fheroes2::AGG::GetICN( ICN::SPANEL, Audio::isValid() ? 3 : 2 );
84         if ( Audio::isValid() && conf.SoundVolume() ) {
85             value = std::to_string( conf.SoundVolume() );
86         }
87         else {
88             value = _( "off" );
89         }
90 
91         drawOption( rects[1], soundVolumeOption, _( "Effects" ), value );
92 
93         // Music Type.
94         const MusicSource musicType = conf.MusicType();
95         const fheroes2::Sprite & musicTypeIcon = fheroes2::AGG::GetICN( ICN::SPANEL, musicType == MUSIC_EXTERNAL ? 11 : 10 );
96         if ( musicType == MUSIC_MIDI_ORIGINAL ) {
97             value = _( "MIDI" );
98         }
99         else if ( musicType == MUSIC_MIDI_EXPANSION ) {
100             value = _( "MIDI Expansion" );
101         }
102         else if ( musicType == MUSIC_EXTERNAL ) {
103             value = _( "External" );
104         }
105 
106         drawOption( rects[2], musicTypeIcon, _( "Music Type" ), value );
107 
108         // Hero's movement speed.
109         const int heroSpeed = conf.HeroesMoveSpeed();
110         uint32_t heroSpeedIconId = 9;
111         if ( heroSpeed >= 4 ) {
112             heroSpeedIconId = 3 + heroSpeed / 2;
113         }
114         else if ( heroSpeed > 0 ) {
115             heroSpeedIconId = 4;
116         }
117 
118         const fheroes2::Sprite & heroSpeedIcon = fheroes2::AGG::GetICN( ICN::SPANEL, heroSpeedIconId );
119         if ( heroSpeed == 10 ) {
120             value = _( "Jump" );
121         }
122         else {
123             value = std::to_string( heroSpeed );
124         }
125 
126         drawOption( rects[3], heroSpeedIcon, _( "Hero Speed" ), value );
127 
128         // AI's movement speed.
129         const int aiSpeed = conf.AIMoveSpeed();
130         uint32_t aiSpeedIconId = 9;
131         if ( aiSpeed >= 4 ) {
132             aiSpeedIconId = 3 + aiSpeed / 2;
133         }
134         else if ( aiSpeed > 0 ) {
135             aiSpeedIconId = 4;
136         }
137 
138         const fheroes2::Sprite & aiSpeedIcon = fheroes2::AGG::GetICN( ICN::SPANEL, aiSpeedIconId );
139         if ( aiSpeed == 0 ) {
140             value = _( "Don't Show" );
141         }
142         else if ( aiSpeed == 10 ) {
143             value = _( "Jump" );
144         }
145         else {
146             value = std::to_string( aiSpeed );
147         }
148 
149         drawOption( rects[4], aiSpeedIcon, _( "Enemy Speed" ), value );
150 
151         // Scrolling speed.
152         const int scrollSpeed = conf.ScrollSpeed();
153         uint32_t scrollSpeedIconId = 7;
154         if ( scrollSpeed < SCROLL_NORMAL ) {
155             scrollSpeedIconId = 4;
156         }
157         else if ( scrollSpeed < SCROLL_FAST1 ) {
158             scrollSpeedIconId = 5;
159         }
160         else if ( scrollSpeed < SCROLL_FAST2 ) {
161             scrollSpeedIconId = 6;
162         }
163 
164         const fheroes2::Sprite & scrollSpeedIcon = fheroes2::AGG::GetICN( ICN::SPANEL, scrollSpeedIconId );
165         drawOption( rects[5], scrollSpeedIcon, _( "Scroll Speed" ), std::to_string( scrollSpeed ) );
166 
167         // Interface theme.
168         const bool isEvilInterface = conf.ExtGameEvilInterface();
169         const fheroes2::Sprite & interfaceThemeIcon = fheroes2::AGG::GetICN( ICN::SPANEL, isEvilInterface ? 17 : 16 );
170         if ( isEvilInterface ) {
171             value = _( "Evil" );
172         }
173         else {
174             value = _( "Good" );
175         }
176 
177         drawOption( rects[6], interfaceThemeIcon, _( "Interface Type" ), value );
178 
179         // Interface show/hide state.
180         const bool isHiddenInterface = conf.ExtGameHideInterface();
181         const fheroes2::Sprite & interfaceStateIcon = isHiddenInterface ? fheroes2::AGG::GetICN( ICN::ESPANEL, 4 ) : fheroes2::AGG::GetICN( ICN::SPANEL, 16 );
182         if ( isHiddenInterface ) {
183             value = _( "Hide" );
184         }
185         else {
186             value = _( "Show" );
187         }
188 
189         drawOption( rects[7], interfaceStateIcon, _( "Interface" ), value );
190 
191         // Auto-battles.
192         if ( conf.BattleAutoResolve() ) {
193             const bool spellcast = conf.BattleAutoSpellcast();
194             value = spellcast ? _( "Auto Resolve" ) : _( "Auto, No Spells" );
195 
196             const fheroes2::Sprite & autoBattleIcon = fheroes2::AGG::GetICN( ICN::CSPANEL, spellcast ? 7 : 6 );
197             drawOption( rects[8], autoBattleIcon, _( "Battles" ), value );
198         }
199         else {
200             const fheroes2::Sprite & autoBattleIcon = fheroes2::AGG::GetICN( ICN::SPANEL, 18 );
201             drawOption( rects[8], autoBattleIcon, _( "Battles" ), _( "Manual" ) );
202         }
203     }
204 
openSystemOptionsDialog()205     DialogAction openSystemOptionsDialog()
206     {
207         const CursorRestorer cursorRestorer( true, Cursor::POINTER );
208 
209         Settings & conf = Settings::Get();
210         const bool isEvilInterface = conf.ExtGameEvilInterface();
211 
212         fheroes2::Display & display = fheroes2::Display::instance();
213 
214         const fheroes2::Sprite & dialog = fheroes2::AGG::GetICN( ( isEvilInterface ? ICN::SPANBKGE : ICN::SPANBKG ), 0 );
215         const fheroes2::Sprite & dialogShadow = fheroes2::AGG::GetICN( ( isEvilInterface ? ICN::SPANBKGE : ICN::SPANBKG ), 1 );
216 
217         const fheroes2::Point dialogOffset( ( display.width() - dialog.width() ) / 2, ( display.height() - dialog.height() ) / 2 );
218         const fheroes2::Point shadowOffset( dialogOffset.x - BORDERWIDTH, dialogOffset.y );
219 
220         fheroes2::ImageRestorer back( display, shadowOffset.x, shadowOffset.y, dialog.width() + BORDERWIDTH, dialog.height() + BORDERWIDTH );
221         const fheroes2::Rect dialogArea( dialogOffset.x, dialogOffset.y, dialog.width(), dialog.height() );
222 
223         fheroes2::Fill( display, dialogArea.x, dialogArea.y, dialogArea.width, dialogArea.height, 0 );
224         fheroes2::Blit( dialogShadow, display, dialogArea.x - BORDERWIDTH, dialogArea.y + BORDERWIDTH );
225         fheroes2::Blit( dialog, display, dialogArea.x, dialogArea.y );
226 
227         const fheroes2::Sprite & optionSprite = fheroes2::AGG::GetICN( ICN::SPANEL, 0 );
228         const fheroes2::Point optionOffset( 36 + dialogArea.x, 47 + dialogArea.y );
229         const fheroes2::Point optionStep( 92, 110 );
230 
231         std::vector<fheroes2::Rect> roi;
232 
233         for ( int32_t y = 0; y < 3; ++y ) {
234             for ( int32_t x = 0; x < 3; ++x ) {
235                 roi.emplace_back( optionOffset.x + x * optionStep.x, optionOffset.y + y * optionStep.y, optionSprite.width(), optionSprite.height() );
236             }
237         }
238 
239         const fheroes2::Rect & musicVolumeRoi = roi[0];
240         const fheroes2::Rect & soundVolumeRoi = roi[1];
241         const fheroes2::Rect & musicTypeRoi = roi[2];
242         const fheroes2::Rect & heroSpeedRoi = roi[3];
243         const fheroes2::Rect & aiSpeedRoi = roi[4];
244         const fheroes2::Rect & scrollSpeedRoi = roi[5];
245         const fheroes2::Rect & interfaceTypeRoi = roi[6];
246         const fheroes2::Rect & interfaceStateRoi = roi[7];
247         const fheroes2::Rect & battleResolveRoi = roi[8];
248 
249         drawDialog( roi );
250 
251         const fheroes2::Point buttonOffset( 112 + dialogArea.x, 362 + dialogArea.y );
252         fheroes2::Button buttonOkay( buttonOffset.x, buttonOffset.y, isEvilInterface ? ICN::SPANBTNE : ICN::SPANBTN, 0, 1 );
253         buttonOkay.draw();
254 
255         display.render();
256 
257         bool saveConfig = false;
258 
259         // dialog menu loop
260         LocalEvent & le = LocalEvent::Get();
261         while ( le.HandleEvents() ) {
262             le.MousePressLeft( buttonOkay.area() ) ? buttonOkay.drawOnPress() : buttonOkay.drawOnRelease();
263 
264             if ( le.MouseClickLeft( buttonOkay.area() ) || HotKeyCloseWindow ) {
265                 break;
266             }
267 
268             // set music or sound volume
269             bool saveMusicVolume = false;
270             bool saveSoundVolume = false;
271             if ( Audio::isValid() ) {
272                 if ( le.MouseClickLeft( musicVolumeRoi ) ) {
273                     conf.SetMusicVolume( ( conf.MusicVolume() + 1 ) % 11 );
274                     saveMusicVolume = true;
275                 }
276                 else if ( le.MouseWheelUp( musicVolumeRoi ) ) {
277                     conf.SetMusicVolume( conf.MusicVolume() + 1 );
278                     saveMusicVolume = true;
279                 }
280                 else if ( le.MouseWheelDn( musicVolumeRoi ) ) {
281                     conf.SetMusicVolume( conf.MusicVolume() - 1 );
282                     saveMusicVolume = true;
283                 }
284                 if ( saveMusicVolume ) {
285                     Music::Volume( static_cast<int16_t>( Mixer::MaxVolume() * conf.MusicVolume() / 10 ) );
286                 }
287 
288                 if ( le.MouseClickLeft( soundVolumeRoi ) ) {
289                     conf.SetSoundVolume( ( conf.SoundVolume() + 1 ) % 11 );
290                     saveSoundVolume = true;
291                 }
292                 else if ( le.MouseWheelUp( soundVolumeRoi ) ) {
293                     conf.SetSoundVolume( conf.SoundVolume() + 1 );
294                     saveSoundVolume = true;
295                 }
296                 else if ( le.MouseWheelDn( soundVolumeRoi ) ) {
297                     conf.SetSoundVolume( conf.SoundVolume() - 1 );
298                     saveSoundVolume = true;
299                 }
300                 if ( saveSoundVolume ) {
301                     Game::EnvironmentSoundMixer();
302                 }
303             }
304 
305             // set music type
306             bool saveMusicType = false;
307             if ( le.MouseClickLeft( musicTypeRoi ) ) {
308                 int type = conf.MusicType() + 1;
309                 // If there's no expansion files we skip this option
310                 if ( type == MUSIC_MIDI_EXPANSION && !conf.isPriceOfLoyaltySupported() )
311                     ++type;
312 
313                 const Game::MusicRestorer musicRestorer;
314 
315                 conf.SetMusicType( type > MUSIC_EXTERNAL ? 0 : type );
316 
317                 Game::SetCurrentMusic( MUS::UNKNOWN );
318 
319                 saveMusicType = true;
320             }
321 
322             // set hero speed
323             bool saveHeroSpeed = false;
324             if ( le.MouseClickLeft( heroSpeedRoi ) ) {
325                 conf.SetHeroesMoveSpeed( conf.HeroesMoveSpeed() % 10 + 1 );
326                 saveHeroSpeed = true;
327             }
328             else if ( le.MouseWheelUp( heroSpeedRoi ) ) {
329                 conf.SetHeroesMoveSpeed( conf.HeroesMoveSpeed() + 1 );
330                 saveHeroSpeed = true;
331             }
332             else if ( le.MouseWheelDn( heroSpeedRoi ) ) {
333                 conf.SetHeroesMoveSpeed( conf.HeroesMoveSpeed() - 1 );
334                 saveHeroSpeed = true;
335             }
336 
337             // set ai speed
338             bool saveAISpeed = false;
339             if ( le.MouseClickLeft( aiSpeedRoi ) ) {
340                 conf.SetAIMoveSpeed( ( conf.AIMoveSpeed() + 1 ) % 11 );
341                 saveAISpeed = true;
342             }
343             else if ( le.MouseWheelUp( aiSpeedRoi ) ) {
344                 conf.SetAIMoveSpeed( conf.AIMoveSpeed() + 1 );
345                 saveAISpeed = true;
346             }
347             else if ( le.MouseWheelDn( aiSpeedRoi ) ) {
348                 conf.SetAIMoveSpeed( conf.AIMoveSpeed() - 1 );
349                 saveAISpeed = true;
350             }
351 
352             if ( saveHeroSpeed || saveAISpeed ) {
353                 Game::UpdateGameSpeed();
354             }
355 
356             // set scroll speed
357             bool saveScrollSpeed = false;
358             if ( le.MouseClickLeft( scrollSpeedRoi ) ) {
359                 conf.SetScrollSpeed( conf.ScrollSpeed() % SCROLL_FAST2 + 1 );
360                 saveScrollSpeed = true;
361             }
362             else if ( le.MouseWheelUp( scrollSpeedRoi ) ) {
363                 conf.SetScrollSpeed( conf.ScrollSpeed() + 1 );
364                 saveScrollSpeed = true;
365             }
366             else if ( le.MouseWheelDn( scrollSpeedRoi ) ) {
367                 conf.SetScrollSpeed( conf.ScrollSpeed() - 1 );
368                 saveScrollSpeed = true;
369             }
370 
371             // set interface theme
372             if ( le.MouseClickLeft( interfaceTypeRoi ) ) {
373                 return DialogAction::ChangeInterfaceTheme;
374             }
375 
376             // set interface hide/show
377             if ( le.MouseClickLeft( interfaceStateRoi ) ) {
378                 return DialogAction::UpdateInterface;
379             }
380 
381             // toggle manual/auto battles
382             bool saveAutoBattle = false;
383             if ( le.MouseClickLeft( battleResolveRoi ) ) {
384                 if ( conf.BattleAutoResolve() ) {
385                     if ( conf.BattleAutoSpellcast() ) {
386                         conf.setBattleAutoSpellcast( false );
387                     }
388                     else {
389                         conf.setBattleAutoResolve( false );
390                     }
391                 }
392                 else {
393                     conf.setBattleAutoResolve( true );
394                     conf.setBattleAutoSpellcast( true );
395                 }
396                 saveAutoBattle = true;
397             }
398 
399             if ( le.MousePressRight( musicVolumeRoi ) )
400                 Dialog::Message( _( "Music" ), _( "Toggle ambient music level." ), Font::BIG );
401             else if ( le.MousePressRight( soundVolumeRoi ) )
402                 Dialog::Message( _( "Effects" ), _( "Toggle foreground sounds level." ), Font::BIG );
403             else if ( le.MousePressRight( musicTypeRoi ) )
404                 Dialog::Message( _( "Music Type" ), _( "Change the type of music." ), Font::BIG );
405             else if ( le.MousePressRight( heroSpeedRoi ) )
406                 Dialog::Message( _( "Hero Speed" ), _( "Change the speed at which your heroes move on the main screen." ), Font::BIG );
407             else if ( le.MousePressRight( aiSpeedRoi ) )
408                 Dialog::Message( _( "Enemy Speed" ), _( "Sets the speed that A.I. heroes move at.  You can also elect not to view A.I. movement at all." ), Font::BIG );
409             else if ( le.MousePressRight( scrollSpeedRoi ) )
410                 Dialog::Message( _( "Scroll Speed" ), _( "Sets the speed at which you scroll the window." ), Font::BIG );
411             else if ( le.MousePressRight( interfaceTypeRoi ) )
412                 Dialog::Message( _( "Interface Type" ), _( "Toggle the type of interface you want to use." ), Font::BIG );
413             else if ( le.MousePressRight( interfaceStateRoi ) )
414                 Dialog::Message( _( "Interface" ), _( "Toggle interface visibility." ), Font::BIG );
415             else if ( le.MousePressRight( battleResolveRoi ) )
416                 Dialog::Message( _( "Battles" ), _( "Toggle instant battle mode." ), Font::BIG );
417             else if ( le.MousePressRight( buttonOkay.area() ) )
418                 Dialog::Message( _( "OK" ), _( "Exit this menu." ), Font::BIG );
419 
420             if ( saveMusicVolume || saveSoundVolume || saveMusicType || saveHeroSpeed || saveAISpeed || saveScrollSpeed || saveAutoBattle ) {
421                 // redraw
422                 fheroes2::Blit( dialog, display, dialogArea.x, dialogArea.y );
423                 drawDialog( roi );
424                 buttonOkay.draw();
425                 display.render();
426 
427                 saveConfig = true;
428             }
429         }
430 
431         if ( saveConfig ) {
432             return DialogAction::SaveConfiguration;
433         }
434 
435         return DialogAction::Close;
436     }
437 }
438 
439 namespace fheroes2
440 {
showSystemOptionsDialog()441     void showSystemOptionsDialog()
442     {
443         // We should make file writing only once.
444         bool saveConfiguration = false;
445 
446         DialogAction action = DialogAction::Open;
447 
448         while ( action != DialogAction::Close ) {
449             switch ( action ) {
450             case DialogAction::Open:
451                 action = openSystemOptionsDialog();
452                 break;
453             case DialogAction::ChangeInterfaceTheme: {
454                 Settings & conf = Settings::Get();
455                 conf.SetEvilInterface( !conf.ExtGameEvilInterface() );
456                 saveConfiguration = true;
457 
458                 Interface::Basic & basicInterface = Interface::Basic::Get();
459                 Interface::GameArea & gamearea = basicInterface.GetGameArea();
460                 const fheroes2::Point prevCenter = gamearea.getCurrentCenterInPixels();
461 
462                 basicInterface.Reset();
463                 gamearea.SetCenterInPixels( prevCenter );
464                 basicInterface.Redraw( Interface::REDRAW_ALL );
465 
466                 action = openSystemOptionsDialog();
467                 break;
468             }
469             case DialogAction::UpdateInterface: {
470                 Settings & conf = Settings::Get();
471                 conf.SetHideInterface( !conf.ExtGameHideInterface() );
472                 saveConfiguration = true;
473 
474                 Interface::Basic & basicInterface = Interface::Basic::Get();
475                 Interface::GameArea & gamearea = basicInterface.GetGameArea();
476                 const fheroes2::Point prevCenter = gamearea.getCurrentCenterInPixels();
477                 const fheroes2::Rect prevRoi = gamearea.GetROI();
478 
479                 basicInterface.SetHideInterface( conf.ExtGameHideInterface() );
480 
481                 basicInterface.Reset();
482 
483                 const fheroes2::Rect newRoi = gamearea.GetROI();
484 
485                 gamearea.SetCenterInPixels( prevCenter + fheroes2::Point( newRoi.width / 2, newRoi.height / 2 ) + fheroes2::Point( newRoi.x, newRoi.y )
486                                             - fheroes2::Point( prevRoi.width / 2, prevRoi.height / 2 ) - fheroes2::Point( prevRoi.x, prevRoi.y ) );
487 
488                 // We need to redraw radar first due to the nature of restorers. Only then we can redraw everything.
489                 basicInterface.Redraw( Interface::REDRAW_RADAR );
490                 basicInterface.Redraw( Interface::REDRAW_ALL );
491 
492                 action = openSystemOptionsDialog();
493                 break;
494             }
495             case DialogAction::SaveConfiguration:
496                 Settings::Get().Save( "fheroes2.cfg" );
497                 return;
498             default:
499                 break;
500             }
501         }
502 
503         if ( saveConfiguration ) {
504             Settings::Get().Save( "fheroes2.cfg" );
505         }
506     }
507 }
508