/**
* @ file options . cpp
*
* Load and save options from the diablo . ini file .
*/
# include <cstdint>
# include <fstream>
# include <locale>
# ifdef __ANDROID__
# include "SDL.h"
# include <jni.h>
# endif
# ifdef __vita__
# include <psp2/apputil.h>
# include <psp2/system_param.h>
# endif
# ifdef __3DS__
# include "platform/ctr/locale.hpp"
# endif
# ifdef _WIN32
// clang-format off
# include <windows.h>
# include <winnls.h>
// clang-format on
# endif
# define SI_SUPPORT_IOSTREAMS
# include <SimpleIni.h>
# include "diablo.h"
# include "discord/discord.h"
# include "engine/demomode.h"
# include "hwcursor.hpp"
# include "options.h"
# include "qol/monhealthbar.h"
# include "qol/xpbar.h"
# include "utils/file_util.h"
# include "utils/language.h"
# include "utils/log.hpp"
# include "utils/paths.h"
# include "utils/utf8.hpp"
namespace devilution {
# ifndef DEFAULT_WIDTH
# define DEFAULT_WIDTH 640
# endif
# ifndef DEFAULT_HEIGHT
# define DEFAULT_HEIGHT 480
# endif
# ifndef DEFAULT_AUDIO_SAMPLE_RATE
# define DEFAULT_AUDIO_SAMPLE_RATE 22050
# endif
# ifndef DEFAULT_AUDIO_CHANNELS
# define DEFAULT_AUDIO_CHANNELS 2
# endif
# ifndef DEFAULT_AUDIO_BUFFER_SIZE
# define DEFAULT_AUDIO_BUFFER_SIZE 2048
# endif
# ifndef DEFAULT_AUDIO_RESAMPLING_QUALITY
# define DEFAULT_AUDIO_RESAMPLING_QUALITY 5
# endif
namespace {
# if defined(__ANDROID__) || defined(__APPLE__)
constexpr OptionEntryFlags OnlyIfNoImplicitRenderer = OptionEntryFlags : : Invisible ;
# else
constexpr OptionEntryFlags OnlyIfNoImplicitRenderer = OptionEntryFlags : : None ;
# endif
# if defined(__ANDROID__) || defined(TARGET_OS_IPHONE)
constexpr OptionEntryFlags OnlyIfSupportsWindowed = OptionEntryFlags : : Invisible ;
# else
constexpr OptionEntryFlags OnlyIfSupportsWindowed = OptionEntryFlags : : None ;
# endif
std : : string GetIniPath ( )
{
auto path = paths : : ConfigPath ( ) + std : : string ( " diablo.ini " ) ;
return path ;
}
CSimpleIni & GetIni ( )
{
static CSimpleIni ini ;
static bool isIniLoaded = false ;
if ( ! isIniLoaded ) {
auto path = GetIniPath ( ) ;
auto stream = CreateFileStream ( path . c_str ( ) , std : : fstream : : in | std : : fstream : : binary ) ;
ini . SetSpaces ( false ) ;
ini . SetMultiKey ( ) ;
if ( stream )
ini . LoadData ( * stream ) ;
isIniLoaded = true ;
}
return ini ;
}
bool IniChanged = false ;
/**
* @ brief Checks if a ini entry is changed by comparing value before and after
*/
class IniChangedChecker {
public :
IniChangedChecker ( const char * sectionName , const char * keyName )
{
this - > sectionName_ = sectionName ;
this - > keyName_ = keyName ;
oldValue_ = GetValue ( ) ;
if ( ! oldValue_ ) {
// No entry found in original ini => new entry => changed
IniChanged = true ;
}
}
~ IniChangedChecker ( )
{
auto newValue = GetValue ( ) ;
if ( oldValue_ ! = newValue )
IniChanged = true ;
}
private :
std : : optional < std : : string > GetValue ( )
{
std : : list < CSimpleIni : : Entry > values ;
if ( ! GetIni ( ) . GetAllValues ( sectionName_ , keyName_ , values ) )
return std : : nullopt ;
std : : string ret ;
for ( auto & entry : values ) {
if ( entry . pItem ! = nullptr )
ret . append ( entry . pItem ) ;
ret . append ( " \n " ) ;
}
return ret ;
}
std : : optional < std : : string > oldValue_ ;
const char * sectionName_ ;
const char * keyName_ ;
} ;
int GetIniInt ( const char * keyname , const char * valuename , int defaultValue )
{
return GetIni ( ) . GetLongValue ( keyname , valuename , defaultValue ) ;
}
bool GetIniBool ( const char * sectionName , const char * keyName , bool defaultValue )
{
return GetIni ( ) . GetBoolValue ( sectionName , keyName , defaultValue ) ;
}
float GetIniFloat ( const char * sectionName , const char * keyName , float defaultValue )
{
return ( float ) GetIni ( ) . GetDoubleValue ( sectionName , keyName , defaultValue ) ;
}
bool GetIniStringVector ( const char * sectionName , const char * keyName , std : : vector < std : : string > & stringValues )
{
std : : list < CSimpleIni : : Entry > values ;
if ( ! GetIni ( ) . GetAllValues ( sectionName , keyName , values ) ) {
return false ;
}
for ( auto & entry : values ) {
stringValues . emplace_back ( entry . pItem ) ;
}
return true ;
}
void SetIniValue ( const char * keyname , const char * valuename , int value )
{
IniChangedChecker changedChecker ( keyname , valuename ) ;
GetIni ( ) . SetLongValue ( keyname , valuename , value , nullptr , false , true ) ;
}
void SetIniValue ( const char * keyname , const char * valuename , std : : uint8_t value )
{
IniChangedChecker changedChecker ( keyname , valuename ) ;
GetIni ( ) . SetLongValue ( keyname , valuename , value , nullptr , false , true ) ;
}
void SetIniValue ( const char * keyname , const char * valuename , std : : uint32_t value )
{
IniChangedChecker changedChecker ( keyname , valuename ) ;
GetIni ( ) . SetLongValue ( keyname , valuename , value , nullptr , false , true ) ;
}
void SetIniValue ( const char * keyname , const char * valuename , bool value )
{
IniChangedChecker changedChecker ( keyname , valuename ) ;
GetIni ( ) . SetLongValue ( keyname , valuename , value ? 1 : 0 , nullptr , false , true ) ;
}
void SetIniValue ( const char * keyname , const char * valuename , float value )
{
IniChangedChecker changedChecker ( keyname , valuename ) ;
GetIni ( ) . SetDoubleValue ( keyname , valuename , value , nullptr , true ) ;
}
void SetIniValue ( const char * keyname , const char * valuename , const std : : vector < std : : string > & stringValues )
{
IniChangedChecker changedChecker ( keyname , valuename ) ;
bool firstSet = true ;
for ( auto & value : stringValues ) {
GetIni ( ) . SetValue ( keyname , valuename , value . c_str ( ) , nullptr , firstSet ) ;
firstSet = false ;
}
if ( firstSet )
GetIni ( ) . SetValue ( keyname , valuename , " " , nullptr , true ) ;
}
void SaveIni ( )
{
if ( ! IniChanged )
return ;
auto iniPath = GetIniPath ( ) ;
auto stream = CreateFileStream ( iniPath . c_str ( ) , std : : fstream : : out | std : : fstream : : trunc | std : : fstream : : binary ) ;
GetIni ( ) . Save ( * stream , true ) ;
IniChanged = false ;
}
# if SDL_VERSION_ATLEAST(2, 0, 0)
bool HardwareCursorSupported ( )
{
SDL_version v ;
SDL_GetVersion ( & v ) ;
return SDL_VERSIONNUM ( v . major , v . minor , v . patch ) > = SDL_VERSIONNUM ( 2 , 0 , 12 ) ;
}
bool HardwareCursorDefault ( )
{
# if defined(__ANDROID__) || defined(TARGET_OS_IPHONE)
// See https://github.com/diasurgical/devilutionX/issues/2502
return false ;
# else
return HardwareCursorSupported ( ) ;
# endif
}
# endif
void OptionGrabInputChanged ( )
{
# ifdef USE_SDL1
SDL_WM_GrabInput ( * sgOptions . Gameplay . grabInput ? SDL_GRAB_ON : SDL_GRAB_OFF ) ;
# else
if ( ghMainWnd ! = nullptr )
SDL_SetWindowGrab ( ghMainWnd , * sgOptions . Gameplay . grabInput ? SDL_TRUE : SDL_FALSE ) ;
# endif
}
void OptionExperienceBarChanged ( )
{
if ( ! gbRunGame )
return ;
if ( * sgOptions . Gameplay . experienceBar )
InitXPBar ( ) ;
else
FreeXPBar ( ) ;
}
void OptionEnemyHealthBarChanged ( )
{
if ( ! gbRunGame )
return ;
if ( * sgOptions . Gameplay . enemyHealthBar )
InitMonsterHealthBar ( ) ;
else
FreeMonsterHealthBar ( ) ;
}
void OptionShowFPSChanged ( )
{
if ( * sgOptions . Graphics . showFPS )
EnableFrameCount ( ) ;
else
frameflag = false ;
}
void OptionLanguageCodeChanged ( )
{
LanguageInitialize ( ) ;
init_language_archives ( ) ;
}
void OptionGameModeChanged ( )
{
gbIsHellfire = * sgOptions . StartUp . gameMode = = StartUpGameMode : : Hellfire ;
discord_manager : : UpdateMenu ( true ) ;
}
void OptionSharewareChanged ( )
{
gbIsSpawn = * sgOptions . StartUp . shareware ;
}
void OptionAudioChanged ( )
{
effects_cleanup_sfx ( ) ;
music_stop ( ) ;
snd_deinit ( ) ;
snd_init ( ) ;
music_start ( TMUSIC_INTRO ) ;
if ( gbRunGame )
sound_init ( ) ;
else
ui_sound_init ( ) ;
}
} // namespace
void SetIniValue ( const char * sectionName , const char * keyName , const char * value , int len )
{
IniChangedChecker changedChecker ( sectionName , keyName ) ;
auto & ini = GetIni ( ) ;
std : : string stringValue ( value , len ! = 0 ? len : strlen ( value ) ) ;
ini . SetValue ( sectionName , keyName , stringValue . c_str ( ) , nullptr , true ) ;
}
bool GetIniValue ( const char * sectionName , const char * keyName , char * string , int stringSize , const char * defaultString )
{
const char * value = GetIni ( ) . GetValue ( sectionName , keyName ) ;
if ( value = = nullptr ) {
CopyUtf8 ( string , defaultString , stringSize ) ;
return false ;
}
CopyUtf8 ( string , value , stringSize ) ;
return true ;
}
/** Game options */
Options sgOptions ;
bool sbWasOptionsLoaded = false ;
void LoadOptions ( )
{
for ( OptionCategoryBase * pCategory : sgOptions . GetCategories ( ) ) {
for ( OptionEntryBase * pEntry : pCategory - > GetEntries ( ) ) {
pEntry - > LoadFromIni ( pCategory - > GetKey ( ) ) ;
}
}
sgOptions . Diablo . lastSinglePlayerHero = GetIniInt ( " Diablo " , " LastSinglePlayerHero " , 0 ) ;
sgOptions . Diablo . lastMultiplayerHero = GetIniInt ( " Diablo " , " LastMultiplayerHero " , 0 ) ;
sgOptions . Hellfire . lastSinglePlayerHero = GetIniInt ( " Hellfire " , " LastSinglePlayerHero " , 0 ) ;
sgOptions . Hellfire . lastMultiplayerHero = GetIniInt ( " Hellfire " , " LastMultiplayerHero " , 0 ) ;
GetIniValue ( " Hellfire " , " SItem " , sgOptions . Hellfire . szItem , sizeof ( sgOptions . Hellfire . szItem ) , " " ) ;
sgOptions . Audio . nSoundVolume = GetIniInt ( " Audio " , " Sound Volume " , VOLUME_MAX ) ;
sgOptions . Audio . nMusicVolume = GetIniInt ( " Audio " , " Music Volume " , VOLUME_MAX ) ;
sgOptions . Graphics . nGammaCorrection = GetIniInt ( " Graphics " , " Gamma Correction " , 100 ) ;
sgOptions . Gameplay . nTickRate = GetIniInt ( " Game " , " Speed " , 20 ) ;
GetIniValue ( " Network " , " Bind Address " , sgOptions . Network . szBindAddress , sizeof ( sgOptions . Network . szBindAddress ) , " 0.0.0.0 " ) ;
sgOptions . Network . nPort = GetIniInt ( " Network " , " Port " , 6112 ) ;
GetIniValue ( " Network " , " Previous Host " , sgOptions . Network . szPreviousHost , sizeof ( sgOptions . Network . szPreviousHost ) , " " ) ;
for ( size_t i = 0 ; i < QUICK_MESSAGE_OPTIONS ; i + + )
GetIniStringVector ( " NetMsg " , QuickMessages [ i ] . key , sgOptions . Chat . szHotKeyMsgs [ i ] ) ;
GetIniValue ( " Controller " , " Mapping " , sgOptions . Controller . szMapping , sizeof ( sgOptions . Controller . szMapping ) , " " ) ;
sgOptions . Controller . bSwapShoulderButtonMode = GetIniBool ( " Controller " , " Swap Shoulder Button Mode " , false ) ;
sgOptions . Controller . bDpadHotkeys = GetIniBool ( " Controller " , " Dpad Hotkeys " , false ) ;
sgOptions . Controller . fDeadzone = GetIniFloat ( " Controller " , " deadzone " , 0.07F ) ;
# ifdef __vita__
sgOptions . Controller . bRearTouch = GetIniBool ( " Controller " , " Enable Rear Touchpad " , true ) ;
# endif
keymapper . Load ( ) ;
if ( demo : : IsRunning ( ) )
demo : : OverrideOptions ( ) ;
sbWasOptionsLoaded = true ;
}
void SaveOptions ( )
{
for ( OptionCategoryBase * pCategory : sgOptions . GetCategories ( ) ) {
for ( OptionEntryBase * pEntry : pCategory - > GetEntries ( ) ) {
pEntry - > SaveToIni ( pCategory - > GetKey ( ) ) ;
}
}
SetIniValue ( " Diablo " , " LastSinglePlayerHero " , sgOptions . Diablo . lastSinglePlayerHero ) ;
SetIniValue ( " Diablo " , " LastMultiplayerHero " , sgOptions . Diablo . lastMultiplayerHero ) ;
SetIniValue ( " Hellfire " , " SItem " , sgOptions . Hellfire . szItem ) ;
SetIniValue ( " Hellfire " , " LastSinglePlayerHero " , sgOptions . Hellfire . lastSinglePlayerHero ) ;
SetIniValue ( " Hellfire " , " LastMultiplayerHero " , sgOptions . Hellfire . lastMultiplayerHero ) ;
SetIniValue ( " Audio " , " Sound Volume " , sgOptions . Audio . nSoundVolume ) ;
SetIniValue ( " Audio " , " Music Volume " , sgOptions . Audio . nMusicVolume ) ;
SetIniValue ( " Graphics " , " Gamma Correction " , sgOptions . Graphics . nGammaCorrection ) ;
SetIniValue ( " Game " , " Speed " , sgOptions . Gameplay . nTickRate ) ;
SetIniValue ( " Network " , " Bind Address " , sgOptions . Network . szBindAddress ) ;
SetIniValue ( " Network " , " Port " , sgOptions . Network . nPort ) ;
SetIniValue ( " Network " , " Previous Host " , sgOptions . Network . szPreviousHost ) ;
for ( size_t i = 0 ; i < QUICK_MESSAGE_OPTIONS ; i + + )
SetIniValue ( " NetMsg " , QuickMessages [ i ] . key , sgOptions . Chat . szHotKeyMsgs [ i ] ) ;
SetIniValue ( " Controller " , " Mapping " , sgOptions . Controller . szMapping ) ;
SetIniValue ( " Controller " , " Swap Shoulder Button Mode " , sgOptions . Controller . bSwapShoulderButtonMode ) ;
SetIniValue ( " Controller " , " Dpad Hotkeys " , sgOptions . Controller . bDpadHotkeys ) ;
SetIniValue ( " Controller " , " deadzone " , sgOptions . Controller . fDeadzone ) ;
# ifdef __vita__
SetIniValue ( " Controller " , " Enable Rear Touchpad " , sgOptions . Controller . bRearTouch ) ;
# endif
keymapper . Save ( ) ;
SaveIni ( ) ;
}
string_view OptionEntryBase : : GetName ( ) const
{
return _ ( name . data ( ) ) ;
}
string_view OptionEntryBase : : GetDescription ( ) const
{
return _ ( description . data ( ) ) ;
}
OptionEntryFlags OptionEntryBase : : GetFlags ( ) const
{
return flags ;
}
void OptionEntryBase : : SetValueChangedCallback ( std : : function < void ( ) > callback )
{
this - > callback = callback ;
}
void OptionEntryBase : : NotifyValueChanged ( )
{
if ( callback )
callback ( ) ;
}
void OptionEntryBoolean : : LoadFromIni ( string_view category )
{
value = GetIniBool ( category . data ( ) , key . data ( ) , defaultValue ) ;
}
void OptionEntryBoolean : : SaveToIni ( string_view category ) const
{
SetIniValue ( category . data ( ) , key . data ( ) , value ) ;
}
void OptionEntryBoolean : : SetValue ( bool value )
{
this - > value = value ;
this - > NotifyValueChanged ( ) ;
}
OptionEntryType OptionEntryBoolean : : GetType ( ) const
{
return OptionEntryType : : Boolean ;
}
string_view OptionEntryBoolean : : GetValueDescription ( ) const
{
return value ? _ ( " ON " ) : _ ( " OFF " ) ;
}
OptionEntryType OptionEntryListBase : : GetType ( ) const
{
return OptionEntryType : : List ;
}
string_view OptionEntryListBase : : GetValueDescription ( ) const
{
return GetListDescription ( GetActiveListIndex ( ) ) ;
}
void OptionEntryEnumBase : : LoadFromIni ( string_view category )
{
value = GetIniInt ( category . data ( ) , key . data ( ) , defaultValue ) ;
}
void OptionEntryEnumBase : : SaveToIni ( string_view category ) const
{
SetIniValue ( category . data ( ) , key . data ( ) , value ) ;
}
void OptionEntryEnumBase : : SetValueInternal ( int value )
{
this - > value = value ;
this - > NotifyValueChanged ( ) ;
}
void OptionEntryEnumBase : : AddEntry ( int value , string_view name )
{
entryValues . push_back ( value ) ;
entryNames . push_back ( name ) ;
}
size_t OptionEntryEnumBase : : GetListSize ( ) const
{
return entryValues . size ( ) ;
}
string_view OptionEntryEnumBase : : GetListDescription ( size_t index ) const
{
return _ ( entryNames [ index ] . data ( ) ) ;
}
size_t OptionEntryEnumBase : : GetActiveListIndex ( ) const
{
auto iterator = std : : find ( entryValues . begin ( ) , entryValues . end ( ) , value ) ;
if ( iterator = = entryValues . end ( ) )
return 0 ;
return std : : distance ( entryValues . begin ( ) , iterator ) ;
}
void OptionEntryEnumBase : : SetActiveListIndex ( size_t index )
{
this - > value = entryValues [ index ] ;
this - > NotifyValueChanged ( ) ;
}
void OptionEntryIntBase : : LoadFromIni ( string_view category )
{
value = GetIniInt ( category . data ( ) , key . data ( ) , defaultValue ) ;
if ( std : : find ( entryValues . begin ( ) , entryValues . end ( ) , value ) = = entryValues . end ( ) ) {
entryValues . push_back ( value ) ;
std : : sort ( entryValues . begin ( ) , entryValues . end ( ) ) ;
entryNames . clear ( ) ;
}
}
void OptionEntryIntBase : : SaveToIni ( string_view category ) const
{
SetIniValue ( category . data ( ) , key . data ( ) , value ) ;
}
void OptionEntryIntBase : : SetValueInternal ( int value )
{
this - > value = value ;
this - > NotifyValueChanged ( ) ;
}
void OptionEntryIntBase : : AddEntry ( int value )
{
entryValues . push_back ( value ) ;
}
size_t OptionEntryIntBase : : GetListSize ( ) const
{
return entryValues . size ( ) ;
}
string_view OptionEntryIntBase : : GetListDescription ( size_t index ) const
{
if ( entryNames . empty ( ) ) {
for ( auto value : entryValues ) {
entryNames . push_back ( fmt : : format ( " {} " , value ) ) ;
}
}
return entryNames [ index ] . data ( ) ;
}
size_t OptionEntryIntBase : : GetActiveListIndex ( ) const
{
auto iterator = std : : find ( entryValues . begin ( ) , entryValues . end ( ) , value ) ;
if ( iterator = = entryValues . end ( ) )
return 0 ;
return std : : distance ( entryValues . begin ( ) , iterator ) ;
}
void OptionEntryIntBase : : SetActiveListIndex ( size_t index )
{
this - > value = entryValues [ index ] ;
this - > NotifyValueChanged ( ) ;
}
OptionCategoryBase : : OptionCategoryBase ( string_view key , string_view name , string_view description )
: key ( key )
, name ( name )
, description ( description )
{
}
string_view OptionCategoryBase : : GetKey ( ) const
{
return key ;
}
string_view OptionCategoryBase : : GetName ( ) const
{
return _ ( name . data ( ) ) ;
}
string_view OptionCategoryBase : : GetDescription ( ) const
{
return _ ( description . data ( ) ) ;
}
StartUpOptions : : StartUpOptions ( )
: OptionCategoryBase ( " StartUp " , N_ ( " Start Up " ) , N_ ( " Start Up Settings " ) )
, gameMode ( " Game " , OptionEntryFlags : : NeedHellfireMpq | OptionEntryFlags : : RecreateUI , N_ ( " Game Mode " ) , N_ ( " Play Diablo or Hellfire. " ) , StartUpGameMode : : Ask ,
{
{ StartUpGameMode : : Diablo , N_ ( " Diablo " ) } ,
// Ask is missing, cause we want to hide it from UI-Settings.
{ StartUpGameMode : : Hellfire , N_ ( " Hellfire " ) } ,
} )
, shareware ( " Shareware " , OptionEntryFlags : : NeedDiabloMpq | OptionEntryFlags : : RecreateUI , N_ ( " Restrict to Shareware " ) , N_ ( " Makes the game compatible with the demo. Enables multiplayer with friends who don't own a full copy of Diablo. " ) , false )
, diabloIntro ( " Diablo Intro " , OptionEntryFlags : : OnlyDiablo , N_ ( " Intro " ) , N_ ( " Shown Intro cinematic. " ) , StartUpIntro : : Once ,
{
{ StartUpIntro : : Off , N_ ( " OFF " ) } ,
// Once is missing, cause we want to hide it from UI-Settings.
{ StartUpIntro : : On , N_ ( " ON " ) } ,
} )
, hellfireIntro ( " Hellfire Intro " , OptionEntryFlags : : OnlyHellfire , N_ ( " Intro " ) , N_ ( " Shown Intro cinematic. " ) , StartUpIntro : : Once ,
{
{ StartUpIntro : : Off , N_ ( " OFF " ) } ,
// Once is missing, cause we want to hide it from UI-Settings.
{ StartUpIntro : : On , N_ ( " ON " ) } ,
} )
, splash ( " Splash " , OptionEntryFlags : : None , N_ ( " Splash " ) , N_ ( " Shown splash screen. " ) , StartUpSplash : : LogoAndTitleDialog ,
{
{ StartUpSplash : : LogoAndTitleDialog , N_ ( " Logo and Title Screen " ) } ,
{ StartUpSplash : : TitleDialog , N_ ( " Title Screen " ) } ,
{ StartUpSplash : : None , N_ ( " None " ) } ,
} )
{
gameMode . SetValueChangedCallback ( OptionGameModeChanged ) ;
shareware . SetValueChangedCallback ( OptionSharewareChanged ) ;
}
std : : vector < OptionEntryBase * > StartUpOptions : : GetEntries ( )
{
return {
& gameMode ,
& shareware ,
& diabloIntro ,
& hellfireIntro ,
& splash ,
} ;
}
DiabloOptions : : DiabloOptions ( )
: OptionCategoryBase ( " Diablo " , N_ ( " Diablo " ) , N_ ( " Diablo specific Settings " ) )
{
}
std : : vector < OptionEntryBase * > DiabloOptions : : GetEntries ( )
{
return { } ;
}
HellfireOptions : : HellfireOptions ( )
: OptionCategoryBase ( " Hellfire " , N_ ( " Hellfire " ) , N_ ( " Hellfire specific Settings " ) )
{
}
std : : vector < OptionEntryBase * > HellfireOptions : : GetEntries ( )
{
return { } ;
}
AudioOptions : : AudioOptions ( )
: OptionCategoryBase ( " Audio " , N_ ( " Audio " ) , N_ ( " Audio Settings " ) )
, walkingSound ( " Walking Sound " , OptionEntryFlags : : None , N_ ( " Walking Sound " ) , N_ ( " Player emits sound when walking. " ) , true )
, autoEquipSound ( " Auto Equip Sound " , OptionEntryFlags : : None , N_ ( " Auto Equip Sound " ) , N_ ( " Automatically equipping items on pickup emits the equipment sound. " ) , false )
, itemPickupSound ( " Item Pickup Sound " , OptionEntryFlags : : None , N_ ( " Item Pickup Sound " ) , N_ ( " Picking up items emits the items pickup sound. " ) , false )
, sampleRate ( " Sample Rate " , OptionEntryFlags : : CantChangeInGame , N_ ( " Sample Rate " ) , N_ ( " Output sample rate (Hz). " ) , DEFAULT_AUDIO_SAMPLE_RATE , { 22050 , 44100 , 48000 } )
, channels ( " Channels " , OptionEntryFlags : : CantChangeInGame , N_ ( " Channels " ) , N_ ( " Number of output channels. " ) , DEFAULT_AUDIO_CHANNELS , { 1 , 2 } )
, bufferSize ( " Buffer Size " , OptionEntryFlags : : CantChangeInGame , N_ ( " Buffer Size " ) , N_ ( " Buffer size (number of frames per channel). " ) , DEFAULT_AUDIO_BUFFER_SIZE , { 1024 , 2048 , 5120 } )
, resamplingQuality ( " Resampling Quality " , OptionEntryFlags : : CantChangeInGame , N_ ( " Resampling Quality " ) , N_ ( " Quality of the resampler, from 0 (lowest) to 10 (highest). " ) , DEFAULT_AUDIO_RESAMPLING_QUALITY , { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 } )
{
sampleRate . SetValueChangedCallback ( OptionAudioChanged ) ;
channels . SetValueChangedCallback ( OptionAudioChanged ) ;
bufferSize . SetValueChangedCallback ( OptionAudioChanged ) ;
resamplingQuality . SetValueChangedCallback ( OptionAudioChanged ) ;
}
std : : vector < OptionEntryBase * > AudioOptions : : GetEntries ( )
{
return {
& walkingSound ,
& autoEquipSound ,
& itemPickupSound ,
& sampleRate ,
& channels ,
& bufferSize ,
& resamplingQuality ,
} ;
}
OptionEntryResolution : : OptionEntryResolution ( )
: OptionEntryListBase ( " " , OptionEntryFlags : : CantChangeInGame | OptionEntryFlags : : RecreateUI , N_ ( " Resolution " ) , N_ ( " Affect the game's internal resolution and determine your view area. Note: This can differ from screen resolution, when Upscaling, Integer Scaling or Fit to Screen is used. " ) )
{
}
void OptionEntryResolution : : LoadFromIni ( string_view category )
{
size = { GetIniInt ( category . data ( ) , " Width " , DEFAULT_WIDTH ) , GetIniInt ( category . data ( ) , " Height " , DEFAULT_HEIGHT ) } ;
}
void OptionEntryResolution : : SaveToIni ( string_view category ) const
{
SetIniValue ( category . data ( ) , " Width " , size . width ) ;
SetIniValue ( category . data ( ) , " Height " , size . height ) ;
}
void OptionEntryResolution : : CheckResolutionsAreInitialized ( ) const
{
if ( ! resolutions . empty ( ) )
return ;
std : : vector < Size > sizes ;
float scaleFactor = GetDpiScalingFactor ( ) ;
// Add resolutions
# ifdef USE_SDL1
auto * modes = SDL_ListModes ( nullptr , SDL_FULLSCREEN | SDL_HWPALETTE ) ;
// SDL_ListModes returns -1 if any resolution is allowed (for example returned on 3DS)
if ( modes ! = nullptr & & modes ! = ( SDL_Rect * * ) - 1 ) {
for ( size_t i = 0 ; modes [ i ] ! = nullptr ; i + + ) {
if ( modes [ i ] - > w < modes [ i ] - > h ) {
std : : swap ( modes [ i ] - > w , modes [ i ] - > h ) ;
}
sizes . emplace_back ( Size {
static_cast < int > ( modes [ i ] - > w * scaleFactor ) ,
static_cast < int > ( modes [ i ] - > h * scaleFactor ) } ) ;
}
}
# else
int displayModeCount = SDL_GetNumDisplayModes ( 0 ) ;
for ( int i = 0 ; i < displayModeCount ; i + + ) {
SDL_DisplayMode mode ;
if ( SDL_GetDisplayMode ( 0 , i , & mode ) ! = 0 ) {
ErrSdl ( ) ;
}
if ( mode . w < mode . h ) {
std : : swap ( mode . w , mode . h ) ;
}
sizes . emplace_back ( Size {
static_cast < int > ( mode . w * scaleFactor ) ,
static_cast < int > ( mode . h * scaleFactor ) } ) ;
}
# endif
// Ensures that the ini specified resolution is present in resolution list even if it doesn't match a monitor resolution (for example if played in window mode)
sizes . push_back ( this - > size ) ;
// Ensures that the vanilla/default resolution is always present
sizes . emplace_back ( Size { DEFAULT_WIDTH , DEFAULT_HEIGHT } ) ;
// Sort by width then by height
std : : sort ( sizes . begin ( ) , sizes . end ( ) ,
[ ] ( const Size & x , const Size & y ) - > bool {
if ( x . width = = y . width )
return x . height > y . height ;
return x . width > y . width ;
} ) ;
// Remove duplicate entries
sizes . erase ( std : : unique ( sizes . begin ( ) , sizes . end ( ) ) , sizes . end ( ) ) ;
for ( auto & size : sizes ) {
resolutions . emplace_back ( size , fmt : : format ( " {}x{} " , size . width , size . height ) ) ;
}
}
size_t OptionEntryResolution : : GetListSize ( ) const
{
CheckResolutionsAreInitialized ( ) ;
return resolutions . size ( ) ;
}
string_view OptionEntryResolution : : GetListDescription ( size_t index ) const
{
CheckResolutionsAreInitialized ( ) ;
return resolutions [ index ] . second ;
}
size_t OptionEntryResolution : : GetActiveListIndex ( ) const
{
CheckResolutionsAreInitialized ( ) ;
auto found = std : : find_if ( resolutions . begin ( ) , resolutions . end ( ) , [ this ] ( const auto & x ) { return x . first = = this - > size ; } ) ;
if ( found = = resolutions . end ( ) )
return 0 ;
return std : : distance ( resolutions . begin ( ) , found ) ;
}
void OptionEntryResolution : : SetActiveListIndex ( size_t index )
{
size = resolutions [ index ] . first ;
NotifyValueChanged ( ) ;
}
GraphicsOptions : : GraphicsOptions ( )
: OptionCategoryBase ( " Graphics " , N_ ( " Graphics " ) , N_ ( " Graphics Settings " ) )
, fullscreen ( " Fullscreen " , OnlyIfSupportsWindowed | OptionEntryFlags : : CantChangeInGame | OptionEntryFlags : : RecreateUI , N_ ( " Fullscreen " ) , N_ ( " Display the game in windowed or fullscreen mode. " ) , true )
# if !defined(USE_SDL1) || defined(__3DS__)
, fitToScreen ( " Fit to Screen " , OptionEntryFlags : : CantChangeInGame | OptionEntryFlags : : RecreateUI , N_ ( " Fit to Screen " ) , N_ ( " Automatically adjust the game window to your current desktop screen aspect ratio and resolution. " ) , true )
# endif
# ifndef USE_SDL1
, upscale ( " Upscale " , OnlyIfNoImplicitRenderer | OptionEntryFlags : : CantChangeInGame | OptionEntryFlags : : RecreateUI , N_ ( " Upscale " ) , N_ ( " Enables image scaling from the game resolution to your monitor resolution. Prevents changing the monitor resolution and allows window resizing. " ) , true )
, scaleQuality ( " Scaling Quality " , OptionEntryFlags : : None , N_ ( " Scaling Quality " ) , N_ ( " Enables optional filters to the output image when upscaling. " ) , ScalingQuality : : AnisotropicFiltering ,
{
{ ScalingQuality : : NearestPixel , N_ ( " Nearest Pixel " ) } ,
{ ScalingQuality : : BilinearFiltering , N_ ( " Bilinear " ) } ,
{ ScalingQuality : : AnisotropicFiltering , N_ ( " Anisotropic " ) } ,
} )
, integerScaling ( " Integer Scaling " , OptionEntryFlags : : CantChangeInGame | OptionEntryFlags : : RecreateUI , N_ ( " Integer Scaling " ) , N_ ( " Scales the image using whole number pixel ratio. " ) , false )
, vSync ( " Vertical Sync " , OptionEntryFlags : : RecreateUI , N_ ( " Vertical Sync " ) , N_ ( " Forces waiting for Vertical Sync. Prevents tearing effect when drawing a frame. Disabling it can help with mouse lag on some systems. " ) , true )
# endif
, blendedTransparancy ( " Blended Transparency " , OptionEntryFlags : : CantChangeInGame , N_ ( " Blended Transparency " ) , N_ ( " Enables uniform transparency mode. This setting affects the transparency on walls, game text menus, and boxes. If disabled will default to old checkerboard transparency. " ) , true )
, colorCycling ( " Color Cycling " , OptionEntryFlags : : None , N_ ( " Color Cycling " ) , N_ ( " Color cycling effect used for water, lava, and acid animation. " ) , true )
# if SDL_VERSION_ATLEAST(2, 0, 0)
, hardwareCursor ( " Hardware Cursor " , OptionEntryFlags : : CantChangeInGame | OptionEntryFlags : : RecreateUI | ( HardwareCursorSupported ( ) ? OptionEntryFlags : : None : OptionEntryFlags : : Invisible ) , N_ ( " Hardware Cursor " ) , N_ ( " Use a hardware cursor " ) , HardwareCursorDefault ( ) )
, hardwareCursorForItems ( " Hardware Cursor For Items " , OptionEntryFlags : : CantChangeInGame | ( HardwareCursorSupported ( ) ? OptionEntryFlags : : None : OptionEntryFlags : : Invisible ) , N_ ( " Hardware Cursor For Items " ) , N_ ( " Use a hardware cursor for items. " ) , false )
, hardwareCursorMaxSize ( " Hardware Cursor Maximum Size " , OptionEntryFlags : : CantChangeInGame | OptionEntryFlags : : RecreateUI | ( HardwareCursorSupported ( ) ? OptionEntryFlags : : None : OptionEntryFlags : : Invisible ) , N_ ( " Hardware Cursor Maximum Size " ) , N_ ( " Maximum width / height for the hardware cursor. Larger cursors fall back to software. " ) , 128 , { 0 , 64 , 128 , 256 , 512 } )
# endif
, limitFPS ( " FPS Limiter " , OptionEntryFlags : : None , N_ ( " FPS Limiter " ) , N_ ( " FPS is limited to avoid high CPU load. Limit considers refresh rate. " ) , true )
, showFPS ( " Show FPS " , OptionEntryFlags : : None , N_ ( " Show FPS " ) , N_ ( " Displays the FPS in the upper left corner of the screen. " ) , true )
{
resolution . SetValueChangedCallback ( ResizeWindow ) ;
fullscreen . SetValueChangedCallback ( SetFullscreenMode ) ;
# if !defined(USE_SDL1) || defined(__3DS__)
fitToScreen . SetValueChangedCallback ( ResizeWindow ) ;
# endif
# ifndef USE_SDL1
upscale . SetValueChangedCallback ( ResizeWindow ) ;
scaleQuality . SetValueChangedCallback ( ReinitializeTexture ) ;
integerScaling . SetValueChangedCallback ( ReinitializeIntegerScale ) ;
vSync . SetValueChangedCallback ( ReinitializeRenderer ) ;
# endif
showFPS . SetValueChangedCallback ( OptionShowFPSChanged ) ;
}
std : : vector < OptionEntryBase * > GraphicsOptions : : GetEntries ( )
{
// clang-format off
return {
& resolution ,
# ifndef __vita__
& fullscreen ,
# endif
# if !defined(USE_SDL1) || defined(__3DS__)
& fitToScreen ,
# endif
# ifndef USE_SDL1
& upscale ,
& scaleQuality ,
& integerScaling ,
& vSync ,
# endif
& blendedTransparancy ,
& colorCycling ,
# if SDL_VERSION_ATLEAST(2, 0, 0)
& hardwareCursor ,
& hardwareCursorForItems ,
& hardwareCursorMaxSize ,
# endif
& limitFPS ,
& showFPS ,
} ;
// clang-format on
}
GameplayOptions : : GameplayOptions ( )
: OptionCategoryBase ( " Game " , N_ ( " Gameplay " ) , N_ ( " Gameplay Settings " ) )
, runInTown ( " Run in Town " , OptionEntryFlags : : CantChangeInMultiPlayer , N_ ( " Run in Town " ) , N_ ( " Enable jogging/fast walking in town for Diablo and Hellfire. This option was introduced in the expansion. " ) , false )
, grabInput ( " Grab Input " , OptionEntryFlags : : None , N_ ( " Grab Input " ) , N_ ( " When enabled mouse is locked to the game window. " ) , false )
, theoQuest ( " Theo Quest " , OptionEntryFlags : : CantChangeInGame | OptionEntryFlags : : OnlyHellfire , N_ ( " Theo Quest " ) , N_ ( " Enable Little Girl quest. " ) , false )
, cowQuest ( " Cow Quest " , OptionEntryFlags : : CantChangeInGame | OptionEntryFlags : : OnlyHellfire , N_ ( " Cow Quest " ) , N_ ( " Enable Jersey's quest. Lester the farmer is replaced by the Complete Nut. " ) , false )
, friendlyFire ( " Friendly Fire " , OptionEntryFlags : : CantChangeInMultiPlayer , N_ ( " Friendly Fire " ) , N_ ( " Allow arrow/spell damage between players in multiplayer even when the friendly mode is on. " ) , true )
, testBard ( " Test Bard " , OptionEntryFlags : : CantChangeInGame , N_ ( " Test Bard " ) , N_ ( " Force the Bard character type to appear in the hero selection menu. " ) , false )
, testBarbarian ( " Test Barbarian " , OptionEntryFlags : : CantChangeInGame , N_ ( " Test Barbarian " ) , N_ ( " Force the Barbarian character type to appear in the hero selection menu. " ) , false )
, experienceBar ( " Experience Bar " , OptionEntryFlags : : None , N_ ( " Experience Bar " ) , N_ ( " Experience Bar is added to the UI at the bottom of the screen. " ) , false )
, enemyHealthBar ( " Enemy Health Bar " , OptionEntryFlags : : None , N_ ( " Enemy Health Bar " ) , N_ ( " Enemy Health Bar is displayed at the top of the screen. " ) , false )
, autoGoldPickup ( " Auto Gold Pickup " , OptionEntryFlags : : None , N_ ( " Auto Gold Pickup " ) , N_ ( " Gold is automatically collected when in close proximity to the player. " ) , false )
, autoElixirPickup ( " Auto Elixir Pickup " , OptionEntryFlags : : None , N_ ( " Auto Elixir Pickup " ) , N_ ( " Elixirs are automatically collected when in close proximity to the player. " ) , false )
, autoPickupInTown ( " Auto Pickup in Town " , OptionEntryFlags : : None , N_ ( " Auto Pickup in Town " ) , N_ ( " Automatically pickup items in town. " ) , false )
, adriaRefillsMana ( " Adria Refills Mana " , OptionEntryFlags : : None , N_ ( " Adria Refills Mana " ) , N_ ( " Adria will refill your mana when you visit her shop. " ) , false )
, autoEquipWeapons ( " Auto Equip Weapons " , OptionEntryFlags : : None , N_ ( " Auto Equip Weapons " ) , N_ ( " Weapons will be automatically equipped on pickup or purchase if enabled. " ) , true )
, autoEquipArmor ( " Auto Equip Armor " , OptionEntryFlags : : None , N_ ( " Auto Equip Armor " ) , N_ ( " Armor will be automatically equipped on pickup or purchase if enabled. " ) , false )
, autoEquipHelms ( " Auto Equip Helms " , OptionEntryFlags : : None , N_ ( " Auto Equip Helms " ) , N_ ( " Helms will be automatically equipped on pickup or purchase if enabled. " ) , false )
, autoEquipShields ( " Auto Equip Shields " , OptionEntryFlags : : None , N_ ( " Auto Equip Shields " ) , N_ ( " Shields will be automatically equipped on pickup or purchase if enabled. " ) , false )
, autoEquipJewelry ( " Auto Equip Jewelry " , OptionEntryFlags : : None , N_ ( " Auto Equip Jewelry " ) , N_ ( " Jewelry will be automatically equipped on pickup or purchase if enabled. " ) , false )
, randomizeQuests ( " Randomize Quests " , OptionEntryFlags : : CantChangeInGame , N_ ( " Randomize Quests " ) , N_ ( " Randomly selecting available quests for new games. " ) , true )
, showMonsterType ( " Show Monster Type " , OptionEntryFlags : : None , N_ ( " Show Monster Type " ) , N_ ( " Hovering over a monster will display the type of monster in the description box in the UI. " ) , false )
, autoRefillBelt ( " Auto Refill Belt " , OptionEntryFlags : : None , N_ ( " Auto Refill Belt " ) , N_ ( " Refill belt from inventory when belt item is consumed. " ) , false )
, disableCripplingShrines ( " Disable Crippling Shrines " , OptionEntryFlags : : None , N_ ( " Disable Crippling Shrines " ) , N_ ( " When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines and Sacred Shrines are not able to be clicked on and labeled as disabled. " ) , false )
, quickCast ( " Quick Cast " , OptionEntryFlags : : None , N_ ( " Quick Cast " ) , N_ ( " Spell hotkeys instantly cast the spell, rather than switching the readied spell. " ) , false )
, numHealPotionPickup ( " Heal Potion Pickup " , OptionEntryFlags : : None , N_ ( " Heal Potion Pickup " ) , N_ ( " Number of Healing potions to pick up automatically. " ) , 0 , { 0 , 1 , 2 , 4 , 8 , 16 } )
, numFullHealPotionPickup ( " Full Heal Potion Pickup " , OptionEntryFlags : : None , N_ ( " Full Heal Potion Pickup " ) , N_ ( " Number of Full Healing potions to pick up automatically. " ) , 0 , { 0 , 1 , 2 , 4 , 8 , 16 } )
, numManaPotionPickup ( " Mana Potion Pickup " , OptionEntryFlags : : None , N_ ( " Mana Potion Pickup " ) , N_ ( " Number of Mana potions to pick up automatically. " ) , 0 , { 0 , 1 , 2 , 4 , 8 , 16 } )
, numFullManaPotionPickup ( " Full Mana Potion Pickup " , OptionEntryFlags : : None , N_ ( " Full Mana Potion Pickup " ) , N_ ( " Number of Full Mana potions to pick up automatically. " ) , 0 , { 0 , 1 , 2 , 4 , 8 , 16 } )
, numRejuPotionPickup ( " Rejuvenation Potion Pickup " , OptionEntryFlags : : None , N_ ( " Rejuvenation Potion Pickup " ) , N_ ( " Number of Rejuvenation potions to pick up automatically. " ) , 0 , { 0 , 1 , 2 , 4 , 8 , 16 } )
, numFullRejuPotionPickup ( " Full Rejuvenation Potion Pickup " , OptionEntryFlags : : None , N_ ( " Full Rejuvenation Potion Pickup " ) , N_ ( " Number of Full Rejuvenation potions to pick up automatically. " ) , 0 , { 0 , 1 , 2 , 4 , 8 , 16 } )
{
grabInput . SetValueChangedCallback ( OptionGrabInputChanged ) ;
experienceBar . SetValueChangedCallback ( OptionExperienceBarChanged ) ;
enemyHealthBar . SetValueChangedCallback ( OptionEnemyHealthBarChanged ) ;
}
std : : vector < OptionEntryBase * > GameplayOptions : : GetEntries ( )
{
return {
& runInTown ,
& grabInput ,
& theoQuest ,
& cowQuest ,
& friendlyFire ,
& testBard ,
& testBarbarian ,
& experienceBar ,
& enemyHealthBar ,
& autoGoldPickup ,
& autoElixirPickup ,
& autoPickupInTown ,
& adriaRefillsMana ,
& autoEquipWeapons ,
& autoEquipArmor ,
& autoEquipHelms ,
& autoEquipShields ,
& autoEquipJewelry ,
& randomizeQuests ,
& showMonsterType ,
& autoRefillBelt ,
& disableCripplingShrines ,
& quickCast ,
& numHealPotionPickup ,
& numFullHealPotionPickup ,
& numManaPotionPickup ,
& numFullManaPotionPickup ,
& numRejuPotionPickup ,
& numFullRejuPotionPickup ,
} ;
}
ControllerOptions : : ControllerOptions ( )
: OptionCategoryBase ( " Controller " , N_ ( " Controller " ) , N_ ( " Controller Settings " ) )
{
}
std : : vector < OptionEntryBase * > ControllerOptions : : GetEntries ( )
{
return { } ;
}
NetworkOptions : : NetworkOptions ( )
: OptionCategoryBase ( " Network " , N_ ( " Network " ) , N_ ( " Network Settings " ) )
{
}
std : : vector < OptionEntryBase * > NetworkOptions : : GetEntries ( )
{
return { } ;
}
ChatOptions : : ChatOptions ( )
: OptionCategoryBase ( " NetMsg " , N_ ( " Chat " ) , N_ ( " Chat Settings " ) )
{
}
std : : vector < OptionEntryBase * > ChatOptions : : GetEntries ( )
{
return { } ;
}
OptionEntryLanguageCode : : OptionEntryLanguageCode ( )
: OptionEntryListBase ( " Code " , OptionEntryFlags : : CantChangeInGame | OptionEntryFlags : : RecreateUI , N_ ( " Language " ) , N_ ( " Define what language to use in game. " ) )
{
}
void OptionEntryLanguageCode : : LoadFromIni ( string_view category )
{
if ( GetIniValue ( category . data ( ) , key . data ( ) , szCode , sizeof ( szCode ) ) ) {
if ( HasTranslation ( szCode ) ) {
// User preferred language is available
return ;
}
}
// Might be a first run or the user has attempted to load a translation that doesn't exist via manual ini edit. Try find a best fit from the platform locale information.
std : : vector < std : : string > locales { } ;
# ifdef __ANDROID__
JNIEnv * env = ( JNIEnv * ) SDL_AndroidGetJNIEnv ( ) ;
jobject activity = ( jobject ) SDL_AndroidGetActivity ( ) ;
jclass clazz ( env - > GetObjectClass ( activity ) ) ;
jmethodID method_id = env - > GetMethodID ( clazz , " getLocale " , " ()Ljava/lang/String; " ) ;
jstring jLocale = ( jstring ) env - > CallObjectMethod ( activity , method_id ) ;
const char * cLocale = env - > GetStringUTFChars ( jLocale , nullptr ) ;
locales . emplace_back ( cLocale ) ;
env - > ReleaseStringUTFChars ( jLocale , cLocale ) ;
env - > DeleteLocalRef ( jLocale ) ;
env - > DeleteLocalRef ( activity ) ;
env - > DeleteLocalRef ( clazz ) ;
# elif defined(__vita__)
int32_t language = SCE_SYSTEM_PARAM_LANG_ENGLISH_US ; // default to english
const char * vita_locales [ ] = {
" ja_JP " ,
" en_US " ,
" fr_FR " ,
" es_ES " ,
" de_DE " ,
" it_IT " ,
" nl_NL " ,
" pt_PT " ,
" ru_RU " ,
" ko_KR " ,
" zh_TW " ,
" zh_CN " ,
" fi_FI " ,
" sv_SE " ,
" da_DK " ,
" no_NO " ,
" pl_PL " ,
" pt_BR " ,
" en_GB " ,
" tr_TR " ,
} ;
SceAppUtilInitParam initParam ;
SceAppUtilBootParam bootParam ;
memset ( & initParam , 0 , sizeof ( SceAppUtilInitParam ) ) ;
memset ( & bootParam , 0 , sizeof ( SceAppUtilBootParam ) ) ;
sceAppUtilInit ( & initParam , & bootParam ) ;
sceAppUtilSystemParamGetInt ( SCE_SYSTEM_PARAM_ID_LANG , & language ) ;
if ( language < 0 | | language > SCE_SYSTEM_PARAM_LANG_TURKISH )
language = SCE_SYSTEM_PARAM_LANG_ENGLISH_US ; // default to english
locales . emplace_back ( vita_locales [ language ] ) ;
sceAppUtilShutdown ( ) ;
# elif defined(__3DS__)
locales . push_back ( n3ds : : GetLocale ( ) ) ;
# elif defined(_WIN32)
# if WINVER >= 0x0600
auto wideCharToUtf8 = [ ] ( PWSTR wideString ) {
char utf8Buffer [ 12 ] { } ;
// We only handle 5 character locales (lang2-region2), so don't bother reading past that. This does leave the resulting string unterminated but the buffer was zero initialised anyway.
WideCharToMultiByte ( CP_UTF8 , 0 , wideString , 5 , utf8Buffer , sizeof ( utf8Buffer ) , nullptr , nullptr ) ;
// GetUserDefaultLocaleName could return an ISO 639-2/T string (three letter language code) or even an arbitrary custom locale, however we only handle 639-1 (two letter language code) locale names when checking the fallback language.
// if a region is included in the locale do a simple transformation to the expected POSIX style.
std : : replace ( utf8Buffer , utf8Buffer + sizeof ( utf8Buffer ) , ' - ' , ' _ ' ) ;
return std : : move ( std : : string ( utf8Buffer ) ) ;
} ;
WCHAR localeBuffer [ LOCALE_NAME_MAX_LENGTH ] ;
if ( GetUserDefaultLocaleName ( localeBuffer , LOCALE_NAME_MAX_LENGTH ) ! = 0 ) {
// Found a user default locale convert from WIN32's default of UTF16 to UTF8 and add to the list
locales . push_back ( wideCharToUtf8 ( localeBuffer ) ) ;
}
ULONG numberOfLanguages ;
ULONG bufferSize = LOCALE_NAME_MAX_LENGTH ;
GetUserPreferredUILanguages ( MUI_LANGUAGE_NAME , & numberOfLanguages , localeBuffer , & bufferSize ) ;
PWSTR bufferOffset = localeBuffer ;
for ( unsigned i = 0 ; i < numberOfLanguages ; i + + ) {
// Found one (or more) user preferred UI language(s), add these to the list to check as well
locales . push_back ( wideCharToUtf8 ( bufferOffset ) ) ;
// GetUserPreferredUILanguages returns a null separated list of strings, need to increment past the null terminating the current string.
bufferOffset + = lstrlenW ( localeBuffer ) + 1 ;
}
# else
// Fallback method for older versions of windows, this is deprecated since Vista
char localeBuffer [ LOCALE_NAME_MAX_LENGTH ] ;
// Deliberately not using the unicode versions here as the information retrieved should be represented in ASCII/single byte UTF8 codepoints.
if ( GetLocaleInfoA ( LOCALE_USER_DEFAULT , LOCALE_SISO639LANGNAME , localeBuffer , LOCALE_NAME_MAX_LENGTH ) ! = 0 ) {
std : : string locale { localeBuffer } ;
if ( GetLocaleInfoA ( LOCALE_USER_DEFAULT , LOCALE_SISO3166CTRYNAME , localeBuffer , LOCALE_NAME_MAX_LENGTH ) ! = 0 ) {
locale . append ( " _ " ) ;
locale . append ( localeBuffer ) ;
}
locales . push_back ( std : : move ( locale ) ) ;
}
# endif
# else
locales . emplace_back ( std : : locale ( " " ) . name ( ) . substr ( 0 , 5 ) ) ;
# endif
// Insert non-regional locale codes after the last regional variation so we fallback to neutral translations if no regional translation exists that meets user preferences.
for ( auto localeIter = locales . rbegin ( ) ; localeIter ! = locales . rend ( ) ; localeIter + + ) {
auto regionSeparator = localeIter - > find ( ' _ ' ) ;
if ( regionSeparator ! = std : : string : : npos ) {
std : : string neutralLocale = localeIter - > substr ( 0 , regionSeparator ) ;
if ( std : : find ( locales . rbegin ( ) , localeIter , neutralLocale ) = = localeIter ) {
localeIter = std : : make_reverse_iterator ( locales . insert ( localeIter . base ( ) , neutralLocale ) ) ;
}
}
}
LogVerbose ( " Found {} user preferred locales " , locales ) ;
for ( const auto & locale : locales ) {
LogVerbose ( " Trying to load translation: {} " , locale ) ;
if ( HasTranslation ( locale ) ) {
LogVerbose ( " Best match locale: {} " , locale ) ;
CopyUtf8 ( szCode , locale , sizeof ( szCode ) ) ;
return ;
}
}
LogVerbose ( " No suitable translation found " ) ;
strcpy ( szCode , " en " ) ;
}
void OptionEntryLanguageCode : : SaveToIni ( string_view category ) const
{
SetIniValue ( category . data ( ) , key . data ( ) , szCode ) ;
}
void OptionEntryLanguageCode : : CheckLanguagesAreInitialized ( ) const
{
if ( ! languages . empty ( ) )
return ;
// Add well-known supported languages
languages . emplace_back ( " bg " , " Bulgarian " ) ;
languages . emplace_back ( " cs " , " Czech " ) ;
languages . emplace_back ( " da " , " Danish " ) ;
languages . emplace_back ( " de " , " German " ) ;
languages . emplace_back ( " en " , " English " ) ;
languages . emplace_back ( " es " , " Spanish " ) ;
languages . emplace_back ( " fr " , " French " ) ;
languages . emplace_back ( " ja " , " Japanese " ) ;
languages . emplace_back ( " hr " , " Croatian " ) ;
languages . emplace_back ( " it " , " Italian " ) ;
languages . emplace_back ( " ko_KR " , " Korean " ) ;
languages . emplace_back ( " pl " , " Polish " ) ;
languages . emplace_back ( " pt_BR " , " Portuguese (Brazil) " ) ;
languages . emplace_back ( " ro_RO " , " Romanian " ) ;
languages . emplace_back ( " ru " , " Russian " ) ;
languages . emplace_back ( " sv " , " Swedish " ) ;
languages . emplace_back ( " uk " , " Ukrainian " ) ;
languages . emplace_back ( " zh_CN " , " Simplified Chinese " ) ;
languages . emplace_back ( " zh_TW " , " Traditional Chinese " ) ;
// Ensures that the ini specified language is present in languages list even if unknown (for example if someone starts to translate a new language)
if ( std : : find_if ( languages . begin ( ) , languages . end ( ) , [ this ] ( const auto & x ) { return x . first = = this - > szCode ; } ) = = languages . end ( ) ) {
languages . emplace_back ( szCode , szCode ) ;
}
}
size_t OptionEntryLanguageCode : : GetListSize ( ) const
{
CheckLanguagesAreInitialized ( ) ;
return languages . size ( ) ;
}
string_view OptionEntryLanguageCode : : GetListDescription ( size_t index ) const
{
CheckLanguagesAreInitialized ( ) ;
return languages [ index ] . second ;
}
size_t OptionEntryLanguageCode : : GetActiveListIndex ( ) const
{
CheckLanguagesAreInitialized ( ) ;
auto found = std : : find_if ( languages . begin ( ) , languages . end ( ) , [ this ] ( const auto & x ) { return x . first = = this - > szCode ; } ) ;
if ( found = = languages . end ( ) )
return 0 ;
return std : : distance ( languages . begin ( ) , found ) ;
}
void OptionEntryLanguageCode : : SetActiveListIndex ( size_t index )
{
strcpy ( szCode , languages [ index ] . first . c_str ( ) ) ;
NotifyValueChanged ( ) ;
}
LanguageOptions : : LanguageOptions ( )
: OptionCategoryBase ( " Language " , N_ ( " Language " ) , N_ ( " Language Settings " ) )
{
code . SetValueChangedCallback ( OptionLanguageCodeChanged ) ;
}
std : : vector < OptionEntryBase * > LanguageOptions : : GetEntries ( )
{
return {
& code ,
} ;
}
} // namespace devilution